@henols/c64-re-tools 2.0.0-rc.8 → 2.0.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 (118) hide show
  1. package/README.md +98 -115
  2. package/package.json +1 -1
  3. package/skills/c64-assembler/SKILL.md +22 -6
  4. package/skills/c64-assembler/scripts/assemble.ts +42 -14
  5. package/skills/c64-assembler/scripts/lib/compare.ts +48 -0
  6. package/skills/c64-basic/SKILL.md +10 -1
  7. package/skills/c64-basic/scripts/basic.ts +3 -3
  8. package/skills/c64-disk/SKILL.md +13 -4
  9. package/skills/c64-disk/scripts/disk.ts +7 -7
  10. package/skills/c64-disk/scripts/{containers.ts → lib/containers.ts} +2 -2
  11. package/skills/c64-docs/SKILL.md +138 -0
  12. package/skills/c64-docs/references/page-types.md +141 -0
  13. package/skills/c64-docs/references/style.md +43 -0
  14. package/skills/c64-docs/scripts/generate.ts +109 -0
  15. package/skills/c64-docs/scripts/lib/links.ts +167 -0
  16. package/skills/c64-docs/scripts/lib/markdown.ts +79 -0
  17. package/skills/c64-docs/scripts/lib/reference.ts +406 -0
  18. package/skills/c64-docs/scripts/lib/source-map.ts +81 -0
  19. package/skills/c64-docs/scripts/link.ts +239 -0
  20. package/skills/c64-emulator/SKILL.md +16 -5
  21. package/skills/c64-knowledge/SKILL.md +140 -18
  22. package/skills/c64-knowledge/scripts/knowledge.ts +737 -112
  23. package/skills/c64-knowledge/scripts/lib/listing.ts +162 -0
  24. package/skills/c64-memory-map/SKILL.md +11 -2
  25. package/skills/c64-memory-map/scripts/memmap.ts +2 -2
  26. package/skills/c64-provenance/SKILL.md +66 -11
  27. package/skills/c64-provenance/scripts/{compare.ts → lib/compare.ts} +3 -3
  28. package/skills/c64-provenance/scripts/lib/library.ts +584 -0
  29. package/skills/c64-provenance/scripts/{runtime.ts → lib/runtime.ts} +2 -2
  30. package/skills/c64-provenance/scripts/{separate.ts → lib/separate.ts} +3 -3
  31. package/skills/c64-provenance/scripts/provenance.ts +242 -40
  32. package/skills/c64-reverse-engineering/SKILL.md +51 -26
  33. package/skills/c64-source/SKILL.md +76 -28
  34. package/skills/c64-source/scripts/generate.ts +425 -127
  35. package/skills/c64-source/scripts/{loader.ts → lib/loader.ts} +84 -25
  36. package/skills/c64-source/scripts/lib/source.ts +1121 -0
  37. package/skills/c64-static-analysis/SKILL.md +35 -18
  38. package/skills/c64-static-analysis/scripts/analyze.ts +184 -55
  39. package/skills/c64-static-analysis/scripts/{coverage.ts → lib/coverage.ts} +19 -25
  40. package/skills/c64-static-analysis/scripts/{findings.ts → lib/findings.ts} +6 -2
  41. package/skills/c64-static-analysis/scripts/{idioms.ts → lib/idioms.ts} +21 -12
  42. package/skills/c64-static-analysis/scripts/{instructions.ts → lib/instructions.ts} +1 -1
  43. package/skills/c64-static-analysis/scripts/lib/macros.ts +233 -0
  44. package/skills/c64-static-analysis/scripts/lib/pitfalls.ts +68 -0
  45. package/skills/c64-static-analysis/scripts/lib/pointers.ts +108 -0
  46. package/skills/c64-testing/SKILL.md +36 -7
  47. package/skills/c64-testing/references/scenario-format.md +12 -8
  48. package/skills/c64-testing/scripts/checklist.ts +3 -3
  49. package/skills/c64-testing/scripts/diff.ts +167 -0
  50. package/skills/c64-testing/scripts/lib/diff.ts +70 -0
  51. package/skills/c64-testing/scripts/{scenario.ts → lib/scenario.ts} +45 -34
  52. package/skills/c64-testing/scripts/run.ts +73 -21
  53. package/skills/c64-testing/scripts/test.ts +5 -5
  54. package/skills/c64-unpacker/SKILL.md +23 -9
  55. package/skills/c64-unpacker/scripts/{evidence.ts → lib/evidence.ts} +1 -1
  56. package/skills/c64-unpacker/scripts/{loads.ts → lib/loads.ts} +4 -4
  57. package/skills/c64-unpacker/scripts/{machine.ts → lib/machine.ts} +4 -4
  58. package/skills/c64-unpacker/scripts/{stages.ts → lib/stages.ts} +17 -5
  59. package/skills/c64-unpacker/scripts/unpack.ts +41 -24
  60. package/src/c64.ts +103 -5
  61. package/src/cli/cleanup.ts +1 -1
  62. package/src/cli/main.ts +7 -4
  63. package/src/cli/status.ts +70 -3
  64. package/src/graphics.ts +158 -0
  65. package/src/host/main.ts +43 -8
  66. package/src/host/server.ts +10 -3
  67. package/src/host/tools/petcat.ts +1 -1
  68. package/src/host/tools/status.ts +1 -1
  69. package/src/host/vice/adapter.ts +4 -3
  70. package/src/host/vice/process.ts +2 -2
  71. package/src/host/vice/screen.ts +1 -42
  72. package/src/host/vice/session.ts +196 -23
  73. package/src/host-client/autostart.ts +1 -1
  74. package/src/host-client/connect.ts +1 -1
  75. package/src/host-client/coverage.ts +2 -29
  76. package/src/host-client/image.ts +66 -0
  77. package/src/host-client/steps.ts +9 -4
  78. package/src/host-client/tools.ts +1 -0
  79. package/src/host-client/transfer.ts +39 -11
  80. package/src/host-client/vice-session.ts +33 -16
  81. package/src/knowledge/conflicts.ts +161 -0
  82. package/src/knowledge/database.ts +150 -38
  83. package/src/knowledge/expression.ts +209 -0
  84. package/src/knowledge/graph.ts +104 -0
  85. package/src/knowledge/history.ts +70 -22
  86. package/src/knowledge/import.ts +64 -55
  87. package/src/knowledge/join.ts +211 -0
  88. package/src/knowledge/program.ts +266 -0
  89. package/src/knowledge/read.ts +258 -27
  90. package/src/knowledge/schema.ts +199 -75
  91. package/src/knowledge/text.ts +415 -0
  92. package/src/knowledge/write.ts +740 -57
  93. package/src/mcp/main.ts +1 -1
  94. package/src/mcp/output.ts +15 -8
  95. package/src/mcp/server.ts +3 -1
  96. package/src/mcp/tools/debug.ts +97 -16
  97. package/src/mcp/tools/execution.ts +38 -5
  98. package/src/mcp/tools/machine.ts +11 -2
  99. package/src/mcp/tools/media.ts +18 -13
  100. package/src/mcp/tools/memory.ts +17 -8
  101. package/src/mcp/tools/video.ts +9 -9
  102. package/src/native/discover.ts +2 -2
  103. package/src/native/ghidra/analyze.ts +2 -2
  104. package/src/native/ghidra/index.ts +1 -1
  105. package/src/native/ghidra/language/c64rt_6510.slaspec +4 -1
  106. package/src/native/ghidra/scripts/C64Export.java +1 -1
  107. package/src/native/ghidra/scripts/C64Prepare.java +7 -1
  108. package/src/native/types.ts +14 -8
  109. package/{skills/c64-source/scripts → src}/opcodes.ts +75 -3
  110. package/src/platform.ts +70 -7
  111. package/src/project.ts +180 -3
  112. package/src/protocol/messages.ts +1 -1
  113. package/src/protocol/tools.ts +2 -16
  114. package/src/protocol/vice.ts +124 -15
  115. package/src/script.ts +46 -7
  116. package/src/trace.ts +55 -23
  117. package/skills/c64-assembler/scripts/compare.ts +0 -84
  118. package/skills/c64-source/scripts/source.ts +0 -595
