@henols/c64-re-tools 2.0.0-rc.13 → 2.0.0-rc.16

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 (58) hide show
  1. package/package.json +1 -1
  2. package/skills/c64-assembler/SKILL.md +7 -2
  3. package/skills/c64-assembler/scripts/assemble.ts +13 -5
  4. package/skills/c64-disk/SKILL.md +1 -1
  5. package/skills/c64-disk/scripts/disk.ts +1 -1
  6. package/skills/c64-emulator/SKILL.md +8 -4
  7. package/skills/c64-memory-map/SKILL.md +2 -2
  8. package/skills/c64-provenance/SKILL.md +1 -1
  9. package/skills/c64-provenance/scripts/provenance.ts +4 -4
  10. package/skills/c64-reverse-engineering/SKILL.md +2 -1
  11. package/skills/c64-source/SKILL.md +2 -2
  12. package/skills/c64-source/scripts/generate.ts +4 -4
  13. package/skills/c64-source/scripts/{loader.ts → lib/loader.ts} +11 -0
  14. package/skills/c64-static-analysis/SKILL.md +4 -3
  15. package/skills/c64-static-analysis/scripts/analyze.ts +16 -10
  16. package/skills/c64-static-analysis/scripts/{coverage.ts → lib/coverage.ts} +20 -0
  17. package/skills/c64-static-analysis/scripts/{idioms.ts → lib/idioms.ts} +14 -3
  18. package/skills/c64-testing/SKILL.md +3 -2
  19. package/skills/c64-testing/references/scenario-format.md +1 -1
  20. package/skills/c64-testing/scripts/checklist.ts +1 -1
  21. package/skills/c64-testing/scripts/{scenario.ts → lib/scenario.ts} +11 -9
  22. package/skills/c64-testing/scripts/run.ts +4 -4
  23. package/skills/c64-testing/scripts/test.ts +1 -1
  24. package/skills/c64-unpacker/SKILL.md +4 -1
  25. package/skills/c64-unpacker/scripts/unpack.ts +4 -4
  26. package/src/c64.ts +10 -0
  27. package/src/cli/cleanup.ts +1 -1
  28. package/src/cli/main.ts +2 -1
  29. package/src/cli/status.ts +69 -2
  30. package/src/host/main.ts +14 -1
  31. package/src/host/vice/session.ts +71 -15
  32. package/src/host-client/steps.ts +7 -2
  33. package/src/host-client/vice-session.ts +5 -3
  34. package/src/mcp/server.ts +1 -0
  35. package/src/mcp/tools/debug.ts +22 -7
  36. package/src/mcp/tools/execution.ts +17 -3
  37. package/src/mcp/tools/machine.ts +11 -2
  38. package/src/mcp/tools/media.ts +1 -0
  39. package/src/mcp/tools/memory.ts +7 -2
  40. package/src/mcp/tools/video.ts +3 -4
  41. package/src/native/ghidra/language/c64rt_6510.slaspec +4 -1
  42. package/src/protocol/vice.ts +32 -6
  43. /package/skills/c64-assembler/scripts/{compare.ts → lib/compare.ts} +0 -0
  44. /package/skills/c64-disk/scripts/{containers.ts → lib/containers.ts} +0 -0
  45. /package/skills/c64-provenance/scripts/{compare.ts → lib/compare.ts} +0 -0
  46. /package/skills/c64-provenance/scripts/{library.ts → lib/library.ts} +0 -0
  47. /package/skills/c64-provenance/scripts/{runtime.ts → lib/runtime.ts} +0 -0
  48. /package/skills/c64-provenance/scripts/{separate.ts → lib/separate.ts} +0 -0
  49. /package/skills/c64-source/scripts/{opcodes.ts → lib/opcodes.ts} +0 -0
  50. /package/skills/c64-source/scripts/{source.ts → lib/source.ts} +0 -0
  51. /package/skills/c64-static-analysis/scripts/{findings.ts → lib/findings.ts} +0 -0
  52. /package/skills/c64-static-analysis/scripts/{instructions.ts → lib/instructions.ts} +0 -0
  53. /package/skills/c64-static-analysis/scripts/{macros.ts → lib/macros.ts} +0 -0
  54. /package/skills/c64-static-analysis/scripts/{pointers.ts → lib/pointers.ts} +0 -0
  55. /package/skills/c64-unpacker/scripts/{evidence.ts → lib/evidence.ts} +0 -0
  56. /package/skills/c64-unpacker/scripts/{loads.ts → lib/loads.ts} +0 -0
  57. /package/skills/c64-unpacker/scripts/{machine.ts → lib/machine.ts} +0 -0
  58. /package/skills/c64-unpacker/scripts/{stages.ts → lib/stages.ts} +0 -0
