c64bridge 1.0.3 → 1.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/README.md +12 -12
  2. package/data/embeddings_basic.json +1 -1
  3. package/data/embeddings_hardware.json +1 -1
  4. package/data/embeddings_mixed.json +1 -1
  5. package/data/embeddings_other.json +1 -1
  6. package/data/graphics/character-set.csv +64 -64
  7. package/dist/.tsbuildinfo +1 -1
  8. package/dist/c64Client.js +68 -61
  9. package/dist/config.js +75 -38
  10. package/dist/device.js +154 -120
  11. package/dist/knowledge.js +2 -1
  12. package/dist/petscii.js +18 -15
  13. package/dist/petsciiArt.js +40 -14
  14. package/dist/rag/indexer.js +2 -1
  15. package/dist/sidwave.js +1 -1
  16. package/dist/sidwaveCompiler.js +3 -9
  17. package/dist/tools/audio.js +34 -8
  18. package/dist/tools/debug.js +9 -9
  19. package/dist/tools/errors.js +12 -1
  20. package/dist/tools/hex.js +9 -0
  21. package/dist/tools/input.js +13 -7
  22. package/dist/tools/memory.js +5 -4
  23. package/dist/tools/meta/artifacts.js +8 -3
  24. package/dist/tools/meta/audio.js +25 -28
  25. package/dist/tools/meta/background.js +6 -1
  26. package/dist/tools/meta/config.js +12 -4
  27. package/dist/tools/meta/configInventory.js +4 -1
  28. package/dist/tools/meta/filesystem.js +37 -10
  29. package/dist/tools/meta/graphics.js +10 -15
  30. package/dist/tools/meta/memory.js +12 -18
  31. package/dist/tools/meta/program.js +44 -16
  32. package/dist/tools/pollValidator.js +30 -2
  33. package/dist/tools/programRunners.js +5 -4
  34. package/dist/tools/registry/batch.js +4 -2
  35. package/dist/tools/registry/config.js +1 -1
  36. package/dist/tools/registry/disk.js +5 -0
  37. package/dist/tools/registry/extract.js +3 -0
  38. package/dist/tools/registry/graphics.js +1 -0
  39. package/dist/tools/registry/program.js +1 -1
  40. package/dist/tools/registry/sound.js +3 -3
  41. package/dist/tools/registry/system.js +3 -3
  42. package/dist/tools/storage.js +2 -2
  43. package/dist/tools/translation/assembler.js +150 -47
  44. package/dist/tools/translation/basicTokenizer.js +39 -9
  45. package/dist/tools/types.js +11 -4
  46. package/dist/vicBitmap.js +16 -0
  47. package/dist/vice/mockServer.js +7 -3
  48. package/dist/vice/viceClient.js +16 -6
  49. package/mcp/schemas/c64_config.schema.json +2 -2
  50. package/mcp/schemas/c64_program.schema.json +10 -18
  51. package/mcp/schemas/c64_sound.schema.json +8 -6
  52. package/mcp/schemas/c64_system.schema.json +9 -7
  53. package/mcp/server.json +1 -1
  54. package/mcp/tools.json +58 -43
  55. package/mcp.json +1 -1
  56. package/package.json +1 -1
  57. package/scripts/mockC64Server.mjs +98 -19
  58. package/server.json +2 -2
@@ -5,14 +5,13 @@ import { promises as fs } from "node:fs";
5
5
  import { dirname, resolve as resolvePath } from "node:path";
6
6
  import { createHash } from "node:crypto";
7
7
  import { normalizeErrorDetails } from "./util.js";
8
+ import { stripHexByteSeparators } from "../hex.js";
8
9
  function supportsMachinePause(ctx) {
9
10
  const platformId = ctx.platform?.id ?? ctx.client?.type;
10
11
  return platformId !== "vice";
11
12
  }
12
13
  function hexClean(input) {
13
- const trimmed = input.trim();
14
- const withoutPrefix = trimmed.startsWith("$") ? trimmed.slice(1) : trimmed;
15
- return withoutPrefix.replace(/[\s_]/g, "").toLowerCase();
14
+ return stripHexByteSeparators(input).toLowerCase();
16
15
  }
