@henols/c64-re-tools 2.1.0-rc.2 → 2.1.1

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 +1 -0
  3. package/skills/c64-assembler/scripts/assembler.ts +10 -0
  4. package/skills/c64-disk/SKILL.md +2 -2
  5. package/skills/c64-disk/scripts/disk.ts +24 -10
  6. package/skills/c64-disk/scripts/lib/containers.ts +9 -9
  7. package/skills/c64-docs/scripts/lib/links.ts +3 -3
  8. package/skills/c64-docs/scripts/lib/reference.ts +2 -2
  9. package/skills/c64-emulator/SKILL.md +8 -2
  10. package/skills/c64-knowledge/SKILL.md +3 -1
  11. package/skills/c64-knowledge/scripts/knowledge.ts +59 -43
  12. package/skills/c64-memory-map/SKILL.md +1 -1
  13. package/skills/c64-memory-map/scripts/memory-map.ts +3 -2
  14. package/skills/c64-provenance/SKILL.md +5 -4
  15. package/skills/c64-provenance/scripts/lib/library.ts +18 -6
  16. package/skills/c64-provenance/scripts/lib/separate.ts +45 -6
  17. package/skills/c64-provenance/scripts/provenance.ts +5 -4
  18. package/skills/c64-source/SKILL.md +2 -0
  19. package/skills/c64-source/scripts/lib/source.ts +2 -2
  20. package/skills/c64-source/scripts/source.ts +10 -6
  21. package/skills/c64-static-analysis/scripts/lib/idioms.ts +6 -2
  22. package/skills/c64-static-analysis/scripts/static-analysis.ts +4 -3
  23. package/skills/c64-testing/SKILL.md +11 -1
  24. package/skills/c64-testing/references/recording-format.md +2 -0
  25. package/skills/c64-testing/references/scenario-format.md +6 -0
  26. package/skills/c64-testing/scripts/lib/diff-files.ts +63 -13
  27. package/skills/c64-testing/scripts/lib/diff.ts +3 -3
  28. package/skills/c64-testing/scripts/lib/recording.ts +47 -12
  29. package/skills/c64-testing/scripts/lib/replay.ts +11 -0
  30. package/skills/c64-testing/scripts/lib/runs.ts +64 -4
  31. package/skills/c64-testing/scripts/lib/scenario.ts +43 -6
  32. package/skills/c64-testing/scripts/testing.ts +15 -2
  33. package/skills/c64-unpacker/SKILL.md +9 -5
  34. package/skills/c64-unpacker/scripts/lib/evidence.ts +4 -5
  35. package/skills/c64-unpacker/scripts/lib/loads.ts +3 -2
  36. package/skills/c64-unpacker/scripts/unpacker.ts +75 -25
  37. package/src/host/vice/session.ts +99 -10
  38. package/src/host-client/image.ts +32 -0
  39. package/src/host-client/steps.ts +106 -6
  40. package/src/host-client/tools.ts +1 -1
  41. package/src/host-client/transfer.ts +35 -8
  42. package/src/knowledge/expression.ts +1 -1
  43. package/src/knowledge/history.ts +19 -2
  44. package/src/knowledge/names.ts +2 -1
  45. package/src/knowledge/program.ts +1 -16
  46. package/src/knowledge/read.ts +43 -0
  47. package/src/knowledge/schema.ts +3 -2
  48. package/src/knowledge/text.ts +1 -0
  49. package/src/knowledge/write.ts +40 -6
  50. package/src/mcp/output.ts +5 -0
  51. package/src/mcp/server.ts +19 -4
  52. package/src/mcp/tools/debug.ts +7 -6
  53. package/src/mcp/tools/execution.ts +18 -5
  54. package/src/mcp/tools/media.ts +14 -2
  55. package/src/mcp/tools/memory.ts +7 -3
  56. package/src/mcp/tools/video.ts +5 -3
  57. package/src/project.ts +1 -1
  58. package/src/protocol/vice.ts +4 -1
