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.
- package/README.md +161 -135
- package/data/assembly/assembly-spec.md +1 -1
- package/data/context/bootstrap.md +3 -3
- package/data/context/fast-paths.md +3 -1
- package/data/graphics/petscii-style-guide.md +1 -1
- package/data/graphics/sprite-charset-best-practices.md +14 -13
- package/data/sound/sid-file-structure.md +5 -5
- package/data/sound/sid-programming-best-practices.md +5 -5
- package/data/vice/vice-binary-monitor-spec.md +26 -10
- package/dist/.tsbuildinfo +1 -1
- package/dist/c64Client.js +107 -8
- package/dist/device.js +43 -3
- package/dist/rag/indexer.js +18 -8
- package/dist/sidwaveCompiler.js +1 -1
- package/dist/tools/debug.js +158 -5
- package/dist/tools/input.js +336 -0
- package/dist/tools/memory.js +326 -1
- package/dist/tools/programRunners.js +45 -6
- package/dist/tools/registry/batch.js +167 -0
- package/dist/tools/registry/graphics.js +101 -1
- package/dist/tools/registry/index.js +4 -0
- package/dist/tools/registry/input.js +2 -0
- package/dist/tools/registry/memory.js +61 -1
- package/dist/tools/responses.js +14 -1
- package/dist/tools/schema.js +4 -5
- package/dist/{assemblyConverter.js → tools/translation/assembler.js} +66 -163
- package/dist/{basicConverter.js → tools/translation/basicTokenizer.js} +1 -1
- package/dist/tools/translation/disassembler.js +150 -0
- package/dist/tools/translation/opcodeInventory.js +126 -0
- package/dist/tools/translation/symbolRegistry.js +49 -0
- package/dist/tools/types.js +19 -4
- package/dist/vice/process.js +43 -2
- package/dist/vice/viceClient.js +5 -1
- package/doc/reviews/audit.md +143 -0
- package/mcp/schemas/c64_batch.schema.json +53 -0
- package/mcp/schemas/c64_config.schema.json +461 -2
- package/mcp/schemas/c64_debug.schema.json +641 -3
- package/mcp/schemas/c64_disk.schema.json +340 -2
- package/mcp/schemas/c64_drive.schema.json +157 -2
- package/mcp/schemas/c64_extract.schema.json +359 -2
- package/mcp/schemas/c64_graphics.schema.json +415 -3
- package/mcp/schemas/c64_input.schema.json +254 -0
- package/mcp/schemas/c64_memory.schema.json +562 -3
- package/mcp/schemas/c64_printer.schema.json +253 -2
- package/mcp/schemas/c64_program.schema.json +499 -2
- package/mcp/schemas/c64_rag.schema.json +82 -2
- package/mcp/schemas/c64_select_backend.schema.json +35 -2
- package/mcp/schemas/c64_sound.schema.json +793 -2
- package/mcp/schemas/c64_stream.schema.json +73 -2
- package/mcp/schemas/c64_system.schema.json +301 -2
- package/mcp/schemas/c64_vice.schema.json +71 -2
- package/mcp/server.json +1 -1
- package/mcp/tools.json +7509 -1929
- package/mcp.json +1 -1
- package/package.json +2 -2
- package/scripts/c64-cli.mjs +2 -2
- package/scripts/run-coverage.mjs +61 -3
- package/scripts/run-tests.mjs +1 -0
- package/scripts/run-tests.ts +3 -0
- package/scripts/update-readme.ts +79 -2
- package/server.json +2 -2
package/dist/tools/memory.js
CHANGED
|
@@ -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
|
|
2
|
-
import {
|
|
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
|
|
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
|
|
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
|
+
});
|