c64bridge 1.0.5 → 1.0.7
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/AGENTS.md +1 -1
- package/README.md +3 -3
- package/data/sound/sidwave.md +113 -36
- package/data/sound/sidwave.schema.json +138 -28
- package/dist/.tsbuildinfo +1 -1
- package/dist/c64Client.js +83 -93
- package/dist/chargen.js +0 -4
- package/dist/config.js +26 -6
- package/dist/device.js +130 -24
- package/dist/diagnostics.js +14 -5
- package/dist/logger.js +3 -2
- package/dist/loggingHttpClient.js +12 -1
- package/dist/mcp-server.js +19 -9
- package/dist/petscii.js +0 -5
- package/dist/redact.js +166 -0
- package/dist/sidwave.js +453 -61
- package/dist/sidwaveCompiler.js +1424 -269
- package/dist/tools/audio.js +70 -12
- package/dist/tools/developer.js +17 -9
- package/dist/tools/errors.js +7 -0
- package/dist/tools/graphics.js +4 -4
- package/dist/tools/input.js +10 -13
- package/dist/tools/machineControl.js +7 -7
- package/dist/tools/memory.js +6 -6
- package/dist/tools/meta/audio.js +8 -2
- package/dist/tools/meta/config.js +5 -4
- package/dist/tools/meta/configInventory.js +25 -2
- package/dist/tools/meta/diagnostics.js +2 -1
- package/dist/tools/meta/graphics.js +1 -1
- package/dist/tools/meta/memory.js +4 -4
- package/dist/tools/meta/program.js +12 -8
- package/dist/tools/printer.js +4 -4
- package/dist/tools/programRunners.js +8 -8
- package/dist/tools/registry/batch.js +1 -7
- 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 +44 -12
- package/dist/tools/translation/basicTokenizer.js +5 -1
- package/dist/tools/translation/opcodeInventory.js +1 -1
- package/dist/tools/vice.js +1 -1
- package/dist/vice/mockServer.js +92 -5
- package/dist/vice/readiness.js +4 -9
- package/dist/vice/viceClient.js +18 -0
- package/doc/audit/secret-handling.md +61 -0
- package/mcp/schemas/c64_config.schema.json +19 -2
- 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_vice.schema.json +3 -3
- package/mcp/server.json +1 -1
- package/mcp/tools.json +33 -12
- package/mcp.json +1 -1
- package/package.json +5 -2
- package/scripts/mockC64Server.mjs +15 -1
- package/scripts/run-coverage.mjs +31 -1
- package/scripts/update-readme.ts +3 -2
- package/server.json +20 -3
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isSensitiveConfigItem, redactConfigValue, REDACTED_PLACEHOLDER } from "../../redact.js";
|
|
1
2
|
export function normalizeConfigInventory(details) {
|
|
2
3
|
const categories = details?.categories;
|
|
3
4
|
if (!Array.isArray(categories)) {
|
|
@@ -55,12 +56,15 @@ export function normalizeConfigInventory(details) {
|
|
|
55
56
|
}
|
|
56
57
|
return inventory;
|
|
57
58
|
}
|
|
58
|
-
export async function captureConfigSnapshot(client) {
|
|
59
|
+
export async function captureConfigSnapshot(client, options = {}) {
|
|
59
60
|
const inventory = normalizeConfigInventory(await client.configsList());
|
|
60
61
|
const categories = {};
|
|
61
62
|
for (const entry of inventory) {
|
|
62
63
|
try {
|
|
63
|
-
const
|
|
64
|
+
const normalized = normalizeSnapshotValue(await client.configGet(entry.category, entry.item), entry);
|
|
65
|
+
const value = options.includeSecretValues
|
|
66
|
+
? normalized
|
|
67
|
+
: redactConfigValue(normalized, entry.category, entry.item);
|
|
64
68
|
setSnapshotValue(categories, entry, value);
|
|
65
69
|
}
|
|
66
70
|
catch (error) {
|
|
@@ -71,6 +75,25 @@ export async function captureConfigSnapshot(client) {
|
|
|
71
75
|
}
|
|
72
76
|
return { inventory, categories };
|
|
73
77
|
}
|
|
78
|
+
/**
|
|
79
|
+
* Drops redacted credential placeholders from a snapshot payload before it is
|
|
80
|
+
* written back, so restoring a default snapshot never sets a password to `***`.
|
|
81
|
+
*/
|
|
82
|
+
export function stripRedactedSecrets(categories) {
|
|
83
|
+
const strip = (category, value) => {
|
|
84
|
+
if (value === null || typeof value !== "object" || Array.isArray(value))
|
|
85
|
+
return value;
|
|
86
|
+
const kept = {};
|
|
87
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
88
|
+
if (entry === REDACTED_PLACEHOLDER && isSensitiveConfigItem(category, key))
|
|
89
|
+
continue;
|
|
90
|
+
// Per-item snapshot entries keep the firmware wrapper: { category: { item: value } }.
|
|
91
|
+
kept[key] = strip(entry !== null && typeof entry === "object" && !Array.isArray(entry) ? key : category, entry);
|
|
92
|
+
}
|
|
93
|
+
return kept;
|
|
94
|
+
};
|
|
95
|
+
return Object.fromEntries(Object.entries(categories).map(([category, items]) => [category, strip(category, items)]));
|
|
96
|
+
}
|
|
74
97
|
export function validateSnapshotCategories(value) {
|
|
75
98
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
76
99
|
throw new Error("Snapshot categories payload must be an object");
|
|
@@ -74,7 +74,8 @@ export const tools = [
|
|
|
74
74
|
version,
|
|
75
75
|
info,
|
|
76
76
|
};
|
|
77
|
-
|
|
77
|
+
const result = jsonResult(report, { success: report.isHealthy });
|
|
78
|
+
return report.isHealthy ? result : { ...result, isError: true };
|
|
78
79
|
}
|
|
79
80
|
catch (error) {
|
|
80
81
|
if (error instanceof ToolError)
|
|
@@ -223,7 +223,7 @@ export const tools = [
|
|
|
223
223
|
try {
|
|
224
224
|
const read = await ctx.client.readMemory(`$${formatAddressHex(startAddress)}`, String(length));
|
|
225
225
|
if (!read.success || typeof read.data !== "string") {
|
|
226
|
-
throw new ToolExecutionError("C64
|
|
226
|
+
throw new ToolExecutionError("C64 backend reported failure while reading memory", { details: normalizeErrorDetails(read.details) });
|
|
227
227
|
}
|
|
228
228
|
const bytes = hexToBytes(read.data);
|
|
229
229
|
if (bytes.length < length) {
|
|
@@ -97,7 +97,7 @@ export const tools = [
|
|
|
97
97
|
if (canPause) {
|
|
98
98
|
const pauseResult = await ctx.client.pause();
|
|
99
99
|
if (!pauseResult.success) {
|
|
100
|
-
throw new ToolExecutionError("C64
|
|
100
|
+
throw new ToolExecutionError("C64 backend reported failure while pausing", { details: normalizeErrorDetails(pauseResult.details) });
|
|
101
101
|
}
|
|
102
102
|
paused = true;
|
|
103
103
|
}
|
|
@@ -105,7 +105,7 @@ export const tools = [
|
|
|
105
105
|
try {
|
|
106
106
|
const pre = await ctx.client.readMemory(parsed.address, String(Math.max(1, readLen)));
|
|
107
107
|
if (!pre.success) {
|
|
108
|
-
throw new ToolExecutionError("C64
|
|
108
|
+
throw new ToolExecutionError("C64 backend reported failure while reading memory", { details: normalizeErrorDetails(pre.details) });
|
|
109
109
|
}
|
|
110
110
|
preReadHex = pre.data ?? "$";
|
|
111
111
|
const preBytes = hexToBytes(preReadHex);
|
|
@@ -125,11 +125,11 @@ export const tools = [
|
|
|
125
125
|
}
|
|
126
126
|
const write = await ctx.client.writeMemory(parsed.address, bytesToHex(writeBytes));
|
|
127
127
|
if (!write.success) {
|
|
128
|
-
throw new ToolExecutionError("C64
|
|
128
|
+
throw new ToolExecutionError("C64 backend reported failure while writing memory", { details: normalizeErrorDetails(write.details) });
|
|
129
129
|
}
|
|
130
130
|
const post = await ctx.client.readMemory(parsed.address, String(Math.max(1, writeBytes.length)));
|
|
131
131
|
if (!post.success) {
|
|
132
|
-
throw new ToolExecutionError("C64
|
|
132
|
+
throw new ToolExecutionError("C64 backend reported failure while reading back memory", { details: normalizeErrorDetails(post.details) });
|
|
133
133
|
}
|
|
134
134
|
const postBytes = hexToBytes(post.data ?? "$");
|
|
135
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";
|
|
@@ -206,7 +207,7 @@ const batchRunWithAssertionsArgsSchema = objectSchema({
|
|
|
206
207
|
assertions: optionalSchema(arraySchema(objectSchema({
|
|
207
208
|
description: "Assertion to check after run",
|
|
208
209
|
properties: {
|
|
209
|
-
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"] }),
|
|
210
211
|
pattern: optionalSchema(stringSchema({ description: "Pattern for screen_contains", minLength: 1 })),
|
|
211
212
|
address: optionalSchema(stringSchema({ description: "Address for memory_equals", minLength: 1 })),
|
|
212
213
|
expected: optionalSchema(stringSchema({ description: "Expected value (hex)", minLength: 1 })),
|
|
@@ -720,17 +721,20 @@ export const tools = [
|
|
|
720
721
|
status = "fail";
|
|
721
722
|
}
|
|
722
723
|
else if (assertion.type === "sid_silent") {
|
|
723
|
-
//
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
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}`);
|
|
727
731
|
}
|
|
728
|
-
const
|
|
729
|
-
const isSilent =
|
|
732
|
+
const silence = await measureSidSilence(ctx);
|
|
733
|
+
const isSilent = silence.silent;
|
|
730
734
|
assertionResults.push({
|
|
731
735
|
type: assertion.type,
|
|
732
736
|
status: isSilent ? "pass" : "fail",
|
|
733
|
-
message: isSilent ? undefined :
|
|
737
|
+
message: isSilent ? undefined : `SID output is not silent (peak RMS ${silence.maxRms.toFixed(3)} > ${silence.threshold})`,
|
|
734
738
|
});
|
|
735
739
|
if (!isSilent)
|
|
736
740
|
status = "fail";
|
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
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
|
-
import { assemblyToPrgDetailed, AssemblyError
|
|
2
|
+
import { assemblyToPrgDetailed, AssemblyError } 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";
|
|
@@ -267,7 +267,7 @@ async function executeUploadRunBasic(rawArgs, ctx) {
|
|
|
267
267
|
const runBasic = async (source) => ctx.client.uploadAndRunBasic(source);
|
|
268
268
|
let result = await runBasic(activeProgram);
|
|
269
269
|
if (!result.success) {
|
|
270
|
-
throw new ToolExecutionError("C64
|
|
270
|
+
throw new ToolExecutionError("C64 backend reported failure while running BASIC program", {
|
|
271
271
|
details: extractFailureDetails(result.details),
|
|
272
272
|
});
|
|
273
273
|
}
|
|
@@ -442,13 +442,13 @@ async function executeUploadRunAsm(rawArgs, ctx) {
|
|
|
442
442
|
// Assemble locally to expose structured metadata and capture symbols
|
|
443
443
|
const asmResult = assemblyToPrgDetailed(parsed.program);
|
|
444
444
|
const prg = asmResult.prg;
|
|
445
|
-
const entryAddress =
|
|
445
|
+
const entryAddress = asmResult.entryAddress;
|
|
446
446
|
if (ctx.platform.id === "vice") {
|
|
447
447
|
setViceSymbols(asmResult.symbols.entries());
|
|
448
448
|
}
|
|
449
449
|
const result = await ctx.client.uploadAndRunAsm(parsed.program);
|
|
450
450
|
if (!result.success) {
|
|
451
|
-
return toolErrorResult(new ToolExecutionError("C64
|
|
451
|
+
return toolErrorResult(new ToolExecutionError("C64 backend reported failure while running assembly program", {
|
|
452
452
|
details: extractFailureDetails(result.details),
|
|
453
453
|
}));
|
|
454
454
|
}
|
|
@@ -516,7 +516,7 @@ async function executeLoadPrg(rawArgs, ctx) {
|
|
|
516
516
|
ctx.logger.info("Loading PRG file", { path: parsed.path });
|
|
517
517
|
const result = await ctx.client.loadPrgFile(parsed.path);
|
|
518
518
|
if (!result.success) {
|
|
519
|
-
throw new ToolExecutionError("C64
|
|
519
|
+
throw new ToolExecutionError("C64 backend reported failure while loading PRG", {
|
|
520
520
|
details: extractFailureDetails(result.details),
|
|
521
521
|
});
|
|
522
522
|
}
|
|
@@ -566,7 +566,7 @@ async function executeRunPrg(rawArgs, ctx) {
|
|
|
566
566
|
}
|
|
567
567
|
const result = await ctx.client.runPrgFile(parsed.path);
|
|
568
568
|
if (!result.success) {
|
|
569
|
-
throw new ToolExecutionError("C64
|
|
569
|
+
throw new ToolExecutionError("C64 backend reported failure while running PRG", {
|
|
570
570
|
details: extractFailureDetails(result.details),
|
|
571
571
|
});
|
|
572
572
|
}
|
|
@@ -601,7 +601,7 @@ async function executeRunCrt(rawArgs, ctx) {
|
|
|
601
601
|
ctx.logger.info("Running CRT file", { path: parsed.path });
|
|
602
602
|
const result = await ctx.client.runCrtFile(parsed.path);
|
|
603
603
|
if (!result.success) {
|
|
604
|
-
throw new ToolExecutionError("C64
|
|
604
|
+
throw new ToolExecutionError("C64 backend reported failure while running CRT", {
|
|
605
605
|
details: extractFailureDetails(result.details),
|
|
606
606
|
});
|
|
607
607
|
}
|
|
@@ -773,7 +773,7 @@ export const programRunnersModule = defineToolModule({
|
|
|
773
773
|
ctx.logger.info("Running CRT file", { path: parsed.path });
|
|
774
774
|
const result = await ctx.client.runCrtFile(parsed.path);
|
|
775
775
|
if (!result.success) {
|
|
776
|
-
throw new ToolExecutionError("C64
|
|
776
|
+
throw new ToolExecutionError("C64 backend reported failure while running CRT", {
|
|
777
777
|
details: extractFailureDetails(result.details),
|
|
778
778
|
});
|
|
779
779
|
}
|
|
@@ -2,7 +2,7 @@ import { defineToolModule, } from "../types.js";
|
|
|
2
2
|
import { getPlatformStatus } from "../../platform.js";
|
|
3
3
|
import { arraySchema, booleanSchema, objectSchema, optionalSchema, stringSchema, } from "../schema.js";
|
|
4
4
|
import { jsonResult } from "../responses.js";
|
|
5
|
-
import {
|
|
5
|
+
import { unknownErrorResult } from "../errors.js";
|
|
6
6
|
const commandSchema = objectSchema({
|
|
7
7
|
description: "A single batch command.",
|
|
8
8
|
properties: {
|
|
@@ -155,12 +155,6 @@ export const batchModuleGroup = defineToolModule({
|
|
|
155
155
|
return jsonResult(summary, { success: failed === 0, succeeded, failed, total: parsed.commands.length });
|
|
156
156
|
}
|
|
157
157
|
catch (error) {
|
|
158
|
-
if (error instanceof ToolValidationError) {
|
|
159
|
-
return {
|
|
160
|
-
content: [{ type: "text", text: error.message }],
|
|
161
|
-
isError: true,
|
|
162
|
-
};
|
|
163
|
-
}
|
|
164
158
|
return unknownErrorResult(error);
|
|
165
159
|
}
|
|
166
160
|
},
|
|
@@ -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",
|
package/dist/tools/storage.js
CHANGED
|
@@ -97,10 +97,10 @@ const createD64ArgsSchema = objectSchema({
|
|
|
97
97
|
properties: {
|
|
98
98
|
path: diskImagePathSchema,
|
|
99
99
|
tracks: optionalSchema(numberSchema({
|
|
100
|
-
description: "Track count
|
|
100
|
+
description: "Track count: 35 for a standard disk, up to 41 for the extended area (firmware limit).",
|
|
101
101
|
integer: true,
|
|
102
102
|
minimum: 35,
|
|
103
|
-
maximum:
|
|
103
|
+
maximum: 41,
|
|
104
104
|
})),
|
|
105
105
|
diskname: diskNameSchema,
|
|
106
106
|
},
|
|
@@ -175,7 +175,7 @@ export const storageModule = defineToolModule({
|
|
|
175
175
|
drivesListArgsSchema.parse(args ?? {});
|
|
176
176
|
ctx.logger.info("Listing Ultimate drives");
|
|
177
177
|
const details = await ctx.client.drivesList();
|
|
178
|
-
return textResult("Retrieved
|
|
178
|
+
return textResult("Retrieved drive status.", {
|
|
179
179
|
success: true,
|
|
180
180
|
drives: details,
|
|
181
181
|
});
|
|
@@ -221,14 +221,16 @@ export const storageModule = defineToolModule({
|
|
|
221
221
|
mode: parsed.mode,
|
|
222
222
|
});
|
|
223
223
|
if (!result.success) {
|
|
224
|
-
throw new ToolExecutionError("C64
|
|
224
|
+
throw new ToolExecutionError("C64 backend reported failure while mounting disk image", {
|
|
225
225
|
details: normaliseFailure(result.details),
|
|
226
226
|
});
|
|
227
227
|
}
|
|
228
228
|
const details = toRecord(result.details) ?? {};
|
|
229
229
|
const finalMode = typeof details.mode === "string" ? details.mode : parsed.mode ?? null;
|
|
230
230
|
const finalType = typeof details.type === "string" ? details.type : parsed.type ?? null;
|
|
231
|
-
|
|
231
|
+
// Backend notes (for example VICE's autoload resetting the machine) belong in the visible text.
|
|
232
|
+
const note = typeof details.note === "string" ? ` ${details.note}` : "";
|
|
233
|
+
return textResult(`Mounted ${parsed.image} on ${parsed.drive}.${note}`, {
|
|
232
234
|
success: true,
|
|
233
235
|
drive: parsed.drive,
|
|
234
236
|
image: parsed.image,
|
|
@@ -269,7 +271,7 @@ export const storageModule = defineToolModule({
|
|
|
269
271
|
ctx.logger.info("Removing disk image", { drive: parsed.drive });
|
|
270
272
|
const result = await ctx.client.driveRemove(parsed.drive);
|
|
271
273
|
if (!result.success) {
|
|
272
|
-
throw new ToolExecutionError("C64
|
|
274
|
+
throw new ToolExecutionError("C64 backend reported failure while removing disk image", {
|
|
273
275
|
details: normaliseFailure(result.details),
|
|
274
276
|
});
|
|
275
277
|
}
|
|
@@ -311,7 +313,7 @@ export const storageModule = defineToolModule({
|
|
|
311
313
|
ctx.logger.info("Resetting drive", { drive: parsed.drive });
|
|
312
314
|
const result = await ctx.client.driveReset(parsed.drive);
|
|
313
315
|
if (!result.success) {
|
|
314
|
-
throw new ToolExecutionError("C64
|
|
316
|
+
throw new ToolExecutionError("C64 backend reported failure while resetting drive", {
|
|
315
317
|
details: normaliseFailure(result.details),
|
|
316
318
|
});
|
|
317
319
|
}
|
|
@@ -353,7 +355,7 @@ export const storageModule = defineToolModule({
|
|
|
353
355
|
ctx.logger.info("Powering on drive", { drive: parsed.drive });
|
|
354
356
|
const result = await ctx.client.driveOn(parsed.drive);
|
|
355
357
|
if (!result.success) {
|
|
356
|
-
throw new ToolExecutionError("C64
|
|
358
|
+
throw new ToolExecutionError("C64 backend reported failure while powering on drive", {
|
|
357
359
|
details: normaliseFailure(result.details),
|
|
358
360
|
});
|
|
359
361
|
}
|
|
@@ -396,7 +398,7 @@ export const storageModule = defineToolModule({
|
|
|
396
398
|
ctx.logger.info("Powering off drive", { drive: parsed.drive });
|
|
397
399
|
const result = await ctx.client.driveOff(parsed.drive);
|
|
398
400
|
if (!result.success) {
|
|
399
|
-
throw new ToolExecutionError("C64
|
|
401
|
+
throw new ToolExecutionError("C64 backend reported failure while powering off drive", {
|
|
400
402
|
details: normaliseFailure(result.details),
|
|
401
403
|
});
|
|
402
404
|
}
|
|
@@ -439,7 +441,7 @@ export const storageModule = defineToolModule({
|
|
|
439
441
|
ctx.logger.info("Loading drive ROM", { drive: parsed.drive, path: parsed.path });
|
|
440
442
|
const result = await ctx.client.driveLoadRom(parsed.drive, parsed.path);
|
|
441
443
|
if (!result.success) {
|
|
442
|
-
throw new ToolExecutionError("C64
|
|
444
|
+
throw new ToolExecutionError("C64 backend reported failure while loading drive ROM", {
|
|
443
445
|
details: normaliseFailure(result.details),
|
|
444
446
|
});
|
|
445
447
|
}
|
|
@@ -482,7 +484,7 @@ export const storageModule = defineToolModule({
|
|
|
482
484
|
ctx.logger.info("Setting drive mode", { drive: parsed.drive, mode: parsed.mode });
|
|
483
485
|
const result = await ctx.client.driveSetMode(parsed.drive, parsed.mode);
|
|
484
486
|
if (!result.success) {
|
|
485
|
-
throw new ToolExecutionError("C64
|
|
487
|
+
throw new ToolExecutionError("C64 backend reported failure while setting drive mode", {
|
|
486
488
|
details: normaliseFailure(result.details),
|
|
487
489
|
});
|
|
488
490
|
}
|
|
@@ -541,7 +543,7 @@ export const storageModule = defineToolModule({
|
|
|
541
543
|
{
|
|
542
544
|
name: "create_d64",
|
|
543
545
|
description: "Create a blank D64 disk image on the Ultimate filesystem.",
|
|
544
|
-
summary: "Allocates a 35
|
|
546
|
+
summary: "Allocates a 35 to 41 track D64 image with an optional disk name.",
|
|
545
547
|
inputSchema: createD64ArgsSchema.jsonSchema,
|
|
546
548
|
relatedResources: ["c64://guide/bootstrap"],
|
|
547
549
|
tags: ["disk", "create"],
|
|
@@ -569,14 +571,16 @@ export const storageModule = defineToolModule({
|
|
|
569
571
|
diskname: parsed.diskname,
|
|
570
572
|
});
|
|
571
573
|
if (!result.success) {
|
|
572
|
-
throw new ToolExecutionError("C64
|
|
574
|
+
throw new ToolExecutionError("C64 backend reported failure while creating D64 image", {
|
|
573
575
|
details: normaliseFailure(result.details),
|
|
574
576
|
});
|
|
575
577
|
}
|
|
578
|
+
const firmwareTracks = toRecord(result.details)?.tracks;
|
|
576
579
|
return textResult(`Created D64 image at ${parsed.path}.`, {
|
|
577
580
|
success: true,
|
|
578
581
|
path: parsed.path,
|
|
579
|
-
|
|
582
|
+
// The firmware reports the track count it used, including its default of 35.
|
|
583
|
+
tracks: typeof firmwareTracks === "number" ? firmwareTracks : parsed.tracks ?? 35,
|
|
580
584
|
diskname: parsed.diskname ?? null,
|
|
581
585
|
details: toRecord(result.details) ?? {},
|
|
582
586
|
});
|
|
@@ -618,7 +622,7 @@ export const storageModule = defineToolModule({
|
|
|
618
622
|
diskname: parsed.diskname,
|
|
619
623
|
});
|
|
620
624
|
if (!result.success) {
|
|
621
|
-
throw new ToolExecutionError("C64
|
|
625
|
+
throw new ToolExecutionError("C64 backend reported failure while creating D71 image", {
|
|
622
626
|
details: normaliseFailure(result.details),
|
|
623
627
|
});
|
|
624
628
|
}
|
|
@@ -666,7 +670,7 @@ export const storageModule = defineToolModule({
|
|
|
666
670
|
diskname: parsed.diskname,
|
|
667
671
|
});
|
|
668
672
|
if (!result.success) {
|
|
669
|
-
throw new ToolExecutionError("C64
|
|
673
|
+
throw new ToolExecutionError("C64 backend reported failure while creating D81 image", {
|
|
670
674
|
details: normaliseFailure(result.details),
|
|
671
675
|
});
|
|
672
676
|
}
|
|
@@ -717,7 +721,7 @@ export const storageModule = defineToolModule({
|
|
|
717
721
|
diskname: parsed.diskname,
|
|
718
722
|
});
|
|
719
723
|
if (!result.success) {
|
|
720
|
-
throw new ToolExecutionError("C64
|
|
724
|
+
throw new ToolExecutionError("C64 backend reported failure while creating DNP image", {
|
|
721
725
|
details: normaliseFailure(result.details),
|
|
722
726
|
});
|
|
723
727
|
}
|
package/dist/tools/streaming.js
CHANGED
|
@@ -80,7 +80,7 @@ export const streamingModule = defineToolModule({
|
|
|
80
80
|
});
|
|
81
81
|
const result = await ctx.client.streamStart(parsed.stream, parsed.target);
|
|
82
82
|
if (!result.success) {
|
|
83
|
-
throw new ToolExecutionError(withFailureReason("C64
|
|
83
|
+
throw new ToolExecutionError(withFailureReason("C64 backend reported failure while starting stream", result.details), {
|
|
84
84
|
details: toRecord(result.details),
|
|
85
85
|
});
|
|
86
86
|
}
|
|
@@ -120,7 +120,7 @@ export const streamingModule = defineToolModule({
|
|
|
120
120
|
});
|
|
121
121
|
const result = await ctx.client.streamStop(parsed.stream);
|
|
122
122
|
if (!result.success) {
|
|
123
|
-
throw new ToolExecutionError(withFailureReason("C64
|
|
123
|
+
throw new ToolExecutionError(withFailureReason("C64 backend reported failure while stopping stream", result.details), {
|
|
124
124
|
details: toRecord(result.details),
|
|
125
125
|
});
|
|
126
126
|
}
|
|
@@ -126,7 +126,13 @@ export function assemblyToPrgDetailed(source, options) {
|
|
|
126
126
|
const assembler = new Assembler(statements, loadAddress);
|
|
127
127
|
assembler.runPass(0);
|
|
128
128
|
assembler.runPass(1);
|
|
129
|
-
|
|
129
|
+
const prg = assembler.buildPrg();
|
|
130
|
+
const loadAddressOfPrg = prg.length >= 2 ? prg.readUInt16LE(0) : loadAddress;
|
|
131
|
+
const stubTarget = prg.length >= 2 ? prgEntryAddress(prg) : loadAddressOfPrg;
|
|
132
|
+
const entryAddress = stubTarget !== loadAddressOfPrg
|
|
133
|
+
? stubTarget
|
|
134
|
+
: assembler.getFirstInstructionAddress() ?? loadAddressOfPrg;
|
|
135
|
+
return { prg, symbols: assembler.getSymbols(), entryAddress };
|
|
130
136
|
}
|
|
131
137
|
function normalizeSource(input) {
|
|
132
138
|
return input.replace(/\r\n?/g, "\n");
|
|
@@ -510,8 +516,14 @@ function parseUnaryExpression(stream, terminators) {
|
|
|
510
516
|
}
|
|
511
517
|
// In operand position, "<" and ">" are byte selectors and "*" is the current
|
|
512
518
|
// PC; parseBinaryExpression consumes them as binary operators elsewhere.
|
|
519
|
+
// As in ACME, 64tass, DASM and Kick Assembler, "<" and ">" apply to the
|
|
520
|
+
// whole rest of the expression: "#>msg+1" is the high byte of msg+1.
|
|
521
|
+
if (token.kind === "punct" && (token.value === "<" || token.value === ">")) {
|
|
522
|
+
const operand = parseBinaryExpression(stream, terminators, 0);
|
|
523
|
+
return { type: "unary", op: token.value, operand };
|
|
524
|
+
}
|
|
513
525
|
if (token.kind === "punct" &&
|
|
514
|
-
(token.value === "+" || token.value === "-" || token.value === "!" || token.value === "~"
|
|
526
|
+
(token.value === "+" || token.value === "-" || token.value === "!" || token.value === "~")) {
|
|
515
527
|
const operand = parseUnaryExpression(stream, terminators);
|
|
516
528
|
return { type: "unary", op: token.value, operand };
|
|
517
529
|
}
|
|
@@ -708,6 +720,8 @@ class Assembler {
|
|
|
708
720
|
this.pc = 0;
|
|
709
721
|
this.lowestWritten = MAX_ADDRESS;
|
|
710
722
|
this.highestWritten = 0;
|
|
723
|
+
/** Reserved (.res) ranges from the last pass, as [start, end] inclusive. */
|
|
724
|
+
this.reserved = [];
|
|
711
725
|
this.symbolTable.reset();
|
|
712
726
|
}
|
|
713
727
|
runPass(pass) {
|
|
@@ -719,6 +733,7 @@ class Assembler {
|
|
|
719
733
|
this.highestWritten = 0;
|
|
720
734
|
this.firstInstructionAddress = undefined;
|
|
721
735
|
}
|
|
736
|
+
this.reserved = [];
|
|
722
737
|
// Loading a PRG at $0801 makes BASIC relink the bytes there as program
|
|
723
738
|
// lines, which corrupts raw machine code. Code that starts at $0801
|
|
724
739
|
// without its own org therefore gets a "10 SYS" stub that jumps to the
|
|
@@ -766,13 +781,33 @@ class Assembler {
|
|
|
766
781
|
if (this.lowestWritten > this.highestWritten) {
|
|
767
782
|
return Buffer.alloc(0);
|
|
768
783
|
}
|
|
769
|
-
|
|
770
|
-
|
|
784
|
+
let start = this.lowestWritten;
|
|
785
|
+
let end = this.highestWritten;
|
|
786
|
+
// Reserved space that adjoins the emitted bytes (a trailing "counter: .res 1")
|
|
787
|
+
// is part of the image and zero-filled, as in ca65 and DASM; detached blocks
|
|
788
|
+
// such as zero-page variables are not, so they do not stretch the PRG.
|
|
789
|
+
for (let changed = true; changed;) {
|
|
790
|
+
changed = false;
|
|
791
|
+
for (const [resStart, resEnd] of this.reserved) {
|
|
792
|
+
// Adjoining or overlapping blocks extend the image.
|
|
793
|
+
if (resStart <= end + 1 && resEnd > end) {
|
|
794
|
+
end = resEnd;
|
|
795
|
+
changed = true;
|
|
796
|
+
}
|
|
797
|
+
if (resEnd >= start - 1 && resStart < start) {
|
|
798
|
+
start = resStart;
|
|
799
|
+
changed = true;
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
}
|
|
771
803
|
const body = this.memory.subarray(start, end + 1);
|
|
772
804
|
const header = Buffer.alloc(2);
|
|
773
805
|
header.writeUInt16LE(start, 0);
|
|
774
806
|
return Buffer.concat([header, Buffer.from(body)]);
|
|
775
807
|
}
|
|
808
|
+
getFirstInstructionAddress() {
|
|
809
|
+
return this.firstInstructionAddress;
|
|
810
|
+
}
|
|
776
811
|
getSymbols() {
|
|
777
812
|
return this.symbolTable.allGlobals();
|
|
778
813
|
}
|
|
@@ -898,14 +933,11 @@ class Assembler {
|
|
|
898
933
|
if (amount < 0) {
|
|
899
934
|
throw new AssemblyError("reserve directive requires a non-negative size", location);
|
|
900
935
|
}
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
else {
|
|
907
|
-
this.pc += amount;
|
|
908
|
-
}
|
|
936
|
+
// Reserved space advances the PC without emitting; buildPrg includes it
|
|
937
|
+
// (zero-filled) only where it adjoins emitted bytes.
|
|
938
|
+
if (amount > 0)
|
|
939
|
+
this.reserved.push([this.pc, Math.min(0xffff, this.pc + amount - 1)]);
|
|
940
|
+
this.pc = (this.pc + amount) & 0xffff;
|
|
909
941
|
break;
|
|
910
942
|
}
|
|
911
943
|
case "include": {
|
|
@@ -116,7 +116,11 @@ function tokenize(content) {
|
|
|
116
116
|
}
|
|
117
117
|
continue;
|
|
118
118
|
}
|
|
119
|
-
|
|
119
|
+
// REM text folds only ASCII a-z, like string text (LIST would show them as
|
|
120
|
+
// graphics); full Unicode upper-casing could change length or leave Latin-1.
|
|
121
|
+
const plainChar = inRemark
|
|
122
|
+
? (char >= "a" && char <= "z" ? char.toUpperCase() : char)
|
|
123
|
+
: (!inString ? upper[index] : char) ?? char;
|
|
120
124
|
bytes.push(asciiToPetscii(plainChar));
|
|
121
125
|
if (inData && !inString && plainChar === ":") {
|
|
122
126
|
inData = false;
|
|
@@ -55,7 +55,7 @@ const OPCODE_ROWS = [
|
|
|
55
55
|
// 0xE0-0xEF
|
|
56
56
|
[["CPX", M.Imm], ["SBC", M.IndX], ["NOP", M.Imm], ["INS", M.IndX], ["CPX", M.Zp], ["SBC", M.Zp], ["INC", M.Zp], ["INS", M.Zp], ["INX", M.Imp], ["SBC", M.Imm], ["NOP", M.Imp], ["SBC", M.Imm], ["CPX", M.Abs], ["SBC", M.Abs], ["INC", M.Abs], ["INS", M.Abs]],
|
|
57
57
|
// 0xF0-0xFF
|
|
58
|
-
[["BEQ", M.Rel], ["SBC", M.IndY], ["HLT", M.Imp], ["INS", M.IndY], ["NOP", M.
|
|
58
|
+
[["BEQ", M.Rel], ["SBC", M.IndY], ["HLT", M.Imp], ["INS", M.IndY], ["NOP", M.ZpX], ["SBC", M.ZpX], ["INC", M.ZpX], ["INS", M.ZpX], ["SED", M.Imp], ["SBC", M.AbsY], ["NOP", M.Imp], ["INS", M.AbsY], ["NOP", M.AbsX], ["SBC", M.AbsX], ["INC", M.AbsX], ["INS", M.AbsX]],
|
|
59
59
|
];
|
|
60
60
|
const ILLEGAL_MAP = [
|
|
61
61
|
0x989C, 0x9C9C, 0x888C, 0x9C9C,
|
package/dist/tools/vice.js
CHANGED
|
@@ -33,7 +33,7 @@ const resourceValueSchema = {
|
|
|
33
33
|
},
|
|
34
34
|
};
|
|
35
35
|
const resourceNameSchema = stringSchema({
|
|
36
|
-
description: "VICE resource name (example: SidEngine,
|
|
36
|
+
description: "VICE resource name (example: SidEngine, SidModel).",
|
|
37
37
|
minLength: 1,
|
|
38
38
|
maxLength: 64,
|
|
39
39
|
pattern: RESOURCE_NAME_PATTERN,
|