@@ -4,7 +4,7 @@
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
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
+ // multi-load program. No native unpack tool is used.
8
8
 
9
9
  import { parseArgs } from "node:util";
10
10
 
@@ -17,14 +17,14 @@ import { readProjectFile, writeProjectFile } from "#src/host-client/transfer.ts"
17
17
  import { ViceSessionClient } from "#src/host-client/vice-session.ts";
18
18
  import { resolveProjectPath } from "#src/project.ts";
19
19
  import { runScript, UsageError } from "#src/script.ts";
20
- import { basicStart, inspect, PackingError, readMemoryMap, underRom, writtenThenExecuted } from "./evidence.ts";
21
- import { loads, readLoadSteps } from "./loads.ts";
22
- import { callReturn, captureSegments, executedIn, inRanges, mergeMaps, stageCandidates, stagePoints, type Range } from "./stages.ts";
23
- import { entryState, IO_AREAS, json, machineState, readRam, READY_FRAMES, vectorsOf } from "./machine.ts";
20
+ import { basicStart, inspect, PackingError, readMemoryMap, underRom, writtenThenExecuted } from "./lib/evidence.ts";
21
+ 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
24
 
25
25
  const USAGE = `unpack.ts inspect <program.prg>
26
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>] [--timeout-frames <n>] [--entry <address>] [--steps <file>] [--min-stage <bytes>]
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
28
  unpack.ts stages <program.prg> [--frames <n>] [--entry <address>] [--steps <file>] [--min-stage <bytes>]
29
29
  unpack.ts loads <program> --steps <file> --out <dir> [--drive] [--min-block <bytes>]
30
30
 
@@ -37,6 +37,8 @@ capture run the program until the CPU gets to <address> after the steps, and w
37
37
  for each segment, and entry_state.json (registers, $00/$01, pages
38
38
  0-3, VIC, SID and CIA registers, vectors, and what differs from a
39
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.
40
42
  --entry starts it there instead of RUN.
41
43
 
42
44
  stages run the program for <n> frames (default 3000) and report each stage:
@@ -74,7 +76,7 @@ const MAX_ADVANCE = 10_000;
74
76
  /** The most ranges one memory map read gives. */
75
77
  const MAX_RANGES = 1000;
76
78
 
77
- /** Reads and checks a steps file: action steps with $ addresses only (D43). */
79
+ /** Reads and checks a steps file: action steps with $ addresses only. */
78
80
  function readSteps(path: string): ActionStep[] {
79
81
  let value: unknown;
80
82
  try {
@@ -102,7 +104,6 @@ async function runSteps(client: ViceSessionClient, steps: ActionStep[]): Promise
102
104
  async function trace(program: string, options: { frames: number; entry?: number; steps: ActionStep[] }): Promise<{ output: Record<string, unknown>; failed: boolean }> {
103
105
  const { bytes } = readProjectFile(program);
104
106
  if (bytes.length < 3) throw new UsageError("the file is too short for a PRG");
105
- const load = bytes[0]! | (bytes[1]! << 8);
106
107
  if (options.entry === undefined && basicStart(bytes) === undefined) throw new UsageError(NO_BASIC);
107
108
  const client = await ViceSessionClient.open({ videoStandard: "pal" });
108
109
  try {
@@ -190,7 +191,7 @@ const SYS_FRAMES = 50;
190
191
  * BASIC computes: an exec breakpoint over that part of the program stops there.
191
192
  */
192
193
  /**
193
- * Starts the program as a c64-testing scenario does (D62): hard reset, load,
194
+ * Starts the program as a c64-testing scenario does: hard reset, load,
194
195
  * an empty memory map, then RUN up to the first BASIC line, or the pc at
195
196
  * `entry`. Gives why it did not start.
196
197
  */
@@ -203,7 +204,7 @@ async function startLoaded(client: ViceSessionClient, program: string, bytes: Ui
203
204
  return undefined;
204
205
  }
205
206
 
206
- /** The raster position now: where a step's frames end (D62). */
207
+ /** The raster position now: where a step's frames end. */
207
208
  async function rasterAt(client: ViceSessionClient): Promise<{ line: number; cycle: number }> {
208
209
  const { timing } = await client.observe({ timing: true });
209
210
  return { line: timing!.rasterLine, cycle: timing!.rasterCycle };
@@ -219,7 +220,7 @@ const MAX_STAGES = 16;
219
220
  /** Most stops of one re-run of a chunk: own code and writes over it each stop once. */
220
221
  const MAX_STAGE_STOPS = 4096;
221
222
  const STAGE_SNAPSHOT = "stage-chunk";
222
- /** The snapshot at a call into written code, while the run checks if the call returns (D59). */
223
+ /** The snapshot at a call into written code, while the run checks if the call returns. */
223
224
  const CALL_SNAPSHOT = "stage-call";
224
225
 
225
226
  interface Stage {
@@ -245,7 +246,7 @@ function trackedRunUntil(step: ActionStep): step is { runUntil: { at: string };
245
246
  }
246
247
 
247
248
  /**
248
- * Starts the program and follows its stages (D43). The memory map holds what
249
+ * Starts the program and follows its stages. The memory map holds what
249
250
  * the current stage wrote; when the CPU executed a block of at least
250
251
  * `minStage` written bytes during a chunk of frames, the chunk runs again from
251
252
  * its snapshot with exec breakpoints over those blocks, which stop at the
@@ -266,7 +267,7 @@ async function follow(
266
267
  if (options.entry === undefined && basic === undefined) throw new UsageError(NO_BASIC);
267
268
  const notStarted = await startLoaded(client, program, bytes, options.entry);
268
269
  if (notStarted !== undefined) return { stages: [], frames: 0, reached: false, failed: notStarted };
269
- // The frames of each step end where a scenario's do: the first step counts from RUN, a later one from where it starts (D62).
270
+ // The frames of each step end where a scenario's do: the first step counts from RUN, a later one from where it starts.
270
271
  let anchor: { line: number; cycle: number } | undefined = await rasterAt(client);
271
272
  const framed = () => (anchor === undefined ? {} : { anchor });
272
273
  const found: Stage[] = [];
@@ -276,10 +277,10 @@ async function follow(
276
277
  let firstFrame = 0;
277
278
  let total = 0;
278
279
  let ended = false;
279
- /** Where the run stops: the target of a runUntil step while it runs, then until (R2-06). */
280
+ /** Where the run stops: the target of a runUntil step while it runs, then until. */
280
281
  let stopAt: number | undefined;
281
282
  const atTarget = (pc: number) => stopAt !== undefined && pc === stopAt;
282
- // The current stage: its memory map since it started, chunk by chunk, its own code, and the written code it calls (D59).
283
+ // The current stage: its memory map since it started, chunk by chunk, its own code, and the written code it calls.
283
284
  let stage = { map: new Uint8Array(0x10000), own: new Set<number>(), called: [] as Range[] };
284
285
  const startStage = async (next: number, last: Uint8Array) => {
285
286
  mergeMaps(stage.map, last);
@@ -355,7 +356,7 @@ async function follow(
355
356
  left -= advanced;
356
357
  const pc = run.pc ?? (await client.registersGet("c64")).pc;
357
358
  map = await readCoverage(client);
358
- // A frame can end in written code that the stage called before (D59): that stop starts no stage.
359
+ // A frame can end in written code that the stage called before: that stop starts no stage.
359
360
  if (inRanges(candidates, pc) && !inRanges(stage.called, pc)) {
360
361
  if (written(pc)) {
361
362
  const back = left > 0 && !atTarget(pc) ? await returns(map, left) : undefined;
@@ -443,7 +444,7 @@ async function follow(
443
444
  stopAt = undefined;
444
445
  }
445
446
  };
446
- // The steps run first and in the tracked time: a frames step and a runUntil step to an address (R2-06). The others act between two chunks.
447
+ // The steps run first and in the tracked time: a frames step and a runUntil step to an address. The others act between two chunks.
447
448
  for (const [index, step] of options.steps.entries()) {
448
449
  if ("frames" in step) await track(step.frames);
449
450
  else if (trackedRunUntil(step)) {
@@ -496,7 +497,7 @@ async function stages(program: string, options: { frames: number; entry?: number
496
497
 
497
498
  async function capture(
498
499
  program: string,
499
- options: { until: number; out: string; range?: [number, number]; timeoutFrames: number; entry?: number; steps: ActionStep[]; minStage: number },
500
+ options: { until: number; out: string; range?: [number, number]; exclude: Array<{ start: number; end: number }>; timeoutFrames: number; entry?: number; steps: ActionStep[]; minStage: number },
500
501
  ): Promise<{ output: Record<string, unknown>; failed: boolean }> {
501
502
  const client = await ViceSessionClient.open({ videoStandard: "pal" });
502
503
  try {
@@ -513,7 +514,12 @@ async function capture(
513
514
  const memory = await readRam(client, 0x0000, 0x10000);
514
515
  const state = await machineState(client);
515
516
  // The segments: what the stages wrote, without loader leftovers, with no overlap.
516
- const segments = options.range !== undefined ? [{ start: options.range[0], end: options.range[1] }] : captureSegments(followed.stages, registers.pc);
517
+ const found = options.range !== undefined ? [{ start: options.range[0], end: options.range[1] }] : captureSegments(followed.stages, registers.pc);
518
+ const inside = options.exclude.find((range) => registers.pc >= range.start && registers.pc <= range.end);
519
+ if (inside !== undefined) {
520
+ 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
+ }
522
+ const segments = withoutRanges(found, options.exclude);
517
523
  const file = (path: string) => `${options.out}/${path}`;
518
524
  const write = (path: string, bytes: Uint8Array) => writeProjectFile(file(path), bytes, { what: "--out" });
519
525
  write("image.bin", memory);
@@ -530,6 +536,7 @@ async function capture(
530
536
  {
531
537
  entry: formatC64Address(registers.pc),
532
538
  segments: segments.map((segment, index) => ({ ...showRange(segment), ...("writtenBy" in segment ? { writtenBy: segment.writtenBy } : {}), file: files[index] })),
539
+ ...(options.exclude.length === 0 ? {} : { excluded: options.exclude.map(showRange) }),
533
540
  vectors,
534
541
  stages: showStages(followed.stages),
535
542
  image: "image.bin",
@@ -556,6 +563,7 @@ async function capture(
556
563
  out: options.out,
557
564
  files: ["image.json", "image.bin", "entry_state.json", ...files],
558
565
  segments: segments.map(showRange),
566
+ ...(options.exclude.length === 0 ? {} : { excluded: options.exclude.map(showRange) }),
559
567
  stages: followed.stages.length,
560
568
  differsFromReset: differs.length,
561
569
  },
@@ -571,7 +579,7 @@ const OPTIONS_OF: Record<string, string[]> = {
571
579
  inspect: [],
572
580
  trace: ["frames", "entry", "steps"],
573
581
  stages: ["frames", "entry", "steps", "min-stage"],
574
- capture: ["until", "out", "range", "timeout-frames", "entry", "steps", "min-stage"],
582
+ capture: ["until", "out", "range", "exclude", "timeout-frames", "entry", "steps", "min-stage"],
575
583
  loads: ["steps", "out", "drive", "min-block"],
576
584
  };
577
585
 
@@ -584,6 +592,7 @@ async function run(argv: string[]): Promise<{ output: unknown; failed: boolean }
584
592
  until: { type: "string" },
585
593
  out: { type: "string" },
586
594
  range: { type: "string" },
595
+ exclude: { type: "string", multiple: true },
587
596
  "timeout-frames": { type: "string" },
588
597
  frames: { type: "string" },
589
598
  entry: { type: "string" },
@@ -636,7 +645,7 @@ async function run(argv: string[]): Promise<{ output: unknown; failed: boolean }
636
645
  try {
637
646
  resolveProjectPath(values.out);
638
647
  } catch {
639
- throw new UsageError(`--out must be a path relative to the project directory that stays inside it, not ${values.out}`);
648
+ throw new UsageError(`--out must be a path relative to the project folder that stays inside it, not ${values.out}`);
640
649
  }
641
650
  const minBlock = values["min-block"] === undefined ? 64 : Number(values["min-block"]);
642
651
  if (!Number.isInteger(minBlock) || minBlock < 1 || minBlock > 0x10000) throw new UsageError("--min-block must be from 1 to 65536");
@@ -652,10 +661,17 @@ async function run(argv: string[]): Promise<{ output: unknown; failed: boolean }
652
661
  } else if (rest.length > 0) {
653
662
  throw new UsageError("too many arguments");
654
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
+ });
655
671
  try {
656
672
  resolveProjectPath(values.out);
657
673
  } catch {
658
- throw new UsageError(`--out must be a path relative to the project directory that stays inside it, not ${values.out}`);
674
+ throw new UsageError(`--out must be a path relative to the project folder that stays inside it, not ${values.out}`);
659
675
  }
660
676
  const timeoutFrames = values["timeout-frames"] === undefined ? 3000 : Number(values["timeout-frames"]);
661
677
  if (!Number.isInteger(timeoutFrames) || timeoutFrames < 1 || timeoutFrames > 30_000) throw new UsageError("--timeout-frames must be from 1 to 30000");
@@ -666,6 +682,7 @@ async function run(argv: string[]): Promise<{ output: unknown; failed: boolean }
666
682
  until: address(values.until, "--until"),
667
683
  out: values.out,
668
684
  ...(range === undefined ? {} : { range }),
685
+ exclude,
669
686
  timeoutFrames,
670
687
  steps,
671
688
  minStage,
@@ -674,8 +691,8 @@ async function run(argv: string[]): Promise<{ output: unknown; failed: boolean }
674
691
  }
675
692
 
676
693
  await runScript(
677
- async () => {
678
- const { output, failed } = await run(process.argv.slice(2));
694
+ async (argv) => {
695
+ const { output, failed } = await run(argv);
679
696
  if (failed) process.exitCode = 1;
680
697
  return output;
681
698
  },
package/src/c64.ts CHANGED
@@ -20,6 +20,16 @@ export function formatC64Address(value: number): string {
20
20
  return `$${checkAddress(value).toString(16).padStart(4, "0")}`;
21
21
  }
22
22
 
23
+ /** VIC-II state with its addresses as $xxxx; the other values stay numbers. */
24
+ export function formatVicii<T extends { screenAddress: number; graphicsAddress: number }>(state: T): Omit<T, "screenAddress" | "graphicsAddress"> & { screenAddress: string; graphicsAddress: string } {
25
+ return { ...state, screenAddress: formatC64Address(state.screenAddress), graphicsAddress: formatC64Address(state.graphicsAddress) };
26
+ }
27
+
28
+ /** A sprite with its data address as $xxxx; the other values stay numbers. */
29
+ export function formatSprite<T extends { dataAddress: number }>(sprite: T): Omit<T, "dataAddress"> & { dataAddress: string } {
30
+ return { ...sprite, dataAddress: formatC64Address(sprite.dataAddress) };
31
+ }
32
+
23
33
  /** A byte as $xx. Throws RangeError outside 0-255. */
24
34
  export function formatC64Byte(value: number): string {
25
35
  if (!Number.isInteger(value) || value < 0 || value > 0xff) throw new RangeError(`C64 byte out of range: ${value}`);
@@ -37,8 +47,25 @@ export function prgRange(prg: Uint8Array): { start: number; end: number; body: U
37
47
  return { start, end: start + prg.length - 3, body: prg.subarray(2) };
38
48
  }
39
49
 
40
- /** How 6502 code refers to an address. */
41
- export const REFERENCE_KINDS = ["call", "jump", "read", "write", "reference"] as const;
50
+ /** The tables of addresses: words, or words minus 1 for an RTS dispatch; a split table has all low bytes, then all high bytes. */
51
+ export const POINTER_TABLE_TYPES = ["addresses", "addresses-split", "rts-addresses", "rts-addresses-split"] as const;
52
+ export type PointerTableType = (typeof POINTER_TABLE_TYPES)[number];
53
+
54
+ /** The entries of a table of addresses in [start, end]: the address of each low and high byte, and the target that the entry names. */
55
+ export function pointerTableEntries(table: { start: number; end: number; type: PointerTableType }, byteAt: (address: number) => number): Array<{ low: number; high: number; target: number }> {
56
+ const length = table.end - table.start + 1;
57
+ const split = table.type === "addresses-split" || table.type === "rts-addresses-split";
58
+ const minus = table.type === "rts-addresses" || table.type === "rts-addresses-split" ? 1 : 0;
59
+ const count = Math.floor(length / 2);
60
+ return Array.from({ length: count }, (_, index) => {
61
+ const low = split ? table.start + index : table.start + 2 * index;
62
+ const high = split ? table.start + count + index : low + 1;
63
+ return { low, high, target: ((byteAt(low) | (byteAt(high) << 8)) + minus) & 0xffff };
64
+ });
65
+ }
66
+
67
+ /** How 6502 code refers to an address. A pointer is code that builds the address from immediates. */
68
+ export const REFERENCE_KINDS = ["call", "jump", "read", "write", "reference", "pointer"] as const;
42
69
  export type ReferenceKind = (typeof REFERENCE_KINDS)[number];
43
70
 
44
71
  /** A symbol or label name: assembler label syntax, at most 64 characters; a leading dot marks a local label. */
@@ -163,9 +190,9 @@ const MIN_BASIC_LOAD = 0x0400;
163
190
  * zero, the "marker" is the first machine code after the BASIC text (SYS
164
191
  * lines that jump right behind themselves). The stored links are not read:
165
192
  * BASIC relinks a loaded program, and a crack can store odd links to hide
166
- * lines from LIST. A load address below $0400 is a memory dump (F-12), and so
193
+ * lines from LIST. A load address below $0400 is a memory dump, and so
167
194
  * is a file whose bytes run past $ffff: a 64 KiB dump from $0000 always does
168
- * (b2/01, R3-02).
195
+ *.
169
196
  */
170
197
  export function parseTokenized(program: Uint8Array): TokenizedProgram {
171
198
  if (program.length < 4) return { reason: "The file is too short for a BASIC program." };
@@ -347,7 +374,7 @@ export function findHandoffs(lines: TokenizedLine[]): BasicHandoff[] {
347
374
  return handoffs;
348
375
  }
349
376
 
350
- // Execution coverage (D40): the byte format of a coverage map, and the 6502
377
+ // Execution coverage: the byte format of a coverage map, and the 6502
351
378
  // instruction lengths that turn the instruction starts in it into code ranges.
352
379
  // VICE marks only the opcode byte of an executed instruction as executed.
353
380
 
@@ -409,3 +436,74 @@ export function executedRamCode(map: Uint8Array, base: number, memory: Uint8Arra
409
436
  }
410
437
  return ranges;
411
438
  }
439
+
440
+ // Coverage flows: the seeds that a coverage map gives a static analysis.
441
+
442
+ /**
443
+ * Instructions after which the next instruction starts a flow for the seeds: the
444
+ * ones that do not fall through, and JSR, because an analyzer that takes the
445
+ * called routine for one that does not return stops there (the Bruce Lee run).
446
+ */
447
+ const FLOW_ENDS = new Set([0x00, 0x20, 0x40, 0x4c, 0x60, 0x6c]);
448
+ /**
449
+ * The executed instruction starts in RAM that begin a flow: the first after
450
+ * a gap, or the first after an instruction that does not fall through or is a
451
+ * JSR. The analyzers follow the rest of a flow themselves.
452
+ */
453
+ export function coverageEntries(map: Uint8Array, image: (address: number) => number | undefined): number[] {
454
+ const entries: number[] = [];
455
+ let falls: number | undefined;
456
+ for (let address = 0; address < 0x10000; address++) {
457
+ if ((map[address]! & COVERAGE.ramExecute) === 0) continue;
458
+ const opcode = image(address);
459
+ if (opcode === undefined) continue;
460
+ if (falls !== address) entries.push(address);
461
+ falls = FLOW_ENDS.has(opcode) ? undefined : address + instructionLength(opcode);
462
+ }
463
+ return entries;
464
+ }
465
+
466
+ /** Every executed instruction start in RAM inside the image: VICE marks the opcode byte of an instruction as executed, not its operand. */
467
+ export function executedStarts(map: Uint8Array, image: (address: number) => number | undefined): number[] {
468
+ const starts: number[] = [];
469
+ for (let address = 0; address < 0x10000; address++) if ((map[address]! & COVERAGE.ramExecute) !== 0 && image(address) !== undefined) starts.push(address);
470
+ return starts;
471
+ }
472
+
473
+ const BRANCHES = new Set([0x10, 0x30, 0x50, 0x70, 0x90, 0xb0, 0xd0, 0xf0]);
474
+
475
+ /** Instructions that never fall through to the next one. */
476
+ const NO_FALL = new Set([0x00, 0x40, 0x4c, 0x60, 0x6c]);
477
+
478
+ /**
479
+ * The coverage flow starts split for Ghidra. A start is a code start, a
480
+ * place in a routine, when it follows an executed JSR, or when an executed
481
+ * branch or JMP from outside its own flow reaches it (the loop of a routine
482
+ * that jumps back to its own start does not count). Any other start, for
483
+ * example a vector or table target, is a routine entry.
484
+ */
485
+ export function coverageSeedKinds(map: Uint8Array, image: (address: number) => number | undefined): { routines: number[]; codeStarts: number[] } {
486
+ const entries = coverageEntries(map, image);
487
+ const flowOf = new Map<number, number>();
488
+ for (const entry of entries) {
489
+ // A flow runs over the executed instructions that fall through, also over a JSR.
490
+ for (let at = entry; ; ) {
491
+ const opcode = image(at);
492
+ if (opcode === undefined || (map[at]! & COVERAGE.ramExecute) === 0 || flowOf.has(at)) break;
493
+ flowOf.set(at, entry);
494
+ if (NO_FALL.has(opcode)) break;
495
+ at += instructionLength(opcode);
496
+ }
497
+ }
498
+ const inside = new Set<number>();
499
+ for (const address of executedStarts(map, image)) {
500
+ const opcode = image(address)!;
501
+ const low = image(address + 1);
502
+ const high = image(address + 2);
503
+ const reach = (target: number) => flowOf.get(address) !== target && inside.add(target);
504
+ if (BRANCHES.has(opcode) && low !== undefined) reach((address + 2 + (low < 0x80 ? low : low - 0x100)) & 0xffff);
505
+ if (opcode === 0x4c && low !== undefined && high !== undefined) reach(low | (high << 8));
506
+ if (opcode === 0x20) inside.add(address + 3);
507
+ }
508
+ return { routines: entries.filter((entry) => !inside.has(entry)), codeStarts: entries.filter((entry) => inside.has(entry)) };
509
+ }
@@ -29,7 +29,7 @@ interface Manifest {
29
29
  }
30
30
 
31
31
  /** The directory that the manifest and relative item paths belong to. */
32
- function scopeRoot(scope: Scope, cwd = process.cwd()): string {
32
+ export function scopeRoot(scope: Scope, cwd = process.cwd()): string {
33
33
  return scope === "global" ? homedir() : cwd;
34
34
  }
35
35
 
package/src/cli/main.ts CHANGED
@@ -14,7 +14,7 @@ import { WireFailure } from "../protocol/messages.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";
17
- import { hostStatusLines, toolLines } from "./status.ts";
17
+ import { hostStatusLines, installedLines, toolLines } from "./status.ts";
18
18
 
19
19
  const HELP = `c64-re-tools: Commodore 64 reverse engineering and development
