@henols/c64-re-tools 1.0.0 → 2.0.0-rc.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.
Files changed (138) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +190 -37
  3. package/distribution/package.ts +36 -0
  4. package/distribution/plugin.ts +120 -0
  5. package/package.json +34 -40
  6. package/skills/c64-assembler/SKILL.md +73 -0
  7. package/skills/c64-assembler/scripts/assemble.ts +103 -0
  8. package/skills/c64-basic/SKILL.md +67 -0
  9. package/skills/c64-basic/scripts/basic.ts +46 -0
  10. package/skills/c64-disk/SKILL.md +79 -0
  11. package/skills/c64-disk/scripts/disk.ts +65 -0
  12. package/skills/c64-emulator/SKILL.md +124 -0
  13. package/skills/c64-knowledge/SKILL.md +102 -0
  14. package/skills/c64-knowledge/scripts/knowledge.ts +257 -0
  15. package/skills/c64-memory-map/SKILL.md +76 -0
  16. package/skills/c64-memory-map/scripts/memmap.ts +81 -0
  17. package/skills/c64-memory-map/scripts/platform.ts +424 -0
  18. package/skills/c64-provenance/SKILL.md +76 -0
  19. package/skills/c64-provenance/scripts/compare.ts +92 -0
  20. package/skills/c64-provenance/scripts/provenance.ts +83 -0
  21. package/skills/c64-reverse-engineering/SKILL.md +90 -0
  22. package/skills/c64-static-analysis/SKILL.md +96 -0
  23. package/skills/c64-static-analysis/scripts/analyze.ts +175 -0
  24. package/skills/c64-static-analysis/scripts/findings.ts +73 -0
  25. package/skills/c64-testing/SKILL.md +79 -0
  26. package/skills/c64-testing/references/scenario-format.md +76 -0
  27. package/skills/c64-testing/scripts/checklist.ts +51 -0
  28. package/skills/c64-testing/scripts/scenario.ts +473 -0
  29. package/skills/c64-testing/scripts/test.ts +55 -0
  30. package/skills/c64-unpacker/SKILL.md +82 -0
  31. package/skills/c64-unpacker/scripts/evidence.ts +135 -0
  32. package/skills/c64-unpacker/scripts/unpack.ts +216 -0
  33. package/src/c64.ts +281 -0
  34. package/src/cli/ap-sdk.ts +44 -0
  35. package/src/cli/cleanup.ts +194 -0
  36. package/src/cli/main.ts +122 -0
  37. package/src/cli/status.ts +34 -0
  38. package/src/host/main.ts +121 -0
  39. package/src/host/server.ts +409 -0
  40. package/src/host/tools/c1541.ts +255 -0
  41. package/src/host/tools/index.ts +31 -0
  42. package/src/host/tools/petcat.ts +70 -0
  43. package/src/host/tools/status.ts +30 -0
  44. package/src/host/tools/version.ts +41 -0
  45. package/src/host/vice/adapter.ts +761 -0
  46. package/src/host/vice/binary-monitor.ts +389 -0
  47. package/src/host/vice/chips.ts +108 -0
  48. package/src/host/vice/connect.ts +34 -0
  49. package/src/host/vice/process.ts +402 -0
  50. package/src/host/vice/screen.ts +183 -0
  51. package/src/host/vice/session.ts +1316 -0
  52. package/src/host/vice/text-monitor.ts +223 -0
  53. package/src/host-client/connect.ts +241 -0
  54. package/src/host-client/tools.ts +89 -0
  55. package/src/host-client/transfer.ts +100 -0
  56. package/src/host-client/vice-session.ts +254 -0
  57. package/src/knowledge/database.ts +132 -0
  58. package/src/knowledge/history.ts +128 -0
  59. package/src/knowledge/import.ts +344 -0
  60. package/src/knowledge/read.ts +191 -0
  61. package/src/knowledge/schema.ts +140 -0
  62. package/src/knowledge/write.ts +348 -0
  63. package/src/mcp/main.ts +109 -0
  64. package/src/mcp/server.ts +307 -0
  65. package/src/mcp/tools/debug.ts +237 -0
  66. package/src/mcp/tools/execution.ts +116 -0
  67. package/src/mcp/tools/input.ts +69 -0
  68. package/src/mcp/tools/machine.ts +97 -0
  69. package/src/mcp/tools/media.ts +106 -0
  70. package/src/mcp/tools/memory.ts +203 -0
  71. package/src/mcp/tools/video.ts +260 -0
  72. package/src/native/acme.ts +116 -0
  73. package/src/native/discover.ts +141 -0
  74. package/src/native/dxa.ts +166 -0
  75. package/src/native/ghidra/analyze.ts +86 -0
  76. package/src/native/ghidra/index.ts +157 -0
  77. package/src/native/ghidra/language/c64rt_6510.cspec +34 -0
  78. package/src/native/ghidra/language/c64rt_6510.ldefs +15 -0
  79. package/src/native/ghidra/language/c64rt_6510.pspec +15 -0
  80. package/src/native/ghidra/language/c64rt_6510.slaspec +596 -0
  81. package/src/native/ghidra/scripts/C64Export.java +242 -0
  82. package/src/native/ghidra/scripts/C64Prepare.java +60 -0
  83. package/src/native/local.ts +34 -0
  84. package/src/native/processes.ts +350 -0
  85. package/src/native/run.ts +172 -0
  86. package/src/native/staging.ts +83 -0
  87. package/src/native/status.ts +101 -0
  88. package/src/native/types.ts +228 -0
  89. package/src/native/watchdog.ts +78 -0
  90. package/src/project.ts +39 -0
  91. package/src/protocol/framing.ts +132 -0
  92. package/src/protocol/messages.ts +252 -0
  93. package/src/protocol/tools.ts +247 -0
  94. package/src/protocol/vice.ts +1235 -0
  95. package/src/script.ts +59 -0
  96. package/src/time.ts +15 -0
  97. package/src/trace.ts +162 -0
  98. package/THIRD-PARTY-NOTICES.md +0 -26
  99. package/bin/cli.mjs +0 -283
  100. package/skills/acme-build/SKILL.md +0 -260
  101. package/skills/acme-build/scripts/acme.mjs +0 -341
  102. package/skills/acme-build/template.a +0 -53
  103. package/skills/c64-disk-access/SKILL.md +0 -159
  104. package/skills/c64-disk-access/scripts/c1541.mjs +0 -569
  105. package/skills/c64-memory-mapping/SKILL.md +0 -631
  106. package/skills/c64-memory-mapping/memmap.json +0 -8800
  107. package/skills/c64-memory-mapping/scripts/driver.mjs +0 -553
  108. package/skills/c64-petcat/SKILL.md +0 -87
  109. package/skills/c64-petcat/scripts/petcat.mjs +0 -221
  110. package/skills/c64-program-recon/SKILL.md +0 -706
  111. package/skills/c64-program-recon/references/control-flow.md +0 -180
  112. package/skills/c64-program-recon/references/graphics.md +0 -79
  113. package/skills/c64-program-recon/references/observation-hazards.md +0 -143
  114. package/skills/c64-program-recon/references/reconstruction.md +0 -133
  115. package/skills/c64-program-recon/references/sound-and-input.md +0 -74
  116. package/skills/c64-program-recon/references/tool-selection.md +0 -78
  117. package/skills/c64-program-recon/scripts/derive.mjs +0 -364
  118. package/skills/c64-program-recon/scripts/packer-finding.mjs +0 -709
  119. package/skills/c64-program-recon/templates/memory-map.template.md +0 -101
  120. package/skills/c64-provenance-diff/SKILL.md +0 -292
  121. package/skills/c64-provenance-diff/scripts/diff-images.mjs +0 -984
  122. package/skills/c64-provenance-diff/scripts/recovery-schema.mjs +0 -395
  123. package/skills/c64-ram-capture/RELEASES.json.example +0 -17
  124. package/skills/c64-ram-capture/SKILL.md +0 -474
  125. package/skills/c64-ram-capture/scripts/compare-cross-binary.mjs +0 -525
  126. package/skills/c64-ram-capture/scripts/compare.mjs +0 -258
  127. package/skills/c64-ram-capture/scripts/derive-transients.mjs +0 -575
  128. package/skills/c64-ram-capture/scripts/dump-artifacts.mjs +0 -317
  129. package/skills/c64-ram-capture/scripts/mcp-module.mjs +0 -174
  130. package/skills/c64-ram-capture/scripts/project-paths.mjs +0 -81
  131. package/skills/c64-ram-capture/scripts/releases.mjs +0 -109
  132. package/skills/c64-ram-capture/scripts/vsf-slice.mjs +0 -147
  133. package/skills/c64-ram-capture/scripts/watch-loads.mjs +0 -581
  134. package/skills/c64-ram-capture/templates/capture-record.template.md +0 -99
  135. package/skills/c64-ram-capture/transients/README.md +0 -137
  136. package/skills/routine-queue-walker/SKILL.md +0 -394
  137. package/skills/routine-queue-walker/scripts/completeness-report.mjs +0 -465
  138. package/skills/vice-wedge-triage/SKILL.md +0 -269
