c64bridge 1.0.4 → 1.0.6
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 +2 -2
- 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/data/sound/sidwave.md +113 -36
- package/data/sound/sidwave.schema.json +138 -28
- package/dist/.tsbuildinfo +1 -1
- package/dist/c64Client.js +144 -150
- package/dist/config.js +93 -39
- package/dist/device.js +252 -130
- package/dist/diagnostics.js +10 -2
- package/dist/knowledge.js +2 -1
- package/dist/mcp-server.js +12 -4
- package/dist/petscii.js +18 -15
- package/dist/petsciiArt.js +40 -14
- package/dist/rag/indexer.js +2 -1
- package/dist/sidwave.js +453 -61
- package/dist/sidwaveCompiler.js +1424 -275
- package/dist/tools/audio.js +97 -18
- package/dist/tools/debug.js +9 -9
- package/dist/tools/developer.js +7 -7
- package/dist/tools/errors.js +19 -1
- package/dist/tools/graphics.js +4 -4
- package/dist/tools/hex.js +9 -0
- package/dist/tools/input.js +21 -18
- package/dist/tools/machineControl.js +6 -6
- package/dist/tools/memory.js +11 -10
- package/dist/tools/meta/audio.js +8 -2
- package/dist/tools/meta/background.js +6 -1
- package/dist/tools/meta/filesystem.js +37 -10
- package/dist/tools/meta/graphics.js +11 -16
- package/dist/tools/meta/memory.js +16 -22
- package/dist/tools/meta/program.js +28 -9
- package/dist/tools/pollValidator.js +30 -2
- package/dist/tools/printer.js +4 -4
- package/dist/tools/programRunners.js +10 -9
- package/dist/tools/registry/disk.js +7 -5
- package/dist/tools/storage.js +21 -17
- package/dist/tools/streaming.js +2 -2
- package/dist/tools/translation/assembler.js +192 -57
- package/dist/tools/translation/basicTokenizer.js +44 -10
- package/dist/tools/translation/opcodeInventory.js +1 -1
- package/dist/tools/types.js +11 -4
- package/dist/tools/vice.js +1 -1
- package/dist/vicBitmap.js +16 -0
- package/dist/vice/mockServer.js +99 -8
- package/dist/vice/readiness.js +4 -9
- package/dist/vice/viceClient.js +34 -6
- package/mcp/schemas/c64_disk.schema.json +2 -2
- package/mcp/schemas/c64_input.schema.json +3 -3
- package/mcp/schemas/c64_program.schema.json +2 -2
- package/mcp/schemas/c64_system.schema.json +6 -4
- package/mcp/schemas/c64_vice.schema.json +3 -3
- package/mcp/server.json +1 -1
- package/mcp/tools.json +20 -14
- package/mcp.json +1 -1
- package/package.json +1 -1
- package/scripts/mockC64Server.mjs +113 -20
- package/scripts/run-coverage.mjs +31 -1
- package/scripts/update-readme.ts +3 -2
- package/server.json +2 -2
package/dist/tools/memory.js
CHANGED
|
@@ -8,6 +8,7 @@ import { ToolError, ToolExecutionError, ToolValidationError, toolErrorResult, un
|
|
|
8
8
|
import { disassemble, formatDisassembly } from "./translation/disassembler.js";
|
|
9
9
|
import { getViceSymbols } from "./translation/symbolRegistry.js";
|
|
10
10
|
import { resolveAddressSymbol } from "../knowledge.js";
|
|
11
|
+
import { stripHexByteSeparators } from "./hex.js";
|
|
11
12
|
function toRecord(details) {
|
|
12
13
|
if (details && typeof details === "object") {
|
|
13
14
|
return details;
|
|
@@ -24,9 +25,7 @@ function normaliseFailure(details) {
|
|
|
24
25
|
return { value: details };
|
|
25
26
|
}
|
|
26
27
|
function cleanHex(input) {
|
|
27
|
-
|
|
28
|
-
const withoutPrefix = trimmed.startsWith("$") ? trimmed.slice(1) : trimmed;
|
|
29
|
-
return withoutPrefix.replace(/[\s_]/g, "").toUpperCase();
|
|
28
|
+
return stripHexByteSeparators(input).toUpperCase();
|
|
30
29
|
}
|
|
31
30
|
function parseHexInternal(value, path) {
|
|
32
31
|
const cleaned = cleanHex(value);
|
|
@@ -187,7 +186,7 @@ async function executeReadMemory(rawArgs, ctx) {
|
|
|
187
186
|
ctx.logger.info("Reading C64 memory", { address: parsed.address, length: parsed.length });
|
|
188
187
|
const result = await ctx.client.readMemory(parsed.address, String(parsed.length));
|
|
189
188
|
if (!result.success) {
|
|
190
|
-
throw new ToolExecutionError("C64
|
|
189
|
+
throw new ToolExecutionError("C64 backend reported failure while reading memory", {
|
|
191
190
|
details: normaliseFailure(result.details),
|
|
192
191
|
});
|
|
193
192
|
}
|
|
@@ -201,7 +200,9 @@ async function executeReadMemory(rawArgs, ctx) {
|
|
|
201
200
|
hexData: result.data ?? null,
|
|
202
201
|
details: detailRecord,
|
|
203
202
|
};
|
|
204
|
-
|
|
203
|
+
// Many clients show only the text content, so it carries the bytes too.
|
|
204
|
+
const hexSummary = typeof result.data === "string" && result.data.length > 0 ? `\n${result.data}` : "";
|
|
205
|
+
const base = textResult(`Read ${resolvedLength} bytes starting at ${resolvedAddress}.${hexSummary}`, data);
|
|
205
206
|
return {
|
|
206
207
|
...base,
|
|
207
208
|
structuredContent: {
|
|
@@ -228,7 +229,7 @@ async function executeWriteMemory(rawArgs, ctx) {
|
|
|
228
229
|
if (!shouldVerify) {
|
|
229
230
|
const result = await ctx.client.writeMemory(parsed.address, writeInfo.canonical);
|
|
230
231
|
if (!result.success) {
|
|
231
|
-
throw new ToolExecutionError("C64
|
|
232
|
+
throw new ToolExecutionError("C64 backend reported failure while writing memory", {
|
|
232
233
|
details: normaliseFailure(result.details),
|
|
233
234
|
});
|
|
234
235
|
}
|
|
@@ -250,7 +251,7 @@ async function executeWriteMemory(rawArgs, ctx) {
|
|
|
250
251
|
if (canPause) {
|
|
251
252
|
const pauseResult = await ctx.client.pause(pinnedFacade);
|
|
252
253
|
if (!pauseResult.success) {
|
|
253
|
-
throw new ToolExecutionError("C64
|
|
254
|
+
throw new ToolExecutionError("C64 backend reported failure while pausing", {
|
|
254
255
|
details: normaliseFailure(pauseResult.details),
|
|
255
256
|
});
|
|
256
257
|
}
|
|
@@ -261,7 +262,7 @@ async function executeWriteMemory(rawArgs, ctx) {
|
|
|
261
262
|
const readLength = Math.max(1, Math.max(writeInfo.bytes.length, expectedBytes.length));
|
|
262
263
|
const preRead = await ctx.client.readMemory(parsed.address, String(readLength), pinnedFacade);
|
|
263
264
|
if (!preRead.success) {
|
|
264
|
-
throw new ToolExecutionError("C64
|
|
265
|
+
throw new ToolExecutionError("C64 backend reported failure while reading memory", {
|
|
265
266
|
details: normaliseFailure(preRead.details),
|
|
266
267
|
});
|
|
267
268
|
}
|
|
@@ -289,13 +290,13 @@ async function executeWriteMemory(rawArgs, ctx) {
|
|
|
289
290
|
}
|
|
290
291
|
const writeResult = await ctx.client.writeMemory(parsed.address, writeInfo.canonical, pinnedFacade);
|
|
291
292
|
if (!writeResult.success) {
|
|
292
|
-
throw new ToolExecutionError("C64
|
|
293
|
+
throw new ToolExecutionError("C64 backend reported failure while writing memory", {
|
|
293
294
|
details: normaliseFailure(writeResult.details),
|
|
294
295
|
});
|
|
295
296
|
}
|
|
296
297
|
const postRead = await ctx.client.readMemory(parsed.address, String(Math.max(1, writeInfo.bytes.length)), pinnedFacade);
|
|
297
298
|
if (!postRead.success) {
|
|
298
|
-
throw new ToolExecutionError("C64
|
|
299
|
+
throw new ToolExecutionError("C64 backend reported failure while reading back memory", {
|
|
299
300
|
details: normaliseFailure(postRead.details),
|
|
300
301
|
});
|
|
301
302
|
}
|
package/dist/tools/meta/audio.js
CHANGED
|
@@ -267,7 +267,7 @@ export const tools = [
|
|
|
267
267
|
const results = [];
|
|
268
268
|
let restoreError;
|
|
269
269
|
try {
|
|
270
|
-
for (const backend of requestedBackends) {
|
|
270
|
+
for (const [backendIndex, backend] of requestedBackends.entries()) {
|
|
271
271
|
context.client.switchBackend(backend);
|
|
272
272
|
context.setPlatform(backend);
|
|
273
273
|
await trySilenceSid(context);
|
|
@@ -303,7 +303,11 @@ export const tools = [
|
|
|
303
303
|
analysis,
|
|
304
304
|
};
|
|
305
305
|
}
|
|
306
|
-
|
|
306
|
+
// Stop this backend's playback only before moving on to the next one;
|
|
307
|
+
// the last backend keeps playing so the preset can be heard in full.
|
|
308
|
+
if (backendIndex < requestedBackends.length - 1) {
|
|
309
|
+
await trySilenceSid(context);
|
|
310
|
+
}
|
|
307
311
|
backendResult.verification = verification;
|
|
308
312
|
backendResult.success = true;
|
|
309
313
|
results.push(backendResult);
|
|
@@ -493,6 +497,8 @@ export const tools = [
|
|
|
493
497
|
entryAddress: compiled.entryAddress,
|
|
494
498
|
prgBytes: compiled.prg.length,
|
|
495
499
|
title: document.song?.title ?? null,
|
|
500
|
+
basicFreeBytes: compiled.basicFreeBytes,
|
|
501
|
+
warnings: compiled.warnings,
|
|
496
502
|
},
|
|
497
503
|
playback: {
|
|
498
504
|
method: playbackMethod,
|
|
@@ -231,7 +231,12 @@ const noArgsSchema = objectSchema({ description: "No arguments", properties: {},
|
|
|
231
231
|
const startBackgroundTaskArgsSchema = objectSchema({
|
|
232
232
|
description: "Start a named background task that invokes a simple operation at fixed intervals.",
|
|
233
233
|
properties: {
|
|
234
|
-
name
|
|
234
|
+
// The name becomes part of the task folder path, so it must be a single safe path segment.
|
|
235
|
+
name: stringSchema({
|
|
236
|
+
description: "Unique task name (letters, digits, spaces, '_', '-', '.'; must not start with '.' or a space).",
|
|
237
|
+
minLength: 1,
|
|
238
|
+
pattern: /^[A-Za-z0-9_][A-Za-z0-9_. -]*$/,
|
|
239
|
+
}),
|
|
235
240
|
operation: stringSchema({ description: "Operation name (e.g., read, read_screen)", minLength: 1 }),
|
|
236
241
|
arguments: optionalSchema(objectSchema({ description: "Operation-specific arguments", properties: {}, additionalProperties: true }), {}),
|
|
237
242
|
intervalMs: optionalSchema(numberSchema({ description: "Interval milliseconds", integer: true, minimum: 1, default: 1000 }), 1000),
|
|
@@ -45,6 +45,28 @@ const filesystemStatsArgsSchema = objectSchema({
|
|
|
45
45
|
required: [],
|
|
46
46
|
additionalProperties: false,
|
|
47
47
|
});
|
|
48
|
+
/**
|
|
49
|
+
* Find a drive in drivesList() output. Ultimate firmware lists drives as a/b
|
|
50
|
+
* with a configurable bus_id, so "drive8" or "8" is matched by bus number.
|
|
51
|
+
*/
|
|
52
|
+
function findListedDrive(drives, drive) {
|
|
53
|
+
const wanted = drive.trim().toLowerCase();
|
|
54
|
+
const exact = drives.find((d) => String(d?.id ?? "").toLowerCase() === wanted);
|
|
55
|
+
if (exact)
|
|
56
|
+
return exact;
|
|
57
|
+
// VICE lists drives as driveN; the Ultimate letters a and b default to buses 8 and 9 there.
|
|
58
|
+
const letterBus = wanted === "a" ? 8 : wanted === "b" ? 9 : undefined;
|
|
59
|
+
const bus = /^(?:drive)?\s*(\d+)$/.exec(wanted);
|
|
60
|
+
if (!bus && letterBus === undefined)
|
|
61
|
+
return undefined;
|
|
62
|
+
const busId = letterBus ?? Number(bus[1]);
|
|
63
|
+
if (letterBus !== undefined) {
|
|
64
|
+
return drives.find((d) => String(d?.id ?? "").toLowerCase() === `drive${busId}`);
|
|
65
|
+
}
|
|
66
|
+
const onBus = drives.filter((d) => Number(d?.raw?.bus_id) === busId || String(d?.id ?? "").toLowerCase() === `drive${busId}`);
|
|
67
|
+
// Prefer the enabled drive; a disabled drive can keep the same bus_id setting.
|
|
68
|
+
return onBus.find((d) => d?.raw?.enabled !== false) ?? onBus[0];
|
|
69
|
+
}
|
|
48
70
|
function extractPaths(value) {
|
|
49
71
|
const acc = [];
|
|
50
72
|
const stack = [value];
|
|
@@ -651,7 +673,7 @@ export const tools = [
|
|
|
651
673
|
if (powerOnIfNeeded) {
|
|
652
674
|
try {
|
|
653
675
|
const drives = await ctx.client.drivesList();
|
|
654
|
-
const targetDrive = Array.isArray(drives) ? drives
|
|
676
|
+
const targetDrive = Array.isArray(drives) ? findListedDrive(drives, drive) : null;
|
|
655
677
|
if (!targetDrive) {
|
|
656
678
|
log.push({ step: "check_drive_exists", success: false, error: `Drive ${drive} not found` });
|
|
657
679
|
throw new ToolExecutionError(`Drive ${drive} not found in firmware drive list`, { details: { drive } });
|
|
@@ -676,18 +698,23 @@ export const tools = [
|
|
|
676
698
|
throw new ToolExecutionError("Failed to check or power on drive", { details: { error: error instanceof Error ? error.message : String(error) } });
|
|
677
699
|
}
|
|
678
700
|
}
|
|
679
|
-
// Step 2:
|
|
701
|
+
// Step 2: Switch the drive emulation mode. The mount's own "mode"
|
|
702
|
+
// option is the read/write attachment mode, not the drive type.
|
|
703
|
+
if (mode) {
|
|
704
|
+
const setMode = await ctx.client.driveSetMode(drive, mode);
|
|
705
|
+
log.push({ step: "set_mode", success: setMode.success === true, details: setMode.details });
|
|
706
|
+
if (!setMode.success) {
|
|
707
|
+
throw new ToolExecutionError(`Failed to set drive ${drive} to ${mode} mode`, { details: setMode.details });
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
// Step 3: Mount with retries
|
|
680
711
|
let mountSuccess = false;
|
|
681
712
|
let lastMountError = null;
|
|
682
713
|
let attempts = 0;
|
|
683
714
|
for (let attempt = 0; attempt <= maxRetries && !mountSuccess; attempt += 1) {
|
|
684
715
|
attempts = attempt + 1;
|
|
685
716
|
try {
|
|
686
|
-
const
|
|
687
|
-
if (mode) {
|
|
688
|
-
mountOptions.mode = mode;
|
|
689
|
-
}
|
|
690
|
-
const mount = await ctx.client.driveMount(drive, imagePath, mountOptions);
|
|
717
|
+
const mount = await ctx.client.driveMount(drive, imagePath);
|
|
691
718
|
if (mount.success) {
|
|
692
719
|
mountSuccess = true;
|
|
693
720
|
log.push({ step: `mount_attempt_${attempts}`, success: true, details: mount.details });
|
|
@@ -717,7 +744,7 @@ export const tools = [
|
|
|
717
744
|
details: { lastError: lastMountError }
|
|
718
745
|
});
|
|
719
746
|
}
|
|
720
|
-
// Step
|
|
747
|
+
// Step 4: Reset drive if requested
|
|
721
748
|
if (resetAfterMount) {
|
|
722
749
|
try {
|
|
723
750
|
const reset = await ctx.client.driveReset(drive);
|
|
@@ -735,12 +762,12 @@ export const tools = [
|
|
|
735
762
|
ctx.logger.warn?.("Drive reset threw error", { error });
|
|
736
763
|
}
|
|
737
764
|
}
|
|
738
|
-
// Step
|
|
765
|
+
// Step 5: Verify mount
|
|
739
766
|
let verificationResult = null;
|
|
740
767
|
if (verifyMount) {
|
|
741
768
|
try {
|
|
742
769
|
const drives = await ctx.client.drivesList();
|
|
743
|
-
const targetDrive = Array.isArray(drives) ? drives
|
|
770
|
+
const targetDrive = Array.isArray(drives) ? findListedDrive(drives, drive) : null;
|
|
744
771
|
if (!targetDrive) {
|
|
745
772
|
log.push({ step: "verify", success: false, error: "Drive not found in list" });
|
|
746
773
|
throw new ToolExecutionError("Drive disappeared after mount", { details: { drive } });
|
|
@@ -13,21 +13,16 @@ function parseAddressNumeric(value) {
|
|
|
13
13
|
throw new ToolValidationError("Address must be a non-empty string", { path: "$.address" });
|
|
14
14
|
}
|
|
15
15
|
const trimmed = value.trim();
|
|
16
|
-
|
|
17
|
-
let
|
|
18
|
-
if (
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
else if (trimmed
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
else if (trimmed.startsWith("%")) {
|
|
27
|
-
radix = 2;
|
|
28
|
-
literal = trimmed.slice(1);
|
|
29
|
-
}
|
|
30
|
-
const parsed = Number.parseInt(literal, radix);
|
|
16
|
+
// Anchored patterns: Number.parseInt alone would read "$08G0" as $0008.
|
|
17
|
+
let parsed = Number.NaN;
|
|
18
|
+
if (/^\$[0-9a-f]+$/i.test(trimmed))
|
|
19
|
+
parsed = Number.parseInt(trimmed.slice(1), 16);
|
|
20
|
+
else if (/^0x[0-9a-f]+$/i.test(trimmed))
|
|
21
|
+
parsed = Number.parseInt(trimmed.slice(2), 16);
|
|
22
|
+
else if (/^%[01]+$/.test(trimmed))
|
|
23
|
+
parsed = Number.parseInt(trimmed.slice(1), 2);
|
|
24
|
+
else if (/^[0-9]+$/.test(trimmed))
|
|
25
|
+
parsed = Number.parseInt(trimmed, 10);
|
|
31
26
|
if (!Number.isFinite(parsed) || Number.isNaN(parsed)) {
|
|
32
27
|
throw new ToolValidationError("Invalid address value", { path: "$.address", details: { value } });
|
|
33
28
|
}
|
|
@@ -228,7 +223,7 @@ export const tools = [
|
|
|
228
223
|
try {
|
|
229
224
|
const read = await ctx.client.readMemory(`$${formatAddressHex(startAddress)}`, String(length));
|
|
230
225
|
if (!read.success || typeof read.data !== "string") {
|
|
231
|
-
throw new ToolExecutionError("C64
|
|
226
|
+
throw new ToolExecutionError("C64 backend reported failure while reading memory", { details: normalizeErrorDetails(read.details) });
|
|
232
227
|
}
|
|
233
228
|
const bytes = hexToBytes(read.data);
|
|
234
229
|
if (bytes.length < length) {
|
|
@@ -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
|
}
|
|
@@ -103,7 +97,7 @@ export const tools = [
|
|
|
103
97
|
if (canPause) {
|
|
104
98
|
const pauseResult = await ctx.client.pause();
|
|
105
99
|
if (!pauseResult.success) {
|
|
106
|
-
throw new ToolExecutionError("C64
|
|
100
|
+
throw new ToolExecutionError("C64 backend reported failure while pausing", { details: normalizeErrorDetails(pauseResult.details) });
|
|
107
101
|
}
|
|
108
102
|
paused = true;
|
|
109
103
|
}
|
|
@@ -111,7 +105,7 @@ export const tools = [
|
|
|
111
105
|
try {
|
|
112
106
|
const pre = await ctx.client.readMemory(parsed.address, String(Math.max(1, readLen)));
|
|
113
107
|
if (!pre.success) {
|
|
114
|
-
throw new ToolExecutionError("C64
|
|
108
|
+
throw new ToolExecutionError("C64 backend reported failure while reading memory", { details: normalizeErrorDetails(pre.details) });
|
|
115
109
|
}
|
|
116
110
|
preReadHex = pre.data ?? "$";
|
|
117
111
|
const preBytes = hexToBytes(preReadHex);
|
|
@@ -131,11 +125,11 @@ export const tools = [
|
|
|
131
125
|
}
|
|
132
126
|
const write = await ctx.client.writeMemory(parsed.address, bytesToHex(writeBytes));
|
|
133
127
|
if (!write.success) {
|
|
134
|
-
throw new ToolExecutionError("C64
|
|
128
|
+
throw new ToolExecutionError("C64 backend reported failure while writing memory", { details: normalizeErrorDetails(write.details) });
|
|
135
129
|
}
|
|
136
130
|
const post = await ctx.client.readMemory(parsed.address, String(Math.max(1, writeBytes.length)));
|
|
137
131
|
if (!post.success) {
|
|
138
|
-
throw new ToolExecutionError("C64
|
|
132
|
+
throw new ToolExecutionError("C64 backend reported failure while reading back memory", { details: normalizeErrorDetails(post.details) });
|
|
139
133
|
}
|
|
140
134
|
const postBytes = hexToBytes(post.data ?? "$");
|
|
141
135
|
const diffs = [];
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { objectSchema, stringSchema, arraySchema, numberSchema, optionalSchema, booleanSchema } from "../schema.js";
|
|
2
2
|
import { jsonResult } from "../responses.js";
|
|
3
3
|
import { ToolError, ToolUnsupportedPlatformError, ToolExecutionError, toolErrorResult, unknownErrorResult } from "../errors.js";
|
|
4
|
+
import { measureSidSilence } from "../audio.js";
|
|
4
5
|
import { getPlatformStatus } from "../../platform.js";
|
|
5
6
|
import { promises as fs } from "node:fs";
|
|
6
7
|
import { resolve as resolvePath, join as joinPath } from "node:path";
|
|
@@ -8,6 +9,7 @@ import { sleep, formatTimestampSpec } from "./util.js";
|
|
|
8
9
|
import { getTasksHomeDir } from "./background.js";
|
|
9
10
|
import { Jimp } from "jimp";
|
|
10
11
|
import { withDiagnosticSpan } from "../../diagnostics.js";
|
|
12
|
+
import { stripHexByteSeparators } from "../hex.js";
|
|
11
13
|
const C64_SCREENSHOT_PALETTE = [
|
|
12
14
|
0x000000ff,
|
|
13
15
|
0xffffffff,
|
|
@@ -62,6 +64,9 @@ function canonicalGreetingBackends() {
|
|
|
62
64
|
function uniqueGreetingBackends(backends) {
|
|
63
65
|
return backends.filter((backend, index) => backends.indexOf(backend) === index);
|
|
64
66
|
}
|
|
67
|
+
function normaliseAssertionHex(value) {
|
|
68
|
+
return stripHexByteSeparators(value).toUpperCase();
|
|
69
|
+
}
|
|
65
70
|
function normaliseScreenText(screen) {
|
|
66
71
|
return screen.replace(/\r\n?/g, "\n").replace(/\s+/g, " ").trim().toUpperCase();
|
|
67
72
|
}
|
|
@@ -202,7 +207,7 @@ const batchRunWithAssertionsArgsSchema = objectSchema({
|
|
|
202
207
|
assertions: optionalSchema(arraySchema(objectSchema({
|
|
203
208
|
description: "Assertion to check after run",
|
|
204
209
|
properties: {
|
|
205
|
-
type: stringSchema({ description: "Assertion type", enum: ["screen_contains", "memory_equals", "sid_silent"] }),
|
|
210
|
+
type: stringSchema({ description: "Assertion type; sid_silent needs the C64 Ultimate audio stream (c64u backend).", enum: ["screen_contains", "memory_equals", "sid_silent"] }),
|
|
206
211
|
pattern: optionalSchema(stringSchema({ description: "Pattern for screen_contains", minLength: 1 })),
|
|
207
212
|
address: optionalSchema(stringSchema({ description: "Address for memory_equals", minLength: 1 })),
|
|
208
213
|
expected: optionalSchema(stringSchema({ description: "Expected value (hex)", minLength: 1 })),
|
|
@@ -696,9 +701,17 @@ export const tools = [
|
|
|
696
701
|
else if (assertion.type === "memory_equals") {
|
|
697
702
|
const addr = assertion.address ?? "$0400";
|
|
698
703
|
const expected = assertion.expected ?? "$00";
|
|
699
|
-
|
|
700
|
-
const
|
|
701
|
-
|
|
704
|
+
// Compare every expected byte, and treat a failed read as an error rather than as $00.
|
|
705
|
+
const expectedHex = normaliseAssertionHex(expected);
|
|
706
|
+
if (expectedHex.length === 0 || expectedHex.length % 2 !== 0 || !/^[0-9A-F]+$/.test(expectedHex)) {
|
|
707
|
+
throw new Error(`memory_equals expected value ${expected} is not a whole number of hex bytes`);
|
|
708
|
+
}
|
|
709
|
+
const result = await ctx.client.readMemory(addr, String(expectedHex.length / 2));
|
|
710
|
+
if (!result.success || typeof result.data !== "string") {
|
|
711
|
+
throw new Error(`Failed to read memory at ${addr} for memory_equals assertion`);
|
|
712
|
+
}
|
|
713
|
+
const actual = result.data;
|
|
714
|
+
const matched = normaliseAssertionHex(actual) === expectedHex;
|
|
702
715
|
assertionResults.push({
|
|
703
716
|
type: assertion.type,
|
|
704
717
|
status: matched ? "pass" : "fail",
|
|
@@ -708,14 +721,20 @@ export const tools = [
|
|
|
708
721
|
status = "fail";
|
|
709
722
|
}
|
|
710
723
|
else if (assertion.type === "sid_silent") {
|
|
711
|
-
//
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
724
|
+
// SID control registers are write-only (an Ultimate 64 reads $D404 as $00
|
|
725
|
+
// whatever was written), so silence is measured from the Ultimate audio
|
|
726
|
+
// stream. Other backends would fall back to the host microphone, which
|
|
727
|
+
// measures the room rather than the C64, so they report an error instead.
|
|
728
|
+
const backend = ctx.platform?.id ?? await ctx.client.getActiveBackendType?.();
|
|
729
|
+
if (backend !== undefined && backend !== "c64u") {
|
|
730
|
+
throw new Error(`sid_silent needs the C64 Ultimate audio stream and is not available on ${backend}`);
|
|
731
|
+
}
|
|
732
|
+
const silence = await measureSidSilence(ctx);
|
|
733
|
+
const isSilent = silence.silent;
|
|
715
734
|
assertionResults.push({
|
|
716
735
|
type: assertion.type,
|
|
717
736
|
status: isSilent ? "pass" : "fail",
|
|
718
|
-
message: isSilent ? undefined :
|
|
737
|
+
message: isSilent ? undefined : `SID output is not silent (peak RMS ${silence.maxRms.toFixed(3)} > ${silence.threshold})`,
|
|
719
738
|
});
|
|
720
739
|
if (!isSilent)
|
|
721
740
|
status = "fail";
|
|
@@ -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);
|
package/dist/tools/printer.js
CHANGED
|
@@ -278,7 +278,7 @@ export const printerModule = defineToolModule({
|
|
|
278
278
|
formFeed: parsed.formFeed,
|
|
279
279
|
});
|
|
280
280
|
if (!result.success) {
|
|
281
|
-
throw new ToolExecutionError("C64
|
|
281
|
+
throw new ToolExecutionError("C64 backend reported failure while printing text", {
|
|
282
282
|
details: normaliseFailure(result.details),
|
|
283
283
|
});
|
|
284
284
|
}
|
|
@@ -337,7 +337,7 @@ export const printerModule = defineToolModule({
|
|
|
337
337
|
ensureMsb: parsed.ensureMsb,
|
|
338
338
|
});
|
|
339
339
|
if (!result.success) {
|
|
340
|
-
throw new ToolExecutionError("C64
|
|
340
|
+
throw new ToolExecutionError("C64 backend reported failure while printing Commodore bitmap", {
|
|
341
341
|
details: normaliseFailure(result.details),
|
|
342
342
|
});
|
|
343
343
|
}
|
|
@@ -398,7 +398,7 @@ export const printerModule = defineToolModule({
|
|
|
398
398
|
timesPerLine: parsed.timesPerLine,
|
|
399
399
|
});
|
|
400
400
|
if (!result.success) {
|
|
401
|
-
throw new ToolExecutionError("C64
|
|
401
|
+
throw new ToolExecutionError("C64 backend reported failure while printing Epson bitmap", {
|
|
402
402
|
details: normaliseFailure(result.details),
|
|
403
403
|
});
|
|
404
404
|
}
|
|
@@ -458,7 +458,7 @@ export const printerModule = defineToolModule({
|
|
|
458
458
|
secondaryAddress: parsed.secondaryAddress,
|
|
459
459
|
});
|
|
460
460
|
if (!result.success) {
|
|
461
|
-
throw new ToolExecutionError("C64
|
|
461
|
+
throw new ToolExecutionError("C64 backend reported failure while defining DLL characters", {
|
|
462
462
|
details: normaliseFailure(result.details),
|
|
463
463
|
});
|
|
464
464
|
}
|
|
@@ -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,
|
|
@@ -266,7 +267,7 @@ async function executeUploadRunBasic(rawArgs, ctx) {
|
|
|
266
267
|
const runBasic = async (source) => ctx.client.uploadAndRunBasic(source);
|
|
267
268
|
let result = await runBasic(activeProgram);
|
|
268
269
|
if (!result.success) {
|
|
269
|
-
throw new ToolExecutionError("C64
|
|
270
|
+
throw new ToolExecutionError("C64 backend reported failure while running BASIC program", {
|
|
270
271
|
details: extractFailureDetails(result.details),
|
|
271
272
|
});
|
|
272
273
|
}
|
|
@@ -441,13 +442,13 @@ 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 =
|
|
445
|
+
const entryAddress = asmResult.entryAddress;
|
|
445
446
|
if (ctx.platform.id === "vice") {
|
|
446
447
|
setViceSymbols(asmResult.symbols.entries());
|
|
447
448
|
}
|
|
448
449
|
const result = await ctx.client.uploadAndRunAsm(parsed.program);
|
|
449
450
|
if (!result.success) {
|
|
450
|
-
return toolErrorResult(new ToolExecutionError("C64
|
|
451
|
+
return toolErrorResult(new ToolExecutionError("C64 backend reported failure while running assembly program", {
|
|
451
452
|
details: extractFailureDetails(result.details),
|
|
452
453
|
}));
|
|
453
454
|
}
|
|
@@ -515,7 +516,7 @@ async function executeLoadPrg(rawArgs, ctx) {
|
|
|
515
516
|
ctx.logger.info("Loading PRG file", { path: parsed.path });
|
|
516
517
|
const result = await ctx.client.loadPrgFile(parsed.path);
|
|
517
518
|
if (!result.success) {
|
|
518
|
-
throw new ToolExecutionError("C64
|
|
519
|
+
throw new ToolExecutionError("C64 backend reported failure while loading PRG", {
|
|
519
520
|
details: extractFailureDetails(result.details),
|
|
520
521
|
});
|
|
521
522
|
}
|
|
@@ -565,7 +566,7 @@ async function executeRunPrg(rawArgs, ctx) {
|
|
|
565
566
|
}
|
|
566
567
|
const result = await ctx.client.runPrgFile(parsed.path);
|
|
567
568
|
if (!result.success) {
|
|
568
|
-
throw new ToolExecutionError("C64
|
|
569
|
+
throw new ToolExecutionError("C64 backend reported failure while running PRG", {
|
|
569
570
|
details: extractFailureDetails(result.details),
|
|
570
571
|
});
|
|
571
572
|
}
|
|
@@ -600,7 +601,7 @@ async function executeRunCrt(rawArgs, ctx) {
|
|
|
600
601
|
ctx.logger.info("Running CRT file", { path: parsed.path });
|
|
601
602
|
const result = await ctx.client.runCrtFile(parsed.path);
|
|
602
603
|
if (!result.success) {
|
|
603
|
-
throw new ToolExecutionError("C64
|
|
604
|
+
throw new ToolExecutionError("C64 backend reported failure while running CRT", {
|
|
604
605
|
details: extractFailureDetails(result.details),
|
|
605
606
|
});
|
|
606
607
|
}
|
|
@@ -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: [
|
|
@@ -772,7 +773,7 @@ export const programRunnersModule = defineToolModule({
|
|
|
772
773
|
ctx.logger.info("Running CRT file", { path: parsed.path });
|
|
773
774
|
const result = await ctx.client.runCrtFile(parsed.path);
|
|
774
775
|
if (!result.success) {
|
|
775
|
-
throw new ToolExecutionError("C64
|
|
776
|
+
throw new ToolExecutionError("C64 backend reported failure while running CRT", {
|
|
776
777
|
details: extractFailureDetails(result.details),
|
|
777
778
|
});
|
|
778
779
|
}
|
|
@@ -76,7 +76,7 @@ const createImageArgsSchema = objectSchema({
|
|
|
76
76
|
maxLength: 16,
|
|
77
77
|
})),
|
|
78
78
|
tracks: optionalSchema(numberSchema({
|
|
79
|
-
description: "Track count (D64 supports 35
|
|
79
|
+
description: "Track count (D64 supports 35 to 41; DNP requires explicit tracks).",
|
|
80
80
|
integer: true,
|
|
81
81
|
minimum: 1,
|
|
82
82
|
maximum: 255,
|
|
@@ -210,10 +210,10 @@ const diskOperations = [
|
|
|
210
210
|
const { path, diskname, tracks } = parsed;
|
|
211
211
|
switch (format) {
|
|
212
212
|
case "d64":
|
|
213
|
-
if (tracks !== undefined && tracks
|
|
214
|
-
throw new ToolValidationError("D64 images support 35
|
|
213
|
+
if (tracks !== undefined && (tracks < 35 || tracks > 41)) {
|
|
214
|
+
throw new ToolValidationError("D64 images support 35 to 41 tracks", {
|
|
215
215
|
path: "$.tracks",
|
|
216
|
-
details: {
|
|
216
|
+
details: { minimum: 35, maximum: 41, received: tracks },
|
|
217
217
|
});
|
|
218
218
|
}
|
|
219
219
|
return storageModule.invoke("create_d64", { path, tracks, diskname }, ctx);
|
|
@@ -274,8 +274,10 @@ export const diskModuleGroup = defineToolModule({
|
|
|
274
274
|
variants: diskOperations.map((operation) => operation.schema),
|
|
275
275
|
}),
|
|
276
276
|
tags: ["storage", "drive", "grouped"],
|
|
277
|
-
|
|
277
|
+
// The VICE Binary Monitor has no command to detach a disk image, so unmount is Ultimate-only.
|
|
278
|
+
operationPlatforms: { unmount: ["c64u", "u2"], file_info: ["c64u", "u2"], create_image: ["c64u", "u2"], find_and_run: ["c64u", "u2"] },
|
|
278
279
|
operationToolNames: {
|
|
280
|
+
unmount: "drive_remove",
|
|
279
281
|
file_info: "file_info",
|
|
280
282
|
create_image: "create_image",
|
|
281
283
|
find_and_run: "find_and_run_program_by_name",
|