qwen-code/packages/node-repl/.gitignore
顾盼 be891657f7
refactor(node-repl)!: deliver the persistent Node REPL as a standalone MCP server (#9499)
* 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>
2026-08-23 14:20:39 +00:00

5 lines
75 B
Text

dist/
build/
*.tsbuildinfo
.qwen_node_repl_cell_*
src/fixtures/napi/build/