@henols/vice-mcp 0.2.0 → 0.2.2

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.
Files changed (49) hide show
  1. package/README.md +2 -1
  2. package/THIRD-PARTY-NOTICES.md +1 -1
  3. package/anno-acme-ident.ts +97 -0
  4. package/anno-cli.ts +1465 -0
  5. package/anno-confidence.ts +233 -0
  6. package/anno-coverage.ts +2465 -0
  7. package/anno-d64.ts +310 -0
  8. package/anno-derive.ts +590 -0
  9. package/anno-details.ts +169 -0
  10. package/anno-enum-gen.ts +533 -0
  11. package/anno-export-asm.ts +1310 -0
  12. package/anno-index.ts +150 -0
  13. package/anno-memmap-render.ts +672 -0
  14. package/anno-regbits-gen.ts +421 -0
  15. package/anno-regbits.json +1370 -0
  16. package/anno-register.ts +240 -0
  17. package/anno-store.ts +3486 -0
  18. package/anno-symbols.ts +266 -0
  19. package/anno-tools.ts +2111 -0
  20. package/anno-types.ts +1636 -0
  21. package/block-class.ts +201 -0
  22. package/build.ts +1 -1
  23. package/capability-registry.ts +3 -1
  24. package/disasm-decoder.ts +14 -14
  25. package/disasm-opcodes.ts +4 -4
  26. package/disasm-renderer.ts +2 -2
  27. package/hostpath.ts +1 -1
  28. package/install-resources.ts +1 -1
  29. package/package.json +23 -3
  30. package/prg-image.ts +119 -0
  31. package/repo-root.ts +20 -5
  32. package/resources/broker-launch.mjs +8 -4
  33. package/resources/vice-launcher.sh +3 -3
  34. package/stock-address.ts +5 -5
  35. package/stock-cia.ts +2 -2
  36. package/stock-condition.ts +7 -7
  37. package/stock-connect.ts +1 -1
  38. package/stock-dispatch.ts +35 -5
  39. package/stock-execution.ts +5 -3
  40. package/stock-input.ts +9 -9
  41. package/stock-machine.ts +17 -6
  42. package/stock-protocol.ts +16 -11
  43. package/stock-registers.ts +54 -29
  44. package/stock-sprites.ts +3 -3
  45. package/stock-symbols.ts +33 -9
  46. package/stock-timing.ts +1 -1
  47. package/stock-vicii.ts +1 -1
  48. package/version.ts +1 -1
  49. package/vice-proxy.ts +168 -0
package/vice-proxy.ts CHANGED
@@ -185,6 +185,130 @@ import * as stockDispatch from "./stock-dispatch.ts";
185
185
  // below, strictly after the DENY_LIST check -- see the comment at that call
186
186
  // site for why the ordering is load-bearing.
187
187
  import { capabilityRefusalMessage } from "./capability-registry.ts";
