c64bridge 1.0.3 → 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.
- package/README.md +12 -12
- package/dist/.tsbuildinfo +1 -1
- package/dist/c64Client.js +3 -0
- package/dist/device.js +32 -14
- package/dist/tools/audio.js +7 -2
- package/dist/tools/meta/artifacts.js +8 -3
- package/dist/tools/meta/audio.js +25 -28
- package/dist/tools/meta/config.js +12 -4
- package/dist/tools/meta/configInventory.js +4 -1
- package/dist/tools/meta/program.js +25 -12
- package/dist/tools/registry/batch.js +4 -2
- package/dist/tools/registry/config.js +1 -1
- package/dist/tools/registry/disk.js +5 -0
- package/dist/tools/registry/extract.js +3 -0
- package/dist/tools/registry/graphics.js +1 -0
- package/dist/tools/registry/program.js +1 -1
- package/dist/tools/registry/sound.js +3 -3
- package/dist/tools/registry/system.js +3 -3
- package/dist/tools/storage.js +2 -2
- package/mcp/schemas/c64_config.schema.json +2 -2
- package/mcp/schemas/c64_program.schema.json +10 -18
- package/mcp/schemas/c64_sound.schema.json +8 -6
- package/mcp/schemas/c64_system.schema.json +3 -3
- package/mcp/server.json +1 -1
- package/mcp/tools.json +52 -39
- package/mcp.json +1 -1
- package/package.json +1 -1
- package/server.json +2 -2
package/dist/c64Client.js
CHANGED
|
@@ -1141,6 +1141,9 @@ export class C64Client {
|
|
|
1141
1141
|
if (facade.type === "vice") {
|
|
1142
1142
|
return this.captureViceFrames(requestedCount);
|
|
1143
1143
|
}
|
|
1144
|
+
if (facade.type !== "c64u") {
|
|
1145
|
+
throw new Error("Video frame capture is only available on C64U/U64 and VICE; U2 has no video stream");
|
|
1146
|
+
}
|
|
1144
1147
|
return this.captureC64uVideoFrames(facade, requestedCount, {
|
|
1145
1148
|
reuseSession: options?.reuseSession === true,
|
|
1146
1149
|
keepAliveMs: Math.max(0, Math.trunc(options?.keepAliveMs ?? 0)),
|
package/dist/device.js
CHANGED
|
@@ -632,10 +632,6 @@ export class ViceBackend {
|
|
|
632
632
|
}
|
|
633
633
|
async powerCycle() { return this.nuclearReset(); }
|
|
634
634
|
async nuclearReset() {
|
|
635
|
-
const poweroffResult = await this.poweroff();
|
|
636
|
-
if (!poweroffResult.success) {
|
|
637
|
-
return poweroffResult;
|
|
638
|
-
}
|
|
639
635
|
if (!this.manageProcess) {
|
|
640
636
|
return {
|
|
641
637
|
success: false,
|
|
@@ -645,6 +641,10 @@ export class ViceBackend {
|
|
|
645
641
|
},
|
|
646
642
|
};
|
|
647
643
|
}
|
|
644
|
+
const poweroffResult = await this.poweroff();
|
|
645
|
+
if (!poweroffResult.success) {
|
|
646
|
+
return poweroffResult;
|
|
647
|
+
}
|
|
648
648
|
try {
|
|
649
649
|
await this.ensureProcess();
|
|
650
650
|
return { success: true };
|
|
@@ -694,7 +694,10 @@ export class ViceBackend {
|
|
|
694
694
|
return drives;
|
|
695
695
|
});
|
|
696
696
|
}
|
|
697
|
-
async driveMount(drive, imagePath) {
|
|
697
|
+
async driveMount(drive, imagePath, options) {
|
|
698
|
+
if (options?.type !== undefined || options?.mode !== undefined) {
|
|
699
|
+
throw unsupported("driveMount type/mode overrides; omit these options on VICE");
|
|
700
|
+
}
|
|
698
701
|
const n = parseDriveNumber(drive);
|
|
699
702
|
await this.withClient(async (client) => {
|
|
700
703
|
await client.resourceSet(`Drive${n}CPUEnabled`, 1);
|
|
@@ -760,8 +763,18 @@ export class ViceBackend {
|
|
|
760
763
|
};
|
|
761
764
|
}
|
|
762
765
|
async configGet(_category, item) {
|
|
763
|
-
if (!item)
|
|
764
|
-
|
|
766
|
+
if (!item) {
|
|
767
|
+
if (_category !== "VICE")
|
|
768
|
+
throw new Error(`Unknown VICE configuration category '${_category}'`);
|
|
769
|
+
const inventory = await this.configsList();
|
|
770
|
+
return this.withClient(async (client) => {
|
|
771
|
+
const values = {};
|
|
772
|
+
for (const name of inventory.categories[0].items) {
|
|
773
|
+
values[name] = (await client.resourceGet(name)).value;
|
|
774
|
+
}
|
|
775
|
+
return values;
|
|
776
|
+
});
|
|
777
|
+
}
|
|
765
778
|
return await this.withClient(async (client) => {
|
|
766
779
|
const res = await client.resourceGet(item);
|
|
767
780
|
return { category: _category, item, value: res.value, type: res.type };
|
|
@@ -771,9 +784,7 @@ export class ViceBackend {
|
|
|
771
784
|
let parsed = value;
|
|
772
785
|
await this.withClient(async (client) => {
|
|
773
786
|
const current = await client.resourceGet(item);
|
|
774
|
-
parsed = current.type
|
|
775
|
-
if (current.type === "int" && !Number.isFinite(parsed))
|
|
776
|
-
throw new Error(`VICE resource '${item}' requires an integer value`);
|
|
787
|
+
parsed = parseViceConfigValue(value, current.type, item);
|
|
777
788
|
await client.resourceSet(item, parsed);
|
|
778
789
|
});
|
|
779
790
|
return { success: true, details: { item, value: parsed } };
|
|
@@ -784,11 +795,8 @@ export class ViceBackend {
|
|
|
784
795
|
for (const [category, items] of Object.entries(payload)) {
|
|
785
796
|
for (const [item, value] of Object.entries(items)) {
|
|
786
797
|
try {
|
|
787
|
-
const str = String(value ?? "");
|
|
788
798
|
const current = await client.resourceGet(item);
|
|
789
|
-
const parsed = current.type
|
|
790
|
-
if (current.type === "int" && !Number.isFinite(parsed))
|
|
791
|
-
throw new Error(`VICE resource '${item}' requires an integer value`);
|
|
799
|
+
const parsed = parseViceConfigValue(value, current.type, item);
|
|
792
800
|
await client.resourceSet(item, parsed);
|
|
793
801
|
results.push({ item: `${category}/${item}`, success: true });
|
|
794
802
|
}
|
|
@@ -814,6 +822,16 @@ export class ViceBackend {
|
|
|
814
822
|
}
|
|
815
823
|
ViceBackend.supervisors = new Map();
|
|
816
824
|
ViceBackend.cleanupRegistered = false;
|
|
825
|
+
function parseViceConfigValue(value, type, item) {
|
|
826
|
+
if (type !== "int")
|
|
827
|
+
return String(value ?? "");
|
|
828
|
+
const parsed = value === true || value === "true" ? 1
|
|
829
|
+
: value === false || value === "false" ? 0 : Number(value);
|
|
830
|
+
if (!Number.isInteger(parsed) || parsed < -0x80000000 || parsed > 0x7fffffff) {
|
|
831
|
+
throw new Error(`VICE resource '${item}' requires a 32-bit integer value`);
|
|
832
|
+
}
|
|
833
|
+
return parsed;
|
|
834
|
+
}
|
|
817
835
|
function unsupported(name) { const err = new Error(`Operation '${name}' is not supported by the VICE backend in phase one`); err.code = "UNSUPPORTED"; return err; }
|
|
818
836
|
/** Ultimate file APIs treat slash as a path separator. Encode each name once,
|
|
819
837
|
* preserving leading and nested separators for firmware routing. */
|
package/dist/tools/audio.js
CHANGED
|
@@ -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"),
|
|
@@ -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,
|
|
@@ -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
|
|
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
|
|
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);
|
package/dist/tools/meta/audio.js
CHANGED
|
@@ -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
|
|
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 (
|
|
289
|
-
|
|
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
|
-
}))
|
|
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
|
|
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 (
|
|
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 (
|
|
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:
|
|
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
|
-
&& (!
|
|
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:
|
|
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"]
|
|
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: "
|
|
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",
|
package/dist/tools/storage.js
CHANGED
|
@@ -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
|
},
|
|
@@ -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": {
|