@henols/c64-re-tools 2.0.0-rc.2 → 2.0.0-rc.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/README.md +19 -59
  2. package/package.json +1 -1
  3. package/skills/c64-assembler/SKILL.md +5 -2
  4. package/skills/c64-assembler/scripts/assemble.ts +8 -1
  5. package/skills/c64-assembler/scripts/compare.ts +84 -0
  6. package/skills/c64-basic/SKILL.md +1 -2
  7. package/skills/c64-disk/SKILL.md +20 -6
  8. package/skills/c64-disk/scripts/containers.ts +124 -0
  9. package/skills/c64-disk/scripts/disk.ts +59 -5
  10. package/skills/c64-emulator/SKILL.md +11 -4
  11. package/skills/c64-knowledge/SKILL.md +53 -3
  12. package/skills/c64-knowledge/scripts/knowledge.ts +258 -29
  13. package/skills/c64-memory-map/SKILL.md +4 -1
  14. package/skills/c64-memory-map/scripts/memmap.ts +14 -3
  15. package/skills/c64-provenance/SKILL.md +37 -9
  16. package/skills/c64-provenance/scripts/compare.ts +60 -29
  17. package/skills/c64-provenance/scripts/provenance.ts +248 -19
  18. package/skills/c64-provenance/scripts/runtime.ts +179 -0
  19. package/skills/c64-provenance/scripts/separate.ts +132 -0
  20. package/skills/c64-reverse-engineering/SKILL.md +65 -11
  21. package/skills/c64-source/SKILL.md +119 -0
  22. package/skills/c64-source/scripts/generate.ts +402 -0
  23. package/skills/c64-source/scripts/loader.ts +320 -0
  24. package/skills/c64-source/scripts/opcodes.ts +61 -0
  25. package/skills/c64-source/scripts/source.ts +558 -0
  26. package/skills/c64-static-analysis/SKILL.md +27 -4
  27. package/skills/c64-static-analysis/scripts/analyze.ts +102 -13
  28. package/skills/c64-static-analysis/scripts/coverage.ts +78 -0
  29. package/skills/c64-static-analysis/scripts/findings.ts +6 -4
  30. package/skills/c64-static-analysis/scripts/idioms.ts +144 -0
  31. package/skills/c64-static-analysis/scripts/instructions.ts +105 -0
  32. package/skills/c64-testing/SKILL.md +27 -3
  33. package/skills/c64-testing/references/scenario-format.md +43 -3
  34. package/skills/c64-testing/scripts/run.ts +189 -0
  35. package/skills/c64-testing/scripts/scenario.ts +153 -135
  36. package/skills/c64-testing/scripts/test.ts +16 -6
  37. package/skills/c64-unpacker/SKILL.md +65 -14
  38. package/skills/c64-unpacker/scripts/evidence.ts +86 -21
  39. package/skills/c64-unpacker/scripts/loads.ts +174 -0
  40. package/skills/c64-unpacker/scripts/machine.ts +68 -0
  41. package/skills/c64-unpacker/scripts/unpack.ts +302 -48
  42. package/src/c64.ts +91 -0
  43. package/src/cli/status.ts +3 -1
  44. package/src/host/main.ts +10 -25
  45. package/src/host/server.ts +8 -40
  46. package/src/host/vice/adapter.ts +25 -10
  47. package/src/host/vice/session.ts +64 -20
  48. package/src/host-client/autostart.ts +45 -0
  49. package/src/host-client/connect.ts +4 -5
  50. package/src/host-client/coverage.ts +59 -0
  51. package/src/host-client/steps.ts +170 -0
  52. package/src/host-client/transfer.ts +29 -3
  53. package/src/host-client/vice-session.ts +42 -13
  54. package/src/knowledge/history.ts +40 -13
  55. package/src/knowledge/import.ts +72 -56
  56. package/src/knowledge/read.ts +203 -52
  57. package/src/knowledge/schema.ts +94 -12
  58. package/src/knowledge/write.ts +432 -123
  59. package/src/mcp/main.ts +1 -4
  60. package/src/mcp/output.ts +32 -0
  61. package/src/mcp/server.ts +4 -2
  62. package/src/mcp/tools/debug.ts +110 -24
  63. package/src/mcp/tools/input.ts +1 -0
  64. package/src/mcp/tools/media.ts +22 -6
  65. package/src/mcp/tools/memory.ts +57 -10
  66. package/src/mcp/tools/video.ts +9 -3
  67. package/src/native/discover.ts +5 -2
  68. package/src/native/ghidra/index.ts +19 -3
  69. package/src/native/ghidra/scripts/C64Export.java +33 -1
  70. package/src/native/status.ts +15 -3
  71. package/src/native/types.ts +30 -1
  72. package/{skills/c64-memory-map/scripts → src}/platform.ts +18 -2
  73. package/src/protocol/messages.ts +17 -10
  74. package/src/protocol/vice.ts +72 -18
  75. package/src/script.ts +3 -2
@@ -3,33 +3,59 @@
3
3
  // wrote and then executed: the run-time evidence that also finds a simple
4
4
  // cruncher whose bytes look like code. "capture" runs the program in an
5
5
  // emulator until it reaches a given address (the unpacked entry point) and
6
- // writes the memory into the project. No native unpack tool is used.
6
+ // writes the memory into the project. "loads" captures each part of a
7
+ // multi-load program (D53). No native unpack tool is used.
7
8
 
8
- import { mkdirSync, writeFileSync } from "node:fs";
9
- import { dirname } from "node:path";
10
9
  import { parseArgs } from "node:util";
11
10
 