@@ -3,8 +3,9 @@
3
3
  import { z } from "zod";
4
4
 
5
5
  import { formatC64Address } from "../../c64.ts";
6
+ import { PACKAGE } from "../../package.ts";
6
7
  import { VIDEO_STANDARDS } from "../../protocol/messages.ts";
7
- import { RESET_MODES, RUN_STATES, WINDOW_ACTIONS } from "../../protocol/vice.ts";
8
+ import { POINT_STOP_REASONS, RESET_MODES, RUN_STATES, WINDOW_ACTIONS } from "../../protocol/vice.ts";
8
9
  import { AddressOutput, defineTool } from "../server.ts";
9
10
 
10
11
  export const c64Status = defineTool({
@@ -14,7 +15,9 @@ export const c64Status = defineTool({
14
15
  "Get the state of the C64. The result tells if the CPU runs or has stopped, the video standard (pal or ntsc), " +
15
16
  "and if warp mode is on. It also tells if the emulator window is open (window). " +
16
17
  "When the CPU has stopped, the result also gives the program counter (pc). " +
17
- "cartridge is true while a cartridge from c64_autostart is attached: a reset keeps it, and an autostart of another file detaches it.",
18
+ "When a breakpoint, a watchpoint or a JAM opcode stopped the CPU, stopReason tells which, and pointId gives the id of the breakpoint or watchpoint. " +
19
+ "cartridge is true while a cartridge from c64_autostart is attached: a reset keeps it, and an autostart of another file detaches it. " +
20
+ "version is the c64-re-tools version of this MCP server.",
18
21
  inputSchema: z.object({}).strict(),
19
22
  outputSchema: z.object({
20
23
  state: z.enum(RUN_STATES),
@@ -22,7 +25,10 @@ export const c64Status = defineTool({
22
25
  warp: z.boolean(),
23
26
  window: z.boolean().describe("true when the emulator window is open (c64_window)"),
24
27
  pc: AddressOutput.optional().describe("program counter. Only when the CPU is stopped"),
28
+ stopReason: z.enum(POINT_STOP_REASONS).optional().describe("what stopped the CPU. Only when a breakpoint, a watchpoint or a JAM opcode stopped it"),
29
+ pointId: z.number().int().min(1).optional().describe("the id of the breakpoint or watchpoint that stopped the CPU"),
25
30
  cartridge: z.boolean().describe("true while a cartridge from c64_autostart is attached"),
31
+ version: z.string().describe("the c64-re-tools version of this MCP server"),
26
32
  }),
27
33
  readOnly: true,
28
34
  async run(_input, session) {
@@ -33,7 +39,10 @@ export const c64Status = defineTool({
33
39
  warp: status.warp,
34
40
  window: status.window,
35
41
  cartridge: status.cartridge,
42
+ version: PACKAGE.version,
36
43
  ...(status.pc === undefined ? {} : { pc: formatC64Address(status.pc) }),
44
+ ...(status.stopReason === undefined ? {} : { stopReason: status.stopReason }),
45
+ ...(status.pointId === undefined ? {} : { pointId: status.pointId }),
37
46
  };
38
47
  },
39
48
  });
@@ -23,6 +23,7 @@ export const c64Autostart = defineTool({
23
23
  "A cartridge stays in the C64 through every reset, as on a real C64. An autostart of a file that is not a cartridge detaches it first. " +
24
24
  "Both joysticks are centred first: joystick port 1 uses the keyboard lines, and a held direction changes the RUN that the autostart types. " +
25
25
  "Without index, a disk image loads like LOAD\"*\",8,1. index selects one directory entry by its number in the c64-disk directory list (0 is the first entry). " +
26
+ "The tool refuses an index of a DEL entry, or an index after the last entry. " +
26
27
  "The machine runs when this tool returns, and the load continues.",
27
28
  inputSchema: z
28
29
  .object({
@@ -12,6 +12,8 @@ import { AddressInput, AddressOutput, Byte, defineTool, HexData, HexDataInput, M
12
12
  /** With out, a read or a disassembly can be larger than one answer of the session: it is read in parts (D39). */
13
13
  const MAX_MEMORY_READ_OUT = 0x10000;
14
14
  const MAX_DISASSEMBLE_OUT = 32768;
15
+ /** A memory byte in a result: two lowercase hex digits, as memory data is. */
16
+ const HexByte = z.string().regex(/^[0-9a-f]{2}$/);
15
17
 
16
18
  export const c64MemoryRead = defineTool({
17
19
  name: "c64_memory_read",
@@ -182,6 +184,7 @@ export const c64MemoryCompare = defineTool({
182
184
  description:
183
185
  `Compare two memory ranges of size bytes (1 to ${MAX_COMPARE_SIZE}). Each range has its own address, space and view. ` +
184
186
  `The result tells if they are equal, how many bytes differ, and the first ${MAX_COMPARE_DIFFERENCES} differences with their offset. ` +
187
+ "The bytes of a difference are lowercase hex. " +
185
188
  "This does not change the run state of the machine.",
186
189
  inputSchema: z
187
190
  .object({ left: Location, right: Location, size: z.number().int().min(1).max(MAX_COMPARE_SIZE) })
@@ -189,11 +192,13 @@ export const c64MemoryCompare = defineTool({
189
192
  outputSchema: z.object({
190
193
  equal: z.boolean(),
191
194
  differentBytes: z.number().int().min(0),
192
- firstDifferences: z.array(z.object({ offset: z.number().int().min(0), left: Byte, right: Byte })),
195
+ firstDifferences: z.array(z.object({ offset: z.number().int().min(0), left: HexByte, right: HexByte })),
193
196
  }),
194
197
  readOnly: true,
195
198
  async run(input, session) {
196
- return session.memoryCompare(input);
199
+ const result = await session.memoryCompare(input);
200
+ const hex = (byte: number) => byte.toString(16).padStart(2, "0");
201
+ return { ...result, firstDifferences: result.firstDifferences.map((difference) => ({ ...difference, left: hex(difference.left), right: hex(difference.right) })) };
197
202
  },
198
203
  });
199
204
 
@@ -3,7 +3,7 @@
3
3
 
4
4
  import { z } from "zod";
5
5
 
6
- import { formatC64Address } from "../../c64.ts";
6
+ import { formatC64Address, formatSprite, formatVicii } from "../../c64.ts";
7
7
  import { WireFailure } from "../../protocol/messages.ts";
8
8
  import { CIA_SELECTIONS, MAX_MASK_RECTANGLES, MAX_MEMORY_READ, MAX_OBSERVE_BYTES, MAX_OBSERVE_RANGES, SPACES, VICII_MODES, type ObserveParams } from "../../protocol/vice.ts";
9
9
  import { OutInput, ProjectInput, writeOut, WrittenOutput } from "../output.ts";
@@ -31,8 +31,7 @@ export const c64Vicii = defineTool({
31
31
  }),
32
32
  readOnly: true,
33
33
  async run(_input, session) {
34
- const state = await session.vicii();
35
- return { ...state, screenAddress: formatC64Address(state.screenAddress), graphicsAddress: formatC64Address(state.graphicsAddress) };
34
+ return formatVicii(await session.vicii());
36
35
  },
37
36
  });
38
37
 
@@ -62,7 +61,7 @@ export const c64Sprite = defineTool({
62
61
  readOnly: true,
63
62
  async run(input, session) {
64
63
  const result = await session.sprites(input.sprites ?? []);
65
- return { sprites: result.sprites.map((sprite) => ({ ...sprite, dataAddress: formatC64Address(sprite.dataAddress) })) };
64
+ return { sprites: result.sprites.map(formatSprite) };
66
65
  },
67
66
  });
68
67
 
@@ -263,13 +263,16 @@ REL: target is rel [ target = inst_next + rel; ] { export *:2 target; }
263
263
  }
264
264
 
265
265
  # BRK skips its padding byte: the return address is two bytes on.
266
+ # The vector at $FFFE is in ROM unless the program banks the KERNAL out, and
267
+ # the analysis cannot know the banking. Thus BRK ends the flow, and Ghidra
268
+ # does not follow the RAM bytes at $FFFE of a flat image (R2-07).
266
269
  :BRK is op=0x00 {
267
270
  *:2 (SP - 1) = inst_next + 1;
268
271
  SP = SP - 2;
269
272
  pushP(1);
270
273
  I = 1;
271
274
  local vector:2 = 0xfffe;
272
- goto [*:2 vector];
275
+ return [*:2 vector];
273
276
  }
274
277
 
275
278
  :CLC is op=0x18 { C = 0; }
@@ -337,8 +337,16 @@ export interface MachineStatus {
337
337
  pc?: number;
338
338
  /** True while a cartridge from an autostart is attached (D64). */
339
339
  cartridge: boolean;
340
+ /** Present only while stopped by a user breakpoint or watchpoint, or by a JAM. */
341
+ stopReason?: PointStopReason;
342
+ /** The id of the breakpoint or watchpoint that stopped the machine. */
343
+ pointId?: number;
340
344
  }
341
345
 
346
+ /** What stopped the machine outside a request that stops it. */
347
+ export const POINT_STOP_REASONS = ["breakpoint", "watchpoint", "jam"] as const;
348
+ export type PointStopReason = (typeof POINT_STOP_REASONS)[number];
349
+
342
350
  export interface MemoryReadParams {
343
351
  address: number;
344
352
  size: number;
@@ -406,6 +414,10 @@ export interface ExecutionResult {
406
414
  pc?: number;
407
415
  executed?: number;
408
416
  advancedFrames?: number;
417
+ /** advance-frames only: frames when all frames ran, else what stopped the run early. */
418
+ stopReason?: "frames" | PointStopReason;
419
+ /** The id of the breakpoint or watchpoint that stopped the run. */
420
+ pointId?: number;
409
421
  }
410
422
 
411
423
  /** c64_run_until targets. */
@@ -425,6 +437,8 @@ export interface RunUntilResult {
425
437
  /** The PC of the CPU in `space`: the CPU that stopped. */
426
438
  pc: number;
427
439
  space: Space;
440
+ /** The id of the breakpoint or watchpoint that stopped the run. */
441
+ pointId?: number;
428
442
  }
429
443
 
430
444
  export interface ViceOperations {
@@ -469,7 +483,8 @@ export interface ViceOperations {
469
483
  backtrace: { params: { depth: number; space: Space }; result: { frames: BacktraceFrame[] } };
470
484
  /** `cycles` is a decimal string. */
471
485
  timing: { params: { action: (typeof TIMING_ACTIONS)[number] }; result: { started: boolean } | { cycles: string } };
472
- profile: { params: { limit: number }; result: { entries: ProfileEntry[] } };
486
+ /** clear starts the profiler again from zero. */
487
+ profile: { params: { action: "read"; limit: number } | { action: "clear" }; result: { entries: ProfileEntry[] } | { cleared: boolean } };
473
488
  memmap: {
474
489
  params: { action: "read"; start: number; end: number; maxRanges: number } | { action: "clear" };
475
490
  /** `next`: more ranges follow from this address; read again from there. */
@@ -646,9 +661,11 @@ export function validateViceParams<O extends ViceOperation>(op: O, params: unkno
646
661
  return result as ViceOperations[O]["params"];
647
662
  }
648
663
  case "profile": {
649
- onlyFields(params, ["limit"]);
664
+ onlyFields(params, params.action === "clear" ? ["action"] : ["action", "limit"]);
665
+ if (params.action === "clear") return { action: "clear" } as ViceOperations[O]["params"];
666
+ if (params.action !== "read") invalid("action must be read or clear");
650
667
  if (!isInteger(params.limit, 1, MAX_PROFILE)) invalid(`limit must be an integer from 1 to ${MAX_PROFILE}`);
651
- return { limit: params.limit } as ViceOperations[O]["params"];
668
+ return { action: "read", limit: params.limit } as ViceOperations[O]["params"];
652
669
  }
653
670
  case "memmap": {
654
671
  onlyFields(params, params.action === "clear" ? ["action"] : ["action", "start", "end", "maxRanges"]);
@@ -1011,7 +1028,9 @@ export function validateViceResult<O extends ViceOperation>(op: O, value: unknow
1011
1028
  typeof value.warp === "boolean" &&
1012
1029
  typeof value.window === "boolean" &&
1013
1030
  (value.pc === undefined || isInteger(value.pc, 0, 0xffff)) &&
1014
- typeof value.cartridge === "boolean";
1031
+ typeof value.cartridge === "boolean" &&
1032
+ (value.stopReason === undefined || isOneOf(POINT_STOP_REASONS, value.stopReason)) &&
1033
+ (value.pointId === undefined || isInteger(value.pointId, 1, Number.MAX_SAFE_INTEGER));
1015
1034
  if (!ok) throw new ProtocolError("status result is malformed");
1016
1035
  break;
1017
1036
  }
@@ -1080,7 +1099,8 @@ export function validateViceResult<O extends ViceOperation>(op: O, value: unknow
1080
1099
  isOneOf(STOP_REASONS, value.stopReason) &&
1081
1100
  isOneOf(RUN_STATES, value.state) &&
1082
1101
  isInteger(value.pc, 0, 0xffff) &&
1083
- isOneOf(SPACES, value.space);
1102
+ isOneOf(SPACES, value.space) &&
1103
+ (value.pointId === undefined || isInteger(value.pointId, 1, Number.MAX_SAFE_INTEGER));
1084
1104
  if (!ok) throw new ProtocolError("runUntil result is malformed");
1085
1105
  break;
1086
1106
  }
@@ -1089,7 +1109,9 @@ export function validateViceResult<O extends ViceOperation>(op: O, value: unknow
1089
1109
  isOneOf(RUN_STATES, value.state) &&
1090
1110
  (value.pc === undefined || isInteger(value.pc, 0, 0xffff)) &&
1091
1111
  (value.executed === undefined || isInteger(value.executed, 0, MAX_EXECUTION_COUNT)) &&
1092
- (value.advancedFrames === undefined || isInteger(value.advancedFrames, 0, MAX_EXECUTION_COUNT));
1112
+ (value.advancedFrames === undefined || isInteger(value.advancedFrames, 0, MAX_EXECUTION_COUNT)) &&
1113
+ (value.stopReason === undefined || value.stopReason === "frames" || isOneOf(POINT_STOP_REASONS, value.stopReason)) &&
1114
+ (value.pointId === undefined || isInteger(value.pointId, 1, Number.MAX_SAFE_INTEGER));
1093
1115
  if (!ok) throw new ProtocolError("execution result is malformed");
1094
1116
  break;
1095
1117
  }
@@ -1198,6 +1220,10 @@ export function validateViceResult<O extends ViceOperation>(op: O, value: unknow
1198
1220
  break;
1199
1221
  }
1200
1222
  case "profile": {
1223
+ if ("cleared" in value) {
1224
+ if (value.cleared !== true) throw new ProtocolError("profile result is malformed");
1225
+ break;
1226
+ }
1201
1227
  const ok =
1202
1228
  Array.isArray(value.entries) &&
1203
1229
  value.entries.every(