20
20
 
@@ -27,10 +27,12 @@ Usage:
27
27
 
28
28
  Harnesses: claude, codex, pi, opencode, gemini, copilot, cursor, windsurf
29
29
  (comma-separated; default: all). Without --global the files go into the
30
- current project; with --global into your home directory.
30
+ current folder; with --global into your home directory.
31
31
 
32
- Run it in the project: npx -y ${PACKAGE.name}@${PACKAGE.tag} install. The VICE
33
- MCP declaration starts the latest c64-re-tools-mcp through npx. The Host
32
+ Run it once at the root of the repository:
33
+ npx -y ${PACKAGE.name}@${PACKAGE.tag} install. Each project folder below
34
+ the root uses that install; a project is a folder with .c64-re-tools/. The
35
+ VICE MCP declaration starts the latest c64-re-tools-mcp through npx. The Host
34
36
  Runtime runs on the machine with VICE: start it there with
35
37
  npx -y --package=${PACKAGE.name}@${PACKAGE.tag} c64-re-tools-host.
36
38
  It runs VICE and the tools that come with it (c1541, petcat). The skill
@@ -50,6 +52,7 @@ async function status(): Promise<number> {
50
52
  console.log(`c64-re-tools ${PACKAGE.version}`);
51
53
  console.log(`Package: ${join(here, "..", "..")}`);
52
54
  console.log(`Node: ${process.version} (${process.execPath})`);
55
+ for (const line of installedLines()) console.log(line);
53
56
  // The skill scripts run these tools here, on this machine.
54
57
  const local = await localToolStatus(localToolContext());
55
58
  for (const line of toolLines("Tools here:", local)) console.log(line);
package/src/cli/status.ts CHANGED
@@ -1,6 +1,10 @@
1
- // The status report of the CLI: the tools on this machine and the Host
2
- // Runtime with its tools.
1
+ // The status report of the CLI: the installed skills and MCP declarations,
2
+ // the tools on this machine and the Host Runtime with its tools.
3
3
 
4
+ import { readFileSync } from "node:fs";
5
+ import { isAbsolute, join } from "node:path";
6
+
7
+ import { installedItems, scopeRoot, type InstallItem, type Scope } from "./cleanup.ts";
4
8
  import { hostTools, type ToolCallOptions } from "../host-client/tools.ts";
5
9
  import { viceVersionOf } from "../native/discover.ts";
6
10
  import { PACKAGE } from "../package.ts";
@@ -34,7 +38,7 @@ export async function hostStatusLines(options: ToolCallOptions = {}): Promise<{
34
38
  try {
35
39
  ({ host, tools } = await hostTools(options));
36
40
  } catch (error) {
37
- // status is read by a person: the instruction for an agent (D37) stays out.
41
+ // status is read by a person: the instruction for an agent stays out.
38
42
  return { lines: [`Host Runtime: not reachable. ${(error as Error).message.replace(` ${STOP_AND_ASK}`, "")}`], ready: false };
39
43
  }
40
44
  const mixed = mixedViceBuilds(tools);
@@ -59,3 +63,66 @@ export function mixedViceBuilds(tools: readonly ToolStatus[]): string | undefine
59
63
  if (new Set(builds).size <= 1) return undefined;
60
64
  return `the VICE tools come from different VICE builds: ${running.map((tool) => `${tool.version} (${tool.path})`).join(", ")}. Put x64sc, c1541 and petcat of one VICE first on PATH, or set C64RT_VICE, C64RT_C1541 and C64RT_PETCAT.`;
61
65
  }
66
+
67
+ /**
68
+ * The skills and MCP declarations that install wrote for this project and
69
+ * globally: the c64-re-tools version of each harness's skills,
70
+ * from their package.json, and the command that starts the MCP server. A
71
+ * warning names skills of another version than this one.
72
+ */
73
+ export function installedLines(cwd = process.cwd()): string[] {
74
+ const lines: string[] = [];
75
+ for (const scope of ["project", "global"] as const satisfies readonly Scope[]) {
76
+ const items = installedItems(scope, undefined, cwd);
77
+ if (items.length === 0) continue;
78
+ const root = scopeRoot(scope, cwd);
79
+ lines.push(`Installed ${scope === "project" ? `in this project (${root})` : "globally"}:`);
80
+ for (const harness of [...new Set(items.map((item) => item.harness))]) {
81
+ const skills = items.filter((item) => item.harness === harness && item.kind === "skill");
82
+ const versions = [...new Set(skills.map((item) => skillVersion(item, root)).filter((version) => version !== undefined))];
83
+ if (skills.length > 0) {
84
+ lines.push(` ${harness}: ${skills.length} skills, c64-re-tools ${versions.length === 0 ? "of an unknown version" : versions.join(" and ")}`);
85
+ const other = versions.filter((version) => version !== PACKAGE.version);
86
+ if (other.length > 0) lines.push(` Warning: the ${harness} skills are c64-re-tools ${other.join(" and ")} and this is ${PACKAGE.version}. Run npx -y ${PACKAGE.name}@${PACKAGE.tag} update${scope === "global" ? " --global" : ""} to install this version.`);
87
+ }
88
+ for (const item of items.filter((candidate) => candidate.harness === harness && candidate.kind === "mcp")) {
89
+ lines.push(` ${harness} MCP server: ${mcpCommand(item, root) ?? `declared in ${item.files.join(", ")}`}`);
90
+ }
91
+ }
92
+ }
93
+ if (lines.length === 0) return ["Installed: nothing in this project or globally."];
94
+ lines.push(" The MCP server gives its own version in the result of c64_status.");
95
+ return lines;
96
+ }
97
+
98
+ function itemPath(file: string, root: string): string {
99
+ return isAbsolute(file) ? file : join(root, file);
100
+ }
101
+
102
+ /** The version in the package.json of an installed skill; a skill without scripts has none. */
103
+ function skillVersion(item: InstallItem, root: string): string | undefined {
104
+ const file = item.files.find((candidate) => candidate.endsWith("/package.json") || candidate.endsWith("\\package.json"));
105
+ if (file === undefined) return undefined;
106
+ try {
107
+ const { version } = JSON.parse(readFileSync(itemPath(file, root), "utf8")) as { version?: unknown };
108
+ return typeof version === "string" ? version : undefined;
109
+ } catch {
110
+ return undefined;
111
+ }
112
+ }
113
+
114
+ /** The command of the MCP server in a JSON configuration file, or undefined for another format. */
115
+ function mcpCommand(item: InstallItem, root: string): string | undefined {
116
+ const key = item.detail?.mergeKey;
117
+ const [file] = item.files;
118
+ if (key === undefined || file === undefined) return undefined;
119
+ try {
120
+ const servers = (JSON.parse(readFileSync(itemPath(file, root), "utf8")) as Record<string, unknown>)[key] as Record<string, { command?: unknown; args?: unknown }> | undefined;
121
+ const server = servers?.[item.name];
122
+ if (typeof server?.command !== "string") return undefined;
123
+ const args = Array.isArray(server.args) ? server.args.filter((arg): arg is string => typeof arg === "string") : [];
124
+ return [server.command, ...args].join(" ");
125
+ } catch {
126
+ return undefined;
127
+ }
128
+ }
@@ -0,0 +1,158 @@
1
+ // Indexed images: the PNG encoder of emulator frames, and the C64 graphics
2
+ // in memory as images for the documentation. A region of the knowledge
3
+ // gives no display mode and no colours, so a sprite, a bitmap or a charset is read
4
+ // both in hires and in multicolour, with fixed pattern palettes.
5
+
6
+ import { crc32, deflateSync } from "node:zlib";
7
+
8
+ export interface IndexedFrame {
9
+ width: number;
10
+ height: number;
11
+ /** One palette index per pixel, row by row. */
12
+ pixels: Uint8Array;
13
+ }
14
+
15
+ export type Rgb = readonly [number, number, number];
16
+
17
+ function chunk(type: string, data: Uint8Array): Buffer {
18
+ const length = Buffer.alloc(4);
19
+ length.writeUInt32BE(data.length, 0);
20
+ const body = Buffer.concat([Buffer.from(type, "latin1"), data]);
21
+ const crc = Buffer.alloc(4);
22
+ crc.writeUInt32BE(crc32(body), 0);
23
+ return Buffer.concat([length, body, crc]);
24
+ }
25
+
26
+ /** Encodes an indexed frame as an 8-bit palette PNG. */
27
+ export function encodePng(frame: IndexedFrame, palette: ReadonlyArray<Rgb>): Buffer {
28
+ 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`);
30
+ }
31
+ const header = Buffer.alloc(13);
32
+ header.writeUInt32BE(frame.width, 0);
33
+ header.writeUInt32BE(frame.height, 4);
34
+ header[8] = 8; // bit depth
35
+ header[9] = 3; // indexed colour
36
+ const rows = Buffer.alloc((frame.width + 1) * frame.height);
37
+ for (let row = 0; row < frame.height; row++) {
38
+ // Filter byte 0 (none), then the row.
39
+ rows.set(frame.pixels.subarray(row * frame.width, (row + 1) * frame.width), row * (frame.width + 1) + 1);
40
+ }
41
+ return Buffer.concat([
42
+ Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]),
43
+ chunk("IHDR", header),
44
+ chunk("PLTE", Buffer.from(palette.flat())),
45
+ chunk("IDAT", deflateSync(rows)),
46
+ chunk("IEND", new Uint8Array(0)),
47
+ ]);
48
+ }
49
+
50
+ export type GraphicsMode = "hires" | "multicolour";
51
+
52
+ /**
53
+ * The pattern palettes: the index is the pixel value (hires: the bit; multicolour:
54
+ * the bit pair, 0 = background $d021, 1 = $d022 or screen high nibble, 2 = $d023 or
55
+ * screen low nibble, 3 = colour RAM), and the last index is the gap between sprites.
56
+ */
57
+ export const PATTERN_PALETTES: Record<GraphicsMode, readonly Rgb[]> = {
58
+ hires: [
59
+ [0x00, 0x00, 0x00],
60
+ [0xff, 0xff, 0xff],
61
+ [0x35, 0x28, 0x79],
62
+ ],
63
+ multicolour: [
64
+ [0x00, 0x00, 0x00],
65
+ [0xad, 0xad, 0xad],
66
+ [0x6c, 0x6c, 0x6c],
67
+ [0xed, 0xf1, 0x71],
68
+ [0x35, 0x28, 0x79],
69
+ ],
70
+ };
71
+
72
+ /** The pixel values of one byte, left to right: 8 hires pixels, or 4 multicolour pixels of 2 each. */
73
+ function bytePixels(byte: number, mode: GraphicsMode): number[] {
74
+ if (mode === "hires") return Array.from({ length: 8 }, (_, bit) => (byte >> (7 - bit)) & 1);
75
+ return Array.from({ length: 8 }, (_, pixel) => (byte >> (6 - 2 * (pixel >> 1))) & 3);
76
+ }
77
+
78
+ export const SPRITE = { width: 24, height: 21, bytes: 63 } as const;
79
+
80
+ /**
81
+ * The sprites of `bytes` as one sheet, `perRow` sprites wide with a gap of one
82
+ * pixel. Each sprite takes `stride` bytes (64 in a VIC bank, 63 when packed).
83
+ */
84
+ export function spriteSheet(bytes: Uint8Array, mode: GraphicsMode, stride = 64, perRow = 8): IndexedFrame {
85
+ const count = Math.max(1, Math.floor((bytes.length + stride - SPRITE.bytes) / stride));
86
+ const columns = Math.min(count, perRow);
87
+ const rows = Math.ceil(count / perRow);
88
+ const width = columns * (SPRITE.width + 1) + 1;
89
+ const height = rows * (SPRITE.height + 1) + 1;
90
+ const gap = PATTERN_PALETTES[mode].length - 1;
91
+ const pixels = new Uint8Array(width * height).fill(gap);
92
+ for (let sprite = 0; sprite < count; sprite++) {
93
+ const left = (sprite % perRow) * (SPRITE.width + 1) + 1;
94
+ const top = Math.floor(sprite / perRow) * (SPRITE.height + 1) + 1;
95
+ for (let row = 0; row < SPRITE.height; row++) {
96
+ for (let column = 0; column < 3; column++) {
97
+ const values = bytePixels(bytes[sprite * stride + row * 3 + column] ?? 0, mode);
98
+ values.forEach((value, x) => (pixels[(top + row) * width + left + column * 8 + x] = value));
99
+ }
100
+ }
101
+ }
102
+ return { width, height, pixels };
103
+ }
104
+
105
+ /** Cells of 8 bytes as 8x8 pixels, `perRow` cells to a row, cell 0 at the top left. */
106
+ function cellFrame(bytes: Uint8Array, mode: GraphicsMode, perRow: number): IndexedFrame {
107
+ const cells = Math.ceil(bytes.length / 8);
108
+ const width = Math.min(cells, perRow) * 8;
109
+ const height = Math.ceil(cells / perRow) * 8;
110
+ const pixels = new Uint8Array(width * height);
111
+ for (let cell = 0; cell < cells; cell++) {
112
+ const left = (cell % perRow) * 8;
113
+ const top = Math.floor(cell / perRow) * 8;
114
+ for (let row = 0; row < 8; row++) {
115
+ bytePixels(bytes[cell * 8 + row] ?? 0, mode).forEach((value, x) => (pixels[(top + row) * width + left + x] = value));
116
+ }
117
+ }
118
+ return { width, height, pixels };
119
+ }
120
+
121
+ /** A bitmap of 8x8 cells of 8 bytes, 40 cells to a row, as the VIC reads it. */
122
+ export function bitmapFrame(bytes: Uint8Array, mode: GraphicsMode): IndexedFrame {
123
+ return cellFrame(bytes, mode, 40);
124
+ }
125
+
126
+ /** A character set: characters of 8 bytes, 16 to a row, so character N is in row N / 16 and column N % 16. */
127
+ export function charsetFrame(bytes: Uint8Array, mode: GraphicsMode): IndexedFrame {
128
+ return cellFrame(bytes, mode, 16);
129
+ }
130
+
131
+ /** Each pixel as a square of `factor` pixels, so a small image is readable on a page. */
132
+ export function scaleFrame(frame: IndexedFrame, factor: number): IndexedFrame {
133
+ const width = frame.width * factor;
134
+ const height = frame.height * factor;
135
+ const pixels = new Uint8Array(width * height);
136
+ for (let y = 0; y < height; y++) {
137
+ for (let x = 0; x < width; x++) pixels[y * width + x] = frame.pixels[Math.floor(y / factor) * frame.width + Math.floor(x / factor)]!;
138
+ }
139
+ return { width, height, pixels };
140
+ }
141
+
142
+ /**
143
+ * Screen codes as text rows of 40, as the character ROM shows the upper-case
144
+ * set: @, A-Z, the signs and digits; a reversed code (bit 7) shows as its
145
+ * code without bit 7, and a graphics code as a dot.
146
+ */
147
+ export function screenText(bytes: Uint8Array): string[] {
148
+ const rows: string[] = [];
149
+ for (let start = 0; start < bytes.length; start += 40) {
150
+ let row = "";
151
+ for (const code of bytes.subarray(start, start + 40)) {
152
+ const plain = code & 0x7f;
153
+ row += plain === 0 ? "@" : plain <= 26 ? String.fromCharCode(0x40 + plain) : plain <= 31 ? "[£]^<"[plain - 27]! : plain <= 63 ? String.fromCharCode(plain) : ".";
154
+ }
155
+ rows.push(row);
156
+ }
157
+ return rows;
158
+ }