188
+ // Plan 29-01: the curated anno_* tool surface's DEFINITIONS, imported
189
+ // STATICALLY -- registration below happens synchronously at module scope, so
190
+ // a dynamic import cannot serve it. This costs nothing at module load: no
191
+ // store file is opened here. The owned SQLite annotation store stays behind
192
+ // anno-tools.ts's own runAnnoTool(), which opens it, answers exactly one call
193
+ // against it and closes it again (D-06), reached only when a tool is called.
194
+ import { ANNO_TOOL_DEFINITIONS, runAnnoTool } from "./anno-tools.ts";
195
+
196
+ // ------------------------------------------------------------ anno subcommand
197
+ //
198
+ // D-06 / RESEARCH.md Open Question #1 (plan 10-04): `vice-mcp anno <verb>` is
199
+ // the ONLY surface that resolves identically across the Claude Code plugin
200
+ // route and both npm-installer routes -- `installer/bin/cli.mjs`'s
201
+ // `viceServerEntry()` always launches this server via `npx` in BOTH
202
+ // npm-installer modes (`--vendor` only pre-resolves the package; it never
203
+ // places `src/mcp/vice/*.ts` as plain files inside a consuming project),
204
+ // so any design resolving a filesystem path to the seam would silently fail
205
+ // to resolve for npm-installed users. This bin is the one surface proven to
206
+ // work in all three routes.
207
+ //
208
+ // This branch runs as the first executable statement of the module body,
209
+ // deliberately ABOVE `ACTIVE_BACKEND`'s backend probe (which shells out to a
210
+ // binary's `--help`), above the manifest read, and above
211
+ // `new MCPServer(...)`/`server.startStdio()` far below -- a CLI invocation
212
+ // must never open a socket, never probe a binary, and never write a byte of
213
+ // JSON-RPC to stdout. WHAT NOT TO DO: never let this branch fall through
214
+ // into the server path, and never print anything on stdout on the server
215
+ // path that a CLI caller could confuse for `anno` output.
216
+ //
217
+ // Ending the process here is deliberate and is NOT a violation of this
218
+ // file's standing "never end the process from a teardown handler" rule (see
219
+ // that handler's own comment further down): that rule protects the
220
+ // long-lived server's lease-release path, and this branch ends the process
221
+ // before any lease, socket or handler exists. A dynamic import is used
222
+ // (not a static one) so the CLI module is not part of the server's startup
223
+ // cost on the normal, non-`anno` path.
224
+ //
225
+ // IN-01 (10-REVIEW.md; 11.1-CONTEXT.md AUDIT-01, D-11.1-04): `console.log`/
226
+ // `console.error` writes to `process.stdout`/`process.stderr` are
227
+ // ASYNCHRONOUS on POSIX once the fd is a pipe (Node opens pipe/socket fds
228
+ // non-blocking, unlike a TTY or a regular file), so a bare `process.exit()`
229
+ // immediately after can discard whatever write has not yet drained --
230
+ // measured at a 128 KiB truncation point on this host's Node for a single
231
+ // write exceeding the OS pipe's capacity. The trigger this was FOUND
232
+ // through -- a CLI verb that echoed a spawned child's whole stderr -- was
233
+ // withdrawn with the analyser it spawned (plan 29-07, D-14), but the hazard
234
+ // is a property of the exit path rather than of that verb: any verb that
235
+ // prints a diagnostic larger than the pipe's capacity hits it, and the one
236
+ // case where the user most needs the diagnostic is exactly the case a piped
237
+ // invocation could silently lose it in. `drainStdio()` below explicitly
238
+ // awaits both streams' own pending writes (a `write("", cb)`-style
239
+ // zero-length write's callback fires only once every prior queued write has
240
+ // actually flushed) before the terminating `process.exit(code)`.
241
+ //
242
+ // T-11.1-EXITHANG: the drain is BOUNDED to `ANNO_CLI_DRAIN_TIMEOUT_MS`. An
243
+ // exit that hangs forever waiting on a pipe nobody reads is worse than a
244
+ // truncated diagnostic -- this project's standing rule is that a teardown
245
+ // path never becomes a hang (see the "never end the process from a teardown
246
+ // handler" comment above; a BOUNDED drain here does not violate that rule
247
+ // for the same reason the original unbounded `process.exit()` did not: this
248
+ // still runs before any lease, socket or handler exists, and now also can
249
+ // never block indefinitely).
250
+ const ANNO_CLI_DRAIN_TIMEOUT_MS = 300;
251
+
252
+ /** Resolves once `stream`'s own pending writes have flushed, or after
253
+ * `timeoutMs`, whichever comes first. A zero-length `write("", cb)`'s
254
+ * callback fires strictly after every write queued ahead of it on the same
255
+ * stream has completed -- so this is a genuine drain barrier, not a fixed
256
+ * sleep. Guarded so a stream that is not writable (already closed/ended,
257
+ * e.g. under `> /dev/null` teardown races) resolves immediately rather than
258
+ * calling `write()` on it. */
259
+ function drainStdio(stream: NodeJS.WriteStream): Promise<void> {
260
+ return new Promise((resolve) => {
261
+ if (!stream.writable) {
262
+ resolve();
263
+ return;
264
+ }
265
+ const timer = setTimeout(resolve, ANNO_CLI_DRAIN_TIMEOUT_MS);
266
+ stream.write("", () => {
267
+ clearTimeout(timer);
268
+ resolve();
269
+ });
270
+ });
271
+ }
272
+
273
+ if (process.argv[2] === "anno") {
274
+ // A broken pipe (the reader closing early, e.g. `| head`) makes
275
+ // `stream.write()` fail with EPIPE. Awaiting `drainStdio()` below gives
276
+ // that failure the chance to actually surface as Node's stream 'error'
277
+ // event -- which throws UNCAUGHT and crashes the process if nothing is
278
+ // listening, converting what used to be a silent (bare `process.exit()`
279
+ // outran the async error) success into a stack-trace crash. This
280
+ // mirrors the SAME EPIPE class this file already guards against for the
281
+ // server path further down (see that handler's own
282
+ // modelcontextprotocol/typescript-sdk#1564 citation) -- registered here
283
+ // too because this branch exits long before reaching that one.
284
+ process.stdout.on("error", () => {});
285
+ process.stderr.on("error", () => {});
286
+
287
+ // Test-only escape hatch, never documented to end users and inert unless
288
+ // this exact env var is set: writes a deterministic filler payload
289
+ // through this SAME drained-exit path, so vice-proxy.test.ts can measure
290
+ // an exact byte count well above any OS pipe capacity without needing any
291
+ // real project file at all. That independence is the point: measured at
292
+ // the time, neither `--help`'s ~5.6 KB USAGE text nor a synthesized or
293
+ // garbage project file fed to a verb's error path scaled anywhere near
294
+ // 128 KiB on this host, so no real invocation could reach the truncation
295
+ // point on demand (see 11.1-05-SUMMARY.md for those measurements). The
296
+ // hatch outlives the routes it was measured against, because it depends on
297
+ // none of them. Never reachable
298
+ // from a real `anno <verb>` invocation: the check is against a specific,
299
+ // unambiguous env var name no real caller would ever set.
300
+ const testFillBytes = process.env.VICE_TEST_ANNO_CLI_STDOUT_FILL_BYTES;
301
+ if (testFillBytes) {
302
+ process.stdout.write("x".repeat(Number(testFillBytes)));
303
+ await Promise.all([drainStdio(process.stdout), drainStdio(process.stderr)]);
304
+ process.exit(0);
305
+ }
306
+
307
+ const { runAnnoCli } = await import("./anno-cli.ts");
308
+ const code = await runAnnoCli(process.argv.slice(3));
309
+ await Promise.all([drainStdio(process.stdout), drainStdio(process.stderr)]);
310
+ process.exit(code);
311
+ }
188
312
 
