c64bridge 0.9.0 → 0.9.2

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 (61) hide show
  1. package/README.md +161 -135
  2. package/data/assembly/assembly-spec.md +1 -1
  3. package/data/context/bootstrap.md +3 -3
  4. package/data/context/fast-paths.md +3 -1
  5. package/data/graphics/petscii-style-guide.md +1 -1
  6. package/data/graphics/sprite-charset-best-practices.md +14 -13
  7. package/data/sound/sid-file-structure.md +5 -5
  8. package/data/sound/sid-programming-best-practices.md +5 -5
  9. package/data/vice/vice-binary-monitor-spec.md +26 -10
  10. package/dist/.tsbuildinfo +1 -1
  11. package/dist/c64Client.js +107 -8
  12. package/dist/device.js +43 -3
  13. package/dist/rag/indexer.js +18 -8
  14. package/dist/sidwaveCompiler.js +1 -1
  15. package/dist/tools/debug.js +158 -5
  16. package/dist/tools/input.js +336 -0
  17. package/dist/tools/memory.js +326 -1
  18. package/dist/tools/programRunners.js +45 -6
  19. package/dist/tools/registry/batch.js +167 -0
  20. package/dist/tools/registry/graphics.js +101 -1
  21. package/dist/tools/registry/index.js +4 -0
  22. package/dist/tools/registry/input.js +2 -0
  23. package/dist/tools/registry/memory.js +61 -1
  24. package/dist/tools/responses.js +14 -1
  25. package/dist/tools/schema.js +4 -5
  26. package/dist/{assemblyConverter.js → tools/translation/assembler.js} +66 -163
  27. package/dist/{basicConverter.js → tools/translation/basicTokenizer.js} +1 -1
  28. package/dist/tools/translation/disassembler.js +150 -0
  29. package/dist/tools/translation/opcodeInventory.js +126 -0
  30. package/dist/tools/translation/symbolRegistry.js +49 -0
  31. package/dist/tools/types.js +19 -4
  32. package/dist/vice/process.js +43 -2
  33. package/dist/vice/viceClient.js +5 -1
  34. package/doc/reviews/audit.md +143 -0
  35. package/mcp/schemas/c64_batch.schema.json +53 -0
  36. package/mcp/schemas/c64_config.schema.json +461 -2
  37. package/mcp/schemas/c64_debug.schema.json +641 -3
  38. package/mcp/schemas/c64_disk.schema.json +340 -2
  39. package/mcp/schemas/c64_drive.schema.json +157 -2
  40. package/mcp/schemas/c64_extract.schema.json +359 -2
  41. package/mcp/schemas/c64_graphics.schema.json +415 -3
  42. package/mcp/schemas/c64_input.schema.json +254 -0
  43. package/mcp/schemas/c64_memory.schema.json +562 -3
  44. package/mcp/schemas/c64_printer.schema.json +253 -2
  45. package/mcp/schemas/c64_program.schema.json +499 -2
  46. package/mcp/schemas/c64_rag.schema.json +82 -2
  47. package/mcp/schemas/c64_select_backend.schema.json +35 -2
  48. package/mcp/schemas/c64_sound.schema.json +793 -2
  49. package/mcp/schemas/c64_stream.schema.json +73 -2
  50. package/mcp/schemas/c64_system.schema.json +301 -2
  51. package/mcp/schemas/c64_vice.schema.json +71 -2
  52. package/mcp/server.json +1 -1
  53. package/mcp/tools.json +7509 -1929
  54. package/mcp.json +1 -1
  55. package/package.json +2 -2
  56. package/scripts/c64-cli.mjs +2 -2
  57. package/scripts/run-coverage.mjs +61 -3
  58. package/scripts/run-tests.mjs +1 -0
  59. package/scripts/run-tests.ts +3 -0
  60. package/scripts/update-readme.ts +79 -2
  61. package/server.json +2 -2
@@ -1,8 +1,13 @@
1
1
  import { Buffer } from "node:buffer";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
2
4
  import { defineToolModule, OPERATION_DISCRIMINATOR, } from "./types.js";
3
5
  import { booleanSchema, numberSchema, objectSchema, optionalSchema, stringSchema } from "./schema.js";
4
- import { textResult } from "./responses.js";
6
+ import { jsonResult, textResult } from "./responses.js";
5
7
  import { ToolError, ToolExecutionError, ToolValidationError, toolErrorResult, unknownErrorResult, } from "./errors.js";
