+1
-1
| { | ||
| "name": "moshcode", | ||
| "version": "0.63.0", | ||
| "version": "0.64.0", | ||
| "type": "module", | ||
@@ -5,0 +5,0 @@ "description": "moshcode — a metal wrapper for coding engines and native UGig/CoinPay workflow CLIs, with OpenPRD and moshscript", |
+26
-4
@@ -408,11 +408,33 @@ // Agentic-coding engines moshcode can install + wrap. `moshcode install <name>` | ||
| * upgrade to run engine installers/updaters. | ||
| * | ||
| * With `{ capture: true }` the child's stdout/stderr are piped and *echoed | ||
| * through* rather than inherited, and the combined text comes back as `output`. | ||
| * The terminal still sees exactly what it saw before — the tee exists so a | ||
| * caller can read the engine's own words about *why* it exited non-zero, which | ||
| * a bare exit code cannot tell apart (see `alreadyRegistered` in mcp.mjs). | ||
| * Inherit stays the default: piping costs a couple of streams, and every other | ||
| * caller runs installers whose output nobody needs to parse. | ||
| * | ||
| * stdin is inherited either way, so a child that prompts still reaches the user. | ||
| */ | ||
| export function runCmd(cmd, args = []) { | ||
| export function runCmd(cmd, args = [], { capture = false } = {}) { | ||
| return new Promise((resolve) => { | ||
| let child; | ||
| const spec = spawnSpec(cmd, args); | ||
| try { child = spawn(spec.cmd, spec.args, { stdio: "inherit" }); } | ||
| const stdio = capture ? ["inherit", "pipe", "pipe"] : "inherit"; | ||
| try { child = spawn(spec.cmd, spec.args, { stdio }); } | ||
| catch (e) { resolve({ ok: false, error: e }); return; } | ||
| child.on("error", (e) => resolve({ ok: false, error: e })); | ||
| child.on("exit", (code, signal) => resolve({ ok: true, code, signal })); | ||
| let output = ""; | ||
| if (capture) { | ||
| for (const [stream, sink] of [[child.stdout, process.stdout], [child.stderr, process.stderr]]) { | ||
| stream?.on("data", (chunk) => { output += chunk.toString(); sink.write(chunk); }); | ||
| } | ||
| } | ||
| child.on("error", (e) => resolve({ ok: false, error: e, output })); | ||
| // "exit" fires as soon as the process is gone, which with pipes can leave | ||
| // the last chunk still queued — the one line we are trying to read. "close" | ||
| // waits for the streams too. With stdio inherited there are no streams, so | ||
| // the two are the same moment and existing callers are unaffected; the | ||
| // distinction is kept explicit so neither branch changes by accident. | ||
| child.on(capture ? "close" : "exit", (code, signal) => resolve({ ok: true, code, signal, output })); | ||
| }); | ||
@@ -419,0 +441,0 @@ } |
+19
-0
@@ -108,2 +108,18 @@ // `/mcp` and `/skill` command flows, shared by the TUI and the CLI. Each parses | ||
| // A remote server is a URL and nothing else — every engine's builder pushes | ||
| // the target alone and discards `args`. So a leftover token here is not a | ||
| // command line, it is something the user typed that this command will silently | ||
| // throw away. `mcp install <url> --dry-run` is the case that matters: the flag | ||
| // does not exist, it lands here, and the install goes ahead and writes to | ||
| // every engine's config — the exact opposite of what the person typing it | ||
| // expected. Say so instead of dropping it on the floor. | ||
| if (!cmdParts && target && isRemoteTarget(target) && args.length) { | ||
| const extra = args[0]; | ||
| return { | ||
| error: extra.startsWith("-") | ||
| ? `unknown mcp flag "${extra}" — mcp takes --name, -t/--transport, -e/--env, and -H/--header, and has no --dry-run` | ||
| : `unexpected argument "${extra}" after a remote server URL — a URL server takes no command arguments`, | ||
| }; | ||
| } | ||
| if (verb === "install" && !name) { | ||
@@ -189,2 +205,5 @@ if (target && isRemoteTarget(target)) name = deriveName(target); | ||
| if (r.status === "added" || r.status === "installed" || r.status === "removed") console.log(line(r.key, ok(r.status))); | ||
| // Nothing to do and nothing wrong: grey, like the other "we didn't act" | ||
| // rows, rather than the green of a change we actually made. | ||
| else if (r.status === "already") console.log(line(r.key, ash("already registered"))); | ||
| else if (r.status === "failed") console.log(line(r.key, err(`failed${r.code != null ? ` (code ${r.code})` : r.signal ? ` (${r.signal})` : ""}`))); | ||
@@ -191,0 +210,0 @@ else if (r.status === "not-installed") console.log(line(r.key, ash("not installed — /install " + r.key))); |
+30
-5
@@ -8,3 +8,3 @@ // Register MCP (Model Context Protocol) servers across every engine that | ||
| // Coding engines that can register MCP servers. Aider has no MCP support. | ||
| export const MCP_ENGINES = ["claude", "gemini", "codex", "opencode", "privacycode"]; | ||
| export const MCP_ENGINES = ["claude", "gemini", "qwen", "codex", "opencode", "privacycode"]; | ||
@@ -75,3 +75,7 @@ /** Is this target a remote server URL (vs a local stdio command)? */ | ||
| } | ||
| case "gemini": { | ||
| // Qwen Code is a Gemini CLI fork and kept the whole `mcp add` surface — | ||
| // same `-s/-t/-e/-H` flags, same "URL or command" positional. It shares the | ||
| // builder rather than getting a copy, so the two can only drift on purpose. | ||
| case "gemini": | ||
| case "qwen": { | ||
| const argv = ["mcp", "add", "-s", "user"]; | ||
@@ -144,4 +148,22 @@ if (remote) argv.push("-t", transport); | ||
| /** | ||
| * Did this engine exit non-zero only because the server was already there? | ||
| * | ||
| * Registering the same server twice is the normal way to re-run `mcp install`, | ||
| * and it is not a failure — but Claude Code and Gemini/Qwen exit 1 on it, so the | ||
| * fan-out summary painted `claude ✗ failed (code 1)` next to opencode's cheerful | ||
| * green box. Read from a box where four engines already had the server, that | ||
| * says "moshcode cannot register with Claude Code" — which is exactly the wrong | ||
| * conclusion, and the reason this function exists rather than a nicer exit code. | ||
| * | ||
| * Matched against the engine's own words, so it stays honest: an engine that | ||
| * fails for any *other* reason still comes back failed. | ||
| */ | ||
| const ALREADY_RE = /already (?:exists|configured|registered|added)|exists in (?:user|global|project) config/i; | ||
| export function alreadyRegistered(r) { | ||
| return ALREADY_RE.test(String(r?.output ?? "")); | ||
| } | ||
| /** | ||
| * Execute a plan: run each installed, non-skipped engine's `mcp add`. Returns | ||
| * results [{ key, status: "added"|"skipped"|"failed"|"not-installed", reason? }]. | ||
| * results [{ key, status: "added"|"already"|"skipped"|"failed"|"not-installed", reason? }]. | ||
| * `run` is injectable for tests; defaults to the real spawner. | ||
@@ -154,6 +176,9 @@ */ | ||
| if (!item.installed) { results.push({ key: item.key, status: "not-installed" }); continue; } | ||
| const r = await run(item.bin, item.argv); | ||
| results.push({ key: item.key, status: ranOk(r) ? "added" : "failed", code: r.code, signal: r.signal ?? null }); | ||
| // capture so a non-zero exit can be read for "already exists" rather than | ||
| // reported as a failure; the child's output still reaches the terminal. | ||
| const r = await run(item.bin, item.argv, { capture: true }); | ||
| const status = ranOk(r) ? "added" : alreadyRegistered(r) ? "already" : "failed"; | ||
| results.push({ key: item.key, status, code: r.code, signal: r.signal ?? null }); | ||
| } | ||
| return results; | ||
| } |
Sorry, the diff of this file is too big to display
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
Found 2 instances
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
Found 2 instances
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
1768614
0.24%31573
0.2%1512
0.27%