@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.
- package/README.md +2 -1
- package/THIRD-PARTY-NOTICES.md +1 -1
- package/anno-acme-ident.ts +97 -0
- package/anno-cli.ts +1465 -0
- package/anno-confidence.ts +233 -0
- package/anno-coverage.ts +2465 -0
- package/anno-d64.ts +310 -0
- package/anno-derive.ts +590 -0
- package/anno-details.ts +169 -0
- package/anno-enum-gen.ts +533 -0
- package/anno-export-asm.ts +1310 -0
- package/anno-index.ts +150 -0
- package/anno-memmap-render.ts +672 -0
- package/anno-regbits-gen.ts +421 -0
- package/anno-regbits.json +1370 -0
- package/anno-register.ts +240 -0
- package/anno-store.ts +3486 -0
- package/anno-symbols.ts +266 -0
- package/anno-tools.ts +2111 -0
- package/anno-types.ts +1636 -0
- package/block-class.ts +201 -0
- package/build.ts +1 -1
- package/capability-registry.ts +3 -1
- package/disasm-decoder.ts +14 -14
- package/disasm-opcodes.ts +4 -4
- package/disasm-renderer.ts +2 -2
- package/hostpath.ts +1 -1
- package/install-resources.ts +1 -1
- package/package.json +23 -3
- package/prg-image.ts +119 -0
- package/repo-root.ts +20 -5
- package/resources/broker-launch.mjs +8 -4
- package/resources/vice-launcher.sh +3 -3
- package/stock-address.ts +5 -5
- package/stock-cia.ts +2 -2
- package/stock-condition.ts +7 -7
- package/stock-connect.ts +1 -1
- package/stock-dispatch.ts +35 -5
- package/stock-execution.ts +5 -3
- package/stock-input.ts +9 -9
- package/stock-machine.ts +17 -6
- package/stock-protocol.ts +16 -11
- package/stock-registers.ts +54 -29
- package/stock-sprites.ts +3 -3
- package/stock-symbols.ts +33 -9
- package/stock-timing.ts +1 -1
- package/stock-vicii.ts +1 -1
- package/version.ts +1 -1
- 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();
|