@henols/vice-mcp 0.2.2 → 0.2.4

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 (84) hide show
  1. package/README.md +2 -2
  2. package/THIRD-PARTY-NOTICES.md +422 -1
  3. package/anno-bank.ts +171 -0
  4. package/anno-cli.ts +1736 -163
  5. package/anno-confidence.ts +2 -2
  6. package/anno-derive.ts +6 -6
  7. package/anno-details.ts +4 -4
  8. package/anno-enum-gen.ts +416 -30
  9. package/anno-export-asm.ts +1211 -126
  10. package/anno-graphics.ts +338 -0
  11. package/anno-hazard-report.ts +1367 -0
  12. package/anno-import.ts +495 -0
  13. package/anno-index.ts +8 -8
  14. package/anno-join.ts +480 -0
  15. package/anno-memmap-render.ts +22 -21
  16. package/anno-provenance-ledger.ts +472 -0
  17. package/anno-regbits-gen.ts +13 -13
  18. package/anno-register.ts +159 -0
  19. package/anno-store-export.ts +661 -0
  20. package/anno-store.ts +635 -124
  21. package/anno-symbols.ts +7 -7
  22. package/anno-tools.ts +1169 -16
  23. package/anno-types.ts +313 -40
  24. package/backend-detect.mts +124 -312
  25. package/build.ts +3 -1
  26. package/capture-predicate.ts +597 -0
  27. package/channel-lock.ts +349 -0
  28. package/evid-ingest.ts +217 -0
  29. package/evid-reconcile.ts +316 -0
  30. package/host-tool-client.ts +430 -0
  31. package/incident-record.ts +23 -12
  32. package/install-resources.ts +29 -13
  33. package/memmap-lookup.ts +285 -0
  34. package/package.json +27 -8
  35. package/prg-image.ts +1 -2
  36. package/repo-root.ts +87 -3
  37. package/resources/backend-detect.mjs +98 -236
  38. package/resources/broker-control.mjs +220 -54
  39. package/resources/broker-epoch.mjs +7 -8
  40. package/resources/broker-kill.mjs +36 -31
  41. package/resources/broker-launch.mjs +511 -374
  42. package/resources/broker-state.mjs +69 -24
  43. package/resources/container-guard.mjs +1 -1
  44. package/resources/ghidra-project.mjs +790 -0
  45. package/resources/host-tool.mjs +2533 -0
  46. package/resources/vice-broker.mjs +434 -290
  47. package/resources/vice-launcher.sh +127 -9
  48. package/stock-address.ts +1 -1
  49. package/stock-condition.ts +1 -1
  50. package/stock-connect.ts +9 -5
  51. package/stock-derived.ts +29 -37
  52. package/stock-diagnose.ts +200 -36
  53. package/stock-dispatch.ts +179 -77
  54. package/stock-handler.ts +1 -1
  55. package/stock-paths.ts +18 -14
  56. package/stock-petscii.ts +1 -1
  57. package/stock-protocol.ts +1 -1
  58. package/stock-recycle.ts +83 -2
  59. package/stock-reproducible-run.ts +811 -0
  60. package/stock-run-until.ts +100 -1
  61. package/stock-symbols.ts +4 -4
  62. package/stock-timing.ts +1 -1
  63. package/stop-oracle.ts +167 -0
  64. package/text-capability-probe.ts +660 -0
  65. package/text-connect.ts +157 -0
  66. package/text-protocol.ts +810 -0
  67. package/text-tools.ts +778 -0
  68. package/textmon-backtrace.ts +385 -0
  69. package/textmon-cpuhistory.ts +335 -0
  70. package/textmon-memmap.ts +494 -0
  71. package/textmon-profile.ts +458 -0
  72. package/textmon-registers.ts +748 -0
  73. package/tools-manifest.stock.json +864 -3
  74. package/vice-broker-client.ts +253 -108
  75. package/vice-errors.ts +268 -0
  76. package/vice-proxy.ts +339 -2144
  77. package/vsf-slice.ts +640 -0
  78. package/anno-d64.ts +0 -310
  79. package/capability-registry.ts +0 -390
  80. package/refresh-manifest.ts +0 -124
  81. package/tools-manifest.json +0 -1223
  82. package/vice-probe.ts +0 -278
  83. package/vice-sync.ts +0 -336
  84. package/vice.ts +0 -772
