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
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 "./basicConverter.js";
12
- import { assemblyToPrg } from "./assemblyConverter.js";
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
- if (length <= 0) {
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
- if (dataBuffer.length === 0) {
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.length > 0 ? this.extraArgs : undefined,
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) {
@@ -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
- // Collect all markdown files under doc/ recursively, plus any explicitly provided RAG_DOC_FILES
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
- try {
480
- const mdFiles = await collectFiles(DOC_ROOT, [".md"]);
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
- catch { }
488
- for (const f of ENV_DOC_FILES) {
489
- if (!shouldExcludeDocFile(f)) {
490
- docCandidates.push(f);
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
@@ -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 "./assemblyConverter.js";
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.
@@ -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
- ctx.logger.info("Created VICE checkpoint", { id: created.id, start: formatAddress(created.start) });
205
- return jsonResult({ checkpoint: formatCheckpoint(created) }, { success: true, id: created.id });
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,