diff --git a/README.md b/README.md index 52b5605..737b593 100644 --- a/README.md +++ b/README.md @@ -76,8 +76,8 @@ use this only in an isolated container, VM, or workspace you trust: ```sh moshcode agents claude # claude agents --dangerously-skip-permissions (agent view) -moshcode agents opencode # opencode agent list (agent view) -moshcode agents privacycode # privacycode agent list (agent view) +moshcode agents opencode # opencode --auto (autonomous) +moshcode agents privacycode # privacycode --auto (autonomous) moshcode agents codex # codex --dangerously-bypass-approvals-and-sandbox (autonomous) moshcode agents gemini # gemini --approval-mode=yolo (autonomous) moshcode agents kimi # kimi --yolo (autonomous) diff --git a/src/engines.mjs b/src/engines.mjs index b8ae53c..437e30e 100644 --- a/src/engines.mjs +++ b/src/engines.mjs @@ -5,11 +5,11 @@ // // `agentsView` (optional) is the exact argv that opens the engine's native // agent list/view — used by `/agents ` when the engine actually has one -// (claude, opencode). It's the FULL leading args (subcommand + any flags that -// subcommand accepts), because not every agents-subcommand takes the engine's -// bypass flag (e.g. `opencode agent list` takes none). Engines without an -// `agentsView` fall back to `agentArgs` — an autonomous session with native -// approvals bypassed/auto-approved. +// (currently claude). It's the FULL leading args (subcommand + any flags that +// subcommand accepts). Engines without an `agentsView` fall back to +// `agentArgs` — an autonomous session with native approvals +// bypassed/auto-approved. Do not use a machine-readable, one-shot list command +// as an agents view: `/agents` promises to hand the terminal to a live session. import { spawn } from "node:child_process"; import { existsSync, mkdtempSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs"; import { homedir, tmpdir } from "node:os"; @@ -22,17 +22,19 @@ export const ENGINES = { desc: "opencode — the open-source coding agent (SST/anomalyco)", bin: "opencode", agentArgs: ["--auto"], - agentsView: ["agent", "list"], // `opencode agent list` — lists agents; the `agent` subcommand takes no bypass flag install: { cmd: "bash", args: ["-c", "curl -fsSL https://opencode.ai/install | bash"] }, upgrade: { cmd: "opencode", args: ["upgrade"] }, + // The installer appends this directory to a shell profile. The moshcode + // process that ran it cannot see that PATH change, so search it directly. + binDirs: [path.join(homedir(), ".opencode", "bin")], }, privacycode: { desc: "privacycode — privacy-first coding agent (profullstack)", bin: "privacycode", // An opencode derivative, so it speaks the same flags/subcommands. agentArgs: ["--auto"], - agentsView: ["agent", "list"], install: { cmd: "sh", args: ["-c", "curl -fsSL https://getprivacycode.com/install | sh"] }, + binDirs: [path.join(homedir(), ".privacycode", "bin")], // Deliberately no native updater. `privacycode upgrade` is opencode's, and // it works out how to update itself by recognising where it was installed — // it knows opencode's own locations, not this fork's ~/.privacycode/bin. It @@ -263,9 +265,10 @@ export function pickAiEngine(preferred) { /** Engine entries annotated with install status. */ export function engineStatus() { - // Search each engine's own install dir as well as PATH — kimi's installer only - // adds ~/.kimi-code/bin to your shell rc, so PATH alone reports it missing in - // the very session that installed it. (Inert for engines without binDirs.) + // Search each engine's own install dir as well as PATH — several curl-based + // installers only add their bin directory to a shell rc, so PATH alone + // reports them missing in the very session that installed them. (Inert for + // engines without binDirs.) return Object.entries(ENGINES).map(([key, e]) => ({ key, ...e, installed: isInstalled(e.bin, e.binDirs) })); } diff --git a/test/engines.test.mjs b/test/engines.test.mjs index b963d73..ff300b1 100644 --- a/test/engines.test.mjs +++ b/test/engines.test.mjs @@ -8,7 +8,7 @@ import { readFileSync, writeFileSync, } from "node:fs"; -import { tmpdir } from "node:os"; +import { homedir, tmpdir } from "node:os"; import path from "node:path"; import { fileURLToPath } from "node:url"; import { spawn } from "node:child_process"; @@ -31,10 +31,12 @@ const EXPECTED_AGENT_ARGS = { }; // What an agent-mode launch actually runs (agentLaunchArgs): the engine's native -// agents-view invocation where it has one, else its autonomous bypass flags. +// interactive agents view where it has one, else its autonomous bypass flags. +// `opencode agent list` and `privacycode agent list` print data and exit, so +// those are not interactive views and must not be used here. const EXPECTED_LAUNCH_ARGS = { - opencode: ["agent", "list"], - privacycode: ["agent", "list"], + opencode: ["--auto"], + privacycode: ["--auto"], claude: ["agents", "--dangerously-skip-permissions"], codex: ["--dangerously-bypass-approvals-and-sandbox"], gemini: ["--approval-mode=yolo"], @@ -219,6 +221,12 @@ test("executable lookup searches a tool's own install dir when PATH misses it", assert.equal(r.code, 0); }); +test("curl-installed engines declare their installer bin directories", () => { + assert.deepEqual(ENGINES.opencode.binDirs, [path.join(homedir(), ".opencode", "bin")]); + assert.deepEqual(ENGINES.privacycode.binDirs, [path.join(homedir(), ".privacycode", "bin")]); + assert.deepEqual(ENGINES.kimi.binDirs, [path.join(homedir(), ".kimi-code", "bin")]); +}); + test("PATH still wins over a tool's install dir", async () => { // Two copies, different exit codes: whichever one runs identifies itself. const pathDir = tempDir("moshcode-bindirs-path-");