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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. package/package.json +1 -1
  2. package/skills/c64-assembler/SKILL.md +24 -9
  3. package/skills/c64-assembler/scripts/assembler.ts +95 -0
  4. package/skills/c64-basic/SKILL.md +17 -9
  5. package/skills/c64-basic/scripts/basic.ts +26 -36
  6. package/skills/c64-disk/SKILL.md +17 -7
  7. package/skills/c64-disk/scripts/disk.ts +130 -104
  8. package/skills/c64-disk/scripts/lib/containers.ts +10 -12
  9. package/skills/c64-docs/SKILL.md +47 -28
  10. package/skills/c64-docs/references/style.md +6 -6
  11. package/skills/c64-docs/scripts/docs.ts +323 -0
  12. package/skills/c64-docs/scripts/lib/source-map.ts +1 -1
  13. package/skills/c64-emulator/SKILL.md +11 -7
  14. package/skills/c64-knowledge/SKILL.md +34 -21
  15. package/skills/c64-knowledge/scripts/knowledge.ts +597 -718
  16. package/skills/c64-knowledge/scripts/lib/listing.ts +5 -7
  17. package/skills/c64-memory-map/SKILL.md +18 -10
  18. package/skills/c64-memory-map/scripts/memory-map.ts +81 -0
  19. package/skills/c64-provenance/SKILL.md +33 -24
  20. package/skills/c64-provenance/scripts/lib/compare.ts +8 -12
  21. package/skills/c64-provenance/scripts/lib/leftovers.ts +19 -20
  22. package/skills/c64-provenance/scripts/lib/library.ts +15 -25
  23. package/skills/c64-provenance/scripts/lib/runtime.ts +12 -37
  24. package/skills/c64-provenance/scripts/lib/separate.ts +9 -10
  25. package/skills/c64-provenance/scripts/provenance.ts +177 -279
  26. package/skills/c64-reverse-engineering/SKILL.md +13 -12
  27. package/skills/c64-source/SKILL.md +28 -16
  28. package/skills/c64-source/scripts/lib/loader.ts +52 -77
  29. package/skills/c64-source/scripts/lib/source.ts +33 -42
  30. package/skills/c64-source/scripts/{generate.ts → source.ts} +68 -109
  31. package/skills/c64-static-analysis/SKILL.md +24 -13
  32. package/skills/c64-static-analysis/scripts/lib/coverage.ts +3 -4
  33. package/skills/c64-static-analysis/scripts/lib/findings.ts +2 -2
  34. package/skills/c64-static-analysis/scripts/lib/instructions.ts +1 -1
  35. package/skills/c64-static-analysis/scripts/lib/macros.ts +6 -8
  36. package/skills/c64-static-analysis/scripts/{analyze.ts → static-analysis.ts} +58 -118
  37. package/skills/c64-testing/SKILL.md +48 -32
  38. package/skills/c64-testing/references/recording-format.md +48 -19
  39. package/skills/c64-testing/references/scenario-format.md +24 -19
  40. package/skills/c64-testing/scripts/{diff.ts → lib/diff-files.ts} +18 -69
  41. package/skills/c64-testing/scripts/lib/diff.ts +5 -3
  42. package/skills/c64-testing/scripts/lib/play.ts +244 -54
  43. package/skills/c64-testing/scripts/lib/recording.ts +153 -120
  44. package/skills/c64-testing/scripts/{replay.ts → lib/replay.ts} +28 -86
  45. package/skills/c64-testing/scripts/{run.ts → lib/runs.ts} +36 -59
  46. package/skills/c64-testing/scripts/lib/scenario.ts +73 -81
  47. package/skills/c64-testing/scripts/testing.ts +189 -0
  48. package/skills/c64-unpacker/SKILL.md +42 -25
  49. package/skills/c64-unpacker/scripts/lib/evidence.ts +3 -4
  50. package/skills/c64-unpacker/scripts/lib/loads.ts +12 -10
  51. package/skills/c64-unpacker/scripts/lib/machine.ts +0 -2
  52. package/skills/c64-unpacker/scripts/lib/stages.ts +14 -19
  53. package/skills/c64-unpacker/scripts/{unpack.ts → unpacker.ts} +109 -211
  54. package/src/c64.ts +35 -20
  55. package/src/cli/main.ts +2 -2
  56. package/src/errors.ts +38 -0
  57. package/src/graphics.ts +3 -1
  58. package/src/host/server.ts +3 -2
  59. package/src/host/vice/session.ts +4 -4
  60. package/src/host-client/entry-state.ts +106 -0
  61. package/src/host-client/image.ts +21 -10
  62. package/src/host-client/steps.ts +285 -108
  63. package/src/host-client/transfer.ts +21 -15
  64. package/src/host-client/vice-session.ts +1 -1
  65. package/src/knowledge/database.ts +3 -23
  66. package/src/knowledge/expression.ts +8 -11
  67. package/src/knowledge/history.ts +2 -2
  68. package/src/knowledge/import.ts +13 -14
  69. package/src/knowledge/names.ts +50 -0
  70. package/src/knowledge/program.ts +11 -10
  71. package/src/knowledge/read.ts +16 -21
  72. package/src/knowledge/schema.ts +9 -3
  73. package/src/knowledge/text.ts +5 -5
  74. package/src/knowledge/write.ts +93 -137
  75. package/src/mcp/inputs.ts +105 -0
  76. package/src/mcp/output.ts +2 -13
  77. package/src/mcp/server.ts +12 -60
  78. package/src/mcp/tools/debug.ts +17 -12
  79. package/src/mcp/tools/execution.ts +5 -32
  80. package/src/mcp/tools/input.ts +7 -32
  81. package/src/mcp/tools/media.ts +6 -5
  82. package/src/mcp/tools/memory.ts +11 -38
  83. package/src/mcp/tools/video.ts +6 -5
  84. package/src/native/c64run.ts +1 -1
  85. package/src/native/dxa.ts +7 -6
  86. package/src/native/ghidra/analyze.ts +2 -1
  87. package/src/native/ghidra/index.ts +2 -1
  88. package/src/native/processes.ts +4 -3
  89. package/src/native/status.ts +2 -2
  90. package/src/native/types.ts +3 -7
  91. package/src/opcodes.ts +13 -13
  92. package/src/platform.ts +9 -9
  93. package/src/project.ts +16 -26
  94. package/src/protocol/messages.ts +3 -21
  95. package/src/protocol/vice.ts +7 -7
  96. package/src/script.ts +491 -49
  97. package/skills/c64-assembler/scripts/assemble.ts +0 -138
  98. package/skills/c64-docs/scripts/generate.ts +0 -109
  99. package/skills/c64-docs/scripts/link.ts +0 -239
  100. package/skills/c64-memory-map/scripts/memmap.ts +0 -92
  101. package/skills/c64-testing/scripts/checklist.ts +0 -51
  102. package/skills/c64-testing/scripts/record.ts +0 -96
  103. package/skills/c64-testing/scripts/test.ts +0 -65