12
- import { formatC64Address, parseC64Address, textToPetscii } from "#src/c64.ts";
11
+ import { EXECUTE, formatC64Address, parseC64Address, READ, textToPetscii, WRITE } from "#src/c64.ts";
12
+ import { autostartLoaded, LOAD_LIMIT_FRAMES } from "#src/host-client/autostart.ts";
13
+ import { executedIn, readCoverage, writtenBlocks } from "#src/host-client/coverage.ts";
14
+ import { checkActionSteps, runActionStep, type ActionStep } from "#src/host-client/steps.ts";
13
15
  import { WireFailure } from "#src/host-client/tools.ts";
14
- import { readProjectFile } from "#src/host-client/transfer.ts";
16
+ import { readProjectFile, writeProjectFile } from "#src/host-client/transfer.ts";
15
17
  import { ViceSessionClient } from "#src/host-client/vice-session.ts";
16
18
  import { resolveProjectPath } from "#src/project.ts";
17
19
  import { runScript, UsageError } from "#src/script.ts";
18
20
  import { inspect, PackingError, readMemoryMap, underRom, writtenThenExecuted } from "./evidence.ts";
21
+ import { loads, readLoadSteps } from "./loads.ts";
22
+ import { entryState, IO_AREAS, json, machineState, readRam, READY_FRAMES, vectorsOf } from "./machine.ts";
19
23
 
20
24
  const USAGE = `unpack.ts inspect <program.prg>
21
- unpack.ts trace <program.prg> [--frames <n>] [--entry <address>]
22
- unpack.ts capture <program.prg> --until <address> --out <file> [--range <start> <end>] [--timeout-frames <n>]
25
+ unpack.ts trace <program.prg> [--frames <n>] [--entry <address>] [--steps <file>]
26
+ unpack.ts capture <program.prg> --until <address> --out <dir> [--range <start> <end>] [--timeout-frames <n>] [--steps <file>] [--min-stage <bytes>]
27
+ unpack.ts stages <program.prg> [--frames <n>] [--entry <address>] [--steps <file>] [--min-stage <bytes>]
28
+ unpack.ts loads <program> --steps <file> --out <dir> [--drive] [--min-block <bytes>]
23
29
 
24
30
  inspect packing evidence: the BASIC start and the entropy of the bytes
25
31
  trace run the program for <n> frames (default 3000) and report the memory
26
32
  that it wrote and then executed; --entry starts it there instead of RUN
27
- capture run the program until the CPU gets to <address>, then write memory:
28
- with --range, a PRG of that range; without it, all 64 KiB of RAM
33
+ capture run the program until the CPU gets to <address>, and write into the
34
+ directory --out: image.bin (64 KiB of RAM), image.json (the entry,
35
+ the segments that the stages wrote, the vectors, the stages), a PRG
36
+ for each segment, and entry_state.json (registers, $00/$01, pages
37
+ 0-3, VIC, SID and CIA registers, vectors, and what differs from a
38
+ clean reset). --range gives one segment in place of the found ones.
39
+
40
+ stages run the program for <n> frames (default 3000) and report each stage:
41
+ where it starts, the memory it wrote, and where the next one starts.
42
+ A stage starts when the CPU executes a block of at least
43
+ --min-stage bytes (default 64) that the stage before wrote.
44
+
45
+ loads run a multi-load program (a PRG, or a disk, tape or cartridge image)
46
+ with the steps file; each capture step {"capture": "<name>"} writes
47
+ <dir>/<name>/ with image.bin, image.json, entry_state.json and a PRG
48
+ for each block of at least --min-block bytes (default 64) that the C64
49
+ side changed since the previous capture. --drive also writes the 2 KiB
50
+ drive RAM as drive8.prg, for knowledge space drive8.
51
+
52
+ --steps <file> a JSON list of input steps that run right after the program
53
+ starts: write, registers, joystick, type, frames and runUntil, as in
54
+ c64-testing scenarios, with $ addresses only. Use it to press a key
55
+ in an intro or to get through a menu.
29
56
 
30
57
  Addresses are $ and four hex digits. The result is one JSON object.`;
31
58
 
32
- const MAX_READ = 4096;
33
59
 