@@ -3,9 +3,10 @@
3
3
  import { z } from "zod";
4
4
 
5
5
  import { formatC64Address } from "../../c64.ts";
6
+ import { isRefusal } from "../../errors.ts";
6
7
  import { EXECUTION_ACTIONS, MAX_EXECUTION_COUNT, MAX_HISTORY, MAX_TIMEOUT_FRAMES, POINT_STOP_REASONS, RUN_STATES, SPACES, STOP_REASONS, type RunTarget } from "../../protocol/vice.ts";
7
8
  import { Byte, SpaceInput, TOOL_INPUTS } from "../inputs.ts";
8
- import { AddressOutput, defineTool, toCondition, ToolOutput } from "../server.ts";
9
+ import { AddressOutput, defineTool, toCondition, ToolOutput, type ViceSessionApi } from "../server.ts";
9
10
  import { c64Observe } from "./video.ts";
10
11
 
11
12
  export const c64Execution = defineTool({
@@ -19,6 +20,8 @@ export const c64Execution = defineTool({
19
20
  "advance-frames runs exactly count video frames (count is necessary). It stops at the same raster position, count frames later. " +
20
21
  "A breakpoint, a watchpoint or a JAM opcode can stop it before that. advancedFrames tells how many frames ran. " +
21
22
  "stopReason tells why it stopped: frames (all frames ran), breakpoint, watchpoint or jam. pointId gives the id of the breakpoint or watchpoint. " +
23
+ "space tells which CPU stopped: c64, or drive8 for a breakpoint or watchpoint in the disk drive. pc is the address in that space. " +
24
+ "A CPU that a JAM stopped runs again only after c64_reset, a snapshot restore or c64_autostart. " +
22
25
  "To wait for a breakpoint or watchpoint after resume, use advance-frames with a large count. " +
23
26
  "step, next, until-return and advance-frames first stop a running CPU. Use count only with step, next and advance-frames. " +
24
27
  "step, next and until-return work only in space c64. For the drive, use c64_run_until with an address in space drive8.",
@@ -42,6 +45,7 @@ export const c64Execution = defineTool({
42
45
  advancedFrames: z.number().int().min(0).max(MAX_EXECUTION_COUNT).optional().describe("frames advance-frames ran"),
43
46
  stopReason: z.enum(["frames", ...POINT_STOP_REASONS]).optional().describe("advance-frames: why it stopped"),
44
47
  pointId: z.number().int().min(1).optional().describe("the id of the breakpoint or watchpoint that stopped advance-frames"),
48
+ space: z.enum(SPACES).optional().describe("advance-frames: the CPU that stopped, c64 or drive8. pc is the address in that space"),
45
49
  }),
46
50
  readOnly: false,
47
51
  async run(input, session) {
@@ -54,6 +58,7 @@ export const c64Execution = defineTool({
54
58
  ...(result.advancedFrames === undefined ? {} : { advancedFrames: result.advancedFrames }),
55
59
  ...(result.stopReason === undefined ? {} : { stopReason: result.stopReason }),
56
60
  ...(result.pointId === undefined ? {} : { pointId: result.pointId }),
61
+ ...(result.space === undefined ? {} : { space: result.space }),
57
62
  };
58
63
  },
59
64
  });
@@ -93,6 +98,7 @@ export const c64RunUntil = defineTool({
93
98
  history: z
94
99
  .array(z.object({ address: AddressOutput, bytes: z.string(), text: z.string(), a: Byte, x: Byte, y: Byte, sp: Byte, rasterLine: z.number().int().min(0).optional(), rasterCycle: z.number().int().min(0).optional() }))
95
100
  .optional(),
101
+ historyMissing: z.string().optional().describe("why the result has no history, for example an old history after a JAM"),
96
102
  }),
97
103
  readOnly: false,
98
104
  async run(input, session) {
@@ -108,10 +114,7 @@ export const c64RunUntil = defineTool({
108
114
  }
109
115
  const result = await session.runUntil({ target, timeoutFrames: input.timeoutFrames });
110
116
  const answer = { ...result, pc: formatC64Address(result.pc) };
111
- const history =
112
- input.history === undefined
113
- ? {}
114
- : { history: (await session.cpuHistory({ limit: input.history, space: result.space })).entries.map((entry) => ({ ...entry, address: formatC64Address(entry.address) })) };
117
+ const history = input.history === undefined ? {} : await runHistory(session, input.history, result.space);
115
118
  if (input.observe === undefined) return { ...answer, ...history };
116
119
  // The observation at the stop: an image of the screen comes with the result as c64_observe gives it.
117
120
  const observed = await c64Observe.run(input.observe, session);
@@ -120,4 +123,14 @@ export const c64RunUntil = defineTool({
120
123
  },
121
124
  });
122
125
 
126
+ /** The history at the stop; the run result stays when the session refuses the history. */
127
+ async function runHistory(session: ViceSessionApi, limit: number, space: "c64" | "drive8") {
128
+ try {
129
+ return { history: (await session.cpuHistory({ limit, space })).entries.map((entry) => ({ ...entry, address: formatC64Address(entry.address) })) };
130
+ } catch (error) {
131
+ if (isRefusal(error) && error.code === "operation-failed") return { historyMissing: error.message };
132
+ throw error;
133
+ }
134
+ }
135
+
123
136
  export const executionTools = [c64Execution, c64RunUntil];
@@ -2,14 +2,17 @@
2
2
  // c64_snapshot. Paths are relative to the project folder; the host-client
3
3
  // reads the file and the Host Runtime receives only its bytes.
4
4
 
5
+ import { extname } from "node:path";
6
+
5
7
  import { z } from "zod";
6
8
 
7
9
  import { formatC64Address } from "../../c64.ts";
8
10
  import { projectFolder, readProjectFile } from "../../host-client/transfer.ts";
11
+ import { WireFailure } from "../../protocol/messages.ts";
9
12
  import { AUTOSTART_TYPES, DISK_TYPES, MAX_SNAPSHOTS, RUN_STATES } from "../../protocol/vice.ts";
10
13
  import { OutInput, ProjectInput, writeOut, WrittenOutput } from "../output.ts";
11
14
  import { AddressInput } from "../inputs.ts";
12
- import { AddressOutput, defineTool, requireFields, TransientName } from "../server.ts";
15
+ import { AddressOutput, defineTool, requireFields, requireOut, TransientName } from "../server.ts";
13
16
 
14
17
  const ProjectPath = z.string().min(1).describe("path relative to the project folder, for example build/game.prg");
15
18
 
@@ -63,6 +66,10 @@ export const c64ProgramLoad = defineTool({
63
66
  outputSchema: z.object({ state: z.enum(RUN_STATES), loadAddress: AddressOutput, size: z.number().int().min(1).max(0x10000) }),
64
67
  readOnly: false,
65
68
  async run(input, session) {
69
+ const type = extname(input.path).slice(1).toLowerCase();
70
+ if (type !== "prg" && (AUTOSTART_TYPES as readonly string[]).includes(type)) {
71
+ throw new WireFailure("invalid-input", `${input.path} is a .${type} image, not a PRG. Use c64_autostart, or c64_disk_attach for a disk image.`);
72
+ }
66
73
  const root = projectFolder(input.project);
67
74
  const result = await session.programLoad(input.address === undefined ? { path: input.path, root } : { path: input.path, address: input.address, root });
68
75
  return { state: result.state, loadAddress: formatC64Address(result.loadAddress), size: result.size };
@@ -118,12 +125,17 @@ export const c64Snapshot = defineTool({
118
125
  requireFields(action, fields, []);
119
126
  return session.snapshot({ action });
120
127
  }
128
+ if (action === "restore") {
129
+ if (fields.name !== undefined && fields.file !== undefined) throw new WireFailure("invalid-input", "Action restore takes name or file, not both.");
130
+ if (fields.name === undefined && fields.file === undefined) throw new WireFailure("invalid-input", "Action restore needs name (a snapshot of this session) or file (a .vsf in the project).");
131
+ }
121
132
  if (action === "restore" && fields.file !== undefined) {
122
133
  requireFields(action, fields, ["file", "project"], ["file"]);
123
134
  return session.snapshotRestore(readProjectFile(fields.file, { root: projectFolder(fields.project) }).bytes);
124
135
  }
125
136
  if (action === "save") {
126
- requireFields(action, fields, fields.out === undefined ? ["name"] : ["name", "out", "project"], ["name"]);
137
+ requireOut(fields);
138
+ requireFields(action, fields, ["name", "out", "project"], ["name"]);
127
139
  const saved = await session.snapshot({ action, name: fields.name! });
128
140
  return fields.out === undefined ? saved : { ...saved, ...writeOut(fields.out, await session.snapshotExport(fields.name!), fields.project) };
129
141
  }
@@ -6,7 +6,7 @@ import { z } from "zod";
6
6
  import { formatC64Address } from "../../c64.ts";
7
7
  import { WireFailure } from "../../protocol/messages.ts";
8
8
  import { MAX_COMPARE_DIFFERENCES, MAX_COMPARE_SIZE, MAX_DISASSEMBLE, MAX_MEMORY_READ, MAX_MEMORY_WRITE, MAX_SEARCH_PATTERN, MAX_SEARCH_RESULTS, type RegisterValues } from "../../protocol/vice.ts";
9
- import { OutInput, ProjectInput, writeOut, WrittenOutput } from "../output.ts";
9
+ import { linesText, OutInput, ProjectInput, writeOut, WrittenOutput } from "../output.ts";
10
10
  import { AddressInput, Byte, MemoryViewInput, SpaceInput, TOOL_INPUTS } from "../inputs.ts";
11
11
  import { AddressOutput, defineTool, HexData, requireFields } from "../server.ts";
12
12
 
@@ -142,7 +142,11 @@ export const c64MemorySearch = defineTool({
142
142
  throw new WireFailure("invalid-input", `pattern has ${input.pattern.length} bytes. The limit is ${MAX_SEARCH_PATTERN}.`);
143
143
  }
144
144
  if (input.pattern.every((token) => token === null)) throw new WireFailure("invalid-input", "pattern needs at least one byte that is not ??.");
145
- const result = await session.memorySearch({ ...input, end: input.end ?? 0xffff });
145
+ const end = input.end ?? 0xffff;
146
+ if (end < input.start) throw new WireFailure("invalid-input", "end is lower than start.");
147
+ // VICE refuses a range shorter than the pattern. Such a range has no match.
148
+ if (end - input.start + 1 < input.pattern.length) return { matches: [] };
149
+ const result = await session.memorySearch({ ...input, end });
146
150
  return { matches: result.matches.map(formatC64Address) };
147
151
  },
148
152
  });
@@ -220,7 +224,7 @@ export const c64Disassemble = defineTool({
220
224
  address = instruction.address + instruction.bytes.length / 2;
221
225
  }
222
226
  }
223
- return { count: lines.length, ...writeOut(out, `${lines.join("\n")}\n`, project) };
227
+ return { count: lines.length, ...writeOut(out, linesText(lines), project) };
224
228
  },
225
229
  });
226
230
 
@@ -8,7 +8,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";
10
10
  import { AddressInput, Byte, MemoryViewInput, SpaceInput } from "../inputs.ts";
11
- import { AddressOutput, defineTool, HexData, requireFields, ToolOutput, TransientName } from "../server.ts";
11
+ import { AddressOutput, defineTool, HexData, requireFields, requireOut, ToolOutput, TransientName } from "../server.ts";
12
12
 
13
13
  const Color = z.number().int().min(0).max(15);
14
14
  const NO_CHANGE = "This does not change the run state of the machine.";
@@ -170,14 +170,16 @@ export const c64Screen = defineTool({
170
170
  async run(input, session) {
171
171
  const { action, ...fields } = input;
172
172
  if (action === "capture") {
173
- requireFields(action, fields, fields.out === undefined ? ["baseline"] : ["baseline", "out", "project"]);
173
+ requireOut(fields);
174
+ requireFields(action, fields, ["baseline", "out", "project"]);
174
175
  const shot = await session.screenCapture(fields.baseline);
175
176
  const result = { width: shot.width, height: shot.height, ...(shot.baseline === undefined ? {} : { baseline: shot.baseline }) };
176
177
  if (fields.out !== undefined) return { ...result, ...writeOut(fields.out, Buffer.from(shot.png, "base64"), fields.project) };
177
178
  return new ToolOutput(result, [{ type: "image", data: shot.png, mimeType: "image/png" }]);
178
179
  }
179
180
  if (action === "compare") {
180
- requireFields(action, fields, ["baseline", "maxMismatchRatio", "mask", "includeDiff", ...(fields.out === undefined ? [] : ["out", "project"])], ["baseline"]);
181
+ requireOut(fields);
182
+ requireFields(action, fields, ["baseline", "maxMismatchRatio", "mask", "includeDiff", "out", "project"], ["baseline"]);
181
183
  const { diffPng, ...result } = await session.screenCompare({
182
184
  baseline: fields.baseline!,
183
185
  maxMismatchRatio: fields.maxMismatchRatio ?? 0,
package/src/project.ts CHANGED
@@ -119,7 +119,7 @@ export function namedProject(folder: string, cwd: string = process.cwd()): strin
119
119
  if (path === homedir()) throw new Refusal("invalid-input", "The home folder is not a project folder.");
120
120
  const outer = foldersUpFrom(path).slice(1).find(isProject);
121
121
  if (outer !== undefined) {
122
- throw new Refusal("invalid-input", `${folder} is inside the project ${relative(cwd, outer) || "."}. Give that project folder.`);
122
+ throw new Refusal("invalid-input", `${folder} is inside the project ${relative(cwd, outer) || outer}. Give that project folder.`);
123
123
  }
124
124
  return path;
125
125
  }
@@ -462,6 +462,8 @@ export interface ExecutionResult {
462
462
  stopReason?: "frames" | PointStopReason;
463
463
  /** The id of the breakpoint or watchpoint that stopped the run. */
464
464
  pointId?: number;
465
+ /** advance-frames only: the CPU that stopped, and so the space of pc. */
466
+ space?: "c64" | "drive8";
465
467
  }
466
468
 
467
469
  /** c64_run_until targets. */
@@ -1171,7 +1173,8 @@ export function validateViceResult<O extends ViceOperation>(op: O, value: unknow
1171
1173
  (value.executed === undefined || isInteger(value.executed, 0, MAX_EXECUTION_COUNT)) &&
1172
1174
  (value.advancedFrames === undefined || isInteger(value.advancedFrames, 0, MAX_EXECUTION_COUNT)) &&
1173
1175
  (value.stopReason === undefined || value.stopReason === "frames" || isOneOf(POINT_STOP_REASONS, value.stopReason)) &&
1174
- (value.pointId === undefined || isInteger(value.pointId, 1, Number.MAX_SAFE_INTEGER));
1176
+ (value.pointId === undefined || isInteger(value.pointId, 1, Number.MAX_SAFE_INTEGER)) &&
1177
+ (value.space === undefined || isOneOf(SPACES, value.space));
1175
1178
  if (!ok) throw new ProtocolError("execution result is malformed");
1176
1179
  break;
1177
1180
  }