@@ -0,0 +1,105 @@
1
+ // The inputs of the MCP tools that change or wait for the machine. The action
2
+ // steps of the scripts take the same fields, ranges and defaults (host-client/
3
+ // steps.ts, without zod, because the scripts install without packages);
4
+ // inputs.test.ts checks that both take the same inputs.
5
+
6
+ import { z } from "zod";
7
+
8
+ import { c64AddressOf } from "../c64.ts";
9
+ import { DEFAULT_TIMEOUT_FRAMES, keyboardBytes, MAX_RASTER_LINE } from "../host-client/steps.ts";
10
+ import { COMPARISONS, CONDITION_REGISTERS, JOYSTICK_DIRECTIONS, MAX_KEYBOARD_BYTES, MAX_MEMORY_WRITE, MAX_TIMEOUT_FRAMES, MEMORY_VIEWS, SPACES } from "../protocol/vice.ts";
11
+
12
+ export const Byte = z.number().int().min(0).max(255);
13
+
14
+ /** Input bytes: hex in either case, two digits per byte, at least one byte. Parses to lowercase. */
15
+ export const HexDataInput = z
16
+ .string()
17
+ .regex(/^(?:[0-9a-fA-F]{2})+$/, "must be hex bytes, two digits per byte, no separators, for example a9008d20d0")
18
+ .transform((value) => value.toLowerCase());
19
+
20
+ export const SpaceInput = z.enum(SPACES).default("c64").describe("c64 is the computer. drive8 is the 1541 disk drive CPU");
21
+
22
+ export const MemoryViewInput = z
23
+ .enum(MEMORY_VIEWS)
24
+ .default("cpu")
25
+ .describe("cpu reads what the CPU sees now (ROM and I/O where banked in). ram reads the RAM underneath (c64 only)");
26
+
27
+ /** A tool address: $ and 1 to 4 hex digits, either case. Parses to a number. */
28
+ export const AddressInput = z
29
+ .string()
30
+ .refine((value) => c64AddressOf(value) !== undefined, {
31
+ message: "must be $ and 1 to 4 hex digits, for example $c000 or $fb. A tool takes no names: get the address of a name with knowledge.ts at <name> of the c64-knowledge skill",
32
+ })
33
+ .transform((value) => c64AddressOf(value)!)
34
+ .describe("C64 address: $ and 1 to 4 hex digits, $0000 to $ffff");
35
+
36
+ const rasterLine = z.number().int().min(0).max(MAX_RASTER_LINE).describe("raster line: PAL 0-311, NTSC 0-262");
37
+
38
+ const condition = z
39
+ .discriminatedUnion("kind", [
40
+ z.object({ kind: z.literal("register"), register: z.enum(CONDITION_REGISTERS), operator: z.enum(COMPARISONS), value: Byte }).strict(),
41
+ z.object({ kind: z.literal("memory"), address: AddressInput, operator: z.enum(COMPARISONS), value: Byte, space: SpaceInput, view: MemoryViewInput }).strict(),
42
+ z.object({ kind: z.literal("raster"), line: rasterLine, cycle: z.number().int().min(0).max(64).optional().describe("cycle in the line: PAL 0-62, NTSC 0-64. True from this cycle on") }).strict(),
43
+ ])
44
+ .describe("register: a CPU register compared to a byte. memory: a byte in memory compared to a byte. raster: the raster position");
45
+
46
+ const flags = z.object({ n: z.boolean(), v: z.boolean(), b: z.boolean(), d: z.boolean(), i: z.boolean(), z: z.boolean(), c: z.boolean() });
47
+
48
+ export const TOOL_INPUTS = {
49
+ condition,
50
+
51
+ runTarget: z.discriminatedUnion("kind", [
52
+ z.object({ kind: z.literal("address"), address: AddressInput, space: SpaceInput, condition: condition.optional().describe("stop at the address only when this is true") }).strict(),
53
+ z.object({ kind: z.literal("memory"), address: AddressInput, operator: z.enum(COMPARISONS), value: Byte, space: SpaceInput, view: MemoryViewInput }).strict(),
54
+ z.object({ kind: z.literal("raster"), line: rasterLine, cycle: z.number().int().min(0).max(64).optional().describe("cycle in the line: PAL 0-62, NTSC 0-64. Default 0") }).strict(),
55
+ ]),
56
+
57
+ timeoutFrames: z.number().int().min(1).max(MAX_TIMEOUT_FRAMES).default(DEFAULT_TIMEOUT_FRAMES),
58
+
59
+ memoryWrite: z
60
+ .object({
61
+ address: AddressInput,
62
+ data: HexDataInput.describe("bytes as hex, two digits per byte, no separators, for example a9008d20d0"),
63
+ space: SpaceInput,
64
+ view: MemoryViewInput,
65
+ })
66
+ .strict()
67
+ .refine((value) => value.data.length / 2 <= MAX_MEMORY_WRITE, { message: `data has at most ${MAX_MEMORY_WRITE} bytes`, path: ["data"] }),
68
+
69
+ flags,
70
+
71
+ registerValues: z
72
+ .object({
73
+ pc: AddressInput.optional(),
74
+ a: Byte.optional(),
75
+ x: Byte.optional(),
76
+ y: Byte.optional(),
77
+ sp: Byte.optional(),
78
+ flags: flags.partial().strict().optional().describe("any of n, v, b, d, i, z and c. Flags that you do not name keep their value"),
79
+ })
80
+ .strict()
81
+ .refine((value) => Object.entries(value).some(([name, item]) => item !== undefined && !(name === "flags" && Object.keys(item as object).length === 0)), { message: "name at least one register" }),
82
+
83
+ joystick: z
84
+ .object({
85
+ port: z.union([z.literal(1), z.literal(2)]).describe("control port 1 or 2"),
86
+ direction: z.enum(JOYSTICK_DIRECTIONS),
87
+ fire: z.boolean().default(false),
88
+ })
89
+ .strict(),
90
+
91
+ keyboard: z
92
+ .object({
93
+ mode: z.enum(["text", "petscii"]),
94
+ text: z.string().min(1).optional().describe("mode text: the text to type"),
95
+ bytes: z.array(z.number().int().min(1).max(255)).min(1).optional().describe("mode petscii: PETSCII codes to type"),
96
+ })
97
+ .strict()
98
+ .superRefine((value, context) => {
99
+ if (value.mode === "text" && (value.text === undefined || value.bytes !== undefined)) return context.addIssue({ code: "custom", message: "mode text takes text and no bytes" });
100
+ if (value.mode === "petscii" && (value.bytes === undefined || value.text !== undefined)) return context.addIssue({ code: "custom", message: "mode petscii takes bytes and no text" });
101
+ const keys = keyboardBytes(value);
102
+ if (typeof keys === "string") context.addIssue({ code: "custom", message: keys, path: ["text"] });
103
+ else if (keys.length > MAX_KEYBOARD_BYTES) context.addIssue({ code: "custom", message: `the keyboard queue takes at most ${MAX_KEYBOARD_BYTES} keys at once. This input has ${keys.length} keys` });
104
+ }),
105
+ };
package/src/mcp/output.ts CHANGED
@@ -3,11 +3,9 @@
3
3
  // host never sees the path. The project is the folder that `project` names,