189
313
  const HERE_DIR = dirname(fileURLToPath(import.meta.url));
190
314
 
@@ -193,6 +317,23 @@ const HERE_DIR = dirname(fileURLToPath(import.meta.url));
193
317
  // selection, the tools construction loop's dispatch choice, and the final
194
318
  // ready log line) reads THIS constant, never the raw environment variable
195
319
  // and never a second detection call.
320
+ //
321
+ // `ACTIVE_BACKEND.binPath` is what `vice_ping`'s `resolvedBinaryPath` field
322
+ // reports (see stock-dispatch.ts's `handlePing()`). It is resolved exactly
323
+ // ONCE here, at MCP-server process startup, by a bare `x64sc` `$PATH` probe
324
+ // run in THIS process's own environment -- it is NOT re-probed per request
325
+ // and has no connection to the broker, a separate, already-running process
326
+ // that leases whichever instance it chose to whatever request comes in. On a
327
+ // host where bare `x64sc` resolves to one build, `resolvedBinaryPath` reports
328
+ // that build's path on every ping, even when the broker actually launched
329
+ // (or later recycled to) a different one. The authoritative per-instance
330
+ // answer lives in the broker's own launch record (`epoch.json`'s `vice_bin`
331
+ // field, written by `broker-epoch.mts`) -- Phase 8.2 plan 04's own
332
+ // walkthrough had to route around this field entirely and prove backend
333
+ // identity from that launch record plus a live `ps -o args=` read instead
334
+ // (2026-08-19 finding, closed as a documentation fix by Phase 15 plan 15-09
335
+ // rather than a per-request requery, which would be a behavioural change out
336
+ // of a disposition phase's remit).
196
337
  const ACTIVE_BACKEND = backendDetect.resolvedBackend();
