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/c64Client.js
CHANGED
|
@@ -8,8 +8,8 @@ See <https://www.gnu.org/licenses/> for details.
|
|
|
8
8
|
import { Buffer } from "node:buffer";
|
|
9
9
|
import { createSocket } from "node:dgram";
|
|
10
10
|
import axios from "axios";
|
|
11
|
-
import { basicToPrg } from "./
|
|
12
|
-
import { assemblyToPrg } from "./
|
|
11
|
+
import { basicToPrg } from "./tools/translation/basicTokenizer.js";
|
|
12
|
+
import { assemblyToPrg } from "./tools/translation/assembler.js";
|
|
13
13
|
import { screenCodesToAscii } from "./petscii.js";
|
|
14
14
|
import { resolveAddressSymbol } from "./knowledge.js";
|
|
15
15
|
import { createAllFacades, createFacade } from "./device.js";
|
|
@@ -28,6 +28,17 @@ const DEFAULT_TEXT_FOREGROUND = 1;
|
|
|
28
28
|
const DEFAULT_BORDER_COLOR = 6;
|
|
29
29
|
const DEFAULT_BACKGROUND_COLOR = 0;
|
|
30
30
|
const SPACE_SCREEN_CODE = 0x20;
|
|
31
|
+
function validateMemoryRange(address, length) {
|
|
32
|
+
if (!Number.isInteger(address) || address < 0 || address > 0xffff) {
|
|
33
|
+
throw new Error("Address must be within 0x0000 - 0xFFFF");
|
|
34
|
+
}
|
|
35
|
+
if (!Number.isInteger(length) || length <= 0) {
|
|
36
|
+
throw new Error("Length must be greater than zero");
|
|
37
|
+
}
|
|
38
|
+
if (address + length - 1 > 0xffff) {
|
|
39
|
+
throw new Error("Memory range must stay within 0x0000 - 0xFFFF");
|
|
40
|
+
}
|
|
41
|
+
}
|
|
31
42
|
export class C64Client {
|
|
32
43
|
constructor(baseUrl, options = {}) {
|
|
33
44
|
this.allFacades = new Map();
|
|
@@ -481,9 +492,7 @@ export class C64Client {
|
|
|
481
492
|
const resolved = resolveAddressSymbol(addressInput);
|
|
482
493
|
const address = resolved ?? this.parseNumeric(addressInput);
|
|
483
494
|
const length = this.parseNumeric(lengthInput);
|
|
484
|
-
|
|
485
|
-
throw new Error("Length must be greater than zero");
|
|
486
|
-
}
|
|
495
|
+
validateMemoryRange(address, length);
|
|
487
496
|
const rawBytes = await this.readMemoryRaw(address, length);
|
|
488
497
|
const bytes = rawBytes.slice(0, length);
|
|
489
498
|
return {
|
|
@@ -507,9 +516,7 @@ export class C64Client {
|
|
|
507
516
|
const resolved = resolveAddressSymbol(addressInput);
|
|
508
517
|
const address = resolved ?? this.parseNumeric(addressInput);
|
|
509
518
|
const dataBuffer = this.hexStringToBuffer(bytesInput);
|
|
510
|
-
|
|
511
|
-
throw new Error("No bytes provided");
|
|
512
|
-
}
|
|
519
|
+
validateMemoryRange(address, dataBuffer.length);
|
|
513
520
|
// Prefer PUT with hex data for up to 128 bytes; fall back to POST binary for larger writes
|
|
514
521
|
const addrStr = this.formatAddress(address);
|
|
515
522
|
try {
|
|
@@ -1467,6 +1474,7 @@ export class C64Client {
|
|
|
1467
1474
|
* Public to allow advanced polling and monitoring use cases.
|
|
1468
1475
|
*/
|
|
1469
1476
|
async readMemoryRaw(address, length) {
|
|
1477
|
+
validateMemoryRange(address, length);
|
|
1470
1478
|
try {
|
|
1471
1479
|
const facade = await this.facadePromise;
|
|
1472
1480
|
return await facade.readMemory(address, length);
|
|
@@ -1492,6 +1500,84 @@ export class C64Client {
|
|
|
1492
1500
|
}
|
|
1493
1501
|
return body instanceof ArrayBuffer ? new Uint8Array(body) : this.extractBytes(body);
|
|
1494
1502
|
}
|
|
1503
|
+
/**
|
|
1504
|
+
* Low-level memory write that delegates to the active backend's facade.
|
|
1505
|
+
* Public so tools that need to drive cross-platform machine state (e.g.
|
|
1506
|
+
* keyboard-buffer injection) can avoid backend-specific paths.
|
|
1507
|
+
*/
|
|
1508
|
+
async writeMemoryRaw(address, bytes) {
|
|
1509
|
+
if (!Number.isInteger(address) || address < 0 || address > 0xffff) {
|
|
1510
|
+
throw new Error("Address must be within 0x0000 - 0xFFFF");
|
|
1511
|
+
}
|
|
1512
|
+
const facade = await this.facadePromise;
|
|
1513
|
+
const buf = Buffer.isBuffer(bytes) ? bytes : Buffer.from(bytes);
|
|
1514
|
+
validateMemoryRange(address, buf.length);
|
|
1515
|
+
await facade.writeMemory(address, buf);
|
|
1516
|
+
}
|
|
1517
|
+
/**
|
|
1518
|
+
* Inject a sequence of PETSCII byte values into the KERNAL keyboard buffer
|
|
1519
|
+
* using shared memory writes. Works on both C64U and VICE because both
|
|
1520
|
+
* backends expose `writeMemory`/`readMemory` against the C64 zero-page and
|
|
1521
|
+
* the keyboard queue at $0277.
|
|
1522
|
+
*
|
|
1523
|
+
* The KERNAL buffer ($0277-$0280) holds 10 bytes; $00C6 (NDX) tracks the
|
|
1524
|
+
* number of pending characters. We chunk the input, then poll NDX until
|
|
1525
|
+
* the kernel has consumed each chunk before queueing the next one.
|
|
1526
|
+
*
|
|
1527
|
+
* @param bytes Raw PETSCII bytes (already token-expanded by caller)
|
|
1528
|
+
* @param options Optional knobs for chunk size, drain timing, and total
|
|
1529
|
+
* timeout. The defaults mirror KERNAL behavior.
|
|
1530
|
+
*/
|
|
1531
|
+
async injectKeyboardQueue(bytes, options) {
|
|
1532
|
+
const data = Buffer.isBuffer(bytes)
|
|
1533
|
+
? bytes
|
|
1534
|
+
: Buffer.from(bytes instanceof Uint8Array ? bytes : Uint8Array.from(bytes));
|
|
1535
|
+
if (data.length === 0) {
|
|
1536
|
+
return;
|
|
1537
|
+
}
|
|
1538
|
+
const chunkSize = Math.max(1, Math.min(10, options?.chunkSize ?? 10));
|
|
1539
|
+
const pollMs = Math.max(1, options?.drainPollMs ?? 8);
|
|
1540
|
+
const totalTimeoutMs = Math.max(50, options?.drainTimeoutMs ?? 2000);
|
|
1541
|
+
const facade = await this.facadePromise;
|
|
1542
|
+
const KEYD = 0x0277; // KERNAL keyboard buffer base
|
|
1543
|
+
const NDX = 0x00C6; // pending byte count
|
|
1544
|
+
for (let offset = 0; offset < data.length; offset += chunkSize) {
|
|
1545
|
+
const slice = data.subarray(offset, Math.min(data.length, offset + chunkSize));
|
|
1546
|
+
// Write the bytes into the queue first so KERNAL never reads a stale
|
|
1547
|
+
// length+old-byte combination.
|
|
1548
|
+
await facade.writeMemory(KEYD, Buffer.from(slice));
|
|
1549
|
+
await facade.writeMemory(NDX, Buffer.from([slice.length]));
|
|
1550
|
+
// Drain: wait for KERNAL to consume the queue. If the machine is paused
|
|
1551
|
+
// or otherwise not running, give up after the timeout and continue —
|
|
1552
|
+
// higher layers can decide whether that is a failure for their context.
|
|
1553
|
+
const drainStart = Date.now();
|
|
1554
|
+
while (Date.now() - drainStart < totalTimeoutMs) {
|
|
1555
|
+
const ndx = await facade.readMemory(NDX, 1);
|
|
1556
|
+
if ((ndx[0] ?? 0) === 0) {
|
|
1557
|
+
break;
|
|
1558
|
+
}
|
|
1559
|
+
await new Promise((resolve) => setTimeout(resolve, pollMs));
|
|
1560
|
+
}
|
|
1561
|
+
}
|
|
1562
|
+
}
|
|
1563
|
+
async viceExitMonitor() {
|
|
1564
|
+
await this.withViceMonitor((client) => client.exitMonitor());
|
|
1565
|
+
}
|
|
1566
|
+
async viceKeyboardFeed(text) {
|
|
1567
|
+
await this.withViceMonitor((client) => client.keyboardFeed(text));
|
|
1568
|
+
}
|
|
1569
|
+
async viceMemSet(address, bytes) {
|
|
1570
|
+
const buf = Buffer.isBuffer(bytes) ? bytes : Buffer.from(bytes);
|
|
1571
|
+
await this.withViceMonitor((client) => client.memSet(address, buf));
|
|
1572
|
+
}
|
|
1573
|
+
async viceMemGet(address, length) {
|
|
1574
|
+
const end = (address + length - 1) & 0xffff;
|
|
1575
|
+
return this.withViceMonitor((client) => client.memGet(address, end));
|
|
1576
|
+
}
|
|
1577
|
+
async viceNuclearReset() {
|
|
1578
|
+
const backend = await this.requireViceBackend();
|
|
1579
|
+
await backend.nuclearReset();
|
|
1580
|
+
}
|
|
1495
1581
|
async viceCheckpointList() {
|
|
1496
1582
|
return this.withViceMonitor((client) => client.checkpointList());
|
|
1497
1583
|
}
|
|
@@ -1621,17 +1707,27 @@ export class C64Client {
|
|
|
1621
1707
|
const lower = trimmed.toLowerCase();
|
|
1622
1708
|
let radix = 10;
|
|
1623
1709
|
let literal = lower;
|
|
1710
|
+
let pattern;
|
|
1624
1711
|
if (lower.startsWith("$")) {
|
|
1625
1712
|
radix = 16;
|
|
1626
1713
|
literal = lower.slice(1);
|
|
1714
|
+
pattern = /^[0-9a-f]+$/;
|
|
1627
1715
|
}
|
|
1628
1716
|
else if (lower.startsWith("0x")) {
|
|
1629
1717
|
radix = 16;
|
|
1630
1718
|
literal = lower.slice(2);
|
|
1719
|
+
pattern = /^[0-9a-f]+$/;
|
|
1631
1720
|
}
|
|
1632
1721
|
else if (lower.startsWith("%")) {
|
|
1633
1722
|
radix = 2;
|
|
1634
1723
|
literal = lower.slice(1);
|
|
1724
|
+
pattern = /^[01]+$/;
|
|
1725
|
+
}
|
|
1726
|
+
else {
|
|
1727
|
+
pattern = /^[0-9]+$/;
|
|
1728
|
+
}
|
|
1729
|
+
if (!pattern.test(literal)) {
|
|
1730
|
+
throw new Error(`Unable to parse numeric value "${value}"`);
|
|
1635
1731
|
}
|
|
1636
1732
|
const parsed = Number.parseInt(literal, radix);
|
|
1637
1733
|
if (!Number.isFinite(parsed) || Number.isNaN(parsed) || parsed < 0) {
|
|
@@ -1665,6 +1761,9 @@ export class C64Client {
|
|
|
1665
1761
|
if (cleaned.length % 2 !== 0) {
|
|
1666
1762
|
throw new Error("Hex string must contain an even number of characters");
|
|
1667
1763
|
}
|
|
1764
|
+
if (!/^[0-9a-f]+$/.test(cleaned)) {
|
|
1765
|
+
throw new Error("Hex string contains non-hexadecimal characters");
|
|
1766
|
+
}
|
|
1668
1767
|
return Buffer.from(cleaned, "hex");
|
|
1669
1768
|
}
|
|
1670
1769
|
hzToSidFrequency(hz, system = "PAL") {
|
package/dist/device.js
CHANGED
|
@@ -339,15 +339,15 @@ export class ViceBackend {
|
|
|
339
339
|
extraArgs: this.extraArgs,
|
|
340
340
|
});
|
|
341
341
|
}
|
|
342
|
-
const handle = await startViceProcess({
|
|
342
|
+
const handle = await startViceProcess(buildManagedViceProcessOptions({
|
|
343
343
|
binary: this.exe,
|
|
344
344
|
directory: this.directory,
|
|
345
345
|
host: this.host,
|
|
346
346
|
port: this.port,
|
|
347
347
|
warp: this.warp,
|
|
348
348
|
visible: this.visible,
|
|
349
|
-
extraArgs: this.extraArgs
|
|
350
|
-
});
|
|
349
|
+
extraArgs: this.extraArgs,
|
|
350
|
+
}));
|
|
351
351
|
this.lastProcessStart = Date.now();
|
|
352
352
|
if (this.debugEnabled) {
|
|
353
353
|
console.error("[vice-backend] VICE process started", { pid: handle.process.pid });
|
|
@@ -378,6 +378,7 @@ export class ViceBackend {
|
|
|
378
378
|
return;
|
|
379
379
|
}
|
|
380
380
|
ViceBackend.cleanupRegistered = true;
|
|
381
|
+
/* c8 ignore next 5 -- global process exit hooks are not practical to exercise in the unit test process */
|
|
381
382
|
process.once("exit", () => {
|
|
382
383
|
for (const [, handle] of ViceBackend.supervisors) {
|
|
383
384
|
handle.stop().catch(() => { });
|
|
@@ -579,6 +580,31 @@ export class ViceBackend {
|
|
|
579
580
|
return { success: false, details };
|
|
580
581
|
}
|
|
581
582
|
}
|
|
583
|
+
async nuclearReset() {
|
|
584
|
+
const poweroffResult = await this.poweroff();
|
|
585
|
+
if (!poweroffResult.success) {
|
|
586
|
+
return poweroffResult;
|
|
587
|
+
}
|
|
588
|
+
if (!this.manageProcess) {
|
|
589
|
+
return {
|
|
590
|
+
success: false,
|
|
591
|
+
details: {
|
|
592
|
+
code: "UNSUPPORTED",
|
|
593
|
+
message: "VICE nuclear reset requires a managed process; unmanaged instances can only be powered off.",
|
|
594
|
+
},
|
|
595
|
+
};
|
|
596
|
+
}
|
|
597
|
+
try {
|
|
598
|
+
await this.ensureProcess();
|
|
599
|
+
return { success: true };
|
|
600
|
+
}
|
|
601
|
+
catch (error) {
|
|
602
|
+
const details = error instanceof Error
|
|
603
|
+
? { message: error.message }
|
|
604
|
+
: error;
|
|
605
|
+
return { success: false, details };
|
|
606
|
+
}
|
|
607
|
+
}
|
|
582
608
|
async menuButton() { throw unsupported("menuButton"); }
|
|
583
609
|
async debugregRead() { throw unsupported("debugregRead"); }
|
|
584
610
|
async debugregWrite(_v) { throw unsupported("debugregWrite"); }
|
|
@@ -838,12 +864,26 @@ function resolveViceLaunch(options, dependencies = {}) {
|
|
|
838
864
|
}),
|
|
839
865
|
};
|
|
840
866
|
}
|
|
867
|
+
function buildManagedViceProcessOptions(input) {
|
|
868
|
+
return {
|
|
869
|
+
binary: input.binary,
|
|
870
|
+
directory: input.directory,
|
|
871
|
+
host: input.host,
|
|
872
|
+
port: input.port,
|
|
873
|
+
warp: input.warp,
|
|
874
|
+
visible: input.visible,
|
|
875
|
+
extraArgs: input.extraArgs.length > 0 ? input.extraArgs : undefined,
|
|
876
|
+
};
|
|
877
|
+
}
|
|
841
878
|
export function __resolveViceBinaryForTests(options, findBinary) {
|
|
842
879
|
return resolveViceBinary(options, findBinary ?? which);
|
|
843
880
|
}
|
|
844
881
|
export function __resolveViceLaunchForTests(options, dependencies) {
|
|
845
882
|
return resolveViceLaunch(options, dependencies);
|
|
846
883
|
}
|
|
884
|
+
export function __buildViceProcessOptionsForTests(input) {
|
|
885
|
+
return buildManagedViceProcessOptions(input);
|
|
886
|
+
}
|
|
847
887
|
export async function createFacade(logger, options) {
|
|
848
888
|
// Caller-forced preference: use c64u with provided base URL (used by tests and server wiring)
|
|
849
889
|
if (options?.preferredC64uBaseUrl) {
|
package/dist/rag/indexer.js
CHANGED
|
@@ -474,20 +474,30 @@ export async function buildAllIndexes({ model, embeddingsDir: overrideDir, basic
|
|
|
474
474
|
repoMetadataCache.clear();
|
|
475
475
|
const embeddingsDir = resolveEmbeddingsDir(overrideDir);
|
|
476
476
|
const paths = embeddingIndexPaths(embeddingsDir);
|
|
477
|
-
//
|
|
477
|
+
// Production indexing defaults to the repo docs tree. Tests and other callers can
|
|
478
|
+
// override that set explicitly, including passing an empty array to opt out.
|
|
478
479
|
const docCandidates = [];
|
|
479
|
-
|
|
480
|
-
const
|
|
481
|
-
for (const f of mdFiles) {
|
|
480
|
+
if (docFiles !== undefined) {
|
|
481
|
+
for (const f of docFiles) {
|
|
482
482
|
if (!shouldExcludeDocFile(f)) {
|
|
483
483
|
docCandidates.push(f);
|
|
484
484
|
}
|
|
485
485
|
}
|
|
486
486
|
}
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
487
|
+
else {
|
|
488
|
+
try {
|
|
489
|
+
const mdFiles = await collectFiles(DOC_ROOT, [".md"]);
|
|
490
|
+
for (const f of mdFiles) {
|
|
491
|
+
if (!shouldExcludeDocFile(f)) {
|
|
492
|
+
docCandidates.push(f);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
catch { }
|
|
497
|
+
for (const f of ENV_DOC_FILES) {
|
|
498
|
+
if (!shouldExcludeDocFile(f)) {
|
|
499
|
+
docCandidates.push(f);
|
|
500
|
+
}
|
|
491
501
|
}
|
|
492
502
|
}
|
|
493
503
|
// De-duplicate
|
package/dist/sidwaveCompiler.js
CHANGED
|
@@ -6,7 +6,7 @@ Licensed under the GNU General Public License v2.0 or later.
|
|
|
6
6
|
See <https://www.gnu.org/licenses/> for details.
|
|
7
7
|
*/
|
|
8
8
|
import { Buffer } from "node:buffer";
|
|
9
|
-
import { assemblyToPrg } from "./
|
|
9
|
+
import { assemblyToPrg } from "./tools/translation/assembler.js";
|
|
10
10
|
import { midiToHz, noteNameToMidi } from "./sidwave.js";
|
|
11
11
|
/**
|
|
12
12
|
* Small SIDWAVE to PRG compiler: generates a 50/60Hz player that writes SID registers from a precomputed frame table.
|
package/dist/tools/debug.js
CHANGED
|
@@ -57,6 +57,7 @@ const createCheckpointArgsSchema = objectSchema({
|
|
|
57
57
|
stopOnHit: optionalSchema(booleanSchema({ description: "Pause execution when hit (default true).", default: true })),
|
|
58
58
|
enabled: optionalSchema(booleanSchema({ description: "Whether the checkpoint is enabled (default true).", default: true })),
|
|
59
59
|
temporary: optionalSchema(booleanSchema({ description: "Automatically remove after first hit.", default: false })),
|
|
60
|
+
label: optionalSchema(stringSchema({ description: "Optional human-readable label stored in-process.", maxLength: 64 })),
|
|
60
61
|
operations: optionalSchema(objectSchema({
|
|
61
62
|
description: "Checkpoint operation filters.",
|
|
62
63
|
properties: {
|
|
@@ -147,6 +148,42 @@ const stepReturnArgsSchema = objectSchema({
|
|
|
147
148
|
required: ["op"],
|
|
148
149
|
additionalProperties: false,
|
|
149
150
|
});
|
|
151
|
+
const getMonitorStateArgsSchema = objectSchema({
|
|
152
|
+
description: "Read CPU registers and return the current monitor state.",
|
|
153
|
+
properties: {
|
|
154
|
+
op: literalSchema("get_monitor_state"),
|
|
155
|
+
memspace: memspaceSchema,
|
|
156
|
+
},
|
|
157
|
+
required: ["op"],
|
|
158
|
+
additionalProperties: false,
|
|
159
|
+
});
|
|
160
|
+
const waitForStateArgsSchema = objectSchema({
|
|
161
|
+
description: "Poll CPU registers until PC equals expectedPC or timeout elapses.",
|
|
162
|
+
properties: {
|
|
163
|
+
op: literalSchema("wait_for_state"),
|
|
164
|
+
expectedPC: optionalSchema(stringSchema({ description: "Target PC address to wait for (e.g. $0810).", minLength: 1 })),
|
|
165
|
+
timeoutMs: optionalSchema(numberSchema({ description: "Maximum wait time in milliseconds.", integer: true, minimum: 1, maximum: 60000, default: 5000 })),
|
|
166
|
+
pollMs: optionalSchema(numberSchema({ description: "Poll interval in milliseconds.", integer: true, minimum: 10, maximum: 1000, default: 100 })),
|
|
167
|
+
},
|
|
168
|
+
required: ["op"],
|
|
169
|
+
additionalProperties: false,
|
|
170
|
+
});
|
|
171
|
+
const nuclearResetArgsSchema = objectSchema({
|
|
172
|
+
description: "Kill and restart the VICE process (managed instances only).",
|
|
173
|
+
properties: {
|
|
174
|
+
op: literalSchema("nuclear_reset"),
|
|
175
|
+
},
|
|
176
|
+
required: ["op"],
|
|
177
|
+
additionalProperties: false,
|
|
178
|
+
});
|
|
179
|
+
const continueExecutionArgsSchema = objectSchema({
|
|
180
|
+
description: "Exit the Binary Monitor and resume CPU execution (BM 0xAA Exit).",
|
|
181
|
+
properties: {
|
|
182
|
+
op: literalSchema("continue_execution"),
|
|
183
|
+
},
|
|
184
|
+
required: ["op"],
|
|
185
|
+
additionalProperties: false,
|
|
186
|
+
});
|
|
150
187
|
const debugOperationSchemas = [
|
|
151
188
|
listCheckpointsArgsSchema,
|
|
152
189
|
getCheckpointArgsSchema,
|
|
@@ -159,14 +196,19 @@ const debugOperationSchemas = [
|
|
|
159
196
|
setRegistersArgsSchema,
|
|
160
197
|
stepArgsSchema,
|
|
161
198
|
stepReturnArgsSchema,
|
|
199
|
+
getMonitorStateArgsSchema,
|
|
200
|
+
waitForStateArgsSchema,
|
|
201
|
+
nuclearResetArgsSchema,
|
|
202
|
+
continueExecutionArgsSchema,
|
|
162
203
|
];
|
|
204
|
+
const checkpointLabels = new Map();
|
|
163
205
|
const debugOperationHandlers = {
|
|
164
206
|
list_checkpoints: async (_args, ctx) => {
|
|
165
207
|
try {
|
|
166
208
|
listCheckpointsArgsSchema.parse(_args);
|
|
167
209
|
ctx.logger.info("Listing VICE checkpoints");
|
|
168
210
|
const checkpoints = await ctx.client.viceCheckpointList();
|
|
169
|
-
return jsonResult({ checkpoints: checkpoints.map(formatCheckpoint) }, { success: true, count: checkpoints.length });
|
|
211
|
+
return jsonResult({ checkpoints: checkpoints.map((cp) => formatCheckpoint(cp, checkpointLabels.get(cp.id))) }, { success: true, count: checkpoints.length });
|
|
170
212
|
}
|
|
171
213
|
catch (error) {
|
|
172
214
|
return handleToolError(error);
|
|
@@ -176,7 +218,7 @@ const debugOperationHandlers = {
|
|
|
176
218
|
try {
|
|
177
219
|
const parsed = getCheckpointArgsSchema.parse(args);
|
|
178
220
|
const checkpoint = await ctx.client.viceCheckpointGet(parsed.id);
|
|
179
|
-
return jsonResult({ checkpoint: formatCheckpoint(checkpoint) }, { success: true });
|
|
221
|
+
return jsonResult({ checkpoint: formatCheckpoint(checkpoint, checkpointLabels.get(checkpoint.id)) }, { success: true });
|
|
180
222
|
}
|
|
181
223
|
catch (error) {
|
|
182
224
|
return handleToolError(error);
|
|
@@ -201,8 +243,11 @@ const debugOperationHandlers = {
|
|
|
201
243
|
},
|
|
202
244
|
memspace: normaliseMemspace(parsed.memspace),
|
|
203
245
|
});
|
|
204
|
-
|
|
205
|
-
|
|
246
|
+
if (parsed.label) {
|
|
247
|
+
checkpointLabels.set(created.id, parsed.label);
|
|
248
|
+
}
|
|
249
|
+
ctx.logger.info("Created VICE checkpoint", { id: created.id, start: formatAddress(created.start), label: parsed.label });
|
|
250
|
+
return jsonResult({ checkpoint: formatCheckpoint(created, checkpointLabels.get(created.id)) }, { success: true, id: created.id });
|
|
206
251
|
}
|
|
207
252
|
catch (error) {
|
|
208
253
|
return handleToolError(error);
|
|
@@ -212,6 +257,7 @@ const debugOperationHandlers = {
|
|
|
212
257
|
try {
|
|
213
258
|
const parsed = deleteCheckpointArgsSchema.parse(args);
|
|
214
259
|
await ctx.client.viceCheckpointDelete(parsed.id);
|
|
260
|
+
checkpointLabels.delete(parsed.id);
|
|
215
261
|
ctx.logger.info("Deleted VICE checkpoint", { id: parsed.id });
|
|
216
262
|
return textResult(`Deleted checkpoint ${parsed.id}.`, { success: true, id: parsed.id });
|
|
217
263
|
}
|
|
@@ -345,6 +391,87 @@ const debugOperationHandlers = {
|
|
|
345
391
|
return handleToolError(error);
|
|
346
392
|
}
|
|
347
393
|
},
|
|
394
|
+
get_monitor_state: async (args, ctx) => {
|
|
395
|
+
try {
|
|
396
|
+
const parsed = getMonitorStateArgsSchema.parse(args);
|
|
397
|
+
const memspace = normaliseMemspace(parsed.memspace);
|
|
398
|
+
const [metadata, values] = await Promise.all([
|
|
399
|
+
ctx.client.viceRegistersAvailable(memspace),
|
|
400
|
+
ctx.client.viceRegistersGet(memspace),
|
|
401
|
+
]);
|
|
402
|
+
const metadataById = new Map(metadata.map((m) => [m.id, m]));
|
|
403
|
+
const registers = values.map((v) => {
|
|
404
|
+
const meta = metadataById.get(v.id);
|
|
405
|
+
return { id: v.id, name: meta?.name, bits: meta?.bits, size: v.size, value: v.value };
|
|
406
|
+
});
|
|
407
|
+
const pc = registers.find((r) => r.name?.toUpperCase() === "PC");
|
|
408
|
+
ctx.logger.info("Read monitor state", { memspace, pc: pc ? formatAddress(pc.value) : "unknown" });
|
|
409
|
+
return jsonResult({ memspace, registers, pc: pc ? formatAddress(pc.value) : undefined }, { success: true, memspace, count: registers.length });
|
|
410
|
+
}
|
|
411
|
+
catch (error) {
|
|
412
|
+
return handleToolError(error);
|
|
413
|
+
}
|
|
414
|
+
},
|
|
415
|
+
wait_for_state: async (args, ctx) => {
|
|
416
|
+
try {
|
|
417
|
+
const parsed = waitForStateArgsSchema.parse(args);
|
|
418
|
+
const timeoutMs = parsed.timeoutMs ?? 5000;
|
|
419
|
+
const pollMs = parsed.pollMs ?? 100;
|
|
420
|
+
const targetPC = parsed.expectedPC ? parseAddress(parsed.expectedPC, "expectedPC") : undefined;
|
|
421
|
+
const metadata = await ctx.client.viceRegistersAvailable(0);
|
|
422
|
+
const metadataById = new Map(metadata.map((m) => [m.id, m]));
|
|
423
|
+
const pcId = metadata.find((m) => m.name.toUpperCase() === "PC")?.id;
|
|
424
|
+
const deadline = Date.now() + timeoutMs;
|
|
425
|
+
let values = [];
|
|
426
|
+
let currentPC;
|
|
427
|
+
while (Date.now() < deadline) {
|
|
428
|
+
values = await ctx.client.viceRegistersGet(0);
|
|
429
|
+
currentPC = pcId !== undefined ? values.find((v) => v.id === pcId)?.value : undefined;
|
|
430
|
+
if (targetPC === undefined || currentPC === targetPC) {
|
|
431
|
+
break;
|
|
432
|
+
}
|
|
433
|
+
await new Promise((res) => setTimeout(res, pollMs));
|
|
434
|
+
}
|
|
435
|
+
const matched = targetPC === undefined || currentPC === targetPC;
|
|
436
|
+
const regs = values.map((v) => {
|
|
437
|
+
const meta = metadataById.get(v.id);
|
|
438
|
+
return { id: v.id, name: meta?.name, bits: meta?.bits, size: v.size, value: v.value };
|
|
439
|
+
});
|
|
440
|
+
ctx.logger.info("wait_for_state settled", { matched, pc: currentPC !== undefined ? formatAddress(currentPC) : "unknown" });
|
|
441
|
+
return jsonResult({
|
|
442
|
+
matched,
|
|
443
|
+
timedOut: !matched,
|
|
444
|
+
pc: currentPC !== undefined ? formatAddress(currentPC) : undefined,
|
|
445
|
+
registers: regs,
|
|
446
|
+
}, { success: true, matched });
|
|
447
|
+
}
|
|
448
|
+
catch (error) {
|
|
449
|
+
return handleToolError(error);
|
|
450
|
+
}
|
|
451
|
+
},
|
|
452
|
+
nuclear_reset: async (args, ctx) => {
|
|
453
|
+
try {
|
|
454
|
+
nuclearResetArgsSchema.parse(args);
|
|
455
|
+
ctx.logger.info("Performing nuclear reset of VICE process");
|
|
456
|
+
await ctx.client.viceNuclearReset();
|
|
457
|
+
checkpointLabels.clear();
|
|
458
|
+
return textResult("VICE process stopped and restarted.", { success: true });
|
|
459
|
+
}
|
|
460
|
+
catch (error) {
|
|
461
|
+
return handleToolError(error);
|
|
462
|
+
}
|
|
463
|
+
},
|
|
464
|
+
continue_execution: async (args, ctx) => {
|
|
465
|
+
try {
|
|
466
|
+
continueExecutionArgsSchema.parse(args);
|
|
467
|
+
await ctx.client.viceExitMonitor();
|
|
468
|
+
ctx.logger.info("Exited Binary Monitor — CPU resumed");
|
|
469
|
+
return textResult("Execution resumed.", { success: true });
|
|
470
|
+
}
|
|
471
|
+
catch (error) {
|
|
472
|
+
return handleToolError(error);
|
|
473
|
+
}
|
|
474
|
+
},
|
|
348
475
|
};
|
|
349
476
|
const debugOperationDispatcher = createOperationDispatcher("c64_debug", debugOperationHandlers);
|
|
350
477
|
export const debugModuleGroup = defineToolModule({
|
|
@@ -384,6 +511,31 @@ export const debugModuleGroup = defineToolModule({
|
|
|
384
511
|
description: "Read CPU registers from memspace 0 (default).",
|
|
385
512
|
arguments: { op: "get_registers" },
|
|
386
513
|
},
|
|
514
|
+
{
|
|
515
|
+
name: "Create labelled breakpoint",
|
|
516
|
+
description: "Set a breakpoint at $0810 with a descriptive label.",
|
|
517
|
+
arguments: { op: "create_checkpoint", address: "$0810", label: "entry_point" },
|
|
518
|
+
},
|
|
519
|
+
{
|
|
520
|
+
name: "Get monitor state",
|
|
521
|
+
description: "Read current CPU state including all registers and PC.",
|
|
522
|
+
arguments: { op: "get_monitor_state" },
|
|
523
|
+
},
|
|
524
|
+
{
|
|
525
|
+
name: "Wait for PC",
|
|
526
|
+
description: "Poll until PC reaches $0810 or 5 s elapses.",
|
|
527
|
+
arguments: { op: "wait_for_state", expectedPC: "$0810", timeoutMs: 5000 },
|
|
528
|
+
},
|
|
529
|
+
{
|
|
530
|
+
name: "Nuclear reset",
|
|
531
|
+
description: "Kill and restart the VICE process, clearing all state.",
|
|
532
|
+
arguments: { op: "nuclear_reset" },
|
|
533
|
+
},
|
|
534
|
+
{
|
|
535
|
+
name: "Continue execution",
|
|
536
|
+
description: "Exit the BM monitor and let the CPU run freely.",
|
|
537
|
+
arguments: { op: "continue_execution" },
|
|
538
|
+
},
|
|
387
539
|
],
|
|
388
540
|
execute: debugOperationDispatcher,
|
|
389
541
|
},
|
|
@@ -395,9 +547,10 @@ function handleToolError(error) {
|
|
|
395
547
|
}
|
|
396
548
|
return unknownErrorResult(error);
|
|
397
549
|
}
|
|
398
|
-
function formatCheckpoint(checkpoint) {
|
|
550
|
+
function formatCheckpoint(checkpoint, label) {
|
|
399
551
|
return {
|
|
400
552
|
id: checkpoint.id,
|
|
553
|
+
...(label ? { label } : {}),
|
|
401
554
|
start: formatAddress(checkpoint.start),
|
|
402
555
|
end: formatAddress(checkpoint.end),
|
|
403
556
|
enabled: checkpoint.enabled,
|