c64bridge 0.9.3 → 1.0.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 (65) hide show
  1. package/AGENTS.md +19 -3
  2. package/README.md +190 -148
  3. package/dist/.tsbuildinfo +1 -1
  4. package/dist/c64Client.js +277 -41
  5. package/dist/config.js +30 -18
  6. package/dist/device.js +191 -125
  7. package/dist/knowledge.js +1 -1
  8. package/dist/loggingHttpClient.js +15 -3
  9. package/dist/mcp-server.js +65 -16
  10. package/dist/platform.js +13 -2
  11. package/dist/rag/indexer.js +11 -8
  12. package/dist/rag/init.js +11 -8
  13. package/dist/sidwaveCompiler.js +2 -2
  14. package/dist/tools/audio.js +27 -6
  15. package/dist/tools/developer.js +6 -6
  16. package/dist/tools/graphics.js +4 -4
  17. package/dist/tools/input.js +136 -24
  18. package/dist/tools/machineControl.js +85 -6
  19. package/dist/tools/memory.js +21 -6
  20. package/dist/tools/meta/artifacts.js +20 -8
  21. package/dist/tools/meta/background.js +11 -1
  22. package/dist/tools/meta/index.js +1 -1
  23. package/dist/tools/pollValidator.js +19 -51
  24. package/dist/tools/printer.js +1 -0
  25. package/dist/tools/programRunners.js +5 -5
  26. package/dist/tools/rag.js +1 -1
  27. package/dist/tools/registry/batch.js +1 -1
  28. package/dist/tools/registry/config.js +5 -5
  29. package/dist/tools/registry/disk.js +2 -2
  30. package/dist/tools/registry/drive.js +2 -2
  31. package/dist/tools/registry/graphics.js +2 -2
  32. package/dist/tools/registry/memory.js +2 -2
  33. package/dist/tools/registry/platform.js +9 -5
  34. package/dist/tools/registry/printer.js +1 -0
  35. package/dist/tools/registry/program.js +5 -5
  36. package/dist/tools/registry/rag.js +1 -1
  37. package/dist/tools/registry/sound.js +4 -4
  38. package/dist/tools/registry/system.js +13 -3
  39. package/dist/tools/storage.js +1 -1
  40. package/dist/vice/mockServer.js +33 -0
  41. package/dist/vice/process.js +11 -1
  42. package/dist/vice/viceClient.js +35 -6
  43. package/doc/c64u/c64-openapi.yaml +1130 -774
  44. package/doc/reviews/hardening/01-fable/prompt-handover.md +125 -0
  45. package/doc/reviews/hardening/01-fable/prompt.md +98 -0
  46. package/doc/reviews/hardening/01-fable/review-prompt.md +468 -0
  47. package/doc/reviews/hardening/01-fable/review.md +1234 -0
  48. package/doc/u2/u2-openapi.yaml +1192 -0
  49. package/generated/c64/index.js +142 -9
  50. package/generated/openapi/c64u/index.js +1117 -0
  51. package/generated/openapi/u2/index.js +929 -0
  52. package/mcp/schemas/c64_graphics.schema.json +3 -3
  53. package/mcp/schemas/c64_input.schema.json +249 -8
  54. package/mcp/schemas/c64_memory.schema.json +3 -3
  55. package/mcp/schemas/c64_program.schema.json +3 -3
  56. package/mcp/schemas/c64_select_backend.schema.json +2 -0
  57. package/mcp/schemas/c64_system.schema.json +41 -1
  58. package/mcp/server.json +1 -1
  59. package/mcp/tools.json +386 -44
  60. package/mcp.json +14 -2
  61. package/package.json +4 -2
  62. package/scripts/mockC64Server.mjs +171 -2
  63. package/scripts/prepare-release.mjs +13 -2
  64. package/scripts/update-readme.ts +18 -30
  65. package/server.json +3 -3