17
16
  function hexToBytes(input) {
18
17
  const cleaned = hexClean(input);
@@ -31,21 +30,16 @@ function parseAddressNumeric(value) {
31
30
  throw new ToolValidationError("Address must be a non-empty string", { path: "$.address" });
32
31
  }
33
32
  const lower = value.trim().toLowerCase();
34
- let radix = 10;
35
- let literal = lower;
36
- if (lower.startsWith("$")) {
37
- radix = 16;
38
- literal = lower.slice(1);
39
- }
40
- else if (lower.startsWith("0x")) {
41
- radix = 16;
42
- literal = lower.slice(2);
43
- }
44
- else if (lower.startsWith("%")) {
45
- radix = 2;
46
- literal = lower.slice(1);
47
- }
48
- const parsed = Number.parseInt(literal, radix);
33
+ // Anchored patterns: Number.parseInt alone would read "$20ZZ" as $0020.
34
+ let parsed = Number.NaN;
35
+ if (/^\$[0-9a-f]+$/.test(lower))
36
+ parsed = Number.parseInt(lower.slice(1), 16);
37
+ else if (/^0x[0-9a-f]+$/.test(lower))
38
+ parsed = Number.parseInt(lower.slice(2), 16);
39
+ else if (/^%[01]+$/.test(lower))
40
+ parsed = Number.parseInt(lower.slice(1), 2);
41
+ else if (/^[0-9]+$/.test(lower))
42
+ parsed = Number.parseInt(lower, 10);
49
43
  if (!Number.isFinite(parsed) || Number.isNaN(parsed) || parsed < 0 || parsed > 0xFFFF) {
50
44
  throw new ToolValidationError("Invalid address value", { path: "$.address", details: { value } });
51
45
  }
@@ -1,12 +1,14 @@
1
1
  import { objectSchema, stringSchema, arraySchema, numberSchema, optionalSchema, booleanSchema } from "../schema.js";
2
2
  import { jsonResult } from "../responses.js";
3
- import { ToolError, ToolExecutionError, toolErrorResult, unknownErrorResult } from "../errors.js";
3
+ import { ToolError, ToolUnsupportedPlatformError, ToolExecutionError, toolErrorResult, unknownErrorResult } from "../errors.js";
4
+ import { getPlatformStatus } from "../../platform.js";
4
5
  import { promises as fs } from "node:fs";
5
6
  import { resolve as resolvePath, join as joinPath } from "node:path";
6
7
  import { sleep, formatTimestampSpec } from "./util.js";
7
8
  import { getTasksHomeDir } from "./background.js";
8
9
  import { Jimp } from "jimp";
9
10
  import { withDiagnosticSpan } from "../../diagnostics.js";
11
+ import { stripHexByteSeparators } from "../hex.js";
10
12
  const C64_SCREENSHOT_PALETTE = [
11
13
  0x000000ff,
12
14
  0xffffffff,
@@ -56,11 +58,14 @@ function shouldUseVisibleViceGreetingFastPath(rawArgs, requestedBackends) {
56
58
  && !hasOwnProperty(args, "outputPath");
57
59
  }
58
60
  function canonicalGreetingBackends() {
59
- return ["vice", "c64u"];
61
+ return ["vice", "c64u", "u2"];
60
62
  }
61
63
  function uniqueGreetingBackends(backends) {
62
64
  return backends.filter((backend, index) => backends.indexOf(backend) === index);
63
65
  }
66
+ function normaliseAssertionHex(value) {
67
+ return stripHexByteSeparators(value).toUpperCase();
68
+ }
64
69
  function normaliseScreenText(screen) {
65
70
  return screen.replace(/\r\n?/g, "\n").replace(/\s+/g, " ").trim().toUpperCase();
66
71
  }
@@ -226,9 +231,9 @@ const crossPlatformGreetingArgsSchema = objectSchema({
226
231
  properties: {
227
232
  platforms: optionalSchema(arraySchema(stringSchema({
228
233
  description: "Backends to target in sequence.",
229
- enum: ["vice", "c64u"],
234
+ enum: ["vice", "c64u", "u2"],
230
235
  minLength: 1,
231
- })), ["vice", "c64u"]),
236
+ }))),
232
237
  messageTemplate: optionalSchema(stringSchema({
233
238
  description: "Greeting template. Use {PLATFORM}/{BACKEND} for uppercase or {platform}/{backend} for lowercase substitution.",
234
239
  minLength: 1,
@@ -238,7 +243,7 @@ const crossPlatformGreetingArgsSchema = objectSchema({
238
243
  default: true,
239
244
  }), true),
240
245
  captureScreenshot: optionalSchema(booleanSchema({
241
- description: "Capture a framebuffer screenshot and save it as a PNG per backend.",
246
+ description: "Capture a framebuffer screenshot on C64U/U64 or VICE. U2 skips screenshots by default; explicitly requesting one on U2 fails before execution.",
242
247
  default: true,
243
248
  }), true),
244
249
  outputPath: optionalSchema(stringSchema({
@@ -311,11 +316,14 @@ export const tools = [
311
316
  details: { availableBackends },
312
317
  });
313
318
  }
319
+ if (args?.captureScreenshot === true && requestedBackends.includes("u2")) {
320
+ throw new ToolUnsupportedPlatformError("capture_frame", "u2", ["c64u", "vice"]);
321
+ }
314
322
  const visibleViceFastPath = shouldUseVisibleViceGreetingFastPath(args ?? {}, requestedBackends);
315
323
  const verify = visibleViceFastPath ? false : parsed.verify !== false;
316
324
  const captureScreenshot = visibleViceFastPath ? false : parsed.captureScreenshot !== false;
317
325
  const restoreActiveBackend = parsed.restoreActiveBackend !== false;
318
- const outputPath = captureScreenshot || parsed.outputPath
326
+ const outputPath = (captureScreenshot && requestedBackends.some((backend) => backend !== "u2")) || parsed.outputPath
319
327
  ? resolvePath(String(parsed.outputPath ?? joinPath(process.cwd(), "artifacts", "greetings", `run_${Date.now()}`)))
320
328
  : undefined;
321
329
  const timeoutMs = parsed.timeoutMs ?? 1500;
@@ -330,6 +338,7 @@ export const tools = [
330
338
  }
331
339
  try {
332
340
  for (const backend of requestedBackends) {
341
+ const backendCaptureScreenshot = captureScreenshot && backend !== "u2";
333
342
  const expectedText = applyGreetingTemplate(template, backend);
334
343
  const program = buildGreetingProgram(expectedText);
335
344
  const timeline = [];
@@ -358,7 +367,7 @@ export const tools = [
358
367
  ctx.setPlatform(backend);
359
368
  });
360
369
  let prepareCapturePromise;
361
- if (captureScreenshot && backend === "c64u" && typeof client.prepareVideoCapture === "function") {
370
+ if (backendCaptureScreenshot && backend === "c64u" && typeof client.prepareVideoCapture === "function") {
362
371
  prepareCapturePromise = timedStep("prepare_capture", async () => {
363
372
  await client.prepareVideoCapture?.({ keepAliveMs: Math.max(500, timeoutMs) });
364
373
  preparedCaptureSession = true;
@@ -410,7 +419,7 @@ export const tools = [
410
419
  let screenshotPath;
411
420
  let screenshotAnalysis;
412
421
  let screenshotError;
413
- if (captureScreenshot) {
422
+ if (backendCaptureScreenshot) {
414
423
  try {
415
424
  const capture = await timedStep("capture_screenshot", () => client.captureFrames({
416
425
  count: 1,
@@ -447,7 +456,7 @@ export const tools = [
447
456
  }
448
457
  backendResult.verification = {
449
458
  screenContainsExpectedText: verify ? screenMatched : undefined,
450
- screenshotCaptured: captureScreenshot ? !screenshotError : undefined,
459
+ screenshotCaptured: backendCaptureScreenshot ? !screenshotError : undefined,
451
460
  screenshotAnalysis,
452
461
  };
453
462
  if (screenshotPath) {
@@ -458,7 +467,7 @@ export const tools = [
458
467
  }
459
468
  backendResult.success = runResult.success
460
469
  && (!verify || screenMatched)
461
- && (!captureScreenshot || !screenshotError);
470
+ && (!backendCaptureScreenshot || !screenshotError);
462
471
  backendResult.promptToVisibleLatencyMs = timeline
463
472
  .filter((entry) => entry.ok === true && (entry.name === "switch_backend" || entry.name === "render_greeting_screen" || entry.name === "verify_screen"))
464
473
  .reduce((total, entry) => total + Number(entry.latencyMs ?? 0), 0);
@@ -642,6 +651,10 @@ export const tools = [
642
651
  try {
643
652
  const parsed = batchRunWithAssertionsArgsSchema.parse(args ?? {});
644
653
  const programs = parsed.programs;
654
+ const platform = ctx.platform?.id ?? getPlatformStatus().id;
655
+ if (platform === "vice" && programs.some((program) => program.path.toLowerCase().endsWith(".crt"))) {
656
+ throw new ToolUnsupportedPlatformError("run_crt", platform, ["c64u", "u2"]);
657
+ }
645
658
  const continueOnError = parsed.continueOnError ?? false;
646
659
  const durationMs = parsed.durationMs ?? 2000;
647
660
  const resetDelayMs = parsed.resetDelayMs ?? 100;
@@ -659,10 +672,14 @@ export const tools = [
659
672
  // Run program
660
673
  const ext = program.path.toLowerCase().split(".").pop();
661
674
  if (ext === "prg") {
662
- await ctx.client.runPrgFile(program.path);
675
+ const run = await ctx.client.runPrgFile(program.path);
676
+ if (!run.success)
677
+ throw new ToolExecutionError("PRG playback failed", { details: run.details });
663
678
  }
664
679
  else if (ext === "crt") {
665
- await ctx.client.runCrtFile(program.path);
680
+ const run = await ctx.client.runCrtFile(program.path);
681
+ if (!run.success)
682
+ throw new ToolExecutionError("CRT playback failed", { details: run.details });
666
683
  }
667
684
  await sleep(durationMs);
668
685
  // Check assertions
@@ -683,9 +700,17 @@ export const tools = [
683
700
  else if (assertion.type === "memory_equals") {
684
701
  const addr = assertion.address ?? "$0400";
685
702
  const expected = assertion.expected ?? "$00";
686
- const result = await ctx.client.readMemory(addr, "1");
687
- const actual = result.data ?? "$00";
688
- const matched = actual.toLowerCase() === expected.toLowerCase();
703
+ // Compare every expected byte, and treat a failed read as an error rather than as $00.
704
+ const expectedHex = normaliseAssertionHex(expected);
705
+ if (expectedHex.length === 0 || expectedHex.length % 2 !== 0 || !/^[0-9A-F]+$/.test(expectedHex)) {
706
+ throw new Error(`memory_equals expected value ${expected} is not a whole number of hex bytes`);
707
+ }
708
+ const result = await ctx.client.readMemory(addr, String(expectedHex.length / 2));
709
+ if (!result.success || typeof result.data !== "string") {
710
+ throw new Error(`Failed to read memory at ${addr} for memory_equals assertion`);
711
+ }
712
+ const actual = result.data;
713
+ const matched = normaliseAssertionHex(actual) === expectedHex;
689
714
  assertionResults.push({
690
715
  type: assertion.type,
691
716
  status: matched ? "pass" : "fail",
@@ -697,7 +722,10 @@ export const tools = [
697
722
  else if (assertion.type === "sid_silent") {
698
723
  // Check SID gate bits are off (simple check)
699
724
  const result = await ctx.client.readMemory("$D404", "1");
700
- const gate1 = parseInt((result.data ?? "$00").slice(1), 16) & 0x01;
725
+ if (!result.success || typeof result.data !== "string") {
726
+ throw new Error("Failed to read SID voice 1 control register for sid_silent assertion");
727
+ }
728
+ const gate1 = parseInt(result.data.slice(1), 16) & 0x01;
701
729
  const isSilent = gate1 === 0;
702
730
  assertionResults.push({
703
731
  type: assertion.type,
@@ -75,7 +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 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.
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. The apostrophe covers UNDEF'D STATEMENT, REDIM'D ARRAY and CAN'T CONTINUE.
79
79
  if (errorMatch) {
80
80
  const errorType = errorMatch[1]?.trim().replace(/\s+/g, " ");
81
81
  const lineStr = errorMatch[2];
@@ -87,6 +87,19 @@ function extractBasicError(screen) {
87
87
  }
88
88
  return null;
89
89
  }
90
+ /**
91
+ * True when the most recent typed SYS command is followed by a READY. prompt,
92
+ * which is how a machine-code routine that ends with RTS hands back to BASIC.
93
+ */
94
+ function returnedToBasicAfterSys(screen) {
95
+ const lines = screen.split("\n").map((line) => line.trim().toUpperCase());
96
+ let sysLine = -1;
97
+ lines.forEach((line, index) => {
98
+ if (/^SYS\s*\d+$/.test(line))
99
+ sysLine = index;
100
+ });
101
+ return sysLine >= 0 && lines.slice(sysLine + 1).some((line) => line === "READY.");
102
+ }
90
103
  /**
91
104
  * Poll for BASIC program outcome by detecting errors on screen.
92
105
  * First waits for RUN or ERROR to appear, then continues polling for errors.
@@ -157,7 +170,12 @@ async function pollAsmOutcome(client, logger, config) {
157
170
  try {
158
171
  const screen = await client.readScreen();
159
172
  const upperScreen = screen.toUpperCase();
160
- if (upperScreen.includes("RUN") || upperScreen.includes("ERROR")) {
173
+ if (returnedToBasicAfterSys(screen)) {
174
+ // The typed SYS line is followed by READY., so the routine ran and returned with RTS.
175
+ logger.debug("ASM program returned to BASIC", { elapsed: Date.now() - startTime });
176
+ return { status: "ok", type: "ASM" };
177
+ }
178
+ if (upperScreen.includes("RUN") || upperScreen.includes("SYS") || upperScreen.includes("ERROR")) {
161
179
  runDetected = true;
162
180
  logger.debug("RUN detected, starting stabilization window", {
163
181
  elapsed: Date.now() - startTime,
@@ -182,6 +200,16 @@ async function pollAsmOutcome(client, logger, config) {
182
200
  const pollDeadline = Date.now() + config.maxMs;
183
201
  while (Date.now() < pollDeadline && !alive) {
184
202
  pollCount++;
203
+ try {
204
+ // A routine that works for a while before RTS shows READY. only after the first screen read.
205
+ if (returnedToBasicAfterSys(await client.readScreen())) {
206
+ logger.debug("ASM program returned to BASIC", { pollCount, elapsed: Date.now() - startTime });
207
+ return { status: "ok", type: "ASM" };
208
+ }
209
+ }
210
+ catch (error) {
211
+ logger.debug("Failed to read screen during ASM polling", { error, pollCount });
212
+ }
185
213
  try {
186
214
  const screenRam = await client.readMemoryRaw(0x0400, 0x03e8);
187
215
  const signature = computeCrc32(screenRam);
@@ -1,5 +1,5 @@
1
1
  import fs from "node:fs";
2
- import { assemblyToPrgDetailed, AssemblyError } from "./translation/assembler.js";
2
+ import { assemblyToPrgDetailed, AssemblyError, prgEntryAddress } from "./translation/assembler.js";
3
3
  import { basicToPrg } from "./translation/basicTokenizer.js";
4
4
  import { defineToolModule, OPERATION_DISCRIMINATOR, } from "./types.js";
5
5
  import { booleanSchema, objectSchema, optionalSchema, stringSchema } from "./schema.js";
@@ -52,7 +52,8 @@ function parseBasicRuntimeErrors(screen) {
52
52
  continue;
53
53
  }
54
54
  seen.add(lineNumber);
55
- const typeMatch = /\?([A-Z ?]+?)\s+ERROR\s+IN/i.exec(row.toUpperCase());
55
+ // The apostrophe covers UNDEF'D STATEMENT, REDIM'D ARRAY and UNDEF'D FUNCTION.
56
+ const typeMatch = /\?([A-Z' ?]+?)\s+ERROR\s+IN/i.exec(row.toUpperCase());
56
57
  const type = typeMatch?.[1]?.trim().replace(/\s+/g, " ") || undefined;
57
58
  errors.push({
58
59
  line: lineNumber,
@@ -441,7 +442,7 @@ async function executeUploadRunAsm(rawArgs, ctx) {
441
442
  // Assemble locally to expose structured metadata and capture symbols
442
443
  const asmResult = assemblyToPrgDetailed(parsed.program);
443
444
  const prg = asmResult.prg;
444
- const entryAddress = prg.readUInt16LE(0);
445
+ const entryAddress = prgEntryAddress(prg);
445
446
  if (ctx.platform.id === "vice") {
446
447
  setViceSymbols(asmResult.symbols.entries());
447
448
  }
@@ -686,7 +687,7 @@ export const programRunnersModule = defineToolModule({
686
687
  {
687
688
  name: "Set screen char",
688
689
  description: "Write 1 to $0400 then RTS",
689
- arguments: { program: ".org $0801\nstart: lda #$01\n sta $0400\n rts" },
690
+ arguments: { program: "start: lda #$01\n sta $0400\n rts" },
690
691
  },
691
692
  ],
692
693
  workflowHints: [
@@ -114,10 +114,12 @@ export const batchModuleGroup = defineToolModule({
114
114
  try {
115
115
  const toolResult = await toolRegistry.invoke(toolName, args, {
116
116
  ...ctx,
117
- platform: getPlatformStatus(),
117
+ platform: typeof ctx.client.getActiveBackendType === "function"
118
+ ? { ...(ctx.platform ?? getPlatformStatus()), id: await ctx.client.getActiveBackendType() }
119
+ : ctx.platform ?? getPlatformStatus(),
118
120
  });
119
121
  const elapsedMs = Date.now() - cmdStart;
120
- const isError = toolResult.isError === true;
122
+ const isError = toolResult.isError === true || toolResult.metadata?.success === false;
121
123
  const text = toolResult.content.map((c) => c.text).join("\n");
122
124
  const resultPayload = toolResult.structuredContent?.data ?? text;
123
125
  if (isError) {
@@ -24,7 +24,7 @@ const configRestoreArgsSchema = objectSchema({
24
24
  minLength: 1,
25
25
  }),
26
26
  applyToFlash: optionalSchema(booleanSchema({
27
- description: "When true, save the restored configuration to flash immediately.",
27
+ description: "When true, save the restored configuration to flash immediately (C64U/U64 and U2 only).",
28
28
  default: false,
29
29
  })),
30
30
  },
@@ -161,6 +161,11 @@ const diskOperations = [
161
161
  const typedAttachment = attachmentMode;
162
162
  const typedDriveMode = driveMode;
163
163
  if (verify) {
164
+ if (typedType || typedAttachment) {
165
+ throw new ToolValidationError("type and attachmentMode are not applied by the verified mount workflow; set verify to false to use them", {
166
+ path: typedType ? "$.type" : "$.attachmentMode",
167
+ });
168
+ }
164
169
  return metaModule.invoke("drive_mount_and_verify", {
165
170
  drive: parsed.drive,
166
171
  imagePath: parsed.image,
@@ -32,6 +32,7 @@ const extractOperations = [
32
32
  const extractOperationHandlers = createOperationHandlers(extractOperations);
33
33
  export const extractModule = defineToolModule({
34
34
  domain: "extract",
35
+ supportedPlatforms: ["c64u", "u2", "vice"],
35
36
  summary: "Grouped extraction helpers for sprites, charsets, memory dumps, and diagnostics.",
36
37
  resources: ["c64://guide/bootstrap", "c64://basic/spec", "c64://assembly/6510-spec"],
37
38
  defaultTags: ["extract", "diagnostics"],
@@ -49,6 +50,8 @@ export const extractModule = defineToolModule({
49
50
  variants: extractOperations.map((operation) => operation.schema),
50
51
  }),
51
52
  tags: ["extract", "diagnostics", "grouped"],
53
+ operationPlatforms: { fs_stats: ["c64u", "u2"] },
54
+ operationToolNames: { fs_stats: "filesystem_stats_by_extension" },
52
55
  examples: [
53
56
  {
54
57
  name: "Dump RAM to file",
@@ -216,6 +216,7 @@ export const graphicsModuleGroup = defineToolModule({
216
216
  tools: [
217
217
  {
218
218
  name: "c64_graphics",
219
+ operationPlatforms: { capture_frame: ["c64u", "vice"] },
219
220
  description: "Grouped entry point for frame capture and graphics rendering workflows.",
220
221
  summary: "Captures frames, renders PETSCII text or art, previews sprites, and displays bitmap images from one tool.",
221
222
  inputSchema: discriminatedUnionSchema({
@@ -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", "u2"], run_crt: ["c64u", "u2"], bundle_run: ["c64u", "u2"] },
72
+ operationPlatforms: { load_prg: ["c64u", "u2"], run_crt: ["c64u", "u2"] },
73
73
  operationToolNames: {
74
74
  load_prg: "load_prg",
75
75
  run_crt: "run_crt",
@@ -161,9 +161,9 @@ export const soundModuleGroup = defineToolModule({
161
161
  capture_samples: ["c64u"],
162
162
  play_sid_file: ["c64u", "u2"],
163
163
  play_mod_file: ["c64u", "u2"],
164
- pipeline: ["c64u", "u2"],
165
- analyze: ["c64u"],
166
- record_analyze: ["c64u"],
164
+ pipeline: ["c64u", "u2", "vice"],
165
+ analyze: ["c64u", "u2", "vice"],
166
+ record_analyze: ["c64u", "u2", "vice"],
167
167
  },
168
168
  operationToolNames: {
169
169
  play_sid_file: "sidplay_file",
@@ -27,7 +27,7 @@ const systemOperations = [
27
27
  },
28
28
  {
29
29
  op: "poweroff",
30
- schema: extendSchemaWithOp("poweroff", ensureDescriptor(machineDescriptorIndex, "poweroff").inputSchema, { description: "Request a controlled shutdown via the Ultimate firmware." }),
30
+ schema: extendSchemaWithOp("poweroff", ensureDescriptor(machineDescriptorIndex, "poweroff").inputSchema, { description: "Shut down C64U/U64 or quit VICE. U2-family hardware cannot power off." }),
31
31
  handler: async (rawArgs, ctx) => invokeModuleTool(machineControlModule, "poweroff", rawArgs, ctx),
32
32
  },
33
33
  {
@@ -93,8 +93,8 @@ export const systemModuleGroup = defineToolModule({
93
93
  variants: systemOperations.map((operation) => operation.schema),
94
94
  }),
95
95
  tags: ["system", "control", "grouped"],
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" },
96
+ operationPlatforms: { poweroff: ["c64u", "vice"], pause: ["c64u", "u2"], resume: ["c64u", "u2"], menu: ["c64u", "u2"], read_menu_screen: ["c64u", "u2"] },
97
+ operationToolNames: { poweroff: "poweroff", pause: "pause", resume: "resume", menu: "menu_button", read_menu_screen: "read_menu_screen" },
98
98
  examples: [
99
99
  {
100
100
  name: "Soft reset",
@@ -39,11 +39,11 @@ const driveMountArgsSchema = objectSchema({
39
39
  drive: driveIdentifierSchema,
40
40
  image: diskImagePathSchema,
41
41
  type: optionalSchema(stringSchema({
42
- description: "Override the disk image type when auto-detection is incorrect.",
42
+ description: "Override the disk image type when auto-detection is incorrect (C64U/U64 and U2 only; VICE uses auto-detection).",
43
43
  enum: ["d64", "g64", "d71", "g71", "d81"],
44
44
  })),
45
45
  mode: optionalSchema(stringSchema({
46
- description: "Attachment mode controlling how the firmware treats the mounted image.",
46
+ description: "Attachment mode controlling how the firmware treats the mounted image (C64U/U64 and U2 only).",
47
47
  enum: ["readwrite", "readonly", "unlinked"],
48
48
  })),
49
49
  },