diff --git a/.env.example b/.env.example index 3672919..91a42f2 100644 --- a/.env.example +++ b/.env.example @@ -18,6 +18,21 @@ OPENAI_API_KEY= PI_DEFAULT_PROVIDER= PI_DEFAULT_MODEL= +# Claude Code — model override for CCExecutor +CLAUDE_CODE_MODEL= + +# OpenCode — proxy and model for OpenCodeExecutor +OPENCODE_PROXY=http://localhost:18901/v1 +OPENCODE_MODEL= + +# Grok CLI — model for GrokExecutor +GROK_MODEL= +GROK_PROXY=http://localhost:18901/v1 + +# Antigrav (agy) — binary path and model for AntigravExecutor +AGY_BIN=C:\Users\Artale\AppData\Local\agy\bin\agy.exe +AGY_MODEL=gemini-2.5-pro + # ── PAI Integration ────────────────────────────────────── # Path to cloned PAI repository PAI_DIRECTORY= diff --git a/src/examples/executors/antigrav-executor.ts b/src/examples/executors/antigrav-executor.ts new file mode 100644 index 0000000..69b3da0 --- /dev/null +++ b/src/examples/executors/antigrav-executor.ts @@ -0,0 +1,105 @@ +/** + * Antigrav Executor — PhaseExecutor for Antigravity CLI (agy). + * + * Antigrav is a general-purpose agentic CLI by Google. + * Supports sandboxed execution, plugins, and multi-model routing. + * + * Integration: `agy --print ""` for non-interactive execution. + * + * Binary: C:\Users\Artale\AppData\Local\agy\bin\agy.exe (install via irm) + * Docs: https://antigravity.google/cli + * + * Usage: + * const ag = new AntigravExecutor({ model: "gemini-2.5-pro" }); + * const result = await feedbackLoop.run(task, ag); + */ + +import { exec } from "node:child_process"; +import * as path from "node:path"; +import * as fs from "node:fs"; +import type { LoopIteration } from "../../core/types.js"; +import type { PhaseExecutor } from "../../tier2-primitives/loops/feedback-loop.js"; + +function findAgyBin(): string { + const candidates = [ + path.join(process.env.LOCALAPPDATA || "", "agy", "bin", "agy.exe"), + path.join(process.env.HOME || "", "AppData", "Local", "agy", "bin", "agy.exe"), + "agy", + ]; + for (const c of candidates) { + try { if (fs.existsSync(c)) return c; } catch {} + } + return "agy"; +} + +export interface AntigravConfig { + /** agy binary path */ + bin?: string; + /** Model name */ + model?: string; + /** Use sandbox mode */ + sandbox?: boolean; + /** Timeout per phase in ms */ + timeoutMs?: number; +} + +export class AntigravExecutor implements PhaseExecutor { + private config: AntigravConfig; + + constructor(config?: AntigravConfig) { + this.config = { + bin: process.env.AGY_BIN || findAgyBin(), + model: process.env.AGY_MODEL || "gemini-2.5-pro", + sandbox: false, + timeoutMs: 120000, + ...config, + }; + } + + async plan(task: string, previousIteration: LoopIteration | null): Promise { + const prev = previousIteration ? `\nPrevious: ${previousIteration.reflection?.slice(0, 200)}` : ""; + return this.call(`You are in the PLAN phase.\nTask: ${task}${prev}`); + } + + async execute(plan: string): Promise { + return this.call(`EXECUTE:\n${plan}`); + } + + async observe(executed: string): Promise { + return this.call(`OBSERVE:\n${executed}`); + } + + async reflect(observation: string, previousIteration: LoopIteration | null): Promise { + const prev = previousIteration ? `\nExpected: ${previousIteration.refinement?.slice(0, 200)}` : ""; + return this.call(`REFLECT:\n${observation}${prev}`); + } + + async refine(reflection: string): Promise { + return this.call(`REFINE:\n${reflection}`); + } + + private async call(prompt: string): Promise { + const modelFlag = `--model "${this.config.model}"`; + const sandboxFlag = this.config.sandbox ? "--sandbox" : ""; + const cmd = `"${this.config.bin}" --print ${modelFlag} ${sandboxFlag} "${prompt.replace(/"/g, '\\"')}" 2>/dev/null`; + + return new Promise((resolve) => { + exec(cmd, { timeout: this.config.timeoutMs, maxBuffer: 10 * 1024 * 1024 }, (err, stdout) => { + resolve(stdout.trim() || `[Antigrav error: ${err?.message?.slice(0, 200) ?? "unknown"}]`); + }); + }); + } + + static describeConfig(): string { + const bin = process.env.AGY_BIN || findAgyBin(); + return [ + "Antigrav (agy) Executor", + ` Binary: ${bin}`, + ` Model: ${process.env.AGY_MODEL || "gemini-2.5-pro (set AGY_MODEL)"}`, + " Mode: --print (non-interactive)", + "", + "Install: irm https://antigravity.google/cli/install.ps1 | iex", + "Docs: https://antigravity.google/cli", + ].join("\n"); + } +} diff --git a/src/examples/executors/cc-executor.ts b/src/examples/executors/cc-executor.ts new file mode 100644 index 0000000..d518958 --- /dev/null +++ b/src/examples/executors/cc-executor.ts @@ -0,0 +1,87 @@ +/** + * Claude Code Executor — PhaseExecutor using the `claude` CLI. + * + * Claude Code is Anthropic's official agentic coding tool. + * Ships with /goal, Outcomes, Dynamic Workflows, and vision. + * + * Integration: shells out to `claude -p ""` for non-interactive execution. + * + * Usage: + * const cc = new ClaudeCodeExecutor(); + * const result = await feedbackLoop.run(task, cc); + */ + +import { exec } from "node:child_process"; +import type { LoopIteration } from "../../core/types.js"; +import type { PhaseExecutor } from "../../tier2-primitives/loops/feedback-loop.js"; + +export interface CCConfig { + /** Claude Code binary path */ + bin?: string; + /** Model override (e.g. "claude-sonnet-4-6") */ + model?: string; + /** Max turns per phase */ + maxTurns?: number; + /** Timeout per phase in ms */ + timeoutMs?: number; +} + +export class ClaudeCodeExecutor implements PhaseExecutor { + private config: CCConfig; + + constructor(config?: CCConfig) { + this.config = { + bin: "claude", + model: process.env.CLAUDE_CODE_MODEL, + maxTurns: 5, + timeoutMs: 120000, + ...config, + }; + } + + async plan(task: string, previousIteration: LoopIteration | null): Promise { + const prev = previousIteration + ? `\nPrevious reflection: ${previousIteration.reflection}\nPrevious refinement: ${previousIteration.refinement}` + : ""; + return this.callClaude(`You are in the PLAN phase.\nTask: ${task}${prev}\n\nProduce a specific, actionable plan with concrete steps.`); + } + + async execute(plan: string): Promise { + return this.callClaude(`You are in the EXECUTE phase.\nPlan:\n${plan}\n\nCarry it out step by step.`); + } + + async observe(executed: string): Promise { + return this.callClaude(`You are in the OBSERVE phase.\nOutput:\n${executed}\n\nExtract specific findings.`); + } + + async reflect(observation: string, previousIteration: LoopIteration | null): Promise { + const prev = previousIteration ? `\nExpected: ${previousIteration.refinement}` : ""; + return this.callClaude(`You are in the REFLECT phase.\nObservations:\n${observation}${prev}\n\nCompare to expectations. What improved, regressed, or is missing?`); + } + + async refine(reflection: string): Promise { + return this.callClaude(`You are in the REFINE phase.\nReflection:\n${reflection}\n\nProduce a refined approach.`); + } + + private async callClaude(prompt: string): Promise { + const modelFlag = this.config.model ? `-m "${this.config.model}"` : ""; + const cmd = `"${this.config.bin}" -p "${prompt.replace(/"/g, '\\"')}" ${modelFlag} 2>/dev/null`; + + return new Promise((resolve) => { + exec(cmd, { timeout: this.config.timeoutMs, maxBuffer: 10 * 1024 * 1024 }, (err, stdout) => { + resolve(stdout.trim() || `[Claude Code error: ${err?.message?.slice(0, 200) ?? "unknown"}]`); + }); + }); + } + + static describeConfig(): string { + return [ + "Claude Code Executor", + ` Binary: claude`, + ` Model: ${process.env.CLAUDE_CODE_MODEL ?? "default (set CLAUDE_CODE_MODEL)"}`, + " Mode: CLI -p (non-interactive)", + "", + "Requires Claude Code CLI: https://docs.anthropic.com/docs/claude-code", + ].join("\n"); + } +} diff --git a/src/examples/executors/codex-executor.ts b/src/examples/executors/codex-executor.ts new file mode 100644 index 0000000..c5c7a03 --- /dev/null +++ b/src/examples/executors/codex-executor.ts @@ -0,0 +1,85 @@ +/** + * Codex Executor — PhaseExecutor using OpenAI Codex CLI. + * + * Codex is OpenAI's agentic coding tool (ChatGPT Plus/Pro). + * Also accessible through Pi: `pi --provider codex --model codex` + * + * Integration: `codex ""` or through Pi's Codex provider. + * + * Usage: + * const codex = new CodexExecutor(); + * const result = await feedbackLoop.run(task, codex); + * + * // Via Pi's Codex provider: + * const viaPi = new CodexExecutor({ usePi: true }); + */ + +import { exec } from "node:child_process"; +import type { LoopIteration } from "../../core/types.js"; +import type { PhaseExecutor } from "../../tier2-primitives/loops/feedback-loop.js"; + +export interface CodexConfig { + /** Codex binary path */ + bin?: string; + /** Pipe through Pi's Codex provider instead of direct Codex CLI */ + usePi?: boolean; + /** Timeout per phase in ms */ + timeoutMs?: number; +} + +export class CodexExecutor implements PhaseExecutor { + private config: CodexConfig; + + constructor(config?: CodexConfig) { + this.config = { + bin: "codex", + usePi: false, + timeoutMs: 120000, + ...config, + }; + } + + async plan(task: string, previousIteration: LoopIteration | null): Promise { + const prev = previousIteration ? `\nPrevious: ${previousIteration.reflection?.slice(0, 200)}` : ""; + return this.call(`PLAN: ${task}${prev}`); + } + + async execute(plan: string): Promise { + return this.call(`EXECUTE:\n${plan}`); + } + + async observe(executed: string): Promise { + return this.call(`OBSERVE:\n${executed}`); + } + + async reflect(observation: string, previousIteration: LoopIteration | null): Promise { + const prev = previousIteration ? `\nExpected: ${previousIteration.refinement?.slice(0, 200)}` : ""; + return this.call(`REFLECT:\n${observation}${prev}`); + } + + async refine(reflection: string): Promise { + return this.call(`REFINE:\n${reflection}`); + } + + private async call(prompt: string): Promise { + const cmd = this.config.usePi + ? `pi --provider codex --model codex --print --no-session "${prompt.replace(/"/g, '\\"')}" 2>/dev/null` + : `"${this.config.bin}" "${prompt.replace(/"/g, '\\"')}" 2>/dev/null`; + + return new Promise((resolve) => { + exec(cmd, { timeout: this.config.timeoutMs, maxBuffer: 10 * 1024 * 1024 }, (err, stdout) => { + resolve(stdout.trim() || `[Codex error: ${err?.message?.slice(0, 200) ?? "unknown"}]`); + }); + }); + } + + static describeConfig(): string { + return [ + "Codex Executor", + " Direct: codex (requires Codex CLI)", + " Via Pi: pi --provider codex (requires ChatGPT Plus/Pro subscription)", + "", + "Codex requires ChatGPT Plus/Pro: https://codex.ai", + ].join("\n"); + } +} diff --git a/src/examples/executors/grok-executor.ts b/src/examples/executors/grok-executor.ts new file mode 100644 index 0000000..50446ed --- /dev/null +++ b/src/examples/executors/grok-executor.ts @@ -0,0 +1,100 @@ +/** + * Grok CLI Executor — PhaseExecutor using the `grok` CLI. + * + * Grok CLI is xAI's agentic coding tool (grok.exe at ~/.grok/bin/grok.exe). + * Routes through local proxy at :18901 (configured in ~/.grok/config.toml). + * + * Integration: shells out to `grok -m -p ""`. + * + * Usage: + * const grok = new GrokExecutor({ model: "grok-3" }); + * const result = await feedbackLoop.run(task, grok); + */ + +import { exec } from "node:child_process"; +import * as path from "node:path"; +import * as fs from "node:fs"; +import type { LoopIteration } from "../../core/types.js"; +import type { PhaseExecutor } from "../../tier2-primitives/loops/feedback-loop.js"; + +function findGrokBin(): string { + const candidates = [ + path.join(process.env.HOME || "", ".grok", "bin", "grok.exe"), + path.join(process.env.USERPROFILE || "", ".grok", "bin", "grok.exe"), + "grok", + ]; + for (const c of candidates) { + try { if (fs.existsSync(c)) return c; } catch {} + } + return "grok"; +} + +export interface GrokConfig { + /** Grok CLI binary path */ + bin?: string; + /** Model name */ + model?: string; + /** Proxy URL */ + proxyUrl?: string; + /** Timeout per phase in ms */ + timeoutMs?: number; +} + +export class GrokExecutor implements PhaseExecutor { + private config: GrokConfig; + + constructor(config?: GrokConfig) { + this.config = { + bin: findGrokBin(), + model: process.env.GROK_MODEL ?? "grok-3", + proxyUrl: process.env.GROK_PROXY ?? "http://localhost:18901/v1", + timeoutMs: 120000, + ...config, + }; + } + + async plan(task: string, previousIteration: LoopIteration | null): Promise { + const prev = previousIteration ? `\nPrevious: ${previousIteration.reflection?.slice(0, 200)}` : ""; + return this.callGrok(`You are in the PLAN phase.\nTask: ${task}${prev}`); + } + + async execute(plan: string): Promise { + return this.callGrok(`EXECUTE:\n${plan}`); + } + + async observe(executed: string): Promise { + return this.callGrok(`OBSERVE:\n${executed}`); + } + + async reflect(observation: string, previousIteration: LoopIteration | null): Promise { + const prev = previousIteration ? `\nExpected: ${previousIteration.refinement?.slice(0, 200)}` : ""; + return this.callGrok(`REFLECT:\n${observation}${prev}`); + } + + async refine(reflection: string): Promise { + return this.callGrok(`REFINE:\n${reflection}`); + } + + private async callGrok(prompt: string): Promise { + const modelFlag = `-m "${this.config.model}"`; + const cmd = `"${this.config.bin}" ${modelFlag} -p "${prompt.replace(/"/g, '\\"')}" --output-format text 2>/dev/null`; + + return new Promise((resolve) => { + exec(cmd, { timeout: this.config.timeoutMs, maxBuffer: 10 * 1024 * 1024 }, (err, stdout) => { + resolve(stdout.trim() || `[Grok error: ${err?.message?.slice(0, 200) ?? "unknown"}]`); + }); + }); + } + + static describeConfig(): string { + return [ + "Grok CLI Executor", + ` Binary: ${findGrokBin()}`, + ` Model: ${process.env.GROK_MODEL ?? "grok-3 (set GROK_MODEL)"}`, + ` Proxy: ${process.env.GROK_PROXY ?? "http://localhost:18901/v1"}`, + " Mode: CLI -p (non-interactive)", + "", + "See ~/.grok/config.toml for proxy/model configuration.", + ].join("\n"); + } +} diff --git a/src/examples/executors/opencode-executor.ts b/src/examples/executors/opencode-executor.ts new file mode 100644 index 0000000..0f1a9b5 --- /dev/null +++ b/src/examples/executors/opencode-executor.ts @@ -0,0 +1,86 @@ +/** + * OpenCode Executor — PhaseExecutor using the `opencode` CLI. + * + * OpenCode is the user's primary coding agent (from AGENTS.md). + * It's an open-source agent that routes through a local proxy at :18901. + * + * Integration: shells out to `opencode -p ""` or pipes via stdin. + * + * Usage: + * const oc = new OpenCodeExecutor(); + * const result = await feedbackLoop.run(task, oc); + */ + +import { exec } from "node:child_process"; +import type { LoopIteration } from "../../core/types.js"; +import type { PhaseExecutor } from "../../tier2-primitives/loops/feedback-loop.js"; + +export interface OpenCodeConfig { + /** OpenCode binary path */ + bin?: string; + /** Model override */ + model?: string; + /** Proxy URL (default: http://localhost:18901/v1) */ + proxyUrl?: string; + /** Timeout per phase in ms */ + timeoutMs?: number; +} + +export class OpenCodeExecutor implements PhaseExecutor { + private config: OpenCodeConfig; + + constructor(config?: OpenCodeConfig) { + this.config = { + bin: "opencode", + proxyUrl: "http://localhost:18901/v1", + timeoutMs: 120000, + ...config, + }; + } + + async plan(task: string, previousIteration: LoopIteration | null): Promise { + const prev = previousIteration ? `\nPrevious: ${previousIteration.reflection?.slice(0, 200)}` : ""; + return this.callOpenCode(`PLAN: ${task}${prev}`); + } + + async execute(plan: string): Promise { + return this.callOpenCode(`EXECUTE:\n${plan}`); + } + + async observe(executed: string): Promise { + return this.callOpenCode(`OBSERVE:\n${executed}`); + } + + async reflect(observation: string, previousIteration: LoopIteration | null): Promise { + const prev = previousIteration ? `\nExpected: ${previousIteration.refinement?.slice(0, 200)}` : ""; + return this.callOpenCode(`REFLECT:\n${observation}${prev}`); + } + + async refine(reflection: string): Promise { + return this.callOpenCode(`REFINE:\n${reflection}`); + } + + private async callOpenCode(prompt: string): Promise { + const modelFlag = this.config.model ? `-m "${this.config.model}"` : ""; + const proxyFlag = `--proxy "${this.config.proxyUrl}"`; + const cmd = `"${this.config.bin}" -p "${prompt.replace(/"/g, '\\"')}" ${modelFlag} ${proxyFlag} 2>/dev/null`; + + return new Promise((resolve) => { + exec(cmd, { timeout: this.config.timeoutMs, maxBuffer: 10 * 1024 * 1024 }, (err, stdout) => { + resolve(stdout.trim() || `[OpenCode error: ${err?.message?.slice(0, 200) ?? "unknown"}]`); + }); + }); + } + + static describeConfig(): string { + return [ + "OpenCode Executor", + ` Binary: opencode`, + ` Proxy: ${process.env.OPENCODE_PROXY ?? "http://localhost:18901/v1"}`, + ` Model: ${process.env.OPENCODE_MODEL ?? "default (set OPENCODE_MODEL)"}`, + " Mode: CLI -p (non-interactive)", + "", + "See ~/.grok/AGENTS.md for proxy configuration.", + ].join("\n"); + } +}