c64bridge 1.0.2 → 1.0.4

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.
@@ -4,7 +4,8 @@ import { recordAndAnalyzeAudio } from "../audio/record_and_analyze_audio.js";
4
4
  import { defineToolModule } from "./types.js";
5
5
  import { booleanSchema, numberSchema, objectSchema, optionalSchema, stringSchema, } from "./schema.js";
6
6
  import { jsonResult, textResult } from "./responses.js";
7
- import { ToolError, ToolExecutionError, ToolValidationError, toolErrorResult, unknownErrorResult, } from "./errors.js";
7
+ import { ToolError, ToolExecutionError, ToolValidationError, ToolUnsupportedPlatformError, toolErrorResult, unknownErrorResult, } from "./errors.js";
8
+ import { getPlatformStatus } from "../platform.js";
8
9
  const NOTE_PATTERN = /^([A-Ga-g])([#b]?)(-?\d+)$/;
9
10
  const DEFAULT_SILENCE_VERIFY_DURATION_SECONDS = 1.5;
10
11
  const DEFAULT_SILENCE_VERIFY_RMS_THRESHOLD = 0.02;
@@ -244,7 +245,7 @@ const musicCompileArgsSchema = objectSchema({
244
245
  enum: ["yaml", "json"],
245
246
  })),
246
247
  output: optionalSchema(stringSchema({
247
- description: "Playback target format.",
248
+ description: "Playback target format. PRG works on all backends; SID playback requires C64U/U64 or U2 (SID export with dryRun works everywhere).",
248
249
  enum: ["prg", "sid"],
249
250
  default: "prg",
250
251
  }), "prg"),
@@ -256,7 +257,7 @@ const musicCompileArgsSchema = objectSchema({
256
257
  additionalProperties: false,
257
258
  });
258
259
  const recordAndAnalyzeArgsSchema = objectSchema({
259
- description: "Record audio from the default input and analyze SID playback characteristics.",
260
+ description: "Record audio and analyze SID playback characteristics. On the c64u backend the audio comes from the Ultimate audio stream; on other backends it is recorded from the host default input device (microphone).",
260
261
  properties: {
261
262
  durationSeconds: numberSchema({
262
263
  description: "Capture duration in seconds (0.5 - 30).",
@@ -902,6 +903,10 @@ export const audioModule = defineToolModule({
902
903
  throw new ToolValidationError("Provide sidwave or cpg source", { path: "$.sidwave" });
903
904
  }
904
905
  const outputFormat = parsed.output ?? "prg";
906
+ const platform = ctx.platform?.id ?? getPlatformStatus().id;
907
+ if (outputFormat === "sid" && !parsed.dryRun && platform === "vice") {
908
+ throw new ToolUnsupportedPlatformError("sidplay_attachment (use output: prg or dryRun: true on VICE)", platform, ["c64u", "u2"]);
909
+ }
905
910
  ctx.logger.info("Compiling SIDWAVE composition", {
906
911
  output: outputFormat,
907
912
  dryRun: parsed.dryRun,
@@ -963,7 +968,7 @@ export const audioModule = defineToolModule({
963
968
  },
964
969
  {
965
970
  name: "record_and_analyze_audio",
966
- description: "Record audio from the default input device and analyze SID playback characteristics.",
971
+ description: "Record audio and analyze SID playback characteristics. On the c64u backend the audio comes from the Ultimate audio stream; on other backends it is recorded from the host default input device (microphone).",
967
972
  summary: "Captures PCM data, extracts notes, tempo, and deviation metrics for SID verification workflows.",
968
973
  inputSchema: recordAndAnalyzeArgsSchema.jsonSchema,
969
974
  relatedResources: ["c64://sound/sid/spec", "c64://sound/sid/file-format"],
@@ -981,7 +986,8 @@ export const audioModule = defineToolModule({
981
986
  try {
982
987
  const parsed = recordAndAnalyzeArgsSchema.parse(args ?? {});
983
988
  ctx.logger.info("Recording audio for analysis", { durationSeconds: parsed.durationSeconds });
984
- const result = await recordAndAnalyzeAudio({
989
+ const analyzer = resolveAnalyzer(ctx);
990
+ const result = await analyzer({
985
991
  durationSeconds: parsed.durationSeconds,
986
992
  expectedSidwave: normaliseSidwaveInput(parsed.expectedSidwave),
987
993
  });
@@ -1,6 +1,7 @@
1
1
  import { objectSchema, stringSchema, arraySchema, numberSchema, optionalSchema, booleanSchema } from "../schema.js";
2
2
  import { jsonResult } from "../responses.js";
3
- import { ToolError, ToolValidationError, toolErrorResult, unknownErrorResult } from "../errors.js";
3
+ import { ToolError, ToolValidationError, ToolUnsupportedPlatformError, 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, relative, sep, isAbsolute } from "node:path";
6
7
  const bundleRunArtifactsArgsSchema = objectSchema({
@@ -18,7 +19,7 @@ const bundleRunArtifactsArgsSchema = objectSchema({
18
19
  required: ["address", "length"],
19
20
  additionalProperties: false,
20
21
  }))),
21
- captureDebugReg: optionalSchema(booleanSchema({ description: "Capture debugreg state", default: true }), true),
22
+ captureDebugReg: optionalSchema(booleanSchema({ description: "Capture Ultimate debug registers (C64U/U64 only). Defaults to true on C64U/U64 and false on U2/VICE." })),
22
23
  },
23
24
  required: ["runId", "outputPath"],
24
25
  additionalProperties: false,
@@ -46,7 +47,11 @@ export const tools = [
46
47
  }
47
48
  const outputPath = resolvePath(String(parsed.outputPath));
48
49
  const captureScreen = parsed.captureScreen !== false;
49
- const captureDebugReg = parsed.captureDebugReg !== false;
50
+ const platform = ctx.platform?.id ?? getPlatformStatus().id;
51
+ const captureDebugReg = parsed.captureDebugReg ?? (platform === "c64u");
52
+ if (captureDebugReg && platform !== "c64u") {
53
+ throw new ToolUnsupportedPlatformError("debugreg_read", platform, ["c64u"]);
54
+ }
50
55
  const memoryRanges = (parsed.memoryRanges ?? []);
51
56
  await fs.mkdir(outputPath, { recursive: true });
52
57
  const runPath = containedPath(outputPath, runId);
@@ -2,7 +2,8 @@ import { compileSidwaveToPrg, compileSidwaveToSid } from "../../sidwaveCompiler.
2
2
  import { parseSidwave } from "../../sidwave.js";
3
3
  import { recordAndAnalyzeAudio } from "../../audio/record_and_analyze_audio.js";
4
4
  import { jsonResult } from "../responses.js";
5
- import { ToolExecutionError, toolErrorResult, unknownErrorResult } from "../errors.js";
5
+ import { ToolExecutionError, ToolUnsupportedPlatformError, toolErrorResult, unknownErrorResult } from "../errors.js";
6
+ import { getPlatformStatus } from "../../platform.js";
6
7
  import { sleep } from "./util.js";
7
8
  import { arraySchema, booleanSchema, numberSchema, objectSchema, optionalSchema, stringSchema } from "../schema.js";
8
9
  const DEFAULT_SILENCE_DURATION_SECONDS = 1.5;
@@ -68,7 +69,7 @@ const musicCompilePlayAnalyzeArgsSchema = objectSchema({
68
69
  properties: {
69
70
  sidwave: optionalSchema(stringSchema({ description: "SIDWAVE source in YAML or JSON format.", minLength: 1 })),
70
71
  cpg: optionalSchema(stringSchema({ description: "Legacy CPG input format.", minLength: 1 })),
71
- output: optionalSchema(stringSchema({ description: "Playback artifact format.", enum: ["prg", "sid"], default: "prg" }), "prg"),
72
+ output: optionalSchema(stringSchema({ description: "Playback artifact format. PRG works on all backends; SID attachment playback requires C64U/U64 or U2.", enum: ["prg", "sid"], default: "prg" }), "prg"),
72
73
  waitBeforeCaptureMs: optionalSchema(numberSchema({ description: "Delay between starting playback and beginning analysis capture (milliseconds).", minimum: 0, maximum: 5000 }), DEFAULT_PLAYBACK_WAIT_MS),
73
74
  analysisDurationSeconds: optionalSchema(numberSchema({ description: "Audio capture length in seconds.", minimum: 0.5, maximum: 20 }), DEFAULT_ANALYSIS_DURATION_SECONDS),
74
75
  expectedSidwave: optionalSchema(stringSchema({ description: "Optional expected SIDWAVE used to refine analysis comparisons.", minLength: 1 })),
@@ -91,11 +92,11 @@ const musicPlayPresetArgsSchema = objectSchema({
91
92
  }), "fuer_elise"),
92
93
  platforms: optionalSchema(arraySchema(stringSchema({
93
94
  description: "Backends to target in sequence.",
94
- enum: ["vice", "c64u"],
95
+ enum: ["vice", "c64u", "u2"],
95
96
  minLength: 1,
96
97
  }))),
97
98
  verify: optionalSchema(booleanSchema({
98
- description: "Capture and analyze audio on supported backends after playback starts.",
99
+ description: "Capture and analyze audio after playback starts: native streaming on C64U/U64, host audio input on U2/VICE (PortAudio required).",
99
100
  default: true,
100
101
  }), true),
101
102
  analysisDurationSeconds: optionalSchema(numberSchema({
@@ -147,7 +148,7 @@ function normalizeSidwaveInput(input) {
147
148
  return undefined;
148
149
  }
149
150
  function canonicalPresetBackends() {
150
- return ["vice", "c64u"];
151
+ return ["vice", "c64u", "u2"];
151
152
  }
152
153
  function uniquePresetBackends(backends) {
153
154
  return backends.filter((backend, index) => backends.indexOf(backend) === index);
@@ -285,30 +286,22 @@ export const tools = [
285
286
  }
286
287
  let verification = null;
287
288
  if (verify) {
288
- if (backend === "c64u") {
289
- if (waitBeforeCaptureMs > 0) {
290
- await sleep(waitBeforeCaptureMs);
291
- }
292
- const analysis = await analyzer({
293
- durationSeconds: analysisDurationSeconds,
294
- expectedSidwave: preset.sidwave,
295
- });
296
- const metrics = extractRmsMetrics((analysis?.analysis?.global_metrics ?? {}));
297
- verification = {
298
- mode: "audio-analysis",
299
- durationSeconds: analysis.analysis?.durationSeconds ?? analysisDurationSeconds,
300
- averageRms: metrics.average,
301
- maxRms: metrics.max,
302
- voices: analysis.analysis?.voices ?? [],
303
- analysis,
304
- };
305
- }
306
- else {
307
- verification = {
308
- mode: "playback-launch",
309
- note: "Audio analysis is only available on c64u; verified that playback launched on this backend.",
310
- };
289
+ if (waitBeforeCaptureMs > 0) {
290
+ await sleep(waitBeforeCaptureMs);
311
291
  }
292
+ const analysis = await analyzer({
293
+ durationSeconds: analysisDurationSeconds,
294
+ expectedSidwave: preset.sidwave,
295
+ });
296
+ const metrics = extractRmsMetrics((analysis?.analysis?.global_metrics ?? {}));
297
+ verification = {
298
+ mode: "audio-analysis",
299
+ durationSeconds: analysis.analysis?.durationSeconds ?? analysisDurationSeconds,
300
+ averageRms: metrics.average,
301
+ maxRms: metrics.max,
302
+ voices: analysis.analysis?.voices ?? [],
303
+ analysis,
304
+ };
312
305
  }
313
306
  await trySilenceSid(context);
314
307
  backendResult.verification = verification;
@@ -409,6 +402,10 @@ export const tools = [
409
402
  throw new ToolExecutionError("Provide sidwave or cpg source");
410
403
  }
411
404
  const format = (parsed.output ?? "prg");
405
+ const platform = context.platform?.id ?? getPlatformStatus().id;
406
+ if (format === "sid" && platform === "vice") {
407
+ throw new ToolUnsupportedPlatformError("sidplay_attachment (use output: prg on VICE)", platform, ["c64u", "u2"]);
408
+ }
412
409
  const waitBeforeCaptureMs = parsed.waitBeforeCaptureMs ?? DEFAULT_PLAYBACK_WAIT_MS;
413
410
  const analysisDurationSeconds = parsed.analysisDurationSeconds ?? DEFAULT_ANALYSIS_DURATION_SECONDS;
414
411
  const verifySilenceBefore = parsed.verifySilenceBefore ?? true;
@@ -1,6 +1,7 @@
1
1
  import { objectSchema, stringSchema, optionalSchema, booleanSchema } from "../schema.js";
2
2
  import { jsonResult } from "../responses.js";
3
- import { ToolError, ToolExecutionError, ToolValidationError, toolErrorResult, unknownErrorResult } from "../errors.js";
3
+ import { ToolError, ToolUnsupportedPlatformError, ToolExecutionError, ToolValidationError, toolErrorResult, unknownErrorResult } from "../errors.js";
4
+ import { getPlatformStatus } from "../../platform.js";
4
5
  import { promises as fs } from "node:fs";
5
6
  import { dirname, resolve as resolvePath } from "node:path";
6
7
  import { captureConfigSnapshot, validateSnapshotCategories } from "./configInventory.js";
@@ -10,7 +11,7 @@ const configSnapshotAndRestoreArgsSchema = objectSchema({
10
11
  properties: {
11
12
  action: stringSchema({ description: "One of: snapshot|restore|diff", enum: ["snapshot", "restore", "diff"] }),
12
13
  path: stringSchema({ description: "Snapshot file path", minLength: 1 }),
13
- applyToFlash: optionalSchema(booleanSchema({ description: "Save configuration to flash after restore", default: false }), false),
14
+ applyToFlash: optionalSchema(booleanSchema({ description: "Save configuration to flash after restore (C64U/U64 and U2 only)", default: false }), false),
14
15
  },
15
16
  required: ["action", "path"],
16
17
  additionalProperties: false,
@@ -26,9 +27,13 @@ export const tools = [
26
27
  try {
27
28
  const parsed = configSnapshotAndRestoreArgsSchema.parse(args ?? {});
28
29
  const action = parsed.action;
30
+ const platform = ctx.platform?.id ?? getPlatformStatus().id;
31
+ if (action === "restore" && parsed.applyToFlash && platform === "vice") {
32
+ throw new ToolUnsupportedPlatformError("config_save_to_flash", platform, ["c64u", "u2"]);
33
+ }
29
34
  const path = resolvePath(String(parsed.path));
30
- await fs.mkdir(dirname(path), { recursive: true });
31
35
  if (action === "snapshot") {
36
+ await fs.mkdir(dirname(path), { recursive: true });
32
37
  const [version, info, snapshotData] = await Promise.all([
33
38
  ctx.client.version(),
34
39
  ctx.client.info(),
@@ -66,7 +71,10 @@ export const tools = [
66
71
  throw new ToolExecutionError("Batch update failed", { details: normalizeErrorDetails(result.details) });
67
72
  }
68
73
  if (parsed.applyToFlash) {
69
- await ctx.client.configSaveToFlash();
74
+ const saved = await ctx.client.configSaveToFlash();
75
+ if (!saved.success) {
76
+ throw new ToolExecutionError("Configuration restored, but saving to flash failed", { details: normalizeErrorDetails(saved.details) });
77
+ }
70
78
  }
71
79
  return jsonResult({ restored: true, categories: Object.keys(payload).length }, { success: true });
72
80
  }
@@ -96,7 +96,10 @@ function normalizeSnapshotValue(value, entry) {
96
96
  if (value
97
97
  && typeof value === "object"
98
98
  && !Array.isArray(value)
99
- && Object.keys(value).length === 1
99
+ && (Object.keys(value).length === 1
100
+ || (value.category === entry.category
101
+ && value.item === entry.item
102
+ && ["int", "string"].includes(String(value.type))))
100
103
  && Object.prototype.hasOwnProperty.call(value, "value")) {
101
104
  return value.value;
102
105
  }
@@ -1,6 +1,7 @@
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";
@@ -56,7 +57,7 @@ function shouldUseVisibleViceGreetingFastPath(rawArgs, requestedBackends) {
56
57
  && !hasOwnProperty(args, "outputPath");
57
58
  }
58
59
  function canonicalGreetingBackends() {
59
- return ["vice", "c64u"];
60
+ return ["vice", "c64u", "u2"];
60
61
  }
61
62
  function uniqueGreetingBackends(backends) {
62
63
  return backends.filter((backend, index) => backends.indexOf(backend) === index);
@@ -226,9 +227,9 @@ const crossPlatformGreetingArgsSchema = objectSchema({
226
227
  properties: {
227
228
  platforms: optionalSchema(arraySchema(stringSchema({
228
229
  description: "Backends to target in sequence.",
229
- enum: ["vice", "c64u"],
230
+ enum: ["vice", "c64u", "u2"],
230
231
  minLength: 1,
231
- })), ["vice", "c64u"]),
232
+ }))),
232
233
  messageTemplate: optionalSchema(stringSchema({
233
234
  description: "Greeting template. Use {PLATFORM}/{BACKEND} for uppercase or {platform}/{backend} for lowercase substitution.",
234
235
  minLength: 1,
@@ -238,7 +239,7 @@ const crossPlatformGreetingArgsSchema = objectSchema({
238
239
  default: true,
239
240
  }), true),
240
241
  captureScreenshot: optionalSchema(booleanSchema({
241
- description: "Capture a framebuffer screenshot and save it as a PNG per backend.",
242
+ description: "Capture a framebuffer screenshot on C64U/U64 or VICE. U2 skips screenshots by default; explicitly requesting one on U2 fails before execution.",
242
243
  default: true,
243
244
  }), true),
244
245
  outputPath: optionalSchema(stringSchema({
@@ -311,11 +312,14 @@ export const tools = [
311
312
  details: { availableBackends },
312
313
  });
313
314
  }
315
+ if (args?.captureScreenshot === true && requestedBackends.includes("u2")) {
316
+ throw new ToolUnsupportedPlatformError("capture_frame", "u2", ["c64u", "vice"]);
317
+ }
314
318
  const visibleViceFastPath = shouldUseVisibleViceGreetingFastPath(args ?? {}, requestedBackends);
315
319
  const verify = visibleViceFastPath ? false : parsed.verify !== false;
316
320
  const captureScreenshot = visibleViceFastPath ? false : parsed.captureScreenshot !== false;
317
321
  const restoreActiveBackend = parsed.restoreActiveBackend !== false;
318
- const outputPath = captureScreenshot || parsed.outputPath
322
+ const outputPath = (captureScreenshot && requestedBackends.some((backend) => backend !== "u2")) || parsed.outputPath
319
323
  ? resolvePath(String(parsed.outputPath ?? joinPath(process.cwd(), "artifacts", "greetings", `run_${Date.now()}`)))
320
324
  : undefined;
321
325
  const timeoutMs = parsed.timeoutMs ?? 1500;
@@ -330,6 +334,7 @@ export const tools = [
330
334
  }
331
335
  try {
332
336
  for (const backend of requestedBackends) {
337
+ const backendCaptureScreenshot = captureScreenshot && backend !== "u2";
333
338
  const expectedText = applyGreetingTemplate(template, backend);
334
339
  const program = buildGreetingProgram(expectedText);
335
340
  const timeline = [];
@@ -358,7 +363,7 @@ export const tools = [
358
363
  ctx.setPlatform(backend);
359
364
  });
360
365
  let prepareCapturePromise;
361
- if (captureScreenshot && backend === "c64u" && typeof client.prepareVideoCapture === "function") {
366
+ if (backendCaptureScreenshot && backend === "c64u" && typeof client.prepareVideoCapture === "function") {
362
367
  prepareCapturePromise = timedStep("prepare_capture", async () => {
363
368
  await client.prepareVideoCapture?.({ keepAliveMs: Math.max(500, timeoutMs) });
364
369
  preparedCaptureSession = true;
@@ -410,7 +415,7 @@ export const tools = [
410
415
  let screenshotPath;
411
416
  let screenshotAnalysis;
412
417
  let screenshotError;
413
- if (captureScreenshot) {
418
+ if (backendCaptureScreenshot) {
414
419
  try {
415
420
  const capture = await timedStep("capture_screenshot", () => client.captureFrames({
416
421
  count: 1,
@@ -447,7 +452,7 @@ export const tools = [
447
452
  }
448
453
  backendResult.verification = {
449
454
  screenContainsExpectedText: verify ? screenMatched : undefined,
450
- screenshotCaptured: captureScreenshot ? !screenshotError : undefined,
455
+ screenshotCaptured: backendCaptureScreenshot ? !screenshotError : undefined,
451
456
  screenshotAnalysis,
452
457
  };
453
458
  if (screenshotPath) {
@@ -458,7 +463,7 @@ export const tools = [
458
463
  }
459
464
  backendResult.success = runResult.success
460
465
  && (!verify || screenMatched)
461
- && (!captureScreenshot || !screenshotError);
466
+ && (!backendCaptureScreenshot || !screenshotError);
462
467
  backendResult.promptToVisibleLatencyMs = timeline
463
468
  .filter((entry) => entry.ok === true && (entry.name === "switch_backend" || entry.name === "render_greeting_screen" || entry.name === "verify_screen"))
464
469
  .reduce((total, entry) => total + Number(entry.latencyMs ?? 0), 0);
@@ -642,6 +647,10 @@ export const tools = [
642
647
  try {
643
648
  const parsed = batchRunWithAssertionsArgsSchema.parse(args ?? {});
644
649
  const programs = parsed.programs;
650
+ const platform = ctx.platform?.id ?? getPlatformStatus().id;
651
+ if (platform === "vice" && programs.some((program) => program.path.toLowerCase().endsWith(".crt"))) {
652
+ throw new ToolUnsupportedPlatformError("run_crt", platform, ["c64u", "u2"]);
653
+ }
645
654
  const continueOnError = parsed.continueOnError ?? false;
646
655
  const durationMs = parsed.durationMs ?? 2000;
647
656
  const resetDelayMs = parsed.resetDelayMs ?? 100;
@@ -659,10 +668,14 @@ export const tools = [
659
668
  // Run program
660
669
  const ext = program.path.toLowerCase().split(".").pop();
661
670
  if (ext === "prg") {
662
- await ctx.client.runPrgFile(program.path);
671
+ const run = await ctx.client.runPrgFile(program.path);
672
+ if (!run.success)
673
+ throw new ToolExecutionError("PRG playback failed", { details: run.details });
663
674
  }
664
675
  else if (ext === "crt") {
665
- await ctx.client.runCrtFile(program.path);
676
+ const run = await ctx.client.runCrtFile(program.path);
677
+ if (!run.success)
678
+ throw new ToolExecutionError("CRT playback failed", { details: run.details });
666
679
  }
667
680
  await sleep(durationMs);
668
681
  // Check assertions
@@ -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",
@@ -131,7 +131,7 @@ const soundOperations = [
131
131
  },
132
132
  {
133
133
  op: "record_analyze",
134
- schema: extendSchemaWithOp("record_analyze", ensureDescriptor(audioDescriptorIndex, "record_and_analyze_audio").inputSchema, { description: "Record audio for a fixed duration and return SID analysis metrics." }),
134
+ schema: extendSchemaWithOp("record_analyze", ensureDescriptor(audioDescriptorIndex, "record_and_analyze_audio").inputSchema, { description: "Record audio for a fixed duration and return SID analysis metrics. Uses the Ultimate audio stream on c64u and the host microphone on other backends." }),
135
135
  handler: async (rawArgs, ctx) => invokeModuleTool(audioModule, "record_and_analyze_audio", rawArgs, ctx),
136
136
  },
137
137
  ];
@@ -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
  },
@@ -32,6 +32,15 @@ function toRecord(details) {
32
32
  }
33
33
  return undefined;
34
34
  }
35
+ function withFailureReason(message, details) {
36
+ const record = toRecord(details);
37
+ const body = toRecord(record?.data) ?? toRecord(record?.response) ?? record;
38
+ const errors = body?.errors;
39
+ const reason = Array.isArray(errors) && errors.length > 0
40
+ ? errors.map(String).join("; ")
41
+ : record?.message ?? record?.reason ?? (typeof details === "string" ? details : undefined);
42
+ return typeof reason === "string" && reason.length > 0 ? `${message}: ${reason}` : message;
43
+ }
35
44
  export const streamingModule = defineToolModule({
36
45
  domain: "streaming",
37
46
  summary: "Long-running or streaming workflows such as audio capture or SID playback monitoring.",
@@ -71,7 +80,7 @@ export const streamingModule = defineToolModule({
71
80
  });
72
81
  const result = await ctx.client.streamStart(parsed.stream, parsed.target);
73
82
  if (!result.success) {
74
- throw new ToolExecutionError("C64 firmware reported failure while starting stream", {
83
+ throw new ToolExecutionError(withFailureReason("C64 firmware reported failure while starting stream", result.details), {
75
84
  details: toRecord(result.details),
76
85
  });
77
86
  }
@@ -111,7 +120,7 @@ export const streamingModule = defineToolModule({
111
120
  });
112
121
  const result = await ctx.client.streamStop(parsed.stream);
113
122
  if (!result.success) {
114
- throw new ToolExecutionError("C64 firmware reported failure while stopping stream", {
123
+ throw new ToolExecutionError(withFailureReason("C64 firmware reported failure while stopping stream", result.details), {
115
124
  details: toRecord(result.details),
116
125
  });
117
126
  }
@@ -53,7 +53,7 @@
53
53
  "boolean",
54
54
  "null"
55
55
  ],
56
- "description": "When true, save the restored configuration to flash immediately.",
56
+ "description": "When true, save the restored configuration to flash immediately (C64U/U64 and U2 only).",
57
57
  "default": false
58
58
  },
59
59
  "root": {
@@ -452,7 +452,7 @@
452
452
  "boolean",
453
453
  "null"
454
454
  ],
455
- "description": "When true, save the restored configuration to flash immediately.",
455
+ "description": "When true, save the restored configuration to flash immediately (C64U/U64 and U2 only).",
456
456
  "default": false
457
457
  },
458
458
  "op": {
@@ -185,8 +185,7 @@
185
185
  "boolean",
186
186
  "null"
187
187
  ],
188
- "description": "Capture debugreg state",
189
- "default": true
188
+ "description": "Capture Ultimate debug registers (C64U/U64 only). Defaults to true on C64U/U64 and false on U2/VICE."
190
189
  },
191
190
  "platforms": {
192
191
  "type": [
@@ -199,13 +198,10 @@
199
198
  "minLength": 1,
200
199
  "enum": [
201
200
  "vice",
202
- "c64u"
201
+ "c64u",
202
+ "u2"
203
203
  ]
204
- },
205
- "default": [
206
- "vice",
207
- "c64u"
208
- ]
204
+ }
209
205
  },
210
206
  "messageTemplate": {
211
207
  "type": [
@@ -221,7 +217,7 @@
221
217
  "boolean",
222
218
  "null"
223
219
  ],
224
- "description": "Capture a framebuffer screenshot and save it as a PNG per backend.",
220
+ "description": "Capture a framebuffer screenshot on C64U/U64 or VICE. U2 skips screenshots by default; explicitly requesting one on U2 fails before execution.",
225
221
  "default": true
226
222
  },
227
223
  "restoreActiveBackend": {
@@ -623,8 +619,7 @@
623
619
  "boolean",
624
620
  "null"
625
621
  ],
626
- "description": "Capture debugreg state",
627
- "default": true
622
+ "description": "Capture Ultimate debug registers (C64U/U64 only). Defaults to true on C64U/U64 and false on U2/VICE."
628
623
  },
629
624
  "op": {
630
625
  "const": "bundle_run"
@@ -667,13 +662,10 @@
667
662
  "minLength": 1,
668
663
  "enum": [
669
664
  "vice",
670
- "c64u"
665
+ "c64u",
666
+ "u2"
671
667
  ]
672
- },
673
- "default": [
674
- "vice",
675
- "c64u"
676
- ]
668
+ }
677
669
  },
678
670
  "messageTemplate": {
679
671
  "type": [
@@ -697,7 +689,7 @@
697
689
  "boolean",
698
690
  "null"
699
691
  ],
700
- "description": "Capture a framebuffer screenshot and save it as a PNG per backend.",
692
+ "description": "Capture a framebuffer screenshot on C64U/U64 or VICE. U2 skips screenshots by default; explicitly requesting one on U2 fails before execution.",
701
693
  "default": true
702
694
  },
703
695
  "outputPath": {