c64bridge 0.9.0 → 0.9.1

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 (49) hide show
  1. package/README.md +158 -164
  2. package/data/vice/vice-binary-monitor-spec.md +21 -4
  3. package/dist/.tsbuildinfo +1 -1
  4. package/dist/c64Client.js +76 -2
  5. package/dist/device.js +43 -3
  6. package/dist/rag/indexer.js +18 -8
  7. package/dist/sidwaveCompiler.js +1 -1
  8. package/dist/{assemblyConverter.js → tools/assember.js} +10 -1
  9. package/dist/{basicConverter.js → tools/basicTokenizer.js} +1 -1
  10. package/dist/tools/debug.js +158 -5
  11. package/dist/tools/disassembler.js +478 -0
  12. package/dist/tools/input.js +338 -0
  13. package/dist/tools/memory.js +317 -1
  14. package/dist/tools/programRunners.js +45 -6
  15. package/dist/tools/registry/batch.js +167 -0
  16. package/dist/tools/registry/graphics.js +101 -1
  17. package/dist/tools/registry/index.js +4 -0
  18. package/dist/tools/registry/input.js +2 -0
  19. package/dist/tools/registry/memory.js +61 -1
  20. package/dist/tools/schema.js +4 -5
  21. package/dist/tools/symbolRegistry.js +49 -0
  22. package/dist/tools/types.js +19 -4
  23. package/dist/vice/process.js +43 -2
  24. package/dist/vice/viceClient.js +5 -1
  25. package/mcp/schemas/c64_batch.schema.json +53 -0
  26. package/mcp/schemas/c64_config.schema.json +461 -2
  27. package/mcp/schemas/c64_debug.schema.json +641 -3
  28. package/mcp/schemas/c64_disk.schema.json +340 -2
  29. package/mcp/schemas/c64_drive.schema.json +157 -2
  30. package/mcp/schemas/c64_extract.schema.json +359 -2
  31. package/mcp/schemas/c64_graphics.schema.json +415 -3
  32. package/mcp/schemas/c64_input.schema.json +136 -0
  33. package/mcp/schemas/c64_memory.schema.json +562 -3
  34. package/mcp/schemas/c64_printer.schema.json +253 -2
  35. package/mcp/schemas/c64_program.schema.json +499 -2
  36. package/mcp/schemas/c64_rag.schema.json +82 -2
  37. package/mcp/schemas/c64_select_backend.schema.json +35 -2
  38. package/mcp/schemas/c64_sound.schema.json +793 -2
  39. package/mcp/schemas/c64_stream.schema.json +73 -2
  40. package/mcp/schemas/c64_system.schema.json +301 -2
  41. package/mcp/schemas/c64_vice.schema.json +71 -2
  42. package/mcp/server.json +1 -1
  43. package/mcp/tools.json +6718 -1257
  44. package/mcp.json +1 -1
  45. package/package.json +1 -1
  46. package/scripts/run-coverage.mjs +7 -0
  47. package/scripts/run-tests.ts +2 -0
  48. package/scripts/update-readme.ts +79 -2
  49. 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/basicTokenizer.js";
12
+ import { assemblyToPrg } from "./tools/assember.js";
13
13
  import { screenCodesToAscii } from "./petscii.js";
14
14
  import { resolveAddressSymbol } from "./knowledge.js";
15
15
  import { createAllFacades, createFacade } from "./device.js";
@@ -1492,6 +1492,80 @@ export class C64Client {
1492
1492
  }
1493
1493
  return body instanceof ArrayBuffer ? new Uint8Array(body) : this.extractBytes(body);
1494
1494
  }
