@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.
- package/package.json +1 -1
- package/skills/c64-assembler/SKILL.md +7 -2
- package/skills/c64-assembler/scripts/assemble.ts +13 -5
- package/skills/c64-disk/SKILL.md +1 -1
- package/skills/c64-disk/scripts/disk.ts +1 -1
- package/skills/c64-emulator/SKILL.md +8 -4
- package/skills/c64-memory-map/SKILL.md +2 -2
- package/skills/c64-provenance/SKILL.md +1 -1
- package/skills/c64-provenance/scripts/provenance.ts +4 -4
- package/skills/c64-reverse-engineering/SKILL.md +2 -1
- package/skills/c64-source/SKILL.md +1 -1
- package/skills/c64-source/scripts/generate.ts +3 -3
- package/skills/c64-static-analysis/SKILL.md +4 -3
- package/skills/c64-static-analysis/scripts/analyze.ts +16 -10
- package/skills/c64-static-analysis/scripts/{coverage.ts → lib/coverage.ts} +20 -0
- package/skills/c64-static-analysis/scripts/{idioms.ts → lib/idioms.ts} +14 -3
- package/skills/c64-testing/SKILL.md +3 -2
- package/skills/c64-testing/references/scenario-format.md +1 -1
- package/skills/c64-testing/scripts/checklist.ts +1 -1
- package/skills/c64-testing/scripts/{scenario.ts → lib/scenario.ts} +11 -9
- package/skills/c64-testing/scripts/run.ts +4 -4
- package/skills/c64-testing/scripts/test.ts +1 -1
- package/skills/c64-unpacker/SKILL.md +4 -1
- package/skills/c64-unpacker/scripts/unpack.ts +4 -4
- package/src/c64.ts +10 -0
- package/src/cli/cleanup.ts +1 -1
- package/src/cli/main.ts +2 -1
- package/src/cli/status.ts +69 -2
- package/src/host/main.ts +14 -1
- package/src/host/vice/session.ts +71 -15
- package/src/host-client/steps.ts +7 -2
- package/src/host-client/vice-session.ts +5 -3
- package/src/mcp/server.ts +1 -0
- package/src/mcp/tools/debug.ts +22 -7
- package/src/mcp/tools/execution.ts +17 -3
- package/src/mcp/tools/machine.ts +11 -2
- package/src/mcp/tools/media.ts +1 -0
- package/src/mcp/tools/memory.ts +7 -2
- package/src/mcp/tools/video.ts +3 -4
- package/src/native/ghidra/language/c64rt_6510.slaspec +4 -1
- package/src/protocol/vice.ts +32 -6
- /package/skills/c64-assembler/scripts/{compare.ts → lib/compare.ts} +0 -0
- /package/skills/c64-disk/scripts/{containers.ts → lib/containers.ts} +0 -0
- /package/skills/c64-provenance/scripts/{compare.ts → lib/compare.ts} +0 -0
- /package/skills/c64-provenance/scripts/{library.ts → lib/library.ts} +0 -0
- /package/skills/c64-provenance/scripts/{runtime.ts → lib/runtime.ts} +0 -0
- /package/skills/c64-provenance/scripts/{separate.ts → lib/separate.ts} +0 -0
- /package/skills/c64-source/scripts/{loader.ts → lib/loader.ts} +0 -0
- /package/skills/c64-source/scripts/{opcodes.ts → lib/opcodes.ts} +0 -0
- /package/skills/c64-source/scripts/{source.ts → lib/source.ts} +0 -0
- /package/skills/c64-static-analysis/scripts/{findings.ts → lib/findings.ts} +0 -0
- /package/skills/c64-static-analysis/scripts/{instructions.ts → lib/instructions.ts} +0 -0
- /package/skills/c64-static-analysis/scripts/{macros.ts → lib/macros.ts} +0 -0
- /package/skills/c64-static-analysis/scripts/{pointers.ts → lib/pointers.ts} +0 -0
- /package/skills/c64-unpacker/scripts/{evidence.ts → lib/evidence.ts} +0 -0
- /package/skills/c64-unpacker/scripts/{loads.ts → lib/loads.ts} +0 -0
- /package/skills/c64-unpacker/scripts/{machine.ts → lib/machine.ts} +0 -0
- /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}`);
|
package/src/cli/cleanup.ts
CHANGED
|
@@ -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
|
|
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({
|
|
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) {
|
package/src/host/vice/session.ts
CHANGED
|
@@ -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 {
|
|
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
|
|
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
|
|
466
|
-
|
|
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")
|
|
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 {
|
|
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
|
-
|
|
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
|
|
687
|
-
|
|
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, {
|
|
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
|
}
|
package/src/host-client/steps.ts
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
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<
|
|
190
|
-
|
|
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 {
|
package/src/mcp/tools/debug.ts
CHANGED
|
@@ -208,19 +208,33 @@ export const c64Profile = defineTool({
|
|
|
208
208
|
name: "c64_profile",
|
|
209
209
|
title: "Cycle profile",
|
|
210
210
|
description:
|
|
211
|
-
"
|
|
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
|
|
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:
|
|
230
|
+
readOnly: false,
|
|
222
231
|
async run(input, session) {
|
|
223
|
-
const {
|
|
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
|
|
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({
|
|
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;
|
package/src/mcp/tools/machine.ts
CHANGED
|
@@ -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
|
-
"
|
|
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
|
});
|
package/src/mcp/tools/media.ts
CHANGED
|
@@ -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({
|