8
+ import { disassemble, formatDisassembly } from "./translation/disassembler.js";
9
+ import { getViceSymbols } from "./translation/symbolRegistry.js";
10
+ import { resolveAddressSymbol } from "../knowledge.js";
6
11
  function toRecord(details) {
7
12
  if (details && typeof details === "object") {
8
13
  return details;
@@ -34,6 +39,12 @@ function parseHexInternal(value, path) {
34
39
  if (cleaned.length === 0) {
35
40
  return { bytes: new Uint8Array(), canonical: "$" };
36
41
  }
42
+ if (!/^[0-9A-F]+$/.test(cleaned)) {
43
+ throw new ToolValidationError("Hex string contains non-hexadecimal characters", {
44
+ path,
45
+ details: { value },
46
+ });
47
+ }
37
48
  const bytes = Uint8Array.from(Buffer.from(cleaned, "hex"));
38
49
  return { bytes, canonical: `$${cleaned}` };
39
50
  }
@@ -79,6 +90,18 @@ function supportsMachinePause(ctx) {
79
90
  const platformId = ctx.platform?.id;
80
91
  return platformId !== "vice";
81
92
  }
93
+ export const __memoryHelpersForTests = {
94
+ toRecord,
95
+ normaliseFailure,
96
+ cleanHex,
97
+ parseUserHex,
98
+ parseFirmwareHex,
99
+ formatByte,
100
+ resolveAddressLabel,
101
+ resolveLength,
102
+ supportsMachinePause,
103
+ stripOperationDiscriminator,
104
+ };
82
105
  const readScreenArgsSchema = objectSchema({
83
106
  description: "No arguments are required for reading the current screen contents.",
84
107
  properties: {},
@@ -352,11 +375,313 @@ async function executeWriteMemory(rawArgs, ctx) {
352
375
  return unknownErrorResult(error);
353
376
  }
354
377
  }
378
+ const disassembleArgsSchema = objectSchema({
379
+ description: "Parameters for disassembling a block of VICE memory into 6502/6510 instructions.",
380
+ properties: {
381
+ address: stringSchema({
382
+ description: "Start address as $HHHH, decimal, or symbol name.",
383
+ minLength: 1,
384
+ }),
385
+ length: numberSchema({
386
+ description: "Number of bytes to read and disassemble.",
387
+ integer: true,
388
+ minimum: 1,
389
+ maximum: 4096,
390
+ default: 64,
391
+ }),
392
+ instructionCount: optionalSchema(numberSchema({
393
+ description: "Stop after this many instructions (overrides length as a termination condition).",
394
+ integer: true,
395
+ minimum: 1,
396
+ maximum: 512,
397
+ })),
398
+ },
399
+ required: ["address"],
400
+ additionalProperties: false,
401
+ });
402
+ async function executeDisassemble(rawArgs, ctx) {
403
+ try {
404
+ const parsed = disassembleArgsSchema.parse(rawArgs ?? {});
405
+ const length = parsed.length ?? 64;
406
+ ctx.logger.info("Disassembling VICE memory", { address: parsed.address, length });
407
+ const address = parseAddressArg(parsed.address, "$.address");
408
+ validateRangeWithinAddressSpace(address, length, "$.address");
409
+ const bytes = await ctx.client.readMemoryRaw(address, length);
410
+ const symbols = ctx.platform?.id === "vice" ? getViceSymbols() : undefined;
411
+ const lines = disassemble(bytes, address, parsed.instructionCount, symbols);
412
+ const text = formatDisassembly(lines);
413
+ const addrHex = `$${address.toString(16).toUpperCase().padStart(4, "0")}`;
414
+ return textResult(`Disassembly of ${lines.length} instructions starting at ${addrHex}:\n\n${text}`, {
415
+ success: true,
416
+ address: addrHex,
417
+ bytesRead: bytes.length,
418
+ instructionCount: lines.length,
419
+ });
420
+ }
421
+ catch (error) {
422
+ if (error instanceof ToolError) {
423
+ return toolErrorResult(error);
424
+ }
425
+ return unknownErrorResult(error);
426
+ }
427
+ }
428
+ // ---------------------------------------------------------------------------
429
+ // Shared address-parsing helper
430
+ // ---------------------------------------------------------------------------
431
+ function parseAddressArg(raw, fieldPath) {
432
+ const trimmed = raw.trim();
433
+ let addr;
434
+ if (/^\$[0-9A-Fa-f]+$/.test(trimmed)) {
435
+ addr = parseInt(trimmed.slice(1), 16);
436
+ }
437
+ else if (/^0[xX][0-9A-Fa-f]+$/.test(trimmed)) {
438
+ addr = parseInt(trimmed.slice(2), 16);
439
+ }
440
+ else if (/^[0-9]+$/.test(trimmed)) {
441
+ addr = parseInt(trimmed, 10);
442
+ }
443
+ else {
444
+ const resolved = resolveAddressSymbol(trimmed);
445
+ if (resolved === undefined) {
446
+ throw new ToolValidationError(`Unknown address or symbol: ${trimmed}`, { path: fieldPath });
447
+ }
448
+ addr = resolved;
449
+ }
450
+ if (!Number.isInteger(addr) || addr < 0 || addr > 0xffff) {
451
+ throw new ToolValidationError("Address must be in range $0000-$FFFF", { path: fieldPath });
452
+ }
453
+ return addr;
454
+ }
455
+ function fmtAddr(addr) {
456
+ return `$${addr.toString(16).toUpperCase().padStart(4, "0")}`;
457
+ }
458
+ function validateRangeWithinAddressSpace(start, length, fieldPath) {
459
+ const end = start + length - 1;
460
+ if (end > 0xffff) {
461
+ throw new ToolValidationError(`${fieldPath} range exceeds address space; ${fmtAddr(start)} + ${length} bytes would pass $FFFF.`, { path: fieldPath });
462
+ }
463
+ }
464
+ // ---------------------------------------------------------------------------
465
+ // copy_memory
466
+ // ---------------------------------------------------------------------------
467
+ const copyMemoryArgsSchema = objectSchema({
468
+ description: "Copy a region of RAM from one address to another.",
469
+ properties: {
470
+ source: stringSchema({ description: "Source start address ($HHHH, decimal, or symbol).", minLength: 1 }),
471
+ dest: stringSchema({ description: "Destination start address.", minLength: 1 }),
472
+ length: numberSchema({ description: "Number of bytes to copy (1–16384).", integer: true, minimum: 1, maximum: 16384 }),
473
+ },
474
+ required: ["source", "dest", "length"],
475
+ additionalProperties: false,
476
+ });
477
+ async function executeCopyMemory(rawArgs, ctx) {
478
+ try {
479
+ const parsed = copyMemoryArgsSchema.parse(rawArgs ?? {});
480
+ const src = parseAddressArg(parsed.source, "$.source");
481
+ const dst = parseAddressArg(parsed.dest, "$.dest");
482
+ const len = parsed.length;
483
+ validateRangeWithinAddressSpace(src, len, "$.source");
484
+ validateRangeWithinAddressSpace(dst, len, "$.dest");
485
+ ctx.logger.info("Copying memory", { src: fmtAddr(src), dst: fmtAddr(dst), len });
486
+ const bytes = await ctx.client.readMemoryRaw(src, len);
487
+ await ctx.client.writeMemoryRaw(dst, bytes);
488
+ return textResult(`Copied ${bytes.length} bytes from ${fmtAddr(src)} to ${fmtAddr(dst)}.`, {
489
+ success: true, source: fmtAddr(src), dest: fmtAddr(dst), bytesCopied: bytes.length,
490
+ });
491
+ }
492
+ catch (error) {
493
+ if (error instanceof ToolError)
494
+ return toolErrorResult(error);
495
+ return unknownErrorResult(error);
496
+ }
497
+ }
498
+ // ---------------------------------------------------------------------------
499
+ // fill_memory
500
+ // ---------------------------------------------------------------------------
501
+ const fillMemoryArgsSchema = objectSchema({
502
+ description: "Fill a memory region with a repeating byte pattern.",
503
+ properties: {
504
+ address: stringSchema({ description: "Start address ($HHHH, decimal, or symbol).", minLength: 1 }),
505
+ length: numberSchema({ description: "Number of bytes to fill (1–16384).", integer: true, minimum: 1, maximum: 16384 }),
506
+ pattern: stringSchema({
507
+ description: "Hex bytes to repeat, e.g. 'FF' or 'AA 55'. Space-separated or run together.",
508
+ minLength: 2,
509
+ pattern: /^[\s0-9A-Fa-f$]+$/,
510
+ }),
511
+ },
512
+ required: ["address", "length", "pattern"],
513
+ additionalProperties: false,
514
+ });
515
+ async function executeFillMemory(rawArgs, ctx) {
516
+ try {
517
+ const parsed = fillMemoryArgsSchema.parse(rawArgs ?? {});
518
+ const addr = parseAddressArg(parsed.address, "$.address");
519
+ validateRangeWithinAddressSpace(addr, parsed.length, "$.address");
520
+ const { bytes: patternBytes } = parseUserHex(parsed.pattern, "$.pattern");
521
+ if (patternBytes.length === 0)
522
+ throw new ToolValidationError("Pattern must not be empty", { path: "$.pattern" });
523
+ const buf = new Uint8Array(parsed.length);
524
+ for (let i = 0; i < parsed.length; i++)
525
+ buf[i] = patternBytes[i % patternBytes.length];
526
+ await ctx.client.writeMemoryRaw(addr, buf);
527
+ ctx.logger.info("Filled memory", { address: fmtAddr(addr), length: parsed.length, pattern: parsed.pattern });
528
+ return textResult(`Filled ${parsed.length} bytes at ${fmtAddr(addr)} with pattern ${parsed.pattern.trim()}.`, {
529
+ success: true, address: fmtAddr(addr), length: parsed.length, pattern: parsed.pattern.trim(),
530
+ });
531
+ }
532
+ catch (error) {
533
+ if (error instanceof ToolError)
534
+ return toolErrorResult(error);
535
+ return unknownErrorResult(error);
536
+ }
537
+ }
538
+ // ---------------------------------------------------------------------------
539
+ // search_memory
540
+ // ---------------------------------------------------------------------------
541
+ const searchMemoryArgsSchema = objectSchema({
542
+ description: "Search for a byte pattern within a memory range.",
543
+ properties: {
544
+ startAddress: stringSchema({ description: "Start of search range ($HHHH, decimal, or symbol).", minLength: 1 }),
545
+ endAddress: stringSchema({ description: "End of search range (inclusive).", minLength: 1 }),
546
+ pattern: stringSchema({
547
+ description: "Hex bytes to find, e.g. 'A9 00' for LDA #$00.",
548
+ minLength: 2,
549
+ pattern: /^[\s0-9A-Fa-f$]+$/,
550
+ }),
551
+ maxResults: optionalSchema(numberSchema({ description: "Maximum matches to return (default 10).", integer: true, minimum: 1, maximum: 100, default: 10 })),
552
+ },
553
+ required: ["startAddress", "endAddress", "pattern"],
554
+ additionalProperties: false,
555
+ });
556
+ async function executeSearchMemory(rawArgs, ctx) {
557
+ try {
558
+ const parsed = searchMemoryArgsSchema.parse(rawArgs ?? {});
559
+ const start = parseAddressArg(parsed.startAddress, "$.startAddress");
560
+ const end = parseAddressArg(parsed.endAddress, "$.endAddress");
561
+ if (end < start)
562
+ throw new ToolValidationError("endAddress must be ≥ startAddress", { path: "$.endAddress" });
563
+ const { bytes: needle } = parseUserHex(parsed.pattern, "$.pattern");
564
+ if (needle.length === 0)
565
+ throw new ToolValidationError("Pattern must not be empty", { path: "$.pattern" });
566
+ const maxResults = parsed.maxResults ?? 10;
567
+ const len = end - start + 1;
568
+ ctx.logger.info("Searching memory", { start: fmtAddr(start), end: fmtAddr(end), pattern: parsed.pattern });
569
+ const haystack = await ctx.client.readMemoryRaw(start, len);
570
+ const matches = [];
571
+ outer: for (let i = 0; i <= haystack.length - needle.length && matches.length < maxResults; i++) {
572
+ for (let j = 0; j < needle.length; j++) {
573
+ if (haystack[i + j] !== needle[j])
574
+ continue outer;
575
+ }
576
+ matches.push(fmtAddr(start + i));
577
+ }
578
+ return jsonResult({ found: matches.length, matches, pattern: parsed.pattern.trim(), range: { start: fmtAddr(start), end: fmtAddr(end) } }, { success: true, found: matches.length });
579
+ }
580
+ catch (error) {
581
+ if (error instanceof ToolError)
582
+ return toolErrorResult(error);
583
+ return unknownErrorResult(error);
584
+ }
585
+ }
586
+ // ---------------------------------------------------------------------------
587
+ // compare_memory
588
+ // ---------------------------------------------------------------------------
589
+ const compareMemoryArgsSchema = objectSchema({
590
+ description: "Compare two memory regions and report differing bytes.",
591
+ properties: {
592
+ address1: stringSchema({ description: "First region start address.", minLength: 1 }),
593
+ address2: stringSchema({ description: "Second region start address.", minLength: 1 }),
594
+ length: numberSchema({ description: "Number of bytes to compare (1–16384).", integer: true, minimum: 1, maximum: 16384 }),
595
+ maxDiffs: optionalSchema(numberSchema({ description: "Max differences to report (default 10).", integer: true, minimum: 1, maximum: 200, default: 10 })),
596
+ },
597
+ required: ["address1", "address2", "length"],
598
+ additionalProperties: false,
599
+ });
600
+ async function executeCompareMemory(rawArgs, ctx) {
601
+ try {
602
+ const parsed = compareMemoryArgsSchema.parse(rawArgs ?? {});
603
+ const a1 = parseAddressArg(parsed.address1, "$.address1");
604
+ const a2 = parseAddressArg(parsed.address2, "$.address2");
605
+ const len = parsed.length;
606
+ const maxDiffs = parsed.maxDiffs ?? 10;
607
+ validateRangeWithinAddressSpace(a1, len, "$.address1");
608
+ validateRangeWithinAddressSpace(a2, len, "$.address2");
609
+ ctx.logger.info("Comparing memory regions", { a1: fmtAddr(a1), a2: fmtAddr(a2), len });
610
+ const [r1, r2] = await Promise.all([
611
+ ctx.client.readMemoryRaw(a1, len),
612
+ ctx.client.readMemoryRaw(a2, len),
613
+ ]);
614
+ const diffs = [];
615
+ for (let i = 0; i < len && diffs.length < maxDiffs; i++) {
616
+ if (r1[i] !== r2[i]) {
617
+ diffs.push({
618
+ offset: i,
619
+ address1: fmtAddr(a1 + i),
620
+ address2: fmtAddr(a2 + i),
621
+ value1: formatByte(r1[i] ?? 0),
622
+ value2: formatByte(r2[i] ?? 0),
623
+ });
624
+ }
625
+ }
626
+ const identical = diffs.length === 0;
627
+ return jsonResult({ identical, diffCount: diffs.length, diffs, region1: fmtAddr(a1), region2: fmtAddr(a2), length: len }, { success: true, identical, diffCount: diffs.length });
628
+ }
629
+ catch (error) {
630
+ if (error instanceof ToolError)
631
+ return toolErrorResult(error);
632
+ return unknownErrorResult(error);
633
+ }
634
+ }
635
+ // ---------------------------------------------------------------------------
636
+ // save_memory
637
+ // ---------------------------------------------------------------------------
638
+ const saveMemoryArgsSchema = objectSchema({
639
+ description: "Dump a memory region to a local file, optionally with a PRG load-address header.",
640
+ properties: {
641
+ startAddress: stringSchema({ description: "Start address of region to save.", minLength: 1 }),
642
+ endAddress: stringSchema({ description: "End address (inclusive).", minLength: 1 }),
643
+ filePath: stringSchema({ description: "Absolute or relative path for the output file.", minLength: 1 }),
644
+ asPrg: optionalSchema(booleanSchema({ description: "Prepend a 2-byte little-endian load-address header (PRG format). Default: true.", default: true })),
645
+ },
646
+ required: ["startAddress", "endAddress", "filePath"],
647
+ additionalProperties: false,
648
+ });
649
+ async function executeSaveMemory(rawArgs, ctx) {
650
+ try {
651
+ const parsed = saveMemoryArgsSchema.parse(rawArgs ?? {});
652
+ const start = parseAddressArg(parsed.startAddress, "$.startAddress");
653
+ const end = parseAddressArg(parsed.endAddress, "$.endAddress");
654
+ if (end < start)
655
+ throw new ToolValidationError("endAddress must be ≥ startAddress", { path: "$.endAddress" });
656
+ const asPrg = parsed.asPrg !== false;
657
+ const len = end - start + 1;
658
+ const resolvedPath = path.resolve(parsed.filePath);
659
+ ctx.logger.info("Saving memory to file", { start: fmtAddr(start), end: fmtAddr(end), filePath: resolvedPath, asPrg });
660
+ const bytes = await ctx.client.readMemoryRaw(start, len);
661
+ const header = asPrg ? Buffer.from([start & 0xff, (start >> 8) & 0xff]) : Buffer.alloc(0);
662
+ await fs.promises.mkdir(path.dirname(resolvedPath), { recursive: true });
663
+ await fs.promises.writeFile(resolvedPath, Buffer.concat([header, Buffer.from(bytes)]));
664
+ const fileSize = bytes.length + header.length;
665
+ return textResult(`Saved ${bytes.length} bytes (${fmtAddr(start)}–${fmtAddr(end)}) to ${resolvedPath}${asPrg ? " (PRG, with load address header)" : ""}.`, { success: true, filePath: resolvedPath, bytesRead: bytes.length, fileSize, asPrg, startAddress: fmtAddr(start), endAddress: fmtAddr(end) });
666
+ }
667
+ catch (error) {
668
+ if (error instanceof ToolError)
669
+ return toolErrorResult(error);
670
+ return unknownErrorResult(error);
671
+ }
672
+ }
355
673
  export const memoryOperationHandlers = {
356
674
  read: async (args, ctx) => executeReadMemory(stripOperationDiscriminator(args), ctx),
357
675
  write: async (args, ctx) => executeWriteMemory(stripOperationDiscriminator(args), ctx),
358
676
  read_screen: async (args, ctx) => executeReadScreen(stripOperationDiscriminator(args), ctx),
677
+ disassemble: async (args, ctx) => executeDisassemble(stripOperationDiscriminator(args), ctx),
678
+ copy_memory: async (args, ctx) => executeCopyMemory(stripOperationDiscriminator(args), ctx),
679
+ fill_memory: async (args, ctx) => executeFillMemory(stripOperationDiscriminator(args), ctx),
680
+ search_memory: async (args, ctx) => executeSearchMemory(stripOperationDiscriminator(args), ctx),
681
+ compare_memory: async (args, ctx) => executeCompareMemory(stripOperationDiscriminator(args), ctx),
682
+ save_memory: async (args, ctx) => executeSaveMemory(stripOperationDiscriminator(args), ctx),
359
683
  };
684
+ export { disassembleArgsSchema, copyMemoryArgsSchema, fillMemoryArgsSchema, searchMemoryArgsSchema, compareMemoryArgsSchema, saveMemoryArgsSchema, };
360
685
  export const memoryModule = defineToolModule({
361
686
  domain: "memory",
362
687
  summary: "Screen, main memory, and low-level inspection utilities.",
@@ -1,10 +1,12 @@
1
- import { assemblyToPrg, AssemblyError } from "../assemblyConverter.js";
2
- import { basicToPrg } from "../basicConverter.js";
1
+ import fs from "node:fs";
2
+ import { assemblyToPrgDetailed, AssemblyError } from "./translation/assembler.js";
3
+ import { basicToPrg } from "./translation/basicTokenizer.js";
3
4
  import { defineToolModule, OPERATION_DISCRIMINATOR, } from "./types.js";
4
- import { booleanSchema, objectSchema, stringSchema } from "./schema.js";
5
+ import { booleanSchema, objectSchema, optionalSchema, stringSchema } from "./schema.js";
5
6
  import { textResult } from "./responses.js";
6
7
  import { ToolExecutionError, ToolError, ToolValidationError, toolErrorResult, unknownErrorResult, } from "./errors.js";
7
8
  import { pollForProgramOutcome } from "./pollValidator.js";
9
+ import { clearViceSymbols, setViceSymbols, parseViceSymbolFile } from "./translation/symbolRegistry.js";
8
10
  function extractFailureDetails(details) {
9
11
  if (details === undefined || details === null) {
10
12
  return undefined;
@@ -163,6 +165,14 @@ function normalizeRuntimeErrors(errors) {
163
165
  text: error.raw,
164
166
  }));
165
167
  }
168
+ export const __programRunnerHelpersForTests = {
169
+ extractFailureDetails,
170
+ toRecord,
171
+ stripOperationDiscriminator,
172
+ parseBasicRuntimeErrors,
173
+ attemptAutoFixBasicProgram,
174
+ normalizeRuntimeErrors,
175
+ };
166
176
  function structuredExecutionError(message, data) {
167
177
  return {
168
178
  content: [
@@ -221,6 +231,10 @@ const prgFileArgsSchema = objectSchema({
221
231
  description: prgPathDescription,
222
232
  minLength: 1,
223
233
  }),
234
+ symbolsFile: optionalSchema(stringSchema({
235
+ description: "Path to a VICE symbol file (.vs) to load alongside the PRG. Symbols are used by the disassemble tool when annotating addresses.",
236
+ minLength: 1,
237
+ })),
224
238
  },
225
239
  required: ["path"],
226
240
  additionalProperties: false,
@@ -424,9 +438,13 @@ async function executeUploadRunAsm(rawArgs, ctx) {
424
438
  sourceLength: parsed.program.length,
425
439
  ...(shouldVerify ? { verify: true } : {}),
426
440
  });
427
- // Assemble locally to expose structured metadata
428
- const prg = assemblyToPrg(parsed.program);
441
+ // Assemble locally to expose structured metadata and capture symbols
442
+ const asmResult = assemblyToPrgDetailed(parsed.program);
443
+ const prg = asmResult.prg;
429
444
  const entryAddress = prg.readUInt16LE(0);
445
+ if (ctx.platform.id === "vice") {
446
+ setViceSymbols(asmResult.symbols.entries());
447
+ }
430
448
  const result = await ctx.client.uploadAndRunAsm(parsed.program);
431
449
  if (!result.success) {
432
450
  return toolErrorResult(new ToolExecutionError("C64 firmware reported failure while running assembly program", {
@@ -527,6 +545,24 @@ async function executeRunPrg(rawArgs, ctx) {
527
545
  try {
528
546
  const parsed = prgFileArgsSchema.parse(rawArgs ?? {});
529
547
  ctx.logger.info("Running PRG file", { path: parsed.path });
548
+ let symbolsLoaded = 0;
549
+ if (ctx.platform.id === "vice") {
550
+ clearViceSymbols();
551
+ if (parsed.symbolsFile) {
552
+ try {
553
+ const content = await fs.promises.readFile(parsed.symbolsFile, "utf8");
554
+ const syms = parseViceSymbolFile(content);
555
+ setViceSymbols(syms.entries());
556
+ symbolsLoaded = syms.size;
557
+ ctx.logger.info("Loaded VICE symbols from file", { path: parsed.symbolsFile, count: symbolsLoaded });
558
+ }
559
+ catch (fileError) {
560
+ throw new ToolExecutionError(`Failed to read symbols file: ${parsed.symbolsFile}`, {
561
+ details: { message: fileError instanceof Error ? fileError.message : String(fileError) },
562
+ });
563
+ }
564
+ }
565
+ }
530
566
  const result = await ctx.client.runPrgFile(parsed.path);
531
567
  if (!result.success) {
532
568
  throw new ToolExecutionError("C64 firmware reported failure while running PRG", {
@@ -539,12 +575,15 @@ async function executeRunPrg(rawArgs, ctx) {
539
575
  path: parsed.path,
540
576
  entryAddress: null,
541
577
  resources: ["c64://guide/bootstrap"],
578
+ ...(symbolsLoaded > 0 ? { symbolsLoaded } : {}),
542
579
  };
543
- const base = textResult(`PRG ${parsed.path} loaded and executed.`, {
580
+ const symbolNote = symbolsLoaded > 0 ? ` Loaded ${symbolsLoaded} debug symbol(s).` : "";
581
+ const base = textResult(`PRG ${parsed.path} loaded and executed.${symbolNote}`, {
544
582
  success: true,
545
583
  path: parsed.path,
546
584
  entryAddress: null,
547
585
  details: toRecord(result.details) ?? null,
586
+ ...(symbolsLoaded > 0 ? { symbolsLoaded } : {}),
548
587
  });
549
588
  return { ...base, structuredContent: { type: "json", data } };
550
589
  }
@@ -0,0 +1,167 @@
1
+ import { defineToolModule, } from "../types.js";
2
+ import { getPlatformStatus } from "../../platform.js";
3
+ import { arraySchema, booleanSchema, objectSchema, optionalSchema, stringSchema, } from "../schema.js";
4
+ import { jsonResult } from "../responses.js";
5
+ import { ToolValidationError, unknownErrorResult } from "../errors.js";
6
+ const commandSchema = objectSchema({
7
+ description: "A single batch command.",
8
+ properties: {
9
+ tool: stringSchema({ description: "MCP tool name (e.g. c64_memory, c64_debug).", minLength: 1 }),
10
+ args: objectSchema({
11
+ description: "Arguments to pass to the tool (must include 'op' for grouped tools).",
12
+ properties: {},
13
+ additionalProperties: true,
14
+ }),
15
+ description: optionalSchema(stringSchema({ description: "Optional label for this command in the result.", maxLength: 128 })),
16
+ },
17
+ required: ["tool", "args"],
18
+ additionalProperties: false,
19
+ });
20
+ const executeBatchArgsSchema = objectSchema({
21
+ description: "Execute multiple tool calls in one request.",
22
+ properties: {
23
+ commands: arraySchema(commandSchema, {
24
+ description: "Ordered list of commands to execute.",
25
+ minItems: 1,
26
+ }),
27
+ stopOnError: optionalSchema(booleanSchema({
28
+ description: "Halt the batch after the first failure. Default: true.",
29
+ default: true,
30
+ })),
31
+ },
32
+ required: ["commands"],
33
+ additionalProperties: false,
34
+ });
35
+ export const batchModuleGroup = defineToolModule({
36
+ domain: "batch",
37
+ summary: "Execute multiple tool calls in a single MCP request for reduced round-trip overhead.",
38
+ supportedPlatforms: ["c64u", "vice"],
39
+ resources: [],
40
+ prompts: [],
41
+ defaultTags: ["batch", "orchestration"],
42
+ workflowHints: [
43
+ "Use execute_batch when you need to run 3 or more sequential operations — it eliminates per-call network overhead.",
44
+ "Each command must include 'tool' and 'args'; grouped tools require 'op' inside args.",
45
+ ],
46
+ tools: [
47
+ {
48
+ name: "c64_batch",
49
+ description: "Execute multiple c64bridge tool calls in a single request. Reduces latency for multi-step workflows.",
50
+ summary: "Runs an ordered list of tool commands and returns all results with per-command timing.",
51
+ inputSchema: executeBatchArgsSchema.jsonSchema,
52
+ tags: ["batch", "orchestration", "grouped"],
53
+ examples: [
54
+ {
55
+ name: "Read registers then step",
56
+ description: "Get registers, step one instruction, get registers again",
57
+ arguments: {
58
+ commands: [
59
+ { tool: "c64_debug", args: { op: "get_registers" }, description: "before" },
60
+ { tool: "c64_debug", args: { op: "step", count: 1 }, description: "step" },
61
+ { tool: "c64_debug", args: { op: "get_registers" }, description: "after" },
62
+ ],
63
+ },
64
+ },
65
+ {
66
+ name: "Clear screen and type text",
67
+ description: "Fill screen RAM then feed keyboard",
68
+ arguments: {
69
+ commands: [
70
+ { tool: "c64_memory", args: { op: "fill_memory", address: "$0400", length: 1000, pattern: "20" } },
71
+ { tool: "c64_input", args: { op: "write_text", text: "HELLO{RETURN}" } },
72
+ ],
73
+ },
74
+ },
75
+ ],
76
+ execute: async (rawArgs, ctx) => {
77
+ try {
78
+ const parsed = executeBatchArgsSchema.parse(rawArgs);
79
+ const stopOnError = parsed.stopOnError !== false;
80
+ // Lazy import to avoid circular dependency at module-init time.
81
+ const { toolRegistry } = await import("./index.js");
82
+ const results = [];
83
+ let succeeded = 0;
84
+ let failed = 0;
85
+ const batchStart = Date.now();
86
+ for (let i = 0; i < parsed.commands.length; i++) {
87
+ const cmd = parsed.commands[i];
88
+ const toolName = typeof cmd.tool === "string" ? cmd.tool : "";
89
+ const args = (typeof cmd.args === "object" && cmd.args !== null) ? cmd.args : {};
90
+ const label = typeof cmd.description === "string" ? cmd.description : undefined;
91
+ if (!toolName) {
92
+ const err = { index: i, tool: String(cmd.tool), description: label, success: false, error: "tool name must be a non-empty string", elapsedMs: 0 };
93
+ results.push(err);
94
+ failed++;
95
+ if (stopOnError)
96
+ break;
97
+ continue;
98
+ }
99
+ if (toolName === "c64_batch") {
100
+ results.push({
101
+ index: i,
102
+ tool: toolName,
103
+ description: label,
104
+ success: false,
105
+ error: "Nested c64_batch execution is not supported.",
106
+ elapsedMs: 0,
107
+ });
108
+ failed++;
109
+ if (stopOnError)
110
+ break;
111
+ continue;
112
+ }
113
+ const cmdStart = Date.now();
114
+ try {
115
+ const toolResult = await toolRegistry.invoke(toolName, args, {
116
+ ...ctx,
117
+ platform: getPlatformStatus(),
118
+ });
119
+ const elapsedMs = Date.now() - cmdStart;
120
+ const isError = toolResult.isError === true;
121
+ const text = toolResult.content.map((c) => c.text).join("\n");
122
+ const resultPayload = toolResult.structuredContent?.data ?? text;
123
+ if (isError) {
124
+ results.push({ index: i, tool: toolName, description: label, success: false, error: text, elapsedMs });
125
+ failed++;
126
+ if (stopOnError)
127
+ break;
128
+ }
129
+ else {
130
+ results.push({ index: i, tool: toolName, description: label, success: true, result: resultPayload, elapsedMs });
131
+ succeeded++;
132
+ }
133
+ }
134
+ catch (err) {
135
+ const elapsedMs = Date.now() - cmdStart;
136
+ const message = err instanceof Error ? err.message : String(err);
137
+ results.push({ index: i, tool: toolName, description: label, success: false, error: message, elapsedMs });
138
+ failed++;
139
+ if (stopOnError)
140
+ break;
141
+ }
142
+ }
143
+ const totalElapsedMs = Date.now() - batchStart;
144
+ const summary = {
145
+ total: parsed.commands.length,
146
+ executed: results.length,
147
+ succeeded,
148
+ failed,
149
+ elapsedMs: totalElapsedMs,
150
+ results,
151
+ };
152
+ ctx.logger.info("Batch complete", { total: parsed.commands.length, succeeded, failed, elapsedMs: totalElapsedMs });
153
+ return jsonResult(summary, { success: failed === 0, succeeded, failed, total: parsed.commands.length });
154
+ }
155
+ catch (error) {
156
+ if (error instanceof ToolValidationError) {
157
+ return {
158
+ content: [{ type: "text", text: error.message }],
159
+ isError: true,
160
+ };
161
+ }
162
+ return unknownErrorResult(error);
163
+ }
164
+ },
165
+ },
166
+ ],
167
+ });