mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-08-25 00:26:31 +00:00
* feat(core): add persistent Node REPL runtime * fix(core): align Node REPL phase-one contract * fix(core): align Node REPL module compatibility * fix(core): remove unused Node REPL host broker * refactor(node-repl)!: deliver as a standalone MCP server, revert core tools Replaces the three built-in `packages/core` node_repl tools with a self-contained MCP server package, `@qwen-code/node-repl-mcp`. Why --- Issue #9333 triage accepted the runtime only "for exploration" and gated it on an open maintainer decision: built-in core tool vs MCP-server-first. Reversing OpenAI Codex 0.149.0 settled the shape — it ships code mode as a standalone host with an in-process fallback, and its real-Node `js_repl` (not the restricted V8 `exec`) is the analogue this roadmap needs, because stage 3 (#9335) imports cua-driver's N-API addons, which only real Node can load. Delivering out-of-core also keeps a security-relevant subsystem out of the maintainer-gated `packages/core` until its value is proven. What changed ------------ - New `packages/node-repl`: the kernel, module loader, cell transform, protocol and kernel manager, ported and now dependency-free (local `debug-log`/`win-path`/`tokenizer` replace core utils; `output-adapter` emits MCP content blocks in place of `result-converter`). - Reverted every `packages/core` change, the ~11 packaging files that existed only to ship the runtime assets into six distribution layouts, and the two design/plan docs describing the core delivery. - Deleted the trusted-package/sha256 layer: it was empty and unreachable in production (`module-loader.mjs` 882 -> 483 lines). - Wired the package into `scripts/build.js` and the root `vitest.config.ts`. Net effect: `packages/core` is untouched relative to main; the tool is opt-in via `mcpServers` instead of registered unconditionally for every user. Correctness fixes made while porting ------------------------------------ - Stack line numbers were wrong and drifted with binding count (source line 4 reported as 87). The prelude is now one physical line and the cell compiles with `lineOffset: -1`. - Top-level `var` nested in blocks/try/switch/loops was silently dropped; collection now walks the statement subtree, pruning at function boundaries. Verified against real Node for 16 constructs. - A binding named `nodeRepl` permanently broke the output channel; such cells are now rejected. Ordinary globals stay shadowable, matching plain Node. - Errors thrown by imported modules lost their class, `code` and stack. - A throwing frame handler could discard buffered protocol frames. - A hoisted `var` assigned before a throw is now kept, as Node does. - Unhandled rejections settling after a cell no longer vanish. - Live sandbox timers are capped, so one runaway loop cannot saturate the session's event loop. - Image MIME types are matched case-insensitively on both input paths. - Binding sort no longer depends on host locale collation. - The published bin lacked a shebang and mis-detected its entry point, and the server plus its kernel leaked on every host disconnect. Tests: 146 across 14 files, including a compiled N-API addon fixture, 100 consecutive cells, 10 concurrent isolated kernels, stack-line fidelity, and hoisting semantics. Three smoke scripts cover the adapter, the MCP wire and process lifecycle; the packed tarball was installed into a clean project and driven end to end. Refs #9333 * fix(node-repl): pin zod to the hoisted 3.x so the workspace build type-checks `packages/node-repl` declared `zod ^4.1.13`, so `npm ci` installed a nested zod 4.4.3 for it while `@modelcontextprotocol/sdk` resolved the hoisted zod 3.25.76. Two zod type identities in one compilation made every `registerTool` input schema unassignable (`Type 'ZodString' is not assignable to type 'AnySchema'`), failing `npm run build` for this workspace — and with it the install step of every CI job that builds workspaces. The SDK accepts `^3.25 || ^4.0`, so pin the hoisted 3.x line and drop the now-stale nested lockfile entry. One deduped zod, no behaviour change. This only reproduced with a lockfile-driven install; the local tree had no nested copy, which is why the build passed locally and failed in CI. * fix(lint): lint package .mjs node scripts with the node env The eslint "scripts we run with node" override matched `packages/*/scripts/**/*.js` but not `.mjs`, nor a package-root `build.mjs`. `packages/node-repl` is `type: module`, so its `build.mjs` and `scripts/*.mjs` were linted as browser code and `eslint .` failed with `'process' is not defined` / `'console' is not defined` / `'setTimeout' is not defined`. The pre-commit hook only lints `*.{js,jsx,ts,tsx}`, so `.mjs` files are not checked locally — this only surfaced in the root CI lint step. Add `packages/*/scripts/**/*.mjs` and `packages/*/build.mjs` to the override, matching the existing `.js` entries. Generic, additive, no behaviour change. * fix(node-repl): address round-3 review findings (resolution, error identity, image bound) Triaged the bot's round-3 critical findings against the ported code and fixed the three that were genuine defects here; each has a regression test. - R3-5 (resolution): a symlinked `<cwd>/node_modules` — the norm under pnpm, monorepo hoisting and shared CI caches — was silently dropped, so the documented zero-config `await import('pkg')` failed with "cannot resolve from 0 module roots" while plain Node resolved it. The implicit cwd root was applying a re-link self-comparison that only makes sense for registered roots (which carry a registration-time canonical baseline). Follow the symlink for the implicit root, but skip it entirely when the cwd node_modules is itself a registered root, so the registered root's revocation guard is not undermined. - R3-3 (error identity): an error thrown by a host builtin inside imported code (e.g. `fs.readFileSync` → ENOENT) was rewrapped message-only, dropping `code`/`errno`/`syscall`/`cause`/`stack` — so `catch (e) { if (e.code === 'ENOENT') }`, a ubiquitous Node idiom, silently took the wrong branch. Carry those fields onto the realm-wrapped error. - R3-6 (image bound): image-frame `mimeType` was unbounded — only `data.length` counted against the raw image budget — so a malformed/forged frame could retain a huge string that also got interpolated into a notice and tokenized. Bound the MIME at frame ingestion (a real image MIME is a few dozen chars). - R3-4 is by design (the runtime is not a security boundary), but the tool description recommended `createRequire` without noting it is not subject to the process denial or module-root containment the import path enforces; the description now says so. - R3-1 / R3-2 (the `.mjs` lint failure) were already fixed in an earlier commit. Suite: 148 tests (added symlinked-cwd resolution and host-builtin error-code regressions). The symlink fix initially defeated the registered-root revocation test; the registered-root deferral above resolves both. * docs(node-repl): note top-level function/class re-declaration needs a fresh name Round-3 review R3-12/R3-33: re-declaring an existing top-level function or class in a later cell is a link-time SyntaxError (they persist as `let`, which the prelude re-declares), whereas a plain Node REPL accepts it. A correct fix needs declaration-kind tracking the manager does not carry today; until then the tool description's rerun guidance ('prefer var') is corrected, since a function cannot be made rerunnable via var. --------- Co-authored-by: Claude <noreply@anthropic.com>
80 lines
3.1 KiB
JavaScript
80 lines
3.1 KiB
JavaScript
/**
|
|
* @license
|
|
* Copyright 2025 Qwen
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
/**
|
|
* Build script for @qwen-code/node-repl-mcp.
|
|
*
|
|
* tsc compiles src/**.ts -> dist, but does NOT copy the runtime .mjs assets or
|
|
* the tree-sitter JavaScript grammar wasm. This package ships its own copies
|
|
* (it does not depend on qwen-code core's asset-copy pipeline), so we copy them
|
|
* into dist/runtime/ here. resolveKernelPath() and the wasm loader probe
|
|
* ./runtime/ relative to the compiled module, so this layout works in dist.
|
|
*/
|
|
import { cpSync, existsSync, mkdirSync, rmSync } from 'node:fs';
|
|
import path from 'node:path';
|
|
import { createRequire } from 'node:module';
|
|
import { fileURLToPath } from 'node:url';
|
|
import { execFileSync } from 'node:child_process';
|
|
|
|
const require = createRequire(import.meta.url);
|
|
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
const distRuntime = path.join(here, 'dist', 'runtime');
|
|
|
|
// 0. Clean dist AND the incremental build info together. Removing dist alone
|
|
// would leave tsc's buildinfo claiming everything is up to date, so
|
|
// `tsc --build` would emit nothing and produce an empty dist.
|
|
rmSync(path.join(here, 'dist'), { recursive: true, force: true });
|
|
rmSync(path.join(here, 'tsconfig.build.tsbuildinfo'), { force: true });
|
|
|
|
// 1. Compile TypeScript (tsconfig.build.json excludes *.test.ts from dist).
|
|
// Resolve tsc through node rather than `npx` so this also works on Windows,
|
|
// where execFileSync cannot resolve `npx.cmd` without a shell.
|
|
execFileSync(
|
|
process.execPath,
|
|
[require.resolve('typescript/bin/tsc'), '--build', 'tsconfig.build.json'],
|
|
{ cwd: here, stdio: 'inherit' },
|
|
);
|
|
|
|
// 2. Copy the runtime .mjs kernel + module loader into dist/runtime.
|
|
rmSync(distRuntime, { recursive: true, force: true });
|
|
mkdirSync(distRuntime, { recursive: true });
|
|
for (const asset of ['kernel.mjs', 'module-loader.mjs']) {
|
|
cpSync(
|
|
path.join(here, 'src', 'runtime', asset),
|
|
path.join(distRuntime, asset),
|
|
);
|
|
}
|
|
|
|
// 3. Copy the tree-sitter JavaScript grammar wasm from node_modules.
|
|
const grammarRelative = path.join(
|
|
'tree-sitter-wasms',
|
|
'out',
|
|
'tree-sitter-javascript.wasm',
|
|
);
|
|
const grammarSource = [
|
|
path.join(here, 'node_modules', grammarRelative),
|
|
path.join(here, '..', '..', 'node_modules', grammarRelative),
|
|
].find(existsSync);
|
|
if (!grammarSource) {
|
|
throw new Error(
|
|
'node_repl JavaScript grammar asset (tree-sitter-javascript.wasm) was not found',
|
|
);
|
|
}
|
|
cpSync(grammarSource, path.join(distRuntime, 'tree-sitter-javascript.wasm'));
|
|
|
|
// 4. Sanity-check the emit. A stale buildinfo or a misconfigured tsconfig can
|
|
// make `tsc --build` silently emit nothing, leaving a dist that only has the
|
|
// copied assets — which then fails at runtime instead of at build time.
|
|
for (const required of ['index.js', 'kernel-manager.js', 'mcp-server.js']) {
|
|
const emitted = path.join(here, 'dist', required);
|
|
if (!existsSync(emitted)) {
|
|
throw new Error(
|
|
`build produced no dist/${required} — tsc emitted nothing (stale tsconfig.build.tsbuildinfo?)`,
|
|
);
|
|
}
|
|
}
|
|
|
|
console.log(`node-repl-mcp: runtime assets copied to ${distRuntime}`);
|