@henols/c64-re-tools 2.1.0-rc.1 → 2.1.0

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 (103) hide show
  1. package/package.json +1 -1
  2. package/skills/c64-assembler/SKILL.md +24 -9
  3. package/skills/c64-assembler/scripts/assembler.ts +95 -0
  4. package/skills/c64-basic/SKILL.md +17 -9
  5. package/skills/c64-basic/scripts/basic.ts +26 -36
  6. package/skills/c64-disk/SKILL.md +17 -7
  7. package/skills/c64-disk/scripts/disk.ts +130 -104
  8. package/skills/c64-disk/scripts/lib/containers.ts +10 -12
  9. package/skills/c64-docs/SKILL.md +47 -28
  10. package/skills/c64-docs/references/style.md +6 -6
  11. package/skills/c64-docs/scripts/docs.ts +323 -0
  12. package/skills/c64-docs/scripts/lib/source-map.ts +1 -1
  13. package/skills/c64-emulator/SKILL.md +11 -7
  14. package/skills/c64-knowledge/SKILL.md +34 -21
  15. package/skills/c64-knowledge/scripts/knowledge.ts +597 -718
  16. package/skills/c64-knowledge/scripts/lib/listing.ts +5 -7
  17. package/skills/c64-memory-map/SKILL.md +18 -10
  18. package/skills/c64-memory-map/scripts/memory-map.ts +81 -0
  19. package/skills/c64-provenance/SKILL.md +33 -24
  20. package/skills/c64-provenance/scripts/lib/compare.ts +8 -12
  21. package/skills/c64-provenance/scripts/lib/leftovers.ts +19 -20
  22. package/skills/c64-provenance/scripts/lib/library.ts +15 -25
  23. package/skills/c64-provenance/scripts/lib/runtime.ts +12 -37
  24. package/skills/c64-provenance/scripts/lib/separate.ts +9 -10
  25. package/skills/c64-provenance/scripts/provenance.ts +177 -279
  26. package/skills/c64-reverse-engineering/SKILL.md +13 -12
  27. package/skills/c64-source/SKILL.md +28 -16
  28. package/skills/c64-source/scripts/lib/loader.ts +52 -77
  29. package/skills/c64-source/scripts/lib/source.ts +33 -42
  30. package/skills/c64-source/scripts/{generate.ts → source.ts} +68 -109
  31. package/skills/c64-static-analysis/SKILL.md +24 -13
  32. package/skills/c64-static-analysis/scripts/lib/coverage.ts +3 -4
  33. package/skills/c64-static-analysis/scripts/lib/findings.ts +2 -2
  34. package/skills/c64-static-analysis/scripts/lib/instructions.ts +1 -1
  35. package/skills/c64-static-analysis/scripts/lib/macros.ts +6 -8
  36. package/skills/c64-static-analysis/scripts/{analyze.ts → static-analysis.ts} +58 -118
  37. package/skills/c64-testing/SKILL.md +48 -32
  38. package/skills/c64-testing/references/recording-format.md +48 -19
  39. package/skills/c64-testing/references/scenario-format.md +24 -19
  40. package/skills/c64-testing/scripts/{diff.ts → lib/diff-files.ts} +18 -69
  41. package/skills/c64-testing/scripts/lib/diff.ts +5 -3
  42. package/skills/c64-testing/scripts/lib/play.ts +244 -54
  43. package/skills/c64-testing/scripts/lib/recording.ts +153 -120
  44. package/skills/c64-testing/scripts/{replay.ts → lib/replay.ts} +28 -86
  45. package/skills/c64-testing/scripts/{run.ts → lib/runs.ts} +36 -59
  46. package/skills/c64-testing/scripts/lib/scenario.ts +73 -81
  47. package/skills/c64-testing/scripts/testing.ts +189 -0
  48. package/skills/c64-unpacker/SKILL.md +42 -25
  49. package/skills/c64-unpacker/scripts/lib/evidence.ts +3 -4
  50. package/skills/c64-unpacker/scripts/lib/loads.ts +12 -10
  51. package/skills/c64-unpacker/scripts/lib/machine.ts +0 -2
  52. package/skills/c64-unpacker/scripts/lib/stages.ts +14 -19
  53. package/skills/c64-unpacker/scripts/{unpack.ts → unpacker.ts} +109 -211
  54. package/src/c64.ts +35 -20
  55. package/src/cli/main.ts +2 -2
  56. package/src/errors.ts +38 -0
  57. package/src/graphics.ts +3 -1
  58. package/src/host/server.ts +3 -2
  59. package/src/host/vice/session.ts +4 -4
  60. package/src/host-client/entry-state.ts +106 -0
  61. package/src/host-client/image.ts +21 -10
  62. package/src/host-client/steps.ts +285 -108
  63. package/src/host-client/transfer.ts +21 -15
  64. package/src/host-client/vice-session.ts +1 -1
  65. package/src/knowledge/database.ts +3 -23
  66. package/src/knowledge/expression.ts +8 -11
  67. package/src/knowledge/history.ts +2 -2
  68. package/src/knowledge/import.ts +13 -14
  69. package/src/knowledge/names.ts +50 -0
  70. package/src/knowledge/program.ts +11 -10
  71. package/src/knowledge/read.ts +16 -21
  72. package/src/knowledge/schema.ts +9 -3
  73. package/src/knowledge/text.ts +5 -5
  74. package/src/knowledge/write.ts +93 -137
  75. package/src/mcp/inputs.ts +105 -0
  76. package/src/mcp/output.ts +2 -13
  77. package/src/mcp/server.ts +12 -60
  78. package/src/mcp/tools/debug.ts +17 -12
  79. package/src/mcp/tools/execution.ts +5 -32
  80. package/src/mcp/tools/input.ts +7 -32
  81. package/src/mcp/tools/media.ts +6 -5
  82. package/src/mcp/tools/memory.ts +11 -38
  83. package/src/mcp/tools/video.ts +6 -5
  84. package/src/native/c64run.ts +1 -1
  85. package/src/native/dxa.ts +7 -6
  86. package/src/native/ghidra/analyze.ts +2 -1
  87. package/src/native/ghidra/index.ts +2 -1
  88. package/src/native/processes.ts +4 -3
  89. package/src/native/status.ts +2 -2
  90. package/src/native/types.ts +3 -7
  91. package/src/opcodes.ts +13 -13
  92. package/src/platform.ts +9 -9
  93. package/src/project.ts +16 -26
  94. package/src/protocol/messages.ts +3 -21
  95. package/src/protocol/vice.ts +7 -7
  96. package/src/script.ts +491 -49
  97. package/skills/c64-assembler/scripts/assemble.ts +0 -138
  98. package/skills/c64-docs/scripts/generate.ts +0 -109
  99. package/skills/c64-docs/scripts/link.ts +0 -239
  100. package/skills/c64-memory-map/scripts/memmap.ts +0 -92
  101. package/skills/c64-testing/scripts/checklist.ts +0 -51
  102. package/skills/c64-testing/scripts/record.ts +0 -96
  103. package/skills/c64-testing/scripts/test.ts +0 -65