4
4
  // or the one that the project rules find from the folder the agent started in.
5
5
 
6
- import { createHash } from "node:crypto";
7
-
8
6
  import { z } from "zod";
9
7
 
10
- import { projectFolder, writeProjectFile } from "../host-client/transfer.ts";
8
+ import { projectFolder, writeReported, type Written } from "../host-client/transfer.ts";
11
9
 
12
10
  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
11
 
@@ -17,13 +15,6 @@ export const ProjectInput = z
17
15
  .min(1)
18
16
  .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");
19
17
 
20
- /** What a result says in place of the bytes it wrote. */
21
- export interface Written {
22
- out: string;
23
- bytes: number;
24
- sha256: string;
25
- }
26
-
27
18
  /** The fields of a result with out, optional in an output schema that also has the inline form. */
28
19
  export const WrittenOutput = {
29
20
  out: z.string().optional(),
@@ -33,7 +24,5 @@ export const WrittenOutput = {
33
24
 
34
25
  /** 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
26
  export function writeOut(path: string, data: Uint8Array | string, project: string | undefined): Written {
36
- const bytes = typeof data === "string" ? Buffer.from(data, "utf8") : data;
37
- writeProjectFile(path, bytes, { what: "out", root: projectFolder(project) });
38
- return { out: path, bytes: bytes.length, sha256: createHash("sha256").update(bytes).digest("hex") };
27
+ return writeReported(path, data, { what: "out", root: projectFolder(project) });
39
28
  }
package/src/mcp/server.ts CHANGED
@@ -7,8 +7,10 @@ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
7
7
  import { CallToolRequestSchema, ListToolsRequestSchema, type CallToolResult, type Tool as McpTool } from "@modelcontextprotocol/sdk/types.js";
8
8
  import { z } from "zod";
9
9
 
10
- import { formatC64Address, parseC64Address } from "../c64.ts";
10
+ import { formatC64Address } from "../c64.ts";
11
+ import { Byte, TOOL_INPUTS } from "./inputs.ts";
11
12
  import { LostBeforeAnswer, type ViceSessionClient } from "../host-client/vice-session.ts";
13
+ import { isRefusal } from "../errors.ts";
12
14
  import { shownMessage, WireFailure, type WireError } from "../protocol/messages.ts";
13
15
  import { COMPARISONS, CONDITION_REGISTERS, MEMORY_VIEWS, SPACES, type Condition } from "../protocol/vice.ts";
14
16
  import { elapsedMs, trace, type TraceFields } from "../trace.ts";
@@ -62,35 +64,14 @@ export type SessionSource = () => Promise<ViceSessionApi>;
62
64
  type JsonObject = Record<string, unknown>;
63
65
 
64
66
  // ---------------------------------------------------------------------------
65
- // Public schema primitives shared by the tool groups
66
-
67
- /** Input address: "$" and four hex digits, either case. Parses to a number. */
68
- export const AddressInput = z
69
- .string()
70
- .regex(/^\$[0-9a-fA-F]{4}$/, "must be $ followed by four hex digits, for example $c000")
71
- .transform((value) => parseC64Address(value))
72
- .describe("C64 address, $0000 to $ffff");
67
+ // Public schema primitives shared by the tool groups. The inputs that change or
68
+ // wait for the machine are in inputs.ts: the action steps take the same inputs.
73
69
 
74
70
  /** Output address: "$" and four lowercase hex digits. */
75
71
  export const AddressOutput = z.string().regex(/^\$[0-9a-f]{4}$/).describe("C64 address, $0000 to $ffff");
76
72
 
77
- export const Byte = z.number().int().min(0).max(255);
78
-
79
73
  export const HexData = z.string().regex(/^(?:[0-9a-f]{2})*$/).describe("bytes as lowercase hex, two digits per byte, no separators");
80
74
 
81
- /** Input bytes: hex in either case, two digits per byte, at least one byte. Parses to lowercase. */
82
- export const HexDataInput = z
83
- .string()
84
- .regex(/^(?:[0-9a-fA-F]{2})+$/, "must be hex bytes, two digits per byte, no separators, for example a9008d20d0")
85
- .transform((value) => value.toLowerCase());
86
-
87
- export const SpaceInput = z.enum(SPACES).default("c64").describe("c64 is the computer. drive8 is the 1541 disk drive CPU");
88
-
89
- export const MemoryViewInput = z
90
- .enum(MEMORY_VIEWS)
91
- .default("cpu")
92
- .describe("cpu reads what the CPU sees now (ROM and I/O where banked in). ram reads the RAM underneath (c64 only)");
93
-
94
75
  /** A name the agent gives to something the session keeps, such as a screen baseline or a snapshot. */
95
76
  export const TransientName = z
96
77
  .string()
@@ -132,37 +113,6 @@ export interface ToolDefinition<I extends z.ZodObject = z.ZodObject, O extends z
132
113
  run(input: z.output<I>, session: ViceSessionApi): Promise<z.input<O> | ToolOutput<z.input<O>>>;
133
114
  }
134
115
 
135
- /** A typed condition: register, memory or raster. No expression language. */
136
- export const ConditionInput = z
137
- .discriminatedUnion("kind", [
138
- z
139
- .object({
140
- kind: z.literal("register"),
141
- register: z.enum(CONDITION_REGISTERS),
142
- operator: z.enum(COMPARISONS),
143
- value: Byte,
144
- })
145
- .strict(),
146
- z
147
- .object({
148
- kind: z.literal("memory"),
149
- address: AddressInput,
150
- operator: z.enum(COMPARISONS),
151
- value: Byte,
152
- space: SpaceInput,
153
- view: MemoryViewInput,
154
- })
155
- .strict(),
156
- z
157
- .object({
158
- kind: z.literal("raster"),
159
- line: z.number().int().min(0).describe("raster line: PAL 0-311, NTSC 0-262"),
160
- cycle: z.number().int().min(0).optional().describe("cycle in the line: PAL 0-62, NTSC 0-64. True from this cycle on"),
161
- })
162
- .strict(),
163
- ])
164
- .describe("register: a CPU register compared to a byte. memory: a byte in memory compared to a byte. raster: the raster position");
165
-
166
116
  /** A condition as a result shows it: the input form, with the memory address as "$xxxx". */
167
117
  export const ConditionOutput = z
168
118
  .discriminatedUnion("kind", [
@@ -183,7 +133,7 @@ export function defined<T extends Record<string, unknown>>(value: T): T {
183
133
  }
184
134
 
185
135
  /** A parsed condition input as the protocol type. */
186
- export function toCondition(input: z.output<typeof ConditionInput>): Condition {
136
+ export function toCondition(input: z.output<typeof TOOL_INPUTS.condition>): Condition {
187
137
  return defined(input) as Condition;
188
138
  }
189
139
 
@@ -208,8 +158,9 @@ export function toolListing(tool: ToolDefinition): McpTool {
208
158
  };
209
159
  }
210
160
 
161
+ /** An error in the one form of the toolkit: {"error":{"code","message"}}, as a script prints it. */
211
162
  function errorResult(error: WireError): CallToolResult {
212
- return { isError: true, content: [{ type: "text", text: JSON.stringify({ code: error.code, message: shownMessage(error) }) }] };
163
+ return { isError: true, content: [{ type: "text", text: JSON.stringify({ error: { code: error.code, message: shownMessage(error) } }) }] };
213
164
  }
214
165
 
215
166
  /** "address: Invalid string: must match ..." style messages, one sentence per problem. */
@@ -244,7 +195,7 @@ function outcome(result: CallToolResult): TraceFields {
244
195
  const first = result.content[0];
245
196
  const text = first?.type === "text" ? first.text : "";
246
197
  try {
247
- return { ok: false, ...(JSON.parse(text) as WireError) };
198
+ return { ok: false, ...(JSON.parse(text) as { error: WireError }).error };
248
199
  } catch {
249
200
  return { ok: false, message: text };
250
201
  }
@@ -282,7 +233,7 @@ async function runCall(
282
233
  const extra = output instanceof ToolOutput ? output.content : [];
283
234
  return { content: [{ type: "text", text: JSON.stringify(result) }, ...extra], structuredContent: result };
284
235
  } catch (error) {
285
- if (error instanceof WireFailure) return errorResult(error.toWire());
236
+ if (isRefusal(error)) return errorResult({ code: error.code, message: error.message });
286
237
  log(`${name} failed: ${error instanceof Error ? (error.stack ?? error.message) : String(error)}`);
287
238
  return errorResult(INTERNAL_FAILURE);
288
239
  }
@@ -299,7 +250,8 @@ function packageVersion(): string {
299
250
 
300
251
  const INSTRUCTIONS =
301
252
  "These tools control one live Commodore 64 emulator that belongs to this session. " +
302
- "Addresses are four hex digits after a dollar sign, for example $c000. " +
253
+ "An address is a dollar sign and 1 to 4 hex digits, for example $c000 or $fb. A result gives four hex digits. " +
254
+ "An error is {\"error\": {\"code\", \"message\"}}. " +
303
255
  "Memory data is lowercase hex without separators. " +
304
256
  "Other register values and chip values are decimal numbers. " +
305
257
  "A read does not change the run state of the machine.";
@@ -8,7 +8,8 @@ import { WireFailure } from "../../protocol/messages.ts";
8
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
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";
11
+ import { AddressInput, Byte, SpaceInput, TOOL_INPUTS } from "../inputs.ts";
12
+ import { AddressOutput, ConditionOutput, defineTool, HexData, requireFields, showCondition, toCondition } from "../server.ts";
12
13
  import { spaced } from "./memory.ts";
13
14
 
14
15
  /** No default here: a filled-in default would count as a field the other actions refuse. */
@@ -23,7 +24,7 @@ const BreakpointOutput = z.object({
23
24
  enabled: z.boolean(),
24
25
  condition: ConditionOutput.optional(),
25
26
  });
26
- const WatchpointOutput = BreakpointOutput.extend({ size: z.number().int().min(1).max(MAX_WATCH_SIZE), access: z.enum(WATCH_ACCESS) });
27
+ const WatchpointOutput = BreakpointOutput.omit({ address: true }).extend({ start: AddressOutput, end: AddressOutput, access: z.enum(WATCH_ACCESS) });
27
28
 
28
29
  function showBreakpoint(point: Breakpoint) {
29
30
  const { condition, ...rest } = point;
@@ -31,8 +32,8 @@ function showBreakpoint(point: Breakpoint) {
31
32
  }
32
33
 
33
34
  function showWatchpoint(point: Watchpoint) {
34
- const { condition, ...rest } = point;
35
- return { ...rest, address: formatC64Address(point.address), ...(condition === undefined ? {} : { condition: showCondition(condition) }) };
35
+ const { condition, address, size, ...rest } = point;
36
+ return { ...rest, start: formatC64Address(address), end: formatC64Address(address + size - 1), ...(condition === undefined ? {} : { condition: showCondition(condition) }) };
36
37
  }
37
38
 
38
39
  export const c64Breakpoint = defineTool({
@@ -49,7 +50,7 @@ export const c64Breakpoint = defineTool({
49
50
  action: z.enum(CHECKPOINT_ACTIONS),
50
51
  address: AddressInput.optional().describe("add only"),
51
52
  space: OptionalSpace,
52
- condition: ConditionInput.optional().describe("add only: stop only when this is true"),
53
+ condition: TOOL_INPUTS.condition.optional().describe("add only: stop only when this is true"),
53
54
  id: Id.optional().describe("remove, enable and disable only"),
54
55
  })
55
56
  .strict(),
@@ -83,18 +84,18 @@ export const c64Watchpoint = defineTool({
83
84
  title: "C64 watchpoints",
84
85
  description:
85
86
  "Manage watchpoints. A watchpoint stops the CPU when an instruction reads or writes memory in a range. " +
86
- `Action add sets one at address for size bytes (1 to ${MAX_WATCH_SIZE}, default 1), with access read, write or read-write. ` +
87
+ `Action add sets one from start to end (at most ${MAX_WATCH_SIZE} bytes, end defaults to start), with access read, write or read-write. ` +
87
88
  "With a condition, it stops only when the condition is true. For a write, a memory condition sees the memory after the write. " +
88
89
  "add returns the new id. Breakpoints and watchpoints take their ids from one counter, so the ids of one kind can have gaps. " +
89
90
  "Actions remove, enable and disable take that id. remove returns removed true and the id. Action list gives all watchpoints. Each watchpoint shows its condition, when it has one.",
90
91
  inputSchema: z
91
92
  .object({
92
93
  action: z.enum(CHECKPOINT_ACTIONS),
93
- address: AddressInput.optional().describe("add only"),
94
- size: z.number().int().min(1).max(MAX_WATCH_SIZE).optional().describe("add only. Default 1"),
94
+ start: AddressInput.optional().describe("add only: the first address"),
95
+ end: AddressInput.optional().describe("add only: the last address. Default start"),
95
96
  access: z.enum(WATCH_ACCESS).optional().describe("add only"),
96
97
  space: OptionalSpace,
97
- condition: ConditionInput.optional().describe("add only: stop only when this is true"),
98
+ condition: TOOL_INPUTS.condition.optional().describe("add only: stop only when this is true"),
98
99
  id: Id.optional().describe("remove, enable and disable only"),
99
100
  })
100
101
  .strict(),
@@ -112,11 +113,15 @@ export const c64Watchpoint = defineTool({
112
113
  if (action === "remove") return session.watchpoint({ action, id: fields.id! });
113
114
  return showWatchpoint(await session.watchpoint({ action, id: fields.id! }));
114
115
  }
115
- requireFields(action, fields, ["address", "size", "access", "space", "condition"], ["address", "access"]);
116
+ requireFields(action, fields, ["start", "end", "access", "space", "condition"], ["start", "access"]);
117
+ const start = fields.start!;
118
+ const end = fields.end ?? start;
119
+ if (end < start) throw new WireFailure("invalid-input", "end must not be before start.");
120
+ if (end - start + 1 > MAX_WATCH_SIZE) throw new WireFailure("invalid-input", `A watchpoint covers at most ${MAX_WATCH_SIZE} bytes. This one covers ${end - start + 1}.`);
116
121
  const added = await session.watchpoint({
117
122
  action,
118
- address: fields.address!,
119
- size: fields.size ?? 1,
123
+ address: start,
124
+ size: end - start + 1,
120
125
  access: fields.access!,
121
126
  space: fields.space ?? "c64",
122
127
  ...(fields.condition === undefined ? {} : { condition: toCondition(fields.condition) }),
@@ -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_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";
6
+ 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
+ import { Byte, SpaceInput, TOOL_INPUTS } from "../inputs.ts";
8
+ import { AddressOutput, defineTool, toCondition, ToolOutput } from "../server.ts";
8
9
  import { c64Observe } from "./video.ts";
9
10
 
10
11
  export const c64Execution = defineTool({
@@ -57,34 +58,6 @@ export const c64Execution = defineTool({
57
58
  },
58
59
  });
59
60
 
60
- const RunTargetInput = z.discriminatedUnion("kind", [
61
- z
62
- .object({
63
- kind: z.literal("address"),
64
- address: AddressInput,
65
- space: SpaceInput,
66
- condition: ConditionInput.optional().describe("stop at the address only when this is true"),
67
- })
68
- .strict(),
69
- z
70
- .object({
71
- kind: z.literal("memory"),
72
- address: AddressInput,
73
- operator: z.enum(COMPARISONS),
74
- value: Byte,
75
- space: SpaceInput,
76
- view: MemoryViewInput,
77
- })
78
- .strict(),
79
- z
80
- .object({
81
- kind: z.literal("raster"),
82
- line: z.number().int().min(0).describe("raster line: PAL 0-311, NTSC 0-262"),
83
- cycle: z.number().int().min(0).optional().describe("cycle in the line: PAL 0-62, NTSC 0-64. Default 0"),
84
- })
85
- .strict(),
86
- ]);
87
-
88
61
  export const c64RunUntil = defineTool({
89
62
  name: "c64_run_until",
90
63
  title: "Run the C64 until a target",
@@ -103,8 +76,8 @@ export const c64RunUntil = defineTool({
103
76
  "The CPU is always stopped after this tool.",
104
77
  inputSchema: z
105
78
  .object({
106
- target: RunTargetInput,
107
- timeoutFrames: z.number().int().min(1).max(MAX_TIMEOUT_FRAMES).default(3000),
79
+ target: TOOL_INPUTS.runTarget,
80
+ timeoutFrames: TOOL_INPUTS.timeoutFrames,
108
81
  observe: c64Observe.inputSchema.optional().describe("what to read at the stop, as for c64_observe"),
109
82
  history: z.number().int().min(1).max(MAX_HISTORY).optional().describe("the number of instructions before the stop to give"),
110
83
  })
@@ -2,9 +2,9 @@
2
2
 
3
3
  import { z } from "zod";
4
4
 
5
- import { textToPetscii } from "../../c64.ts";
6
- import { WireFailure } from "../../protocol/messages.ts";
7
5
  import { JOYSTICK_DIRECTIONS, MAX_KEYBOARD_BYTES } from "../../protocol/vice.ts";
6
+ import { keyboardBytes } from "../../host-client/steps.ts";
7
+ import { TOOL_INPUTS } from "../inputs.ts";
8
8
  import { defineTool } from "../server.ts";
9
9
 
10
10
  const EMULATOR_FLAW = z.string().optional().describe("a known flaw of the emulator version that affects input, and the version that fixes it");
@@ -15,35 +15,16 @@ export const c64Keyboard = defineTool({
15
15
  description:
16
16
  `Type up to ${MAX_KEYBOARD_BYTES} keys into the C64 keyboard queue. The C64 reads them while it runs. ` +
17
17
  "Only a program that reads keys through the KERNAL sees them. A program that reads the keyboard hardware directly does not see them. Many games do that. " +
18
- "No command presses one key of the keyboard matrix: for one specific key, ask the user to press it in the window. " +
18
+ "No tool presses one key of the keyboard matrix. For one specific key, ask the user to press it in the window, or give a press step in a record file of c64-testing. " +
19
19
  'Mode text types text, for example "RUN\\n". Letters of either case type the normal (unshifted) key. A line break types RETURN. ' +
20
20
  "Mode petscii types PETSCII codes 1 to 255 in bytes, for example [147] to clear the screen. " +
21
21
  "This does not change the run state of the machine. emulatorFlaw in the result names a known flaw of the emulator version that affects input, and the version that fixes it.",
22
- inputSchema: z
23
- .object({
24
- mode: z.enum(["text", "petscii"]),
25
- text: z.string().min(1).optional().describe("mode text: the text to type"),
26
- bytes: z.array(z.number().int().min(1).max(255)).min(1).optional().describe("mode petscii: PETSCII codes to type"),
27
- })
28
- .strict(),
22
+ inputSchema: TOOL_INPUTS.keyboard,
29
23
  outputSchema: z.object({ queuedBytes: z.number().int().min(1).max(MAX_KEYBOARD_BYTES), emulatorFlaw: EMULATOR_FLAW }),
30
24
  readOnly: false,
31
25
  async run(input, session) {
32
- let petscii: Uint8Array;
33
- if (input.mode === "text") {
34
- if (input.text === undefined || input.bytes !== undefined) throw new WireFailure("invalid-input", "Mode text takes text and no bytes.");
35
- try {
36
- petscii = textToPetscii(input.text);
37
- } catch (error) {
38
- throw new WireFailure("invalid-input", `${(error as Error).message}. Use mode petscii for other keys.`);
39
- }
40
- } else {
41
- if (input.bytes === undefined || input.text !== undefined) throw new WireFailure("invalid-input", "Mode petscii takes bytes and no text.");
42
- petscii = Uint8Array.from(input.bytes);
43
- }
44
- if (petscii.length > MAX_KEYBOARD_BYTES) {
45
- throw new WireFailure("invalid-input", `The keyboard queue takes at most ${MAX_KEYBOARD_BYTES} keys at once. This input has ${petscii.length} keys.`);
46
- }
26
+ // The schema checked the keys: keyboardBytes gives bytes here.
27
+ const petscii = keyboardBytes(input) as Uint8Array;
47
28
  return session.keyboard(petscii);
48
29
  },
49
30
  });
@@ -55,13 +36,7 @@ export const c64Joystick = defineTool({
55
36
  "Set the joystick in control port 1 or 2: a direction (or center) and the fire button. " +
56
37
  "The C64 sees this state until you change it. To release the joystick, set direction center and fire false. " +
57
38
  "Most games read port 2. This does not change the run state of the machine. emulatorFlaw in the result names a known flaw of the emulator version that affects input, and the version that fixes it.",
58
- inputSchema: z
59
- .object({
60
- port: z.union([z.literal(1), z.literal(2)]).describe("control port 1 or 2"),
61
- direction: z.enum(JOYSTICK_DIRECTIONS),
62
- fire: z.boolean().default(false),
63
- })
64
- .strict(),
39
+ inputSchema: TOOL_INPUTS.joystick,
65
40
  outputSchema: z.object({ port: z.union([z.literal(1), z.literal(2)]), direction: z.enum(JOYSTICK_DIRECTIONS), fire: z.boolean(), emulatorFlaw: EMULATOR_FLAW }),
66
41
  readOnly: false,
67
42
  async run(input, session) {
@@ -8,7 +8,8 @@ import { formatC64Address } from "../../c64.ts";
8
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
10
  import { OutInput, ProjectInput, writeOut, WrittenOutput } from "../output.ts";
11
- import { AddressInput, AddressOutput, defineTool, requireFields, TransientName } from "../server.ts";
11
+ import { AddressInput } from "../inputs.ts";
12
+ import { AddressOutput, defineTool, requireFields, TransientName } from "../server.ts";
12
13
 
13
14
  const ProjectPath = z.string().min(1).describe("path relative to the project folder, for example build/game.prg");
14
15
 
@@ -91,11 +92,11 @@ export const c64Snapshot = defineTool({
91
92
  "With out, save also writes it to that project file as a snapshot file (.vsf), so a later session can use it. " +
92
93
  "Action restore puts the machine back in that state and stops the CPU. Give name for a snapshot of this session, or file for a .vsf in the project. " +
93
94
  "A file restores only in the emulator version and with the machine settings that saved it. The c64_timing stopwatch keeps its reading. " +
94
- `Action list gives the names, and action discard removes one. A session keeps at most ${MAX_SNAPSHOTS} snapshots until it ends.`,
95
+ `Action list gives the names, and action remove removes one. A session keeps at most ${MAX_SNAPSHOTS} snapshots until it ends.`,
95
96
  inputSchema: z
96
97
  .object({
97
- action: z.enum(["save", "restore", "list", "discard"]),
98
- name: TransientName.optional().describe("save and discard, and restore without file: 1 to 64 letters, digits, dots, underscores or hyphens"),
98
+ action: z.enum(["save", "restore", "list", "remove"]),
99
+ name: TransientName.optional().describe("save and remove, and restore without file: 1 to 64 letters, digits, dots, underscores or hyphens"),
99
100
  out: OutInput.optional().describe("save only: also write the snapshot to this project file, for example snapshots/room0.vsf. An existing file is overwritten"),
100
101
  file: ProjectPath.optional().describe("restore only: a .vsf file in the project, in place of name"),
101
102
  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"),
@@ -107,7 +108,7 @@ export const c64Snapshot = defineTool({
107
108
  restored: z.boolean().optional(),
108
109
  state: z.enum(RUN_STATES).optional(),
109
110
  snapshots: z.array(z.string()).optional(),
110
- discarded: z.boolean().optional(),
111
+ removed: z.boolean().optional(),
111
112
  ...WrittenOutput,
112
113
  }),
113
114
  readOnly: false,
@@ -7,7 +7,8 @@ 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
9
  import { OutInput, ProjectInput, writeOut, WrittenOutput } from "../output.ts";
10
- import { AddressInput, AddressOutput, Byte, defineTool, HexData, HexDataInput, MemoryViewInput, requireFields, SpaceInput } from "../server.ts";
10
+ import { AddressInput, Byte, MemoryViewInput, SpaceInput, TOOL_INPUTS } from "../inputs.ts";
11
+ import { AddressOutput, defineTool, HexData, requireFields } from "../server.ts";
11
12
 
12
13
  /** With out, a read or a disassembly can be larger than one answer of the session: it is read in parts. */
13
14
  const MAX_MEMORY_READ_OUT = 0x10000;
@@ -61,14 +62,7 @@ export const c64MemoryWrite = defineTool({
61
62
  "The range must not go past $ffff. With view cpu, the write goes where the CPU writes now. " +
62
63
  "A write to a ROM address goes to the RAM under it, and a write to I/O goes to the chip. " +
63
64
  "With view ram, the write goes to RAM (c64 only).",
64
- inputSchema: z
65
- .object({
66
- address: AddressInput,
67
- data: HexDataInput.describe("bytes as hex, two digits per byte, no separators, for example a9008d20d0"),
68
- space: SpaceInput,
69
- view: MemoryViewInput,
70
- })
71
- .strict(),
65
+ inputSchema: TOOL_INPUTS.memoryWrite,
72
66
  outputSchema: z.object({ address: AddressOutput, bytesWritten: z.number().int().min(1).max(MAX_MEMORY_WRITE) }),
73
67
  readOnly: false,
74
68
  async run(input, session) {
@@ -77,47 +71,26 @@ export const c64MemoryWrite = defineTool({
77
71
  },
78
72
  });
79
73
 
80
- const Flags = z.object({
81
- n: z.boolean(),
82
- v: z.boolean(),
83
- b: z.boolean(),
84
- d: z.boolean(),
85
- i: z.boolean(),
86
- z: z.boolean(),
87
- c: z.boolean(),
88
- });
89
-
90
- const RegisterValuesInput = z
91
- .object({
92
- pc: AddressInput.optional(),
93
- a: Byte.optional(),
94
- x: Byte.optional(),
95
- y: Byte.optional(),
96
- sp: Byte.optional(),
97
- flags: Flags.partial().strict().optional().describe("any of n, v, b, d, i, z and c. Flags that you do not name keep their value"),
98
- })
99
- .strict();
100
-
101
74
  export const c64Registers = defineTool({
102
75
  name: "c64_registers",
103
76
  title: "C64 CPU registers",
104
77
  description:
105
- "Get or set the CPU registers: pc, a, x, y, sp and the status flags n, v, b, d, i, z and c. " +
106
- "Action get reads them. This does not change the run state of the machine. " +
107
- "Action set writes the registers that values names and returns all registers. Stop the CPU before you use set. " +
78
+ "Read or write the CPU registers: pc, a, x, y, sp and the status flags n, v, b, d, i, z and c. " +
79
+ "Action read reads them. This does not change the run state of the machine. " +
80
+ "Action write writes the registers that values names and returns all registers. Stop the CPU before you use write. " +
108
81
  "Use space drive8 for the CPU of the 1541 disk drive.",
109
82
  inputSchema: z
110
83
  .object({
111
- action: z.enum(["get", "set"]),
84
+ action: z.enum(["read", "write"]),
112
85
  space: SpaceInput,
113
- values: RegisterValuesInput.optional().describe("set only: the registers to write"),
86
+ values: TOOL_INPUTS.registerValues.optional().describe("write only: the registers to write"),
114
87
  })
115
88
  .strict(),
116
- outputSchema: z.object({ pc: AddressOutput, a: Byte, x: Byte, y: Byte, sp: Byte, flags: Flags }),
89
+ outputSchema: z.object({ pc: AddressOutput, a: Byte, x: Byte, y: Byte, sp: Byte, flags: TOOL_INPUTS.flags }),
117
90
  readOnly: false,
118
91
  async run(input, session) {
119
92
  let registers;
120
- if (input.action === "get") {
93
+ if (input.action === "read") {
121
94
  requireFields(input.action, { values: input.values }, []);
122
95
  registers = await session.registersGet(input.space);
123
96
  } else {
@@ -130,7 +103,7 @@ export const c64Registers = defineTool({
130
103
  const flags = Object.fromEntries(Object.entries(input.values?.flags ?? {}).filter(([, value]) => value !== undefined));
131
104
  if (Object.keys(flags).length > 0) values.flags = flags;
132
105
  if (Object.keys(values).length === 0) {
133
- throw new WireFailure("invalid-input", "values must name at least one register for action set.");
106
+ throw new WireFailure("invalid-input", "values must name at least one register for action write.");
134
107
  }
135
108
  registers = await session.registersSet(input.space, values);
136
109
  }
@@ -7,7 +7,8 @@ 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";
10
- import { AddressInput, AddressOutput, Byte, defineTool, HexData, MemoryViewInput, requireFields, SpaceInput, ToolOutput, TransientName } from "../server.ts";
10
+ import { AddressInput, Byte, MemoryViewInput, SpaceInput } from "../inputs.ts";
11
+ import { AddressOutput, defineTool, HexData, requireFields, ToolOutput, TransientName } from "../server.ts";
11
12
 
12
13
  const Color = z.number().int().min(0).max(15);
13
14
  const NO_CHANGE = "This does not change the run state of the machine.";
@@ -138,13 +139,13 @@ export const c64Screen = defineTool({
138
139
  "A mask that covers the whole frame is invalid input. comparedPixels is the number of pixels outside the mask. " +
139
140
  "match is true when the part of compared pixels that differ is at most maxMismatchRatio (default 0). bounds holds every different pixel. " +
140
141
  "With includeDiff, compare also returns an image with the different pixels in magenta. " +
141
- "Action list gives the baseline names. Action discard removes a baseline. Baselines stay until the session ends. " +
142
+ "Action list gives the baseline names. Action remove removes a baseline. Baselines stay until the session ends. " +
142
143
  "The emulator draws frames while the CPU runs. After the CPU stops, the frame stays the same. " +
143
144
  "This does not change the run state of the machine.",
144
145
  inputSchema: z
145
146
  .object({
146
- action: z.enum(["capture", "compare", "list", "discard"]),
147
- baseline: TransientName.optional().describe("capture: keep the frame under this name. compare and discard: the baseline to use"),
147
+ action: z.enum(["capture", "compare", "list", "remove"]),
148
+ baseline: TransientName.optional().describe("capture: keep the frame under this name. compare and remove: the baseline to use"),
148
149
  maxMismatchRatio: z.number().min(0).max(1).optional().describe("compare only: 0 to 1, default 0"),
149
150
  mask: z.array(Rectangle).max(MAX_MASK_RECTANGLES).optional().describe("compare only: rectangles to ignore, in frame pixels"),
150
151
  includeDiff: z.boolean().optional().describe("compare only: also return a difference image"),
@@ -162,7 +163,7 @@ export const c64Screen = defineTool({
162
163
  mismatchRatio: z.number().min(0).max(1).optional(),
163
164
  bounds: Rectangle.optional(),
164
165
  baselines: z.array(z.string()).optional(),
165
- discarded: z.boolean().optional(),
166
+ removed: z.boolean().optional(),
166
167
  ...WrittenOutput,
167
168
  }),
168
169
  readOnly: false,