197
338
 
198
339
  // -------------------------------------------------------------- JSON-RPC
@@ -3016,6 +3157,7 @@ async function forwardToVice(name: string, args: Record<string, unknown>): Promi
3016
3157
  // and asserts that slice contains no promise-awaiting construct and calls
3017
3158
  // the control session's release function exactly once. Do not move either
3018
3159
  // marker away from the code each one bounds.
3160
+ //
3019
3161
  let teardownRan = false;
3020
3162
 
3021
3163
  function releaseLeaseNow(trigger: string): void {
@@ -3220,6 +3362,32 @@ tools[RESULT_CONTINUE_TOOL.name] = buildViceTool(RESULT_CONTINUE_TOOL, (args) =>
3220
3362
  // entry in `tools/list`.
3221
3363
  tools[RECYCLE_TOOL.name] = buildBackendAwareTool(stockDispatch.resolveAdvertisedToolDefinition(RECYCLE_TOOL, ACTIVE_BACKEND.backend, manifestTools), (args) => handleRecycle(args));
3222
3364
  tools[DIAGNOSE_TOOL.name] = buildBackendAwareTool(stockDispatch.resolveAdvertisedToolDefinition(DIAGNOSE_TOOL, ACTIVE_BACKEND.backend, manifestTools), (args) => handleDiagnose(args));
3365
+ // Backend-INDEPENDENT by construction (plan 29-01): the anno_* family never
3366
+ // touches VICE at all -- it reaches a PROXY-LOCAL SQLite annotation store
3367
+ // this repo owns, opened and closed inside the runner itself, so there is no
3368
+ // fork/stock distinction to make and buildBackendAwareTool() would be flatly
3369
+ // wrong here (there is nothing for it to dispatch to on either backend). The
3370
+ // family is in NEITHER tools-manifest.json NOR tools-manifest.stock.json:
3371
+ // both are regenerated by refresh-manifest.ts from a live HOST VICE server's
3372
+ // own tools/list, and a local store file is never that host -- a hand-added
3373
+ // entry in either manifest would be silently wiped on the next refresh.
3374
+ // Registered here via buildViceTool() directly (the SAME exception
3375
+ // RESULT_CONTINUE_TOOL above is), so no anno_* runner can ever reach
3376
+ // forwardToVice(), call(), or ensureViceSession() -- CLAUDE.md's "derived
3377
+ // tools must be intercepted before forwardToVice()" constraint is satisfied
3378
+ // by construction for this family, not by an interception, because the
3379
+ // runner is never wired to forwardToVice() in the first place.
3380
+ // Deliberately NOT named `def` (the manifest loop's own loop variable,
3381
+ // above): `stock-dispatch.test.ts`'s `proxyToolRegistrations()` regex-scans
3382
+ // this file's own `tools[...] = ...;` lines and keys each one by its raw
3383
+ // captured text, so an identically-named loop variable here would make this
3384
+ // registration textually indistinguishable from the manifest loop's -- a
3385
+ // distinct name (`annoDef`) keeps the anno_* family's own exemption from
3386
+ // `buildBackendAwareTool()` from ever being confused with, or accidentally
3387
+ // widened to cover, the manifest loop's registration.
3388
+ for (const annoDef of ANNO_TOOL_DEFINITIONS) {
3389
+ tools[annoDef.name] = buildViceTool(annoDef, (args) => runAnnoTool(annoDef.name, args));
3390
+ }
3223
3391
 
3224
3392
  const server = new MCPServer({ name: "vice", version: PROXY_VERSION, tools });
3225
3393
  await server.startStdio();