@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.
- package/README.md +2 -2
- package/THIRD-PARTY-NOTICES.md +422 -1
- package/anno-bank.ts +171 -0
- package/anno-cli.ts +1736 -163
- package/anno-confidence.ts +2 -2
- package/anno-derive.ts +6 -6
- package/anno-details.ts +4 -4
- package/anno-enum-gen.ts +416 -30
- package/anno-export-asm.ts +1211 -126
- package/anno-graphics.ts +338 -0
- package/anno-hazard-report.ts +1367 -0
- package/anno-import.ts +495 -0
- package/anno-index.ts +8 -8
- package/anno-join.ts +480 -0
- package/anno-memmap-render.ts +22 -21
- package/anno-provenance-ledger.ts +472 -0
- package/anno-regbits-gen.ts +13 -13
- package/anno-register.ts +159 -0
- package/anno-store-export.ts +661 -0
- package/anno-store.ts +635 -124
- package/anno-symbols.ts +7 -7
- package/anno-tools.ts +1169 -16
- package/anno-types.ts +313 -40
- package/backend-detect.mts +124 -312
- package/build.ts +3 -1
- package/capture-predicate.ts +597 -0
- package/channel-lock.ts +349 -0
- package/evid-ingest.ts +217 -0
- package/evid-reconcile.ts +316 -0
- package/host-tool-client.ts +430 -0
- package/incident-record.ts +23 -12
- package/install-resources.ts +29 -13
- package/memmap-lookup.ts +285 -0
- package/package.json +27 -8
- package/prg-image.ts +1 -2
- package/repo-root.ts +87 -3
- package/resources/backend-detect.mjs +98 -236
- package/resources/broker-control.mjs +220 -54
- package/resources/broker-epoch.mjs +7 -8
- package/resources/broker-kill.mjs +36 -31
- package/resources/broker-launch.mjs +511 -374
- package/resources/broker-state.mjs +69 -24
- package/resources/container-guard.mjs +1 -1
- package/resources/ghidra-project.mjs +790 -0
- package/resources/host-tool.mjs +2533 -0
- package/resources/vice-broker.mjs +434 -290
- package/resources/vice-launcher.sh +127 -9
- package/stock-address.ts +1 -1
- package/stock-condition.ts +1 -1
- package/stock-connect.ts +9 -5
- package/stock-derived.ts +29 -37
- package/stock-diagnose.ts +200 -36
- package/stock-dispatch.ts +179 -77
- package/stock-handler.ts +1 -1
- package/stock-paths.ts +18 -14
- package/stock-petscii.ts +1 -1
- package/stock-protocol.ts +1 -1
- package/stock-recycle.ts +83 -2
- package/stock-reproducible-run.ts +811 -0
- package/stock-run-until.ts +100 -1
- package/stock-symbols.ts +4 -4
- package/stock-timing.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/vice-broker-client.ts +253 -108
- package/vice-errors.ts +268 -0
- package/vice-proxy.ts +339 -2144
- package/vsf-slice.ts +640 -0
- package/anno-d64.ts +0 -310
- package/capability-registry.ts +0 -390
- 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/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
|
-
}
|