moshcode 0.63.0 → 0.64.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -4
- package/package.json +1 -1
- package/src/engines.mjs +26 -4
- package/src/integrations.mjs +19 -0
- package/src/mcp.mjs +30 -5
package/README.md
CHANGED
|
@@ -991,10 +991,14 @@ moshcode mcp catalog # what we know how to run
|
|
|
991
991
|
moshcode mcp add porkbun # expands to: npx -y @porkbunllc/mcp-server
|
|
992
992
|
```
|
|
993
993
|
|
|
994
|
-
That registers it across every engine that supports MCP (claude, gemini,
|
|
995
|
-
opencode, privacycode) in one go. Kimi is skipped with a reason: it runs
|
|
996
|
-
servers but has no command to register one from a script — add those
|
|
997
|
-
with its own `/mcp-config`, or in `~/.kimi-code/mcp.json`.
|
|
994
|
+
That registers it across every engine that supports MCP (claude, gemini, qwen,
|
|
995
|
+
codex, opencode, privacycode) in one go. Kimi is skipped with a reason: it runs
|
|
996
|
+
MCP servers but has no command to register one from a script — add those
|
|
997
|
+
in-session with its own `/mcp-config`, or in `~/.kimi-code/mcp.json`.
|
|
998
|
+
|
|
999
|
+
Re-running an install is safe: an engine that already has the server reports
|
|
1000
|
+
`already registered` rather than an error, so the summary only goes red when
|
|
1001
|
+
something actually went wrong.
|
|
998
1002
|
|
|
999
1003
|
The catalog is a convenience, never a gate — an explicit command always wins, so
|
|
1000
1004
|
`moshcode mcp add porkbun -- node ./my-fork.js` runs your fork.
|
package/package.json
CHANGED
package/src/engines.mjs
CHANGED
|
@@ -405,15 +405,37 @@ export function agentLaunchArgs(engine, args = []) {
|
|
|
405
405
|
* Spawn an arbitrary command with stdio inherited (so its own progress/prompts
|
|
406
406
|
* own the terminal). Resolves { ok, code, signal } on exit. Used by install +
|
|
407
407
|
* upgrade to run engine installers/updaters.
|
|
408
|
+
*
|
|
409
|
+
* With `{ capture: true }` the child's stdout/stderr are piped and *echoed
|
|
410
|
+
* through* rather than inherited, and the combined text comes back as `output`.
|
|
411
|
+
* The terminal still sees exactly what it saw before — the tee exists so a
|
|
412
|
+
* caller can read the engine's own words about *why* it exited non-zero, which
|
|
413
|
+
* a bare exit code cannot tell apart (see `alreadyRegistered` in mcp.mjs).
|
|
414
|
+
* Inherit stays the default: piping costs a couple of streams, and every other
|
|
415
|
+
* caller runs installers whose output nobody needs to parse.
|
|
416
|
+
*
|
|
417
|
+
* stdin is inherited either way, so a child that prompts still reaches the user.
|
|
408
418
|
*/
|
|
409
|
-
export function runCmd(cmd, args = []) {
|
|
419
|
+
export function runCmd(cmd, args = [], { capture = false } = {}) {
|
|
410
420
|
return new Promise((resolve) => {
|
|
411
421
|
let child;
|
|
412
422
|
const spec = spawnSpec(cmd, args);
|
|
413
|
-
|
|
423
|
+
const stdio = capture ? ["inherit", "pipe", "pipe"] : "inherit";
|
|
424
|
+
try { child = spawn(spec.cmd, spec.args, { stdio }); }
|
|
414
425
|
catch (e) { resolve({ ok: false, error: e }); return; }
|
|
415
|
-
|
|
416
|
-
|
|
426
|
+
let output = "";
|
|
427
|
+
if (capture) {
|
|
428
|
+
for (const [stream, sink] of [[child.stdout, process.stdout], [child.stderr, process.stderr]]) {
|
|
429
|
+
stream?.on("data", (chunk) => { output += chunk.toString(); sink.write(chunk); });
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
child.on("error", (e) => resolve({ ok: false, error: e, output }));
|
|
433
|
+
// "exit" fires as soon as the process is gone, which with pipes can leave
|
|
434
|
+
// the last chunk still queued — the one line we are trying to read. "close"
|
|
435
|
+
// waits for the streams too. With stdio inherited there are no streams, so
|
|
436
|
+
// the two are the same moment and existing callers are unaffected; the
|
|
437
|
+
// distinction is kept explicit so neither branch changes by accident.
|
|
438
|
+
child.on(capture ? "close" : "exit", (code, signal) => resolve({ ok: true, code, signal, output }));
|
|
417
439
|
});
|
|
418
440
|
}
|
|
419
441
|
|
package/src/integrations.mjs
CHANGED
|
@@ -105,6 +105,22 @@ export function parseMcp(tokens) {
|
|
|
105
105
|
};
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
+
// A remote server is a URL and nothing else — every engine's builder pushes
|
|
109
|
+
// the target alone and discards `args`. So a leftover token here is not a
|
|
110
|
+
// command line, it is something the user typed that this command will silently
|
|
111
|
+
// throw away. `mcp install <url> --dry-run` is the case that matters: the flag
|
|
112
|
+
// does not exist, it lands here, and the install goes ahead and writes to
|
|
113
|
+
// every engine's config — the exact opposite of what the person typing it
|
|
114
|
+
// expected. Say so instead of dropping it on the floor.
|
|
115
|
+
if (!cmdParts && target && isRemoteTarget(target) && args.length) {
|
|
116
|
+
const extra = args[0];
|
|
117
|
+
return {
|
|
118
|
+
error: extra.startsWith("-")
|
|
119
|
+
? `unknown mcp flag "${extra}" — mcp takes --name, -t/--transport, -e/--env, and -H/--header, and has no --dry-run`
|
|
120
|
+
: `unexpected argument "${extra}" after a remote server URL — a URL server takes no command arguments`,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
108
124
|
if (verb === "install" && !name) {
|
|
109
125
|
if (target && isRemoteTarget(target)) name = deriveName(target);
|
|
110
126
|
else return { error: "a stdio command server needs an explicit --name" };
|
|
@@ -185,6 +201,9 @@ export function printSkillTargets(json = false) {
|
|
|
185
201
|
function summarize(results) {
|
|
186
202
|
for (const r of results) {
|
|
187
203
|
if (r.status === "added" || r.status === "installed" || r.status === "removed") console.log(line(r.key, ok(r.status)));
|
|
204
|
+
// Nothing to do and nothing wrong: grey, like the other "we didn't act"
|
|
205
|
+
// rows, rather than the green of a change we actually made.
|
|
206
|
+
else if (r.status === "already") console.log(line(r.key, ash("already registered")));
|
|
188
207
|
else if (r.status === "failed") console.log(line(r.key, err(`failed${r.code != null ? ` (code ${r.code})` : r.signal ? ` (${r.signal})` : ""}`)));
|
|
189
208
|
else if (r.status === "not-installed") console.log(line(r.key, ash("not installed — /install " + r.key)));
|
|
190
209
|
else console.log(line(r.key, ash(`skipped — ${r.reason}`)));
|
package/src/mcp.mjs
CHANGED
|
@@ -5,7 +5,7 @@ import { ENGINES, isInstalled, ranOk, runCmd } from "./engines.mjs";
|
|
|
5
5
|
import { isIP } from "node:net";
|
|
6
6
|
|
|
7
7
|
// Coding engines that can register MCP servers. Aider has no MCP support.
|
|
8
|
-
export const MCP_ENGINES = ["claude", "gemini", "codex", "opencode", "privacycode"];
|
|
8
|
+
export const MCP_ENGINES = ["claude", "gemini", "qwen", "codex", "opencode", "privacycode"];
|
|
9
9
|
|
|
10
10
|
/** Is this target a remote server URL (vs a local stdio command)? */
|
|
11
11
|
export function isRemoteTarget(target) {
|
|
@@ -71,7 +71,11 @@ export function mcpAddArgs(key, spec) {
|
|
|
71
71
|
else argv.push("--", target, ...args);
|
|
72
72
|
return { argv };
|
|
73
73
|
}
|
|
74
|
-
|
|
74
|
+
// Qwen Code is a Gemini CLI fork and kept the whole `mcp add` surface —
|
|
75
|
+
// same `-s/-t/-e/-H` flags, same "URL or command" positional. It shares the
|
|
76
|
+
// builder rather than getting a copy, so the two can only drift on purpose.
|
|
77
|
+
case "gemini":
|
|
78
|
+
case "qwen": {
|
|
75
79
|
const argv = ["mcp", "add", "-s", "user"];
|
|
76
80
|
if (remote) argv.push("-t", transport);
|
|
77
81
|
for (const [k, v] of env) argv.push("-e", `${k}=${v}`);
|
|
@@ -138,9 +142,27 @@ export function planMcpAdd(spec, { installedSet } = {}) {
|
|
|
138
142
|
});
|
|
139
143
|
}
|
|
140
144
|
|
|
145
|
+
/**
|
|
146
|
+
* Did this engine exit non-zero only because the server was already there?
|
|
147
|
+
*
|
|
148
|
+
* Registering the same server twice is the normal way to re-run `mcp install`,
|
|
149
|
+
* and it is not a failure — but Claude Code and Gemini/Qwen exit 1 on it, so the
|
|
150
|
+
* fan-out summary painted `claude ✗ failed (code 1)` next to opencode's cheerful
|
|
151
|
+
* green box. Read from a box where four engines already had the server, that
|
|
152
|
+
* says "moshcode cannot register with Claude Code" — which is exactly the wrong
|
|
153
|
+
* conclusion, and the reason this function exists rather than a nicer exit code.
|
|
154
|
+
*
|
|
155
|
+
* Matched against the engine's own words, so it stays honest: an engine that
|
|
156
|
+
* fails for any *other* reason still comes back failed.
|
|
157
|
+
*/
|
|
158
|
+
const ALREADY_RE = /already (?:exists|configured|registered|added)|exists in (?:user|global|project) config/i;
|
|
159
|
+
export function alreadyRegistered(r) {
|
|
160
|
+
return ALREADY_RE.test(String(r?.output ?? ""));
|
|
161
|
+
}
|
|
162
|
+
|
|
141
163
|
/**
|
|
142
164
|
* Execute a plan: run each installed, non-skipped engine's `mcp add`. Returns
|
|
143
|
-
* results [{ key, status: "added"|"skipped"|"failed"|"not-installed", reason? }].
|
|
165
|
+
* results [{ key, status: "added"|"already"|"skipped"|"failed"|"not-installed", reason? }].
|
|
144
166
|
* `run` is injectable for tests; defaults to the real spawner.
|
|
145
167
|
*/
|
|
146
168
|
export async function runMcpAdd(plan, { run = runCmd } = {}) {
|
|
@@ -148,8 +170,11 @@ export async function runMcpAdd(plan, { run = runCmd } = {}) {
|
|
|
148
170
|
for (const item of plan) {
|
|
149
171
|
if (item.skip) { results.push({ key: item.key, status: "skipped", reason: item.skip }); continue; }
|
|
150
172
|
if (!item.installed) { results.push({ key: item.key, status: "not-installed" }); continue; }
|
|
151
|
-
|
|
152
|
-
|
|
173
|
+
// capture so a non-zero exit can be read for "already exists" rather than
|
|
174
|
+
// reported as a failure; the child's output still reaches the terminal.
|
|
175
|
+
const r = await run(item.bin, item.argv, { capture: true });
|
|
176
|
+
const status = ranOk(r) ? "added" : alreadyRegistered(r) ? "already" : "failed";
|
|
177
|
+
results.push({ key: item.key, status, code: r.code, signal: r.signal ?? null });
|
|
153
178
|
}
|
|
154
179
|
return results;
|
|
155
180
|
}
|