@@ -1,317 +0,0 @@
1
- #!/usr/bin/env node
2
- // The artifact renderer (01-04 Task 1, Part B). This module exists because
3
- // of a structural fact worth stating up front: the executing agent can
4
- // write text, not binary, so the only shape a committable 65536-byte image
5
- // can take under the one permitted route to the emulator is *the agent
6
- // serialises what it fetched via mcp__plugin_c64-re-tools_vice__* tool calls, and a pure
7
- // function renders it*. Every function below takes already-fetched data as
8
- // an argument -- nothing here contacts the emulator.
9
- import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
10
- import { createHash } from "node:crypto";
11
- import { fileURLToPath } from "node:url";
12
- import { dirname, join, resolve, relative } from "node:path";
13
-
14
- import { releaseDir } from "./releases.mjs";
15
- import { projectRoot } from "./project-paths.mjs";
16
- import { addrNum, hex4 } from "./watch-loads.mjs";
17
-
18
- const HERE = dirname(fileURLToPath(import.meta.url));
19
- const REPO_ROOT = projectRoot();
20
-
21
- const die = (m) => { console.error(`error: ${m}`); process.exit(1); };
22
-
23
- function rel(p) {
24
- return relative(REPO_ROOT, p);
25
- }
26
-
27
- // ---------------------------------------------------------------- assembleImage
28
-
29
- /**
30
- * Assemble the ordered `{ address, hex }` chunk records the agent wrote
31
- * after its `mcp__plugin_c64-re-tools_vice__vice_memory_read` calls into a 65536-byte buffer.
32
- * Asserts contiguity from $0000 with no gap and no overlap and a total of
33
- * exactly 65536 bytes, naming the offending address in every failure.
34
- */
35
- export function assembleImage(chunks) {
36
- const sorted = [...chunks].sort((a, b) => addrNum(a.address) - addrNum(b.address));
37
- const bufs = [];
38
- let expected = 0;
39
- for (const c of sorted) {
40
- const addr = addrNum(c.address);
41
- const buf = Buffer.from(c.hex, "hex");
42
- if (addr > expected) {
43
- throw new Error(`assembleImage: gap before address ${hex4(expected)} -- next chunk starts at ${hex4(addr)}`);
44
- }
45
- if (addr < expected) {
46
- throw new Error(`assembleImage: overlap at address ${hex4(addr)} -- a previous chunk already covered up to ${hex4(expected - 1)}`);
47
- }
48
- bufs.push(buf);
49
- expected += buf.length;
50
- }
51
- const total = Buffer.concat(bufs);
52
- if (total.length !== 65536) {
53
- throw new Error(`assembleImage: assembled ${total.length} bytes ending at ${hex4(expected)}, expected exactly 65536`);
54
- }
55
- return total;
56
- }
57
-
58
- /** SHA-256 of a buffer, hex-encoded. `node:crypto` only -- no package added. */
59
- export function sha256Buffer(buf) {
60
- return createHash("sha256").update(buf).digest("hex");
61
- }
62
-
63
- // ----------------------------------------------------------------- vicBank
64
-
65
- /**
66
- * VIC-II bank number (0-3) from CIA2 port A ($DD00)'s low two bits. The
67
- * stored value is the INVERSE of the bank number (c64-memory-mapping skill
68
- * memmap: raw value %00 = "Bank #3" $C000-$FFFF ... %11 = "Bank #0"
69
- * $0000-$3FFF), so bank = 3 - (raw & 3). Verified against
70
- * a committed chip-state sidecar's own recorded
71
- * dd00_raw=193 (0xC1, low bits %01) -> vic_bank=2, which this formula
72
- * reproduces exactly.
73
- */
74
- export function vicBank(dd00Raw) {
75
- return 3 - (dd00Raw & 3);
76
- }
77
-
78
- // --------------------------------------------------------------- screenBase
79
-
80
- /**
81
- * Screen memory base address, derived from $D018's bits 4-7 (screen pointer,
82
- * in 1024-byte units relative to the VIC bank) added to the VIC bank's own
83
- * base (bank * 16384). Verified against the same committed sidecar:
84
- * dd00_raw=193, d018_raw=49 (0x31) -> screen_base=35840, reproduced exactly.
85
- */
86
- export function screenBase(d018Raw, dd00Raw) {
87
- const bank = vicBank(dd00Raw);
88
- const bankBase = bank * 16384;
89
- const screenOffset = ((d018Raw >> 4) & 0xf) * 1024;
90
- return bankBase + screenOffset;
91
- }
92
-
93
- /** Character memory base: $D018 bits 1-3, in 2048-byte units relative to the VIC bank. */
94
- function charsetBase(d018Raw, dd00Raw) {
95
- const bank = vicBank(dd00Raw);
96
- const bankBase = bank * 16384;
97
- const charsetOffset = ((d018Raw >> 1) & 0x7) * 2048;
98
- return bankBase + charsetOffset;
99
- }
100
-
101
- // -------------------------------------------------------------- buildChipState
102
-
103
- /**
104
- * Build the chip-state sidecar in the exact shape the committed
105
- * primary sidecars already use (same top-level keys, same `derived` field
106
- * set), from the register/state readings the agent recorded. `raw` carries
107
- * whatever the agent fetched via vice_registers_get / vice_sprite_get /
108
- * vice_memory_read, keyed to match: `registers`, `sprites`, `cpu` pass
109
- * through verbatim; `dd00_raw`, `d018_raw`, `port01_raw` and
110
- * `sprite_pointers` (the bytes read from the sprite-pointer table at
111
- * screen_base+$3F8..$3FF) feed the derivation.
112
- */
113
- export function buildChipState(raw) {
114
- const dd00 = raw.dd00_raw;
115
- const d018 = raw.d018_raw;
116
- const bank = vicBank(dd00);
117
- const bankBase = bank * 16384;
118
- const screenBaseAddr = screenBase(d018, dd00);
119
- const charsetBaseAddr = charsetBase(d018, dd00);
120
- const port01raw = raw.port01_raw;
121
- const spritePointers = raw.sprite_pointers ?? [];
122
- const spriteDataAddresses = spritePointers.map((p) => bankBase + p * 64);
123
-
124
- return {
125
- schema_version: 1,
126
- release: raw.release,
127
- label: raw.label,
128
- snapshot_name: raw.snapshot_name ?? null,
129
- registers: raw.registers,
130
- sprites: raw.sprites,
131
- cpu: raw.cpu,
132
- derived: {
133
- port01: {
134
- raw: port01raw,
135
- loram: !!(port01raw & 1),
136
- hiram: !!(port01raw & 2),
137
- charen: !!(port01raw & 4),
138
- },
139
- dd00_raw: dd00,
140
- dd00_direct_read: raw.dd00_direct_read ?? dd00,
141
- vic_bank: bank,
142
- d018_raw: d018,
143
- screen_base: screenBaseAddr,
144
- charset_base: charsetBaseAddr,
145
- sprite_pointers: spritePointers,
146
- sprite_data_addresses: spriteDataAddresses,
147
- },
148
- captured_at: raw.captured_at ?? new Date().toISOString(),
149
- };
150
- }
151
-
152
- // ---------------------------------------------------------- buildRangeManifest
153
-
154
- const IO_START = 0xd000;
155
- const IO_END = 0xdfff;
156
-
157
- function powerOnRunLength(image, start) {
158
- const b = image[start];
159
- if (b !== 0x00 && b !== 0xff) return 0;
160
- let end = start;
161
- while (end < image.length && image[end] === b) end++;
162
- return end - start;
163
- }
164
-
165
- /**
166
- * Emit the range manifest in the committed shape: ranges whose union
167
- * covers $0000-$FFFF with no gap and no overlap, a contiguous power-on-
168
- * pattern run of at least 16 bytes marked kind `unused`, the I/O window
169
- * ($D000-$DFFF) marked `io`, everything else `unclassified`, and
170
- * `classification_state` set to the same transient `ranges-only` state the
171
- * committed primary manifests carry.
172
- */
173
- export function buildRangeManifest(image, meta = {}) {
174
- if (image.length !== 65536) {
175
- throw new Error(`buildRangeManifest: image must be exactly 65536 bytes, got ${image.length}`);
176
- }
177
- const ranges = [];
178
- let i = 0;
179
- while (i < 65536) {
180
- if (i >= IO_START && i <= IO_END) {
181
- ranges.push({ start: i, end: IO_END, kind: "io", source: "capture", note: "VIC-II/SID/CIA/color-RAM I/O window" });
182
- i = IO_END + 1;
183
- continue;
184
- }
185
- const runLen = powerOnRunLength(image, i);
186
- if (runLen >= 16) {
187
- const end = i + runLen - 1;
188
- ranges.push({ start: i, end, kind: "unused", source: "capture", note: "contiguous $00/$FF power-on-pattern run of at least 16 bytes" });
189
- i = end + 1;
190
- continue;
191
- }
192
- let j = i;
193
- while (j < 65536 && !(j >= IO_START && j <= IO_END) && powerOnRunLength(image, j) < 16) {
194
- j++;
195
- }
196
- ranges.push({ start: i, end: j - 1, kind: "unclassified", source: "capture", note: "awaiting the loader/cracktro/game three-bucket partition" });
197
- i = j;
198
- }
199
- return {
200
- schema_version: 1,
201
- release: meta.release,
202
- label: meta.label,
203
- snapshot_name: meta.snapshot_name ?? null,
204
- image_bytes: image.length,
205
- offset_equals_address: true,
206
- classification_state: "ranges-only",
207
- ranges,
208
- note: meta.note ?? "",
209
- generated_at: meta.generated_at ?? new Date().toISOString(),
210
- };
211
- }
212
-
213
- // -------------------------------------------------------------- writeDumpSet
214
-
215
- /**
216
- * Render and write the four-file dump set from a committed chunk file's
217
- * contents plus the chip-state raw readings. Returns the written paths and
218
- * the image digest, so a caller can register a `dumps[]` entry.
219
- */
220
- export function writeDumpSet({ releaseId, label, chunks, chipStateRaw, meta = {}, captureExtra = {} }) {
221
- const image = assembleImage(chunks);
222
- const digest = sha256Buffer(image);
223
- const dumpsDir = join(releaseDir(releaseId), "dumps");
224
- mkdirSync(dumpsDir, { recursive: true });
225
-
226
- const binPath = join(dumpsDir, `${releaseId}-${label}.bin`);
227
- writeFileSync(binPath, image);
228
-
229
- const stateOut = buildChipState({ ...chipStateRaw, release: releaseId, label });
230
- const statePath = join(dumpsDir, `${releaseId}-${label}.state.json`);
231
- writeFileSync(statePath, JSON.stringify(stateOut, null, 2) + "\n");
232
-
233
- const manifestOut = buildRangeManifest(image, { release: releaseId, label, ...meta });
234
- const mapPath = join(dumpsDir, `${releaseId}-${label}.map.json`);
235
- writeFileSync(mapPath, JSON.stringify(manifestOut, null, 2) + "\n");
236
-
237
- const captureOut = {
238
- release: releaseId,
239
- label,
240
- sha256: digest,
241
- bytes: image.length,
242
- ...captureExtra,
243
- };
244
- const capturePath = join(dumpsDir, `${releaseId}-${label}.capture.json`);
245
- writeFileSync(capturePath, JSON.stringify(captureOut, null, 2) + "\n");
246
-
247
- return {
248
- bin: rel(binPath),
249
- state: rel(statePath),
250
- map: rel(mapPath),
251
- capture: rel(capturePath),
252
- sha256: digest,
253
- };
254
- }
255
-
256
- // -------------------------------------------------------------------- CLI
257
-
258
- function optValue(rest, name) {
259
- const i = rest.indexOf(`--${name}`);
260
- return i === -1 ? undefined : rest[i + 1];
261
- }
262
-
263
- function readJsonArg(rest, name) {
264
- const p = optValue(rest, name);
265
- if (!p) return undefined;
266
- return JSON.parse(readFileSync(resolve(p), "utf8"));
267
- }
268
-
269
- const VERBS = {
270
- assemble(rest) {
271
- const chunks = readJsonArg(rest, "chunks");
272
- if (!chunks) die("usage: assemble --chunks <chunks.json> [--json]");
273
- const image = assembleImage(chunks);
274
- const digest = sha256Buffer(image);
275
- const result = { bytes: image.length, sha256: digest };
276
- console.log(rest.includes("--json") ? JSON.stringify(result, null, 2) : `${result.bytes} bytes, sha256 ${result.sha256}`);
277
- },
278
-
279
- "chip-state"(rest) {
280
- const raw = readJsonArg(rest, "raw");
281
- if (!raw) die("usage: chip-state --raw <raw.json> [--json]");
282
- const result = buildChipState(raw);
283
- console.log(JSON.stringify(result, null, 2));
284
- },
285
-
286
- manifest(rest) {
287
- const chunks = readJsonArg(rest, "chunks");
288
- const metaArg = readJsonArg(rest, "meta") ?? {};
289
- if (!chunks) die("usage: manifest --chunks <chunks.json> [--meta <meta.json>] [--json]");
290
- const image = assembleImage(chunks);
291
- const result = buildRangeManifest(image, metaArg);
292
- console.log(JSON.stringify(result, null, 2));
293
- },
294
-
295
- "write-set"(rest) {
296
- const releaseId = optValue(rest, "release");
297
- const label = optValue(rest, "label");
298
- const chunks = readJsonArg(rest, "chunks");
299
- const chipStateRaw = readJsonArg(rest, "raw");
300
- const metaArg = readJsonArg(rest, "meta") ?? {};
301
- if (!releaseId || !label || !chunks || !chipStateRaw) {
302
- die("usage: write-set --release <id> --label <label> --chunks <chunks.json> --raw <raw.json> [--meta <meta.json>] [--json]");
303
- }
304
- const result = writeDumpSet({ releaseId, label, chunks, chipStateRaw, meta: metaArg });
305
- console.log(JSON.stringify(result, null, 2));
306
- },
307
- };
308
-
309
- if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
310
- const [cmd, ...rest] = process.argv.slice(2);
311
- if (!cmd || !VERBS[cmd]) {
312
- console.log(`usage: node ${fileURLToPath(import.meta.url)} <assemble|chip-state|manifest|write-set> [--json]`);
313
- process.exitCode = cmd ? 1 : 0;
314
- } else {
315
- VERBS[cmd](rest);
316
- }
317
- }
@@ -1,174 +0,0 @@
1
- #!/usr/bin/env node
2
- // mcp-module.mjs -- the ONE resolution ladder a skill script uses to reach a
3
- // module that lives in the OTHER package (`@henols/vice-mcp`'s `src/mcp/vice/`
4
- // tree, as opposed to this file's own `@henols/c64-re-tools` skills tree).
5
- //
6
- // ---------------------------------------------------------------------------
7
- // WHY THIS FILE EXISTS
8
- // ---------------------------------------------------------------------------
9
- // Before this file existed, `vsf-slice.mjs`
10
- // carried its own copy of this exact three-rung ladder (`ladder()` /
11
- // `resolveTarget()`), and TWO MORE skill scripts (`acme.mjs`,
12
- // `packer-finding.mjs`) were about to migrate onto the host-tool execution
13
- // seam and were each going to need the identical lookup to reach
14
- // `host-tool-client.ts`. Three production consumers were about to carry
15
- // three copies of one ladder -- so it is extracted here instead, and
16
- // `vsf-slice.mjs` is rewritten to import it rather than keep its own.
17
- //
18
- // The underlying constraint this ladder exists to answer is
19
- // `vsf-slice.mjs`'s own recorded finding: the MCP server ships as
20
- // `@henols/vice-mcp`, whose `files[]` lists only `src/mcp/vice/` contents,
21
- // while `src/skills/**` ships in a DIFFERENT package (`@henols/c64-re-tools`).
22
- // A plain `import` from a skill script into the MCP tree resolves on NEITHER
23
- // npm-installer route, so a skill script that needs an MCP-side module must
24
- // LOCATE it on disk and invoke it as a subprocess (`process.execPath
25
- // <resolved-path> ...argv`) -- never a static `import`.
26
- //
27
- // ---------------------------------------------------------------------------
28
- // WHAT THIS IS THE ONE AUTHORITATIVE PLACE FOR
29
- // ---------------------------------------------------------------------------
30
- // - the three-rung resolution order (`$VICE_MCP_DIR`, the in-repo relative
31
- // path, then `require.resolve()` against the published package) for
32
- // locating ANY named file inside `src/mcp/vice/` from a skill's own
33
- // `scripts/` directory;
34
- // - refusing BY NAME, listing every rung tried, when none resolves
35
- // (`refusalMessage()`);
36
- // - computing the in-repo hop count from `import.meta.url` rather than a
37
- // fixed `".."` count -- `hostpath.ts` and `containerpath.ts` both record
38
- // dropping a hard-coded four-level hop when they moved directories once
39
- // already, and this project has ALSO shipped a stale hardcoded snapshot
40
- // offset before (see `vsf-slice.mjs`'s own header) -- a fixed `".."`
41
- // count is a mistake this project keeps making, not a one-off.
42
- //
43
- // ---------------------------------------------------------------------------
44
- // WHAT NOT TO DO
45
- // ---------------------------------------------------------------------------
46
- // - Never carry a second copy of these three rungs, their ordering, or
47
- // their refusal text anywhere else in this repository. A future
48
- // skill-side consumer of an MCP-side module imports `resolveMcpModule`
49
- // from HERE.
50
- // - Never let a resolution failure become a local re-implementation of
51
- // whatever was being resolved -- refuse by name instead. A silent
52
- // fallback that reimplements the target module's own logic is exactly
53
- // the divergence this ladder exists to prevent (see `vsf-slice.mjs`'s own
54
- // header on the stale-offset incident this project has already had).
55
- // - `process.execPath` spawned on a resolved `.ts`/`.mjs` file inside this
56
- // project's OWN tree reaches NO external host binary -- it is the
57
- // interpreter already running the calling script, pointed at an in-tree
58
- // module. This is NOT what the host-tool execution seam (`host-tool.mts`
59
- // / `host-tool-client.ts`) exists for, and must never be confused with
60
- // it: the seam is for binaries that live on the HOST, outside any
61
- // container; this ladder is for locating a file inside this project's own
62
- // two npm packages.
63
- import { existsSync } from "node:fs";
64
- import { createRequire } from "node:module";
65
- import { fileURLToPath } from "node:url";
66
- import { basename, dirname, join, resolve } from "node:path";
67
-
68
- const HERE = dirname(fileURLToPath(import.meta.url));
69
-
70
- /** The package the MCP-side module ships in, for the npm-installer rung. */
71
- export const TARGET_PACKAGE = "@henols/vice-mcp";
72
-
73
- /**
74
- * Walks UP from `startDir` until a directory literally named `name` is
75
- * found, returning its absolute path -- or `null` if the filesystem root is
76
- * reached first without a match. This is what lets the in-repo hop count be
77
- * COMPUTED from `import.meta.url` instead of a fixed `".."` count: whichever
78
- * skill's `scripts/` directory this module is imported from, walking up to
79
- * the `skills/` ancestor and back down to `mcp/vice/` is the same operation.
80
- */
81
- function findAncestorDir(startDir, name) {
82
- let dir = startDir;
83
- for (;;) {
84
- if (basename(dir) === name) return dir;
85
- const parent = dirname(dir);
86
- if (parent === dir) return null; // reached the filesystem root, no match
87
- dir = parent;
88
- }
89
- }
90
-
91
- // Computed once, at module load: HERE is .../src/skills/<skill>/scripts/, so
92
- // walking up to the "skills" ancestor and taking ITS parent ("src") gives the
93
- // root from which "mcp/vice" is a sibling of "skills" -- with no fixed hop
94
- // count written down anywhere in this file.
95
- const SKILLS_DIR = findAncestorDir(HERE, "skills");
96
- const IN_REPO_MCP_VICE_DIR = SKILLS_DIR ? join(dirname(SKILLS_DIR), "mcp", "vice") : null;
97
-
98
- /**
99
- * The resolution ladder for `fileName`, in order, as `{ rung, path, note }`
100
- * records -- returned IN FULL (never short-circuited), so a refusal message
101
- * can name every path tried, which is the whole point of refusing rather
102
- * than guessing:
103
- *
104
- * 1. `$VICE_MCP_DIR` -- an explicit override, checked first so a caller can
105
- * always point this ladder at a known-good tree.
106
- * 2. The in-repo relative path (see `findAncestorDir()` above). The plugin
107
- * distribution keeps both trees in one checkout, so this is the rung
108
- * that resolves during development and in CI.
109
- * 3. `require.resolve()` against the published package, for the
110
- * npm-installer route where the two packages are installed separately.
111
- */
112
- function ladder(fileName) {
113
- const rungs = [];
114
-
115
- const override = process.env.VICE_MCP_DIR;
116
- rungs.push({
117
- rung: "VICE_MCP_DIR",
118
- path: override ? join(resolve(override), fileName) : null,
119
- note: override ? null : "VICE_MCP_DIR is not set",
120
- });
121
-
122
- rungs.push({
123
- rung: "in-repo relative path",
124
- path: IN_REPO_MCP_VICE_DIR ? join(IN_REPO_MCP_VICE_DIR, fileName) : null,
125
- note: IN_REPO_MCP_VICE_DIR ? null : `could not locate an ancestor "skills" directory from ${HERE}`,
126
- });
127
-
128
- let resolved = null;
129
- let note = null;
130
- try {
131
- resolved = createRequire(import.meta.url).resolve(`${TARGET_PACKAGE}/${fileName}`);
132
- } catch (err) {
133
- note = `${TARGET_PACKAGE} is not resolvable from here (${err.code ?? err.message})`;
134
- }
135
- rungs.push({ rung: `${TARGET_PACKAGE} package`, path: resolved, note });
136
-
137
- return rungs;
138
- }
139
-
140
- /**
141
- * Builds the refusal text naming every rung tried plus the instruction to
142
- * set `VICE_MCP_DIR` -- the ONE place this message is worded, so callers
143
- * that construct their own additional context around it (e.g.
144
- * `vsf-slice.mjs`'s own domain-specific rationale) do not restate it.
145
- */
146
- export function refusalMessage(fileName, rungs) {
147
- const tried = rungs
148
- .map((r) => ` ${r.rung}: ${r.path ?? "(no path)"}${r.note ? ` -- ${r.note}` : ""}`)
149
- .join("\n");
150
- return (
151
- `could not resolve ${fileName}.\n` +
152
- `Tried, in order:\n${tried}\n` +
153
- `Set VICE_MCP_DIR to the directory holding ${fileName}.`
154
- );
155
- }
156
-
157
- /**
158
- * Resolves `fileName` (e.g. `"vsf-slice.ts"`, `"host-tool-client.ts"`) against
159
- * the three-rung ladder above, with NO per-file special-casing -- every
160
- * caller gets the same three rungs in the same order.
161
- *
162
- * Returns `{ ok: true, rung, path }` for the first rung whose path exists on
163
- * disk, or `{ ok: false, rungs, message }` when none resolves -- `rungs`
164
- * carries the full attempt list (so a caller can build its own message from
165
- * the raw records instead of `refusalMessage()`, if it wants extra context),
166
- * and `message` is `refusalMessage(fileName, rungs)` already computed. Never
167
- * throws.
168
- */
169
- export function resolveMcpModule(fileName) {
170
- const rungs = ladder(fileName);
171
- const hit = rungs.find((r) => r.path && existsSync(r.path));
172
- if (hit) return { ok: true, rung: hit.rung, path: hit.path };
173
- return { ok: false, rungs, message: refusalMessage(fileName, rungs) };
174
- }
@@ -1,81 +0,0 @@
1
- #!/usr/bin/env node
2
- // Where this toolkit's data lives, resolved portably.
3
- //
4
- // These modules ship as a bundled skill toolkit and may be installed at any
5
- // depth in any project, so nothing here counts directory hops. Two rules:
6
- //
7
- // 1. The project root is found by walking UP for a `.git` marker. Counting
8
- // hops from `import.meta.url` breaks the moment the toolkit is installed
9
- // somewhere other than `src/skills/<skill>/scripts/`, and it breaks
10
- // silently -- paths resolve to a plausible wrong place rather than erroring.
11
- // 2. Every data location is overridable by environment variable, so a project
12
- // that does not use this repo's `recovery/` + `disks/` layout can point the
13
- // toolkit at its own without editing any module.
14
- //
15
- // Pure path arithmetic over the filesystem. Contacts nothing.
16
- import { existsSync } from "node:fs";
17
- import { fileURLToPath } from "node:url";
18
- import { dirname, join, resolve, parse } from "node:path";
19
-
20
- const HERE = dirname(fileURLToPath(import.meta.url));
21
-
22
- /**
23
- * Nearest ancestor directory containing a `.git` entry, starting from this
24
- * file. Falls back to `C64RE_PROJECT_ROOT` when set, which also covers the
25
- * case of running from an export with no git metadata at all.
26
- */
27
- export function projectRoot() {
28
- if (process.env.C64RE_PROJECT_ROOT) return resolve(process.env.C64RE_PROJECT_ROOT);
29
- let dir = HERE;
30
- const { root } = parse(dir);
31
- while (true) {
32
- if (existsSync(join(dir, ".git"))) return dir;
33
- if (dir === root) break;
34
- dir = dirname(dir);
35
- }
36
- throw new Error(
37
- "project-paths: could not locate the project root -- no `.git` found above " +
38
- `${HERE}. Set C64RE_PROJECT_ROOT to the directory that holds your data dirs.`,
39
- );
40
- }
41
-
42
- /**
43
- * Directory holding the release registry and the per-release dump directories.
44
- * Defaults to `<project root>/recovery`; override with `C64RE_DATA_DIR`.
45
- */
46
- export function dataRoot() {
47
- return process.env.C64RE_DATA_DIR
48
- ? resolve(process.env.C64RE_DATA_DIR)
49
- : join(projectRoot(), "recovery");
50
- }
51
-
52
- /**
53
- * Directory holding the disk images a registry entry's `disk_image` is
54
- * relative to. Defaults to the project root itself, because registry entries
55
- * record project-relative paths like `disks/foo.d64`; override with
56
- * `C64RE_DISKS_ROOT` when they are relative to something else.
57
- */
58
- export function disksRoot() {
59
- return process.env.C64RE_DISKS_ROOT
60
- ? resolve(process.env.C64RE_DISKS_ROOT)
61
- : projectRoot();
62
- }
63
-
64
- /** The release registry file. Override the whole path with `C64RE_REGISTRY`. */
65
- export function registryFile() {
66
- return process.env.C64RE_REGISTRY
67
- ? resolve(process.env.C64RE_REGISTRY)
68
- : join(dataRoot(), "RELEASES.json");
69
- }
70
-
71
- /** One release's own data directory. */
72
- export function releaseDataDir(id) {
73
- return join(dataRoot(), id);
74
- }
75
-
76
- if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
77
- console.log(`project root: ${projectRoot()}`);
78
- console.log(`data root: ${dataRoot()}`);
79
- console.log(`disks root: ${disksRoot()}`);
80
- console.log(`registry: ${registryFile()}`);
81
- }
@@ -1,109 +0,0 @@
1
- #!/usr/bin/env node
2
- // N-way release registry accessor. This is the only module that reads a release
3
- // identifier out of the registry -- every other module takes the id as an
4
- // argument and never touches the registry file directly. `release` is the
5
- // primary noun rather than "the canonical image": there are N releases, each
6
- // owning a set of dumps, with `canonical` demoted to a boolean on one entry.
7
- //
8
- // Portable: the registry's location comes from `project-paths.mjs`, so a project
9
- // with a different data layout points the toolkit at its own via
10
- // `C64RE_DATA_DIR` / `C64RE_REGISTRY` rather than editing this file.
11
- import { readFileSync, writeFileSync, existsSync } from "node:fs";
12
- import { fileURLToPath } from "node:url";
13
- import { resolve } from "node:path";
14
-
15
- import { registryFile, releaseDataDir } from "./project-paths.mjs";
16
-
17
- export const registryPath = registryFile();
18
-
19
- const die = (m) => { console.error(`error: ${m}`); process.exit(1); };
20
-
21
- export function loadRegistry() {
22
- if (!existsSync(registryPath)) {
23
- throw new Error(`no registry at ${registryPath}`);
24
- }
25
- return JSON.parse(readFileSync(registryPath, "utf8"));
26
- }
27
-
28
- /**
29
- * Persist the registry. JSON.stringify preserves each object's insertion
30
- * (key) order, so a read-modify-write via upsertRelease keeps a stable key
31
- * order automatically -- callers should not rebuild release objects from
32
- * scratch with a different key order.
33
- */
34
- function saveRegistry(reg) {
35
- writeFileSync(registryPath, JSON.stringify(reg, null, 2) + "\n");
36
- }
37
-
38
- /** The full entry for `id`, or throws with the known-id list on a miss. */
39
- export function release(id) {
40
- const reg = loadRegistry();
41
- const r = reg.releases.find((r) => r.id === id);
42
- if (!r) assertKnownRelease(id, reg);
43
- return r;
44
- }
45
-
46
- /**
47
- * The registry's own N-readiness documentation:
48
- * a top-level `schema_notes` string, sibling to `schema_version` and
49
- * `releases`, stating the mechanical claim that adding a release is one
50
- * `releases[]` entry plus one invocation of `tools/recover.mjs`. Kept as a
51
- * plain top-level field rather than a JSON comment (JSON has none) or a
52
- * per-release field (it describes the registry's shape, not any one
53
- * release). Rehearsed against the real validator in
54
- * a release's own NOTES.md.
55
- */
56
- export function schemaNotes() {
57
- return loadRegistry().schema_notes ?? null;
58
- }
59
-
60
- export function releaseDir(id) {
61
- const reg = loadRegistry();
62
- assertKnownRelease(id, reg);
63
- return releaseDataDir(id);
64
- }
65
-
66
- /** Dies with the list of known ids on a miss -- see plan Layer 2. */
67
- export function assertKnownRelease(id, reg) {
68
- const registry = reg || loadRegistry();
69
- const known = registry.releases.map((r) => r.id);
70
- if (!known.includes(id)) {
71
- throw new Error(`unknown release "${id}" -- known releases: ${known.join(", ")}`);
72
- }
73
- }
74
-
75
- /**
76
- * Read-modify-write: `fn` receives a shallow copy of the release entry and
77
- * returns the replacement; the whole registry is then re-persisted with
78
- * stable key order. This is the only sanctioned way to mutate an entry.
79
- */
80
- export function upsertRelease(id, fn) {
81
- const reg = loadRegistry();
82
- const idx = reg.releases.findIndex((r) => r.id === id);
83
- if (idx === -1) {
84
- throw new Error(`unknown release "${id}" -- known releases: ${reg.releases.map((r) => r.id).join(", ")}`);
85
- }
86
- reg.releases[idx] = fn({ ...reg.releases[idx] });
87
- saveRegistry(reg);
88
- return reg.releases[idx];
89
- }
90
-
91
- // -------------------------------------------------------------------- CLI
92
-
93
- if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
94
- const [cmd, ...rest] = process.argv.slice(2);
95
- if (cmd === "list") {
96
- const reg = loadRegistry();
97
- for (const r of reg.releases) {
98
- console.log(`${r.id} canonical=${r.canonical} disk_image=${r.disk_image} dumps=${r.dumps.length}`);
99
- }
100
- } else if (cmd === "show") {
101
- if (!rest[0]) die("usage: show <release-id>");
102
- console.log(JSON.stringify(release(rest[0]), null, 2));
103
- } else if (cmd === "schema-notes") {
104
- console.log(schemaNotes() ?? "(no schema_notes field set)");
105
- } else {
106
- console.log(`usage: node ${fileURLToPath(import.meta.url)} <list|show <id>|schema-notes>`);
107
- process.exit(cmd ? 1 : 0);
108
- }
109
- }