@henols/c64-re-tools 1.1.0 → 2.0.0-rc.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.
Files changed (138) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +138 -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 +106 -0
  39. package/src/host/server.ts +376 -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 +238 -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 +106 -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 +243 -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,553 +0,0 @@
1
- #!/usr/bin/env node
2
- // Reference driver for the C64 memory map.
3
- //
4
- // One job: answer "what is at this address?", and apply that answer to a
5
- // 6502 listing so bare numeric operands read as *documented* assembly. Every
6
- // address is resolved against the published Commodore 64 memory-map tables (see
7
- // SOURCES below). Everything here is information.
8
- //
9
- // Self-contained by design: no imports outside the Node standard library, no
10
- // sibling skill, no emulator, no other tool. `lookup` and `annotate` run purely
11
- // off the committed memmap.json, so they work offline and anywhere. Only
12
- // `memmap`, which rebuilds that file from the four upstream sources, needs a
13
- // network. A listing to annotate is read from a file or stdin -- whatever
14
- // produced it is none of this script's business.
15
- //
16
- // Node >= 18 (global fetch). No dependencies on purpose.
17
-
18
- import { readFileSync, writeFileSync, existsSync } from "node:fs";
19
- import { fileURLToPath } from "node:url";
20
- import { dirname, join, resolve } from "node:path";
21
-
22
- const HERE = dirname(fileURLToPath(import.meta.url));
23
- // memmap.json lives at the skill root, one level up from scripts/, by decision
24
- // only .mjs modules move into scripts/, data files stay put.
25
- const MEMMAP_JSON = join(HERE, "..", "memmap.json");
26
-
27
- // Reference tables, merged into one address -> meaning index. `kind` selects the
28
- // parser; add a row here to pull in another aay page (e.g. basromma.htm for the
29
- // BASIC ROM) and re-run `node driver.mjs memmap`.
30
- const SOURCES = [
31
- { id: "sta", kind: "staTable", url: "https://sta.c64.org/cbm64mem.html" },
32
- { id: "zim", kind: "zimmers", url: "https://www.zimmers.net/anonftp/pub/cbm/maps/C64.MemoryMap.txt" },
33
- { id: "kernal", kind: "aayList", url: "http://unusedino.de/ec64/technical/aay/c64/krnromma.htm" },
34
- { id: "io", kind: "c64io", url: "https://www.zimmers.net/anonftp/pub/cbm/maps/C64io.txt" },
35
- ];
36
-
37
- // On an exact tie (several sources describe the same single address), prefer
38
- // sta.c64.org -- its prose is the most specific ("Border color (only bits
39
- // #0-#3)" vs "Border Color"). The other sources win wherever sta has only a
40
- // block entry, which is all of ROM.
41
- const SRC_RANK = { sta: 0, zim: 1, kernal: 2, io: 3 };
42
-
43
- // Fields worth keeping from a source that loses on specificity: the canonical
44
- // assembler symbol (zimmers) and a chip register offset like "VIC+17" (emitted
45
- // by parseAayList, so adding vicmain.htm/sidmain.htm/ciamain.htm to SOURCES
46
- // lights it up). Grafted onto whichever entry wins, so a single comment can
47
- // carry prose + symbol + register.
48
- const GRAFT_FIELDS = ["sym", "reg"];
49
-
50
- // Entries wider than this are context, not a specific register: "$E000-$FFFF
51
- // KERNAL ROM" inline on every branch target is pure noise. Wide hits still show
52
- // up in the header block.
53
- // Admits screen RAM ($0400, 1000 bytes) and the $C000-$CFFF block, but not the
54
- // 8 KB BASIC/KERNAL ROM blocks. Only applies to non-flow instructions.
55
- const INLINE_SPAN_MAX = 4096;
56
-
57
- // ------------------------------------------------------------- memory-map table
58
-
59
- // This image's python3 has no `html` stdlib module and the page is ISO-8859-1,
60
- // so both the decode and the entity unescape are done by hand here.
61
- function unescapeHtml(s) {
62
- const named = {
63
- amp: "&", lt: "<", gt: ">", quot: '"', apos: "'", nbsp: " ",
64
- ndash: "-", mdash: "-", hellip: "...", deg: "deg", times: "x", eacute: "e",
65
- };
66
- return s
67
- .replace(/&#(\d+);/g, (_, d) => String.fromCharCode(+d))
68
- .replace(/&#x([0-9a-f]+);/gi, (_, h) => String.fromCharCode(parseInt(h, 16)))
69
- .replace(/&([a-z]+);/gi, (m, n) => named[n.toLowerCase()] ?? m);
70
- }
71
-
72
- function textify(frag) {
73
- let t = frag
74
- .replace(/<a\b[^>]*>.*?<\/a>/gis, " ")
75
- .replace(/<(li|p|ul|\/ul|br)\b[^>]*>/gi, "\n")
76
- .replace(/<[^>]+>/g, " ");
77
- t = unescapeHtml(t)
78
- .split("\n")
79
- .map((l) => l.replace(/[ \t ]+/g, " ").trim())
80
- .filter(Boolean)
81
- .join(" ");
82
- return t.trim();
83
- }
84
-
85
- /** First sentence — short enough to sit at the end of an asm line. */
86
- function shortLabel(desc) {
87
- let m = desc.match(/^(.{0,90}?[.:])(\s|$)/);
88
- let lab = (m ? m[1] : desc.slice(0, 90)).replace(/[.:]+$/, "").trim();
89
- // "$D012 -> Read" is useless: some entries open with a bare access-mode word
90
- // and put the actual meaning after the colon. Reach past it.
91
- if (/^(read|write|bits?|values?)$/i.test(lab)) {
92
- const rest = desc.slice(desc.indexOf(":") + 1).trim();
93
- const m2 = rest.match(/^(.{0,80}?[.:])(\s|$)/);
94
- lab = `${lab}: ${(m2 ? m2[1] : rest.slice(0, 80)).replace(/[.:]+$/, "").trim()}`;
95
- }
96
- // Trim to a word boundary rather than mid-word.
97
- if (lab.length > 88) lab = lab.slice(0, 88).replace(/\s+\S*$/, "") + "...";
98
- return lab;
99
- }
100
-
101
- // The "All About Your 64" pages are flat lists of single addresses:
102
- // <a href="vic17.htm">$D011/53265/VIC+17</a> Control Register 1
103
- // <a href="rome000.htm">$E000/57344</a> EXP continued From BASIC ROM
104
- // The chip/offset token is optional (the KERNAL listing omits it). Descriptions
105
- // run to end of line.
106
- const AAY_RE =
107
- /<a\s+href="[^"]*">\$([0-9A-F]{4})\/\d+(?:\/([A-Za-z0-9]+\+\d+))?<\/a>\s*([^\n|<]*)/gi;
108
-
109
- // zimmers.net C64.MemoryMap.txt, section 1 — plain text, space-aligned:
110
- // LABEL HEX DEC DESCRIPTION
111
- // TXTTAB 002B-002C 43 Pointer: Start of BASIC Text Area ($0801).
112
- // ADRAY1 0003-0004 3 Jump Vector: Convert FAC to Integer in (A/Y)
113
- // ($B1AA). <- continuation line
114
- // The label is this file's unique contribution: the canonical assembler symbol
115
- // names, which none of the other sources carry. The label is optional (block
116
- // rows like "0800-9FFF Normal BASIC Program space." have none).
117
- const ZIM_ROW = /^([A-Z0-9]{1,9})?\s+([0-9A-F]{4})(?:-([0-9A-F]{4}))?\s+\d+\s+(\S.*)$/;
118
-
119
- function parseZimmers(txt, section) {
120
- const lines = txt.split("\n");
121
- // Section 2 ("INPUT/OUTPUT ASSIGNMENTS") is deliberately skipped: it is
122
- // tab-separated bit-level detail already covered by the aay chip pages, and it
123
- // carries OCR damage ("DEOO-DEFF", "Sprite O X Pos") that would inject bogus
124
- // addresses.
125
- const stop = lines.findIndex((l) => /INPUT\/OUTPUT ASSIGNMENTS/i.test(l));
126
- const body = lines.slice(0, stop === -1 ? lines.length : stop);
127
-
128
- const entries = [];
129
- for (const line of body) {
130
- if (!line.trim()) continue;
131
- // An inline listing of the CHRGET routine sits mid-table; its rows look like
132
- // " ,0073 INC $7A" and must not be read as addresses.
133
- if (/,[0-9A-F]{4}\s/.test(line)) continue;
134
- const m = line.match(ZIM_ROW);
135
- if (m) {
136
- const start = parseInt(m[2], 16);
137
- entries.push({
138
- start,
139
- end: m[3] ? parseInt(m[3], 16) : start,
140
- sym: m[1] || null,
141
- desc: m[4].trim(),
142
- section,
143
- });
144
- } else if (/^\s{20,}\S/.test(line) && entries.length) {
145
- entries[entries.length - 1].desc += " " + line.trim();
146
- }
147
- }
148
- for (const e of entries) {
149
- e.desc = e.desc.replace(/\s+/g, " ").trim();
150
- e.label = shortLabel(e.desc);
151
- }
152
- return entries;
153
- }
154
-
155
- // zimmers.net C64io.txt — the I/O map, tab-separated with a variable number of
156
- // tabs, so fields are split and empties dropped rather than column-sliced:
157
- // D011 53265 VIC Control Register
158
- // 7 Raster Compare: (Bit 8) See 53266 <- bit row
159
- // 2-0 Smooth Scroll to Y Dot-Position (0-7)
160
- // Its edge over a plain register list is the per-bit breakdown, kept on the
161
- // register entry and printed by `lookup`.
162
- const IO_ADDR = /^([0-9A-F]{4})(?:-([0-9A-F]{4}))?$/;
163
- const IO_BIT = /^[0-7](?:-[0-7])?$/;
164
-
165
- function parseC64io(txt) {
166
- const entries = [];
167
- let section = null;
168
- let last = null;
169
- for (const line of txt.split("\n")) {
170
- if (!line.trim() || line.trim().startsWith(";")) continue;
171
- const f = line.split("\t").map((x) => x.trim()).filter(Boolean);
172
- if (!f.length) continue;
173
-
174
- const am = f[0].match(IO_ADDR);
175
- if (am && f.length >= 2) {
176
- const start = parseInt(am[1], 16);
177
- const end = am[2] ? parseInt(am[2], 16) : start;
178
- const desc = f.slice(2).join(" ").trim();
179
- // A range row spanning a whole chip is a section header; it is also a
180
- // legitimate (wide) entry, so keep it AND use it as the section.
181
- if (am[2]) section = desc;
182
- if (!desc) continue;
183
- last = { start, end, desc, label: shortLabel(desc), section, bits: [] };
184
- entries.push(last);
185
- } else if (IO_BIT.test(f[0]) && last) {
186
- last.bits.push({ bit: f[0], desc: f.slice(1).join(" ").trim() });
187
- } else if (last && f.length === 1) {
188
- // Wrapped description text.
189
- if (last.bits.length) last.bits[last.bits.length - 1].desc += " " + f[0];
190
- else {
191
- last.desc += " " + f[0];
192
- last.label = shortLabel(last.desc);
193
- }
194
- }
195
- }
196
- for (const e of entries) if (!e.bits.length) delete e.bits;
197
- return entries;
198
- }
199
-
200
- function parseAayList(htmlText, section) {
201
- const entries = [];
202
- for (const m of htmlText.matchAll(AAY_RE)) {
203
- const desc = unescapeHtml(m[3]).replace(/\s+/g, " ").trim();
204
- if (!desc || desc === "-") continue; // a few rows are placeholders
205
- const addr = parseInt(m[1], 16);
206
- entries.push({
207
- start: addr, end: addr,
208
- label: desc, desc,
209
- reg: m[2] || null, // e.g. "VIC+17"
210
- section,
211
- });
212
- }
213
- return entries;
214
- }
215
-
216
- function parseStaTable(htmlText) {
217
- const entries = [];
218
- let section = null;
219
- for (const row of htmlText.split(/<TR\b/i).slice(1)) {
220
- const sec = row.match(/<TD[^>]*COLSPAN=2[^>]*>\s*<B>([\s\S]*?)<\/B>/i);
221
- if (sec) {
222
- section = textify(sec[1]);
223
- continue;
224
- }
225
- const m = row.match(/<TD[^>]*>\s*\$([0-9A-F]{4})(?:-\$([0-9A-F]{4}))?\s*<BR>/i);
226
- if (!m) continue;
227
- const start = parseInt(m[1], 16);
228
- const end = m[2] ? parseInt(m[2], 16) : start;
229
- const tds = row.split(/<TD[^>]*>/i);
230
- const desc = tds.length > 2 ? textify(tds.slice(2).join(" ")) : "";
231
- entries.push({ start, end, label: shortLabel(desc), section, desc });
232
- }
233
- entries.sort((a, b) => a.start - b.start || a.end - b.end);
234
- return entries;
235
- }
236
-
237
- const SECTION_FOR = {
238
- zim: "C64 memory map (labelled)",
239
- kernal: "KERNAL ROM routine",
240
- };
241
-
242
- // Every page here is served as ISO-8859-1 / Latin-1 in practice; decoding as
243
- // UTF-8 mangles the entities. Decode explicitly.
244
- async function fetchLatin1(url) {
245
- const res = await fetch(url);
246
- if (!res.ok) throw new Error(`${url} -> HTTP ${res.status}`);
247
- return new TextDecoder("iso-8859-1").decode(new Uint8Array(await res.arrayBuffer()));
248
- }
249
-
250
- async function cmdMemmap() {
251
- const all = [];
252
- for (const s of SOURCES) {
253
- process.stderr.write(`fetching ${s.url} ... `);
254
- const htmlText = await fetchLatin1(s.url);
255
- const parsers = {
256
- staTable: () => parseStaTable(htmlText),
257
- zimmers: () => parseZimmers(htmlText, SECTION_FOR[s.id]),
258
- c64io: () => parseC64io(htmlText),
259
- aayList: () => parseAayList(htmlText, SECTION_FOR[s.id]),
260
- };
261
- const got = parsers[s.kind]();
262
- if (!got.length) throw new Error(`no entries from ${s.url} — layout changed?`);
263
- got.forEach((e) => (e.src = s.id));
264
- process.stderr.write(`${got.length} entries\n`);
265
- all.push(...got);
266
- }
267
- // Per-source emptiness above is the real guard; this only catches a broad collapse.
268
- if (all.length < 600) throw new Error(`only ${all.length} entries total — layout changed?`);
269
- all.sort((a, b) => a.start - b.start || a.end - b.end);
270
- writeFileSync(MEMMAP_JSON, JSON.stringify({ sources: SOURCES, entries: all }, null, 1));
271
- console.log(`${all.length} entries from ${SOURCES.length} sources -> ${MEMMAP_JSON}`);
272
- }
273
-
274
- let MEMMAP = null;
275
- function memmap() {
276
- if (MEMMAP) return MEMMAP;
277
- if (!existsSync(MEMMAP_JSON)) {
278
- throw new Error(`no ${MEMMAP_JSON}; run: node driver.mjs memmap`);
279
- }
280
- MEMMAP = JSON.parse(readFileSync(MEMMAP_JSON, "utf8")).entries;
281
- // Several sources describe the same address with different strengths. Whoever
282
- // wins on specificity keeps its prose, but the symbol name and register offset
283
- // from the others are grafted on so one comment carries all three. Keyed on
284
- // the exact range so a block entry never inherits a register's symbol.
285
- for (const f of GRAFT_FIELDS) {
286
- const at = new Map();
287
- for (const e of MEMMAP) if (e[f]) at.set(`${e.start}-${e.end}`, e[f]);
288
- for (const e of MEMMAP) if (!e[f]) e[f] = at.get(`${e.start}-${e.end}`) ?? null;
289
- }
290
- return MEMMAP;
291
- }
292
-
293
- /**
294
- * All entries covering `addr`, most specific first: narrowest span wins, then
295
- * the richer source. This ordering is what makes ROM addresses resolve to a
296
- * named routine instead of "KERNAL ROM (8192 bytes)".
297
- */
298
- function lookup(addr) {
299
- return memmap()
300
- .filter((e) => e.start <= addr && addr <= e.end)
301
- .sort(
302
- (a, b) =>
303
- a.end - a.start - (b.end - b.start) ||
304
- (SRC_RANK[a.src] ?? 9) - (SRC_RANK[b.src] ?? 9)
305
- );
306
- }
307
-
308
- // --------------------------------------------------------- operand extraction
309
-
310
- const hex = (n, w = 4) => "$" + n.toString(16).toUpperCase().padStart(w, "0");
311
-
312
- /**
313
- * Pull the address an instruction actually *touches* out of a line of listing
314
- * text. Returns {addr, note} or null.
315
- *
316
- * Deliberately returns null for immediates: in `LDA #$D0` the $D0 is a value,
317
- * not a location, and annotating it "processor port" is actively misleading.
318
- */
319
- const MNEMONICS = ("ADC AND ASL BCC BCS BEQ BIT BMI BNE BPL BRK BVC BVS CLC CLD CLI CLV CMP " +
320
- "CPX CPY DEC DEX DEY EOR INC INX INY JMP JSR LDA LDX LDY LSR NOP ORA PHA PHP PLA PLP " +
321
- "ROL ROR RTI RTS SBC SEC SED SEI STA STX STY TAX TAY TSX TXA TXS TYA").split(" ");
322
-
323
- // Anchoring on the real opcode set (rather than /[A-Z]{3}/) is what lets one
324
- // regex cope with every listing style without being told which it is: a
325
- // disassembler's "$EA34: AD 12 D0 LDA $D012", a bare "lda $d012", and
326
- // hand-written "loop lda $d012" / "loop: lda $d012" alike. The \b after the
327
- // mnemonic is load-bearing: it stops "sta" from matching inside the label
328
- // "start".
329
- const INSTR_RE = new RegExp(`\\b(${MNEMONICS.join("|")})\\b\\s*([^;]*)`, "i");
330
-
331
- function operandAddr(instruction) {
332
- const m = instruction.match(INSTR_RE);
333
- if (!m) return null;
334
- const [, mnemonic, rawOperand] = m;
335
- const op = rawOperand.trim();
336
- if (!op || op === "A") return null;
337
- if (op.startsWith("#")) return null; // immediate — a value, not an address
338
-
339
- // ($xx),Y ($xx,X) ($xxxx) -> the pointer itself is the interesting location
340
- let ind = op.match(/^\(\$([0-9A-F]{2,4})\s*(?:,\s*X)?\)\s*(?:,\s*Y)?$/i);
341
- if (ind) {
342
- return { addr: parseInt(ind[1], 16), note: "pointer", mnemonic };
343
- }
344
- // $xx $xxxx optionally ,X / ,Y
345
- let abs = op.match(/^\$([0-9A-F]{2,4})\s*(?:,\s*[XY])?$/i);
346
- if (abs) {
347
- const idx = /,\s*[XY]/i.test(op);
348
- return { addr: parseInt(abs[1], 16), note: idx ? "indexed" : null, mnemonic };
349
- }
350
- return null;
351
- }
352
-
353
- const FLOW = new Set(["JMP", "JSR", "BNE", "BEQ", "BCC", "BCS", "BMI", "BPL", "BVC", "BVS", "RTS", "RTI"]);
354
-
355
- // ------------------------------------------------------------------- annotate
356
-
357
- function annotate(lines, { maxSpan = INLINE_SPAN_MAX, noHeader = false } = {}) {
358
- const out = [];
359
- const referenced = new Map();
360
-
361
- // Each line is passed through byte-for-byte -- indentation, labels,
362
- // directives, blank lines and existing comments -- and only a trailing `;`
363
- // comment is appended, so documenting a .asm file never reformats it.
364
- // INSTR_RE finds the mnemonic wherever in the line it sits, which is why the
365
- // whole line can be handed to the operand parser unsliced.
366
- for (const body of lines) {
367
- if (!body.trim()) {
368
- out.push(body);
369
- continue;
370
- }
371
- const o = operandAddr(body);
372
- let comment = "";
373
- if (o) {
374
- const hits = lookup(o.addr);
375
- if (hits.length) {
376
- const best = hits[0];
377
- const span = best.end - best.start;
378
- // Record every hit for the header, even the wide ones.
379
- if (!referenced.has(best.start)) referenced.set(best.start, best);
380
- // A branch into "$E000-$FFFF KERNAL ROM" teaches nobody anything, so
381
- // flow instructions only get a comment when the hit is a specific
382
- // vector. Data instructions are the opposite: `STA $0400,X` -> "screen
383
- // memory (1000 bytes)" is exactly what a reader wants, so wide hits
384
- // stay, up to maxSpan (which by default admits screen RAM and the
385
- // $C000 block but not the 8 KB ROM blocks).
386
- const isFlow = FLOW.has(o.mnemonic.toUpperCase());
387
- if (isFlow ? span <= 2 : span <= maxSpan) {
388
- const which = best.start === best.end ? hex(best.start) : `${hex(best.start)}-${hex(best.end)}`;
389
- const tags = [best.sym, best.reg, o.note].filter(Boolean).join(", ");
390
- comment = `; ${which}${tags ? ` (${tags})` : ""} = ${best.label}`;
391
- }
392
- }
393
- }
394
- out.push(comment ? `${body.padEnd(44)}${comment}` : body);
395
- }
396
-
397
- const header = [];
398
- if (referenced.size && !noHeader) {
399
- header.push(";; ------------------------------------------------------------------");
400
- header.push(";; Addresses referenced by this listing");
401
- for (const s of SOURCES) header.push(`;; ${s.url}`);
402
- header.push(";; ------------------------------------------------------------------");
403
- for (const e of [...referenced.values()].sort((a, b) => a.start - b.start)) {
404
- const which = e.start === e.end ? hex(e.start) : `${hex(e.start)}-${hex(e.end)}`;
405
- const tags = [e.sym, e.reg].filter(Boolean).join(", ");
406
- header.push(`;; ${which.padEnd(14)}${e.label}${tags ? ` (${tags})` : ""}`);
407
- if (e.section) header.push(`;; ${"".padEnd(14)} [${e.section}]`);
408
- }
409
- header.push(";; ------------------------------------------------------------------");
410
- header.push("");
411
- }
412
- return header.concat(out).join("\n");
413
- }
414
-
415
- // -------------------------------------------------------------------- commands
416
-
417
- // An address is accepted however the source it was copied from happened to write
418
- // it, so no one has to convert by hand:
419
- //
420
- // $D011 $d011 0xD011 0XD011 D011h d011H D011 d011 hex
421
- // %1101000000010001 binary
422
- // 53265 decimal
423
- //
424
- // A base marker ($, 0x, trailing h, %) settles the base outright. Without one,
425
- // two unambiguous readings remain: a token containing A-F can only be hex, and a
426
- // plain run of decimal digits is read as decimal -- which is how the published
427
- // tables print addresses ("53265 VIC Control Register"), so a number copied
428
- // straight out of one lands on the right register. `#` is tolerated in front,
429
- // for pasting an operand across as-is.
430
- //
431
- // Tried in order, so a marker always beats the markerless readings below it.
432
- const ADDR_FORMS = [
433
- [/^\$([0-9a-f]+)$/i, 16],
434
- [/^0x([0-9a-f]+)$/i, 16],
435
- [/^([0-9a-f]+)h$/i, 16],
436
- [/^%([01]+)$/, 2],
437
- [/^(\d+)$/, 10],
438
- [/^([0-9a-f]+)$/i, 16],
439
- ];
440
-
441
- const ADDR_MAX = 0xffff;
442
-
443
- function parseAddr(s) {
444
- if (s == null) throw new Error("address required");
445
- const t = String(s).trim().replace(/^#/, "").replace(/[_\s]/g, "");
446
- for (const [re, base] of ADDR_FORMS) {
447
- const m = t.match(re);
448
- if (!m) continue;
449
- const n = parseInt(m[1], base);
450
- if (!Number.isFinite(n)) break;
451
- if (n > ADDR_MAX) {
452
- // Digits that overflow as hex but fit as decimal are almost always a
453
- // decimal address that picked up a `$` on the way in.
454
- const asDec = base === 16 && /^\d+$/.test(m[1]) ? parseInt(m[1], 10) : NaN;
455
- const hint =
456
- asDec <= ADDR_MAX ? ` For decimal ${m[1]}, drop the marker: ${m[1]} = ${hex(asDec)}.` : "";
457
- throw new Error(
458
- `${s} reads as ${n}, past the top of the 64K address space ` +
459
- `($0000-${hex(ADDR_MAX)}).${hint}`
460
- );
461
- }
462
- return n;
463
- }
464
- throw new Error(
465
- `bad address: ${s}\n` +
466
- ` hex $D011 / 0xD011 / D011h / D011, binary %1101000000010001, decimal 53265`
467
- );
468
- }
469
-
470
- function flag(argv, name, def = null) {
471
- const i = argv.indexOf(`--${name}`);
472
- return i >= 0 ? argv[i + 1] : def;
473
- }
474
-
475
- const commands = {
476
- memmap: cmdMemmap,
477
-
478
- /** Full prose for an address — the "what is $D011 again?" command. */
479
- async lookup(argv) {
480
- for (const a of argv) {
481
- const addr = parseAddr(a);
482
- const hits = lookup(addr);
483
- console.log(`\n=== ${hex(addr)} ===`);
484
- if (!hits.length) {
485
- console.log("(not in memory map)");
486
- continue;
487
- }
488
- for (const e of hits) {
489
- const which = e.start === e.end ? hex(e.start) : `${hex(e.start)}-${hex(e.end)}`;
490
- const tags = [e.sym, e.reg].filter(Boolean).join(", ");
491
- console.log(`${which}${tags ? ` (${tags})` : ""} [${e.section || "-"}] <${e.src}>`);
492
- console.log(e.desc.replace(/(.{1,96})(\s|$)/g, " $1\n").trimEnd());
493
- for (const b of e.bits || []) console.log(` bit ${b.bit.padEnd(4)} ${b.desc}`);
494
- }
495
- }
496
- },
497
-
498
- /**
499
- * annotate --file listing.asm [--out f.asm] document a listing on disk
500
- * annotate [--out f.asm] ... or one piped in on stdin
501
- *
502
- * The input is any text carrying 6502 mnemonics: hand-written source, or a
503
- * listing from whichever disassembler produced it. Every line, blank ones
504
- * included, is kept.
505
- */
506
- async annotate(argv) {
507
- const outFile = flag(argv, "out");
508
- const inFile = flag(argv, "file");
509
- const maxSpan = Number(flag(argv, "max-span", INLINE_SPAN_MAX));
510
- // A presence test, not `flag()`: `flag()` returns the argv element *following*
511
- // the named flag, which for a valueless flag would silently swallow whatever
512
- // comes next (e.g. `--no-header --file game.asm` would read "--file" as the
513
- // value of `--no-header`).
514
- const noHeader = argv.includes("--no-header");
515
-
516
- // `--file -` and a bare `annotate` both mean stdin; fd 0 reads it whole.
517
- const src = !inFile || inFile === "-" ? 0 : inFile;
518
- const lines = readFileSync(src, "utf8").replace(/\n$/, "").split("\n");
519
-
520
- const text = annotate(lines, { maxSpan, noHeader });
521
- if (outFile) {
522
- writeFileSync(outFile, text + "\n");
523
- console.log(`wrote ${outFile} (${text.split("\n").length} lines)`);
524
- } else {
525
- console.log(text);
526
- }
527
- },
528
-
529
- };
530
-
531
- // `lookup` is exported so anything else that needs to name an address can reuse
532
- // this table instead of reimplementing it. Guarding the CLI dispatch below keeps
533
- // such an import from also running the CLI.
534
- export { lookup };
535
-
536
- if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
537
- const [cmd, ...rest] = process.argv.slice(2);
538
- if (!cmd || !commands[cmd]) {
539
- console.error(`usage: node driver.mjs <command>
540
-
541
- lookup <addr>... full memory-map prose for an address
542
- annotate --file <listing> document a listing or .asm file
543
- [--out f.asm] [--max-span N] [--no-header]
544
- annotate ... the same, reading stdin
545
- memmap (re)build memmap.json from the four sources
546
- (the only command needing a network)`);
547
- process.exit(cmd ? 1 : 0);
548
- }
549
- commands[cmd](rest).catch((e) => {
550
- console.error(`error: ${e.message}`);
551
- process.exit(1);
552
- });
553
- }
@@ -1,87 +0,0 @@
1
- ---
2
- name: c64-petcat
3
- description: Convert a Commodore .prg between PETSCII and ASCII, detokenize a BASIC program into readable text, and read the machine-code handover address out of its startup line, using VICE's own petcat conversion tool as the reference implementation. Use when asked to detokenize a BASIC listing, list what a BASIC stub does, find where a program hands over to machine code, or convert C64 text between PETSCII and ASCII.
4
- ---
5
-
6
- # Detokenizing BASIC with petcat
7
-
8
- One capability, one script, one binary (`petcat`) reached only through the
9
- host-tool execution seam:
10
-
11
- ```bash
12
- S=src/skills/c64-petcat/scripts/petcat.mjs # from the repo root
13
-
14
- node $S decode --image path/to/program.prg # detokenize + resolve the SYS handover
15
- ```
16
-
17
- The script wraps `petcat` and nothing else. The script requires `--image`.
18
- `--out-dir` is optional, defaulting to the image's own directory, exactly like
19
- `acme-build`'s own `--out-dir` default. The script resolves both
20
- **workspace-relative** to the smallest ancestor directory containing both.
21
- This happens before the request ever reaches the seam — the same resolution
22
- `acme-build`/`c64-disk-access` already go through.
23
-
24
- Options: `--image PATH` `--out-dir DIR` `--json`.
25
-
26
- ## Detokenizing and resolving the handover point
27
-
28
- ```bash
29
- node $S decode --image game.prg --json
30
- ```
31
-
32
- Prints the seam's response verbatim as one line of JSON:
33
-
34
- ```json
35
- {"ok":true,"tool":"petcat.decode","exitStatus":0,"results":[{"path":"/abs/path/game.bas.txt","sha256":"...","byteLength":21}],"stderrTail":"","entrypoint":2064,"entrypointReason":"literal SYS argument on BASIC line 10: sys2064"}
36
- ```
37
-
38
- `results[0].path` names the detokenized listing file the seam wrote —
39
- `petcat`'s own decoded text, captured and digested. The listing itself never
40
- crosses inline, no matter how short it is.
41
-
42
- Two fields carry the handover verdict, and BOTH are present on every
43
- `ok: true` response:
44
-
45
- - **`entrypoint`** — a number when the program's `SYS` argument is a literal
46
- decimal value, `null` otherwise.
47
- - **`entrypointReason`** — always a string. When `entrypoint` is a number, it
48
- names the BASIC line the value came from. When `entrypoint` is `null`, it
49
- quotes the unresolved expression verbatim, or states that the listing
50
- carries no handover instruction at all.
51
-
52
- A `null` entrypoint is **not a failure** — it is a resolved "no": the tool
53
- detokenized the program correctly and correctly concluded the entry point is
54
- not a static address. Reported this way, never as a guessed address: a
55
- guessed entry point is expensive downstream, spent on a disassembler that
56
- then has nothing real to work from.
57
-
58
- Non-JSON mode prints the same information as two lines — the resolved entry
59
- point (or the decline and its reason) and the listing file's path — never the
60
- listing's contents inline.
61
-
62
- ## The BASIC dialect
63
-
64
- Fixed server-side, not a flag on this script or a field on the wire — this
65
- project's target is fixed to C64 BASIC V2.0 everywhere already, the same
66
- posture `acme-build` already takes for its own assembler target.
67
-
68
- ## Failure shape
69
-
70
- The seam reports a file `petcat` does not recognise as a BASIC program at
71
- all — including a missing file — as `{"ok":false,"message":"..."}` with a
72
- non-zero exit code. It never reports a success envelope carrying an empty
73
- or guessed verdict. `petcat` itself exits `0` even on garbage input. The
74
- seam's own classifier, not the exit code, is what decides success here.
75
-
76
- ## What this skill does NOT do
77
-
78
- - **No direct binary spawn.** `petcat` runs host-side. This script only ever
79
- constructs a typed request and reads the produced listing file back off
80
- the shared workspace tree — the host-tool execution seam is the only
81
- route.
82
- - **No guessed entry point.** A computed or otherwise unresolvable `SYS`
83
- argument is always reported as a named decline with `entrypoint: null` —
84
- never a fallback value, never an inline listing scan for "something that
85
- looks like an address".
86
- - **No emulator dependency.** This skill names no VICE emulator tool at all
87
- — it works entirely on files, never on a running machine, by construction.