Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
23 changes: 13 additions & 10 deletions src/engines.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@
//
// `agentsView` (optional) is the exact argv that opens the engine's native
// agent list/view — used by `/agents <name>` 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";
Expand All @@ -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
Expand Down Expand Up @@ -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) }));
}

Expand Down
16 changes: 12 additions & 4 deletions test/engines.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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"],
Expand Down Expand Up @@ -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-");
Expand Down
Loading