@henols/c64-re-tools 1.1.0 → 2.0.0-rc.3
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/LICENSE +21 -0
- package/README.md +138 -37
- package/distribution/package.ts +36 -0
- package/distribution/plugin.ts +120 -0
- package/package.json +34 -40
- package/skills/c64-assembler/SKILL.md +73 -0
- package/skills/c64-assembler/scripts/assemble.ts +103 -0
- package/skills/c64-basic/SKILL.md +67 -0
- package/skills/c64-basic/scripts/basic.ts +46 -0
- package/skills/c64-disk/SKILL.md +79 -0
- package/skills/c64-disk/scripts/disk.ts +65 -0
- package/skills/c64-emulator/SKILL.md +124 -0
- package/skills/c64-knowledge/SKILL.md +102 -0
- package/skills/c64-knowledge/scripts/knowledge.ts +257 -0
- package/skills/c64-memory-map/SKILL.md +76 -0
- package/skills/c64-memory-map/scripts/memmap.ts +81 -0
- package/skills/c64-memory-map/scripts/platform.ts +424 -0
- package/skills/c64-provenance/SKILL.md +76 -0
- package/skills/c64-provenance/scripts/compare.ts +92 -0
- package/skills/c64-provenance/scripts/provenance.ts +83 -0
- package/skills/c64-reverse-engineering/SKILL.md +90 -0
- package/skills/c64-static-analysis/SKILL.md +96 -0
- package/skills/c64-static-analysis/scripts/analyze.ts +175 -0
- package/skills/c64-static-analysis/scripts/findings.ts +73 -0
- package/skills/c64-testing/SKILL.md +79 -0
- package/skills/c64-testing/references/scenario-format.md +76 -0
- package/skills/c64-testing/scripts/checklist.ts +51 -0
- package/skills/c64-testing/scripts/scenario.ts +473 -0
- package/skills/c64-testing/scripts/test.ts +55 -0
- package/skills/c64-unpacker/SKILL.md +82 -0
- package/skills/c64-unpacker/scripts/evidence.ts +135 -0
- package/skills/c64-unpacker/scripts/unpack.ts +216 -0
- package/src/c64.ts +281 -0
- package/src/cli/ap-sdk.ts +44 -0
- package/src/cli/cleanup.ts +194 -0
- package/src/cli/main.ts +122 -0
- package/src/cli/status.ts +34 -0
- package/src/host/main.ts +106 -0
- package/src/host/server.ts +376 -0
- package/src/host/tools/c1541.ts +255 -0
- package/src/host/tools/index.ts +31 -0
- package/src/host/tools/petcat.ts +70 -0
- package/src/host/tools/status.ts +30 -0
- package/src/host/tools/version.ts +41 -0
- package/src/host/vice/adapter.ts +761 -0
- package/src/host/vice/binary-monitor.ts +389 -0
- package/src/host/vice/chips.ts +108 -0
- package/src/host/vice/connect.ts +34 -0
- package/src/host/vice/process.ts +402 -0
- package/src/host/vice/screen.ts +183 -0
- package/src/host/vice/session.ts +1316 -0
- package/src/host/vice/text-monitor.ts +223 -0
- package/src/host-client/connect.ts +238 -0
- package/src/host-client/tools.ts +89 -0
- package/src/host-client/transfer.ts +100 -0
- package/src/host-client/vice-session.ts +254 -0
- package/src/knowledge/database.ts +132 -0
- package/src/knowledge/history.ts +128 -0
- package/src/knowledge/import.ts +344 -0
- package/src/knowledge/read.ts +191 -0
- package/src/knowledge/schema.ts +140 -0
- package/src/knowledge/write.ts +348 -0
- package/src/mcp/main.ts +106 -0
- package/src/mcp/server.ts +307 -0
- package/src/mcp/tools/debug.ts +237 -0
- package/src/mcp/tools/execution.ts +116 -0
- package/src/mcp/tools/input.ts +69 -0
- package/src/mcp/tools/machine.ts +97 -0
- package/src/mcp/tools/media.ts +106 -0
- package/src/mcp/tools/memory.ts +203 -0
- package/src/mcp/tools/video.ts +260 -0
- package/src/native/acme.ts +116 -0
- package/src/native/discover.ts +141 -0
- package/src/native/dxa.ts +166 -0
- package/src/native/ghidra/analyze.ts +86 -0
- package/src/native/ghidra/index.ts +157 -0
- package/src/native/ghidra/language/c64rt_6510.cspec +34 -0
- package/src/native/ghidra/language/c64rt_6510.ldefs +15 -0
- package/src/native/ghidra/language/c64rt_6510.pspec +15 -0
- package/src/native/ghidra/language/c64rt_6510.slaspec +596 -0
- package/src/native/ghidra/scripts/C64Export.java +242 -0
- package/src/native/ghidra/scripts/C64Prepare.java +60 -0
- package/src/native/local.ts +34 -0
- package/src/native/processes.ts +350 -0
- package/src/native/run.ts +172 -0
- package/src/native/staging.ts +83 -0
- package/src/native/status.ts +101 -0
- package/src/native/types.ts +228 -0
- package/src/native/watchdog.ts +78 -0
- package/src/project.ts +39 -0
- package/src/protocol/framing.ts +132 -0
- package/src/protocol/messages.ts +243 -0
- package/src/protocol/tools.ts +247 -0
- package/src/protocol/vice.ts +1235 -0
- package/src/script.ts +59 -0
- package/src/time.ts +15 -0
- package/src/trace.ts +162 -0
- package/THIRD-PARTY-NOTICES.md +0 -26
- package/bin/cli.mjs +0 -283
- package/skills/acme-build/SKILL.md +0 -260
- package/skills/acme-build/scripts/acme.mjs +0 -341
- package/skills/acme-build/template.a +0 -53
- package/skills/c64-disk-access/SKILL.md +0 -159
- package/skills/c64-disk-access/scripts/c1541.mjs +0 -569
- package/skills/c64-memory-mapping/SKILL.md +0 -631
- package/skills/c64-memory-mapping/memmap.json +0 -8800
- package/skills/c64-memory-mapping/scripts/driver.mjs +0 -553
- package/skills/c64-petcat/SKILL.md +0 -87
- package/skills/c64-petcat/scripts/petcat.mjs +0 -221
- package/skills/c64-program-recon/SKILL.md +0 -706
- package/skills/c64-program-recon/references/control-flow.md +0 -180
- package/skills/c64-program-recon/references/graphics.md +0 -79
- package/skills/c64-program-recon/references/observation-hazards.md +0 -143
- package/skills/c64-program-recon/references/reconstruction.md +0 -133
- package/skills/c64-program-recon/references/sound-and-input.md +0 -74
- package/skills/c64-program-recon/references/tool-selection.md +0 -78
- package/skills/c64-program-recon/scripts/derive.mjs +0 -364
- package/skills/c64-program-recon/scripts/packer-finding.mjs +0 -709
- package/skills/c64-program-recon/templates/memory-map.template.md +0 -101
- package/skills/c64-provenance-diff/SKILL.md +0 -292
- package/skills/c64-provenance-diff/scripts/diff-images.mjs +0 -984
- package/skills/c64-provenance-diff/scripts/recovery-schema.mjs +0 -395
- package/skills/c64-ram-capture/RELEASES.json.example +0 -17
- package/skills/c64-ram-capture/SKILL.md +0 -474
- package/skills/c64-ram-capture/scripts/compare-cross-binary.mjs +0 -525
- package/skills/c64-ram-capture/scripts/compare.mjs +0 -258
- package/skills/c64-ram-capture/scripts/derive-transients.mjs +0 -575
- package/skills/c64-ram-capture/scripts/dump-artifacts.mjs +0 -317
- package/skills/c64-ram-capture/scripts/mcp-module.mjs +0 -174
- package/skills/c64-ram-capture/scripts/project-paths.mjs +0 -81
- package/skills/c64-ram-capture/scripts/releases.mjs +0 -109
- package/skills/c64-ram-capture/scripts/vsf-slice.mjs +0 -147
- package/skills/c64-ram-capture/scripts/watch-loads.mjs +0 -581
- package/skills/c64-ram-capture/templates/capture-record.template.md +0 -99
- package/skills/c64-ram-capture/transients/README.md +0 -137
- package/skills/routine-queue-walker/SKILL.md +0 -394
- package/skills/routine-queue-walker/scripts/completeness-report.mjs +0 -465
- package/skills/vice-wedge-triage/SKILL.md +0 -269
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
// MCP server creation, tool registration and the one place where failures
|
|
2
|
+
// become MCP results. Tool groups live in tools/; nothing here knows VICE.
|
|
3
|
+
|
|
4
|
+
import { createRequire } from "node:module";
|
|
5
|
+
|
|
6
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
7
|
+
import { CallToolRequestSchema, ListToolsRequestSchema, type CallToolResult, type Tool as McpTool } from "@modelcontextprotocol/sdk/types.js";
|
|
8
|
+
import { z } from "zod";
|
|
9
|
+
|
|
10
|
+
import { formatC64Address, parseC64Address } from "../c64.ts";
|
|
11
|
+
import type { ViceSessionClient } from "../host-client/vice-session.ts";
|
|
12
|
+
import { WireFailure, type WireError } from "../protocol/messages.ts";
|
|
13
|
+
import { COMPARISONS, CONDITION_REGISTERS, MEMORY_VIEWS, SPACES, type Condition } from "../protocol/vice.ts";
|
|
14
|
+
import { elapsedMs, trace, type TraceFields } from "../trace.ts";
|
|
15
|
+
|
|
16
|
+
/** The session operations tools may call. */
|
|
17
|
+
export type ViceSessionApi = Pick<
|
|
18
|
+
ViceSessionClient,
|
|
19
|
+
| "status"
|
|
20
|
+
| "memoryRead"
|
|
21
|
+
| "registersGet"
|
|
22
|
+
| "memoryWrite"
|
|
23
|
+
| "memorySearch"
|
|
24
|
+
| "memoryCompare"
|
|
25
|
+
| "disassemble"
|
|
26
|
+
| "registersSet"
|
|
27
|
+
| "execution"
|
|
28
|
+
| "reset"
|
|
29
|
+
| "warp"
|
|
30
|
+
| "window"
|
|
31
|
+
| "programLoad"
|
|
32
|
+
| "autostart"
|
|
33
|
+
| "diskAttach"
|
|
34
|
+
| "keyboard"
|
|
35
|
+
| "joystick"
|
|
36
|
+
| "screenCapture"
|
|
37
|
+
| "screenCompare"
|
|
38
|
+
| "screenBaselines"
|
|
39
|
+
| "screenDiscard"
|
|
40
|
+
| "snapshot"
|
|
41
|
+
| "cpuHistory"
|
|
42
|
+
| "backtrace"
|
|
43
|
+
| "timing"
|
|
44
|
+
| "profile"
|
|
45
|
+
| "memmap"
|
|
46
|
+
| "vicii"
|
|
47
|
+
| "sprites"
|
|
48
|
+
| "cia"
|
|
49
|
+
| "sid"
|
|
50
|
+
| "observe"
|
|
51
|
+
| "breakpoint"
|
|
52
|
+
| "watchpoint"
|
|
53
|
+
| "runUntil"
|
|
54
|
+
>;
|
|
55
|
+
|
|
56
|
+
/** Resolves the session, opening it on first use when the start-up attempt failed. */
|
|
57
|
+
export type SessionSource = () => Promise<ViceSessionApi>;
|
|
58
|
+
|
|
59
|
+
type JsonObject = Record<string, unknown>;
|
|
60
|
+
|
|
61
|
+
// ---------------------------------------------------------------------------
|
|
62
|
+
// Public schema primitives shared by the tool groups
|
|
63
|
+
|
|
64
|
+
/** Input address: "$" and four hex digits, either case. Parses to a number. */
|
|
65
|
+
export const AddressInput = z
|
|
66
|
+
.string()
|
|
67
|
+
.regex(/^\$[0-9a-fA-F]{4}$/, "must be $ followed by four hex digits, for example $c000")
|
|
68
|
+
.transform((value) => parseC64Address(value))
|
|
69
|
+
.describe("C64 address, $0000 to $ffff");
|
|
70
|
+
|
|
71
|
+
/** Output address: "$" and four lowercase hex digits. */
|
|
72
|
+
export const AddressOutput = z.string().regex(/^\$[0-9a-f]{4}$/).describe("C64 address, $0000 to $ffff");
|
|
73
|
+
|
|
74
|
+
export const Byte = z.number().int().min(0).max(255);
|
|
75
|
+
|
|
76
|
+
export const HexData = z.string().regex(/^(?:[0-9a-f]{2})*$/).describe("bytes as lowercase hex, two digits per byte, no separators");
|
|
77
|
+
|
|
78
|
+
/** Input bytes: hex in either case, two digits per byte, at least one byte. Parses to lowercase. */
|
|
79
|
+
export const HexDataInput = z
|
|
80
|
+
.string()
|
|
81
|
+
.regex(/^(?:[0-9a-fA-F]{2})+$/, "must be hex bytes, two digits per byte, no separators, for example a9008d20d0")
|
|
82
|
+
.transform((value) => value.toLowerCase());
|
|
83
|
+
|
|
84
|
+
export const SpaceInput = z.enum(SPACES).default("c64").describe("c64 is the computer. drive8 is the 1541 disk drive CPU");
|
|
85
|
+
|
|
86
|
+
export const MemoryViewInput = z
|
|
87
|
+
.enum(MEMORY_VIEWS)
|
|
88
|
+
.default("cpu")
|
|
89
|
+
.describe("cpu reads what the CPU sees now (ROM and I/O where banked in). ram reads the RAM underneath (c64 only)");
|
|
90
|
+
|
|
91
|
+
/** A name the agent gives to something the session keeps, such as a screen baseline or a snapshot. */
|
|
92
|
+
export const TransientName = z
|
|
93
|
+
.string()
|
|
94
|
+
.regex(/^[A-Za-z0-9._-]{1,64}$/, "must be 1 to 64 letters, digits, dots, underscores or hyphens")
|
|
95
|
+
.describe("a name you choose: 1 to 64 letters, digits, dots, underscores or hyphens");
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Checks the fields of one action of a multi-action tool: a field that the
|
|
99
|
+
* action does not use, or a missing required field, is an invalid-input error.
|
|
100
|
+
*/
|
|
101
|
+
export function requireFields(action: string, given: Record<string, unknown>, allowed: readonly string[], required: readonly string[] = []): void {
|
|
102
|
+
for (const [field, value] of Object.entries(given)) {
|
|
103
|
+
if (value !== undefined && !allowed.includes(field)) throw new WireFailure("invalid-input", `${field} is not used with action ${action}.`);
|
|
104
|
+
}
|
|
105
|
+
for (const field of required) {
|
|
106
|
+
if (given[field] === undefined) throw new WireFailure("invalid-input", `Action ${action} needs ${field}.`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** A tool result that carries content blocks (for example an image) beside its structured result. */
|
|
111
|
+
export class ToolOutput<T> {
|
|
112
|
+
readonly structured: T;
|
|
113
|
+
readonly content: CallToolResult["content"];
|
|
114
|
+
|
|
115
|
+
constructor(structured: T, content: CallToolResult["content"]) {
|
|
116
|
+
this.structured = structured;
|
|
117
|
+
this.content = content;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export interface ToolDefinition<I extends z.ZodObject = z.ZodObject, O extends z.ZodObject = z.ZodObject> {
|
|
122
|
+
name: string;
|
|
123
|
+
title: string;
|
|
124
|
+
description: string;
|
|
125
|
+
inputSchema: I;
|
|
126
|
+
outputSchema: O;
|
|
127
|
+
/** True for operations that only observe the machine. */
|
|
128
|
+
readOnly: boolean;
|
|
129
|
+
run(input: z.output<I>, session: ViceSessionApi): Promise<z.input<O> | ToolOutput<z.input<O>>>;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** A typed condition: register, memory or raster. No expression language. */
|
|
133
|
+
export const ConditionInput = z
|
|
134
|
+
.discriminatedUnion("kind", [
|
|
135
|
+
z
|
|
136
|
+
.object({
|
|
137
|
+
kind: z.literal("register"),
|
|
138
|
+
register: z.enum(CONDITION_REGISTERS),
|
|
139
|
+
operator: z.enum(COMPARISONS),
|
|
140
|
+
value: Byte,
|
|
141
|
+
})
|
|
142
|
+
.strict(),
|
|
143
|
+
z
|
|
144
|
+
.object({
|
|
145
|
+
kind: z.literal("memory"),
|
|
146
|
+
address: AddressInput,
|
|
147
|
+
operator: z.enum(COMPARISONS),
|
|
148
|
+
value: Byte,
|
|
149
|
+
space: SpaceInput,
|
|
150
|
+
view: MemoryViewInput,
|
|
151
|
+
})
|
|
152
|
+
.strict(),
|
|
153
|
+
z
|
|
154
|
+
.object({
|
|
155
|
+
kind: z.literal("raster"),
|
|
156
|
+
line: z.number().int().min(0).describe("raster line: PAL 0-311, NTSC 0-262"),
|
|
157
|
+
cycle: z.number().int().min(0).optional().describe("cycle in the line: PAL 0-62, NTSC 0-64. True from this cycle on"),
|
|
158
|
+
})
|
|
159
|
+
.strict(),
|
|
160
|
+
])
|
|
161
|
+
.describe("register: a CPU register compared to a byte. memory: a byte in memory compared to a byte. raster: the raster position");
|
|
162
|
+
|
|
163
|
+
/** A condition as a result shows it: the input form, with the memory address as "$xxxx". */
|
|
164
|
+
export const ConditionOutput = z
|
|
165
|
+
.discriminatedUnion("kind", [
|
|
166
|
+
z.object({ kind: z.literal("register"), register: z.enum(CONDITION_REGISTERS), operator: z.enum(COMPARISONS), value: Byte }),
|
|
167
|
+
z.object({ kind: z.literal("memory"), address: AddressOutput, operator: z.enum(COMPARISONS), value: Byte, space: z.enum(SPACES), view: z.enum(MEMORY_VIEWS) }),
|
|
168
|
+
z.object({ kind: z.literal("raster"), line: z.number().int().min(0), cycle: z.number().int().min(0).optional() }),
|
|
169
|
+
])
|
|
170
|
+
.describe("the condition given at add");
|
|
171
|
+
|
|
172
|
+
/** A protocol condition in its result form. */
|
|
173
|
+
export function showCondition(condition: Condition): z.input<typeof ConditionOutput> {
|
|
174
|
+
return condition.kind === "memory" ? { ...condition, address: formatC64Address(condition.address) } : condition;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** Removes keys whose value is undefined, so optional fields stay absent. */
|
|
178
|
+
export function defined<T extends Record<string, unknown>>(value: T): T {
|
|
179
|
+
return Object.fromEntries(Object.entries(value).filter(([, entry]) => entry !== undefined)) as T;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** A parsed condition input as the protocol type. */
|
|
183
|
+
export function toCondition(input: z.output<typeof ConditionInput>): Condition {
|
|
184
|
+
return defined(input) as Condition;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** Keeps a tool's handler typed against its own schemas. */
|
|
188
|
+
export function defineTool<I extends z.ZodObject, O extends z.ZodObject>(tool: ToolDefinition<I, O>): ToolDefinition {
|
|
189
|
+
return tool as unknown as ToolDefinition;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function jsonSchema(schema: z.ZodObject, io: "input" | "output"): McpTool["inputSchema"] {
|
|
193
|
+
const { $schema: _ignored, ...rest } = z.toJSONSchema(schema, { io }) as JsonObject;
|
|
194
|
+
return rest as McpTool["inputSchema"];
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export function toolListing(tool: ToolDefinition): McpTool {
|
|
198
|
+
return {
|
|
199
|
+
name: tool.name,
|
|
200
|
+
title: tool.title,
|
|
201
|
+
description: tool.description,
|
|
202
|
+
inputSchema: jsonSchema(tool.inputSchema, "input"),
|
|
203
|
+
outputSchema: jsonSchema(tool.outputSchema, "output"),
|
|
204
|
+
annotations: { title: tool.title, readOnlyHint: tool.readOnly, openWorldHint: false },
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function errorResult(error: WireError): CallToolResult {
|
|
209
|
+
return { isError: true, content: [{ type: "text", text: JSON.stringify({ code: error.code, message: error.message }) }] };
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/** "address: Invalid string: must match ..." style messages, one sentence per problem. */
|
|
213
|
+
function describeIssues(error: z.ZodError): string {
|
|
214
|
+
return error.issues
|
|
215
|
+
.map((issue) => {
|
|
216
|
+
const where = issue.path.length === 0 ? "input" : issue.path.join(".");
|
|
217
|
+
return `${where}: ${issue.message}`;
|
|
218
|
+
})
|
|
219
|
+
.join(". ");
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const INTERNAL_FAILURE: WireError = { code: "operation-failed", message: "The operation failed inside the c64-re-tools MCP server." };
|
|
223
|
+
|
|
224
|
+
/** Runs one tool call and translates every outcome into an MCP result. The trace gets the call, its time and its outcome. */
|
|
225
|
+
export async function callTool(
|
|
226
|
+
tools: ReadonlyMap<string, ToolDefinition>,
|
|
227
|
+
name: string,
|
|
228
|
+
args: unknown,
|
|
229
|
+
session: SessionSource,
|
|
230
|
+
log: (line: string) => void = () => {},
|
|
231
|
+
): Promise<CallToolResult> {
|
|
232
|
+
const started = performance.now();
|
|
233
|
+
const result = await runCall(tools, name, args, session, log);
|
|
234
|
+
trace().event("tool.call", { name, args, ms: elapsedMs(started), ...outcome(result) });
|
|
235
|
+
return result;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/** What the trace records of a result: the structured result, or the code and message of the error. */
|
|
239
|
+
function outcome(result: CallToolResult): TraceFields {
|
|
240
|
+
if (result.isError !== true) return { ok: true, result: result.structuredContent };
|
|
241
|
+
const first = result.content[0];
|
|
242
|
+
const text = first?.type === "text" ? first.text : "";
|
|
243
|
+
try {
|
|
244
|
+
return { ok: false, ...(JSON.parse(text) as WireError) };
|
|
245
|
+
} catch {
|
|
246
|
+
return { ok: false, message: text };
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
async function runCall(
|
|
251
|
+
tools: ReadonlyMap<string, ToolDefinition>,
|
|
252
|
+
name: string,
|
|
253
|
+
args: unknown,
|
|
254
|
+
session: SessionSource,
|
|
255
|
+
log: (line: string) => void,
|
|
256
|
+
): Promise<CallToolResult> {
|
|
257
|
+
const tool = tools.get(name);
|
|
258
|
+
if (tool === undefined) return errorResult({ code: "invalid-input", message: `There is no tool named ${name}.` });
|
|
259
|
+
const parsed = tool.inputSchema.safeParse(args ?? {});
|
|
260
|
+
if (!parsed.success) return errorResult({ code: "invalid-input", message: describeIssues(parsed.error) });
|
|
261
|
+
try {
|
|
262
|
+
const output = await tool.run(parsed.data, await session());
|
|
263
|
+
// The result goes out in the shape that the tool listing advertises: keys
|
|
264
|
+
// that the output schema does not name are removed, and a result that does
|
|
265
|
+
// not fit the schema is an internal failure.
|
|
266
|
+
const checked = tool.outputSchema.safeParse(output instanceof ToolOutput ? output.structured : output);
|
|
267
|
+
if (!checked.success) {
|
|
268
|
+
log(`${name} gave a result outside its output schema: ${describeIssues(checked.error)}`);
|
|
269
|
+
return errorResult(INTERNAL_FAILURE);
|
|
270
|
+
}
|
|
271
|
+
const result = checked.data as JsonObject;
|
|
272
|
+
const extra = output instanceof ToolOutput ? output.content : [];
|
|
273
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }, ...extra], structuredContent: result };
|
|
274
|
+
} catch (error) {
|
|
275
|
+
if (error instanceof WireFailure) return errorResult(error.toWire());
|
|
276
|
+
log(`${name} failed: ${error instanceof Error ? (error.stack ?? error.message) : String(error)}`);
|
|
277
|
+
return errorResult(INTERNAL_FAILURE);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/** The version of this package. A package.json without a version is a broken installation. */
|
|
282
|
+
function packageVersion(): string {
|
|
283
|
+
const { version } = createRequire(import.meta.url)("../../package.json") as { version?: unknown };
|
|
284
|
+
if (typeof version !== "string" || version === "") {
|
|
285
|
+
throw new Error("The package.json of c64-re-tools has no version. Install c64-re-tools again.");
|
|
286
|
+
}
|
|
287
|
+
return version;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const INSTRUCTIONS =
|
|
291
|
+
"These tools control one live Commodore 64 emulator that belongs to this session. " +
|
|
292
|
+
"Addresses are four hex digits after a dollar sign, for example $c000. " +
|
|
293
|
+
"Memory data is lowercase hex without separators. " +
|
|
294
|
+
"A read does not change the run state of the machine.";
|
|
295
|
+
|
|
296
|
+
export function createMcpServer(options: { tools: readonly ToolDefinition[]; session: SessionSource; log?: (line: string) => void }): Server {
|
|
297
|
+
const tools = new Map(options.tools.map((tool) => [tool.name, tool]));
|
|
298
|
+
const server = new Server(
|
|
299
|
+
{ name: "c64-re-tools", version: packageVersion() },
|
|
300
|
+
{ capabilities: { tools: {} }, instructions: INSTRUCTIONS },
|
|
301
|
+
);
|
|
302
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: options.tools.map(toolListing) }));
|
|
303
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) =>
|
|
304
|
+
callTool(tools, request.params.name, request.params.arguments, options.session, options.log),
|
|
305
|
+
);
|
|
306
|
+
return server;
|
|
307
|
+
}
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
// Debug tools: c64_breakpoint, c64_watchpoint, c64_cpu_history, c64_backtrace,
|
|
2
|
+
// c64_profile, c64_memmap and c64_timing.
|
|
3
|
+
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
|
|
6
|
+
import { formatC64Address } from "../../c64.ts";
|
|
7
|
+
import { CHECKPOINT_ACTIONS, MAX_BACKTRACE, MAX_HISTORY, MAX_MEMMAP_RANGES, MAX_PROFILE, MAX_WATCH_SIZE, SPACES, TIMING_ACTIONS, WATCH_ACCESS, type Breakpoint, type Watchpoint } from "../../protocol/vice.ts";
|
|
8
|
+
import { AddressInput, AddressOutput, Byte, ConditionInput, ConditionOutput, defineTool, HexData, requireFields, showCondition, SpaceInput, toCondition } from "../server.ts";
|
|
9
|
+
|
|
10
|
+
/** No default here: a filled-in default would count as a field the other actions refuse. */
|
|
11
|
+
const OptionalSpace = z.enum(SPACES).optional().describe("add only: c64 (default) or drive8, the 1541 disk drive CPU");
|
|
12
|
+
|
|
13
|
+
const Id = z.number().int().min(1).describe("the id that action add returned");
|
|
14
|
+
|
|
15
|
+
const BreakpointOutput = z.object({
|
|
16
|
+
id: z.number().int().min(1),
|
|
17
|
+
address: AddressOutput,
|
|
18
|
+
space: z.enum(SPACES),
|
|
19
|
+
enabled: z.boolean(),
|
|
20
|
+
condition: ConditionOutput.optional(),
|
|
21
|
+
});
|
|
22
|
+
const WatchpointOutput = BreakpointOutput.extend({ size: z.number().int().min(1).max(MAX_WATCH_SIZE), access: z.enum(WATCH_ACCESS) });
|
|
23
|
+
|
|
24
|
+
function showBreakpoint(point: Breakpoint) {
|
|
25
|
+
const { condition, ...rest } = point;
|
|
26
|
+
return { ...rest, address: formatC64Address(point.address), ...(condition === undefined ? {} : { condition: showCondition(condition) }) };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function showWatchpoint(point: Watchpoint) {
|
|
30
|
+
const { condition, ...rest } = point;
|
|
31
|
+
return { ...rest, address: formatC64Address(point.address), ...(condition === undefined ? {} : { condition: showCondition(condition) }) };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export const c64Breakpoint = defineTool({
|
|
35
|
+
name: "c64_breakpoint",
|
|
36
|
+
title: "C64 breakpoints",
|
|
37
|
+
description:
|
|
38
|
+
"Manage breakpoints. A breakpoint stops the CPU when it is about to execute the instruction at an address. " +
|
|
39
|
+
"Action add sets one at address in space c64 or drive8. With a condition, it stops only when the condition is true. " +
|
|
40
|
+
"add returns the new id. Actions remove, enable and disable take that id. Action list gives all breakpoints. Each breakpoint shows its condition, when it has one. " +
|
|
41
|
+
"Breakpoints stay through a reset.",
|
|
42
|
+
inputSchema: z
|
|
43
|
+
.object({
|
|
44
|
+
action: z.enum(CHECKPOINT_ACTIONS),
|
|
45
|
+
address: AddressInput.optional().describe("add only"),
|
|
46
|
+
space: OptionalSpace,
|
|
47
|
+
condition: ConditionInput.optional().describe("add only: stop only when this is true"),
|
|
48
|
+
id: Id.optional().describe("remove, enable and disable only"),
|
|
49
|
+
})
|
|
50
|
+
.strict(),
|
|
51
|
+
outputSchema: BreakpointOutput.partial().extend({ breakpoints: z.array(BreakpointOutput).optional() }),
|
|
52
|
+
readOnly: false,
|
|
53
|
+
async run(input, session) {
|
|
54
|
+
const { action, ...fields } = input;
|
|
55
|
+
if (action === "list") {
|
|
56
|
+
requireFields(action, fields, []);
|
|
57
|
+
const result = await session.breakpoint({ action });
|
|
58
|
+
return { breakpoints: result.breakpoints.map(showBreakpoint) };
|
|
59
|
+
}
|
|
60
|
+
if (action !== "add") {
|
|
61
|
+
requireFields(action, fields, ["id"], ["id"]);
|
|
62
|
+
return showBreakpoint(await session.breakpoint({ action, id: fields.id! }));
|
|
63
|
+
}
|
|
64
|
+
requireFields(action, fields, ["address", "space", "condition"], ["address"]);
|
|
65
|
+
const added = await session.breakpoint({
|
|
66
|
+
action,
|
|
67
|
+
address: fields.address!,
|
|
68
|
+
space: fields.space ?? "c64",
|
|
69
|
+
...(fields.condition === undefined ? {} : { condition: toCondition(fields.condition) }),
|
|
70
|
+
});
|
|
71
|
+
return showBreakpoint(added);
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
export const c64Watchpoint = defineTool({
|
|
76
|
+
name: "c64_watchpoint",
|
|
77
|
+
title: "C64 watchpoints",
|
|
78
|
+
description:
|
|
79
|
+
"Manage watchpoints. A watchpoint stops the CPU when an instruction reads or writes memory in a range. " +
|
|
80
|
+
`Action add sets one at address for size bytes (1 to ${MAX_WATCH_SIZE}, default 1), with access read, write or read-write. ` +
|
|
81
|
+
"With a condition, it stops only when the condition is true. For a write, a memory condition sees the memory after the write. " +
|
|
82
|
+
"add returns the new id. Actions remove, enable and disable take that id. Action list gives all watchpoints. Each watchpoint shows its condition, when it has one.",
|
|
83
|
+
inputSchema: z
|
|
84
|
+
.object({
|
|
85
|
+
action: z.enum(CHECKPOINT_ACTIONS),
|
|
86
|
+
address: AddressInput.optional().describe("add only"),
|
|
87
|
+
size: z.number().int().min(1).max(MAX_WATCH_SIZE).optional().describe("add only. Default 1"),
|
|
88
|
+
access: z.enum(WATCH_ACCESS).optional().describe("add only"),
|
|
89
|
+
space: OptionalSpace,
|
|
90
|
+
condition: ConditionInput.optional().describe("add only: stop only when this is true"),
|
|
91
|
+
id: Id.optional().describe("remove, enable and disable only"),
|
|
92
|
+
})
|
|
93
|
+
.strict(),
|
|
94
|
+
outputSchema: WatchpointOutput.partial().extend({ watchpoints: z.array(WatchpointOutput).optional() }),
|
|
95
|
+
readOnly: false,
|
|
96
|
+
async run(input, session) {
|
|
97
|
+
const { action, ...fields } = input;
|
|
98
|
+
if (action === "list") {
|
|
99
|
+
requireFields(action, fields, []);
|
|
100
|
+
const result = await session.watchpoint({ action });
|
|
101
|
+
return { watchpoints: result.watchpoints.map(showWatchpoint) };
|
|
102
|
+
}
|
|
103
|
+
if (action !== "add") {
|
|
104
|
+
requireFields(action, fields, ["id"], ["id"]);
|
|
105
|
+
return showWatchpoint(await session.watchpoint({ action, id: fields.id! }));
|
|
106
|
+
}
|
|
107
|
+
requireFields(action, fields, ["address", "size", "access", "space", "condition"], ["address", "access"]);
|
|
108
|
+
const added = await session.watchpoint({
|
|
109
|
+
action,
|
|
110
|
+
address: fields.address!,
|
|
111
|
+
size: fields.size ?? 1,
|
|
112
|
+
access: fields.access!,
|
|
113
|
+
space: fields.space ?? "c64",
|
|
114
|
+
...(fields.condition === undefined ? {} : { condition: toCondition(fields.condition) }),
|
|
115
|
+
});
|
|
116
|
+
return showWatchpoint(added);
|
|
117
|
+
},
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
export const c64CpuHistory = defineTool({
|
|
121
|
+
name: "c64_cpu_history",
|
|
122
|
+
title: "CPU history",
|
|
123
|
+
description:
|
|
124
|
+
"Get the last instructions that the CPU executed, oldest first. " +
|
|
125
|
+
`limit is the number of instructions (1 to ${MAX_HISTORY}, default 50). ` +
|
|
126
|
+
"Each entry gives the address, the bytes, the instruction and the registers a, x, y and sp. " +
|
|
127
|
+
"In space c64, it also gives the raster line and cycle when the instruction started. This does not change the run state of the machine.",
|
|
128
|
+
inputSchema: z.object({ limit: z.number().int().min(1).max(MAX_HISTORY).default(50), space: SpaceInput }).strict(),
|
|
129
|
+
outputSchema: z.object({
|
|
130
|
+
entries: z.array(
|
|
131
|
+
z.object({
|
|
132
|
+
address: AddressOutput,
|
|
133
|
+
bytes: HexData,
|
|
134
|
+
text: z.string(),
|
|
135
|
+
a: Byte,
|
|
136
|
+
x: Byte,
|
|
137
|
+
y: Byte,
|
|
138
|
+
sp: Byte,
|
|
139
|
+
rasterLine: z.number().int().min(0).optional(),
|
|
140
|
+
rasterCycle: z.number().int().min(0).optional(),
|
|
141
|
+
}),
|
|
142
|
+
),
|
|
143
|
+
}),
|
|
144
|
+
readOnly: true,
|
|
145
|
+
async run(input, session) {
|
|
146
|
+
const { entries } = await session.cpuHistory(input);
|
|
147
|
+
return { entries: entries.map((entry) => ({ ...entry, address: formatC64Address(entry.address) })) };
|
|
148
|
+
},
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
export const c64Backtrace = defineTool({
|
|
152
|
+
name: "c64_backtrace",
|
|
153
|
+
title: "Call backtrace",
|
|
154
|
+
description:
|
|
155
|
+
`Get the chain of subroutine calls that leads to the current instruction, most recent first, at most depth calls (1 to ${MAX_BACKTRACE}, default 16). ` +
|
|
156
|
+
"Each frame gives the entry address of the routine and the address it returns to. " +
|
|
157
|
+
"The chain comes from the JSR return addresses on the stack, so it is a best estimate. Interrupts are not in the chain.",
|
|
158
|
+
inputSchema: z.object({ depth: z.number().int().min(1).max(MAX_BACKTRACE).default(16), space: SpaceInput }).strict(),
|
|
159
|
+
outputSchema: z.object({ frames: z.array(z.object({ address: AddressOutput, returnAddress: AddressOutput.optional() })) }),
|
|
160
|
+
readOnly: true,
|
|
161
|
+
async run(input, session) {
|
|
162
|
+
const { frames } = await session.backtrace(input);
|
|
163
|
+
return {
|
|
164
|
+
frames: frames.map((frame) => ({
|
|
165
|
+
address: formatC64Address(frame.address),
|
|
166
|
+
...(frame.returnAddress === undefined ? {} : { returnAddress: formatC64Address(frame.returnAddress) }),
|
|
167
|
+
})),
|
|
168
|
+
};
|
|
169
|
+
},
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
export const c64Timing = defineTool({
|
|
173
|
+
name: "c64_timing",
|
|
174
|
+
title: "Cycle stopwatch",
|
|
175
|
+
description:
|
|
176
|
+
"A stopwatch that counts C64 CPU cycles. Action start sets it to zero. Action read gives the cycles since the last start, " +
|
|
177
|
+
"or since the session started, as a decimal string. A PAL frame has 19656 cycles and an NTSC frame 17095.",
|
|
178
|
+
inputSchema: z.object({ action: z.enum(TIMING_ACTIONS) }).strict(),
|
|
179
|
+
outputSchema: z.object({ started: z.boolean().optional(), cycles: z.string().regex(/^\d+$/).optional() }),
|
|
180
|
+
readOnly: false,
|
|
181
|
+
async run(input, session) {
|
|
182
|
+
return session.timing(input.action);
|
|
183
|
+
},
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
export const c64Profile = defineTool({
|
|
187
|
+
name: "c64_profile",
|
|
188
|
+
title: "Cycle profile",
|
|
189
|
+
description:
|
|
190
|
+
"Get the routines where the C64 CPU spent the most cycles since the session started. " +
|
|
191
|
+
`limit is the maximum number of routines (1 to ${MAX_PROFILE}, default 20). ` +
|
|
192
|
+
"Each entry gives the routine address, its total cycles with the routines it calls, and its own cycles. " +
|
|
193
|
+
"percent is the part of all cycles that the routine used itself. " +
|
|
194
|
+
"Cycle counts are decimal strings.",
|
|
195
|
+
inputSchema: z.object({ limit: z.number().int().min(1).max(MAX_PROFILE).default(20) }).strict(),
|
|
196
|
+
outputSchema: z.object({
|
|
197
|
+
entries: z.array(z.object({ address: AddressOutput, totalCycles: z.string(), selfCycles: z.string(), percent: z.number() })),
|
|
198
|
+
}),
|
|
199
|
+
readOnly: true,
|
|
200
|
+
async run(input, session) {
|
|
201
|
+
const { entries } = await session.profile(input.limit);
|
|
202
|
+
return { entries: entries.map((entry) => ({ ...entry, address: formatC64Address(entry.address) })) };
|
|
203
|
+
},
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
export const c64Memmap = defineTool({
|
|
207
|
+
name: "c64_memmap",
|
|
208
|
+
title: "Memory access map",
|
|
209
|
+
description:
|
|
210
|
+
"Get how the C64 CPU used memory since the session started or since the last clear. " +
|
|
211
|
+
"Action read gives ranges of addresses with the same access (execute, read, write) from start to end. " +
|
|
212
|
+
`It gives at most maxRanges ranges (1 to ${MAX_MEMMAP_RANGES}, default 256). Addresses the CPU did not touch are not in the result. ` +
|
|
213
|
+
"Action clear sets all access records to zero.",
|
|
214
|
+
inputSchema: z
|
|
215
|
+
.object({
|
|
216
|
+
action: z.enum(["read", "clear"]),
|
|
217
|
+
start: AddressInput.optional().describe("read only. Default $0000"),
|
|
218
|
+
end: AddressInput.optional().describe("read only. Default $ffff"),
|
|
219
|
+
maxRanges: z.number().int().min(1).max(MAX_MEMMAP_RANGES).optional().describe("read only. Default 256"),
|
|
220
|
+
})
|
|
221
|
+
.strict(),
|
|
222
|
+
outputSchema: z.object({
|
|
223
|
+
ranges: z.array(z.object({ start: AddressOutput, end: AddressOutput, execute: z.boolean(), read: z.boolean(), write: z.boolean() })).optional(),
|
|
224
|
+
cleared: z.boolean().optional(),
|
|
225
|
+
}),
|
|
226
|
+
readOnly: false,
|
|
227
|
+
async run(input, session) {
|
|
228
|
+
if (input.action === "clear") {
|
|
229
|
+
requireFields("clear", { start: input.start, end: input.end, maxRanges: input.maxRanges }, []);
|
|
230
|
+
return session.memmap({ action: "clear" });
|
|
231
|
+
}
|
|
232
|
+
const result = await session.memmap({ action: "read", start: input.start ?? 0x0000, end: input.end ?? 0xffff, maxRanges: input.maxRanges ?? 256 });
|
|
233
|
+
return { ranges: result.ranges.map((range) => ({ ...range, start: formatC64Address(range.start), end: formatC64Address(range.end) })) };
|
|
234
|
+
},
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
export const debugTools = [c64Breakpoint, c64Watchpoint, c64CpuHistory, c64Backtrace, c64Profile, c64Memmap, c64Timing];
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
// Execution tools: c64_execution and c64_run_until.
|
|
2
|
+
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
|
|
5
|
+
import { formatC64Address } from "../../c64.ts";
|
|
6
|
+
import { COMPARISONS, EXECUTION_ACTIONS, MAX_EXECUTION_COUNT, MAX_TIMEOUT_FRAMES, RUN_STATES, STOP_REASONS, type RunTarget } from "../../protocol/vice.ts";
|
|
7
|
+
import { AddressInput, AddressOutput, Byte, ConditionInput, defineTool, MemoryViewInput, SpaceInput, toCondition } from "../server.ts";
|
|
8
|
+
|
|
9
|
+
export const c64Execution = defineTool({
|
|
10
|
+
name: "c64_execution",
|
|
11
|
+
title: "Control C64 execution",
|
|
12
|
+
description:
|
|
13
|
+
"Control the CPU. pause stops the CPU and gives the program counter (pc). resume lets the CPU run. " +
|
|
14
|
+
"step executes count instructions (default 1) and goes into subroutines. " +
|
|
15
|
+
"next executes count instructions (default 1) and treats a JSR and its subroutine as one instruction. " +
|
|
16
|
+
"until-return runs until the current subroutine returns (after the next RTS or RTI). " +
|
|
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. " +
|
|
19
|
+
"step, next, until-return and advance-frames first stop a running CPU. Use count only with step, next and advance-frames. " +
|
|
20
|
+
"step, next and until-return work only in space c64. For the drive, use c64_run_until with an address in space drive8.",
|
|
21
|
+
inputSchema: z
|
|
22
|
+
.object({
|
|
23
|
+
action: z.enum(EXECUTION_ACTIONS),
|
|
24
|
+
count: z
|
|
25
|
+
.number()
|
|
26
|
+
.int()
|
|
27
|
+
.min(1)
|
|
28
|
+
.max(MAX_EXECUTION_COUNT)
|
|
29
|
+
.optional()
|
|
30
|
+
.describe(`step and next: instructions (default 1). advance-frames: frames (necessary). 1 to ${MAX_EXECUTION_COUNT}`),
|
|
31
|
+
space: SpaceInput,
|
|
32
|
+
})
|
|
33
|
+
.strict(),
|
|
34
|
+
outputSchema: z.object({
|
|
35
|
+
state: z.enum(RUN_STATES),
|
|
36
|
+
pc: AddressOutput.optional(),
|
|
37
|
+
executed: z.number().int().min(0).max(MAX_EXECUTION_COUNT).optional().describe("instructions executed by step or next"),
|
|
38
|
+
advancedFrames: z.number().int().min(0).max(MAX_EXECUTION_COUNT).optional().describe("frames advance-frames ran"),
|
|
39
|
+
}),
|
|
40
|
+
readOnly: false,
|
|
41
|
+
async run(input, session) {
|
|
42
|
+
const params = { action: input.action, space: input.space, ...(input.count === undefined ? {} : { count: input.count }) };
|
|
43
|
+
const result = await session.execution(params);
|
|
44
|
+
return {
|
|
45
|
+
state: result.state,
|
|
46
|
+
...(result.pc === undefined ? {} : { pc: formatC64Address(result.pc) }),
|
|
47
|
+
...(result.executed === undefined ? {} : { executed: result.executed }),
|
|
48
|
+
...(result.advancedFrames === undefined ? {} : { advancedFrames: result.advancedFrames }),
|
|
49
|
+
};
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const RunTargetInput = z.discriminatedUnion("kind", [
|
|
54
|
+
z
|
|
55
|
+
.object({
|
|
56
|
+
kind: z.literal("address"),
|
|
57
|
+
address: AddressInput,
|
|
58
|
+
space: SpaceInput,
|
|
59
|
+
condition: ConditionInput.optional().describe("stop at the address only when this is true"),
|
|
60
|
+
})
|
|
61
|
+
.strict(),
|
|
62
|
+
z
|
|
63
|
+
.object({
|
|
64
|
+
kind: z.literal("memory"),
|
|
65
|
+
address: AddressInput,
|
|
66
|
+
operator: z.enum(COMPARISONS),
|
|
67
|
+
value: Byte,
|
|
68
|
+
space: SpaceInput,
|
|
69
|
+
view: MemoryViewInput,
|
|
70
|
+
})
|
|
71
|
+
.strict(),
|
|
72
|
+
z
|
|
73
|
+
.object({
|
|
74
|
+
kind: z.literal("raster"),
|
|
75
|
+
line: z.number().int().min(0).describe("raster line: PAL 0-311, NTSC 0-262"),
|
|
76
|
+
cycle: z.number().int().min(0).optional().describe("cycle in the line: PAL 0-62, NTSC 0-64. Default 0"),
|
|
77
|
+
})
|
|
78
|
+
.strict(),
|
|
79
|
+
]);
|
|
80
|
+
|
|
81
|
+
export const c64RunUntil = defineTool({
|
|
82
|
+
name: "c64_run_until",
|
|
83
|
+
title: "Run the C64 until a target",
|
|
84
|
+
description:
|
|
85
|
+
"Run the CPU until a target occurs, then stop. " +
|
|
86
|
+
"Target kind address stops before the instruction at an address executes. An optional condition must also be true. " +
|
|
87
|
+
"Kind memory compares the byte at address to value with operator. It stops after a write that makes this comparison true. " +
|
|
88
|
+
"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
|
+
`timeoutFrames (1 to ${MAX_TIMEOUT_FRAMES}, default 3000) limits the run in video frames. ` +
|
|
90
|
+
"reached tells if the target occurred. stopReason tells why the CPU stopped: target, breakpoint, watchpoint, jam (a JAM opcode) or timeout. " +
|
|
91
|
+
"The CPU is always stopped after this tool.",
|
|
92
|
+
inputSchema: z
|
|
93
|
+
.object({
|
|
94
|
+
target: RunTargetInput,
|
|
95
|
+
timeoutFrames: z.number().int().min(1).max(MAX_TIMEOUT_FRAMES).default(3000),
|
|
96
|
+
})
|
|
97
|
+
.strict(),
|
|
98
|
+
outputSchema: z.object({ reached: z.boolean(), stopReason: z.enum(STOP_REASONS), state: z.enum(RUN_STATES), pc: AddressOutput }),
|
|
99
|
+
readOnly: false,
|
|
100
|
+
async run(input, session) {
|
|
101
|
+
const { target: given } = input;
|
|
102
|
+
let target: RunTarget;
|
|
103
|
+
if (given.kind === "address") {
|
|
104
|
+
target = { kind: "address", address: given.address, space: given.space };
|
|
105
|
+
if (given.condition !== undefined) target.condition = toCondition(given.condition);
|
|
106
|
+
} else if (given.kind === "memory") {
|
|
107
|
+
target = given;
|
|
108
|
+
} else {
|
|
109
|
+
target = given.cycle === undefined ? { kind: "raster", line: given.line } : { kind: "raster", line: given.line, cycle: given.cycle };
|
|
110
|
+
}
|
|
111
|
+
const result = await session.runUntil({ target, timeoutFrames: input.timeoutFrames });
|
|
112
|
+
return { ...result, pc: formatC64Address(result.pc) };
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
export const executionTools = [c64Execution, c64RunUntil];
|