@henols/c64-re-tools 2.0.0-rc.13 → 2.0.0-rc.15

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 (58) hide show
  1. package/package.json +1 -1
  2. package/skills/c64-assembler/SKILL.md +7 -2
  3. package/skills/c64-assembler/scripts/assemble.ts +13 -5
  4. package/skills/c64-disk/SKILL.md +1 -1
  5. package/skills/c64-disk/scripts/disk.ts +1 -1
  6. package/skills/c64-emulator/SKILL.md +8 -4
  7. package/skills/c64-memory-map/SKILL.md +2 -2
  8. package/skills/c64-provenance/SKILL.md +1 -1
  9. package/skills/c64-provenance/scripts/provenance.ts +4 -4
  10. package/skills/c64-reverse-engineering/SKILL.md +2 -1
  11. package/skills/c64-source/SKILL.md +1 -1
  12. package/skills/c64-source/scripts/generate.ts +3 -3
  13. package/skills/c64-static-analysis/SKILL.md +4 -3
  14. package/skills/c64-static-analysis/scripts/analyze.ts +16 -10
  15. package/skills/c64-static-analysis/scripts/{coverage.ts → lib/coverage.ts} +20 -0
  16. package/skills/c64-static-analysis/scripts/{idioms.ts → lib/idioms.ts} +14 -3
  17. package/skills/c64-testing/SKILL.md +3 -2
  18. package/skills/c64-testing/references/scenario-format.md +1 -1
  19. package/skills/c64-testing/scripts/checklist.ts +1 -1
  20. package/skills/c64-testing/scripts/{scenario.ts → lib/scenario.ts} +11 -9
  21. package/skills/c64-testing/scripts/run.ts +4 -4
  22. package/skills/c64-testing/scripts/test.ts +1 -1
  23. package/skills/c64-unpacker/SKILL.md +4 -1
  24. package/skills/c64-unpacker/scripts/unpack.ts +4 -4
  25. package/src/c64.ts +10 -0
  26. package/src/cli/cleanup.ts +1 -1
  27. package/src/cli/main.ts +2 -1
  28. package/src/cli/status.ts +69 -2
  29. package/src/host/main.ts +14 -1
  30. package/src/host/vice/session.ts +71 -15
  31. package/src/host-client/steps.ts +7 -2
  32. package/src/host-client/vice-session.ts +5 -3
  33. package/src/mcp/server.ts +1 -0
  34. package/src/mcp/tools/debug.ts +22 -7
  35. package/src/mcp/tools/execution.ts +17 -3
  36. package/src/mcp/tools/machine.ts +11 -2
  37. package/src/mcp/tools/media.ts +1 -0
  38. package/src/mcp/tools/memory.ts +7 -2
  39. package/src/mcp/tools/video.ts +3 -4
  40. package/src/native/ghidra/language/c64rt_6510.slaspec +4 -1
  41. package/src/protocol/vice.ts +32 -6
  42. /package/skills/c64-assembler/scripts/{compare.ts → lib/compare.ts} +0 -0
  43. /package/skills/c64-disk/scripts/{containers.ts → lib/containers.ts} +0 -0
  44. /package/skills/c64-provenance/scripts/{compare.ts → lib/compare.ts} +0 -0
  45. /package/skills/c64-provenance/scripts/{library.ts → lib/library.ts} +0 -0
  46. /package/skills/c64-provenance/scripts/{runtime.ts → lib/runtime.ts} +0 -0
  47. /package/skills/c64-provenance/scripts/{separate.ts → lib/separate.ts} +0 -0
  48. /package/skills/c64-source/scripts/{loader.ts → lib/loader.ts} +0 -0
  49. /package/skills/c64-source/scripts/{opcodes.ts → lib/opcodes.ts} +0 -0
  50. /package/skills/c64-source/scripts/{source.ts → lib/source.ts} +0 -0
  51. /package/skills/c64-static-analysis/scripts/{findings.ts → lib/findings.ts} +0 -0
  52. /package/skills/c64-static-analysis/scripts/{instructions.ts → lib/instructions.ts} +0 -0
  53. /package/skills/c64-static-analysis/scripts/{macros.ts → lib/macros.ts} +0 -0
  54. /package/skills/c64-static-analysis/scripts/{pointers.ts → lib/pointers.ts} +0 -0
  55. /package/skills/c64-unpacker/scripts/{evidence.ts → lib/evidence.ts} +0 -0
  56. /package/skills/c64-unpacker/scripts/{loads.ts → lib/loads.ts} +0 -0
  57. /package/skills/c64-unpacker/scripts/{machine.ts → lib/machine.ts} +0 -0
  58. /package/skills/c64-unpacker/scripts/{stages.ts → lib/stages.ts} +0 -0
@@ -17,10 +17,10 @@ import { readProjectFile, writeProjectFile } from "#src/host-client/transfer.ts"
17
17
  import { ViceSessionClient } from "#src/host-client/vice-session.ts";
18
18
  import { resolveProjectPath } from "#src/project.ts";
19
19
  import { runScript, UsageError } from "#src/script.ts";
20
- import { basicStart, inspect, PackingError, readMemoryMap, underRom, writtenThenExecuted } from "./evidence.ts";
21
- import { loads, readLoadSteps } from "./loads.ts";
22
- import { callReturn, captureSegments, executedIn, inRanges, mergeMaps, stageCandidates, stagePoints, type Range } from "./stages.ts";
23
- import { entryState, IO_AREAS, json, machineState, readRam, READY_FRAMES, vectorsOf } from "./machine.ts";
20
+ import { basicStart, inspect, PackingError, readMemoryMap, underRom, writtenThenExecuted } from "./lib/evidence.ts";
21
+ import { loads, readLoadSteps } from "./lib/loads.ts";
22
+ import { callReturn, captureSegments, executedIn, inRanges, mergeMaps, stageCandidates, stagePoints, type Range } from "./lib/stages.ts";
23
+ import { entryState, IO_AREAS, json, machineState, readRam, READY_FRAMES, vectorsOf } from "./lib/machine.ts";
24
24
 