@@ -6,69 +6,20 @@
6
6
  // writes the memory into the project. "loads" captures each part of a
7
7
  // multi-load program. No native unpack tool is used.
8
8
 
9
- import { parseArgs } from "node:util";
10
-
11
- import { COVERAGE, EXECUTE, formatC64Address, parseC64Address, READ, WRITE } from "#src/c64.ts";
9
+ import { type AddressRange, COVERAGE, EXECUTE, formatC64Address, READ, WRITE } from "#src/c64.ts";
12
10
  import { autostartLoaded, LOAD_LIMIT_FRAMES, RUN_LIMIT_FRAMES, typeRun } from "#src/host-client/autostart.ts";
13
11
  import { readCoverage, writtenBlocks } from "#src/host-client/coverage.ts";
14
- import { checkActionSteps, runActionStep, type ActionStep } from "#src/host-client/steps.ts";
12
+ import { resetToReady } from "#src/host-client/entry-state.ts";
13
+ import { readActionSteps, runActionStep, type ActionStep } from "#src/host-client/steps.ts";
15
14
  import { WireFailure } from "#src/host-client/tools.ts";
16
- import { readProjectFile, writeProjectFile } from "#src/host-client/transfer.ts";
15
+ import { readProjectFile, writeReported, type Written } from "#src/host-client/transfer.ts";
17
16
  import { ViceSessionClient } from "#src/host-client/vice-session.ts";
18
- import { resolveProjectPath } from "#src/project.ts";
19
- import { runScript, UsageError } from "#src/script.ts";
20
- import { basicStart, inspect, PackingError, readMemoryMap, underRom, writtenThenExecuted } from "./lib/evidence.ts";
17
+ import { knowledgeAt, knowledgeNames } from "#src/knowledge/names.ts";
18
+ import { OPTIONS, runScript, UsageError, type Call, type CommandSpec, type OptionSpec } from "#src/script.ts";
19
+ import { basicStart, inspect, readMemoryMap, underRom, writtenThenExecuted } from "./lib/evidence.ts";
21
20
  import { loads, readLoadSteps } from "./lib/loads.ts";
22
- import { callReturn, captureSegments, executedIn, inRanges, mergeMaps, stageCandidates, stagePoints, withoutRanges, type Range } from "./lib/stages.ts";
23
- import { entryState, IO_AREAS, json, machineState, readRam, READY_FRAMES, vectorsOf } from "./lib/machine.ts";
24
-
25
- const USAGE = `unpack.ts inspect <program.prg>
26
- unpack.ts trace <program.prg> [--frames <n>] [--entry <address>] [--steps <file>]
27
- unpack.ts capture <program.prg> --until <address> --out <dir> [--range <start> <end>] [--exclude <start>-<end>]... [--timeout-frames <n>] [--entry <address>] [--steps <file>] [--min-stage <bytes>]
28
- unpack.ts stages <program.prg> [--frames <n>] [--entry <address>] [--steps <file>] [--min-stage <bytes>]
29
- unpack.ts loads <program> --steps <file> --out <dir> [--drive] [--min-block <bytes>]
30
-
31
- inspect packing evidence: the BASIC start and the entropy of the bytes
32
- trace run the program for <n> frames (default 3000) and report the memory
33
- that it wrote and then executed; --entry starts it there instead of RUN
34
- capture run the program until the CPU gets to <address> after the steps, and write into the
35
- directory --out: image.bin (64 KiB of RAM), image.json (the entry,
36
- the segments that the stages wrote, the vectors, the stages), a PRG
37
- for each segment, and entry_state.json (registers, $00/$01, pages
38
- 0-3, VIC, SID and CIA registers, vectors, and what differs from a
39
- clean reset). --range gives one segment in place of the found ones.
40
- --exclude (more than once) leaves a range out of the segments, for
41
- example bytes that a crack left and the program never reads.
42
- --entry starts it there instead of RUN.
43
-
44
- stages run the program for <n> frames (default 3000) and report each stage:
45
- where it starts, the memory it wrote, and where the next one starts.
46
- A stage starts when the CPU executes a block of at least
47
- --min-stage bytes (default 64) that the stage before wrote.
48
-
49
- loads run a multi-load program (a PRG, or a disk, tape or cartridge image)
50
- with the steps file; each capture step {"capture": "<name>"} writes
51
- <dir>/<name>/ with image.bin, image.json, entry_state.json and a PRG
52
- for each block of at least --min-block bytes (default 64) that the C64
53
- side changed since the previous capture. --drive also writes the 2 KiB
54
- drive RAM as drive8.prg, for knowledge space drive8.
55
-
56
- --steps <file> a JSON list of input steps that run right after the program
57
- starts: write, registers, joystick, type, frames and runUntil, as in
58
- c64-testing scenarios, with $ addresses only. Use it to press a key
59
- in an intro or to get through a menu. stages and capture track the
60
- stages in frames steps and in runUntil steps with at alone.
61
-
62
- Addresses are $ and four hex digits. The result is one JSON object.`;
63
-
64
-
65
- function address(text: string, option: string): number {
66
- try {
67
- return parseC64Address(text);
68
- } catch {
69
- throw new UsageError(`${option} must be $ followed by four hex digits, not ${text}`);
70
- }
71
- }
21
+ import { callReturn, captureSegments, executedIn, inRanges, mergeMaps, stageCandidates, stagePoints, withoutRanges } from "./lib/stages.ts";
22
+ import { entryState, IO_AREAS, json, machineState, readRam, vectorsOf } from "./lib/machine.ts";
72
23
 
