@henols/vice-mcp 0.2.1 → 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 -24
- package/{r2000-acme-ident.ts → anno-acme-ident.ts} +13 -13
- package/anno-cli.ts +1465 -0
- package/{r2000-confidence.ts → anno-confidence.ts} +22 -22
- package/anno-coverage.ts +2465 -0
- package/{r2000-d64.ts → anno-d64.ts} +5 -5
- 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/{r2000-memmap-render.ts → anno-memmap-render.ts} +236 -95
- package/{r2000-regbits-gen.ts → anno-regbits-gen.ts} +20 -15
- package/{r2000-regbits.json → anno-regbits.json} +2 -2
- 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 -17
- 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 +9 -9
- package/stock-timing.ts +1 -1
- package/stock-vicii.ts +1 -1
- package/version.ts +1 -1
- package/vice-proxy.ts +68 -46
- package/r2000-cli.ts +0 -1103
- package/r2000-enum-gen.ts +0 -574
- package/r2000-launch.ts +0 -357
- package/r2000-mcp-client.ts +0 -596
- package/r2000-project.ts +0 -190
- package/r2000-symbols.ts +0 -388
- package/r2000-tools.ts +0 -914
- package/r2000-verify.ts +0 -184
package/block-class.ts
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// block-class.ts
|
|
3
|
+
//
|
|
4
|
+
// The ONE place that translates an annotation store's own block-type
|
|
5
|
+
// vocabulary into a neutral block class. Nothing else in this tree may
|
|
6
|
+
// compare a store block-type string, and nothing else may read a store block
|
|
7
|
+
// listing on the census's behalf (SEAM-03).
|
|
8
|
+
//
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
// WHY THIS FILE EXISTS
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
// `anno-coverage.ts` is the coverage instrument: it implements two Validated
|
|
13
|
+
// requirements and measures how much of a binary has actually been reverse-
|
|
14
|
+
// engineered. Its census is a pure function of the raw bytes and the seed set
|
|
15
|
+
// the caller supplies -- deliberately, because deriving completeness from the
|
|
16
|
+
// store's own block table measures the annotator's bookkeeping rather than
|
|
17
|
+
// the annotation.
|
|
18
|
+
//
|
|
19
|
+
// Yet the census's ONLY tie to the external analyser this project rents its
|
|
20
|
+
// annotation store from was four inline comparisons against that analyser's
|
|
21
|
+
// Rust `Display` strings, spread across three code regions and reached from
|
|
22
|
+
// two lookup call sites. Four string literals were the entire reason a
|
|
23
|
+
// 2,292-line instrument looked like glue around somebody else's data model.
|
|
24
|
+
// Any prefix-driven deletion of the rented substrate would have taken the
|
|
25
|
+
// instrument with it.
|
|
26
|
+
//
|
|
27
|
+
// So the tie lives here, in one small module, behind one named function. A
|
|
28
|
+
// later phase swaps THIS module -- the store's vocabulary changes, the census
|
|
29
|
+
// does not. That is the whole point: the boundary is the module, never an
|
|
30
|
+
// argument threaded through the census.
|
|
31
|
+
//
|
|
32
|
+
// The neutral classes are LOWERCASE. That lowercase-ness was once claimed as
|
|
33
|
+
// a protection in its own right -- the argument being that a capitalised
|
|
34
|
+
// store spelling could not accidentally agree with it, so a comparison site
|
|
35
|
+
// left behind somewhere else would get a different answer and move a measured
|
|
36
|
+
// number, loudly. THAT PREMISE IS NOW FALSE, and the loss is recorded here
|
|
37
|
+
// rather than left to be rediscovered: this project's own
|
|
38
|
+
// store's vocabulary is LOWERCASE, and two of its twelve members -- the code
|
|
39
|
+
// spelling and the undefined spelling -- are string-identical to their
|
|
40
|
+
// neutral classes. A left-behind raw comparison against the store's own
|
|
41
|
+
// spelling therefore CAN accidentally agree now, silently, which is exactly
|
|
42
|
+
// what the old rationale promised could not happen.
|
|
43
|
+
//
|
|
44
|
+
// Two guards replace it, and they are why the mapping below is still
|
|
45
|
+
// defended. Neither is a claim in a header:
|
|
46
|
+
//
|
|
47
|
+
// (a) the derived TOTAL cross-check in `block-class.test.ts`. It iterates
|
|
48
|
+
// the store's frozen twelve-member vocabulary from its single home and
|
|
49
|
+
// asserts the class this module returns for EVERY member, with its own
|
|
50
|
+
// non-vacuity assertions on the counts. It reddens the moment either
|
|
51
|
+
// side drifts; a spot check would not.
|
|
52
|
+
// (b) `anno-coverage.test.ts`'s zero-overlap substitutability proof. Its
|
|
53
|
+
// substituted vocabulary (`EXECUTABLE_EXTENT` and its two siblings)
|
|
54
|
+
// shares no string with EITHER accepted vocabulary, so a left-behind
|
|
55
|
+
// comparison site is still observable there -- which is why that
|
|
56
|
+
// vocabulary list is DERIVED from both, not hand-written at four
|
|
57
|
+
// entries.
|
|
58
|
+
//
|
|
59
|
+
// ---------------------------------------------------------------------------
|
|
60
|
+
// WHAT NOT TO DO -- each of these is a specific, named trap
|
|
61
|
+
// ---------------------------------------------------------------------------
|
|
62
|
+
// 1. NEVER accept the census, the raw program bytes, a decoder, or a
|
|
63
|
+
// confidence grade as an argument here. `anno-coverage.ts` records a
|
|
64
|
+
// BYTES-VERSUS-STORE independence axis: one side classifies an address
|
|
65
|
+
// using only the raw bytes and the census, the other using only the
|
|
66
|
+
// store's own documentation, and NEITHER SIDE READS THE OTHER'S INPUT.
|
|
67
|
+
// That axis is the reason its reproducibility figure means anything.
|
|
68
|
+
// One extra argument here collapses it -- and it collapses QUIETLY: the
|
|
69
|
+
// independence test would keep passing while the claim it protects
|
|
70
|
+
// became void. The signature below is two arguments and must stay two.
|
|
71
|
+
// 2. NEVER compare a store vocabulary string outside this module. A second
|
|
72
|
+
// comparison site is a second answer to "what class is this address",
|
|
73
|
+
// and the census must have exactly one.
|
|
74
|
+
// 3. NEVER hold module-level mutable state. The lookup is a pure function
|
|
75
|
+
// of its two arguments, so interleaved or repeated calls cannot observe
|
|
76
|
+
// each other. There is nothing to reset and nothing to synchronise.
|
|
77
|
+
// 4. NEVER import anything census-side, disassembler-side, transport-side
|
|
78
|
+
// or path-translation-side. This module's import list is EMPTY and a
|
|
79
|
+
// committed structural assertion in `block-class.test.ts` keeps it that
|
|
80
|
+
// way -- see trap 1 for why the emptiness is load-bearing rather than
|
|
81
|
+
// tidy.
|
|
82
|
+
|
|
83
|
+
// ---------------------------------------------------------------------------
|
|
84
|
+
// The neutral vocabulary
|
|
85
|
+
// ---------------------------------------------------------------------------
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* The three neutral block classes every consumer speaks. Lowercase tokens.
|
|
89
|
+
*
|
|
90
|
+
* These tokens were once ALSO offered as a guard in their own right, on the
|
|
91
|
+
* ground that no store spelling could collide with them. Two of the store's
|
|
92
|
+
* twelve members now do collide, so that reading is gone -- see this file's
|
|
93
|
+
* header for the loss and for the two derived cross-checks that defend the
|
|
94
|
+
* mapping instead.
|
|
95
|
+
*
|
|
96
|
+
* Three-valued and no finer. The consumers compare this against a
|
|
97
|
+
* classification derived from completely different inputs, so a richer
|
|
98
|
+
* vocabulary would manufacture disagreement out of vocabulary drift rather
|
|
99
|
+
* than measure anything.
|
|
100
|
+
*/
|
|
101
|
+
export type BlockClass = "code" | "data" | "undefined";
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* One entry of a store's block listing, exactly as the curated read tool
|
|
105
|
+
* returns it.
|
|
106
|
+
*
|
|
107
|
+
* `type` carries the STORE'S OWN vocabulary -- whatever spelling the
|
|
108
|
+
* annotation substrate happens to use for its block kinds. This module is
|
|
109
|
+
* the only place in the tree that interprets that field. A consumer that
|
|
110
|
+
* reads `.type` and compares it is re-opening the boundary this file exists
|
|
111
|
+
* to close.
|
|
112
|
+
*/
|
|
113
|
+
export interface BlockEntry {
|
|
114
|
+
start_address: number;
|
|
115
|
+
end_address: number;
|
|
116
|
+
/** The store's own block-kind spelling. Interpreted HERE and nowhere else. */
|
|
117
|
+
type: string;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* The shape of a block classifier: given a store's block listing and an
|
|
122
|
+
* address, the neutral class covering that address, or `null` when no block
|
|
123
|
+
* covers it.
|
|
124
|
+
*
|
|
125
|
+
* Named as a type so a consumer can be handed a substitute implementation
|
|
126
|
+
* for a substitutability proof without that consumer ever naming a concrete
|
|
127
|
+
* store vocabulary.
|
|
128
|
+
*/
|
|
129
|
+
export type BlockClassifier = (blocks: readonly BlockEntry[], address: number) => BlockClass | null;
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* The one production classifier.
|
|
133
|
+
*
|
|
134
|
+
* A linear scan, first-match-wins, with both range ends INCLUSIVE. A
|
|
135
|
+
* `null`/`undefined` hole in the array is skipped rather than thrown on --
|
|
136
|
+
* the listing arrives from a project file this process did not author. That
|
|
137
|
+
* premise applies to `blocks` ITSELF as well, so a non-array argument returns
|
|
138
|
+
* `null` here rather than throwing a `TypeError` out of the `for ... of`
|
|
139
|
+
* (IN-04). Both production callers already pre-guard with
|
|
140
|
+
* `Array.isArray(blocks) ? blocks : []`, so nothing is reachable today; the
|
|
141
|
+
* point is that the defence now lives in the module that DOCUMENTS the
|
|
142
|
+
* premise, instead of only in callers outside it.
|
|
143
|
+
*
|
|
144
|
+
* The mapping is total by construction over BOTH ACCEPTED VOCABULARIES: a
|
|
145
|
+
* code spelling from either becomes `"code"`, an undefined spelling from
|
|
146
|
+
* either becomes `"undefined"`, and EVERY other spelling becomes `"data"` --
|
|
147
|
+
* whether it belongs to one of the two vocabularies or to neither. That
|
|
148
|
+
* fallthrough is not a simplification; it is exactly what the two comparisons
|
|
149
|
+
* this function replaced did when read together, and it is what keeps an
|
|
150
|
+
* unrecognised spelling -- including one from a producer this module has
|
|
151
|
+
* never heard of -- from silently reading as code.
|
|
152
|
+
*
|
|
153
|
+
* The two arms are two VOCABULARIES that happen to differ in case, never one
|
|
154
|
+
* vocabulary compared case-insensitively. A case-insensitive or
|
|
155
|
+
* whitespace-trimming comparison here would silently accept a third spelling
|
|
156
|
+
* nobody chose; `block-class.test.ts` pins that it does not.
|
|
157
|
+
*/
|
|
158
|
+
export const blockClassAt: BlockClassifier = (blocks, address) => {
|
|
159
|
+
if (!Array.isArray(blocks)) return null;
|
|
160
|
+
for (const block of blocks) {
|
|
161
|
+
if (!block) continue;
|
|
162
|
+
if (address >= block.start_address && address <= block.end_address) {
|
|
163
|
+
// TWO ACCEPTED VOCABULARIES, one arm each, in the same order so the
|
|
164
|
+
// pairing reads at a glance. Deliberately NOT folded into a
|
|
165
|
+
// case-insensitive test -- see this function's doc comment.
|
|
166
|
+
//
|
|
167
|
+
// This project's own annotation store, whose twelve block types are
|
|
168
|
+
// lowercase and live in one frozen home the test cross-checks against:
|
|
169
|
+
if (block.type === "code") return "code";
|
|
170
|
+
if (block.type === "undefined") return "undefined";
|
|
171
|
+
// TRANSITIONAL -- the capitalised vocabulary, whose PRODUCER (the
|
|
172
|
+
// external analyser this project used to rent an annotation store from,
|
|
173
|
+
// whose Rust `Display` emitted these spellings) IS GONE AS OF PHASE 29,
|
|
174
|
+
// 2026-08-29. The trigger the previous comment named has therefore
|
|
175
|
+
// already fired, and the arm still stands. That is a decision, recorded
|
|
176
|
+
// here rather than left to be rediscovered as an inert branch:
|
|
177
|
+
//
|
|
178
|
+
// WHY IT SURVIVES ITS OWN TRIGGER: every committed coverage fixture
|
|
179
|
+
// under `fixtures/coverage/**/store.json` is still SPELLED in this
|
|
180
|
+
// vocabulary ("Code", "Byte", "Undefined"). Deleting the two arms
|
|
181
|
+
// below today reclassifies every fixture block as `data` -- silently,
|
|
182
|
+
// because `data` is the total fallthrough and no error is raised
|
|
183
|
+
// anywhere. The fixtures are the census's own controls, so that would
|
|
184
|
+
// move the numbers the controls exist to pin.
|
|
185
|
+
//
|
|
186
|
+
// THE NEW REMOVAL TRIGGER is therefore the FIXTURES being re-spelled
|
|
187
|
+
// into the store's own lowercase vocabulary -- not the producer being
|
|
188
|
+
// deleted, which has happened. Re-spell the fixtures (and their
|
|
189
|
+
// generator) first, observe the census unchanged, then delete these
|
|
190
|
+
// two lines.
|
|
191
|
+
//
|
|
192
|
+
// FATE: carried as a Phase 32 guard-fate item ("every guard pinned to
|
|
193
|
+
// the deleted subject has a recorded fate"), so the ledger picks this
|
|
194
|
+
// arm up deliberately instead of finding it red or inert in CI.
|
|
195
|
+
if (block.type === "Code") return "code";
|
|
196
|
+
if (block.type === "Undefined") return "undefined";
|
|
197
|
+
return "data";
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return null;
|
|
201
|
+
};
|
package/build.ts
CHANGED
|
@@ -138,7 +138,7 @@ export interface BuildOptions {
|
|
|
138
138
|
* `renameSync()` per artifact, and `rename(2)` fails `EXDEV` across mounts.
|
|
139
139
|
* That is why the original implementation staged at `dirname(outDirAbs)`.
|
|
140
140
|
* 2. **Outside any directory a test walks.** Staging at `dirname(outDirAbs)`
|
|
141
|
-
* put a transient `.build-tmp-*` inside
|
|
141
|
+
* put a transient `.build-tmp-*` inside `src/mcp/vice/`, and
|
|
142
142
|
* `vice-mcp-selector-docs.test.ts`'s `walkFiles()` recurses through every
|
|
143
143
|
* directory there except `node_modules` — so a concurrent walk descended
|
|
144
144
|
* into the staging dir and died `ENOENT` when the rename removed it. That
|
package/capability-registry.ts
CHANGED
|
@@ -280,7 +280,9 @@ export const CAPABILITY_REGISTRY: readonly CapabilityEntry[] = [
|
|
|
280
280
|
providedBy: "fork",
|
|
281
281
|
reason:
|
|
282
282
|
"Full resource get/set access was descoped; the fork's tool is a hand-curated whitelist " +
|
|
283
|
-
"subset that never shipped on stock."
|
|
283
|
+
"subset that never shipped on stock. Its advertised WarpMode resource is fork-only: stock " +
|
|
284
|
+
"has no runtime warp resource at all, and warp on stock is a launch-time flag, not a " +
|
|
285
|
+
"resource that can be toggled while running.",
|
|
284
286
|
},
|
|
285
287
|
{
|
|
286
288
|
name: "vice_joystick_tap",
|
package/disasm-decoder.ts
CHANGED
|
@@ -1,24 +1,24 @@
|
|
|
1
1
|
// disasm-decoder.ts
|
|
2
2
|
//
|
|
3
3
|
// The pure `decode(bytes, startAddress, opts) -> Instruction[]` function --
|
|
4
|
-
// D-05's standalone module
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
// transport code
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
4
|
+
// D-05's standalone module, kept import-free of `stock-*.ts`/`vice*.ts`/any
|
|
5
|
+
// `node:` builtin so any future non-tool consumer (a decode call with no
|
|
6
|
+
// socket in the picture) could depend on this one file without dragging in
|
|
7
|
+
// transport code. Two originally-planned consumers never landed: Phase 5's
|
|
8
|
+
// backtrace (DERIV-02) and a CPU-history decode (GAIN-01) were both cut
|
|
9
|
+
// from v0.2.0 scope on 2026-08-17 -- see the startAddress bound below,
|
|
10
|
+
// which is now defense-in-depth on a currently unreachable path rather
|
|
11
|
+
// than a guard against a live in-process caller.
|
|
12
12
|
//
|
|
13
13
|
// ---------------------------------------------------------------------------
|
|
14
14
|
// WHY THIS FILE EXISTS RATHER THAN LIVING INSIDE THE TOOL HANDLER
|
|
15
15
|
// ---------------------------------------------------------------------------
|
|
16
|
-
// `stock-disassemble.ts` (04-05) is the tool-facing consumer
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
// transport code.
|
|
16
|
+
// `stock-disassemble.ts` (04-05) is the tool-facing consumer; the renderer
|
|
17
|
+
// (04-04) is the only other one that exists. Two more were planned (Phase
|
|
18
|
+
// 5's backtrace, DERIV-02, and a CPU-history decode, GAIN-01) but both
|
|
19
|
+
// were cut from v0.2.0 scope on 2026-08-17 before being built. Keeping
|
|
20
|
+
// decode() import-free of `stock-*.ts`/`vice*.ts`/any `node:` builtin means
|
|
21
|
+
// both existing consumers can depend on this file without transport code.
|
|
22
22
|
//
|
|
23
23
|
// ---------------------------------------------------------------------------
|
|
24
24
|
// WHAT NOT TO DO
|
package/disasm-opcodes.ts
CHANGED
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
// Mnemonic naming: two deliberate departures from cc65's own spelling
|
|
44
44
|
// ---------------------------------------------------------------------------
|
|
45
45
|
// cc65 spells two illegal opcodes differently from ACME's verified
|
|
46
|
-
// `!cpu 6510` mnemonic set (
|
|
46
|
+
// `!cpu 6510` mnemonic set (`src/skills/acme-build/SKILL.md`: `lax dcp
|
|
47
47
|
// sax slo rla sre rra isc anc alr arr sbx las tas sha shx shy jam`). This
|
|
48
48
|
// table follows ACME's names, not cc65's, at exactly these two opcodes, so
|
|
49
49
|
// that `acmeExpressible` below can be computed by simple set membership
|
|
@@ -100,9 +100,9 @@
|
|
|
100
100
|
// ---------------------------------------------------------------------------
|
|
101
101
|
// WHY THIS FILE EXISTS
|
|
102
102
|
// ---------------------------------------------------------------------------
|
|
103
|
-
// This is the one table the decoder (04-03)
|
|
104
|
-
//
|
|
105
|
-
//
|
|
103
|
+
// This is the one table the decoder (04-03) and Phase 5's backtrace
|
|
104
|
+
// (DERIV-02) read instruction lengths from. GAIN-01's CPU-history decode, a
|
|
105
|
+
// third would-be consumer, was cut with the whole of Stock-Only Gains, 2026-08-17 (ROADMAP.md). A wrong length here silently desynchronises every instruction after
|
|
106
106
|
// it in every one of those consumers (criterion 2's own wording) -- there is
|
|
107
107
|
// no runtime check downstream that would catch a transcription typo on its
|
|
108
108
|
// own; that is what `disasm-opcodes.test.ts`'s independent bit-pattern
|
package/disasm-renderer.ts
CHANGED
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
// `stock-disassemble.ts` (04-05) wires the real symbol resolver in via
|
|
8
8
|
// `RenderOptions.symbolFor`; this file never imports `stock-address.ts`
|
|
9
9
|
// itself, so it stays importable by anything that only has an
|
|
10
|
-
// `Instruction[]` in hand
|
|
11
|
-
//
|
|
10
|
+
// `Instruction[]` in hand. Both originally-planned such consumers (Phase 5's
|
|
11
|
+
// backtrace, a CPU-history decode) were cut from v0.2.0 scope on 2026-08-17.
|
|
12
12
|
//
|
|
13
13
|
// ---------------------------------------------------------------------------
|
|
14
14
|
// WHY THIS FILE EXISTS
|
package/hostpath.ts
CHANGED
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
// towards mechanism 1.
|
|
29
29
|
//
|
|
30
30
|
// Node >= 18. No dependencies, no network, nothing machine-specific: copy this
|
|
31
|
-
// file into any devcontainer-based project's
|
|
31
|
+
// file into any devcontainer-based project's src/mcp/vice/ and it works.
|
|
32
32
|
//
|
|
33
33
|
// HOSTING CHOICE (01.6.1-02, Criterion B / RESEARCH §3.4 Option B): this
|
|
34
34
|
// module takes the workspace root as an ARGUMENT (an optional `workspaceRoot`
|
package/install-resources.ts
CHANGED
|
@@ -78,7 +78,7 @@ export interface PruneResourcesResult {
|
|
|
78
78
|
export type ResourceStatus = "missing" | "present" | "diverged";
|
|
79
79
|
|
|
80
80
|
/** This module directory's resources/ subdirectory -- a plain SIBLING of this
|
|
81
|
-
* module (scripts/ was flattened away in the
|
|
81
|
+
* module (scripts/ was flattened away in the src/mcp/vice/ move), the
|
|
82
82
|
* tracked source of truth every deployed tools/ file is copied from. Getting
|
|
83
83
|
* this hop wrong is silent and total: readdirSync() throws inside
|
|
84
84
|
* resourceEntries(), but ensureResourcesInstalled() catches everything by
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@henols/vice-mcp",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"description": "VICE emulator MCP server for C64 reverse-engineering: a stdio MCP server that proxies vice tools to a host VICE MCP server.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -53,20 +53,26 @@
|
|
|
53
53
|
"stock-run-until.ts",
|
|
54
54
|
"stock-diagnose.ts",
|
|
55
55
|
"stock-recycle.ts",
|
|
56
|
-
"
|
|
57
|
-
"
|
|
58
|
-
"
|
|
59
|
-
"
|
|
60
|
-
"
|
|
61
|
-
"
|
|
62
|
-
"
|
|
63
|
-
"
|
|
64
|
-
"
|
|
65
|
-
"
|
|
66
|
-
"
|
|
67
|
-
"
|
|
68
|
-
"
|
|
69
|
-
"
|
|
56
|
+
"anno-d64.ts",
|
|
57
|
+
"anno-cli.ts",
|
|
58
|
+
"anno-symbols.ts",
|
|
59
|
+
"anno-regbits-gen.ts",
|
|
60
|
+
"anno-regbits.json",
|
|
61
|
+
"anno-enum-gen.ts",
|
|
62
|
+
"anno-acme-ident.ts",
|
|
63
|
+
"anno-confidence.ts",
|
|
64
|
+
"anno-memmap-render.ts",
|
|
65
|
+
"anno-export-asm.ts",
|
|
66
|
+
"anno-coverage.ts",
|
|
67
|
+
"block-class.ts",
|
|
68
|
+
"prg-image.ts",
|
|
69
|
+
"anno-types.ts",
|
|
70
|
+
"anno-index.ts",
|
|
71
|
+
"anno-store.ts",
|
|
72
|
+
"anno-tools.ts",
|
|
73
|
+
"anno-derive.ts",
|
|
74
|
+
"anno-details.ts",
|
|
75
|
+
"anno-register.ts",
|
|
70
76
|
"resources",
|
|
71
77
|
"tools-manifest.json",
|
|
72
78
|
"tools-manifest.stock.json",
|
|
@@ -74,7 +80,7 @@
|
|
|
74
80
|
"THIRD-PARTY-NOTICES.md"
|
|
75
81
|
],
|
|
76
82
|
"engines": {
|
|
77
|
-
"node": ">=
|
|
83
|
+
"node": ">=24.0.0"
|
|
78
84
|
},
|
|
79
85
|
"publishConfig": {
|
|
80
86
|
"access": "public"
|
|
@@ -87,7 +93,7 @@
|
|
|
87
93
|
"repository": {
|
|
88
94
|
"type": "git",
|
|
89
95
|
"url": "git+https://github.com/henols/c64-re-tools.git",
|
|
90
|
-
"directory": "
|
|
96
|
+
"directory": "src/mcp/vice"
|
|
91
97
|
},
|
|
92
98
|
"homepage": "https://github.com/henols/c64-re-tools#readme",
|
|
93
99
|
"bugs": {
|
package/prg-image.ts
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// prg-image.ts -- the ONE authoritative place in this repo holding pure C64
|
|
3
|
+
// image byte-layout knowledge: how a `.prg` splits into a load address plus a
|
|
4
|
+
// body, what load address a flat 64K RAM capture has, and how to invert a
|
|
5
|
+
// gzip-then-base64 payload back into bytes. These are facts about C64 file
|
|
6
|
+
// formats and about one payload encoding -- they depend on no external
|
|
7
|
+
// analyser, no emulator, and no annotation store, so they belong in a module
|
|
8
|
+
// none of those can take with them when it goes away.
|
|
9
|
+
//
|
|
10
|
+
// This module performs NO filesystem and NO network I/O: every function takes
|
|
11
|
+
// bytes (or a base64 string) and returns values. Callers obtain and persist
|
|
12
|
+
// the bytes themselves. That is the same claim `anno-project.ts` makes about
|
|
13
|
+
// itself, and it must remain true of both files now that the split has
|
|
14
|
+
// happened -- a structural test in `prg-image.test.ts` asserts it from this
|
|
15
|
+
// module's own source rather than trusting this paragraph.
|
|
16
|
+
//
|
|
17
|
+
// WHY THIS FILE EXISTS SEPARATELY: these three functions used to live in
|
|
18
|
+
// `anno-project.ts`, the module that builds a `.regen2000proj` file for the
|
|
19
|
+
// the external analyser analyser. They were never about that analyser. One of them
|
|
20
|
+
// is imported statically by the byte-coverage census (`anno-coverage.ts`), a
|
|
21
|
+
// capability that must keep working independently of whether this repo still
|
|
22
|
+
// drives that analyser at all -- so a census whose only route to a payload
|
|
23
|
+
// decoder ran through analyser glue was one deletion away from breaking with
|
|
24
|
+
// no announcement (SEAM-02). Extracting them under a name that carries no
|
|
25
|
+
// analyser prefix removes that coupling outright instead of recording it as a
|
|
26
|
+
// hazard to remember later. There is deliberately NO re-export left behind in
|
|
27
|
+
// `anno-project.ts`: a compatibility shim would leave the coupling fully
|
|
28
|
+
// intact while looking finished.
|
|
29
|
+
//
|
|
30
|
+
// THIS MODULE MUST BE LISTED IN `package.json`'s `files[]`. It is reachable
|
|
31
|
+
// from the published entry point's import closure, and the STATIC route is
|
|
32
|
+
// named here first because it is the stronger reachability claim: `anno-
|
|
33
|
+
// tools.ts` -- the curated `anno_*` MCP tool surface -- imports `parsePrg` and
|
|
34
|
+
// `flatImageOrigin` from here with a plain top-level import, and `vice-proxy.ts`
|
|
35
|
+
// imports `anno-tools.ts` statically. `anno-coverage.ts` (the byte-coverage
|
|
36
|
+
// census) imports `decodeRawData`, `parsePrg` and `flatImageOrigin` for the
|
|
37
|
+
// same three facts on the CLI's route. `scripts/check-npm-packages.mjs` walks
|
|
38
|
+
// that closure over `files[]` and fails the pack the moment a reachable module
|
|
39
|
+
// sits outside the listed set, exactly as `anno-d64.ts`'s own header records
|
|
40
|
+
// for the same reason.
|
|
41
|
+
//
|
|
42
|
+
// CORRECTED 2026-08-30 (WR-07, plan 29-16). This paragraph previously named
|
|
43
|
+
// `anno-cli.ts` as importing `parsePrg` and `flatImageOrigin` and rested the
|
|
44
|
+
// whole reachability claim on the DYNAMIC import that reaches that file. That
|
|
45
|
+
// was doubly wrong: `anno-cli.ts` imported neither symbol (it imported
|
|
46
|
+
// `decodeRawData` only), and it now imports nothing from here at all -- its
|
|
47
|
+
// image decode delegates to `anno-coverage.ts`'s `loadProjectImage()`. A
|
|
48
|
+
// stated reason for shipping a file has to be true or it is worse than absent.
|
|
49
|
+
//
|
|
50
|
+
// WHAT NOT TO DO:
|
|
51
|
+
// - Never give any function here a filesystem PATH parameter. They take byte
|
|
52
|
+
// arrays and a base64 string, which is precisely what keeps path traversal
|
|
53
|
+
// out of this module's threat surface entirely. Path resolution belongs to
|
|
54
|
+
// the CLI. For the same reason this module imports nothing from either of
|
|
55
|
+
// this repo's two host/container path-translation seams; that absence is
|
|
56
|
+
// asserted structurally by `hostpath-consumers.test.ts`, not merely stated
|
|
57
|
+
// here.
|
|
58
|
+
// - Never relax, reword or reorder either input refusal below. The concrete
|
|
59
|
+
// incident: a 4096-byte flat `.raw` capture fell through to the `.prg`
|
|
60
|
+
// parser, whose first two bytes become the load address, so a truncated
|
|
61
|
+
// capture silently "bootstrapped" with an origin read backwards out of its
|
|
62
|
+
// own payload bytes and exited zero -- every downstream address wrong, no
|
|
63
|
+
// diagnostic. The refusal message texts are a user-visible contract: both
|
|
64
|
+
// `anno-tools.ts`'s `loadImage()` and `anno-coverage.ts`'s
|
|
65
|
+
// `loadProjectImage()` prefix them with the caller's own image path, and
|
|
66
|
+
// tests on both routes match on their wording, so a reworded message
|
|
67
|
+
// breaks a test for a reason that looks unrelated. (Attribution corrected
|
|
68
|
+
// 2026-08-30, WR-07: this line named `anno-cli.ts`, which prefixed them
|
|
69
|
+
// through a `bootstrap` verb deleted by D-14 on 2026-08-29.)
|
|
70
|
+
// - Never add the dispatch ORDER discipline here. Which check runs first for
|
|
71
|
+
// a given input extension belongs to the two loaders that own it --
|
|
72
|
+
// `anno-tools.ts`'s `loadImage()` for the MCP tool surface and
|
|
73
|
+
// `anno-coverage.ts`'s `loadProjectImage()` for the coverage verb. Both
|
|
74
|
+
// dispatch `.raw`/`.bin` by extension BEFORE any length check, so that
|
|
75
|
+
// `flatImageOrigin`'s named refusal is always reachable for those two
|
|
76
|
+
// extensions. Neither function below may start inferring what kind of
|
|
77
|
+
// image it was handed.
|
|
78
|
+
|
|
79
|
+
import { gunzipSync } from "node:zlib";
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Parses a `.prg` file: a little-endian 2-byte load address followed by the
|
|
83
|
+
* payload bytes. This is the C64 program-file convention every C64 loader
|
|
84
|
+
* (and this project's own `acme-build` output) already follows.
|
|
85
|
+
*/
|
|
86
|
+
export function parsePrg(bytes: Uint8Array): { origin: number; body: Uint8Array } {
|
|
87
|
+
if (bytes.length < 3) {
|
|
88
|
+
throw new Error(
|
|
89
|
+
`parsePrg: input is ${bytes.length} byte(s) -- a .prg needs at least 3 bytes (2-byte load address plus at least 1 payload byte)`,
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
const origin = bytes[0]! | (bytes[1]! << 8);
|
|
93
|
+
const body = bytes.subarray(2);
|
|
94
|
+
return { origin, body };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Returns the load address (`0`) for a flat 64K RAM capture, and throws for
|
|
99
|
+
* anything else. Flat 64K is in scope because `ANNO-06` names it directly
|
|
100
|
+
* and it is exactly the shape `c64-ram-capture` already produces (D-03) --
|
|
101
|
+
* this function does not attempt to support any other flat-image size.
|
|
102
|
+
*/
|
|
103
|
+
export function flatImageOrigin(bytes: Uint8Array): number {
|
|
104
|
+
if (bytes.length !== 65536) {
|
|
105
|
+
throw new Error(
|
|
106
|
+
`flatImageOrigin: input is ${bytes.length} byte(s) -- a flat 64K capture must be exactly 65536 bytes`,
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
return 0;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* The inverse of a gzip-then-base64 payload encoding: base64-decode, then
|
|
114
|
+
* gunzip. Exported so tests can prove the payload round-trips exactly,
|
|
115
|
+
* rather than asserting against an opaque blob.
|
|
116
|
+
*/
|
|
117
|
+
export function decodeRawData(base64: string): Uint8Array {
|
|
118
|
+
return gunzipSync(Buffer.from(base64, "base64"));
|
|
119
|
+
}
|
package/repo-root.ts
CHANGED
|
@@ -39,6 +39,19 @@
|
|
|
39
39
|
// `scripts/` segment. Branch 4's hop count below moved from four levels to
|
|
40
40
|
// three to match. Branches 1-3 are depth-independent (an env var check, then
|
|
41
41
|
// a `.git` ancestor walk) and needed no change.
|
|
42
|
+
//
|
|
43
|
+
// FOURTH MOVE (phase 16-04, 2026-08-23): the module directory relocated
|
|
44
|
+
// again, from `.claude/mcp/vice/` to `src/mcp/vice/` (packaging and repo
|
|
45
|
+
// shape). `src`/`mcp`/`vice` is the same three path segments below the
|
|
46
|
+
// repository root as the old `.claude`/`mcp`/`vice` shape -- this is
|
|
47
|
+
// reviewed and confirmed, not assumed, from the segment count itself. Branch
|
|
48
|
+
// 4's hop count was therefore reviewed and deliberately left unchanged: this
|
|
49
|
+
// move relocates the same flat, three-segment shape elsewhere directly under
|
|
50
|
+
// the root, which is a different move from nesting authored sources one
|
|
51
|
+
// level deeper INSIDE this directory (which would make four, and IS what
|
|
52
|
+
// branches 1-3's depth-independence and branch 4's fixed hop count would not
|
|
53
|
+
// survive). See repo-root.test.ts's own standing caution for that
|
|
54
|
+
// distinction, drawn explicitly there for the first time by this move.
|
|
42
55
|
import { existsSync } from "node:fs";
|
|
43
56
|
import { fileURLToPath } from "node:url";
|
|
44
57
|
import { dirname, join, resolve, sep } from "node:path";
|
|
@@ -62,6 +75,8 @@ let warnedNoMarkerFound = false;
|
|
|
62
75
|
export interface RepoRootOptions {
|
|
63
76
|
from?: string;
|
|
64
77
|
env?: NodeJS.ProcessEnv;
|
|
78
|
+
/** Test seam for the marker walk; production uses node:fs existsSync. */
|
|
79
|
+
exists?: (path: string) => boolean;
|
|
65
80
|
}
|
|
66
81
|
|
|
67
82
|
/**
|
|
@@ -84,7 +99,7 @@ function isInside(child: string, parent: string): boolean {
|
|
|
84
99
|
* Claude Code exports for the workspace it is driving. This is the ONLY
|
|
85
100
|
* branch that is correct when this module is consumed as an installed
|
|
86
101
|
* plugin: the MCP's own files then live under the plugin install dir
|
|
87
|
-
* (e.g. `~/.claude/plugins/<marketplace>/<plugin
|
|
102
|
+
* (e.g. `~/.claude/plugins/<marketplace>/<plugin>/src/mcp/vice/`),
|
|
88
103
|
* NOT inside the project the user is working in, so neither the
|
|
89
104
|
* `from`-relative `.git` walk (branch 2, which would find the plugin's
|
|
90
105
|
* OWN checkout) nor a CONTAINER_WORKSPACE_PATH containment check
|
|
@@ -108,13 +123,13 @@ function isInside(child: string, parent: string): boolean {
|
|
|
108
123
|
* be exactly the quiet-wrong-answer failure class this file exists to
|
|
109
124
|
* prevent, so this path emits a one-time stderr note naming both paths.
|
|
110
125
|
* 4. Otherwise, three levels up from `from`, with a one-time stderr note.
|
|
111
|
-
* Last resort only -- three levels is what `<root
|
|
126
|
+
* Last resort only -- three levels is what `<root>/src/mcp/<server>/`
|
|
112
127
|
* implies. In this repo branch 4 never actually runs (there is always a
|
|
113
128
|
* `.git` ancestor), which is exactly why the paired synthetic test in
|
|
114
129
|
* repo-root.test.ts is the only thing that would catch a wrong hop
|
|
115
130
|
* count here.
|
|
116
131
|
*/
|
|
117
|
-
export function repoRoot({ from = HERE, env = process.env }: RepoRootOptions = {}): string {
|
|
132
|
+
export function repoRoot({ from = HERE, env = process.env, exists = existsSync }: RepoRootOptions = {}): string {
|
|
118
133
|
// Branch 0 (plugin-consumption signal): Claude Code sets CLAUDE_PROJECT_DIR
|
|
119
134
|
// to the root of the workspace it is driving. When this module runs as an
|
|
120
135
|
// installed plugin its own files sit outside that workspace, so this is the
|
|
@@ -134,7 +149,7 @@ export function repoRoot({ from = HERE, env = process.env }: RepoRootOptions = {
|
|
|
134
149
|
|
|
135
150
|
let dir = resolve(from);
|
|
136
151
|
while (true) {
|
|
137
|
-
if (
|
|
152
|
+
if (exists(join(dir, ".git"))) {
|
|
138
153
|
return dir;
|
|
139
154
|
}
|
|
140
155
|
const parent = dirname(dir);
|
|
@@ -160,7 +175,7 @@ export function repoRoot({ from = HERE, env = process.env }: RepoRootOptions = {
|
|
|
160
175
|
const fallback = resolve(from, "..", "..", "..");
|
|
161
176
|
console.error(
|
|
162
177
|
`warn: could not find a .git ancestor above ${from} and CONTAINER_WORKSPACE_PATH is not set -- ` +
|
|
163
|
-
`falling back to three levels up (${fallback}), the shape <root
|
|
178
|
+
`falling back to three levels up (${fallback}), the shape <root>/src/mcp/<server>/ implies. ` +
|
|
164
179
|
`This is a last resort; if it's wrong, set CONTAINER_WORKSPACE_PATH or run from inside a git repo.`
|
|
165
180
|
);
|
|
166
181
|
}
|
|
@@ -101,10 +101,14 @@ let warnedRemoteMonitorBindWidened = false;
|
|
|
101
101
|
* the binmon address already used -- one resolution, not two. When
|
|
102
102
|
* `remoteMonitorPort` is omitted (undefined), the returned argv is
|
|
103
103
|
* byte-identical to what this function always returned -- no
|
|
104
|
-
* `-remotemonitor` at all. `-remotemonitoraddress`'s exact spelling
|
|
105
|
-
*
|
|
106
|
-
* Assumption A1
|
|
107
|
-
*
|
|
104
|
+
* `-remotemonitor` at all. `-remotemonitoraddress`'s exact spelling was
|
|
105
|
+
* live-probed against a real fork-3.10 binary and CONFIRMED (RESEARCH.md
|
|
106
|
+
* Assumption A1, closed by 13-PROBE-RESULTS.md §A1: the flag bound a real,
|
|
107
|
+
* accepting text-monitor listener, corroborated independently by
|
|
108
|
+
* `ss -ltnp`). Genuine stock 3.9 was not independently probed in that run --
|
|
109
|
+
* the spelling itself is a symmetrical CLI flag pair and is not
|
|
110
|
+
* version-sensitive, so this is recorded as a low-risk carry-forward rather
|
|
111
|
+
* than implied stock-3.9 coverage. Widening THIS bind away from loopback emits
|
|
108
112
|
* its own one-time stderr note (`warnedRemoteMonitorBindWidened`), naming
|
|
109
113
|
* the resolved address and stating that VICE's TEXT monitor accepts
|
|
110
114
|
* arbitrary monitor commands and is unauthenticated -- Phase 3 dials
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
-
#
|
|
2
|
+
# src/mcp/vice/resources/vice-launcher.sh
|
|
3
3
|
#
|
|
4
4
|
# HAND-AUTHORED -- not generated. It lives beside the generated vice-broker.mjs
|
|
5
5
|
# purely because install-resources.mjs deploys the whole resources/ directory
|
|
@@ -38,7 +38,7 @@ SELF_DIR="$(cd "$(dirname "$SELF_PATH")" && pwd)"
|
|
|
38
38
|
#
|
|
39
39
|
# WHY THIS FUNCTION EXISTS AT ALL: a fixed `".."` hop (`REPO_ROOT="$(cd
|
|
40
40
|
# "$(dirname "$SELF_PATH")/.." && pwd)"`) is wrong from this launcher's own
|
|
41
|
-
# location,
|
|
41
|
+
# location, `src/mcp/vice/resources/` -- four levels below the repo root,
|
|
42
42
|
# not one. NOTHING would error on a wrong fixed hop count: the script would
|
|
43
43
|
# just read a permanently-empty `.vice-supervisor` state directory forever,
|
|
44
44
|
# and restart detection would quietly stop working while every command kept
|
|
@@ -73,7 +73,7 @@ _REPO_ROOT_WARNED_NO_MARKER=0
|
|
|
73
73
|
# paths.
|
|
74
74
|
# 4. Otherwise, a location-shaped last resort, also with a one-time stderr
|
|
75
75
|
# note: FOUR levels up when <absolute-dir>'s own directory is named
|
|
76
|
-
# `resources` (matching `<root
|
|
76
|
+
# `resources` (matching `<root>/src/mcp/vice/resources`),
|
|
77
77
|
# ONE level up otherwise.
|
|
78
78
|
resolve_repo_root() {
|
|
79
79
|
local from="$1" dir parent base
|