25
25
  const USAGE = `unpack.ts inspect <program.prg>
26
26
  unpack.ts trace <program.prg> [--frames <n>] [--entry <address>] [--steps <file>]
package/src/c64.ts CHANGED
@@ -20,6 +20,16 @@ export function formatC64Address(value: number): string {
20
20
  return `$${checkAddress(value).toString(16).padStart(4, "0")}`;
21
21
  }
22
22
 
23
+ /** VIC-II state with its addresses as $xxxx; the other values stay numbers. */
24
+ export function formatVicii<T extends { screenAddress: number; graphicsAddress: number }>(state: T): Omit<T, "screenAddress" | "graphicsAddress"> & { screenAddress: string; graphicsAddress: string } {
25
+ return { ...state, screenAddress: formatC64Address(state.screenAddress), graphicsAddress: formatC64Address(state.graphicsAddress) };
26
+ }
27
+
28
+ /** A sprite with its data address as $xxxx; the other values stay numbers. */
29
+ export function formatSprite<T extends { dataAddress: number }>(sprite: T): Omit<T, "dataAddress"> & { dataAddress: string } {
30
+ return { ...sprite, dataAddress: formatC64Address(sprite.dataAddress) };
31
+ }
32
+
23
33
  /** A byte as $xx. Throws RangeError outside 0-255. */
24
34
  export function formatC64Byte(value: number): string {
25
35
  if (!Number.isInteger(value) || value < 0 || value > 0xff) throw new RangeError(`C64 byte out of range: ${value}`);
@@ -29,7 +29,7 @@ interface Manifest {
29
29
  }
30
30
 
31
31
  /** The directory that the manifest and relative item paths belong to. */
32
- function scopeRoot(scope: Scope, cwd = process.cwd()): string {
32
+ export function scopeRoot(scope: Scope, cwd = process.cwd()): string {
33
33
  return scope === "global" ? homedir() : cwd;
34
34
  }
35
35
 
package/src/cli/main.ts CHANGED
@@ -14,7 +14,7 @@ import { WireFailure } from "../protocol/messages.ts";
14
14
  import { startTrace } from "../trace.ts";
15
15
  import { runApSdk } from "./ap-sdk.ts";
16
16
  import { installedItems, parseTargets, tidyAfterUninstall, undoWindsurfProjectMcp, windsurfBefore, withoutWindsurfMcp, type Scope } from "./cleanup.ts";
17
- import { hostStatusLines, toolLines } from "./status.ts";
17
+ import { hostStatusLines, installedLines, toolLines } from "./status.ts";
18
18
 
19
19
  const HELP = `c64-re-tools: Commodore 64 reverse engineering and development
20
20
 
@@ -52,6 +52,7 @@ async function status(): Promise<number> {
52
52
  console.log(`c64-re-tools ${PACKAGE.version}`);
53
53
  console.log(`Package: ${join(here, "..", "..")}`);
54
54
  console.log(`Node: ${process.version} (${process.execPath})`);
55
+ for (const line of installedLines()) console.log(line);
55
56
  // The skill scripts run these tools here, on this machine.
56
57
  const local = await localToolStatus(localToolContext());
57
58
  for (const line of toolLines("Tools here:", local)) console.log(line);
package/src/cli/status.ts CHANGED
@@ -1,6 +1,10 @@
1
- // The status report of the CLI: the tools on this machine and the Host
2
- // Runtime with its tools.
1
+ // The status report of the CLI: the installed skills and MCP declarations,
2
+ // the tools on this machine and the Host Runtime with its tools.
3
3
 
4
+ import { readFileSync } from "node:fs";
5
+ import { isAbsolute, join } from "node:path";
6
+
7
+ import { installedItems, scopeRoot, type InstallItem, type Scope } from "./cleanup.ts";
4
8
  import { hostTools, type ToolCallOptions } from "../host-client/tools.ts";
5
9
  import { viceVersionOf } from "../native/discover.ts";
6
10
  import { PACKAGE } from "../package.ts";
@@ -59,3 +63,66 @@ export function mixedViceBuilds(tools: readonly ToolStatus[]): string | undefine
59
63
  if (new Set(builds).size <= 1) return undefined;
60
64
  return `the VICE tools come from different VICE builds: ${running.map((tool) => `${tool.version} (${tool.path})`).join(", ")}. Put x64sc, c1541 and petcat of one VICE first on PATH, or set C64RT_VICE, C64RT_C1541 and C64RT_PETCAT.`;
61
65
  }
66
+
67
+ /**
68
+ * The skills and MCP declarations that install wrote for this project and
69
+ * globally (b's R2-01): the c64-re-tools version of each harness's skills,
70
+ * from their package.json, and the command that starts the MCP server. A
71
+ * warning names skills of another version than this one.
72
+ */
73
+ export function installedLines(cwd = process.cwd()): string[] {
74
+ const lines: string[] = [];
75
+ for (const scope of ["project", "global"] as const satisfies readonly Scope[]) {
76
+ const items = installedItems(scope, undefined, cwd);
77
+ if (items.length === 0) continue;
78
+ const root = scopeRoot(scope, cwd);
79
+ lines.push(`Installed ${scope === "project" ? `in this project (${root})` : "globally"}:`);
80
+ for (const harness of [...new Set(items.map((item) => item.harness))]) {
81
+ const skills = items.filter((item) => item.harness === harness && item.kind === "skill");
82
+ const versions = [...new Set(skills.map((item) => skillVersion(item, root)).filter((version) => version !== undefined))];
83
+ if (skills.length > 0) {
84
+ lines.push(` ${harness}: ${skills.length} skills, c64-re-tools ${versions.length === 0 ? "of an unknown version" : versions.join(" and ")}`);
85
+ const other = versions.filter((version) => version !== PACKAGE.version);
86
+ if (other.length > 0) lines.push(` Warning: the ${harness} skills are c64-re-tools ${other.join(" and ")} and this is ${PACKAGE.version}. Run npx -y ${PACKAGE.name}@${PACKAGE.tag} update${scope === "global" ? " --global" : ""} to install this version.`);
87
+ }
88
+ for (const item of items.filter((candidate) => candidate.harness === harness && candidate.kind === "mcp")) {
89
+ lines.push(` ${harness} MCP server: ${mcpCommand(item, root) ?? `declared in ${item.files.join(", ")}`}`);
90
+ }
91
+ }
92
+ }
93
+ if (lines.length === 0) return ["Installed: nothing in this project or globally."];
94
+ lines.push(" The MCP server gives its own version in the result of c64_status.");
95
+ return lines;
96
+ }
97
+
98
+ function itemPath(file: string, root: string): string {
99
+ return isAbsolute(file) ? file : join(root, file);
100
+ }
101
+
102
+ /** The version in the package.json of an installed skill; a skill without scripts has none. */
103
+ function skillVersion(item: InstallItem, root: string): string | undefined {
104
+ const file = item.files.find((candidate) => candidate.endsWith("/package.json") || candidate.endsWith("\\package.json"));
105
+ if (file === undefined) return undefined;
106
+ try {
107
+ const { version } = JSON.parse(readFileSync(itemPath(file, root), "utf8")) as { version?: unknown };
108
+ return typeof version === "string" ? version : undefined;
109
+ } catch {
110
+ return undefined;
111
+ }
112
+ }
113
+
114
+ /** The command of the MCP server in a JSON configuration file, or undefined for another format. */
115
+ function mcpCommand(item: InstallItem, root: string): string | undefined {
116
+ const key = item.detail?.mergeKey;
117
+ const [file] = item.files;
118
+ if (key === undefined || file === undefined) return undefined;
119
+ try {
120
+ const servers = (JSON.parse(readFileSync(itemPath(file, root), "utf8")) as Record<string, unknown>)[key] as Record<string, { command?: unknown; args?: unknown }> | undefined;
121
+ const server = servers?.[item.name];
122
+ if (typeof server?.command !== "string") return undefined;
123
+ const args = Array.isArray(server.args) ? server.args.filter((arg): arg is string => typeof arg === "string") : [];
124
+ return [server.command, ...args].join(" ");
125
+ } catch {
126
+ return undefined;
127
+ }
128
+ }
package/src/host/main.ts CHANGED
@@ -7,6 +7,7 @@ import { ProcessSupervisor } from "../native/processes.ts";
7
7
  import { startTrace, trace } from "../trace.ts";
