diff --git a/src/core/cli-runner.ts b/src/core/cli-runner.ts new file mode 100644 index 0000000..67f9839 --- /dev/null +++ b/src/core/cli-runner.ts @@ -0,0 +1,22 @@ +import { spawn } from "node:child_process"; + +export function runCli(bin: string, args: string[], timeoutMs = 120_000): Promise { + return new Promise((resolve) => { + const child = spawn(bin, args, { + shell: false, + timeout: timeoutMs, + windowsHide: true, + }); + + let stdout = ""; + let stderr = ""; + child.stdout.on("data", (d) => { stdout += d.toString(); }); + child.stderr.on("data", (d) => { stderr += d.toString(); }); + child.on("error", (err) => resolve(`[CLI error: ${err.message.slice(0, 200)}]`)); + child.on("close", (code) => { + const out = stdout.trim(); + if (out) resolve(out); + else resolve(`[CLI exited ${code ?? "unknown"}: ${stderr.slice(0, 200) || "no output"}]`); + }); + }); +} diff --git a/src/examples/demo-compound-cycle.ts b/src/examples/demo-compound-cycle.ts index 610a960..893f92d 100644 --- a/src/examples/demo-compound-cycle.ts +++ b/src/examples/demo-compound-cycle.ts @@ -13,7 +13,7 @@ import { EnhancedMetaAgent } from "../upgrades/enhanced-meta-agent.js"; import { ExaSearch } from "../upgrades/exa-search.js"; import { SkillSync } from "../upgrades/skill-sync.js"; -const API_KEY = process.env.EXA_API_KEY || "622b4eb6-7d6a-4647-ad3a-872bac6586a7"; +const API_KEY = process.env.EXA_API_KEY; function log(msg: string): void { process.stdout.write(msg + "\n"); } function divider(label: string): void { @@ -21,8 +21,12 @@ function divider(label: string): void { log(" " + label); log("=".repeat(56)); } - async function main(): Promise { + if (!API_KEY) { + log("Set EXA_API_KEY to run this demo."); + process.exit(1); + } + // Clean state for demo try { const fs = await import("node:fs"); diff --git a/src/examples/executors/antigrav-executor.ts b/src/examples/executors/antigrav-executor.ts index 69b3da0..ae4eb1c 100644 --- a/src/examples/executors/antigrav-executor.ts +++ b/src/examples/executors/antigrav-executor.ts @@ -14,7 +14,7 @@ * const result = await feedbackLoop.run(task, ag); */ -import { exec } from "node:child_process"; +import { runCli } from "../../core/cli-runner.js"; import * as path from "node:path"; import * as fs from "node:fs"; import type { LoopIteration } from "../../core/types.js"; @@ -79,15 +79,10 @@ export class AntigravExecutor implements PhaseExecutor { } 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"}]`); - }); - }); + const args = ["--print", "--model", this.config.model ?? "gemini-2.5-pro"]; + if (this.config.sandbox) args.push("--sandbox"); + args.push(prompt); + return runCli(this.config.bin ?? "agy", args, this.config.timeoutMs); } static describeConfig(): string { diff --git a/src/examples/executors/cc-executor.ts b/src/examples/executors/cc-executor.ts index d518958..b856d90 100644 --- a/src/examples/executors/cc-executor.ts +++ b/src/examples/executors/cc-executor.ts @@ -11,7 +11,7 @@ * const result = await feedbackLoop.run(task, cc); */ -import { exec } from "node:child_process"; +import { runCli } from "../../core/cli-runner.js"; import type { LoopIteration } from "../../core/types.js"; import type { PhaseExecutor } from "../../tier2-primitives/loops/feedback-loop.js"; @@ -64,14 +64,9 @@ export class ClaudeCodeExecutor implements PhaseExecutor { } 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"}]`); - }); - }); + const args = ["-p", prompt]; + if (this.config.model) args.push("-m", this.config.model); + return runCli(this.config.bin ?? "claude", args, this.config.timeoutMs); } static describeConfig(): string { diff --git a/src/examples/executors/codex-executor.ts b/src/examples/executors/codex-executor.ts index c5c7a03..16cf2f9 100644 --- a/src/examples/executors/codex-executor.ts +++ b/src/examples/executors/codex-executor.ts @@ -14,7 +14,7 @@ * const viaPi = new CodexExecutor({ usePi: true }); */ -import { exec } from "node:child_process"; +import { runCli } from "../../core/cli-runner.js"; import type { LoopIteration } from "../../core/types.js"; import type { PhaseExecutor } from "../../tier2-primitives/loops/feedback-loop.js"; @@ -62,15 +62,9 @@ export class CodexExecutor implements PhaseExecutor { } 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"}]`); - }); - }); + return this.config.usePi + ? runCli("pi", ["--provider", "codex", "--model", "codex", "--print", "--no-session", prompt], this.config.timeoutMs) + : runCli(this.config.bin ?? "codex", [prompt], this.config.timeoutMs); } static describeConfig(): string { diff --git a/src/examples/executors/grok-executor.ts b/src/examples/executors/grok-executor.ts index 50446ed..ab29439 100644 --- a/src/examples/executors/grok-executor.ts +++ b/src/examples/executors/grok-executor.ts @@ -11,7 +11,7 @@ * const result = await feedbackLoop.run(task, grok); */ -import { exec } from "node:child_process"; +import { runCli } from "../../core/cli-runner.js"; import * as path from "node:path"; import * as fs from "node:fs"; import type { LoopIteration } from "../../core/types.js"; @@ -76,14 +76,7 @@ export class GrokExecutor implements PhaseExecutor { } 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"}]`); - }); - }); + return runCli(this.config.bin ?? "grok", ["-m", this.config.model ?? "grok-3", "-p", prompt, "--output-format", "text"], this.config.timeoutMs); } static describeConfig(): string { diff --git a/src/examples/executors/opencode-executor.ts b/src/examples/executors/opencode-executor.ts index 0f1a9b5..f4a90b5 100644 --- a/src/examples/executors/opencode-executor.ts +++ b/src/examples/executors/opencode-executor.ts @@ -11,7 +11,7 @@ * const result = await feedbackLoop.run(task, oc); */ -import { exec } from "node:child_process"; +import { runCli } from "../../core/cli-runner.js"; import type { LoopIteration } from "../../core/types.js"; import type { PhaseExecutor } from "../../tier2-primitives/loops/feedback-loop.js"; @@ -61,15 +61,9 @@ export class OpenCodeExecutor implements PhaseExecutor { } 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"}]`); - }); - }); + const args = ["-p", prompt, "--proxy", this.config.proxyUrl ?? "http://localhost:18901/v1"]; + if (this.config.model) args.push("-m", this.config.model); + return runCli(this.config.bin ?? "opencode", args, this.config.timeoutMs); } static describeConfig(): string { diff --git a/src/index.ts b/src/index.ts index 5e805ee..2470070 100644 --- a/src/index.ts +++ b/src/index.ts @@ -861,6 +861,15 @@ const exa = program .command("exa") .description("Exa AI web search and company research"); +function requireExaApiKey(): string { + const apiKey = process.env.EXA_API_KEY; + if (!apiKey) { + console.error(" ✗ EXA_API_KEY is required. Set it in your environment or .env file."); + process.exit(1); + } + return apiKey; +} + exa .command("search ") .description("Search the web via Exa AI") @@ -871,7 +880,7 @@ exa const { StateStore } = await import("./core/state-store.js"); const { ExaSearch } = await import("./upgrades/exa-search.js"); - const apiKey = process.env.EXA_API_KEY || "622b4eb6-7d6a-4647-ad3a-872bac6586a7"; + const apiKey = requireExaApiKey(); const exa = new ExaSearch(apiKey, new StateStore()); console.log(`\n Searching: ${query}\n`); @@ -891,7 +900,7 @@ exa .action(async (name: string) => { const { ExaSearch } = await import("./upgrades/exa-search.js"); - const apiKey = process.env.EXA_API_KEY || "622b4eb6-7d6a-4647-ad3a-872bac6586a7"; + const apiKey = requireExaApiKey(); const exa = new ExaSearch(apiKey); console.log(`\n Researching: ${name}\n`); @@ -936,7 +945,7 @@ program const { EnhancedMetaAgent } = await import("./upgrades/enhanced-meta-agent.js"); const { SkillSync } = await import("./upgrades/skill-sync.js"); - const apiKey = process.env.EXA_API_KEY || "622b4eb6-7d6a-4647-ad3a-872bac6586a7"; + const apiKey = requireExaApiKey(); const exa = new ExaSearch(apiKey); const agent = new EnhancedMetaAgent();