@henols/c64-re-tools 2.0.0-rc.9 → 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 (117) hide show
  1. package/README.md +97 -116
  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 +18 -4
  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/skills/c64-assembler/scripts/compare.ts +0 -84
  117. package/skills/c64-source/scripts/source.ts +0 -595
package/src/mcp/main.ts CHANGED
@@ -46,7 +46,7 @@ function videoStandard(env: NodeJS.ProcessEnv): VideoStandard {
46
46
  * One session at a time (1 MCP = 1 session = 1 VICE). It opens at start; if
47
47
  * that fails (for example the host was not running yet), the next tool call
48
48
  * tries again. When the host connection is lost, the next call opens a new
49
- * session (D57). A lost session that the emulator answered first gives its
49
+ * session. A lost session that the emulator answered first gives its
50
50
  * machine-state-lost once, unless a pending call got it already. Close stops
51
51
  * an open that is still waiting for the host at once.
52
52
  */
package/src/mcp/output.ts CHANGED
@@ -1,14 +1,21 @@
1
- // Tool results written to a project file instead of the context (D39). The
2
- // MCP server runs on the agent's machine with the project as its working
3
- // directory, so it writes the file itself; the host never sees the path.
1
+ // Tool results written to a project file instead of the context. The
2
+ // MCP server runs on the agent's machine, so it writes the file itself; the
3
+ // host never sees the path. The project is the folder that `project` names,
4
+ // or the one that the project rules find from the folder the agent started in.
4
5
 
5
6
  import { createHash } from "node:crypto";
6
7
 
7
8
  import { z } from "zod";
8
9
 
9
- import { writeProjectFile } from "../host-client/transfer.ts";
10
+ import { projectFolder, writeProjectFile } from "../host-client/transfer.ts";
10
11
 
11
- export const OutInput = z.string().min(1).describe("write the result to this file, relative to the project directory, in place of returning it. An existing file is overwritten");
12
+ export const OutInput = z.string().min(1).describe("write the result to this file, relative to the project folder, in place of returning it. An existing file is overwritten");
13
+
14
+ /** The project of a tool with a path. */
15
+ export const ProjectInput = z
16
+ .string()
17
+ .min(1)
18
+ .describe("the project folder, relative to the folder where the agent started. Give it when that folder is not in a project and has projects below it. The paths of this call are relative to the project folder");
12
19
 
13
20
  /** What a result says in place of the bytes it wrote. */
14
21
  export interface Written {
@@ -24,9 +31,9 @@ export const WrittenOutput = {
24
31
  sha256: z.string().regex(/^[0-9a-f]{64}$/).optional(),
25
32
  };
26
33
 
27
- /** Writes `data` to the project file `path` and gives what the result shows in its place. */
28
- export function writeOut(path: string, data: Uint8Array | string, root?: string): Written {
34
+ /** Writes `data` to the file `path` of the project that `project` names (or the project lookup finds), and gives what the result shows in its place. */
35
+ export function writeOut(path: string, data: Uint8Array | string, project: string | undefined): Written {
29
36
  const bytes = typeof data === "string" ? Buffer.from(data, "utf8") : data;
30
- writeProjectFile(path, bytes, { what: "out", ...(root === undefined ? {} : { root }) });
37
+ writeProjectFile(path, bytes, { what: "out", root: projectFolder(project) });
31
38
  return { out: path, bytes: bytes.length, sha256: createHash("sha256").update(bytes).digest("hex") };
32
39
  }
package/src/mcp/server.ts CHANGED
@@ -41,6 +41,7 @@ export type ViceSessionApi = Pick<
41
41
  | "snapshotExport"
42
42
  | "snapshotRestore"
43
43
  | "cpuHistory"
44
+ | "trace"
44
45
  | "backtrace"
45
46
  | "timing"
46
47
  | "profile"
@@ -265,7 +266,7 @@ async function runCall(
265
266
  try {
266
267
  output = await tool.run(parsed.data, await session());
267
268
  } catch (error) {
268
- // A session that the emulator never answered had no machine state: the call goes again on a new session (D57).
269
+ // A session that the emulator never answered had no machine state: the call goes again on a new session.
269
270
  if (!(error instanceof LostBeforeAnswer)) throw error;
270
271
  output = await tool.run(parsed.data, await session());
271
272
  }
@@ -300,6 +301,7 @@ const INSTRUCTIONS =
300
301
  "These tools control one live Commodore 64 emulator that belongs to this session. " +
301
302
  "Addresses are four hex digits after a dollar sign, for example $c000. " +
302
303
  "Memory data is lowercase hex without separators. " +
304
+ "Other register values and chip values are decimal numbers. " +
303
305
  "A read does not change the run state of the machine.";
304
306
 
305
307
  export function createMcpServer(options: { tools: readonly ToolDefinition[]; session: SessionSource; log?: (line: string) => void }): Server {
@@ -5,10 +5,10 @@ import { z } from "zod";
5
5
 
6
6
  import { COVERAGE, executedRamCode, EXECUTE, formatC64Address, READ, WRITE, type CodeRange } from "../../c64.ts";
7
7
  import { WireFailure } from "../../protocol/messages.ts";
8
- import { CHECKPOINT_ACTIONS, MAX_BACKTRACE, MAX_HISTORY, MAX_MEMMAP_RANGES, MAX_MEMORY_READ, MAX_PROFILE, MAX_WATCH_SIZE, SPACES, TIMING_ACTIONS, WATCH_ACCESS, type Breakpoint, type Watchpoint } from "../../protocol/vice.ts";
8
+ import { CHECKPOINT_ACTIONS, MAX_BACKTRACE, MAX_HISTORY, MAX_TRACE_FRAMES, MAX_TRACE_HITS, MAX_TRACE_POINTS, TRACE_KINDS, TRACE_ENDS, MAX_MEMMAP_RANGES, MAX_MEMORY_READ, MAX_PROFILE, MAX_WATCH_SIZE, SPACES, TIMING_ACTIONS, WATCH_ACCESS, type Breakpoint, type Watchpoint } from "../../protocol/vice.ts";
9
9
  import { readCoverage } from "../../host-client/coverage.ts";
10
- import { OutInput, writeOut, WrittenOutput } from "../output.ts";
11
- import { AddressInput, AddressOutput, Byte, ConditionInput, ConditionOutput, defineTool, HexData, requireFields, showCondition, SpaceInput, toCondition, type ViceSessionApi } from "../server.ts";
10
+ import { OutInput, ProjectInput, writeOut, WrittenOutput } from "../output.ts";
11
+ import { AddressInput, AddressOutput, Byte, ConditionInput, ConditionOutput, defineTool, HexData, requireFields, showCondition, SpaceInput, toCondition } from "../server.ts";
12
12
  import { spaced } from "./memory.ts";
13
13
 
14
14
  /** No default here: a filled-in default would count as a field the other actions refuse. */
@@ -135,7 +135,7 @@ export const c64CpuHistory = defineTool({
135
135
  "In space c64, it also gives the raster line and cycle when the instruction started. " +
136
136
  "With out, it writes the entries as text lines to that project file, and the result gives the file in place of the entries. " +
137
137
  "This does not change the run state of the machine.",
138
- inputSchema: z.object({ limit: z.number().int().min(1).max(MAX_HISTORY).default(50), space: SpaceInput, out: OutInput.optional() }).strict(),
138
+ inputSchema: z.object({ limit: z.number().int().min(1).max(MAX_HISTORY).default(50), space: SpaceInput, out: OutInput.optional(), project: ProjectInput.optional() }).strict(),
139
139
  outputSchema: z.object({
140
140
  entries: z.array(
141
141
  z.object({
@@ -155,7 +155,8 @@ export const c64CpuHistory = defineTool({
155
155
  }),
156
156
  readOnly: false,
157
157
  async run(input, session) {
158
- const { out, ...request } = input;
158
+ const { out, project, ...request } = input;
159
+ if (out === undefined && project !== undefined) throw new WireFailure("invalid-input", "project is used only with out.");
159
160
  const { entries } = await session.cpuHistory(request);
160
161
  if (out === undefined) return { entries: entries.map((entry) => ({ ...entry, address: formatC64Address(entry.address) })) };
161
162
  const byte = (value: number) => value.toString(16).padStart(2, "0");
@@ -163,7 +164,71 @@ export const c64CpuHistory = defineTool({
163
164
  const raster = entry.rasterLine === undefined ? "" : ` line ${entry.rasterLine} cycle ${entry.rasterCycle ?? 0}`;
164
165
  return `${formatC64Address(entry.address)} ${spaced(entry.bytes).padEnd(9)} ${entry.text.padEnd(16)} a=${byte(entry.a)} x=${byte(entry.x)} y=${byte(entry.y)} sp=${byte(entry.sp)}${raster}`;
165
166
  });
166
- return { count: entries.length, ...writeOut(out, `${lines.join("\n")}\n`) };
167
+ return { count: entries.length, ...writeOut(out, `${lines.join("\n")}\n`, project) };
168
+ },
169
+ });
170
+
171
+ /** The hits that a trace result gives inline; out gives all of them. */
172
+ const TRACE_LISTED = 200;
173
+
174
+ export const c64Trace = defineTool({
175
+ name: "c64_trace",
176
+ title: "Trace accesses and instructions",
177
+ description:
178
+ "Run the machine and record each hit of the points. The program does not stop for you. " +
179
+ "A point is exec (an instruction at an address from start to end), read or write (an access from start to end). " +
180
+ `Give 1 to ${MAX_TRACE_POINTS} points. ` +
181
+ "Each hit gives the instruction (pc, bytes, text) and the registers a, x, y and sp before it. " +
182
+ "It also gives the frame from 0, and the raster line and cycle where the instruction started. " +
183
+ "A read or write hit also gives the address that the instruction used, and the byte there after the access. " +
184
+ `The trace ends after frames frames (1 to ${MAX_TRACE_FRAMES}), at maxHits hits (at most ${MAX_TRACE_HITS}), or at a breakpoint, a watchpoint or a JAM. ` +
185
+ `The result lists the first ${TRACE_LISTED} hits. With out, it writes all hits as JSON lines to that project file. ` +
186
+ "Each hit stops the machine for a short time. Thus a point with many hits in each frame makes the trace slow. Use a small range and a maxHits. " +
187
+ "The machine is stopped after the trace.",
188
+ inputSchema: z
189
+ .object({
190
+ points: z.array(z.object({ kind: z.enum(TRACE_KINDS), start: AddressInput, end: AddressInput.optional() }).strict()).min(1).max(MAX_TRACE_POINTS),
191
+ frames: z.number().int().min(1).max(MAX_TRACE_FRAMES),
192
+ maxHits: z.number().int().min(1).max(MAX_TRACE_HITS).default(MAX_TRACE_HITS),
193
+ out: OutInput.optional(),
194
+ project: ProjectInput.optional(),
195
+ })
196
+ .strict(),
197
+ outputSchema: z.object({
198
+ end: z.enum(TRACE_ENDS),
199
+ frames: z.number().int().min(0),
200
+ pc: AddressOutput,
201
+ count: z.number().int().min(0),
202
+ hits: z.array(
203
+ z.object({
204
+ point: z.number().int().min(0),
205
+ pc: AddressOutput,
206
+ bytes: HexData,
207
+ text: z.string(),
208
+ a: Byte,
209
+ x: Byte,
210
+ y: Byte,
211
+ sp: Byte,
212
+ frame: z.number().int().min(0),
213
+ rasterLine: z.number().int().min(0),
214
+ rasterCycle: z.number().int().min(0),
215
+ address: AddressOutput.optional(),
216
+ value: Byte.optional(),
217
+ }),
218
+ ),
219
+ ...WrittenOutput,
220
+ }),
221
+ readOnly: false,
222
+ async run(input, session) {
223
+ const { out, project, points, ...request } = input;
224
+ if (out === undefined && project !== undefined) throw new WireFailure("invalid-input", "project is used only with out.");
225
+ const ranges = points.map((point) => ({ kind: point.kind, start: point.start, end: point.end ?? point.start }));
226
+ if (ranges.some((point) => point.end < point.start)) throw new WireFailure("invalid-input", "A point end must not be before its start.");
227
+ const traced = await session.trace({ points: ranges, ...request });
228
+ const shown = traced.hits.map(({ address, ...hit }) => ({ ...hit, pc: formatC64Address(hit.pc), ...(address === undefined ? {} : { address: formatC64Address(address) }) }));
229
+ const summary = { end: traced.end, frames: traced.frames, pc: formatC64Address(traced.pc), count: traced.hits.length, hits: shown.slice(0, TRACE_LISTED) };
230
+ if (out === undefined) return summary;
231
+ return { ...summary, ...writeOut(out, `${shown.map((hit) => JSON.stringify(hit)).join("\n")}\n`, project) };
167
232
  },
168
233
  });
169
234
 
@@ -207,19 +272,33 @@ export const c64Profile = defineTool({
207
272
  name: "c64_profile",
208
273
  title: "Cycle profile",
209
274
  description:
210
- "Get the routines where the C64 CPU spent the most cycles since the session started or since the last c64_window. " +
275
+ "Action read gets the routines where the C64 CPU spent the most cycles since the session started, the last clear or the last c64_window. " +
276
+ "Action clear sets all counts to zero, so that the next read measures only what runs after it. " +
277
+ "The emulator gives the cycles to the routine that a JSR called last. " +
278
+ "Code that the CPU enters with JMP or a branch counts for that routine. " +
211
279
  "After c64_window or a snapshot restore, the emulator can give the cycles to a routine that ran before the restore, for example $fd50. Do not trust the routine addresses then. " +
212
- `limit is the maximum number of routines (1 to ${MAX_PROFILE}, default 20). ` +
280
+ `limit is the maximum number of routines for read (1 to ${MAX_PROFILE}, default 20). ` +
213
281
  "Each entry gives the routine address, its total cycles with the routines it calls, and its own cycles. " +
214
282
  "percent is the part of all cycles that the routine used itself. " +
215
283
  "Cycle counts are decimal strings.",
216
- inputSchema: z.object({ limit: z.number().int().min(1).max(MAX_PROFILE).default(20) }).strict(),
284
+ inputSchema: z
285
+ .object({
286
+ action: z.enum(["read", "clear"]).default("read"),
287
+ limit: z.number().int().min(1).max(MAX_PROFILE).optional().describe("read only. Default 20"),
288
+ })
289
+ .strict(),
217
290
  outputSchema: z.object({
218
- entries: z.array(z.object({ address: AddressOutput, totalCycles: z.string(), selfCycles: z.string(), percent: z.number() })),
291
+ entries: z.array(z.object({ address: AddressOutput, totalCycles: z.string(), selfCycles: z.string(), percent: z.number() })).optional(),
292
+ cleared: z.boolean().optional(),
219
293
  }),
220
- readOnly: true,
294
+ readOnly: false,
221
295
  async run(input, session) {
222
- const { entries } = await session.profile(input.limit);
296
+ const { action, ...fields } = input;
297
+ if (action === "clear") {
298
+ requireFields("clear", fields, []);
299
+ return session.profile({ action: "clear" });
300
+ }
301
+ const { entries } = await session.profile({ action: "read", limit: fields.limit ?? 20 });
223
302
  return { entries: entries.map((entry) => ({ ...entry, address: formatC64Address(entry.address) })) };
224
303
  },
225
304
  });
@@ -250,7 +329,8 @@ export const c64Memmap = defineTool({
250
329
  "If more ranges follow, the result also gives truncated: true and next. Read again with start set to next. " +
251
330
  "With out, read writes a coverage map to that project file, one byte for each address from start to end. " +
252
331
  "Bits 0 to 2 are RAM read, write and execute. Bits 3 and 4 are ROM read and execute. Bits 5 to 7 are I/O read, write and execute. " +
253
- "Execute is set only on the first byte of each executed instruction. " +
332
+ "Execute is set only on the first byte of each executed instruction. Thus read gives a new range at each instruction of code. For code, use summary or read with out. " +
333
+ "A read on a byte right after executed code, or at a return address, can be a dummy read that the CPU does not use. " +
254
334
  "Action summary gives the counts for RAM, ROM and I/O. It also gives the executed code in RAM with its operand bytes, and the RAM that was written and executed. " +
255
335
  `Each list has at most ${MAX_SUMMARY_RANGES} ranges. ` +
256
336
  "Action clear sets all access records to zero.",
@@ -261,6 +341,7 @@ export const c64Memmap = defineTool({
261
341
  end: AddressInput.optional().describe("read and summary. Default $ffff"),
262
342
  maxRanges: z.number().int().min(1).max(MAX_MEMMAP_RANGES).optional().describe("read without out only. Default 256"),
263
343
  out: OutInput.optional().describe("read only: write a coverage map to this project file, one byte for each address. An existing file is overwritten"),
344
+ project: ProjectInput.optional().describe("read with out only: the project folder, relative to the folder where the agent started. Give it when that folder is not in a project and has projects below it"),
264
345
  })
265
346
  .strict(),
266
347
  outputSchema: z.object({
@@ -307,8 +388,8 @@ export const c64Memmap = defineTool({
307
388
  };
308
389
  }
309
390
  if (fields.out !== undefined) {
310
- requireFields("read", fields, ["start", "end", "out"]);
311
- return writeOut(fields.out, await readCoverage(session, start, end));
391
+ requireFields("read", fields, ["start", "end", "out", "project"]);
392
+ return writeOut(fields.out, await readCoverage(session, start, end), fields.project);
312
393
  }
313
394
  requireFields("read", fields, ["start", "end", "maxRanges"]);
314
395
  const result = await session.memmap({ action: "read", start, end, maxRanges: fields.maxRanges ?? 256 });
@@ -324,4 +405,4 @@ export const c64Memmap = defineTool({
324
405
  },
325
406
  });
326
407
 
327
- export const debugTools = [c64Breakpoint, c64Watchpoint, c64CpuHistory, c64Backtrace, c64Profile, c64Memmap, c64Timing];
408
+ export const debugTools = [c64Breakpoint, c64Watchpoint, c64CpuHistory, c64Trace, c64Backtrace, c64Profile, c64Memmap, c64Timing];
@@ -3,8 +3,9 @@
3
3
  import { z } from "zod";
4
4
 
5
5
  import { formatC64Address } from "../../c64.ts";
6
- import { COMPARISONS, EXECUTION_ACTIONS, MAX_EXECUTION_COUNT, MAX_TIMEOUT_FRAMES, RUN_STATES, SPACES, STOP_REASONS, type RunTarget } from "../../protocol/vice.ts";
7
- import { AddressInput, AddressOutput, Byte, ConditionInput, defineTool, MemoryViewInput, SpaceInput, toCondition } from "../server.ts";
6
+ import { COMPARISONS, 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
+ import { AddressInput, AddressOutput, Byte, ConditionInput, defineTool, MemoryViewInput, SpaceInput, toCondition, ToolOutput } from "../server.ts";
8
+ import { c64Observe } from "./video.ts";
8
9
 
9
10
  export const c64Execution = defineTool({
10
11
  name: "c64_execution",
@@ -15,7 +16,9 @@ export const c64Execution = defineTool({
15
16
  "next executes count instructions (default 1) and treats a JSR and its subroutine as one instruction. " +
16
17
  "until-return runs until the current subroutine returns (after the next RTS or RTI). " +
17
18
  "advance-frames runs exactly count video frames (count is necessary). It stops at the same raster position, count frames later. " +
18
- "A breakpoint or watchpoint can stop it before that. advancedFrames tells how many frames ran. " +
19
+ "A breakpoint, a watchpoint or a JAM opcode can stop it before that. advancedFrames tells how many frames ran. " +
20
+ "stopReason tells why it stopped: frames (all frames ran), breakpoint, watchpoint or jam. pointId gives the id of the breakpoint or watchpoint. " +
21
+ "To wait for a breakpoint or watchpoint after resume, use advance-frames with a large count. " +
19
22
  "step, next, until-return and advance-frames first stop a running CPU. Use count only with step, next and advance-frames. " +
20
23
  "step, next and until-return work only in space c64. For the drive, use c64_run_until with an address in space drive8.",
21
24
  inputSchema: z
@@ -36,6 +39,8 @@ export const c64Execution = defineTool({
36
39
  pc: AddressOutput.optional(),
37
40
  executed: z.number().int().min(0).max(MAX_EXECUTION_COUNT).optional().describe("instructions executed by step or next"),
38
41
  advancedFrames: z.number().int().min(0).max(MAX_EXECUTION_COUNT).optional().describe("frames advance-frames ran"),
42
+ stopReason: z.enum(["frames", ...POINT_STOP_REASONS]).optional().describe("advance-frames: why it stopped"),
43
+ pointId: z.number().int().min(1).optional().describe("the id of the breakpoint or watchpoint that stopped advance-frames"),
39
44
  }),
40
45
  readOnly: false,
41
46
  async run(input, session) {
@@ -46,6 +51,8 @@ export const c64Execution = defineTool({
46
51
  ...(result.pc === undefined ? {} : { pc: formatC64Address(result.pc) }),
47
52
  ...(result.executed === undefined ? {} : { executed: result.executed }),
48
53
  ...(result.advancedFrames === undefined ? {} : { advancedFrames: result.advancedFrames }),
54
+ ...(result.stopReason === undefined ? {} : { stopReason: result.stopReason }),
55
+ ...(result.pointId === undefined ? {} : { pointId: result.pointId }),
49
56
  };
50
57
  },
51
58
  });
@@ -88,15 +95,32 @@ export const c64RunUntil = defineTool({
88
95
  "Kind raster stops at the first instruction at or after a raster line and cycle. If the machine is there now, it stops at the next frame's pass. " +
89
96
  `timeoutFrames (1 to ${MAX_TIMEOUT_FRAMES}, default 3000) limits the run in video frames. ` +
90
97
  "reached tells if the target occurred. stopReason tells why the CPU stopped: target, breakpoint, watchpoint, jam (a JAM opcode) or timeout. " +
98
+ "pointId gives the id of the breakpoint or watchpoint that stopped the CPU. " +
91
99
  "space tells which CPU stopped: c64, or drive8 for a stop in the disk drive. pc is the address in that space. " +
100
+ "With observe, the result also gives what c64_observe gives at the stop, in observed. " +
101
+ `With history (1 to ${MAX_HISTORY}), it also gives the last instructions before the stop, as c64_cpu_history gives them. ` +
102
+ "Thus one call runs to a place and reads the machine there. " +
92
103
  "The CPU is always stopped after this tool.",
93
104
  inputSchema: z
94
105
  .object({
95
106
  target: RunTargetInput,
96
107
  timeoutFrames: z.number().int().min(1).max(MAX_TIMEOUT_FRAMES).default(3000),
108
+ observe: c64Observe.inputSchema.optional().describe("what to read at the stop, as for c64_observe"),
109
+ history: z.number().int().min(1).max(MAX_HISTORY).optional().describe("the number of instructions before the stop to give"),
97
110
  })
98
111
  .strict(),
99
- outputSchema: z.object({ reached: z.boolean(), stopReason: z.enum(STOP_REASONS), state: z.enum(RUN_STATES), pc: AddressOutput, space: z.enum(SPACES) }),
112
+ outputSchema: z.object({
113
+ reached: z.boolean(),
114
+ stopReason: z.enum(STOP_REASONS),
115
+ state: z.enum(RUN_STATES),
116
+ pc: AddressOutput,
117
+ space: z.enum(SPACES),
118
+ pointId: z.number().int().min(1).optional().describe("the id of the breakpoint or watchpoint that stopped the CPU"),
119
+ observed: c64Observe.outputSchema.optional(),
120
+ history: z
121
+ .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() }))
122
+ .optional(),
123
+ }),
100
124
  readOnly: false,
101
125
  async run(input, session) {
102
126
  const { target: given } = input;
@@ -110,7 +134,16 @@ export const c64RunUntil = defineTool({
110
134
  target = given.cycle === undefined ? { kind: "raster", line: given.line } : { kind: "raster", line: given.line, cycle: given.cycle };
111
135
  }
112
136
  const result = await session.runUntil({ target, timeoutFrames: input.timeoutFrames });
113
- return { ...result, pc: formatC64Address(result.pc) };
137
+ const answer = { ...result, pc: formatC64Address(result.pc) };
138
+ const history =
139
+ input.history === undefined
140
+ ? {}
141
+ : { history: (await session.cpuHistory({ limit: input.history, space: result.space })).entries.map((entry) => ({ ...entry, address: formatC64Address(entry.address) })) };
142
+ if (input.observe === undefined) return { ...answer, ...history };
143
+ // The observation at the stop: an image of the screen comes with the result as c64_observe gives it.
144
+ const observed = await c64Observe.run(input.observe, session);
145
+ if (observed instanceof ToolOutput) return new ToolOutput({ ...answer, ...history, observed: observed.structured }, observed.content);
146
+ return { ...answer, ...history, observed };
114
147
  },
115
148
  });
116
149
 
@@ -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
  });
@@ -1,16 +1,16 @@
1
1
  // Media tools: c64_autostart, c64_program_load, c64_disk_attach and
2
- // c64_snapshot. Paths are project-relative; the host-client reads the file and
3
- // the Host Runtime receives only its bytes.
2
+ // c64_snapshot. Paths are relative to the project folder; the host-client
3
+ // reads the file and the Host Runtime receives only its bytes.
4
4
 
5
5
  import { z } from "zod";
6
6
 
7
7
  import { formatC64Address } from "../../c64.ts";
8
- import { readProjectFile } from "../../host-client/transfer.ts";
8
+ import { projectFolder, readProjectFile } from "../../host-client/transfer.ts";
9
9
  import { AUTOSTART_TYPES, DISK_TYPES, MAX_SNAPSHOTS, RUN_STATES } from "../../protocol/vice.ts";
10
- import { OutInput, writeOut, WrittenOutput } from "../output.ts";
10
+ import { OutInput, ProjectInput, writeOut, WrittenOutput } from "../output.ts";
11
11
  import { AddressInput, AddressOutput, defineTool, requireFields, TransientName } from "../server.ts";
12
12
 
13
- const ProjectPath = z.string().min(1).describe("path relative to the project directory, for example build/game.prg");
13
+ const ProjectPath = z.string().min(1).describe("path relative to the project folder, for example build/game.prg");
14
14
 
15
15
  const list = (types: readonly string[]) => types.map((type) => `.${type}`).join(", ");
16
16
 
@@ -23,10 +23,12 @@ 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({
29
30
  path: ProjectPath,
31
+ project: ProjectInput.optional(),
30
32
  index: z.number().int().min(0).max(0xfffe).optional().describe("directory entry of a disk image, 0 is the first entry. Without it, the first file loads as with LOAD\"*\""),
31
33
  run: z.boolean().default(true).describe("true runs the program after it loads"),
32
34
  })
@@ -39,7 +41,7 @@ export const c64Autostart = defineTool({
39
41
  readOnly: false,
40
42
  async run(input, session) {
41
43
  // VICE numbers the directory entries from 1, and its 0 loads "*".
42
- return session.autostart({ path: input.path, index: input.index === undefined ? 0 : input.index + 1, run: input.run });
44
+ return session.autostart({ path: input.path, index: input.index === undefined ? 0 : input.index + 1, run: input.run, root: projectFolder(input.project) });
43
45
  },
44
46
  });
45
47
 
@@ -53,13 +55,15 @@ export const c64ProgramLoad = defineTool({
53
55
  inputSchema: z
54
56
  .object({
55
57
  path: ProjectPath,
58
+ project: ProjectInput.optional(),
56
59
  address: AddressInput.optional().describe("load here instead of at the file's own load address"),
57
60
  })
58
61
  .strict(),
59
62
  outputSchema: z.object({ state: z.enum(RUN_STATES), loadAddress: AddressOutput, size: z.number().int().min(1).max(0x10000) }),
60
63
  readOnly: false,
61
64
  async run(input, session) {
62
- const result = await session.programLoad(input.address === undefined ? { path: input.path } : { path: input.path, address: input.address });
65
+ const root = projectFolder(input.project);
66
+ const result = await session.programLoad(input.address === undefined ? { path: input.path, root } : { path: input.path, address: input.address, root });
63
67
  return { state: result.state, loadAddress: formatC64Address(result.loadAddress), size: result.size };
64
68
  },
65
69
  });
@@ -71,11 +75,11 @@ export const c64DiskAttach = defineTool({
71
75
  `Put a disk image from the project into drive 8 (${list(DISK_TYPES)}). ` +
72
76
  "Nothing loads or runs. To load from the disk, type LOAD on the C64 or use c64_autostart. " +
73
77
  "This does not change the run state of the machine.",
74
- inputSchema: z.object({ path: ProjectPath }).strict(),
78
+ inputSchema: z.object({ path: ProjectPath, project: ProjectInput.optional() }).strict(),
75
79
  outputSchema: z.object({ attached: z.boolean() }),
76
80
  readOnly: false,
77
81
  async run(input, session) {
78
- return session.diskAttach(input);
82
+ return session.diskAttach({ path: input.path, root: projectFolder(input.project) });
79
83
  },
80
84
  });
81
85
 
@@ -94,6 +98,7 @@ export const c64Snapshot = defineTool({
94
98
  name: TransientName.optional().describe("save and discard, and restore without file: 1 to 64 letters, digits, dots, underscores or hyphens"),
95
99
  out: OutInput.optional().describe("save only: also write the snapshot to this project file, for example snapshots/room0.vsf. An existing file is overwritten"),
96
100
  file: ProjectPath.optional().describe("restore only: a .vsf file in the project, in place of name"),
101
+ project: ProjectInput.optional().describe("save with out, and restore with file: the project folder, relative to the folder where the agent started. Give it when that folder is not in a project and has projects below it"),
97
102
  })
98
103
  .strict(),
99
104
  outputSchema: z.object({
@@ -113,13 +118,13 @@ export const c64Snapshot = defineTool({
113
118
  return session.snapshot({ action });
114
119
  }
115
120
  if (action === "restore" && fields.file !== undefined) {
116
- requireFields(action, fields, ["file"], ["file"]);
117
- return session.snapshotRestore(readProjectFile(fields.file).bytes);
121
+ requireFields(action, fields, ["file", "project"], ["file"]);
122
+ return session.snapshotRestore(readProjectFile(fields.file, { root: projectFolder(fields.project) }).bytes);
118
123
  }
119
124
  if (action === "save") {
120
- requireFields(action, fields, ["name", "out"], ["name"]);
125
+ requireFields(action, fields, fields.out === undefined ? ["name"] : ["name", "out", "project"], ["name"]);
121
126
  const saved = await session.snapshot({ action, name: fields.name! });
122
- return fields.out === undefined ? saved : { ...saved, ...writeOut(fields.out, await session.snapshotExport(fields.name!)) };
127
+ return fields.out === undefined ? saved : { ...saved, ...writeOut(fields.out, await session.snapshotExport(fields.name!), fields.project) };
123
128
  }
124
129
  requireFields(action, fields, ["name"], ["name"]);
125
130
  return session.snapshot({ action, name: fields.name! });
@@ -6,12 +6,14 @@ 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, writeOut, WrittenOutput } from "../output.ts";
9
+ import { OutInput, ProjectInput, writeOut, WrittenOutput } from "../output.ts";
10
10
  import { AddressInput, AddressOutput, Byte, defineTool, HexData, HexDataInput, MemoryViewInput, requireFields, SpaceInput } from "../server.ts";
11
11
 
12
- /** With out, a read or a disassembly can be larger than one answer of the session: it is read in parts (D39). */
12
+ /** With out, a read or a disassembly can be larger than one answer of the session: it is read in parts. */
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",
@@ -28,12 +30,14 @@ export const c64MemoryRead = defineTool({
28
30
  space: SpaceInput,
29
31
  view: MemoryViewInput,
30
32
  out: OutInput.optional(),
33
+ project: ProjectInput.optional(),
31
34
  })
32
35
  .strict(),
33
36
  outputSchema: z.object({ address: AddressOutput, size: z.number().int().min(1).max(MAX_MEMORY_READ_OUT), data: HexData.optional(), ...WrittenOutput }),
34
37
  readOnly: false,
35
38
  async run(input, session) {
36
- const { out, ...read } = input;
39
+ const { out, project, ...read } = input;
40
+ if (out === undefined && project !== undefined) throw new WireFailure("invalid-input", "project is used only with out.");
37
41
  if (out === undefined) {
38
42
  if (read.size > MAX_MEMORY_READ) throw new WireFailure("invalid-input", `size can be at most ${MAX_MEMORY_READ} without out.`);
39
43
  const result = await session.memoryRead(read);
@@ -45,7 +49,7 @@ export const c64MemoryRead = defineTool({
45
49
  const part = await session.memoryRead({ ...read, address: at, size: Math.min(MAX_MEMORY_READ, read.address + read.size - at) });
46
50
  parts.push(Buffer.from(part.data, "hex"));
47
51
  }
48
- return { address: formatC64Address(read.address), size: read.size, ...writeOut(out, Buffer.concat(parts)) };
52
+ return { address: formatC64Address(read.address), size: read.size, ...writeOut(out, Buffer.concat(parts), project) };
49
53
  },
50
54
  });
51
55
 
@@ -180,6 +184,7 @@ export const c64MemoryCompare = defineTool({
180
184
  description:
181
185
  `Compare two memory ranges of size bytes (1 to ${MAX_COMPARE_SIZE}). Each range has its own address, space and view. ` +
182
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. " +
183
188
  "This does not change the run state of the machine.",
184
189
  inputSchema: z
185
190
  .object({ left: Location, right: Location, size: z.number().int().min(1).max(MAX_COMPARE_SIZE) })
@@ -187,11 +192,13 @@ export const c64MemoryCompare = defineTool({
187
192
  outputSchema: z.object({
188
193
  equal: z.boolean(),
189
194
  differentBytes: z.number().int().min(0),
190
- 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 })),
191
196
  }),
192
197
  readOnly: true,
193
198
  async run(input, session) {
194
- 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) })) };
195
202
  },
196
203
  });
197
204
 
@@ -210,12 +217,14 @@ export const c64Disassemble = defineTool({
210
217
  space: SpaceInput,
211
218
  view: MemoryViewInput,
212
219
  out: OutInput.optional(),
220
+ project: ProjectInput.optional(),
213
221
  })
214
222
  .strict(),
215
223
  outputSchema: z.object({ instructions: z.array(z.object({ address: AddressOutput, bytes: HexData, text: z.string() })).optional(), count: z.number().int().min(0).optional(), ...WrittenOutput }),
216
224
  readOnly: false,
217
225
  async run(input, session) {
218
- const { out, ...request } = input;
226
+ const { out, project, ...request } = input;
227
+ if (out === undefined && project !== undefined) throw new WireFailure("invalid-input", "project is used only with out.");
219
228
  if (out === undefined) {
220
229
  if (request.count > MAX_DISASSEMBLE) throw new WireFailure("invalid-input", `count can be at most ${MAX_DISASSEMBLE} without out.`);
221
230
  const result = await session.disassemble(request);
@@ -238,7 +247,7 @@ export const c64Disassemble = defineTool({
238
247
  address = instruction.address + instruction.bytes.length / 2;
239
248
  }
240
249
  }
241
- return { count: lines.length, ...writeOut(out, `${lines.join("\n")}\n`) };
250
+ return { count: lines.length, ...writeOut(out, `${lines.join("\n")}\n`, project) };
242
251
  },
243
252
  });
244
253