@@ -75,13 +75,7 @@ function concatBuffers(...buffers) {
75
75
  * Looks for patterns like "?SYNTAX ERROR" or "SYNTAX ERROR IN 120".
76
76
  */
77
77
  function extractBasicError(screen) {
78
- const upperScreen = screen.toUpperCase();
79
- // Check if ERROR appears on screen
80
- if (!upperScreen.includes("ERROR")) {
81
- return null;
82
- }
83
- // Try to match "?ERROR_TYPE ERROR" or "ERROR_TYPE ERROR IN LINE"
84
- const errorMatch = /\?([A-Z\s]+)\s+ERROR(?:\s+IN\s+(\d+))?/i.exec(screen);
78
+ const errorMatch = /(?:^|\n)\s*\?([A-Z\s]+?)\s+ERROR(?:\s+IN\s+(\d+))?(?=\s|$)/im.exec(screen); // A real error has a leading "?"; ordinary output like "0 ERRORS FOUND" must never match.
85
79
  if (errorMatch) {
86
80
  const errorType = errorMatch[1]?.trim().replace(/\s+/g, " ");
87
81
  const lineStr = errorMatch[2];
@@ -91,8 +85,7 @@ function extractBasicError(screen) {
91
85
  line: Number.isFinite(line) ? line : undefined,
92
86
  };
93
87
  }
94
- // Fallback: just "ERROR" appears
95
- return { message: "UNKNOWN ERROR" };
88
+ return null;
96
89
  }
97
90
  /**
98
91
  * Poll for BASIC program outcome by detecting errors on screen.
@@ -124,17 +117,20 @@ async function pollBasicOutcome(client, logger, config) {
124
117
  line: errorInfo.line,
125
118
  };
126
119
  }
127
- // If we detected RUN and no error yet, continue polling
128
- if (runDetected) {
129
- // Continue polling for a bit to catch any delayed errors
130
- // but if we've been polling for a while without errors, consider it successful
131
- }
132
120
  }
133
121
  catch (error) {
134
122
  logger.debug("Failed to read screen during BASIC polling", { error, pollCount });
135
123
  }
136
124
  await delay(config.intervalMs);
137
125
  }
126
+ if (!runDetected) { // Neither "RUN" nor an error ever appeared for the whole window. On working backends the firmware's own LOAD/RUN echo shows this text, so its total absence means execution never started despite the reported success (HARD01-011/036).
127
+ logger.debug("BASIC polling completed without ever observing RUN or an error", { pollCount, elapsed: Date.now() - startTime });
128
+ return {
129
+ status: "error",
130
+ type: "BASIC",
131
+ message: "No execution activity detected: the firmware reported success, but the program does not appear to have run.",
132
+ };
133
+ }
138
134
  logger.debug("BASIC polling completed without errors", { pollCount, elapsed: Date.now() - startTime });
139
135
  return {
140
136
  status: "ok",
@@ -178,38 +174,20 @@ async function pollAsmOutcome(client, logger, config) {
178
174
  }
179
175
  await delay(config.intervalMs);
180
176
  }
181
- // If RUN never appeared, assume instant execution (success)
182
- if (!runDetected) {
183
- logger.debug("RUN never detected, assuming instant execution");
184
- return {
185
- status: "ok",
186
- type: "ASM",
187
- };
177
+ if (!runDetected) { // Fall through to the liveness check rather than assuming success. Skipping it here used to report false success for any run that never types anything visible, e.g. a firmware run_prg that silently no-ops instead of running the PRG (HARD01-036).
178
+ logger.debug("RUN never detected; falling through to hardware liveness check instead of assuming success");
188
179
  }
189
- // Now poll hardware regions to detect activity
190
- let prevIoSignature = null;
191
- let prevJiffyClock = null;
180
+ let previousSignature = null; // Only stable RAM: reading I/O can ack CIA interrupts on Ultimate, and raster/timer values aren't evidence of liveness.
192
181
  let alive = false;
193
182
  const pollDeadline = Date.now() + config.maxMs;
194
183
  while (Date.now() < pollDeadline && !alive) {
195
184
  pollCount++;
196
185
  try {
197
- // Read I/O regions in optimized batches as per comment #3466803675
198
- // All of $D000-$DFFF in a single call (covers VIC-II, SID, CIA1, CIA2)
199
- const ioRegions = await client.readMemoryRaw(0xD000, 0x1000); // $D000-$DFFF
200
- // Read low memory through screen RAM in a single call: $0000-$07E7
201
- // This covers zero page, stack, jiffy clock ($00A0-$00A2), and screen memory
202
- const lowMemAndScreen = await client.readMemoryRaw(0x0000, 0x7E8); // $0000-$07E7
203
- // Extract jiffy clock for separate comparison (at offset $00A0 in the buffer)
204
- const jiffyClock = lowMemAndScreen.slice(0xA0, 0xA3);
205
- // Exclude the jiffy clock from the broad signature so it can be tracked separately.
206
- const lowMemSignatureBuffer = lowMemAndScreen.slice();
207
- lowMemSignatureBuffer.fill(0, 0xA0, 0xA3);
208
- const combinedBuffer = concatBuffers(ioRegions, lowMemSignatureBuffer);
209
- const ioSignature = computeCrc32(combinedBuffer);
186
+ const screenRam = await client.readMemoryRaw(0x0400, 0x03e8);
187
+ const signature = computeCrc32(screenRam);
210
188
  // Check for any activity
211
- if (prevIoSignature !== null && ioSignature !== prevIoSignature) {
212
- logger.debug("Hardware or screen activity detected", {
189
+ if (previousSignature !== null && signature !== previousSignature) {
190
+ logger.debug("Program-visible screen activity detected", {
213
191
  pollCount,
214
192
  elapsed: Date.now() - startTime,
215
193
  signatureChanged: true
@@ -217,17 +195,7 @@ async function pollAsmOutcome(client, logger, config) {
217
195
  alive = true;
218
196
  break;
219
197
  }
220
- if (prevJiffyClock !== null && !buffersEqual(jiffyClock, prevJiffyClock)) {
221
- logger.debug("Jiffy clock advanced", {
222
- pollCount,
223
- elapsed: Date.now() - startTime,
224
- clockChanged: true
225
- });
226
- alive = true;
227
- break;
228
- }
229
- prevIoSignature = ioSignature;
230
- prevJiffyClock = jiffyClock;
198
+ previousSignature = signature;
231
199
  }
232
200
  catch (error) {
233
201
  logger.debug("Failed to read memory during ASM polling", { error, pollCount });
@@ -246,7 +214,7 @@ async function pollAsmOutcome(client, logger, config) {
246
214
  return {
247
215
  status: "crashed",
248
216
  type: "ASM",
249
- reason: "no VIC/CIA/TI/screen progression within window",
217
+ reason: "no program-visible screen progression within window",
250
218
  };
251
219
  }
252
220
  /**
@@ -225,6 +225,7 @@ function validateEpsonDensity(args) {
225
225
  }
226
226
  export const printerModule = defineToolModule({
227
227
  domain: "printer",
228
+ supportedPlatforms: ["c64u", "u2"],
228
229
  summary: "Printer workflow helpers for Commodore MPS and Epson FX devices, including prompt templates.",
229
230
  resources: [
230
231
  "c64://printer/spec",
@@ -668,7 +668,7 @@ export const programRunnersModule = defineToolModule({
668
668
  "Invoke right after you generate BASIC source so it runs on the C64 without extra user steps.",
669
669
  "Ensure the program includes line numbers and uppercase keywords before calling the tool.",
670
670
  ],
671
- supportedPlatforms: ["c64u", "vice"],
671
+ supportedPlatforms: ["c64u", "u2", "vice"],
672
672
  async execute(args, ctx) {
673
673
  return executeUploadRunBasic(args, ctx);
674
674
  },
@@ -693,7 +693,7 @@ export const programRunnersModule = defineToolModule({
693
693
  "Use when the user requests to run new 6502 code; surface any assembler diagnostics in your reply.",
694
694
  "Mention the entry routine or important addresses after execution so the user can continue debugging.",
695
695
  ],
696
- supportedPlatforms: ["c64u", "vice"],
696
+ supportedPlatforms: ["c64u", "u2", "vice"],
697
697
  async execute(args, ctx) {
698
698
  return executeUploadRunAsm(args, ctx);
699
699
  },
@@ -717,7 +717,7 @@ export const programRunnersModule = defineToolModule({
717
717
  "Stage PRG files without running when the user wants to inspect memory first.",
718
718
  "Confirm the Ultimate filesystem path (e.g. //USB0/demo.prg) is accessible before invoking.",
719
719
  ],
720
- supportedPlatforms: ["c64u"],
720
+ supportedPlatforms: ["c64u", "u2"],
721
721
  async execute(args, ctx) {
722
722
  return executeLoadPrg(args, ctx);
723
723
  },
@@ -733,7 +733,7 @@ export const programRunnersModule = defineToolModule({
733
733
  "Call when the user provides a PRG path and expects immediate execution without compiling.",
734
734
  "Mention that firmware issues a RUN so the user knows the machine state changed.",
735
735
  ],
736
- supportedPlatforms: ["c64u", "vice"],
736
+ supportedPlatforms: ["c64u", "u2", "vice"],
737
737
  prerequisites: ["drives_list"],
738
738
  examples: [
739
739
  {
@@ -765,7 +765,7 @@ export const programRunnersModule = defineToolModule({
765
765
  arguments: { path: "//USB0/game.crt" },
766
766
  },
767
767
  ],
768
- supportedPlatforms: ["c64u"],
768
+ supportedPlatforms: ["c64u", "u2"],
769
769
  async execute(args, ctx) {
770
770
  try {
771
771
  const parsed = crtFileArgsSchema.parse(args ?? {});
package/dist/tools/rag.js CHANGED
@@ -181,7 +181,7 @@ export const ragModule = defineToolModule({
181
181
  ],
182
182
  prompts: ["basic-program", "assembly-program"],
183
183
  defaultTags: ["rag", "search"],
184
- supportedPlatforms: ["c64u", "vice"],
184
+ supportedPlatforms: ["c64u", "u2", "vice"],
185
185
  workflowHints: [
186
186
  "Call RAG tools when the user needs references or examples before generating new code.",
187
187
  "Summarise the number of refs returned and suggest follow-up actions like reading specific docs.",
@@ -35,7 +35,7 @@ const executeBatchArgsSchema = objectSchema({
35
35
  export const batchModuleGroup = defineToolModule({
36
36
  domain: "batch",
37
37
  summary: "Execute multiple tool calls in a single MCP request for reduced round-trip overhead.",
38
- supportedPlatforms: ["c64u", "vice"],
38
+ supportedPlatforms: ["c64u", "u2", "vice"],
39
39
  resources: [],
40
40
  prompts: [],
41
41
  defaultTags: ["batch", "orchestration"],
@@ -154,7 +154,7 @@ export const configModuleGroup = defineToolModule({
154
154
  resources: ["c64://guide/bootstrap", "c64://guide/index"],
155
155
  prompts: ["memory-debug"],
156
156
  defaultTags: ["config", "diagnostics"],
157
- supportedPlatforms: ["c64u", "vice"],
157
+ supportedPlatforms: ["c64u", "u2", "vice"],
158
158
  workflowHints: [
159
159
  "List categories before changing values so users can confirm firmware-provided names.",
160
160
  "Mention when operations persist to flash or modify debug registers to highlight impacts.",
@@ -171,12 +171,12 @@ export const configModuleGroup = defineToolModule({
171
171
  }),
172
172
  tags: ["config", "diagnostics", "grouped"],
173
173
  operationPlatforms: {
174
- load_flash: ["c64u"],
175
- save_flash: ["c64u"],
176
- reset_defaults: ["c64u"],
174
+ load_flash: ["c64u", "u2"],
175
+ save_flash: ["c64u", "u2"],
176
+ reset_defaults: ["c64u", "u2"],
177
177
  read_debugreg: ["c64u"],
178
178
  write_debugreg: ["c64u"],
179
- shuffle: ["c64u"],
179
+ shuffle: ["c64u", "u2"],
180
180
  },
181
181
  operationToolNames: {
182
182
  load_flash: "config_load_from_flash",
@@ -251,7 +251,7 @@ const diskOperationHandlers = createOperationHandlers(diskOperations);
251
251
  export const diskModuleGroup = defineToolModule({
252
252
  domain: "storage",
253
253
  summary: "Grouped disk image management, mounting, and discovery tools.",
254
- supportedPlatforms: ["c64u", "vice"],
254
+ supportedPlatforms: ["c64u", "u2", "vice"],
255
255
  resources: ["c64://guide/bootstrap"],
256
256
  prompts: ["drive-management"],
257
257
  defaultTags: ["storage", "drive"],
@@ -269,7 +269,7 @@ export const diskModuleGroup = defineToolModule({
269
269
  variants: diskOperations.map((operation) => operation.schema),
270
270
  }),
271
271
  tags: ["storage", "drive", "grouped"],
272
- operationPlatforms: { file_info: ["c64u"], create_image: ["c64u"], find_and_run: ["c64u"] },
272
+ operationPlatforms: { file_info: ["c64u", "u2"], create_image: ["c64u", "u2"], find_and_run: ["c64u", "u2"] },
273
273
  operationToolNames: {
274
274
  file_info: "file_info",
275
275
  create_image: "create_image",
@@ -33,7 +33,7 @@ const driveOperationHandlers = createOperationHandlers(driveOperations);
33
33
  export const driveModuleGroup = defineToolModule({
34
34
  domain: "drive",
35
35
  summary: "Grouped drive power, reset, ROM, and mode helpers.",
36
- supportedPlatforms: ["c64u", "vice"],
36
+ supportedPlatforms: ["c64u", "u2", "vice"],
37
37
  resources: ["c64://guide/bootstrap"],
38
38
  defaultTags: ["drive", "hardware"],
39
39
  workflowHints: [
@@ -50,7 +50,7 @@ export const driveModuleGroup = defineToolModule({
50
50
  variants: driveOperations.map((operation) => operation.schema),
51
51
  }),
52
52
  tags: ["drive", "hardware", "grouped"],
53
- operationPlatforms: { load_rom: ["c64u"] },
53
+ operationPlatforms: { load_rom: ["c64u", "u2"] },
54
54
  operationToolNames: { load_rom: "drive_load_rom" },
55
55
  examples: [
56
56
  {
@@ -89,7 +89,7 @@ function parseVicState(d011, d016, d018, dd00, d020, d021) {
89
89
  };
90
90
  }
91
91
  const getDisplayStateArgsSchema = objectSchema({
92
- description: "Read VIC-II and CIA2 registers to determine the current graphics mode and memory layout. Works on C64U and VICE; both backends are read through shared memory primitives so the response shape is identical.",
92
+ description: "Read VIC-II and CIA2 registers to determine the current graphics mode and memory layout. The same shared-memory path is used on C64U/U64, U2-family hardware, and VICE, so the response shape is identical.",
93
93
  properties: {
94
94
  op: literalSchema("get_display_state"),
95
95
  },
@@ -212,7 +212,7 @@ export const graphicsModuleGroup = defineToolModule({
212
212
  "Use render helpers for PETSCII text, PETSCII art, sprites, and bitmaps; clarify whether PETSCII art executed or stayed a dry run.",
213
213
  "Mention sprite positions/colours so follow-up memory inspection stays grounded.",
214
214
  ],
215
- supportedPlatforms: ["c64u", "vice"],
215
+ supportedPlatforms: ["c64u", "u2", "vice"],
216
216
  tools: [
217
217
  {
218
218
  name: "c64_graphics",
@@ -27,7 +27,7 @@ const memoryOperations = [
27
27
  },
28
28
  {
29
29
  op: "disassemble",
30
- schema: extendSchemaWithOp("disassemble", disassembleArgsSchema.jsonSchema, { description: "Disassemble a memory region into annotated 6502/6510 instructions, including undocumented opcodes with canonical names. Symbol annotations from `.vs` files are applied when available. Works on both C64U and VICE." }),
30
+ schema: extendSchemaWithOp("disassemble", disassembleArgsSchema.jsonSchema, { description: "Disassemble a memory region into annotated 6502/6510 instructions, including undocumented opcodes with canonical names. Symbol annotations from `.vs` files are applied when available. Works on C64U/U64, U2-family hardware, and VICE." }),
31
31
  handler: groupedMemoryHandlers.disassemble,
32
32
  },
33
33
  {
@@ -60,7 +60,7 @@ const memoryOperationHandlers = createOperationHandlers(memoryOperations);
60
60
  export const memoryModuleGroup = defineToolModule({
61
61
  domain: "memory",
62
62
  summary: "Grouped memory, screen, and polling operations.",
63
- supportedPlatforms: ["c64u", "vice"],
63
+ supportedPlatforms: ["c64u", "u2", "vice"],
64
64
  resources: ["c64://guide/bootstrap", "c64://basic/spec", "c64://assembly/6510-spec"],
65
65
  prompts: ["memory-debug", "basic-program", "assembly-program"],
66
66
  defaultTags: ["memory", "debug"],
@@ -1,4 +1,5 @@
1
1
  import { describePlatformCapabilities } from "../../platform.js";
2
+ import { cancelGeneratedSidPlayback } from "../audio.js";
2
3
  import { debugModuleGroup as debugModule } from "../debug.js";
3
4
  import { viceModuleGroup as viceModule } from "../vice.js";
4
5
  import { jsonResult } from "../responses.js";
@@ -50,7 +51,7 @@ const selectBackendSchema = operationSchema("select", {
50
51
  properties: {
51
52
  backend: {
52
53
  type: "string",
53
- enum: ["c64u", "vice"],
54
+ enum: ["c64u", "u2", "vice"],
54
55
  description: "Backend to activate for subsequent tool calls.",
55
56
  },
56
57
  },
@@ -73,17 +74,20 @@ const platformOperationHandlers = {
73
74
  isError: true,
74
75
  };
75
76
  }
77
+ cancelGeneratedSidPlayback(); // A backend switch is the documented cue to stop it rather than let it keep playing on the old backend.
76
78
  ctx.client.switchBackend(backend);
77
79
  ctx.setPlatform(backend);
78
80
  const { availableTools, unavailableTools } = describeTargetPlatform(backend);
79
- const switchBackTarget = backend === "c64u" ? "vice" : "c64u";
81
+ const switchBackTarget = availableBackends.find((candidate) => candidate !== backend);
80
82
  const data = {
81
83
  success: true,
82
84
  activeBackend: backend,
83
85
  configuredBackends: availableBackends,
84
86
  availableTools,
85
87
  unavailableTools,
86
- usageHint: `Use c64_select_backend again with backend: "${switchBackTarget}" to switch back.`,
88
+ usageHint: switchBackTarget
89
+ ? `Use c64_select_backend again with backend: "${switchBackTarget}" to switch back.`
90
+ : "No other backend is configured.",
87
91
  };
88
92
  return jsonResult(data, { success: true });
89
93
  },
@@ -91,7 +95,7 @@ const platformOperationHandlers = {
91
95
  export const platformModuleGroup = defineToolModule({
92
96
  domain: "platform",
93
97
  summary: "Runtime backend selection and platform-status coordination.",
94
- supportedPlatforms: ["c64u", "vice"],
98
+ supportedPlatforms: ["c64u", "u2", "vice"],
95
99
  resources: ["c64://platform/status", "c64://guide/bootstrap"],
96
100
  prompts: [],
97
101
  defaultTags: ["platform", "backend", "grouped"],
@@ -101,7 +105,7 @@ export const platformModuleGroup = defineToolModule({
101
105
  tools: [
102
106
  {
103
107
  name: "c64_select_backend",
104
- description: "Switch the active backend between C64U hardware and the VICE emulator at runtime.",
108
+ description: "Switch the active backend between C64U/U64 hardware, U2-family cartridges, and the VICE emulator at runtime.",
105
109
  summary: "Changes the active backend without restarting the MCP server and reports resulting tool availability.",
106
110
  inputSchema: discriminatedUnionSchema({
107
111
  description: "Runtime backend selection operations.",
@@ -116,6 +116,7 @@ function invokePrintTool(toolName, rawArgs, ctx) {
116
116
  }
117
117
  export const printerModuleGroup = defineToolModule({
118
118
  domain: "printer",
119
+ supportedPlatforms: ["c64u", "u2"],
119
120
  summary: "Grouped printer text, bitmap, and character definition helpers.",
120
121
  resources: ["c64://guide/bootstrap"],
121
122
  defaultTags: ["printer", "device"],
@@ -12,7 +12,7 @@ const programOperations = [
12
12
  },
13
13
  {
14
14
  op: "run_prg",
15
- schema: extendSchemaWithOp("run_prg", ensureDescriptor(programDescriptorIndex, "run_prg").inputSchema, { description: "Load and execute a PRG from Ultimate-visible storage on c64u or a host-local path on VICE." }),
15
+ schema: extendSchemaWithOp("run_prg", ensureDescriptor(programDescriptorIndex, "run_prg").inputSchema, { description: "Load and execute a PRG from Ultimate-visible storage on C64U/U64 or U2-family hardware, or from a host-local path on VICE." }),
16
16
  handler: async (rawArgs, ctx) => invokeModuleTool(programRunnersModule, "run_prg", rawArgs, ctx),
17
17
  },
18
18
  {
@@ -55,10 +55,10 @@ export const programModule = defineToolModule({
55
55
  defaultTags: ["programs", "execution"],
56
56
  workflowHints: [
57
57
  "Choose BASIC or assembly uploaders based on the language you just generated for the user.",
58
- "Prefer PRG or CRT runners when the user supplies a file path instead of source text; PRG paths are host-local on VICE and Ultimate-visible on c64u.",
59
- "For a quick visible confirmation on VICE and/or C64U, prefer cross_platform_greeting instead of composing manual backend switches and BASIC upload steps.",
58
+ "Prefer PRG or CRT runners when the user supplies a file path instead of source text; PRG paths are host-local on VICE and Ultimate-visible on hardware.",
59
+ "For a quick visible confirmation on VICE, C64U/U64, or U2-family hardware, prefer cross_platform_greeting instead of composing manual backend switches and BASIC upload steps.",
60
60
  ],
61
- supportedPlatforms: ["c64u", "vice"],
61
+ supportedPlatforms: ["c64u", "u2", "vice"],
62
62
  tools: [
63
63
  {
64
64
  name: "c64_program",
@@ -69,7 +69,7 @@ export const programModule = defineToolModule({
69
69
  variants: programOperations.map((operation) => operation.schema),
70
70
  }),
71
71
  tags: ["programs", "execution", "grouped"],
72
- operationPlatforms: { load_prg: ["c64u"], run_crt: ["c64u"], bundle_run: ["c64u"] },
72
+ operationPlatforms: { load_prg: ["c64u", "u2"], run_crt: ["c64u", "u2"], bundle_run: ["c64u", "u2"] },
73
73
  operationToolNames: {
74
74
  load_prg: "load_prg",
75
75
  run_crt: "run_crt",
@@ -25,7 +25,7 @@ export const ragModuleGroup = defineToolModule({
25
25
  "Use BASIC retrieval before synthesising new BASIC code and mention primary resources in responses.",
26
26
  "For assembly, note registers or addresses surfaced so the user can inspect them further.",
27
27
  ],
28
- supportedPlatforms: ["c64u", "vice"],
28
+ supportedPlatforms: ["c64u", "u2", "vice"],
29
29
  tools: [
30
30
  {
31
31
  name: "c64_rag",
@@ -139,7 +139,7 @@ const soundOperationHandlers = createOperationHandlers(soundOperations);
139
139
  export const soundModuleGroup = defineToolModule({
140
140
  domain: "audio",
141
141
  summary: "Grouped SID control, playback, composition, and analysis operations.",
142
- supportedPlatforms: ["c64u", "vice"],
142
+ supportedPlatforms: ["c64u", "u2", "vice"],
143
143
  resources: ["c64://sound/sid/spec", "c64://sound/sidwave/spec", "c64://sound/sid/file-format"],
144
144
  prompts: ["sid-music"],
145
145
  defaultTags: ["sid", "audio"],
@@ -159,9 +159,9 @@ export const soundModuleGroup = defineToolModule({
159
159
  tags: ["sid", "audio", "grouped"],
160
160
  operationPlatforms: {
161
161
  capture_samples: ["c64u"],
162
- play_sid_file: ["c64u"],
163
- play_mod_file: ["c64u"],
164
- pipeline: ["c64u"],
162
+ play_sid_file: ["c64u", "u2"],
163
+ play_mod_file: ["c64u", "u2"],
164
+ pipeline: ["c64u", "u2"],
165
165
  analyze: ["c64u"],
166
166
  record_analyze: ["c64u"],
167
167
  },
@@ -30,11 +30,21 @@ const systemOperations = [
30
30
  schema: extendSchemaWithOp("poweroff", ensureDescriptor(machineDescriptorIndex, "poweroff").inputSchema, { description: "Request a controlled shutdown via the Ultimate firmware." }),
31
31
  handler: async (rawArgs, ctx) => invokeModuleTool(machineControlModule, "poweroff", rawArgs, ctx),
32
32
  },
33
+ {
34
+ op: "power_cycle",
35
+ schema: extendSchemaWithOp("power_cycle", ensureDescriptor(machineDescriptorIndex, "power_cycle").inputSchema, { description: "Return the active backend to a fresh state. C64U/U64 Tool Menu verification requires machine:menu_screen (a C64U firmware version that provides it, or U64 3.15+); U2-family reboots through REST." }),
36
+ handler: async (rawArgs, ctx) => invokeModuleTool(machineControlModule, "power_cycle", rawArgs, ctx),
37
+ },
33
38
  {
34
39
  op: "menu",
35
40
  schema: extendSchemaWithOp("menu", ensureDescriptor(machineDescriptorIndex, "menu_button").inputSchema, { description: "Toggle the Ultimate menu button for navigation." }),
36
41
  handler: async (rawArgs, ctx) => invokeModuleTool(machineControlModule, "menu_button", rawArgs, ctx),
37
42
  },
43
+ {
44
+ op: "read_menu_screen",
45
+ schema: extendSchemaWithOp("read_menu_screen", ensureDescriptor(machineDescriptorIndex, "read_menu_screen").inputSchema, { description: "Read the active Ultimate menu's raw character and colour matrix through machine:menu_screen (a C64U firmware version that provides it, or U64/U2 3.15+)." }),
46
+ handler: async (rawArgs, ctx) => invokeModuleTool(machineControlModule, "read_menu_screen", rawArgs, ctx),
47
+ },
38
48
  {
39
49
  op: "start_task",
40
50
  schema: extendSchemaWithOp("start_task", ensureDescriptor(metaDescriptorIndex, "start_background_task").inputSchema, { description: "Start a named background task that runs on an interval." }),
@@ -65,7 +75,7 @@ const systemOperationHandlers = createOperationHandlers(systemOperations);
65
75
  export const systemModuleGroup = defineToolModule({
66
76
  domain: "system",
67
77
  summary: "Grouped machine control and background task orchestration.",
68
- supportedPlatforms: ["c64u", "vice"],
78
+ supportedPlatforms: ["c64u", "u2", "vice"],
69
79
  resources: ["c64://guide/bootstrap"],
70
80
  prompts: ["memory-debug"],
71
81
  defaultTags: ["system", "control"],
@@ -83,8 +93,8 @@ export const systemModuleGroup = defineToolModule({
83
93
  variants: systemOperations.map((operation) => operation.schema),
84
94
  }),
85
95
  tags: ["system", "control", "grouped"],
86
- operationPlatforms: { pause: ["c64u"], resume: ["c64u"], menu: ["c64u"] },
87
- operationToolNames: { pause: "pause", resume: "resume", menu: "menu_button" },
96
+ operationPlatforms: { pause: ["c64u", "u2"], resume: ["c64u", "u2"], menu: ["c64u", "u2"], read_menu_screen: ["c64u", "u2"] },
97
+ operationToolNames: { pause: "pause", resume: "resume", menu: "menu_button", read_menu_screen: "read_menu_screen" },
88
98
  examples: [
89
99
  {
90
100
  name: "Soft reset",
@@ -143,7 +143,7 @@ const createDnpArgsSchema = objectSchema({
143
143
  export const storageModule = defineToolModule({
144
144
  domain: "storage",
145
145
  summary: "Drive management, disk image creation, and file inspection utilities.",
146
- supportedPlatforms: ["c64u", "vice"],
146
+ supportedPlatforms: ["c64u", "u2", "vice"],
147
147
  resources: ["c64://guide/bootstrap"],
148
148
  prompts: ["memory-debug"],
149
149
  defaultTags: ["drive", "storage"],
@@ -2,6 +2,10 @@ import net from "node:net";
2
2
  import { asciiToScreenCodes } from "./readiness.js";
3
3
  const SCREEN_BASE = 0x0400;
4
4
  const SCREEN_SIZE = 40 * 25;
5
+ const KEYBOARD_BUFFER = 0x0277; // KERNAL keyboard buffer base ($0277), where a typed SYS command lands.
6
+ const SYS_COMMAND_PATTERN = /SYS\s*\d+/i; // upload_run_asm enters an assembled program via a typed SYSnnnnn.
7
+ const HEARTBEAT_GRANULARITY_MS = 10; // Well below the poll interval so consecutive liveness reads always differ.
8
+ const HEARTBEAT_DURATION_MS = 1000; // How long a SYS-triggered program keeps "running".
5
9
  function buildDefaultRegisterMetadata() {
6
10
  return Object.freeze([
7
11
  { id: 0, name: "PC", bits: 16 },
@@ -30,6 +34,7 @@ export class ViceMockServer {
30
34
  this.server = null;
31
35
  this.memory = new Uint8Array(0x10000);
32
36
  this.helloReady = false;
37
+ this.heartbeat = null;
33
38
  this.checkpoints = new Map();
34
39
  this.nextCheckpointId = 1;
35
40
  this.registerMetadata = buildDefaultRegisterMetadata();
@@ -186,6 +191,7 @@ export class ViceMockServer {
186
191
  for (let i = 0; i < length; i += 1) {
187
192
  payload[2 + i] = this.memory[(start + i) & 0xffff];
188
193
  }
194
+ this.applyHeartbeatOverlay(start, length, payload);
189
195
  return buildResponse(cmd, reqId, payload);
190
196
  }
191
197
  case 0x02: { // mem set
@@ -197,6 +203,12 @@ export class ViceMockServer {
197
203
  }
198
204
  if (start <= SCREEN_BASE && end >= SCREEN_BASE) {
199
205
  this.helloReady = false;
206
+ this.heartbeat = null; // A client writing screen RAM has taken over; end any simulated run.
207
+ }
208
+ this.maybeStartAsmHeartbeat(start, end, payload);
209
+ const KEYBOARD_NDX = 0x00c6; // This mock runs no real 6502 code, so simulate the KERNAL IRQ draining $00C6 shortly after each write.
210
+ if (start <= KEYBOARD_NDX && end >= KEYBOARD_NDX) {
211
+ setTimeout(() => { this.memory[KEYBOARD_NDX] = 0; }, 5).unref();
200
212
  }
201
213
  return buildResponse(cmd, reqId);
202
214
  }
@@ -411,6 +423,7 @@ export class ViceMockServer {
411
423
  this.memory[0x31] = 0x03;
412
424
  this.memory[0x32] = 0x08;
413
425
  this.helloReady = false;
426
+ this.heartbeat = null;
414
427
  this.checkpoints.clear();
415
428
  this.nextCheckpointId = 1;
416
429
  this.registerValues.clear();
@@ -431,6 +444,26 @@ export class ViceMockServer {
431
444
  hello.copy(Buffer.from(this.memory.buffer, offset, hello.length));
432
445
  this.helloReady = true;
433
446
  }
447
+ maybeStartAsmHeartbeat(start, end, payload) {
448
+ if (start > KEYBOARD_BUFFER || end < KEYBOARD_BUFFER)
449
+ return; // Only a write covering the KERNAL keyboard buffer can be a typed SYS trigger.
450
+ if (!SYS_COMMAND_PATTERN.test(payload.toString("ascii")))
451
+ return;
452
+ this.heartbeat = { startedAt: Date.now(), baseValue: this.memory[SCREEN_BASE] };
453
+ }
454
+ applyHeartbeatOverlay(start, length, payload) {
455
+ const heartbeat = this.heartbeat;
456
+ if (!heartbeat)
457
+ return;
458
+ const elapsed = Date.now() - heartbeat.startedAt;
459
+ if (elapsed < 0 || elapsed >= HEARTBEAT_DURATION_MS)
460
+ return;
461
+ const offset = SCREEN_BASE - start;
462
+ if (offset < 0 || offset >= length)
463
+ return;
464
+ const ticks = Math.floor(elapsed / HEARTBEAT_GRANULARITY_MS);
465
+ payload[2 + offset] = (heartbeat.baseValue + ticks) & 0xff;
466
+ }
434
467
  }
435
468
  export async function startViceMockServer(options) {
436
469
  const server = new ViceMockServer(options);
@@ -378,10 +378,20 @@ export async function startViceProcess(options) {
378
378
  await terminateProcess(xvfb, "SIGKILL", 200);
379
379
  }
380
380
  };
381
+ const stopSync = () => {
382
+ for (const process of [child, xvfb]) {
383
+ if (!process || process.exitCode !== null || process.signalCode !== null)
384
+ continue;
385
+ try {
386
+ process.kill("SIGKILL");
387
+ }
388
+ catch { }
389
+ }
390
+ };
381
391
  child.once("exit", async () => {
382
392
  await terminateProcess(xvfb, "SIGTERM", 0);
383
393
  });
384
- return { host: options.host, port: options.port, process: child, stop };
394
+ return { host: options.host, port: options.port, process: child, stop, stopSync };
385
395
  }
386
396
  function isIgnorableSocketDirError(error) {
387
397
  if (!error || typeof error !== "object" || !("code" in error)) {