8
8
  import { ListenError, parsePort, startHostServer, stopSignal } from "./server.ts";
9
9
  import { viceFlaws, type ViceVersion } from "../native/discover.ts";
10
+ import { inspect } from "./tools/c1541.ts";
10
11
  import { createToolDispatcher } from "./tools/index.ts";
11
12
  import { checkViceStarts, findVice } from "./vice/process.ts";
12
13
  import { viceSessionFactory } from "./vice/session.ts";
@@ -77,6 +78,9 @@ async function main(): Promise<number> {
77
78
  return 2;
78
79
  }
79
80
 
81
+ // First, so that every start, also a refused one, tells which runtime runs.
82
+ log(`${PACKAGE.name} ${PACKAGE.version} (Node.js ${process.version}, ${process.platform} ${process.arch})`);
83
+
80
84
  // Without an emulator the runtime is useless: refuse to start, by name and with the remedy.
81
85
  try {
82
86
  findVice(process.env);
@@ -111,7 +115,16 @@ async function main(): Promise<number> {
111
115
  host: "0.0.0.0",
112
116
  port,
113
117
  log,
114
- createViceSession: viceSessionFactory({ supervisor, log, flaws }),
118
+ createViceSession: viceSessionFactory({
119
+ supervisor,
120
+ log,
121
+ flaws,
122
+ // c1541 lists the disk, so an autostart index selects a program (R2-08).
123
+ directory: async (imageType, image) => {
124
+ const { result } = await inspect({ action: "directory", imageType }, image, { supervisor, signal: new AbortController().signal, log });
125
+ return result.action === "directory" ? result.entries : [];
126
+ },
127
+ }),
115
128
  tools: createToolDispatcher({ supervisor, log, vice }),
116
129
  });
