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.
Files changed (58) hide show
  1. package/AGENTS.md +1 -1
  2. package/README.md +3 -3
  3. package/data/sound/sidwave.md +113 -36
  4. package/data/sound/sidwave.schema.json +138 -28
  5. package/dist/.tsbuildinfo +1 -1
  6. package/dist/c64Client.js +83 -93
  7. package/dist/chargen.js +0 -4
  8. package/dist/config.js +26 -6
  9. package/dist/device.js +130 -24
  10. package/dist/diagnostics.js +14 -5
  11. package/dist/logger.js +3 -2
  12. package/dist/loggingHttpClient.js +12 -1
  13. package/dist/mcp-server.js +19 -9
  14. package/dist/petscii.js +0 -5
  15. package/dist/redact.js +166 -0
  16. package/dist/sidwave.js +453 -61
  17. package/dist/sidwaveCompiler.js +1424 -269
  18. package/dist/tools/audio.js +70 -12
  19. package/dist/tools/developer.js +17 -9
  20. package/dist/tools/errors.js +7 -0
  21. package/dist/tools/graphics.js +4 -4
  22. package/dist/tools/input.js +10 -13
  23. package/dist/tools/machineControl.js +7 -7
  24. package/dist/tools/memory.js +6 -6
  25. package/dist/tools/meta/audio.js +8 -2
  26. package/dist/tools/meta/config.js +5 -4
  27. package/dist/tools/meta/configInventory.js +25 -2
  28. package/dist/tools/meta/diagnostics.js +2 -1
  29. package/dist/tools/meta/graphics.js +1 -1
  30. package/dist/tools/meta/memory.js +4 -4
  31. package/dist/tools/meta/program.js +12 -8
  32. package/dist/tools/printer.js +4 -4
  33. package/dist/tools/programRunners.js +8 -8
  34. package/dist/tools/registry/batch.js +1 -7
  35. package/dist/tools/registry/disk.js +7 -5
  36. package/dist/tools/storage.js +21 -17
  37. package/dist/tools/streaming.js +2 -2
  38. package/dist/tools/translation/assembler.js +44 -12
  39. package/dist/tools/translation/basicTokenizer.js +5 -1
  40. package/dist/tools/translation/opcodeInventory.js +1 -1
  41. package/dist/tools/vice.js +1 -1
  42. package/dist/vice/mockServer.js +92 -5
  43. package/dist/vice/readiness.js +4 -9
  44. package/dist/vice/viceClient.js +18 -0
  45. package/doc/audit/secret-handling.md +61 -0
  46. package/mcp/schemas/c64_config.schema.json +19 -2
  47. package/mcp/schemas/c64_disk.schema.json +2 -2
  48. package/mcp/schemas/c64_input.schema.json +3 -3
  49. package/mcp/schemas/c64_program.schema.json +2 -2
  50. package/mcp/schemas/c64_vice.schema.json +3 -3
  51. package/mcp/server.json +1 -1
  52. package/mcp/tools.json +33 -12
  53. package/mcp.json +1 -1
  54. package/package.json +5 -2
  55. package/scripts/mockC64Server.mjs +15 -1
  56. package/scripts/run-coverage.mjs +31 -1
  57. package/scripts/update-readme.ts +3 -2
  58. 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 value = normalizeSnapshotValue(await client.configGet(entry.category, entry.item), entry);
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
- return jsonResult(report, { success: report.isHealthy });
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 firmware reported failure while reading memory", { details: normalizeErrorDetails(read.details) });
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 firmware reported failure while pausing", { details: normalizeErrorDetails(pauseResult.details) });
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 firmware reported failure while reading memory", { details: normalizeErrorDetails(pre.details) });
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 firmware reported failure while writing memory", { details: normalizeErrorDetails(write.details) });
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 firmware reported failure while reading back memory", { details: normalizeErrorDetails(post.details) });
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
- // Check SID gate bits are off (simple check)
724
- const result = await ctx.client.readMemory("$D404", "1");
725
- if (!result.success || typeof result.data !== "string") {
726
- throw new Error("Failed to read SID voice 1 control register for sid_silent assertion");
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 gate1 = parseInt(result.data.slice(1), 16) & 0x01;
729
- const isSilent = gate1 === 0;
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 : "SID voice 1 gate is on",
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";
@@ -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 firmware reported failure while printing text", {
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 firmware reported failure while printing Commodore bitmap", {
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 firmware reported failure while printing Epson bitmap", {
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 firmware reported failure while defining DLL characters", {
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, prgEntryAddress } from "./translation/assembler.js";
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 firmware reported failure while running BASIC program", {
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 = prgEntryAddress(prg);
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 firmware reported failure while running assembly program", {
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 firmware reported failure while loading PRG", {
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 firmware reported failure while running PRG", {
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 firmware reported failure while running CRT", {
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 firmware reported failure while running CRT", {
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 { ToolValidationError, unknownErrorResult } from "../errors.js";
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 or 40; DNP requires explicit tracks).",
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 !== 35 && tracks !== 40) {
214
- throw new ToolValidationError("D64 images support 35 or 40 tracks", {
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: { allowed: [35, 40], received: tracks },
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
- operationPlatforms: { file_info: ["c64u", "u2"], create_image: ["c64u", "u2"], find_and_run: ["c64u", "u2"] },
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",
@@ -97,10 +97,10 @@ const createD64ArgsSchema = objectSchema({
97
97
  properties: {
98
98
  path: diskImagePathSchema,
99
99
  tracks: optionalSchema(numberSchema({
100
- description: "Track count (35 for standard, 40 for extended).",
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: 40,
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 Ultimate drive status.", {
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 firmware reported failure while mounting disk image", {
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
- return textResult(`Mounted ${parsed.image} on ${parsed.drive}.`, {
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 firmware reported failure while removing disk image", {
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 firmware reported failure while resetting drive", {
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 firmware reported failure while powering on drive", {
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 firmware reported failure while powering off drive", {
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 firmware reported failure while loading drive ROM", {
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 firmware reported failure while setting drive mode", {
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 or 40 track D64 image with an optional disk name.",
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 firmware reported failure while creating D64 image", {
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
- tracks: parsed.tracks ?? null,
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 firmware reported failure while creating D71 image", {
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 firmware reported failure while creating D81 image", {
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 firmware reported failure while creating DNP image", {
724
+ throw new ToolExecutionError("C64 backend reported failure while creating DNP image", {
721
725
  details: normaliseFailure(result.details),
722
726
  });
723
727
  }
@@ -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 firmware reported failure while starting stream", result.details), {
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 firmware reported failure while stopping stream", result.details), {
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
- return { prg: assembler.buildPrg(), symbols: assembler.getSymbols() };
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 === "~" || 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
- const start = this.lowestWritten;
770
- const end = this.highestWritten;
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
- if (pass === 1) {
902
- for (let i = 0; i < amount; i += 1) {
903
- this.writeByte(0, location);
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
- const plainChar = (!inString && !inRemark ? upper[index] : char) ?? char;
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.ZpS], ["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]],
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,
@@ -33,7 +33,7 @@ const resourceValueSchema = {
33
33
  },
34
34
  };
35
35
  const resourceNameSchema = stringSchema({
36
- description: "VICE resource name (example: SidEngine, C64Model).",
36
+ description: "VICE resource name (example: SidEngine, SidModel).",
37
37
  minLength: 1,
38
38
  maxLength: 64,
39
39
  pattern: RESOURCE_NAME_PATTERN,