34
60
  function address(text: string, option: string): number {
35
61
  try {
@@ -41,13 +67,36 @@ function address(text: string, option: string): number {
41
67
 
42
68
  /** Written-then-executed bytes from this many on are unpacking, decrypting or relocating, not self-changing code. */
43
69
  const UNPACKED_BYTES = 256;
44
- const LOAD_POLL_FRAMES = 25;
45
- const LOAD_LIMIT_FRAMES = 1500;
46
70
  const MAX_ADVANCE = 10_000;
47
71
  /** The most ranges one memory map read gives. */
48
72
  const MAX_RANGES = 1000;
49
73
 
50
- async function trace(program: string, options: { frames: number; entry?: number }): Promise<{ output: Record<string, unknown>; failed: boolean }> {
74
+ /** Reads and checks a steps file: action steps with $ addresses only (D43). */
75
+ function readSteps(path: string): ActionStep[] {
76
+ let value: unknown;
77
+ try {
78
+ value = JSON.parse(readProjectFile(path).bytes.toString("utf8"));
79
+ } catch (error) {
80
+ if (error instanceof WireFailure) throw error;
81
+ throw new UsageError(`--steps ${path} is not JSON: ${(error as Error).message}`);
82
+ }
83
+ return checkActionSteps(value, (message) => {
84
+ throw new UsageError(`--steps ${path}: ${message}`);
85
+ });
86
+ }
87
+
88
+ /** Runs the steps; the reason when a runUntil step did not reach its target. */
89
+ async function runSteps(client: ViceSessionClient, steps: ActionStep[]): Promise<string | undefined> {
90
+ for (const [index, step] of steps.entries()) {
91
+ const notReached = await runActionStep(client, step, parseC64Address);
92
+ if (notReached !== undefined && "runUntil" in step) {
93
+ return `Step ${index + 1} did not reach its target in ${step.timeoutFrames} frames. It stopped by ${notReached.stopReason} at ${formatC64Address(notReached.pc)}.`;
94
+ }
95
+ }
96
+ return undefined;
97
+ }
98
+
99
+ async function trace(program: string, options: { frames: number; entry?: number; steps: ActionStep[] }): Promise<{ output: Record<string, unknown>; failed: boolean }> {
51
100
  const { bytes } = readProjectFile(program);
52
101
  if (bytes.length < 3) throw new UsageError("the file is too short for a PRG");
53
102
  const load = bytes[0]! | (bytes[1]! << 8);
@@ -56,17 +105,12 @@ async function trace(program: string, options: { frames: number; entry?: number
56
105
  const client = await ViceSessionClient.open({ videoStandard: "pal" });
57
106
  try {
58
107
  // Loaded but not started, so the memory map can start empty: the KERNAL start-up writes all of RAM.
59
- await client.autostart({ path: program, index: 0, run: false });
60
- const head = bytes.subarray(2, Math.min(bytes.length, 2 + 64)).toString("hex");
61
- let loaded = false;
62
- for (let frames = 0; frames < LOAD_LIMIT_FRAMES && !loaded; frames += LOAD_POLL_FRAMES) {
63
- await client.execution({ action: "advance-frames", count: LOAD_POLL_FRAMES, space: "c64" });
64
- loaded = (await client.memoryRead({ address: load, size: head.length / 2, space: "c64", view: "ram" })).data === head;
65
- }
66
- if (!loaded) return { output: { traced: false, reason: `The program was not in memory at ${formatC64Address(load)} after ${LOAD_LIMIT_FRAMES} frames.` }, failed: true };
108
+ if (!(await autostartLoaded(client, program, bytes))) return { output: { traced: false, reason: `The program was not in memory at ${formatC64Address(load)} after ${LOAD_LIMIT_FRAMES} frames.` }, failed: true };
67
109
  await client.memmap({ action: "clear" });
68
110
  if (options.entry !== undefined) await client.registersSet("c64", { pc: options.entry });
69
111
  else await client.keyboard(textToPetscii("RUN\n"));
112
+ const stepFailed = await runSteps(client, options.steps);
113
+ if (stepFailed !== undefined) return { output: { traced: false, reason: stepFailed }, failed: true };
70
114
  let ran = 0;
71
115
  while (ran < options.frames) {
72
116
  const step = await client.execution({ action: "advance-frames", count: Math.min(MAX_ADVANCE, options.frames - ran), space: "c64" });
@@ -75,11 +119,12 @@ async function trace(program: string, options: { frames: number; entry?: number
75
119
  if (advanced === 0) break;
76
120
  }
77
121
  const { pc } = await client.registersGet("c64");
78
- const ranges = await readMemoryMap(async (start, end) => {
79
- const map = await client.memmap({ action: "read", start, end, maxRanges: MAX_RANGES });
122
+ const ranges = await readMemoryMap(async (start) => {
123
+ const map = await client.memmap({ action: "read", start, end: 0xffff, maxRanges: MAX_RANGES });
80
124
  if (!("ranges" in map)) throw new WireFailure("operation-failed", "The emulator did not give the memory map.");
81
- return map.ranges;
82
- }, MAX_RANGES);
125
+ const ranges = map.ranges.map(({ start: from, end, flags }) => ({ start: from, end, read: (flags & READ) !== 0, write: (flags & WRITE) !== 0, execute: (flags & EXECUTE) !== 0 }));
126
+ return map.next === undefined ? { ranges } : { ranges, next: map.next };
127
+ });
83
128
  const { written, bytes: count, running } = writtenThenExecuted(ranges, pc);
84
129
  const packing = count >= UNPACKED_BYTES || running !== undefined ? "likely" : count > 0 ? "unclear" : "unlikely";
85
130
  return {
@@ -112,37 +157,214 @@ async function trace(program: string, options: { frames: number; entry?: number
112
157
  }
113
158
  }
114
159
 
115
- async function capture(program: string, options: { until: number; out: string; range?: [number, number]; timeoutFrames: number }): Promise<{ output: Record<string, unknown>; failed: boolean }> {
160
+ /** Frames between two looks at the memory map while stages are followed. */
161
+ const STAGE_CHUNK_FRAMES = 25;
162
+ const MAX_STAGES = 16;
163
+ const STAGE_SNAPSHOT = "stage-chunk";
164
+
165
+ interface Stage {
166
+ entry: number;
167
+ firstFrame: number;
168
+ frames: number;
169
+ wrote: Array<{ start: number; end: number }>;
170
+ next?: number;
171
+ }
172
+
173
+ interface Followed {
174
+ stages: Stage[];
175
+ frames: number;
176
+ /** True when the CPU got to `until`. */
177
+ reached: boolean;
178
+ /** Why the program did not start, when it did not. */
179
+ failed?: string;
180
+ }
181
+
182
+ /**
183
+ * Starts the program and follows its stages (D43). The memory map holds what
184
+ * the current stage wrote; when the CPU executed a block of at least
185
+ * `minStage` written bytes during a chunk of frames, the chunk runs again from
186
+ * its snapshot with exec breakpoints over those blocks, which stop at the
187
+ * first instruction of the next stage. Steps run in the tracked time: a frames
188
+ * step is tracked, the others act between two chunks. With `until`, the
189
+ * run ends when the CPU gets there.
190
+ */
191
+ async function follow(
192
+ client: ViceSessionClient,
193
+ program: string,
194
+ options: { frames: number; entry?: number; steps: ActionStep[]; minStage: number; until?: number },
195
+ ): Promise<Followed> {
196
+ const { bytes } = readProjectFile(program);
197
+ if (bytes.length < 3) throw new UsageError("the file is too short for a PRG");
198
+ const load = bytes[0]! | (bytes[1]! << 8);
199
+ const sys = inspect(bytes).basicStart?.sys;
200
+ if (options.entry === undefined && sys === undefined) throw new UsageError("the program has no BASIC start line; give --entry <address>");
201
+ if (!(await autostartLoaded(client, program, bytes))) return { stages: [], frames: 0, reached: false, failed: `The program was not in memory at ${formatC64Address(load)} after ${LOAD_LIMIT_FRAMES} frames.` };
202
+ await client.memmap({ action: "clear" });
203
+ if (options.entry !== undefined) await client.registersSet("c64", { pc: options.entry });
204
+ else await client.keyboard(textToPetscii("RUN\n"));
205
+ const target = options.until === undefined ? undefined : await client.breakpoint({ action: "add", address: options.until, space: "c64" });
206
+ const found: Stage[] = [];
207
+ let entry = options.entry ?? parseC64Address(sys!);
208
+ let firstFrame = 0;
209
+ let total = 0;
210
+ let ended = false;
211
+ let reached = false;
212
+ const atTarget = (pc: number) => options.until !== undefined && pc === options.until;
213
+ const track = async (frames: number): Promise<void> => {
214
+ let left = frames;
215
+ while (left > 0 && !ended && !reached && found.length < MAX_STAGES) {
216
+ await client.snapshot({ action: "save", name: STAGE_SNAPSHOT });
217
+ const chunk = Math.min(STAGE_CHUNK_FRAMES, left);
218
+ const first = await client.execution({ action: "advance-frames", count: chunk, space: "c64" });
219
+ const firstPc = first.pc ?? (await client.registersGet("c64")).pc;
220
+ const map = await readCoverage(client);
221
+ const entered = writtenBlocks(map, options.minStage).filter((block) => executedIn(map, block));
222
+ if (entered.length === 0) {
223
+ const advanced = first.advancedFrames ?? 0;
224
+ total += advanced;
225
+ left -= advanced;
226
+ if (atTarget(firstPc)) {
227
+ // The stop at until comes before its instruction runs: a target in written memory starts a stage too.
228
+ const blocks = writtenBlocks(map, options.minStage);
229
+ if (blocks.some((block) => firstPc >= block.start && firstPc <= block.end)) {
230
+ found.push({ entry, firstFrame, frames: total - firstFrame, wrote: blocks, next: firstPc });
231
+ entry = firstPc;
232
+ firstFrame = total;
233
+ await client.memmap({ action: "clear" });
234
+ }
235
+ reached = true;
236
+ } else if (advanced === 0) ended = true;
237
+ continue;
238
+ }
239
+ // Again from the start of the chunk, with breakpoints over the blocks: the stop is the next entry.
240
+ await client.snapshot({ action: "restore", name: STAGE_SNAPSHOT });
241
+ const points = [];
242
+ for (const block of entered) points.push(await client.breakpoint({ action: "add", address: block.start, size: block.end - block.start + 1, space: "c64" }));
243
+ const rerun = await client.execution({ action: "advance-frames", count: chunk, space: "c64" });
244
+ for (const point of points) await client.breakpoint({ action: "remove", id: point.id });
245
+ const next = rerun.pc ?? (await client.registersGet("c64")).pc;
246
+ const used = rerun.advancedFrames ?? 0;
247
+ total += used;
248
+ left -= used;
249
+ if (entered.some((block) => next >= block.start && next <= block.end)) {
250
+ found.push({ entry, firstFrame, frames: total - firstFrame, wrote: writtenBlocks(await readCoverage(client), options.minStage), next });
251
+ entry = next;
252
+ firstFrame = total;
253
+ await client.memmap({ action: "clear" });
254
+ } else if (!atTarget(next)) {
255
+ throw new WireFailure("operation-failed", `The run from the snapshot did not enter the written memory again; it stopped at ${formatC64Address(next)}.`);
256
+ }
257
+ if (atTarget(next)) reached = true;
258
+ }
259
+ };
260
+ for (const step of options.steps) {
261
+ if ("frames" in step) await track(step.frames);
262
+ else await runActionStep(client, step, parseC64Address);
263
+ }
264
+ await track(Math.max(0, options.frames - total));
265
+ if (target !== undefined) await client.breakpoint({ action: "remove", id: target.id });
266
+ await client.snapshot({ action: "discard", name: STAGE_SNAPSHOT }).catch(() => undefined);
267
+ found.push({ entry, firstFrame, frames: total - firstFrame, wrote: writtenBlocks(await readCoverage(client), options.minStage) });
268
+ return { stages: found, frames: total, reached };
269
+ }
270
+
271
+ const showRange = (range: { start: number; end: number }) => ({ start: formatC64Address(range.start), end: formatC64Address(range.end) });
272
+ const showStages = (stages: Stage[]) =>
273
+ stages.map((stage, index) => ({
274
+ stage: index,
275
+ entry: formatC64Address(stage.entry),
276
+ frames: stage.frames,
277
+ wrote: stage.wrote.map(showRange),
278
+ ...(stage.next === undefined ? {} : { next: formatC64Address(stage.next) }),
279
+ }));
280
+
281
+ async function stages(program: string, options: { frames: number; entry?: number; steps: ActionStep[]; minStage: number }): Promise<{ output: Record<string, unknown>; failed: boolean }> {
282
+ const client = await ViceSessionClient.open({ videoStandard: "pal" });
283
+ try {
284
+ const followed = await follow(client, program, options);
285
+ if (followed.failed !== undefined) return { output: { followed: false, reason: followed.failed }, failed: true };
286
+ return {
287
+ output: {
288
+ followed: true,
289
+ frames: followed.frames,
290
+ stages: showStages(followed.stages),
291
+ pc: formatC64Address((await client.registersGet("c64")).pc),
292
+ ...(followed.stages.length > MAX_STAGES ? { moreStages: true } : {}),
293
+ },
294
+ failed: false,
295
+ };
296
+ } finally {
297
+ await client.close();
298
+ }
299
+ }
300
+
301
+
302
+ async function capture(
303
+ program: string,
304
+ options: { until: number; out: string; range?: [number, number]; timeoutFrames: number; steps: ActionStep[]; minStage: number },
305
+ ): Promise<{ output: Record<string, unknown>; failed: boolean }> {
116
306
  const client = await ViceSessionClient.open({ videoStandard: "pal" });
117
307
  try {
118
- await client.autostart({ path: program, index: 0, run: true });
119
- const reached = await client.runUntil({ target: { kind: "address", address: options.until, space: "c64" }, timeoutFrames: options.timeoutFrames });
120
- if (!reached.reached) {
308
+ const followed = await follow(client, program, { frames: options.timeoutFrames, steps: options.steps, minStage: options.minStage, until: options.until });
309
+ if (followed.failed !== undefined) return { output: { captured: false, reason: followed.failed }, failed: true };
310
+ const registers = await client.registersGet("c64");
311
+ if (!followed.reached) {
121
312
  return {
122
- output: { captured: false, reason: `The program did not get to ${formatC64Address(options.until)} in ${options.timeoutFrames} frames. It stopped by ${reached.stopReason} at ${formatC64Address(reached.pc)}.` },
313
+ output: { captured: false, reason: `The program did not get to ${formatC64Address(options.until)} in ${options.timeoutFrames} frames. It is at ${formatC64Address(registers.pc)}.`, stages: showStages(followed.stages) },
123
314
  failed: true,
124
315
  };
125
316
  }
126
- const [start, end] = options.range ?? [0x0000, 0xffff];
127
- const memory = Buffer.alloc(end - start + 1);
128
- for (let at = start; at <= end; at += MAX_READ) {
129
- const size = Math.min(MAX_READ, end - at + 1);
130
- // The RAM view: unpacked code is often under the ROMs or the I/O area.
131
- const read = await client.memoryRead({ address: at, size, space: "c64", view: "ram" });
132
- Buffer.from(read.data, "hex").copy(memory, at - start);
133
- }
134
- const bytes = options.range === undefined ? memory : Buffer.concat([Buffer.from([start & 0xff, start >> 8]), memory]);
135
- const path = resolveProjectPath(options.out);
136
- mkdirSync(dirname(path), { recursive: true });
137
- writeFileSync(path, bytes);
138
- const registers = await client.registersGet("c64");
317
+ // The RAM view: unpacked code is often under the ROMs or the I/O area.
318
+ const memory = await readRam(client, 0x0000, 0x10000);
319
+ const state = await machineState(client);
320
+ // The segments: what the stages wrote, without the blocks that hold the code of an earlier stage.
321
+ const earlier = followed.stages.slice(0, -1).map((stage) => stage.entry);
322
+ const written = followed.stages.flatMap((stage, index) => stage.wrote.map((range) => ({ ...range, writtenBy: index })));
323
+ const segments = options.range !== undefined ? [{ start: options.range[0], end: options.range[1] }] : written.filter((range) => !earlier.some((entry) => entry >= range.start && entry <= range.end));
324
+ const file = (path: string) => `${options.out}/${path}`;
325
+ const write = (path: string, bytes: Uint8Array) => writeProjectFile(file(path), bytes, { what: "--out" });
326
+ write("image.bin", memory);
327
+ const files = segments.map((segment) => {
328
+ const name = `segment-${segment.start.toString(16).padStart(4, "0")}-${segment.end.toString(16).padStart(4, "0")}.prg`;
329
+ write(name, Buffer.concat([Buffer.from([segment.start & 0xff, segment.start >> 8]), memory.subarray(segment.start, segment.end + 1)]));
330
+ return name;
331
+ });
332
+ const vectors = vectorsOf(state, memory);
333
+ write(
334
+ "image.json",
335
+ Buffer.from(
336
+ `${JSON.stringify(
337
+ {
338
+ entry: formatC64Address(registers.pc),
339
+ segments: segments.map((segment, index) => ({ ...showRange(segment), ...("writtenBy" in segment ? { writtenBy: segment.writtenBy } : {}), file: files[index] })),
340
+ vectors,
341
+ stages: showStages(followed.stages),
342
+ image: "image.bin",
343
+ },
344
+ null,
345
+ 2,
346
+ )}\n`,
347
+ ),
348
+ );
349
+ // A clean machine at the READY prompt shows what the loader and the stages changed.
350
+ await client.reset({ mode: "hard", run: true });
351
+ await client.execution({ action: "advance-frames", count: READY_FRAMES, space: "c64" });
352
+ const clean = await machineState(client);
353
+ const differs: Array<{ address: string; entry: number; reset: number }> = [];
354
+ const compare = (base: number, entry: Buffer, reset: Buffer) => entry.forEach((value, offset) => value !== reset[offset] && differs.push({ address: formatC64Address(base + offset), entry: value, reset: reset[offset]! }));
355
+ compare(0x0000, state.port, clean.port);
356
+ compare(0x0002, state.pages.subarray(2), clean.pages.subarray(2));
357
+ for (const [name, [address]] of Object.entries(IO_AREAS)) compare(address, state.io[name as keyof typeof IO_AREAS], clean.io[name as keyof typeof IO_AREAS]);
358
+ write("entry_state.json", json(entryState(registers, state, vectors, differs)));
139
359
  return {
140
360
  output: {
141
361
  captured: true,
142
362
  pc: formatC64Address(registers.pc),
143
- output: options.out,
144
- ...(options.range === undefined ? { flat64k: true } : { loadRange: { start: formatC64Address(start), end: formatC64Address(end) } }),
145
- registers: { a: registers.a, x: registers.x, y: registers.y, sp: registers.sp },
363
+ out: options.out,
364
+ files: ["image.json", "image.bin", "entry_state.json", ...files],
365
+ segments: segments.map(showRange),
366
+ stages: followed.stages.length,
367
+ differsFromReset: differs.length,
146
368
  },
147
369
  failed: false,
148
370
  };
@@ -163,6 +385,10 @@ async function run(argv: string[]): Promise<{ output: unknown; failed: boolean }
163
385
  "timeout-frames": { type: "string" },
164
386
  frames: { type: "string" },
165
387
  entry: { type: "string" },
388
+ steps: { type: "string" },
389
+ "min-stage": { type: "string" },
390
+ "min-block": { type: "string" },
391
+ drive: { type: "boolean" },
166
392
  help: { type: "boolean" },
167
393
  },
168
394
  });
@@ -183,8 +409,33 @@ async function run(argv: string[]): Promise<{ output: unknown; failed: boolean }
183
409
  if (rest.length > 0) throw new UsageError("trace takes one program");
184
410
  const frames = values.frames === undefined ? 3000 : Number(values.frames);
185
411
  if (!Number.isInteger(frames) || frames < 1 || frames > 30_000) throw new UsageError("--frames must be from 1 to 30000");
186
- return trace(program, { frames, ...(values.entry === undefined ? {} : { entry: address(values.entry, "--entry") }) });
412
+ const steps = values.steps === undefined ? [] : readSteps(values.steps);
413
+ return trace(program, { frames, steps, ...(values.entry === undefined ? {} : { entry: address(values.entry, "--entry") }) });
414
+ }
415
+ if (command === "stages") {
416
+ if (rest.length > 0) throw new UsageError("stages takes one program");
417
+ const frames = values.frames === undefined ? 3000 : Number(values.frames);
418
+ if (!Number.isInteger(frames) || frames < 1 || frames > 30_000) throw new UsageError("--frames must be from 1 to 30000");
419
+ const minStage = values["min-stage"] === undefined ? 64 : Number(values["min-stage"]);
420
+ if (!Number.isInteger(minStage) || minStage < 1 || minStage > 0x10000) throw new UsageError("--min-stage must be from 1 to 65536");
421
+ const steps = values.steps === undefined ? [] : readSteps(values.steps);
422
+ // A runUntil step cannot be tracked in chunks of frames; capture takes it.
423
+ if (steps.some((step) => "runUntil" in step)) throw new UsageError("stages takes no runUntil step; use frames steps");
424
+ return stages(program, { frames, steps, minStage, ...(values.entry === undefined ? {} : { entry: address(values.entry, "--entry") }) });
425
+ }
426
+ if (command === "loads") {
427
+ if (rest.length > 0) throw new UsageError("loads takes one program");
428
+ if (values.steps === undefined || values.out === undefined) throw new UsageError("loads needs --steps and --out");
429
+ try {
430
+ resolveProjectPath(values.out);
431
+ } catch {
432
+ throw new UsageError(`--out must be a path relative to the project directory that stays inside it, not ${values.out}`);
433
+ }
434
+ const minBlock = values["min-block"] === undefined ? 64 : Number(values["min-block"]);
435
+ if (!Number.isInteger(minBlock) || minBlock < 1 || minBlock > 0x10000) throw new UsageError("--min-block must be from 1 to 65536");
436
+ return loads(program, { out: values.out, steps: readLoadSteps(values.steps), drive: values.drive === true, minBlock });
187
437
  }
438
+ if (values.drive !== undefined || values["min-block"] !== undefined) throw new UsageError("--drive and --min-block work only with loads");
188
439
  if (command !== "capture") throw new UsageError(`unknown command ${command}`);
189
440
  if (values.until === undefined || values.out === undefined) throw new UsageError("capture needs --until and --out");
190
441
  // --range takes two values: the end is the next positional argument.
@@ -203,7 +454,10 @@ async function run(argv: string[]): Promise<{ output: unknown; failed: boolean }
203
454
  }
204
455
  const timeoutFrames = values["timeout-frames"] === undefined ? 3000 : Number(values["timeout-frames"]);
205
456
  if (!Number.isInteger(timeoutFrames) || timeoutFrames < 1 || timeoutFrames > 30_000) throw new UsageError("--timeout-frames must be from 1 to 30000");
206
- return capture(program, { until: address(values.until, "--until"), out: values.out, ...(range === undefined ? {} : { range }), timeoutFrames });
457
+ const steps = values.steps === undefined ? [] : readSteps(values.steps);
458
+ const minStage = values["min-stage"] === undefined ? 64 : Number(values["min-stage"]);
459
+ if (!Number.isInteger(minStage) || minStage < 1 || minStage > 0x10000) throw new UsageError("--min-stage must be from 1 to 65536");
460
+ return capture(program, { until: address(values.until, "--until"), out: values.out, ...(range === undefined ? {} : { range }), timeoutFrames, steps, minStage });
207
461
  }
208
462
 
209
463
  await runScript(
package/src/c64.ts CHANGED
@@ -174,6 +174,34 @@ export function parseTokenized(program: Uint8Array): TokenizedProgram {
174
174
  }
175
175
  }
176
176
 
177
+ /** BASIC V2 keywords for the tokens $80 to $cb, in token order. */
178
+ const KEYWORDS = [
179
+ "END", "FOR", "NEXT", "DATA", "INPUT#", "INPUT", "DIM", "READ", "LET", "GOTO", "RUN", "IF", "RESTORE", "GOSUB", "RETURN", "REM",
180
+ "STOP", "ON", "WAIT", "LOAD", "SAVE", "VERIFY", "DEF", "POKE", "PRINT#", "PRINT", "CONT", "LIST", "CLR", "CMD", "SYS", "OPEN",
181
+ "CLOSE", "GET", "NEW", "TAB(", "TO", "FN", "SPC(", "THEN", "NOT", "STEP", "+", "-", "*", "/", "^", "AND",
182
+ "OR", ">", "=", "<", "SGN", "INT", "ABS", "USR", "FRE", "POS", "SQR", "RND", "LOG", "EXP", "COS", "SIN",
183
+ "TAN", "ATN", "PEEK", "LEN", "STR$", "VAL", "ASC", "CHR$", "LEFT$", "RIGHT$", "MID$", "GO",
184
+ ] as const;
185
+
186
+ /**
187
+ * One BASIC line as text: keywords for tokens, text in quotes and after REM
188
+ * as it is, and {$xx} for bytes that are no character. For evidence, not a listing.
189
+ */
190
+ export function basicLineText(line: TokenizedLine): string {
191
+ let text = `${line.number} `;
192
+ let literal = false;
193
+ for (const byte of line.tokens) {
194
+ if (byte === QUOTE) literal = !literal;
195
+ if (!literal && byte >= 0x80 && byte < 0x80 + KEYWORDS.length) {
196
+ text += KEYWORDS[byte - 0x80];
197
+ if (byte === TOKEN.rem) literal = true;
198
+ } else {
199
+ text += petsciiNameToText(Uint8Array.of(byte));
200
+ }
201
+ }
202
+ return text;
203
+ }
204
+
177
205
  /**
178
206
  * Evaluates a SYS argument that holds only numbers, + - * / and brackets.
179
207
  * Returns undefined for anything that needs run-time values.
@@ -279,3 +307,66 @@ export function findHandoffs(lines: TokenizedLine[]): BasicHandoff[] {
279
307
  }
280
308
  return handoffs;
281
309
  }
310
+
311
+ // Execution coverage (D40): the byte format of a coverage map, and the 6502
312
+ // instruction lengths that turn the instruction starts in it into code ranges.
313
+ // VICE marks only the opcode byte of an executed instruction as executed.
314
+
315
+ /** One byte per address: the access flags as VICE reports them, RAM, ROM and I/O apart. */
316
+ export const COVERAGE = {
317
+ ramRead: 1 << 0,
318
+ ramWrite: 1 << 1,
319
+ ramExecute: 1 << 2,
320
+ romRead: 1 << 3,
321
+ romExecute: 1 << 4,
322
+ ioRead: 1 << 5,
323
+ ioWrite: 1 << 6,
324
+ ioExecute: 1 << 7,
325
+ } as const;
326
+
327
+ export const EXECUTE = COVERAGE.ramExecute | COVERAGE.romExecute | COVERAGE.ioExecute;
328
+ export const READ = COVERAGE.ramRead | COVERAGE.romRead | COVERAGE.ioRead;
329
+ export const WRITE = COVERAGE.ramWrite | COVERAGE.ioWrite;
330
+
331
+ /**
332
+ * The length in bytes of the NMOS 6502/6510 instruction with this opcode, for
333
+ * all 256 opcodes with the undocumented ones. The opcode is aaabbbcc: bbb
334
+ * selects the addressing mode within a group cc. JAM (KIL) and BRK count one byte.
335
+ */
336
+ export function instructionLength(opcode: number): 1 | 2 | 3 {
337
+ const group = opcode & 0b11;
338
+ const mode = (opcode >> 2) & 0b111;
339
+ if (group === 0b00) {
340
+ if (mode === 0b000) return opcode === 0x20 ? 3 : opcode === 0x00 || opcode === 0x40 || opcode === 0x60 ? 1 : 2;
341
+ return ([2, 2, 1, 3, 2, 2, 1, 3] as const)[mode]!;
342
+ }
343
+ if (group === 0b10) {
344
+ if (mode === 0b000) return opcode === 0x02 || opcode === 0x22 || opcode === 0x42 || opcode === 0x62 ? 1 : 2;
345
+ return ([2, 2, 1, 3, 1, 2, 1, 3] as const)[mode]!;
346
+ }
347
+ // Groups 01 and 11 share their addressing modes.
348
+ return ([2, 2, 2, 3, 2, 2, 3, 3] as const)[mode]!;
349
+ }
350
+
351
+ export interface CodeRange {
352
+ start: number;
353
+ end: number;
354
+ }
355
+
356
+ /**
357
+ * The executed code in RAM as ranges: each RAM instruction start in `map`
358
+ * with its operand bytes, by the opcode in `memory` (64 KiB from $0000).
359
+ * `map` starts at `base`. Adjacent and overlapping instructions merge.
360
+ */
361
+ export function executedRamCode(map: Uint8Array, base: number, memory: Uint8Array): CodeRange[] {
362
+ const ranges: CodeRange[] = [];
363
+ for (let offset = 0; offset < map.length; offset++) {
364
+ if ((map[offset]! & COVERAGE.ramExecute) === 0) continue;
365
+ const start = base + offset;
366
+ const end = Math.min(0xffff, start + instructionLength(memory[start]!) - 1);
367
+ const last = ranges.at(-1);
368
+ if (last !== undefined && start <= last.end + 1) last.end = Math.max(last.end, end);
369
+ else ranges.push({ start, end });
370
+ }
371
+ return ranges;
372
+ }
package/src/cli/status.ts CHANGED
@@ -2,6 +2,7 @@
2
2
  // Runtime with its tools.
3
3
 
4
4
  import { hostTools, type ToolCallOptions } from "../host-client/tools.ts";
5
+ import { STOP_AND_ASK } from "../protocol/messages.ts";
5
6
  import type { ToolStatus } from "../protocol/tools.ts";
6
7
 
7
8
  /** One line per tool under `heading`: its version, or why it does not run. */
@@ -28,7 +29,8 @@ export async function hostStatusLines(options: ToolCallOptions = {}): Promise<st
28
29
  try {
29
30
  tools = await hostTools(options);
30
31
  } catch (error) {
31
- return [`Host Runtime: not reachable. ${(error as Error).message}`];
32
+ // status is read by a person: the instruction for an agent (D37) stays out.
33
+ return [`Host Runtime: not reachable. ${(error as Error).message.replace(` ${STOP_AND_ASK}`, "")}`];
32
34
  }
33
35
  return ["Host Runtime: reachable", ...toolLines("Tools on the host:", tools)];
34
36
  }
package/src/host/main.ts CHANGED
@@ -5,7 +5,7 @@ import { parseArgs } from "node:util";
5
5
  import { DEFAULT_HOST_PORT } from "../protocol/messages.ts";
6
6
  import { ProcessSupervisor } from "../native/processes.ts";
7
7
  import { startTrace, trace } from "../trace.ts";
8
- import { isLoopback, ListenError, parsePort, startHostServer, stopSignal } from "./server.ts";
8
+ import { ListenError, parsePort, startHostServer, stopSignal } from "./server.ts";
9
9
  import { createToolDispatcher } from "./tools/index.ts";
10
10
  import { checkViceStarts, findVice } from "./vice/process.ts";
11
11
  import { viceSessionFactory } from "./vice/session.ts";
@@ -13,22 +13,19 @@ import { viceSessionFactory } from "./vice/session.ts";
13
13
  const HELP = `c64-re-tools-host
14
14
 
15
15
  Usage:
16
- npx -y --package=@henols/c64-re-tools@latest c64-re-tools-host [--port <port>] [--listen <address> ...]
16
+ npx -y --package=@henols/c64-re-tools@latest c64-re-tools-host [--port <port>]
17
17
  c64-re-tools-host --help
18
18
 
19
19
  Runs the c64-re-tools Host Runtime in the foreground. It first starts VICE
20
20
  once (x64sc on PATH, or C64RT_VICE) and does not start if VICE cannot run.
21
- It listens on 127.0.0.1:${DEFAULT_HOST_PORT}, starts one VICE emulator for each
21
+ It listens on port ${DEFAULT_HOST_PORT} on every address, so a dev container
22
+ reaches it as host.docker.internal. It starts one VICE emulator for each
22
23
  connected c64-re-tools-mcp process, and runs the tools that come with VICE
23
24
  (c1541, petcat) for skill scripts. Stop it with Ctrl+C; that stops every
24
25
  emulator and tool it started.
25
26
 
26
27
  Options:
27
28
  --port <port> Listen on this port instead (0 picks a free port).
28
- --listen <address> Also listen on this address, on the same port, for
29
- example the container bridge (172.17.0.1). An address
30
- that is not loopback needs C64RT_HOST_TOKEN: set it to
31
- the same secret for this program and for the clients.
32
29
  --help Show this help.`;
33
30
 
34
31
  function log(line: string): void {
@@ -37,9 +34,9 @@ function log(line: string): void {
37
34
  }
38
35
 
39
36
  async function main(): Promise<number> {
40
- let values: { help?: boolean; port?: string; listen?: string[] };
37
+ let values: { help?: boolean; port?: string };
41
38
  try {
42
- ({ values } = parseArgs({ options: { help: { type: "boolean" }, port: { type: "string" }, listen: { type: "string", multiple: true } }, strict: true }));
39
+ ({ values } = parseArgs({ options: { help: { type: "boolean" }, port: { type: "string" } }, strict: true }));
43
40
  } catch (error) {
44
41
  process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n\n${HELP}\n`);
45
42
  return 2;
@@ -56,13 +53,6 @@ async function main(): Promise<number> {
56
53
  return 2;
57
54
  }
58
55
 
59
- const extraHosts = values.listen ?? [];
60
- const token = process.env.C64RT_HOST_TOKEN;
61
- if (extraHosts.some((address) => !isLoopback(address)) && (token === undefined || token.length < 16)) {
62
- process.stderr.write("--listen with an address that is not loopback needs C64RT_HOST_TOKEN with at least 16 characters, so that only your clients can connect.\n");
63
- return 2;
64
- }
65
-
66
56
  // Without an emulator the runtime is useless: refuse to start, by name and with the remedy.
67
57
  try {
68
58
  findVice(process.env);
@@ -90,9 +80,8 @@ async function main(): Promise<number> {
90
80
  let server;
91
81
  try {
92
82
  server = await startHostServer({
83
+ host: "0.0.0.0",
93
84
  port,
94
- extraHosts,
95
- ...(token === undefined || token === "" ? {} : { token }),
96
85
  log,
97
86
  createViceSession: viceSessionFactory({ supervisor, log }),
98
87
  tools: createToolDispatcher({ supervisor, log }),
@@ -102,15 +91,11 @@ async function main(): Promise<number> {
102
91
  process.stderr.write(`Port ${error.port} on ${error.address} is in use. Is another c64-re-tools-host running?\n`);
103
92
  return 1;
104
93
  }
105
- if (error instanceof ListenError && error.code === "EADDRNOTAVAIL") {
106
- process.stderr.write(`The address ${error.address} in --listen does not belong to this machine.\n`);
107
- return 1;
108
- }
109
94
  throw error;
110
95
  }
111
- const addresses = [server.host, ...extraHosts].map((address) => `${address}:${server.port}`);
112
- process.stdout.write(`c64-re-tools-host listening on ${addresses.join(", ")}\n`);
113
- trace().event("host.listening", { addresses });
96
+ const address = `${server.host}:${server.port}`;
97
+ process.stdout.write(`c64-re-tools-host listening on ${address}\n`);
98
+ trace().event("host.listening", { addresses: [address] });
114
99
 
115
100
  trace().event("host.stop", { signal: await stopSignal(log) });
116
101
  await server.close();