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.
- package/README.md +12 -12
- package/data/embeddings_basic.json +1 -1
- package/data/embeddings_hardware.json +1 -1
- package/data/embeddings_mixed.json +1 -1
- package/data/embeddings_other.json +1 -1
- package/data/graphics/character-set.csv +64 -64
- package/dist/.tsbuildinfo +1 -1
- package/dist/c64Client.js +68 -61
- package/dist/config.js +75 -38
- package/dist/device.js +154 -120
- package/dist/knowledge.js +2 -1
- package/dist/petscii.js +18 -15
- package/dist/petsciiArt.js +40 -14
- package/dist/rag/indexer.js +2 -1
- package/dist/sidwave.js +1 -1
- package/dist/sidwaveCompiler.js +3 -9
- package/dist/tools/audio.js +34 -8
- package/dist/tools/debug.js +9 -9
- package/dist/tools/errors.js +12 -1
- package/dist/tools/hex.js +9 -0
- package/dist/tools/input.js +13 -7
- package/dist/tools/memory.js +5 -4
- package/dist/tools/meta/artifacts.js +8 -3
- package/dist/tools/meta/audio.js +25 -28
- package/dist/tools/meta/background.js +6 -1
- package/dist/tools/meta/config.js +12 -4
- package/dist/tools/meta/configInventory.js +4 -1
- package/dist/tools/meta/filesystem.js +37 -10
- package/dist/tools/meta/graphics.js +10 -15
- package/dist/tools/meta/memory.js +12 -18
- package/dist/tools/meta/program.js +44 -16
- package/dist/tools/pollValidator.js +30 -2
- package/dist/tools/programRunners.js +5 -4
- 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/dist/tools/translation/assembler.js +150 -47
- package/dist/tools/translation/basicTokenizer.js +39 -9
- package/dist/tools/types.js +11 -4
- package/dist/vicBitmap.js +16 -0
- package/dist/vice/mockServer.js +7 -3
- package/dist/vice/viceClient.js +16 -6
- 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 +9 -7
- package/mcp/server.json +1 -1
- package/mcp/tools.json +58 -43
- package/mcp.json +1 -1
- package/package.json +1 -1
- package/scripts/mockC64Server.mjs +98 -19
- 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
|
-
|
|
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
|
-
|
|
35
|
-
let
|
|
36
|
-
if (lower
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
else if (lower
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
}))
|
|
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
|
|
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 (
|
|
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 (
|
|
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:
|
|
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
|
-
&& (!
|
|
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
|
-
|
|
687
|
-
const
|
|
688
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
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
|
|
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: "
|
|
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:
|
|
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
|
},
|