package/anno-d64.ts DELETED
@@ -1,310 +0,0 @@
1
- #!/usr/bin/env node
2
- // Pure, offline `.d64` directory listing and named-entry byte extraction --
3
- // the container-side half of D-02's ".d64 is a first-class bootstrap input"
4
- // requirement.
5
- //
6
- // WHY THIS FILE EXISTS HERE, AND NOT AS AN EXTENSION OF
7
- // `src/skills/c64-ram-capture/scripts/d64-parse.mjs`: the researcher's
8
- // own recommendation (RESEARCH.md Open Question #2) was to extend
9
- // `d64-parse.mjs` in place, since it already walks the directory chain. That
10
- // is not reachable in practice: this MCP server ships as `@henols/vice-mcp`,
11
- // whose `files[]` in `package.json` lists only `src/mcp/vice/` contents,
12
- // while `src/skills/**` ships in the *other* package
13
- // (`@henols/c64-re-tools`). An import from this seam into a skill script
14
- // cannot resolve on either npm-installer route (neither copies the sibling
15
- // package's source tree onto disk next to it), and
16
- // `scripts/check-npm-packages.mjs`'s transitive-closure walk over `files[]`
17
- // would fail the pack the moment a reachable module sat outside the listed
18
- // set. So this is a SECOND, independent copy of the sector-chain-walk
19
- // algorithm, container-side, scoped to exactly what the anno bootstrap
20
- // needs -- not a shared library and not an import of the skill-side module.
21
- //
22
- // `d64-parse.mjs` REMAINS the skill-side owner of the algorithm and is left
23
- // entirely untouched by this phase; this module's job is not to grow beyond
24
- // the bootstrap's needs, and neither copy should silently drift into a
25
- // general-purpose disk-image library. If the two ever need to diverge in
26
- // behaviour, that is a deliberate, documented decision, not an accident of
27
- // two files existing.
28
- //
29
- // WHAT NOT TO DO:
30
- // - Never auto-pick a directory entry when the caller does not name one
31
- // (D-02). A silent auto-pick would happily hand a cracktro or loader
32
- // stub's bytes to the analyser instead of the actual game -- precisely
33
- // the failure `c64-provenance-diff` exists to prevent elsewhere in this
34
- // project. Zero matches and multiple matches both throw here; neither
35
- // returns a guess.
36
- // - Never walk a sector chain (the directory's own, or an entry's) without
37
- // the visited-set cycle guard below. A corrupt or adversarial
38
- // next-track/next-sector pointer must be caught, not looped on.
39
- // - Never call the process's exit hook or any console-printing function
40
- // from this module. The fail-loud "no name given -> print the directory
41
- // listing -> exit non-zero" CLI contract belongs to the caller (plan
42
- // 10-04's CLI seam),
43
- // not here -- this module is a pure, offline byte transform with no
44
- // process-level side effects.
45
- //
46
- // Inherited, documented limits (from `d64-parse.mjs`'s own header, carried
47
- // forward unchanged): plain 174848-byte, 35-track 1541 images only. No
48
- // error-info-byte variant (175531 bytes) and no 40-track variant are
49
- // supported -- `assertPlainImage()` below enforces the 174848-byte length
50
- // and throws naming the actual length otherwise.
51
-
52
- /** The four sector-count zones of a standard 35-track 1541 image. */
53
- export function sectorsPerTrack(track: number): number {
54
- if (!Number.isInteger(track) || track < 1 || track > 35) {
55
- throw new Error(`sectorsPerTrack: track ${track} out of range 1-35`);
56
- }
57
- if (track <= 17) return 21;
58
- if (track <= 24) return 19;
59
- if (track <= 30) return 18;
60
- return 17;
61
- }
62
-
63
- /** Byte offset of the start of {track, sector} in a flat 35-track image. */
64
- export function tsToOffset(track: number, sector: number): number {
65
- if (!Number.isInteger(track) || track < 1 || track > 35) {
66
- throw new Error(`tsToOffset: track ${track} out of range 1-35`);
67
- }
68
- const spt = sectorsPerTrack(track);
69
- if (!Number.isInteger(sector) || sector < 0 || sector >= spt) {
70
- throw new Error(
71
- `tsToOffset: sector ${sector} out of range for track ${track} (0-${spt - 1}, this track has ${spt} sectors)`,
72
- );
73
- }
74
- let offset = 0;
75
- for (let t = 1; t < track; t++) offset += sectorsPerTrack(t) * 256;
76
- return offset + sector * 256;
77
- }
78
-
79
- function isInImage(track: number, sector: number): boolean {
80
- if (!Number.isInteger(track) || track < 1 || track > 35) return false;
81
- if (!Number.isInteger(sector) || sector < 0) return false;
82
- return sector < sectorsPerTrack(track);
83
- }
84
-
85
- /**
86
- * WR-05: bounds every sector read against the actual buffer length, not just
87
- * disk geometry. `isInImage()` above only validates {track, sector} against
88
- * the standard 35-track zone table -- it has no access to the image bytes
89
- * and cannot catch a truncated or non-plain image whose geometry is
90
- * otherwise valid. Without this bound check, a bare subarray read clamps
91
- * silently to whatever bytes exist, and a caller reading past that point
92
- * (e.g. `sec[0]`/`sec[1]` for chain pointers, or the payload slice) reads
93
- * `undefined` and derived garbage with no diagnostic. Used at BOTH sector
94
- * read sites (the directory walk and the file-chain walk) so the bound
95
- * check is part of the walk itself, not a separate opt-in the caller can
96
- * forget -- see `assertPlainImage()` below, which stays a distinct,
97
- * unrelated whole-image-length check for its one existing caller.
98
- */
99
- function sectorSlice(image: Uint8Array, track: number, sector: number, what: string): Uint8Array {
100
- const off = tsToOffset(track, sector);
101
- if (off + 256 > image.length) {
102
- throw new Error(
103
- `${what}: sector ${track}/${sector} needs bytes ${off}..${off + 255} but the image is only ` +
104
- `${image.length} bytes -- truncated or non-plain image`,
105
- );
106
- }
107
- return image.subarray(off, off + 256);
108
- }
109
-
110
- /**
111
- * Directory/disk-name bytes are PETSCII, padded with $A0 OR $00 (WR-06: some
112
- * disk-writing tools, and any image where a slot was partially rewritten,
113
- * pad with NUL instead). As in `d64-parse.mjs`, every byte this project's
114
- * disks actually use in a name (A-Z, digits, space, parens) sits at the same
115
- * code point in PETSCII as in ASCII/Latin-1, so only padding needs
116
- * stripping -- there is no general PETSCII<->ASCII table here, deliberately,
117
- * since one is not needed for what these disks contain.
118
- *
119
- * This function's padding definition MUST agree with the inline
120
- * `isEmptySlot` check in `listEntries()` below, which already treats both
121
- * `0xa0` and `0x00` as filler: a name this function prints (via
122
- * `listEntries()`) must be a name `extractEntry()`'s `--entry` argument can
123
- * select, or the printed listing is a dead end (WR-06's reproduced
124
- * incident).
125
- */
126
- function petsciiName(bytes: Uint8Array): string {
127
- let end = bytes.length;
128
- while (end > 0 && (bytes[end - 1] === 0xa0 || bytes[end - 1] === 0x00)) end--;
129
- return Buffer.from(bytes.subarray(0, end)).toString("latin1");
130
- }
131
-
132
- const FILE_TYPES: Record<number, string> = { 0: "DEL", 1: "SEQ", 2: "PRG", 3: "USR", 4: "REL" };
133
-
134
- export interface D64Entry {
135
- name: string;
136
- type: string;
137
- track: number;
138
- sector: number;
139
- sizeBlocks: number;
140
- }
141
-
142
- /**
143
- * Walk the directory chain from track 18 sector 1, exactly as
144
- * `d64-parse.mjs`'s `parseDirectory()` does, returning the flat listing the
145
- * caller can print. Never picks a "best" or "likely" entry -- that decision
146
- * belongs to the caller and to `extractEntry()`'s exact-name match below.
147
- */
148
- export function listEntries(image: Uint8Array): D64Entry[] {
149
- const entries: D64Entry[] = [];
150
- const visited = new Set<string>();
151
- let track = 18;
152
- let sector = 1;
153
-
154
- for (;;) {
155
- const key = `${track}/${sector}`;
156
- if (visited.has(key)) {
157
- throw new Error(
158
- `listEntries: directory chain revisited ${key} -- stopped to avoid an infinite loop (self-referential or cyclic next-sector pointer)`,
159
- );
160
- }
161
- visited.add(key);
162
- if (!isInImage(track, sector)) {
163
- throw new Error(`listEntries: directory chain pointer ${key} is outside the image -- stopped`);
164
- }
165
-
166
- const sec = sectorSlice(image, track, sector, "listEntries");
167
- const nextTrack = sec[0];
168
- const nextSector = sec[1];
169
-
170
- for (let i = 0; i < 8; i++) {
171
- const e = sec.subarray(i * 32, i * 32 + 32);
172
- const typeByte = e[2];
173
- const firstTrack = e[3];
174
- const firstSector = e[4];
175
- const nameBytes = e.subarray(5, 21);
176
- const blocks = e[30] | (e[31] << 8);
177
-
178
- // An all-zero type byte with a blank/padded name is an unused slot,
179
- // not a file -- never listed as an entry.
180
- const isEmptySlot =
181
- typeByte === 0 && firstTrack === 0 && firstSector === 0 &&
182
- [...nameBytes].every((b) => b === 0xa0 || b === 0x00);
183
- if (isEmptySlot) continue;
184
-
185
- entries.push({
186
- name: petsciiName(nameBytes),
187
- type: FILE_TYPES[typeByte & 0x0f] ?? `unknown(0x${(typeByte & 0x0f).toString(16)})`,
188
- track: firstTrack,
189
- sector: firstSector,
190
- sizeBlocks: blocks,
191
- });
192
- }
193
-
194
- if (nextTrack === 0) break; // end of chain, by DOS convention
195
- track = nextTrack;
196
- sector = nextSector;
197
- }
198
-
199
- return entries;
200
- }
201
-
202
- /**
203
- * Resolve `entryName` against `listEntries(image)` by exact,
204
- * case-insensitive match. Zero matches and multiple matches both throw --
205
- * D-02's whole point is that this function never guesses. On success,
206
- * follows that entry's OWN sector chain (starting at its own
207
- * first_track/first_sector, NOT the fixed directory-chain start) and
208
- * concatenates each sector's 254 payload bytes, honouring the 1541 DOS
209
- * end-of-chain convention: when a sector's next-track byte is 0, its
210
- * next-sector byte instead holds the zero-based offset of the LAST used
211
- * byte in that final 256-byte sector (so the last sector contributes
212
- * `usedByte - 1` payload bytes, not 254 -- the payload runs from byte 2 up
213
- * to and INCLUDING byte `usedByte`).
214
- *
215
- * The returned bytes are the file's RAW content INCLUDING its leading 2-byte
216
- * PRG load address, unmodified -- that is deliberate, since the whole point
217
- * of this module is to hand bytes straight to `parsePrg()` in
218
- * `prg-image.ts`, which expects that same 2-byte header.
219
- */
220
- export function extractEntry(image: Uint8Array, entryName: string): Uint8Array {
221
- const entries = listEntries(image);
222
- const needle = entryName.toLowerCase();
223
- const matches = entries.filter((e) => e.name.toLowerCase() === needle);
224
-
225
- if (matches.length === 0) {
226
- const available = entries.map((e) => e.name).join(", ") || "(no entries)";
227
- throw new Error(
228
- `extractEntry: no entry named "${entryName}" found. Available entries: ${available}`,
229
- );
230
- }
231
- if (matches.length > 1) {
232
- throw new Error(
233
- `extractEntry: entry name "${entryName}" is ambiguous -- ${matches.length} entries share this name (at ` +
234
- `${matches.map((m) => `${m.track}/${m.sector}`).join(", ")}). Refusing to pick one; rename or disambiguate on disk.`,
235
- );
236
- }
237
-
238
- const entry = matches[0];
239
- const chunks: Uint8Array[] = [];
240
- const visited = new Set<string>();
241
- let track = entry.track;
242
- let sector = entry.sector;
243
-
244
- for (;;) {
245
- const key = `${track}/${sector}`;
246
- if (visited.has(key)) {
247
- throw new Error(
248
- `extractEntry: sector chain for "${entryName}" revisited ${key} -- stopped to avoid an infinite loop (self-referential or cyclic next-sector pointer)`,
249
- );
250
- }
251
- visited.add(key);
252
- if (!isInImage(track, sector)) {
253
- throw new Error(`extractEntry: sector chain for "${entryName}" points to ${key}, which is outside the image`);
254
- }
255
-
256
- const sec = sectorSlice(image, track, sector, `extractEntry: "${entryName}"`);
257
- const nextTrack = sec[0];
258
- const nextSector = sec[1];
259
-
260
- if (nextTrack === 0) {
261
- // Last sector: byte 1 is the zero-based offset of the last used byte
262
- // in this 256-byte sector (not a next-sector pointer). Payload runs
263
- // from byte 2 up to and including that offset, so its length is
264
- // `usedByte - 1` -- e.g. usedByte === 255 means the whole sector past
265
- // the 2-byte header is payload (254 bytes), matching a non-final
266
- // sector; a smaller usedByte means fewer payload bytes than that.
267
- // WR-05: a corrupt/truncated `usedByte` of 0 or 1 previously silently
268
- // clamped to a zero-length payload via a floor-at-2 clamp, which
269
- // surfaced downstream only as the confusing
270
- // `parsePrg: input is 0 byte(s)`. Throw here instead, naming the
271
- // sector and the observed value.
272
- const usedByte = nextSector;
273
- if (usedByte < 2) {
274
- throw new Error(
275
- `extractEntry: "${entryName}" final sector ${track}/${sector} reports usedByte ${usedByte}, ` +
276
- "which is less than the minimum valid value of 2 -- corrupt or non-plain image",
277
- );
278
- }
279
- chunks.push(sec.subarray(2, usedByte + 1));
280
- break;
281
- }
282
-
283
- chunks.push(sec.subarray(2, 256));
284
- track = nextTrack;
285
- sector = nextSector;
286
- }
287
-
288
- const total = chunks.reduce((n, c) => n + c.length, 0);
289
- const out = new Uint8Array(total);
290
- let pos = 0;
291
- for (const c of chunks) {
292
- out.set(c, pos);
293
- pos += c.length;
294
- }
295
- return out;
296
- }
297
-
298
- /**
299
- * Enforce the inherited, documented limits: plain 174848-byte, 35-track
300
- * images only. 40-track and error-info-byte (175531-byte) variants are
301
- * deliberately out of scope for this phase, exactly as `d64-parse.mjs`
302
- * documents for the skill-side reader.
303
- */
304
- export function assertPlainImage(image: Uint8Array): void {
305
- if (image.length !== 174848) {
306
- throw new Error(
307
- `assertPlainImage: expected a plain 174848-byte, 35-track .d64 image with no error-info bytes, got ${image.length} bytes`,
308
- );
309
- }
310
- }