1495
+ /**
1496
+ * Low-level memory write that delegates to the active backend's facade.
1497
+ * Public so tools that need to drive cross-platform machine state (e.g.
1498
+ * keyboard-buffer injection) can avoid backend-specific paths.
1499
+ */
1500
+ async writeMemoryRaw(address, bytes) {
1501
+ const facade = await this.facadePromise;
1502
+ const buf = Buffer.isBuffer(bytes) ? bytes : Buffer.from(bytes);
1503
+ await facade.writeMemory(address, buf);
1504
+ }
1505
+ /**
1506
+ * Inject a sequence of PETSCII byte values into the KERNAL keyboard buffer
1507
+ * using shared memory writes. Works on both C64U and VICE because both
1508
+ * backends expose `writeMemory`/`readMemory` against the C64 zero-page and
1509
+ * the keyboard queue at $0277.
1510
+ *
1511
+ * The KERNAL buffer ($0277-$0280) holds 10 bytes; $00C6 (NDX) tracks the
1512
+ * number of pending characters. We chunk the input, then poll NDX until
1513
+ * the kernel has consumed each chunk before queueing the next one.
1514
+ *
1515
+ * @param bytes Raw PETSCII bytes (already token-expanded by caller)
1516
+ * @param options Optional knobs for chunk size, drain timing, and total
1517
+ * timeout. The defaults mirror KERNAL behavior.
1518
+ */
1519
+ async injectKeyboardQueue(bytes, options) {
1520
+ const data = Buffer.isBuffer(bytes)
1521
+ ? bytes
1522
+ : Buffer.from(bytes instanceof Uint8Array ? bytes : Uint8Array.from(bytes));
1523
+ if (data.length === 0) {
1524
+ return;
1525
+ }
1526
+ const chunkSize = Math.max(1, Math.min(10, options?.chunkSize ?? 10));
1527
+ const pollMs = Math.max(1, options?.drainPollMs ?? 8);
1528
+ const totalTimeoutMs = Math.max(50, options?.drainTimeoutMs ?? 2000);
1529
+ const facade = await this.facadePromise;
1530
+ const KEYD = 0x0277; // KERNAL keyboard buffer base
1531
+ const NDX = 0x00C6; // pending byte count
1532
+ for (let offset = 0; offset < data.length; offset += chunkSize) {
1533
+ const slice = data.subarray(offset, Math.min(data.length, offset + chunkSize));
1534
+ // Write the bytes into the queue first so KERNAL never reads a stale
1535
+ // length+old-byte combination.
1536
+ await facade.writeMemory(KEYD, Buffer.from(slice));
1537
+ await facade.writeMemory(NDX, Buffer.from([slice.length]));
1538
+ // Drain: wait for KERNAL to consume the queue. If the machine is paused
1539
+ // or otherwise not running, give up after the timeout and continue —
1540
+ // higher layers can decide whether that is a failure for their context.
1541
+ const drainStart = Date.now();
1542
+ while (Date.now() - drainStart < totalTimeoutMs) {
1543
+ const ndx = await facade.readMemory(NDX, 1);
1544
+ if ((ndx[0] ?? 0) === 0) {
1545
+ break;
1546
+ }
1547
+ await new Promise((resolve) => setTimeout(resolve, pollMs));
1548
+ }
1549
+ }
1550
+ }
1551
+ async viceExitMonitor() {
1552
+ await this.withViceMonitor((client) => client.exitMonitor());
1553
+ }
1554
+ async viceKeyboardFeed(text) {
1555
+ await this.withViceMonitor((client) => client.keyboardFeed(text));
1556
+ }
1557
+ async viceMemSet(address, bytes) {
1558
+ const buf = Buffer.isBuffer(bytes) ? bytes : Buffer.from(bytes);
1559
+ await this.withViceMonitor((client) => client.memSet(address, buf));
1560
+ }
1561
+ async viceMemGet(address, length) {
1562
+ const end = (address + length - 1) & 0xffff;
1563
+ return this.withViceMonitor((client) => client.memGet(address, end));
1564
+ }
1565
+ async viceNuclearReset() {
1566
+ const backend = await this.requireViceBackend();
1567
+ await backend.nuclearReset();
1568
+ }
1495
1569
  async viceCheckpointList() {
1496
1570
  return this.withViceMonitor((client) => client.checkpointList());
1497
1571
  }
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/assember.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.
@@ -77,6 +77,9 @@ const DIRECTIVE_ALIASES = {
77
77
  };
78
78
  const RELATIVE_ONLY = new Set(["BCC", "BCS", "BEQ", "BMI", "BNE", "BPL", "BVC", "BVS"]);
79
79
  export function assemblyToPrg(source, options) {
80
+ return assemblyToPrgDetailed(source, options).prg;
81
+ }
82
+ export function assemblyToPrgDetailed(source, options) {
80
83
  const normalized = normalizeSource(source);
81
84
  const loadAddress = options?.loadAddress ?? DEFAULT_LOAD_ADDRESS;
82
85
  const rootFile = options?.fileName ?? "(input)";
@@ -86,7 +89,7 @@ export function assemblyToPrg(source, options) {
86
89
  const assembler = new Assembler(statements, loadAddress);
87
90
  assembler.runPass(0);
88
91
  assembler.runPass(1);
89
- return assembler.buildPrg();
92
+ return { prg: assembler.buildPrg(), symbols: assembler.getSymbols() };
90
93
  }
91
94
  function normalizeSource(input) {
92
95
  return input.replace(/\r\n?/g, "\n");
@@ -608,6 +611,9 @@ class SymbolTable {
608
611
  }
609
612
  return this.globals.get(label);
610
613
  }
614
+ allGlobals() {
615
+ return this.globals;
616
+ }
611
617
  ensureLocalScope(scope) {
612
618
  if (!this.locals.has(scope)) {
613
619
  this.locals.set(scope, new Map());
@@ -658,6 +664,9 @@ class Assembler {
658
664
  header.writeUInt16LE(start, 0);
659
665
  return Buffer.concat([header, Buffer.from(body)]);
660
666
  }
667
+ getSymbols() {
668
+ return this.symbolTable.allGlobals();
669
+ }
661
670
  processLine(line, pass) {
662
671
  for (const label of line.labels) {
663
672
  if (pass === 0) {
@@ -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 { asciiToPetscii, encodeStringWithNames } from "./petscii.js";
9
+ import { asciiToPetscii, encodeStringWithNames } from "../petscii.js";
10
10
  const DEFAULT_START_ADDRESS = 0x0801;
11
11
  const REM_TOKEN = 0x8f;
12
12
  const TOKEN_TABLE = buildTokenTable();
@@ -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,