73
24
  /** Written-then-executed bytes from this many on are unpacking, decrypting or relocating, not self-changing code. */
74
25
  const UNPACKED_BYTES = 256;
@@ -76,26 +27,12 @@ const MAX_ADVANCE = 10_000;
76
27
  /** The most ranges one memory map read gives. */
77
28
  const MAX_RANGES = 1000;
78
29
 
79
- /** Reads and checks a steps file: action steps with $ addresses only. */
80
- function readSteps(path: string): ActionStep[] {
81
- let value: unknown;
82
- try {
83
- value = JSON.parse(readProjectFile(path).bytes.toString("utf8"));
84
- } catch (error) {
85
- if (error instanceof WireFailure) throw error;
86
- throw new UsageError(`--steps ${path} is not JSON: ${(error as Error).message}`);
87
- }
88
- return checkActionSteps(value, (message) => {
89
- throw new UsageError(`--steps ${path}: ${message}`);
90
- });
91
- }
92
-
93
30
  /** Runs the steps; the reason when a runUntil step did not reach its target. */
94
31
  async function runSteps(client: ViceSessionClient, steps: ActionStep[]): Promise<string | undefined> {
95
32
  for (const [index, step] of steps.entries()) {
96
- const notReached = await runActionStep(client, step, parseC64Address);
33
+ const notReached = await runActionStep(client, step, knowledgeAt());
97
34
  if (notReached !== undefined && "runUntil" in step) {
98
- return `Step ${index + 1} did not reach its target in ${step.timeoutFrames} frames. It stopped by ${notReached.stopReason} at ${formatC64Address(notReached.pc)}.`;
35
+ return `Step ${index + 1} did not reach its target in ${step.runUntil.timeoutFrames} frames. It stopped by ${notReached.stopReason} at ${formatC64Address(notReached.pc)}.`;
99
36
  }
100
37
  }
101
38
  return undefined;
@@ -240,9 +177,11 @@ interface Followed {
240
177
  failed?: string;
241
178
  }
242
179
 
243
- /** A runUntil step to an address alone: a breakpoint stops it, so the stages are tracked while it runs. */
244
- function trackedRunUntil(step: ActionStep): step is { runUntil: { at: string }; timeoutFrames: number } {
245
- return "runUntil" in step && "at" in step.runUntil && !("raster" in step.runUntil);
180
+ type RunUntilStep = Extract<ActionStep, { runUntil: unknown }>;
181
+
182
+ /** A runUntil step to an address with no condition: a breakpoint stops it, so the stages are tracked while it runs. */
183
+ function trackedRunUntil(step: ActionStep): step is RunUntilStep & { runUntil: { target: { kind: "address" } } } {
184
+ return "runUntil" in step && step.runUntil.target.kind === "address" && step.runUntil.target.condition === undefined;
246
185
  }
247
186
 
248
187
  /**
@@ -281,7 +220,7 @@ async function follow(
281
220
  let stopAt: number | undefined;
282
221
  const atTarget = (pc: number) => stopAt !== undefined && pc === stopAt;
283
222
  // The current stage: its memory map since it started, chunk by chunk, its own code, and the written code it calls.
284
- let stage = { map: new Uint8Array(0x10000), own: new Set<number>(), called: [] as Range[] };
223
+ let stage = { map: new Uint8Array(0x10000), own: new Set<number>(), called: [] as AddressRange[] };
285
224
  const startStage = async (next: number, last: Uint8Array) => {
286
225
  mergeMaps(stage.map, last);
287
226
  found.push({ entry, firstFrame, frames: total - firstFrame, wrote: writtenBlocks(stage.map, options.minStage), next });
@@ -296,9 +235,9 @@ async function follow(
296
235
  * blocks, and stops at the first opcode that the stage wrote and the CPU
297
236
  * then executes. Own code stops the run once; a write watch arms it again.
298
237
  */
299
- const rerun = async (candidates: Range[], frames: number): Promise<{ used: number; map: Uint8Array; next?: number; atTarget: boolean }> => {
238
+ const rerun = async (candidates: AddressRange[], frames: number): Promise<{ used: number; map: Uint8Array; next?: number; atTarget: boolean }> => {
300
239
  const points = new Map<string, { kind: "exec" | "watch"; id: number }>();
301
- const arm = async (wanted: { exec: Range[]; watch: Range[] }) => {
240
+ const arm = async (wanted: { exec: AddressRange[]; watch: AddressRange[] }) => {
302
241
  const keys = new Set([...wanted.exec.map((range) => `exec:${range.start}:${range.end}`), ...wanted.watch.map((range) => `watch:${range.start}:${range.end}`)]);
303
242
  for (const [key, point] of points) {
304
243
  if (keys.has(key)) continue;
@@ -337,7 +276,7 @@ async function follow(
337
276
  return undefined;
338
277
  } finally {
339
278
  await client.breakpoint({ action: "remove", id: point.id });
340
- await client.snapshot({ action: "discard", name: CALL_SNAPSHOT });
279
+ await client.snapshot({ action: "remove", name: CALL_SNAPSHOT });
341
280
  }
342
281
  };
343
282
  let left = frames;
@@ -448,17 +387,17 @@ async function follow(
448
387
  for (const [index, step] of options.steps.entries()) {
449
388
  if ("frames" in step) await track(step.frames);
450
389
  else if (trackedRunUntil(step)) {
451
- if (!(await trackTo(parseC64Address(step.runUntil.at), step.timeoutFrames))) {
390
+ if (!(await trackTo(knowledgeAt()(step.runUntil.target.address), step.runUntil.timeoutFrames))) {
452
391
  const { pc } = await client.registersGet("c64");
453
- return { stages: found, frames: total, reached: false, failed: `Step ${index + 1} did not reach its target in ${step.timeoutFrames} frames. It is at ${formatC64Address(pc)}.` };
392
+ return { stages: found, frames: total, reached: false, failed: `Step ${index + 1} did not reach its target in ${step.runUntil.timeoutFrames} frames. It is at ${formatC64Address(pc)}.` };
454
393
  }
455
- } else await runActionStep(client, step, parseC64Address);
394
+ } else await runActionStep(client, step, knowledgeAt());
456
395
  }
457
396
  const left = Math.max(0, options.frames - total);
458
397
  let reached = false;
459
398
  if (options.until === undefined) await track(left);
460
399
  else reached = await trackTo(options.until, left);
461
- await client.snapshot({ action: "discard", name: STAGE_SNAPSHOT }).catch(() => undefined);
400
+ await client.snapshot({ action: "remove", name: STAGE_SNAPSHOT }).catch(() => undefined);
462
401
  mergeMaps(stage.map, await readCoverage(client));
463
402
  found.push({ entry, firstFrame, frames: total - firstFrame, wrote: writtenBlocks(stage.map, options.minStage) });
464
403
  return { stages: found, frames: total, reached };
@@ -494,10 +433,9 @@ async function stages(program: string, options: { frames: number; entry?: number
494
433
  }
495
434
  }
496
435
 
497
-
498
436
  async function capture(
499
437
  program: string,
500
- options: { until: number; out: string; range?: [number, number]; exclude: Array<{ start: number; end: number }>; timeoutFrames: number; entry?: number; steps: ActionStep[]; minStage: number },
438
+ options: { until: number; out: string; range?: AddressRange; exclude: AddressRange[]; timeoutFrames: number; entry?: number; steps: ActionStep[]; minStage: number },
501
439
  ): Promise<{ output: Record<string, unknown>; failed: boolean }> {
502
440
  const client = await ViceSessionClient.open({ videoStandard: "pal" });
503
441
  try {
@@ -514,14 +452,15 @@ async function capture(
514
452
  const memory = await readRam(client, 0x0000, 0x10000);
515
453
  const state = await machineState(client);
516
454
  // The segments: what the stages wrote, without loader leftovers, with no overlap.
517
- const found = options.range !== undefined ? [{ start: options.range[0], end: options.range[1] }] : captureSegments(followed.stages, registers.pc);
455
+ const found = options.range !== undefined ? [options.range] : captureSegments(followed.stages, registers.pc);
518
456
  const inside = options.exclude.find((range) => registers.pc >= range.start && registers.pc <= range.end);
519
457
  if (inside !== undefined) {
520
458
  return { output: { captured: false, reason: `The entry ${formatC64Address(registers.pc)} is in --exclude ${formatC64Address(inside.start)}-${formatC64Address(inside.end)}. A segment must hold the entry.` }, failed: true };
521
459
  }
522
460
  const segments = withoutRanges(found, options.exclude);
523
461
  const file = (path: string) => `${options.out}/${path}`;
524
- const write = (path: string, bytes: Uint8Array) => writeProjectFile(file(path), bytes, { what: "--out" });
462
+ const written: Written[] = [];
463
+ const write = (path: string, bytes: Uint8Array) => written.push(writeReported(file(path), bytes, { what: "--out" }));
525
464
  write("image.bin", memory);
526
465
  const files = segments.map((segment) => {
527
466
  const name = `segment-${segment.start.toString(16).padStart(4, "0")}-${segment.end.toString(16).padStart(4, "0")}.prg`;
@@ -547,8 +486,7 @@ async function capture(
547
486
  ),
548
487
  );
549
488
  // A clean machine at the READY prompt shows what the loader and the stages changed.
550
- await client.reset({ mode: "hard", run: true });
551
- await client.execution({ action: "advance-frames", count: READY_FRAMES, space: "c64" });
489
+ await resetToReady(client);
552
490
  const clean = await machineState(client);
553
491
  const differs: Array<{ address: string; entry: number; reset: number }> = [];
554
492
  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]! }));
@@ -561,7 +499,7 @@ async function capture(
561
499
  captured: true,
562
500
  pc: formatC64Address(registers.pc),
563
501
  out: options.out,
564
- files: ["image.json", "image.bin", "entry_state.json", ...files],
502
+ files: written,
565
503
  segments: segments.map(showRange),
566
504
  ...(options.exclude.length === 0 ? {} : { excluded: options.exclude.map(showRange) }),
567
505
  stages: followed.stages.length,
@@ -574,127 +512,87 @@ async function capture(
574
512
  }
575
513
  }
576
514
 
577
- /** The options that each command takes; any other option is refused, not ignored. */
578
- const OPTIONS_OF: Record<string, string[]> = {
579
- inspect: [],
580
- trace: ["frames", "entry", "steps"],
581
- stages: ["frames", "entry", "steps", "min-stage"],
582
- capture: ["until", "out", "range", "exclude", "timeout-frames", "entry", "steps", "min-stage"],
583
- loads: ["steps", "out", "drive", "min-block"],
515
+ const PROGRAM = { name: "<program>", help: "a PRG file, relative to the project folder" };
516
+ const FRAMES: OptionSpec = { ...OPTIONS.frames, max: 30_000, default: 3000, help: "run the program for n frames (default 3000)" };
517
+ const ENTRY: OptionSpec = { ...OPTIONS.entry, help: "start the program here in place of RUN" };
518
+ const STEPS: OptionSpec = { ...OPTIONS.steps, help: "action steps (JSON) that run right after the program starts, for example to press a key in an intro or to get through a menu" };
519
+ const MIN_STAGE: OptionSpec = { kind: "count", value: "<bytes>", min: 1, max: 0x10000, default: 64, help: "a stage starts when the CPU executes a block of at least this many bytes that the stage before wrote (default 64)" };
520
+
521
+ /** The result of a command: PASS, or FAIL with the reason. */
522
+ const outcome = ({ output, failed }: { output: unknown; failed: boolean }) => ({ result: failed ? "FAIL" : "PASS", ...(output as Record<string, unknown>) });
523
+ const entryOf = (call: Call) => {
524
+ const entry = call.address("entry");
525
+ return entry === undefined ? {} : { entry };
526
+ };
527
+ const stepsOf = (call: Call) => {
528
+ const path = call.string("steps");
529
+ return path === undefined ? [] : readActionSteps(path, "--steps");
584
530
  };
585
531
 
586
- async function run(argv: string[]): Promise<{ output: unknown; failed: boolean }> {
587
- const { values, positionals } = parseArgs({
588
- args: argv,
589
- strict: true,
590
- allowPositionals: true,
532
+ const COMMANDS: CommandSpec[] = [
533
+ {
534
+ name: "inspect",
535
+ summary: "Packing evidence: the BASIC start and the entropy of the bytes.",
536
+ subjects: [PROGRAM],
537
+ run: (call) => inspect(readProjectFile(call.subject(0)).bytes),
538
+ },
539
+ {
540
+ name: "trace",
541
+ summary: "Runs the program and reports the memory that it wrote and then executed.",
542
+ subjects: [PROGRAM],
543
+ options: { frames: FRAMES, entry: ENTRY, steps: STEPS },
544
+ run: async (call) => outcome(await trace(call.subject(0), { frames: call.count("frames")!, steps: stepsOf(call), ...entryOf(call) })),
545
+ },
546
+ {
547
+ name: "stages",
548
+ summary: "Runs the program and reports each stage: where it starts, the memory it wrote, and where the next one starts.",
549
+ subjects: [PROGRAM],
550
+ options: { frames: FRAMES, entry: ENTRY, steps: STEPS, "min-stage": MIN_STAGE },
551
+ notes: "stages tracks the stages in frames steps and in runUntil steps to an address target without a condition.",
552
+ run: async (call) => {
553
+ const steps = stepsOf(call);
554
+ // A runUntil step to a raster line or a memory value cannot be tracked in chunks of frames; capture takes it.
555
+ if (steps.some((step) => "runUntil" in step && !trackedRunUntil(step))) throw new UsageError("stages takes a runUntil step only with an address target without a condition. Use frames steps, or capture");
556
+ return outcome(await stages(call.subject(0), { frames: call.count("frames")!, steps, minStage: call.count("min-stage")!, ...entryOf(call) }));
557
+ },
558
+ },
559
+ {
560
+ name: "capture",
561
+ summary: "Runs the program until the CPU gets to --until after the steps, and writes into --out: image.bin (64 KiB of RAM), image.json (the entry, the segments that the stages wrote, the vectors, the stages), a PRG for each segment, and entry_state.json.",
562
+ subjects: [PROGRAM],
591
563
  options: {
592
- until: { type: "string" },
593
- out: { type: "string" },
594
- range: { type: "string" },
595
- exclude: { type: "string", multiple: true },
596
- "timeout-frames": { type: "string" },
597
- frames: { type: "string" },
598
- entry: { type: "string" },
599
- steps: { type: "string" },
600
- "min-stage": { type: "string" },
601
- "min-block": { type: "string" },
602
- drive: { type: "boolean" },
603
- help: { type: "boolean" },
564
+ until: { kind: "address", value: "<address>", required: true, help: "the unpacked entry point" },
565
+ out: { ...OPTIONS.out, value: "<dir>", required: true, help: "the folder for the capture, relative to the project folder" },
566
+ range: { ...OPTIONS.range, help: "one segment in place of the found ones" },
567
+ exclude: { kind: "range", value: "<start>-<end>", multiple: true, help: "a range to leave out of the segments, for example bytes that a crack left and the program never reads" },
568
+ "timeout-frames": { ...OPTIONS["timeout-frames"], max: 30_000, default: 3000, help: "stop the wait for --until after n frames (default 3000)" },
569
+ entry: ENTRY,
570
+ steps: STEPS,
571
+ "min-stage": MIN_STAGE,
604
572
  },
605
- });
606
- if (values.help) throw new UsageError("");
607
- const [command, program, ...rest] = positionals;
608
- if (program === undefined) throw new UsageError("give a command and a program");
609
- const allowed = OPTIONS_OF[command ?? ""];
610
- if (allowed === undefined) throw new UsageError(`unknown command ${command}`);
611
- for (const option of Object.keys(values)) {
612
- if (option !== "help" && !allowed.includes(option)) throw new UsageError(`${command} does not take --${option}`);
613
- }
614
- if (command === "inspect") {
615
- if (rest.length > 0) throw new UsageError("inspect takes one program");
616
- const { bytes } = readProjectFile(program);
617
- try {
618
- return { output: inspect(bytes), failed: false };
619
- } catch (error) {
620
- if (error instanceof PackingError) throw new UsageError(error.message);
621
- throw error;
622
- }
623
- }
624
- if (command === "trace") {
625
- if (rest.length > 0) throw new UsageError("trace takes one program");
626
- const frames = values.frames === undefined ? 3000 : Number(values.frames);
627
- if (!Number.isInteger(frames) || frames < 1 || frames > 30_000) throw new UsageError("--frames must be from 1 to 30000");
628
- const steps = values.steps === undefined ? [] : readSteps(values.steps);
629
- return trace(program, { frames, steps, ...(values.entry === undefined ? {} : { entry: address(values.entry, "--entry") }) });
630
- }
631
- if (command === "stages") {
632
- if (rest.length > 0) throw new UsageError("stages takes one program");
633
- const frames = values.frames === undefined ? 3000 : Number(values.frames);
634
- if (!Number.isInteger(frames) || frames < 1 || frames > 30_000) throw new UsageError("--frames must be from 1 to 30000");
635
- const minStage = values["min-stage"] === undefined ? 64 : Number(values["min-stage"]);
636
- if (!Number.isInteger(minStage) || minStage < 1 || minStage > 0x10000) throw new UsageError("--min-stage must be from 1 to 65536");
637
- const steps = values.steps === undefined ? [] : readSteps(values.steps);
638
- // A runUntil step to a raster line or a memory value cannot be tracked in chunks of frames; capture takes it.
639
- if (steps.some((step) => "runUntil" in step && !trackedRunUntil(step))) throw new UsageError("stages takes a runUntil step only with at alone; use frames steps or runUntil at");
640
- return stages(program, { frames, steps, minStage, ...(values.entry === undefined ? {} : { entry: address(values.entry, "--entry") }) });
641
- }
642
- if (command === "loads") {
643
- if (rest.length > 0) throw new UsageError("loads takes one program");
644
- if (values.steps === undefined || values.out === undefined) throw new UsageError("loads needs --steps and --out");
645
- try {
646
- resolveProjectPath(values.out);
647
- } catch {
648
- throw new UsageError(`--out must be a path relative to the project folder that stays inside it, not ${values.out}`);
649
- }
650
- const minBlock = values["min-block"] === undefined ? 64 : Number(values["min-block"]);
651
- if (!Number.isInteger(minBlock) || minBlock < 1 || minBlock > 0x10000) throw new UsageError("--min-block must be from 1 to 65536");
652
- return loads(program, { out: values.out, steps: readLoadSteps(values.steps), drive: values.drive === true, minBlock });
653
- }
654
- if (values.until === undefined || values.out === undefined) throw new UsageError("capture needs --until and --out");
655
- // --range takes two values: the end is the next positional argument.
656
- let range: [number, number] | undefined;
657
- if (values.range !== undefined) {
658
- if (rest.length !== 1) throw new UsageError("--range takes a start and an end address");
659
- range = [address(values.range, "--range"), address(rest[0]!, "--range")];
660
- if (range[1] < range[0]) throw new UsageError("the --range end must not be before its start");
661
- } else if (rest.length > 0) {
662
- throw new UsageError("too many arguments");
663
- }
664
- const exclude = (values.exclude ?? []).map((value) => {
665
- const parts = value.split("-");
666
- if (parts.length !== 2) throw new UsageError(`--exclude takes <start>-<end>, not ${value}`);
667
- const range = { start: address(parts[0]!, "--exclude"), end: address(parts[1]!, "--exclude") };
668
- if (range.end < range.start) throw new UsageError(`the --exclude end must not be before its start: ${value}`);
669
- return range;
670
- });
671
- try {
672
- resolveProjectPath(values.out);
673
- } catch {
674
- throw new UsageError(`--out must be a path relative to the project folder that stays inside it, not ${values.out}`);
675
- }
676
- const timeoutFrames = values["timeout-frames"] === undefined ? 3000 : Number(values["timeout-frames"]);
677
- if (!Number.isInteger(timeoutFrames) || timeoutFrames < 1 || timeoutFrames > 30_000) throw new UsageError("--timeout-frames must be from 1 to 30000");
678
- const steps = values.steps === undefined ? [] : readSteps(values.steps);
679
- const minStage = values["min-stage"] === undefined ? 64 : Number(values["min-stage"]);
680
- if (!Number.isInteger(minStage) || minStage < 1 || minStage > 0x10000) throw new UsageError("--min-stage must be from 1 to 65536");
681
- return capture(program, {
682
- until: address(values.until, "--until"),
683
- out: values.out,
684
- ...(range === undefined ? {} : { range }),
685
- exclude,
686
- timeoutFrames,
687
- steps,
688
- minStage,
689
- ...(values.entry === undefined ? {} : { entry: address(values.entry, "--entry") }),
690
- });
691
- }
692
-
693
- await runScript(
694
- async (argv) => {
695
- const { output, failed } = await run(argv);
696
- if (failed) process.exitCode = 1;
697
- return output;
573
+ notes: "entry_state.json has the registers, $00/$01, pages 0-3, the VIC, SID and CIA registers, the vectors, and what differs from a clean reset.",
574
+ run: async (call) => {
575
+ const range = call.range("range");
576
+ return outcome(await capture(call.subject(0), { until: call.address("until")!, out: call.string("out")!, ...(range === undefined ? {} : { range }), exclude: call.ranges("exclude"), timeoutFrames: call.count("timeout-frames")!, steps: stepsOf(call), minStage: call.count("min-stage")!, ...entryOf(call) }));
577
+ },
578
+ },
579
+ {
580
+ name: "loads",
581
+ summary: "Runs a multi-load program with the steps file. Each capture step {\"capture\": \"<name>\"} writes <dir>/<name>/ with image.bin, image.json, entry_state.json and a PRG for each block that the C64 side changed since the previous capture.",
582
+ subjects: [{ name: "<program>", help: "a PRG, or a disk, tape or cartridge image, relative to the project folder" }],
583
+ options: {
584
+ steps: { ...STEPS, required: true, help: "action steps and capture steps (JSON)" },
585
+ out: { ...OPTIONS.out, value: "<dir>", required: true, help: "the folder for the captures, relative to the project folder" },
586
+ drive: { kind: "flag", help: "also write the 2 KiB drive RAM as drive8.prg, for knowledge space drive8" },
587
+ "min-block": { kind: "count", value: "<bytes>", min: 1, max: 0x10000, default: 64, help: "write a changed block of at least this many bytes (default 64)" },
588
+ },
589
+ run: async (call) => outcome(await loads(call.subject(0), { out: call.string("out")!, steps: readLoadSteps(call.string("steps")!), drive: call.flag("drive"), minBlock: call.count("min-block")! })),
698
590
  },
699
- { usage: USAGE },
700
- );
591
+ ];
592
+
593
+ await runScript({
594
+ name: "unpacker",
595
+ summary: "Packing evidence of a program, and captures of the program after it unpacked itself. It never names a packer and runs no native unpack tool.",
596
+ names: knowledgeNames,
597
+ commands: COMMANDS,
598
+ });
package/src/c64.ts CHANGED
@@ -1,25 +1,40 @@
1
- const ADDRESS_PATTERN = /^\$([0-9a-fA-F]{4})$/;
1
+ import { Refusal } from "./errors.ts";
2
2
 
3
- /** Returns the value when it is a C64 address (0-$ffff). Throws RangeError otherwise. */
3
+ /** A C64 address as text: $ and 1 to 4 hex digits, either case. */
4
+ const ADDRESS_PATTERN = /^\$([0-9a-fA-F]{1,4})$/;
5
+
6
+ /** The address that the text gives ($ and 1 to 4 hex digits), or undefined. */
7
+ export function c64AddressOf(text: string): number | undefined {
8
+ const match = ADDRESS_PATTERN.exec(text);
9
+ return match === null ? undefined : Number.parseInt(match[1]!, 16);
10
+ }
11
+
12
+ /** Returns the value when it is a C64 address (0-$ffff). Refuses it otherwise. */
4
13
  function checkAddress(value: number): number {
5
- if (!Number.isInteger(value) || value < 0 || value > 0xffff) {
6
- throw new RangeError(`C64 address out of range: ${value}`);
7
- }
14
+ if (!Number.isInteger(value) || value < 0 || value > 0xffff) throw new Refusal("invalid-input", `${value} is not a C64 address: an address is from $0000 to $ffff.`);
8
15
  return value;
9
16
  }
10
17
 
11
- export function parseC64Address(value: string): number {
12
- const match = ADDRESS_PATTERN.exec(value);
13
- if (match === null) {
14
- throw new TypeError(`Invalid C64 address: ${value}`);
15
- }
16
- return checkAddress(Number.parseInt(match[1]!, 16));
18
+ /** The address of $ and 1 to 4 hex digits. Refuses other text. */
19
+ export function parseC64Address(text: string): number {
20
+ const value = c64AddressOf(text);
21
+ if (value === undefined) throw new Refusal("invalid-input", `${JSON.stringify(text)} is not a C64 address: give $ and 1 to 4 hex digits, for example $c000 or $fb.`);
22
+ return value;
17
23
  }
18
24
 
19
25
  export function formatC64Address(value: number): string {
20
26
  return `$${checkAddress(value).toString(16).padStart(4, "0")}`;
21
27
  }
22
28
 
29
+ /** An inclusive span of C64 addresses. */
30
+ export interface AddressRange {
31
+ start: number;
32
+ end: number;
33
+ }
34
+
35
+ /** The knowledge spaces: the C64, the 1541 drive in unit 8, and cartridge banks 0 to 255. */
36
+ export const SPACE_PATTERN = /^(c64|drive8|bank(0|[1-9][0-9]?|1[0-9]{2}|2[0-4][0-9]|25[0-5]))$/;
37
+
23
38
  /** VIC-II state with its addresses as $xxxx; the other values stay numbers. */
24
39
  export function formatVicii<T extends { screenAddress: number; graphicsAddress: number }>(state: T): Omit<T, "screenAddress" | "graphicsAddress"> & { screenAddress: string; graphicsAddress: string } {
25
40
  return { ...state, screenAddress: formatC64Address(state.screenAddress), graphicsAddress: formatC64Address(state.graphicsAddress) };
@@ -30,20 +45,20 @@ export function formatSprite<T extends { dataAddress: number }>(sprite: T): Omit
30
45
  return { ...sprite, dataAddress: formatC64Address(sprite.dataAddress) };
31
46
  }
32
47
 
33
- /** A byte as $xx. Throws RangeError outside 0-255. */
48
+ /** A byte as $xx. Refuses a value outside 0-255. */
34
49
  export function formatC64Byte(value: number): string {
35
- if (!Number.isInteger(value) || value < 0 || value > 0xff) throw new RangeError(`C64 byte out of range: ${value}`);
50
+ if (!Number.isInteger(value) || value < 0 || value > 0xff) throw new Refusal("invalid-input", `${value} is not a byte: a byte is from 0 to 255.`);
36
51
  return `$${value.toString(16).padStart(2, "0")}`;
37
52
  }
38
53
 
39
54
  /**
40
55
  * The memory a PRG loads into: the 2-byte load address, then the body.
41
- * Throws RangeError for a file shorter than 3 bytes or one that runs past $ffff.
56
+ * Refuses a file shorter than 3 bytes or one that runs past $ffff.
42
57
  */
43
58
  export function prgRange(prg: Uint8Array): { start: number; end: number; body: Uint8Array } {
44
- if (prg.length < 3) throw new RangeError("A PRG has a 2-byte load address and at least one byte.");
59
+ if (prg.length < 3) throw new Refusal("invalid-input", "A PRG has a 2-byte load address and at least one byte.");
45
60
  const start = prg[0]! | (prg[1]! << 8);
46
- if (start + prg.length - 2 > 0x10000) throw new RangeError("The PRG runs past $ffff.");
61
+ if (start + prg.length - 2 > 0x10000) throw new Refusal("invalid-input", "The PRG runs past $ffff.");
47
62
  return { start, end: start + prg.length - 3, body: prg.subarray(2) };
48
63
  }
49
64
 
@@ -86,7 +101,7 @@ export function d018Offsets(d018: number): { screen: number; characters: number;
86
101
  * Converts text to the PETSCII bytes the C64 keyboard types after a reset
87
102
  * (upper case/graphics mode). Letters of either case type the unshifted key,
88
103
  * which shows as an upper-case letter. "\n" and "\r\n" type RETURN.
89
- * Throws RangeError naming the first character that has no key.
104
+ * Refuses the text with the first character that has no key.
90
105
  */
91
106
  export function textToPetscii(text: string): Uint8Array {
92
107
  const bytes: number[] = [];
@@ -100,7 +115,7 @@ export function textToPetscii(text: string): Uint8Array {
100
115
  else if (character === "£") bytes.push(0x5c);
101
116
  else if (character === "↑" || character === "^") bytes.push(0x5e);
102
117
  else if (character === "←") bytes.push(0x5f);
103
- else throw new RangeError(`There is no C64 key for ${JSON.stringify(character)}`);
118
+ else throw new Refusal("invalid-input", `There is no C64 key for ${JSON.stringify(character)}`);
104
119
  }
105
120
  return Uint8Array.from(bytes);
106
121
  }
@@ -125,7 +140,7 @@ export function petsciiNameToText(bytes: Uint8Array): string {
125
140
 
126
141
  /**
127
142
  * The reverse of petsciiNameToText. Letters of either case are the unshifted
128
- * letters ($41-$5a). Throws RangeError for text that is no name.
143
+ * letters ($41-$5a). Refuses text that is no name.
129
144
  */
130
145
  export function textToPetsciiName(text: string): Uint8Array {
131
146
  const bytes: number[] = [];
@@ -144,7 +159,7 @@ export function textToPetsciiName(text: string): Uint8Array {
144
159
  else if (character === "↑") bytes.push(0x5e);
145
160
  else if (character === "←") bytes.push(0x5f);
146
161
  else if (code >= 0x20 && code <= 0x5d) bytes.push(code);
147
- else throw new RangeError(`${JSON.stringify(character)} is not a name character. Write other bytes as {$xx}.`);
162
+ else throw new Refusal("invalid-input", `${JSON.stringify(character)} is not a name character. Write other bytes as {$xx}.`);
148
163
  index++;
149
164
  }
150
165
  return Uint8Array.from(bytes);
package/src/cli/main.ts CHANGED
@@ -10,7 +10,7 @@ import { parseArgs } from "node:util";
10
10
  import { oldNodeRefusal, PACKAGE } from "../package.ts";
11
11
  import { localToolContext } from "../native/local.ts";
12
12
  import { localToolStatus } from "../native/status.ts";
13
- import { WireFailure } from "../protocol/messages.ts";
13
+ import { isRefusal } from "../errors.ts";
14
14
  import { startTrace } from "../trace.ts";
15
15
  import { runApSdk } from "./ap-sdk.ts";
16
16
  import { installedItems, parseTargets, tidyAfterUninstall, undoWindsurfProjectMcp, windsurfBefore, withoutWindsurfMcp, type Scope } from "./cleanup.ts";
@@ -124,7 +124,7 @@ try {
124
124
  if ((error as NodeJS.ErrnoException).code?.startsWith("ERR_PARSE_ARGS")) {
125
125
  console.error(`${(error as Error).message}\n\n${HELP}`);
126
126
  process.exitCode = 2;
127
- } else if (error instanceof WireFailure) {
127
+ } else if (isRefusal(error)) {
128
128
  // A refusal by name, for example AP SDK that did not finish in time.
129
129
  console.error(error.message);
130
130
  process.exitCode = 1;
package/src/errors.ts ADDED
@@ -0,0 +1,38 @@
1
+ // The one list of error codes and the one refusal class. Every part of the
2
+ // repository may import this file: the scripts, the MCP server, the host and
3
+ // the knowledge all report a failure as a code from this list and a message.
4
+
5
+ export const ERROR_CODES = [
6
+ "invalid-input",
7
+ "machine-running",
8
+ "machine-unavailable",
9
+ "machine-state-lost",
10
+ "not-found",
11
+ "media-error",
12
+ "limit-exceeded",
13
+ "unsupported-in-space",
14
+ "installation-incomplete",
15
+ "operation-failed",
16
+ "conflict",
17
+ "stale-revision",
18
+ "invalid-database",
19
+ "unsupported-schema",
20
+ ] as const;
21
+ export type ErrorCode = (typeof ERROR_CODES)[number];
22
+
23
+ /** A refused operation: a stable code and a message that tells what is wrong and what to do. */
24
+ export class Refusal extends Error {
25
+ override name = "Refusal";
26
+ readonly code: ErrorCode;
27
+
28
+ constructor(code: ErrorCode, message: string) {
29
+ super(message);
30
+ this.code = code;
31
+ }
32
+ }
33
+
34
+ /** True for an Error with a code from ERROR_CODES: a refusal, whatever its class. */
35
+ export function isRefusal(error: unknown): error is Error & { code: ErrorCode } {
36
+ const code = (error as { code?: unknown } | undefined)?.code;
37
+ return error instanceof Error && typeof code === "string" && (ERROR_CODES as readonly string[]).includes(code);
38
+ }
package/src/graphics.ts CHANGED
@@ -5,6 +5,8 @@
5
5
 
6
6
  import { crc32, deflateSync } from "node:zlib";
7
7
 
8
+ import { Refusal } from "./errors.ts";
9
+
8
10
  export interface IndexedFrame {
9
11
  width: number;
10
12
  height: number;
@@ -26,7 +28,7 @@ function chunk(type: string, data: Uint8Array): Buffer {
26
28
  /** Encodes an indexed frame as an 8-bit palette PNG. */
27
29
  export function encodePng(frame: IndexedFrame, palette: ReadonlyArray<Rgb>): Buffer {
28
30
  for (const index of frame.pixels) {
29
- if (index >= palette.length) throw new Error(`pixel colour ${index} is not in the ${palette.length}-colour palette`);
31
+ if (index >= palette.length) throw new Refusal("operation-failed", `pixel colour ${index} is not in the ${palette.length}-colour palette`);
30
32
  }
31
33
  const header = Buffer.alloc(13);
32
34
  header.writeUInt32BE(frame.width, 0);