fix: harden CLI executors and remove fallback API keys

This commit is contained in:
artale 2026-06-15 01:45:14 +02:00
parent ec49645e25
commit 32338932de
8 changed files with 59 additions and 53 deletions

22
src/core/cli-runner.ts Normal file
View File

@ -0,0 +1,22 @@
import { spawn } from "node:child_process";
export function runCli(bin: string, args: string[], timeoutMs = 120_000): Promise<string> {
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"}]`);
});
});
}

View File

@ -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<void> {
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");

View File

@ -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<string> {
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 {

View File

@ -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<string> {
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 {

View File

@ -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<string> {
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 {

View File

@ -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<string> {
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 {

View File

@ -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<string> {
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 {

View File

@ -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 <query>")
.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();