@henols/vice-mcp 0.2.1 → 0.2.3
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 +4 -3
- package/THIRD-PARTY-NOTICES.md +423 -25
- package/{r2000-acme-ident.ts → anno-acme-ident.ts} +13 -13
- package/anno-bank.ts +171 -0
- package/anno-cli.ts +3040 -0
- package/{r2000-confidence.ts → anno-confidence.ts} +22 -22
- package/anno-coverage.ts +2465 -0
- package/anno-derive.ts +590 -0
- package/anno-details.ts +169 -0
- package/anno-enum-gen.ts +919 -0
- package/anno-export-asm.ts +2396 -0
- package/anno-graphics.ts +338 -0
- package/anno-hazard-report.ts +1367 -0
- package/anno-import.ts +495 -0
- package/anno-index.ts +150 -0
- package/anno-join.ts +480 -0
- package/{r2000-memmap-render.ts → anno-memmap-render.ts} +236 -95
- package/anno-provenance-ledger.ts +472 -0
- package/{r2000-regbits-gen.ts → anno-regbits-gen.ts} +20 -15
- package/{r2000-regbits.json → anno-regbits.json} +2 -2
- package/anno-register.ts +399 -0
- package/anno-store-export.ts +661 -0
- package/anno-store.ts +4002 -0
- package/anno-symbols.ts +266 -0
- package/anno-tools.ts +3264 -0
- package/anno-types.ts +1909 -0
- package/backend-detect.mts +124 -312
- package/block-class.ts +201 -0
- package/build.ts +4 -2
- package/capture-predicate.ts +597 -0
- package/channel-lock.ts +349 -0
- package/disasm-decoder.ts +14 -14
- package/disasm-opcodes.ts +4 -4
- package/disasm-renderer.ts +2 -2
- package/evid-ingest.ts +217 -0
- package/evid-reconcile.ts +316 -0
- package/host-tool-client.ts +430 -0
- package/hostpath.ts +1 -1
- package/incident-record.ts +23 -12
- package/install-resources.ts +30 -14
- package/memmap-lookup.ts +285 -0
- package/package.json +48 -23
- package/prg-image.ts +118 -0
- package/repo-root.ts +107 -8
- package/resources/backend-detect.mjs +98 -236
- package/resources/broker-control.mjs +189 -16
- package/resources/broker-epoch.mjs +1 -1
- package/resources/broker-kill.mjs +8 -2
- package/resources/broker-launch.mjs +373 -214
- package/resources/broker-state.mjs +64 -18
- package/resources/container-guard.mjs +1 -1
- package/resources/ghidra-project.mjs +790 -0
- package/resources/host-tool.mjs +2561 -0
- package/resources/vice-broker.mjs +330 -184
- package/resources/vice-launcher.sh +130 -12
- package/stock-address.ts +6 -6
- package/stock-cia.ts +2 -2
- package/stock-condition.ts +8 -8
- package/stock-connect.ts +10 -6
- package/stock-derived.ts +29 -37
- package/stock-diagnose.ts +200 -36
- package/stock-dispatch.ts +200 -68
- package/stock-execution.ts +5 -3
- package/stock-handler.ts +1 -1
- package/stock-input.ts +9 -9
- package/stock-machine.ts +17 -6
- package/stock-paths.ts +18 -14
- package/stock-petscii.ts +1 -1
- package/stock-protocol.ts +17 -12
- package/stock-recycle.ts +83 -2
- package/stock-registers.ts +54 -29
- package/stock-reproducible-run.ts +811 -0
- package/stock-run-until.ts +100 -1
- package/stock-sprites.ts +3 -3
- package/stock-symbols.ts +13 -13
- package/stock-timing.ts +2 -2
- package/stock-vicii.ts +1 -1
- package/stop-oracle.ts +167 -0
- package/text-capability-probe.ts +660 -0
- package/text-connect.ts +157 -0
- package/text-protocol.ts +810 -0
- package/text-tools.ts +778 -0
- package/textmon-backtrace.ts +385 -0
- package/textmon-cpuhistory.ts +335 -0
- package/textmon-memmap.ts +494 -0
- package/textmon-profile.ts +458 -0
- package/textmon-registers.ts +748 -0
- package/tools-manifest.stock.json +864 -3
- package/version.ts +1 -1
- package/vice-broker-client.ts +189 -42
- package/vice-errors.ts +268 -0
- package/vice-proxy.ts +392 -2175
- package/vsf-slice.ts +640 -0
- package/capability-registry.ts +0 -388
- package/r2000-cli.ts +0 -1103
- package/r2000-d64.ts +0 -310
- 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/refresh-manifest.ts +0 -124
- package/tools-manifest.json +0 -1223
- package/vice-probe.ts +0 -278
- package/vice-sync.ts +0 -336
- package/vice.ts +0 -772
package/memmap-lookup.ts
ADDED
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// memmap-lookup.ts
|
|
3
|
+
//
|
|
4
|
+
// Phase 37, plan 37-01 (IMP-01/AUTO-01's shared foundation): the ONE loader
|
|
5
|
+
// for `c64-memory-mapping`'s own `memmap.json`, its content digest, and
|
|
6
|
+
// narrowest-containing-range selection over its 959 entries.
|
|
7
|
+
//
|
|
8
|
+
// THIS MODULE MUST NEVER IMPORT `hostpath.ts` OR `containerpath.ts` -- every
|
|
9
|
+
// path here is repo-relative and derived from this module's own location, the
|
|
10
|
+
// same posture `anno-regbits-gen.ts` and `dxa-blocks.ts` take for themselves.
|
|
11
|
+
// It also NEVER NAMES `node:sqlite` and NEVER OPENS THE ANNOTATION STORE:
|
|
12
|
+
// `anno-store.ts` is the one module `anno-seam.test.ts` allows to name that
|
|
13
|
+
// dependency, and this module answers a pure question about a static JSON
|
|
14
|
+
// file that has nothing to do with the store's own persistence.
|
|
15
|
+
//
|
|
16
|
+
// D-37-03: `anno-regbits-gen.ts:62` already computes the SAME `HERE`-relative
|
|
17
|
+
// five-hop path to `memmap.json` and `anno-regbits-gen.ts:377` already
|
|
18
|
+
// computes its sha256 digest, for a different purpose (bit-name generation).
|
|
19
|
+
// This module COPIES that formula rather than importing the generator --
|
|
20
|
+
// `anno-regbits-gen.ts`'s own header declares itself "the sole read of
|
|
21
|
+
// c64-memory-mapping's own memmap.json ... for this purpose", and widening
|
|
22
|
+
// that consumer set is a separate decision this plan does not make.
|
|
23
|
+
//
|
|
24
|
+
// WHAT THIS IS THE ONE AUTHORITATIVE PLACE FOR: loading and caching
|
|
25
|
+
// `memmap.json`'s 959 entries, computing the file's own sha256 provenance
|
|
26
|
+
// digest, and the NARROWEST-CONTAINING-RANGE selection rule an address
|
|
27
|
+
// resolves through. `anno-join.ts` calls this module; nothing else needs to.
|
|
28
|
+
//
|
|
29
|
+
// WHAT NOT TO DO:
|
|
30
|
+
// - Never re-derive `MEMMAP_PATH` from a different relative offset. This
|
|
31
|
+
// formula has already broken once when the skills tree moved (plan
|
|
32
|
+
// 16-01's own deviation note) -- one HERE-relative constant, copied
|
|
33
|
+
// verbatim in shape from `anno-regbits-gen.ts:62`.
|
|
34
|
+
// - Never mutate the cached entries array. `loadMemmap()` returns the same
|
|
35
|
+
// frozen array on every call after the first; a caller that needs a
|
|
36
|
+
// filtered view copies it.
|
|
37
|
+
// - Never derive "is this range bank-conditional" from `memmap.json`'s own
|
|
38
|
+
// fields. `BANK_CONDITIONAL_RANGES` below is HAND-MAINTAINED on purpose
|
|
39
|
+
// (37-RESEARCH.md Pitfall 2) -- the schema carries no structured
|
|
40
|
+
// bank-condition field, only free prose inside `desc`.
|
|
41
|
+
|
|
42
|
+
import { createHash } from "node:crypto";
|
|
43
|
+
import { readFileSync } from "node:fs";
|
|
44
|
+
import { dirname, join } from "node:path";
|
|
45
|
+
import { fileURLToPath } from "node:url";
|
|
46
|
+
|
|
47
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
48
|
+
|
|
49
|
+
/** The `HERE`-relative path to `c64-memory-mapping`'s `memmap.json`, using the
|
|
50
|
+
* SAME five-hop formula `anno-regbits-gen.ts:62` uses. Copied, not imported
|
|
51
|
+
* (D-37-03). */
|
|
52
|
+
export const MEMMAP_PATH = join(HERE, "..", "..", "..", "src", "skills", "c64-memory-mapping", "memmap.json");
|
|
53
|
+
|
|
54
|
+
/** One entry as `memmap.json` holds it. MEASURED at plan time: 959 entries,
|
|
55
|
+
* `start`/`end` are INCLUSIVE integers with no length field, and `sym` is
|
|
56
|
+
* present on 219 of them. */
|
|
57
|
+
export interface MemmapEntry {
|
|
58
|
+
start: number;
|
|
59
|
+
end: number;
|
|
60
|
+
label: string;
|
|
61
|
+
section: string;
|
|
62
|
+
desc: string;
|
|
63
|
+
src: string;
|
|
64
|
+
sym?: string;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
interface MemmapDocument {
|
|
68
|
+
sources: unknown[];
|
|
69
|
+
entries: MemmapEntry[];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Module-level cache, exactly as `anno-enum-gen.ts`'s `loadRegBits()` caches
|
|
73
|
+
* its own generated table -- `memmap.json` is read and parsed once per
|
|
74
|
+
* process, never per call. */
|
|
75
|
+
let cachedEntries: readonly MemmapEntry[] | undefined;
|
|
76
|
+
|
|
77
|
+
/** Reads and parses `memmap.json`, caching the parsed array. Refuses BY NAME
|
|
78
|
+
* if the top-level shape is not `{sources, entries}` or if `entries` is
|
|
79
|
+
* empty -- a malformed or truncated memmap.json must not read as "a memmap
|
|
80
|
+
* with nothing in it". */
|
|
81
|
+
export function loadMemmap(): readonly MemmapEntry[] {
|
|
82
|
+
if (cachedEntries !== undefined) return cachedEntries;
|
|
83
|
+
|
|
84
|
+
let parsed: unknown;
|
|
85
|
+
try {
|
|
86
|
+
parsed = JSON.parse(readFileSync(MEMMAP_PATH, "utf8"));
|
|
87
|
+
} catch (err) {
|
|
88
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
89
|
+
throw new Error(`memmap-lookup: ${MEMMAP_PATH} could not be read/parsed as JSON (${reason}).`);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const doc = parsed as Partial<MemmapDocument> | null;
|
|
93
|
+
if (doc === null || typeof doc !== "object" || !Array.isArray(doc.sources) || !Array.isArray(doc.entries)) {
|
|
94
|
+
throw new Error(
|
|
95
|
+
`memmap-lookup: ${MEMMAP_PATH} is not a { sources, entries } document -- refusing to guess a shape for a ` +
|
|
96
|
+
"malformed memmap.json.",
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
if (doc.entries.length === 0) {
|
|
100
|
+
throw new Error(
|
|
101
|
+
`memmap-lookup: ${MEMMAP_PATH}'s "entries" array is empty -- "a memmap with nothing in it" and "a memmap ` +
|
|
102
|
+
'that failed to load" must not read the same, so this refuses rather than returning an empty selector.',
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
cachedEntries = Object.freeze(doc.entries.slice());
|
|
107
|
+
return cachedEntries;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** The sha256 hex digest of `memmap.json`'s RAW BYTES -- the provenance value
|
|
111
|
+
* `AUTO-08` (a later plan) appends to derived comment text. MEASURED at plan
|
|
112
|
+
* time: this equals the same value `anno-regbits.json`'s committed banner
|
|
113
|
+
* carries (`_generated.memmapSha256`), and the test that pins this asserts
|
|
114
|
+
* that RELATION rather than a hard-coded literal, because the map is a living
|
|
115
|
+
* file. */
|
|
116
|
+
export function memmapDigest(): string {
|
|
117
|
+
return createHash("sha256").update(readFileSync(MEMMAP_PATH)).digest("hex");
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* D-37-13: the fixed, machine-parseable prefix `anno-join.ts` appends
|
|
122
|
+
* `memmapDigest()`'s full 64-character lowercase hex digest to, on every
|
|
123
|
+
* derived comment (`AUTO-08`). Chosen to be unlikely to collide with
|
|
124
|
+
* ordinary comment prose and to sit LAST in the comment text (no closing
|
|
125
|
+
* delimiter follows it), so a regex can find it without knowing the
|
|
126
|
+
* preceding label's own shape: `new RegExp(PROVENANCE_TOKEN_PREFIX_ESCAPED +
|
|
127
|
+
* "[0-9a-f]{64}$")`. Exported from here, not `anno-join.ts`, because the
|
|
128
|
+
* digest and the prefix that names it belong to the same module as
|
|
129
|
+
* `memmapDigest()` itself.
|
|
130
|
+
*/
|
|
131
|
+
export const PROVENANCE_TOKEN_PREFIX = "[memmap-sha256:";
|
|
132
|
+
|
|
133
|
+
/** WHICH of the three steps decided a selection, or `unique` when only one
|
|
134
|
+
* entry contained the address at all. `AUTO-02` names two steps
|
|
135
|
+
* (narrowest-range-wins, then the `sym` tie-break); D-37-10 adds a third,
|
|
136
|
+
* because the phase's own headline `$D020` example is NOT resolved by the
|
|
137
|
+
* first two -- MEASURED at plan time, its two 1-byte contenders both lack a
|
|
138
|
+
* `sym`. Reported rather than left for a caller to infer, so a test (and
|
|
139
|
+
* plan 37-04's controls) can assert WHICH rule decided, not merely what it
|
|
140
|
+
* decided. */
|
|
141
|
+
export type MemmapTieBreak = "unique" | "width" | "symbol" | "order";
|
|
142
|
+
|
|
143
|
+
/** What `selectMemmapEntry()` returns for a unique or a resolved-tie hit.
|
|
144
|
+
* `contenderCount` is the number of containing entries the scan considered,
|
|
145
|
+
* so a caller can tell a unique hit from a resolved tie without re-running
|
|
146
|
+
* the scan itself. */
|
|
147
|
+
export interface MemmapSelection {
|
|
148
|
+
entry: MemmapEntry;
|
|
149
|
+
width: number;
|
|
150
|
+
contenderCount: number;
|
|
151
|
+
tieBrokenBy: MemmapTieBreak;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** Inclusive width of one entry: `end - start`. Named once so every step
|
|
155
|
+
* below computes it identically. */
|
|
156
|
+
function inclusiveWidth(entry: MemmapEntry): number {
|
|
157
|
+
return entry.end - entry.start;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* STEP ONE, WIDTH -- the whole of narrowest-range-wins. Prefers the smallest
|
|
162
|
+
* `end - start`; returns every entry tied at that minimum, since a single
|
|
163
|
+
* winner here is `unique`ly correct only when nothing else shares its width.
|
|
164
|
+
* Kept as its own named function (not folded into a single comparator) so
|
|
165
|
+
* plan 37-04's control -- "switch selection to first-match" -- is a single,
|
|
166
|
+
* small, textual replacement of exactly this step, per this plan's own
|
|
167
|
+
* `<read_first>` instruction.
|
|
168
|
+
*/
|
|
169
|
+
function narrowestWidthSurvivors(containing: readonly MemmapEntry[]): MemmapEntry[] {
|
|
170
|
+
let minWidth = Infinity;
|
|
171
|
+
for (const entry of containing) {
|
|
172
|
+
const width = inclusiveWidth(entry);
|
|
173
|
+
if (width < minWidth) minWidth = width;
|
|
174
|
+
}
|
|
175
|
+
return containing.filter((entry) => inclusiveWidth(entry) === minWidth);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* STEP TWO, SYMBOL -- `AUTO-02`'s own tie-break. Among step one's survivors,
|
|
180
|
+
* prefers an entry carrying a non-empty `sym` over one that does not.
|
|
181
|
+
* D-37-11's `$0000` fixture (three equal-width contenders, exactly one
|
|
182
|
+
* carrying `sym: "D6510"`) is this step's own fixture. Returns every
|
|
183
|
+
* `sym`-carrying survivor when at least one exists, else returns every
|
|
184
|
+
* survivor unchanged (this step decided nothing -- step three must run).
|
|
185
|
+
*/
|
|
186
|
+
function symbolSurvivors(survivors: readonly MemmapEntry[]): MemmapEntry[] {
|
|
187
|
+
const withSym = survivors.filter((entry) => typeof entry.sym === "string" && entry.sym.length > 0);
|
|
188
|
+
return withSym.length > 0 ? withSym : survivors.slice();
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* STEP THREE, ORDER -- D-37-10. Among step two's survivors, the entry
|
|
193
|
+
* appearing FIRST in `memmap.json`'s own `entries` array wins. Stated
|
|
194
|
+
* explicitly, rather than left to whatever order a scan happened to
|
|
195
|
+
* produce, because the phase's own headline `$D020` example needs it: two
|
|
196
|
+
* 1-byte contenders there are tied on width AND neither carries a `sym`, so
|
|
197
|
+
* without this named third rule the selection would be an unstated,
|
|
198
|
+
* scan-order accident that a later refactor could change silently. No
|
|
199
|
+
* `sort()` is used here -- a single linear scan over `entries` (the same
|
|
200
|
+
* order the caller supplied) finds the first survivor, which is cheaper and
|
|
201
|
+
* keeps this step's own mutation (plan 37-05's "reverse the tie-break")
|
|
202
|
+
* a one-line replacement rather than a sort-comparator edit.
|
|
203
|
+
*/
|
|
204
|
+
function orderWinner(survivors: readonly MemmapEntry[], entries: readonly MemmapEntry[]): MemmapEntry {
|
|
205
|
+
for (const entry of entries) {
|
|
206
|
+
if (survivors.includes(entry)) return entry;
|
|
207
|
+
}
|
|
208
|
+
// Unreachable: `survivors` is always drawn from `entries` by reference, so
|
|
209
|
+
// the scan above always finds one before falling through.
|
|
210
|
+
return survivors[0]!;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* The THREE-DEEP selection order (D-37-10): narrowest-containing-range,
|
|
215
|
+
* then the `sym` tie-break (`AUTO-02`), then -- because those two do not
|
|
216
|
+
* resolve every real tie in the committed `memmap.json` -- the stated
|
|
217
|
+
* residual rule of "first in `entries` order". Collects every entry whose
|
|
218
|
+
* inclusive `[start, end]` contains `address`; returns `undefined` if none
|
|
219
|
+
* does. `tieBrokenBy` names WHICH step decided, so a caller (and plan
|
|
220
|
+
* 37-04's controls) can tell width-decided from symbol-decided from
|
|
221
|
+
* order-decided, not merely infer it from the winning entry's own shape.
|
|
222
|
+
*/
|
|
223
|
+
export function selectMemmapEntry(address: number, entries: readonly MemmapEntry[] = loadMemmap()): MemmapSelection | undefined {
|
|
224
|
+
const containing: MemmapEntry[] = [];
|
|
225
|
+
for (const entry of entries) {
|
|
226
|
+
if (address < entry.start || address > entry.end) continue;
|
|
227
|
+
containing.push(entry);
|
|
228
|
+
}
|
|
229
|
+
const contenderCount = containing.length;
|
|
230
|
+
if (contenderCount === 0) return undefined;
|
|
231
|
+
if (contenderCount === 1) {
|
|
232
|
+
const entry = containing[0]!;
|
|
233
|
+
return { entry, width: inclusiveWidth(entry), contenderCount, tieBrokenBy: "unique" };
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const widthSurvivors = narrowestWidthSurvivors(containing);
|
|
237
|
+
if (widthSurvivors.length === 1) {
|
|
238
|
+
const entry = widthSurvivors[0]!;
|
|
239
|
+
return { entry, width: inclusiveWidth(entry), contenderCount, tieBrokenBy: "width" };
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const symSurvivors = symbolSurvivors(widthSurvivors);
|
|
243
|
+
if (symSurvivors.length === 1) {
|
|
244
|
+
const entry = symSurvivors[0]!;
|
|
245
|
+
return { entry, width: inclusiveWidth(entry), contenderCount, tieBrokenBy: "symbol" };
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const entry = orderWinner(symSurvivors, entries);
|
|
249
|
+
return { entry, width: inclusiveWidth(entry), contenderCount, tieBrokenBy: "order" };
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/** One hand-maintained bank-conditional range: its meaning depends on the
|
|
253
|
+
* processor port's bits #0-#2 at `$0001`. */
|
|
254
|
+
export interface BankConditionalRange {
|
|
255
|
+
start: number;
|
|
256
|
+
end: number;
|
|
257
|
+
why: string;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* The three inclusive ranges whose interpretation depends on the processor
|
|
262
|
+
* port, HAND-MAINTAINED rather than derived from `memmap.json` (37-RESEARCH.md
|
|
263
|
+
* Pitfall 2): only 6 of 959 entries even mention the condition in free-text
|
|
264
|
+
* `desc`, and there is no structured bank-condition field to scan instead.
|
|
265
|
+
* Nothing in THIS module branches on this list -- a later plan's bank-decode
|
|
266
|
+
* logic does. Landed here so the list has one home rather than being
|
|
267
|
+
* reinvented per caller.
|
|
268
|
+
*/
|
|
269
|
+
export const BANK_CONDITIONAL_RANGES: readonly BankConditionalRange[] = Object.freeze([
|
|
270
|
+
{
|
|
271
|
+
start: 0xa000,
|
|
272
|
+
end: 0xbfff,
|
|
273
|
+
why: "BASIC ROM when banked in, otherwise RAM (bits #0-#2 of $0001 select between them).",
|
|
274
|
+
},
|
|
275
|
+
{
|
|
276
|
+
start: 0xd000,
|
|
277
|
+
end: 0xdfff,
|
|
278
|
+
why: "I/O area, Character ROM, or RAM depending on $0001 bits #0-#2 -- the canonical three-way case.",
|
|
279
|
+
},
|
|
280
|
+
{
|
|
281
|
+
start: 0xe000,
|
|
282
|
+
end: 0xffff,
|
|
283
|
+
why: "KERNAL ROM when banked in, otherwise RAM (bits #0-#2 of $0001 select between them).",
|
|
284
|
+
},
|
|
285
|
+
]);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@henols/vice-mcp",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3",
|
|
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": {
|
|
@@ -9,23 +9,24 @@
|
|
|
9
9
|
"main": "vice-proxy.ts",
|
|
10
10
|
"files": [
|
|
11
11
|
"vice-proxy.ts",
|
|
12
|
-
"vice.ts",
|
|
13
|
-
"vice-sync.ts",
|
|
14
|
-
"vice-probe.ts",
|
|
12
|
+
"vice-errors.ts",
|
|
15
13
|
"vice-broker-client.ts",
|
|
14
|
+
"host-tool-client.ts",
|
|
16
15
|
"version.ts",
|
|
17
16
|
"stock-protocol.ts",
|
|
18
17
|
"stock-connect.ts",
|
|
18
|
+
"text-protocol.ts",
|
|
19
|
+
"text-connect.ts",
|
|
20
|
+
"text-tools.ts",
|
|
21
|
+
"channel-lock.ts",
|
|
19
22
|
"containerpath.ts",
|
|
20
23
|
"hostpath.ts",
|
|
21
24
|
"repo-root.ts",
|
|
22
25
|
"install-resources.ts",
|
|
23
26
|
"incident-record.ts",
|
|
24
|
-
"refresh-manifest.ts",
|
|
25
27
|
"build.ts",
|
|
26
28
|
"container-guard.mts",
|
|
27
29
|
"backend-detect.mts",
|
|
28
|
-
"capability-registry.ts",
|
|
29
30
|
"stock-dispatch.ts",
|
|
30
31
|
"stock-derived.ts",
|
|
31
32
|
"stock-handler.ts",
|
|
@@ -44,6 +45,12 @@
|
|
|
44
45
|
"disasm-opcodes.ts",
|
|
45
46
|
"disasm-decoder.ts",
|
|
46
47
|
"disasm-renderer.ts",
|
|
48
|
+
"textmon-memmap.ts",
|
|
49
|
+
"textmon-cpuhistory.ts",
|
|
50
|
+
"textmon-backtrace.ts",
|
|
51
|
+
"textmon-profile.ts",
|
|
52
|
+
"textmon-registers.ts",
|
|
53
|
+
"text-capability-probe.ts",
|
|
47
54
|
"stock-memory-search.ts",
|
|
48
55
|
"stock-symbols.ts",
|
|
49
56
|
"stock-vicii.ts",
|
|
@@ -51,30 +58,48 @@
|
|
|
51
58
|
"stock-sprites.ts",
|
|
52
59
|
"stock-timing.ts",
|
|
53
60
|
"stock-run-until.ts",
|
|
61
|
+
"stock-reproducible-run.ts",
|
|
54
62
|
"stock-diagnose.ts",
|
|
55
63
|
"stock-recycle.ts",
|
|
56
|
-
"
|
|
57
|
-
"
|
|
58
|
-
"
|
|
59
|
-
"
|
|
60
|
-
"
|
|
61
|
-
"
|
|
62
|
-
"
|
|
63
|
-
"
|
|
64
|
-
"
|
|
65
|
-
"
|
|
66
|
-
"
|
|
67
|
-
"
|
|
68
|
-
"
|
|
69
|
-
"
|
|
64
|
+
"anno-cli.ts",
|
|
65
|
+
"anno-symbols.ts",
|
|
66
|
+
"anno-regbits-gen.ts",
|
|
67
|
+
"anno-regbits.json",
|
|
68
|
+
"anno-enum-gen.ts",
|
|
69
|
+
"anno-acme-ident.ts",
|
|
70
|
+
"anno-confidence.ts",
|
|
71
|
+
"anno-memmap-render.ts",
|
|
72
|
+
"anno-export-asm.ts",
|
|
73
|
+
"anno-provenance-ledger.ts",
|
|
74
|
+
"anno-coverage.ts",
|
|
75
|
+
"block-class.ts",
|
|
76
|
+
"anno-hazard-report.ts",
|
|
77
|
+
"evid-reconcile.ts",
|
|
78
|
+
"evid-ingest.ts",
|
|
79
|
+
"prg-image.ts",
|
|
80
|
+
"vsf-slice.ts",
|
|
81
|
+
"capture-predicate.ts",
|
|
82
|
+
"stop-oracle.ts",
|
|
83
|
+
"anno-types.ts",
|
|
84
|
+
"anno-index.ts",
|
|
85
|
+
"anno-store.ts",
|
|
86
|
+
"anno-store-export.ts",
|
|
87
|
+
"anno-tools.ts",
|
|
88
|
+
"anno-derive.ts",
|
|
89
|
+
"anno-details.ts",
|
|
90
|
+
"anno-register.ts",
|
|
91
|
+
"anno-import.ts",
|
|
92
|
+
"anno-join.ts",
|
|
93
|
+
"anno-bank.ts",
|
|
94
|
+
"anno-graphics.ts",
|
|
95
|
+
"memmap-lookup.ts",
|
|
70
96
|
"resources",
|
|
71
|
-
"tools-manifest.json",
|
|
72
97
|
"tools-manifest.stock.json",
|
|
73
98
|
"README.md",
|
|
74
99
|
"THIRD-PARTY-NOTICES.md"
|
|
75
100
|
],
|
|
76
101
|
"engines": {
|
|
77
|
-
"node": ">=
|
|
102
|
+
"node": ">=24.0.0"
|
|
78
103
|
},
|
|
79
104
|
"publishConfig": {
|
|
80
105
|
"access": "public"
|
|
@@ -87,7 +112,7 @@
|
|
|
87
112
|
"repository": {
|
|
88
113
|
"type": "git",
|
|
89
114
|
"url": "git+https://github.com/henols/c64-re-tools.git",
|
|
90
|
-
"directory": "
|
|
115
|
+
"directory": "src/mcp/vice"
|
|
91
116
|
},
|
|
92
117
|
"homepage": "https://github.com/henols/c64-re-tools#readme",
|
|
93
118
|
"bugs": {
|
package/prg-image.ts
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
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.
|
|
40
|
+
//
|
|
41
|
+
// CORRECTED 2026-08-30 (WR-07, plan 29-16). This paragraph previously named
|
|
42
|
+
// `anno-cli.ts` as importing `parsePrg` and `flatImageOrigin` and rested the
|
|
43
|
+
// whole reachability claim on the DYNAMIC import that reaches that file. That
|
|
44
|
+
// was doubly wrong: `anno-cli.ts` imported neither symbol (it imported
|
|
45
|
+
// `decodeRawData` only), and it now imports nothing from here at all -- its
|
|
46
|
+
// image decode delegates to `anno-coverage.ts`'s `loadProjectImage()`. A
|
|
47
|
+
// stated reason for shipping a file has to be true or it is worse than absent.
|
|
48
|
+
//
|
|
49
|
+
// WHAT NOT TO DO:
|
|
50
|
+
// - Never give any function here a filesystem PATH parameter. They take byte
|
|
51
|
+
// arrays and a base64 string, which is precisely what keeps path traversal
|
|
52
|
+
// out of this module's threat surface entirely. Path resolution belongs to
|
|
53
|
+
// the CLI. For the same reason this module imports nothing from either of
|
|
54
|
+
// this repo's two host/container path-translation seams; that absence is
|
|
55
|
+
// asserted structurally by `hostpath-consumers.test.ts`, not merely stated
|
|
56
|
+
// here.
|
|
57
|
+
// - Never relax, reword or reorder either input refusal below. The concrete
|
|
58
|
+
// incident: a 4096-byte flat `.raw` capture fell through to the `.prg`
|
|
59
|
+
// parser, whose first two bytes become the load address, so a truncated
|
|
60
|
+
// capture silently "bootstrapped" with an origin read backwards out of its
|
|
61
|
+
// own payload bytes and exited zero -- every downstream address wrong, no
|
|
62
|
+
// diagnostic. The refusal message texts are a user-visible contract: both
|
|
63
|
+
// `anno-tools.ts`'s `loadImage()` and `anno-coverage.ts`'s
|
|
64
|
+
// `loadProjectImage()` prefix them with the caller's own image path, and
|
|
65
|
+
// tests on both routes match on their wording, so a reworded message
|
|
66
|
+
// breaks a test for a reason that looks unrelated. (Attribution corrected
|
|
67
|
+
// 2026-08-30, WR-07: this line named `anno-cli.ts`, which prefixed them
|
|
68
|
+
// through a `bootstrap` verb deleted by D-14 on 2026-08-29.)
|
|
69
|
+
// - Never add the dispatch ORDER discipline here. Which check runs first for
|
|
70
|
+
// a given input extension belongs to the two loaders that own it --
|
|
71
|
+
// `anno-tools.ts`'s `loadImage()` for the MCP tool surface and
|
|
72
|
+
// `anno-coverage.ts`'s `loadProjectImage()` for the coverage verb. Both
|
|
73
|
+
// dispatch `.raw`/`.bin` by extension BEFORE any length check, so that
|
|
74
|
+
// `flatImageOrigin`'s named refusal is always reachable for those two
|
|
75
|
+
// extensions. Neither function below may start inferring what kind of
|
|
76
|
+
// image it was handed.
|
|
77
|
+
|
|
78
|
+
import { gunzipSync } from "node:zlib";
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Parses a `.prg` file: a little-endian 2-byte load address followed by the
|
|
82
|
+
* payload bytes. This is the C64 program-file convention every C64 loader
|
|
83
|
+
* (and this project's own `acme-build` output) already follows.
|
|
84
|
+
*/
|
|
85
|
+
export function parsePrg(bytes: Uint8Array): { origin: number; body: Uint8Array } {
|
|
86
|
+
if (bytes.length < 3) {
|
|
87
|
+
throw new Error(
|
|
88
|
+
`parsePrg: input is ${bytes.length} byte(s) -- a .prg needs at least 3 bytes (2-byte load address plus at least 1 payload byte)`,
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
const origin = bytes[0]! | (bytes[1]! << 8);
|
|
92
|
+
const body = bytes.subarray(2);
|
|
93
|
+
return { origin, body };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Returns the load address (`0`) for a flat 64K RAM capture, and throws for
|
|
98
|
+
* anything else. Flat 64K is in scope because `ANNO-06` names it directly
|
|
99
|
+
* and it is exactly the shape `c64-ram-capture` already produces (D-03) --
|
|
100
|
+
* this function does not attempt to support any other flat-image size.
|
|
101
|
+
*/
|
|
102
|
+
export function flatImageOrigin(bytes: Uint8Array): number {
|
|
103
|
+
if (bytes.length !== 65536) {
|
|
104
|
+
throw new Error(
|
|
105
|
+
`flatImageOrigin: input is ${bytes.length} byte(s) -- a flat 64K capture must be exactly 65536 bytes`,
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
return 0;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* The inverse of a gzip-then-base64 payload encoding: base64-decode, then
|
|
113
|
+
* gunzip. Exported so tests can prove the payload round-trips exactly,
|
|
114
|
+
* rather than asserting against an opaque blob.
|
|
115
|
+
*/
|
|
116
|
+
export function decodeRawData(base64: string): Uint8Array {
|
|
117
|
+
return gunzipSync(Buffer.from(base64, "base64"));
|
|
118
|
+
}
|