117
130
  } catch (error) {
@@ -3,8 +3,9 @@
3
3
  // session starts headless and can move the machine into a VICE with a window
4
4
  // and back; breakpoints, watchpoints and the rest go along.
5
5
 
6
- import { WireFailure, type VideoStandard } from "../../protocol/messages.ts";
7
- import { MAX_BASELINES, MAX_SNAPSHOTS, MAX_COMPARE_DIFFERENCES, RASTER, type Breakpoint, type BreakpointParams, type PointRemoved, type Condition, type ExecutionParams, type HistoryEntry, type RunTarget, type RunUntilResult, type ScreenComparison, type CiaSelection, type CiaState, type ObserveParams, type ObserveResult, type SidState, type SpriteState, type ViciiState, type JoystickState, type Watchpoint, type WatchpointParams, type ExecutionResult, type MachineStatus, type RunState, type ViceOperation, type ViceOperations, type WindowResult } from "../../protocol/vice.ts";
6
+ import { isOneOf, WireFailure, type VideoStandard } from "../../protocol/messages.ts";
7
+ import { DISK_IMAGE_TYPES, type DiskFile, type DiskImageType } from "../../protocol/tools.ts";
8
+ import { MAX_BASELINES, MAX_SNAPSHOTS, MAX_COMPARE_DIFFERENCES, RASTER, type Breakpoint, type BreakpointParams, type PointRemoved, type Condition, type ExecutionParams, type HistoryEntry, type RunTarget, type RunUntilResult, type ScreenComparison, type CiaSelection, type CiaState, type ObserveParams, type ObserveResult, type SidState, type SpriteState, type ViciiState, type JoystickState, type Watchpoint, type WatchpointParams, type ExecutionResult, type MachineStatus, type PointStopReason, type RunState, type ViceOperation, type ViceOperations, type WindowResult } from "../../protocol/vice.ts";
8
9
  import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
9
10
  import { tmpdir } from "node:os";
10
11
  import { join } from "node:path";
@@ -123,8 +124,12 @@ export interface SessionOptions {
123
124
  ownPath?: (path: string) => () => void;
124
125
  /** The known flaws of this VICE version (D56); keyboard and joystick results name them. */
125
126
  flaws?: readonly string[];
127
+ /** Lists the entries of a disk image, so an autostart index is checked first (R2-08). Without it, VICE gets the index unchecked. */
128
+ directory?: DiskDirectory;
126
129
  }
127
130
 
131
+ export type DiskDirectory = (type: DiskImageType, image: Buffer) => Promise<DiskFile[]>;
132
+
128
133
  /**
129
134
  * Where VICE keeps a staged file: an autostarted disk stays in drive 8, a tape
130
135
  * in the datasette and a cartridge in the port, until the same place gets
@@ -151,6 +156,7 @@ export class ViceSession implements ViceSessionHandle {
151
156
  readonly #untilReturnLimitMs: number;
152
157
  readonly #stepLimitMs: number;
153
158
  readonly #launch: ViceLauncher | undefined;
159
+ readonly #directory: DiskDirectory | undefined;
154
160
  /** Session files that outlive one VICE: snapshots, staged media, handovers. */
155
161
  readonly #workDir: string;
156
162
  readonly #releaseWorkDir: () => void;
@@ -177,6 +183,8 @@ export class ViceSession implements ViceSessionHandle {
177
183
  #driveStop = false;
178
184
  /** Counts stops caused by checkpoints or a CPU JAM rather than by a monitor command. */
179
185
  #haltCount = 0;
186
+ /** What stopped the machine last, when a user breakpoint or watchpoint or a JAM did; cleared when it runs again. */
187
+ #lastStop: { stopReason: PointStopReason; pointId?: number } | undefined;
180
188
  #queue: Promise<unknown> = Promise.resolve();
181
189
  #closed = false;
182
190
  #lost = false;
@@ -218,6 +226,7 @@ export class ViceSession implements ViceSessionHandle {
218
226
  this.#untilReturnLimitMs = options.untilReturnLimitMs ?? UNTIL_RETURN_LIMIT_MS;
219
227
  this.#stepLimitMs = options.stepLimitMs ?? RUNNING_COMMAND_TIMEOUT_MS;
220
228
  this.#launch = options.launch;
229
+ this.#directory = options.directory;
221
230
  this.#inputFlaw = options.flaws === undefined || options.flaws.length === 0 ? undefined : options.flaws.join(" ");
222
231
  this.#workDir = mkdtempSync(join(tmpdir(), "c64-re-tools-session-"));
223
232
  this.#releaseWorkDir = options.ownPath?.(this.#workDir) ?? (() => {});
@@ -374,7 +383,9 @@ export class ViceSession implements ViceSessionHandle {
374
383
  return this.#programLoad(params as ViceOperations["programLoad"]["params"], onlyAttachment(attachments));
375
384
  case "autostart": {
376
385
  const { type, index, run } = params as ViceOperations["autostart"]["params"];
377
- const file = this.#stage(onlyAttachment(attachments), type);
386
+ const bytes = onlyAttachment(attachments);
387
+ if (index > 0 && this.#directory !== undefined && isOneOf(DISK_IMAGE_TYPES, type)) checkDirectoryEntry(await this.#directory(type, bytes), index);
388
+ const file = this.#stage(bytes, type);
378
389
  // VICE keeps a cartridge through every reset and autostart, as a real C64 does.
379
390
  if (type !== "crt" && this.#cartridge) {
380
391
  await this.#observe(() => this.#machine.detachCartridge());
@@ -462,8 +473,12 @@ export class ViceSession implements ViceSessionHandle {
462
473
  return { cycles: (now - this.#timingStart).toString() };
463
474
  }
464
475
  case "profile": {
465
- const { limit } = params as ViceOperations["profile"]["params"];
466
- return { entries: await this.#observe(() => this.#machine.profile(limit)) };
476
+ const request = params as ViceOperations["profile"]["params"];
477
+ if (request.action === "clear") {
478
+ await this.#observe(() => this.#machine.startProfiler());
479
+ return { cleared: true };
480
+ }
481
+ return { entries: await this.#observe(() => this.#machine.profile(request.limit)) };
467
482
  }
468
483
  case "memmap": {
469
484
  const request = params as ViceOperations["memmap"]["params"];
@@ -508,7 +523,12 @@ export class ViceSession implements ViceSessionHandle {
508
523
  const warp = await this.#observe(() => this.#machine.warp());
509
524
  // Read after #observe: it resumes a running machine, unless a breakpoint or a JAM stopped it meanwhile.
510
525
  const status: MachineStatus = { state: this.#state, videoStandard: this.#videoStandard, warp, window: this.#mode === "window", cartridge: this.#cartridge };
511
- if (status.state === "stopped") status.pc = await this.#pc();
526
+ if (status.state === "stopped") {
527
+ status.pc = await this.#pc();
528
+ // VICE 3.9 sends no JAM event: the opcode at the pc tells.
529
+ const stop = this.#lastStop ?? ((await this.#jammedAt(status.pc)) ? { stopReason: "jam" as const } : undefined);
530
+ if (stop !== undefined) Object.assign(status, stop);
531
+ }
512
532
  return status;
513
533
  }
514
534
 
@@ -539,7 +559,13 @@ export class ViceSession implements ViceSessionHandle {
539
559
  throw new WireFailure("invalid-input", `The anchor is outside the frame: a ${this.#videoStandard} frame has lines 0-${raster.lines - 1} and cycles 0-${raster.cycles - 1}.`);
540
560
  }
541
561
  const outcome = await this.#runFrames(params.count, undefined, params.anchor);
542
- return { state: "stopped", pc: outcome.pc, advancedFrames: outcome.frames };
562
+ return {
563
+ state: "stopped",
564
+ pc: outcome.pc,
565
+ advancedFrames: outcome.frames,
566
+ stopReason: outcome.end === "target" ? "frames" : outcome.end,
567
+ ...(outcome.pointId === undefined ? {} : { pointId: outcome.pointId }),
568
+ };
543
569
  }
544
570
  await this.#stop();
545
571
  if (params.action === "until-return") {
@@ -600,7 +626,7 @@ export class ViceSession implements ViceSessionHandle {
600
626
  * checkpoint, a user breakpoint or watchpoint, or a JAM ends the run early.
601
627
  * The machine is stopped afterwards.
602
628
  */
603
- async #runFrames(limit: number, target?: { checkpoint: number; deferred: boolean }, anchor?: { line: number; cycle: number }): Promise<{ end: RunEnd; frames: number; pc: number; space: "c64" | "drive8" }> {
629
+ async #runFrames(limit: number, target?: { checkpoint: number; deferred: boolean }, anchor?: { line: number; cycle: number }): Promise<{ end: RunEnd; frames: number; pc: number; space: "c64" | "drive8"; pointId?: number }> {
604
630
  await this.#stop();
605
631
  const lines = RASTER[this.#videoStandard].lines;
606
632
  // A frame ends where the raster passes the anchor; without one, where this run starts.
@@ -642,7 +668,8 @@ export class ViceSession implements ViceSessionHandle {
642
668
  }
643
669
  // A drive stop is completed before the result names the computer's pc; `space` says which CPU stopped.
644
670
  const driveStop = this.#driveStop;
645
- return { end, frames, pc: (await this.#completeDriveStop()) ?? stop.pc, space: driveStop ? "drive8" : "c64" };
671
+ const pointId = end === "target" ? undefined : this.#userPoint(stop.hits)?.pointId;
672
+ return { end, frames, pc: (await this.#completeDriveStop()) ?? stop.pc, space: driveStop ? "drive8" : "c64", ...(pointId === undefined ? {} : { pointId }) };
646
673
  }
647
674
  } finally {
648
675
  await this.#machine.deleteCheckpoint(clock);
@@ -659,6 +686,8 @@ export class ViceSession implements ViceSessionHandle {
659
686
  async #completeDriveStop(): Promise<number | undefined> {
660
687
  if (!this.#driveStop) return undefined;
661
688
  this.#driveStop = false;
689
+ // The drive checkpoint stopped the machine; the stop at the boundary keeps that reason.
690
+ const reason = this.#lastStop;
662
691
  const boundary = await this.#machine.addCheckpoint({ start: 0x0000, end: 0xffff, operation: 0x04, space: "c64" });
663
692
  try {
664
693
  for (let attempt = 0; attempt < DRIVE_STOP_TRIES; attempt++) {
@@ -669,6 +698,7 @@ export class ViceSession implements ViceSessionHandle {
669
698
  // The drive can hit its checkpoint again before the computer gets to the next instruction.
670
699
  if (stop.hits.includes(boundary)) {
671
700
  this.#driveStop = false;
701
+ this.#lastStop = reason;
672
702
  trace().event("vice.drive-stop", { pc: stop.pc, resumes: attempt + 1 });
673
703
  return stop.pc;
674
704
  }
@@ -683,14 +713,20 @@ export class ViceSession implements ViceSessionHandle {
683
713
  /** Names what caused a stop during a frame-clocked run. */
684
714
  #stopCause(stop: Stop, clock: number, target: number | undefined): Exclude<RunEnd, "frames" | "jam"> | "clock" | "unknown" {
685
715
  if (target !== undefined && stop.hits.includes(target)) return "target";
686
- const isPoint = (registry: Map<number, { checkpoint: number }>) =>
687
- [...registry.values()].some((point) => stop.hits.includes(point.checkpoint));
688
- if (isPoint(this.#breakpoints)) return "breakpoint";
689
- if (isPoint(this.#watchpoints)) return "watchpoint";
716
+ const point = this.#userPoint(stop.hits);
717
+ if (point !== undefined) return point.stopReason;
690
718
  if (stop.hits.includes(clock)) return "clock";
691
719
  return "unknown";
692
720
  }
693
721
 
722
+ /** The user breakpoint, else the user watchpoint, among the hit VICE checkpoints. */
723
+ #userPoint(hits: number[]): { stopReason: "breakpoint" | "watchpoint"; pointId: number } | undefined {
724
+ for (const [stopReason, registry] of [["breakpoint", this.#breakpoints], ["watchpoint", this.#watchpoints]] as const) {
725
+ for (const [pointId, point] of registry) if (hits.includes(point.checkpoint)) return { stopReason, pointId };
726
+ }
727
+ return undefined;
728
+ }
729
+
694
730
  async #jammedAt(pc: number): Promise<boolean> {
695
731
  const opcode = await this.#machine.readMemory({ address: pc, size: 1, space: "c64", view: "cpu" });
696
732
  return JAM_OPCODES.has(Number.parseInt(opcode, 16));
@@ -736,7 +772,7 @@ export class ViceSession implements ViceSessionHandle {
736
772
  const stopReason = outcome.end === "frames" ? "timeout" : outcome.end;
737
773
  // A stop event carries the computer's PC; a stop in the drive reports the drive's (F-31).
738
774
  const pc = outcome.space === "drive8" ? (await this.#machine.readRegisters("drive8")).pc : outcome.pc;
739
- return { reached: stopReason === "target", stopReason, state: "stopped", pc, space: outcome.space };
775
+ return { reached: stopReason === "target", stopReason, state: "stopped", pc, space: outcome.space, ...(outcome.pointId === undefined ? {} : { pointId: outcome.pointId }) };
740
776
  } finally {
741
777
  await this.#machine.deleteCheckpoint(checkpoint);
742
778
  }
@@ -1185,6 +1221,7 @@ export class ViceSession implements ViceSessionHandle {
1185
1221
  */
1186
1222
  async #resumingCommand(send: () => Promise<void>): Promise<void> {
1187
1223
  this.#state = "running";
1224
+ this.#lastStop = undefined;
1188
1225
  try {
1189
1226
  await send();
1190
1227
  } catch (error) {
@@ -1360,10 +1397,12 @@ export class ViceSession implements ViceSessionHandle {
1360
1397
  const stop: Stop = { pc: decodeProgramCounter(event.body), hits: this.#checkpointHits };
1361
1398
  if (stop.hits.length > 0 || event.type === ResponseType.jam) this.#haltCount++;
1362
1399
  if (stop.hits.some((hit) => this.#adapter?.isDriveCheckpoint(hit))) this.#driveStop = true;
1400
+ this.#lastStop = event.type === ResponseType.jam ? { stopReason: "jam" } : this.#userPoint(stop.hits);
1363
1401
  this.#checkpointHits = [];
1364
1402
  this.#state = "stopped";
1365
1403
  this.#stopWaiters = this.#stopWaiters.filter((waiter) => !waiter(stop));
1366
1404
  } else if (event.type === ResponseType.resumed) {
1405
+ this.#lastStop = undefined;
1367
1406
  this.#state = "running";
1368
1407
  }
1369
1408
  }
@@ -1400,6 +1439,17 @@ async function setPoint(
1400
1439
  return checkpoint;
1401
1440
  }
1402
1441
 
1442
+ /**
1443
+ * Refuses an autostart index that selects no program: VICE numbers the
1444
+ * entries from 1, and the caller from 0, as the c64-disk directory list does.
1445
+ * A DEL entry holds no program (R2-08), and an index past the last entry selects nothing.
1446
+ */
1447
+ function checkDirectoryEntry(entries: DiskFile[], index: number): void {
1448
+ const entry = entries[index - 1];
1449
+ if (entry === undefined) throw new WireFailure("invalid-input", `The disk has ${entries.length} directory entries, so index ${index - 1} selects no entry. The c64-disk directory list gives the index of each entry.`);
1450
+ if (entry.type === "del") throw new WireFailure("invalid-input", `Directory entry ${index - 1} (${entry.name}) is a DEL entry and holds no program. Give the index of a PRG entry.`);
1451
+ }
1452
+
1403
1453
  /** The production factory: launch a headless VICE per session through the host's supervisor. */
1404
1454
  export function viceSessionFactory(options: {
1405
1455
  supervisor: ProcessSupervisor;
@@ -1407,6 +1457,7 @@ export function viceSessionFactory(options: {
1407
1457
  log?: (line: string) => void;
1408
1458
  /** The known flaws of the VICE version that the host checked at start. */
1409
1459
  flaws?: readonly string[];
1460
+ directory?: DiskDirectory;
1410
1461
  }): ViceSessionFactory {
1411
1462
  return async ({ videoStandard, log = options.log ?? (() => {}) }) => {
1412
1463
  const launch: ViceLauncher = (mode, launchLog) =>
@@ -1421,6 +1472,11 @@ export function viceSessionFactory(options: {
1421
1472
  ...(options.env === undefined ? {} : { env: options.env }),
1422
1473
  });
1423
1474
  const vice = await launch("headless", () => {});
1424
- return ViceSession.start(vice, videoStandard, log, { launch, ownPath: (path) => options.supervisor.ownPath(path), ...(options.flaws === undefined ? {} : { flaws: options.flaws }) });
1475
+ return ViceSession.start(vice, videoStandard, log, {
1476
+ launch,
1477
+ ownPath: (path) => options.supervisor.ownPath(path),
1478
+ ...(options.flaws === undefined ? {} : { flaws: options.flaws }),
1479
+ ...(options.directory === undefined ? {} : { directory: options.directory }),
1480
+ });
1425
1481
  };
1426
1482
  }
@@ -90,7 +90,7 @@ export function checkActionStep(kind: string, step: Record<string, unknown>, at:
90
90
  return;
91
91
  }
92
92
  case "frames":
93
- if (!isInteger(step.frames, 1, MAX_EXECUTION_COUNT)) fail(`${at} needs a frame count from 1 to ${MAX_EXECUTION_COUNT}`);
93
+ if (!isInteger(step.frames, 1, MAX_TIMEOUT_FRAMES)) fail(`${at} needs a frame count from 1 to ${MAX_TIMEOUT_FRAMES}`);
94
94
  return;
95
95
  case "runUntil": {
96
96
  const target = step.runUntil;
@@ -154,7 +154,12 @@ export async function runActionStep(client: ViceSessionClient, step: ActionStep,
154
154
  await client.keyboard(textToPetscii(step.type));
155
155
  } else if ("frames" in step) {
156
156
  await stop(client);
157
- await client.execution({ action: "advance-frames", count: step.frames, space: "c64" });
157
+ // One advance-frames runs at most MAX_EXECUTION_COUNT frames. Each part ends at the raster position
158
+ // where it started, so the parts give the same frames as one run; a breakpoint ends them all.
159
+ for (let left = step.frames; left > 0; left -= MAX_EXECUTION_COUNT) {
160
+ const part = await client.execution({ action: "advance-frames", count: Math.min(left, MAX_EXECUTION_COUNT), space: "c64" });
161
+ if (part.stopReason !== "frames") break;
162
+ }
158
163
  } else {
159
164
  const until = step.runUntil;
160
165
  const target: RunTarget =
@@ -2,7 +2,7 @@
2
2
  // one VICE. Exposes C64-domain operations only.
3
3
 
4
4
  import { ProtocolError, WireFailure, type VideoStandard } from "../protocol/messages.ts";
5
- import { validateViceResult, type Breakpoint, type BreakpointParams, type PointRemoved, type Watchpoint, type WatchpointParams, type ExecutionParams, type ExecutionResult, type JoystickState, type MachineStatus, type MemmapRange, type MemoryWriteParams, type RegisterValues, type ResetMode, type RunState, type RunTarget, type RunUntilResult, type MemoryReadParams, type MemoryReadResult, type Registers, type Space, type ViceOperation, type ViceOperations } from "../protocol/vice.ts";
5
+ import { validateViceResult, type Breakpoint, type BreakpointParams, type PointRemoved, type Watchpoint, type WatchpointParams, type ExecutionParams, type ExecutionResult, type JoystickState, type MachineStatus, type MemmapRange, type MemoryWriteParams, type ProfileEntry, type RegisterValues, type ResetMode, type RunState, type RunTarget, type RunUntilResult, type MemoryReadParams, type MemoryReadResult, type Registers, type Space, type ViceOperation, type ViceOperations } from "../protocol/vice.ts";
6
6
  import { HostConnection } from "./connect.ts";
7
7
  import { readProjectFile } from "./transfer.ts";
8
8
 
@@ -186,8 +186,10 @@ export class ViceSessionClient {
186
186
  return this.#request("timing", { action });
187
187
  }
188
188
 
189
- profile(limit: number): Promise<ViceOperations["profile"]["result"]> {
190
- return this.#request("profile", { limit });
189
+ profile(params: { action: "read"; limit: number }): Promise<{ entries: ProfileEntry[] }>;
190
+ profile(params: { action: "clear" }): Promise<{ cleared: boolean }>;
191
+ profile(params: ViceOperations["profile"]["params"]): Promise<ViceOperations["profile"]["result"]> {
192
+ return this.#request("profile", params);
191
193
  }
192
194
 
193
195
  /** Action read gives the ranges; action clear gives cleared. */
package/src/mcp/server.ts CHANGED
@@ -300,6 +300,7 @@ const INSTRUCTIONS =
300
300
  "These tools control one live Commodore 64 emulator that belongs to this session. " +
301
301
  "Addresses are four hex digits after a dollar sign, for example $c000. " +
302
302
  "Memory data is lowercase hex without separators. " +
303
+ "Other register values and chip values are decimal numbers. " +
303
304
  "A read does not change the run state of the machine.";
304
305
 
305
306
  export function createMcpServer(options: { tools: readonly ToolDefinition[]; session: SessionSource; log?: (line: string) => void }): Server {
@@ -208,19 +208,33 @@ export const c64Profile = defineTool({
208
208
  name: "c64_profile",
209
209
  title: "Cycle profile",
210
210
  description:
211
- "Get the routines where the C64 CPU spent the most cycles since the session started or since the last c64_window. " +
211
+ "Action read gets the routines where the C64 CPU spent the most cycles since the session started, the last clear or the last c64_window. " +
212
+ "Action clear sets all counts to zero, so that the next read measures only what runs after it. " +
213
+ "The emulator gives the cycles to the routine that a JSR called last. " +
214
+ "Code that the CPU enters with JMP or a branch counts for that routine. " +
212
215
  "After c64_window or a snapshot restore, the emulator can give the cycles to a routine that ran before the restore, for example $fd50. Do not trust the routine addresses then. " +
213
- `limit is the maximum number of routines (1 to ${MAX_PROFILE}, default 20). ` +
216
+ `limit is the maximum number of routines for read (1 to ${MAX_PROFILE}, default 20). ` +
214
217
  "Each entry gives the routine address, its total cycles with the routines it calls, and its own cycles. " +
215
218
  "percent is the part of all cycles that the routine used itself. " +
216
219
  "Cycle counts are decimal strings.",
217
- inputSchema: z.object({ limit: z.number().int().min(1).max(MAX_PROFILE).default(20) }).strict(),
220
+ inputSchema: z
221
+ .object({
222
+ action: z.enum(["read", "clear"]).default("read"),
223
+ limit: z.number().int().min(1).max(MAX_PROFILE).optional().describe("read only. Default 20"),
224
+ })
225
+ .strict(),
218
226
  outputSchema: z.object({
219
- entries: z.array(z.object({ address: AddressOutput, totalCycles: z.string(), selfCycles: z.string(), percent: z.number() })),
227
+ entries: z.array(z.object({ address: AddressOutput, totalCycles: z.string(), selfCycles: z.string(), percent: z.number() })).optional(),
228
+ cleared: z.boolean().optional(),
220
229
  }),
221
- readOnly: true,
230
+ readOnly: false,
222
231
  async run(input, session) {
223
- const { entries } = await session.profile(input.limit);
232
+ const { action, ...fields } = input;
233
+ if (action === "clear") {
234
+ requireFields("clear", fields, []);
235
+ return session.profile({ action: "clear" });
236
+ }
237
+ const { entries } = await session.profile({ action: "read", limit: fields.limit ?? 20 });
224
238
  return { entries: entries.map((entry) => ({ ...entry, address: formatC64Address(entry.address) })) };
225
239
  },
226
240
  });
@@ -251,7 +265,8 @@ export const c64Memmap = defineTool({
251
265
  "If more ranges follow, the result also gives truncated: true and next. Read again with start set to next. " +
252
266
  "With out, read writes a coverage map to that project file, one byte for each address from start to end. " +
253
267
  "Bits 0 to 2 are RAM read, write and execute. Bits 3 and 4 are ROM read and execute. Bits 5 to 7 are I/O read, write and execute. " +
254
- "Execute is set only on the first byte of each executed instruction. " +
268
+ "Execute is set only on the first byte of each executed instruction. Thus read gives a new range at each instruction of code. For code, use summary or read with out. " +
269
+ "A read on a byte right after executed code, or at a return address, can be a dummy read that the CPU does not use. " +
255
270
  "Action summary gives the counts for RAM, ROM and I/O. It also gives the executed code in RAM with its operand bytes, and the RAM that was written and executed. " +
256
271
  `Each list has at most ${MAX_SUMMARY_RANGES} ranges. ` +
257
272
  "Action clear sets all access records to zero.",
@@ -3,7 +3,7 @@
3
3
  import { z } from "zod";
4
4
 
5
5
  import { formatC64Address } from "../../c64.ts";
6
- import { COMPARISONS, EXECUTION_ACTIONS, MAX_EXECUTION_COUNT, MAX_TIMEOUT_FRAMES, RUN_STATES, SPACES, STOP_REASONS, type RunTarget } from "../../protocol/vice.ts";
6
+ import { COMPARISONS, EXECUTION_ACTIONS, MAX_EXECUTION_COUNT, MAX_TIMEOUT_FRAMES, POINT_STOP_REASONS, RUN_STATES, SPACES, STOP_REASONS, type RunTarget } from "../../protocol/vice.ts";
7
7
  import { AddressInput, AddressOutput, Byte, ConditionInput, defineTool, MemoryViewInput, SpaceInput, toCondition } from "../server.ts";
8
8
 
9
9
  export const c64Execution = defineTool({
@@ -15,7 +15,9 @@ export const c64Execution = defineTool({
15
15
  "next executes count instructions (default 1) and treats a JSR and its subroutine as one instruction. " +
16
16
  "until-return runs until the current subroutine returns (after the next RTS or RTI). " +
17
17
  "advance-frames runs exactly count video frames (count is necessary). It stops at the same raster position, count frames later. " +
18
- "A breakpoint or watchpoint can stop it before that. advancedFrames tells how many frames ran. " +
18
+ "A breakpoint, a watchpoint or a JAM opcode can stop it before that. advancedFrames tells how many frames ran. " +
19
+ "stopReason tells why it stopped: frames (all frames ran), breakpoint, watchpoint or jam. pointId gives the id of the breakpoint or watchpoint. " +
20
+ "To wait for a breakpoint or watchpoint after resume, use advance-frames with a large count. " +
19
21
  "step, next, until-return and advance-frames first stop a running CPU. Use count only with step, next and advance-frames. " +
20
22
  "step, next and until-return work only in space c64. For the drive, use c64_run_until with an address in space drive8.",
21
23
  inputSchema: z
@@ -36,6 +38,8 @@ export const c64Execution = defineTool({
36
38
  pc: AddressOutput.optional(),
37
39
  executed: z.number().int().min(0).max(MAX_EXECUTION_COUNT).optional().describe("instructions executed by step or next"),
38
40
  advancedFrames: z.number().int().min(0).max(MAX_EXECUTION_COUNT).optional().describe("frames advance-frames ran"),
41
+ stopReason: z.enum(["frames", ...POINT_STOP_REASONS]).optional().describe("advance-frames: why it stopped"),
42
+ pointId: z.number().int().min(1).optional().describe("the id of the breakpoint or watchpoint that stopped advance-frames"),
39
43
  }),
40
44
  readOnly: false,
41
45
  async run(input, session) {
@@ -46,6 +50,8 @@ export const c64Execution = defineTool({
46
50
  ...(result.pc === undefined ? {} : { pc: formatC64Address(result.pc) }),
47
51
  ...(result.executed === undefined ? {} : { executed: result.executed }),
48
52
  ...(result.advancedFrames === undefined ? {} : { advancedFrames: result.advancedFrames }),
53
+ ...(result.stopReason === undefined ? {} : { stopReason: result.stopReason }),
54
+ ...(result.pointId === undefined ? {} : { pointId: result.pointId }),
49
55
  };
50
56
  },
51
57
  });
@@ -88,6 +94,7 @@ export const c64RunUntil = defineTool({
88
94
  "Kind raster stops at the first instruction at or after a raster line and cycle. If the machine is there now, it stops at the next frame's pass. " +
89
95
  `timeoutFrames (1 to ${MAX_TIMEOUT_FRAMES}, default 3000) limits the run in video frames. ` +
90
96
  "reached tells if the target occurred. stopReason tells why the CPU stopped: target, breakpoint, watchpoint, jam (a JAM opcode) or timeout. " +
97
+ "pointId gives the id of the breakpoint or watchpoint that stopped the CPU. " +
91
98
  "space tells which CPU stopped: c64, or drive8 for a stop in the disk drive. pc is the address in that space. " +
92
99
  "The CPU is always stopped after this tool.",
93
100
  inputSchema: z
@@ -96,7 +103,14 @@ export const c64RunUntil = defineTool({
96
103
  timeoutFrames: z.number().int().min(1).max(MAX_TIMEOUT_FRAMES).default(3000),
97
104
  })
98
105
  .strict(),
99
- outputSchema: z.object({ reached: z.boolean(), stopReason: z.enum(STOP_REASONS), state: z.enum(RUN_STATES), pc: AddressOutput, space: z.enum(SPACES) }),
106
+ outputSchema: z.object({
107
+ reached: z.boolean(),
108
+ stopReason: z.enum(STOP_REASONS),
109
+ state: z.enum(RUN_STATES),
110
+ pc: AddressOutput,
111
+ space: z.enum(SPACES),
112
+ pointId: z.number().int().min(1).optional().describe("the id of the breakpoint or watchpoint that stopped the CPU"),
113
+ }),
100
114
  readOnly: false,
101
115
  async run(input, session) {
102
116
  const { target: given } = input;
@@ -3,8 +3,9 @@
3
3
  import { z } from "zod";
4
4
 
5
5
  import { formatC64Address } from "../../c64.ts";
6
+ import { PACKAGE } from "../../package.ts";
6
7
  import { VIDEO_STANDARDS } from "../../protocol/messages.ts";
7
- import { RESET_MODES, RUN_STATES, WINDOW_ACTIONS } from "../../protocol/vice.ts";
8
+ import { POINT_STOP_REASONS, RESET_MODES, RUN_STATES, WINDOW_ACTIONS } from "../../protocol/vice.ts";
8
9
  import { AddressOutput, defineTool } from "../server.ts";
9
10
 
10
11
  export const c64Status = defineTool({
@@ -14,7 +15,9 @@ export const c64Status = defineTool({
14
15
  "Get the state of the C64. The result tells if the CPU runs or has stopped, the video standard (pal or ntsc), " +
15
16
  "and if warp mode is on. It also tells if the emulator window is open (window). " +
16
17
  "When the CPU has stopped, the result also gives the program counter (pc). " +
17
- "cartridge is true while a cartridge from c64_autostart is attached: a reset keeps it, and an autostart of another file detaches it.",
18
+ "When a breakpoint, a watchpoint or a JAM opcode stopped the CPU, stopReason tells which, and pointId gives the id of the breakpoint or watchpoint. " +
19
+ "cartridge is true while a cartridge from c64_autostart is attached: a reset keeps it, and an autostart of another file detaches it. " +
20
+ "version is the c64-re-tools version of this MCP server.",
18
21
  inputSchema: z.object({}).strict(),
19
22
  outputSchema: z.object({
20
23
  state: z.enum(RUN_STATES),
@@ -22,7 +25,10 @@ export const c64Status = defineTool({
22
25
  warp: z.boolean(),
23
26
  window: z.boolean().describe("true when the emulator window is open (c64_window)"),
24
27
  pc: AddressOutput.optional().describe("program counter. Only when the CPU is stopped"),
28
+ stopReason: z.enum(POINT_STOP_REASONS).optional().describe("what stopped the CPU. Only when a breakpoint, a watchpoint or a JAM opcode stopped it"),
29
+ pointId: z.number().int().min(1).optional().describe("the id of the breakpoint or watchpoint that stopped the CPU"),
25
30
  cartridge: z.boolean().describe("true while a cartridge from c64_autostart is attached"),
31
+ version: z.string().describe("the c64-re-tools version of this MCP server"),
26
32
  }),
27
33
  readOnly: true,
28
34
  async run(_input, session) {
@@ -33,7 +39,10 @@ export const c64Status = defineTool({
33
39
  warp: status.warp,
34
40
  window: status.window,
35
41
  cartridge: status.cartridge,
42
+ version: PACKAGE.version,
36
43
  ...(status.pc === undefined ? {} : { pc: formatC64Address(status.pc) }),
44
+ ...(status.stopReason === undefined ? {} : { stopReason: status.stopReason }),
45
+ ...(status.pointId === undefined ? {} : { pointId: status.pointId }),
37
46
  };
38
47
  },
39
48
  });
@@ -23,6 +23,7 @@ export const c64Autostart = defineTool({
23
23
  "A cartridge stays in the C64 through every reset, as on a real C64. An autostart of a file that is not a cartridge detaches it first. " +
24
24
  "Both joysticks are centred first: joystick port 1 uses the keyboard lines, and a held direction changes the RUN that the autostart types. " +
25
25
  "Without index, a disk image loads like LOAD\"*\",8,1. index selects one directory entry by its number in the c64-disk directory list (0 is the first entry). " +
26
+ "The tool refuses an index of a DEL entry, or an index after the last entry. " +
26
27
  "The machine runs when this tool returns, and the load continues.",
27
28
  inputSchema: z
28
29
  .object({