c64bridge 0.9.0 → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +158 -164
- package/data/vice/vice-binary-monitor-spec.md +21 -4
- package/dist/.tsbuildinfo +1 -1
- package/dist/c64Client.js +76 -2
- package/dist/device.js +43 -3
- package/dist/rag/indexer.js +18 -8
- package/dist/sidwaveCompiler.js +1 -1
- package/dist/{assemblyConverter.js → tools/assember.js} +10 -1
- package/dist/{basicConverter.js → tools/basicTokenizer.js} +1 -1
- package/dist/tools/debug.js +158 -5
- package/dist/tools/disassembler.js +478 -0
- package/dist/tools/input.js +338 -0
- package/dist/tools/memory.js +317 -1
- package/dist/tools/programRunners.js +45 -6
- package/dist/tools/registry/batch.js +167 -0
- package/dist/tools/registry/graphics.js +101 -1
- package/dist/tools/registry/index.js +4 -0
- package/dist/tools/registry/input.js +2 -0
- package/dist/tools/registry/memory.js +61 -1
- package/dist/tools/schema.js +4 -5
- package/dist/tools/symbolRegistry.js +49 -0
- package/dist/tools/types.js +19 -4
- package/dist/vice/process.js +43 -2
- package/dist/vice/viceClient.js +5 -1
- package/mcp/schemas/c64_batch.schema.json +53 -0
- package/mcp/schemas/c64_config.schema.json +461 -2
- package/mcp/schemas/c64_debug.schema.json +641 -3
- package/mcp/schemas/c64_disk.schema.json +340 -2
- package/mcp/schemas/c64_drive.schema.json +157 -2
- package/mcp/schemas/c64_extract.schema.json +359 -2
- package/mcp/schemas/c64_graphics.schema.json +415 -3
- package/mcp/schemas/c64_input.schema.json +136 -0
- package/mcp/schemas/c64_memory.schema.json +562 -3
- package/mcp/schemas/c64_printer.schema.json +253 -2
- package/mcp/schemas/c64_program.schema.json +499 -2
- package/mcp/schemas/c64_rag.schema.json +82 -2
- package/mcp/schemas/c64_select_backend.schema.json +35 -2
- package/mcp/schemas/c64_sound.schema.json +793 -2
- package/mcp/schemas/c64_stream.schema.json +73 -2
- package/mcp/schemas/c64_system.schema.json +301 -2
- package/mcp/schemas/c64_vice.schema.json +71 -2
- package/mcp/server.json +1 -1
- package/mcp/tools.json +6718 -1257
- package/mcp.json +1 -1
- package/package.json +1 -1
- package/scripts/run-coverage.mjs +7 -0
- package/scripts/run-tests.ts +2 -0
- package/scripts/update-readme.ts +79 -2
- package/server.json +2 -2
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
import { createOperationDispatcher, defineToolModule, } from "./types.js";
|
|
2
|
+
import { arraySchema, literalSchema, numberSchema, objectSchema, optionalSchema, stringSchema, } from "./schema.js";
|
|
3
|
+
import { textResult } from "./responses.js";
|
|
4
|
+
import { ToolValidationError, toolErrorResult, unknownErrorResult, } from "./errors.js";
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
// PETSCII token expansion table
|
|
7
|
+
// Maps {TOKEN} placeholders to PETSCII/control-byte values that are queued via
|
|
8
|
+
// KERNAL keyboard-buffer injection on both C64U and VICE.
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
const PETSCII_TOKENS = {
|
|
11
|
+
RETURN: 13,
|
|
12
|
+
CR: 13,
|
|
13
|
+
CLR: 147,
|
|
14
|
+
HOME: 19,
|
|
15
|
+
DEL: 20,
|
|
16
|
+
INS: 148,
|
|
17
|
+
INSERT: 148,
|
|
18
|
+
STOP: 3,
|
|
19
|
+
RUN: 131,
|
|
20
|
+
SHIFT_RETURN: 141,
|
|
21
|
+
F1: 133,
|
|
22
|
+
F2: 137,
|
|
23
|
+
F3: 134,
|
|
24
|
+
F4: 138,
|
|
25
|
+
F5: 135,
|
|
26
|
+
F6: 139,
|
|
27
|
+
F7: 136,
|
|
28
|
+
F8: 140,
|
|
29
|
+
UP: 145,
|
|
30
|
+
DOWN: 17,
|
|
31
|
+
LEFT: 157,
|
|
32
|
+
RIGHT: 29,
|
|
33
|
+
BLACK: 144,
|
|
34
|
+
WHITE: 5,
|
|
35
|
+
RED: 28,
|
|
36
|
+
CYAN: 159,
|
|
37
|
+
PURPLE: 156,
|
|
38
|
+
GREEN: 30,
|
|
39
|
+
BLUE: 31,
|
|
40
|
+
YELLOW: 158,
|
|
41
|
+
ORANGE: 129,
|
|
42
|
+
BROWN: 149,
|
|
43
|
+
LIGHT_RED: 150,
|
|
44
|
+
DARK_GREY: 151,
|
|
45
|
+
MEDIUM_GREY: 152,
|
|
46
|
+
LIGHT_GREEN: 153,
|
|
47
|
+
LIGHT_BLUE: 154,
|
|
48
|
+
LIGHT_GREY: 155,
|
|
49
|
+
REVERSE_ON: 18,
|
|
50
|
+
REVERSE_OFF: 146,
|
|
51
|
+
FLASH_ON: 130,
|
|
52
|
+
FLASH_OFF: 148,
|
|
53
|
+
LOWERCASE: 14,
|
|
54
|
+
UPPERCASE: 142,
|
|
55
|
+
ESCAPE: 27,
|
|
56
|
+
TAB: 9,
|
|
57
|
+
CURSOR_UP: 145,
|
|
58
|
+
CURSOR_DOWN: 17,
|
|
59
|
+
CURSOR_LEFT: 157,
|
|
60
|
+
CURSOR_RIGHT: 29,
|
|
61
|
+
};
|
|
62
|
+
function expandPetsciiTokens(text) {
|
|
63
|
+
return text.replace(/\{([^}]+)\}/g, (_match, token) => {
|
|
64
|
+
const upper = token.trim().toUpperCase();
|
|
65
|
+
const code = PETSCII_TOKENS[upper];
|
|
66
|
+
if (code !== undefined) {
|
|
67
|
+
return String.fromCharCode(code);
|
|
68
|
+
}
|
|
69
|
+
// Numeric form: {$1D} or {29}
|
|
70
|
+
let num;
|
|
71
|
+
if (upper.startsWith("$")) {
|
|
72
|
+
num = parseInt(upper.slice(1), 16);
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
num = parseInt(upper, 10);
|
|
76
|
+
}
|
|
77
|
+
if (!isNaN(num) && num >= 0 && num <= 255) {
|
|
78
|
+
return String.fromCharCode(num);
|
|
79
|
+
}
|
|
80
|
+
return _match; // leave unrecognised tokens as-is
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
// ---------------------------------------------------------------------------
|
|
84
|
+
// Joystick helpers
|
|
85
|
+
// Port 2 → CIA1 Port A ($DC00), Port 1 → CIA1 Port B ($DC01)
|
|
86
|
+
// Bits 0-4: Up / Down / Left / Right / Fire, active LOW (0 = pressed)
|
|
87
|
+
// ---------------------------------------------------------------------------
|
|
88
|
+
const JOYSTICK_PORT_ADDRESS = {
|
|
89
|
+
1: 0xDC01,
|
|
90
|
+
2: 0xDC00,
|
|
91
|
+
};
|
|
92
|
+
const JOYSTICK_BIT = {
|
|
93
|
+
up: 0,
|
|
94
|
+
down: 1,
|
|
95
|
+
left: 2,
|
|
96
|
+
right: 3,
|
|
97
|
+
fire: 4,
|
|
98
|
+
};
|
|
99
|
+
function joystickByte(controls) {
|
|
100
|
+
let mask = 0xff;
|
|
101
|
+
for (const ctrl of controls) {
|
|
102
|
+
const bit = JOYSTICK_BIT[ctrl.toLowerCase()];
|
|
103
|
+
if (bit !== undefined) {
|
|
104
|
+
mask &= ~(1 << bit);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return mask & 0xff;
|
|
108
|
+
}
|
|
109
|
+
// ---------------------------------------------------------------------------
|
|
110
|
+
// Schemas
|
|
111
|
+
// ---------------------------------------------------------------------------
|
|
112
|
+
export const writeTextArgsSchema = objectSchema({
|
|
113
|
+
description: "Send a text string to the keyboard buffer, with PETSCII token expansion.",
|
|
114
|
+
properties: {
|
|
115
|
+
op: literalSchema("write_text"),
|
|
116
|
+
text: stringSchema({
|
|
117
|
+
description: "Text to type. Use {RETURN}, {CLR}, {F1}…{F8}, {UP/DOWN/LEFT/RIGHT}, colour names, etc.",
|
|
118
|
+
minLength: 1,
|
|
119
|
+
maxLength: 512,
|
|
120
|
+
}),
|
|
121
|
+
delayMs: optionalSchema(numberSchema({
|
|
122
|
+
description: "Delay in ms after sending (default 0).",
|
|
123
|
+
integer: true,
|
|
124
|
+
minimum: 0,
|
|
125
|
+
maximum: 10000,
|
|
126
|
+
default: 0,
|
|
127
|
+
})),
|
|
128
|
+
},
|
|
129
|
+
required: ["op", "text"],
|
|
130
|
+
additionalProperties: false,
|
|
131
|
+
});
|
|
132
|
+
export const keyArgsSchema = objectSchema({
|
|
133
|
+
description: "Tap a single key or hold it for a duration.",
|
|
134
|
+
properties: {
|
|
135
|
+
op: literalSchema("key"),
|
|
136
|
+
key: stringSchema({
|
|
137
|
+
description: "Key to press: a printable character, or a PETSCII token name (RETURN, F1…F8, UP, etc.).",
|
|
138
|
+
minLength: 1,
|
|
139
|
+
maxLength: 32,
|
|
140
|
+
}),
|
|
141
|
+
durationMs: optionalSchema(numberSchema({
|
|
142
|
+
description: "Delay in ms after each queued key press before the next press (default 0 = no delay).",
|
|
143
|
+
integer: true,
|
|
144
|
+
minimum: 0,
|
|
145
|
+
maximum: 5000,
|
|
146
|
+
default: 0,
|
|
147
|
+
})),
|
|
148
|
+
count: optionalSchema(numberSchema({
|
|
149
|
+
description: "Number of times to press the key (default 1).",
|
|
150
|
+
integer: true,
|
|
151
|
+
minimum: 1,
|
|
152
|
+
maximum: 100,
|
|
153
|
+
default: 1,
|
|
154
|
+
})),
|
|
155
|
+
},
|
|
156
|
+
required: ["op", "key"],
|
|
157
|
+
additionalProperties: false,
|
|
158
|
+
});
|
|
159
|
+
export const joystickArgsSchema = objectSchema({
|
|
160
|
+
description: "Simulate joystick input by writing directly to CIA1 Port A/B registers.",
|
|
161
|
+
properties: {
|
|
162
|
+
op: literalSchema("joystick"),
|
|
163
|
+
port: numberSchema({
|
|
164
|
+
description: "Joystick port (1 = $DC01, 2 = $DC00).",
|
|
165
|
+
integer: true,
|
|
166
|
+
minimum: 1,
|
|
167
|
+
maximum: 2,
|
|
168
|
+
}),
|
|
169
|
+
controls: arraySchema(stringSchema({
|
|
170
|
+
description: "Control to activate.",
|
|
171
|
+
enum: ["up", "down", "left", "right", "fire"],
|
|
172
|
+
}), {
|
|
173
|
+
description: "List of controls to activate simultaneously.",
|
|
174
|
+
minItems: 0,
|
|
175
|
+
}),
|
|
176
|
+
action: stringSchema({
|
|
177
|
+
description: "press = hold until a release call; release = restore all bits; tap = press then release after durationMs.",
|
|
178
|
+
enum: ["press", "release", "tap"],
|
|
179
|
+
}),
|
|
180
|
+
durationMs: optionalSchema(numberSchema({
|
|
181
|
+
description: "Duration to hold for tap action in ms (default 80).",
|
|
182
|
+
integer: true,
|
|
183
|
+
minimum: 10,
|
|
184
|
+
maximum: 5000,
|
|
185
|
+
default: 80,
|
|
186
|
+
})),
|
|
187
|
+
},
|
|
188
|
+
required: ["op", "port", "controls", "action"],
|
|
189
|
+
additionalProperties: false,
|
|
190
|
+
});
|
|
191
|
+
// ---------------------------------------------------------------------------
|
|
192
|
+
// Handlers
|
|
193
|
+
// ---------------------------------------------------------------------------
|
|
194
|
+
function textToPetsciiBytes(text) {
|
|
195
|
+
const bytes = [];
|
|
196
|
+
for (let i = 0; i < text.length; i++) {
|
|
197
|
+
bytes.push(text.charCodeAt(i) & 0xff);
|
|
198
|
+
}
|
|
199
|
+
return Uint8Array.from(bytes);
|
|
200
|
+
}
|
|
201
|
+
const inputOperationHandlers = {
|
|
202
|
+
write_text: async (args, ctx) => {
|
|
203
|
+
try {
|
|
204
|
+
const parsed = writeTextArgsSchema.parse(args);
|
|
205
|
+
const expanded = expandPetsciiTokens(parsed.text);
|
|
206
|
+
const petsciiBytes = textToPetsciiBytes(expanded);
|
|
207
|
+
await ctx.client.injectKeyboardQueue(petsciiBytes);
|
|
208
|
+
if (parsed.delayMs && parsed.delayMs > 0) {
|
|
209
|
+
await new Promise((res) => setTimeout(res, parsed.delayMs));
|
|
210
|
+
}
|
|
211
|
+
ctx.logger.info("Sent keyboard text", { length: expanded.length });
|
|
212
|
+
return textResult(`Sent ${expanded.length} character(s) to keyboard buffer.`, { success: true, length: expanded.length });
|
|
213
|
+
}
|
|
214
|
+
catch (error) {
|
|
215
|
+
if (error instanceof ToolValidationError)
|
|
216
|
+
return toolErrorResult(error);
|
|
217
|
+
return unknownErrorResult(error);
|
|
218
|
+
}
|
|
219
|
+
},
|
|
220
|
+
key: async (args, ctx) => {
|
|
221
|
+
try {
|
|
222
|
+
const parsed = keyArgsSchema.parse(args);
|
|
223
|
+
const count = parsed.count ?? 1;
|
|
224
|
+
const durationMs = parsed.durationMs ?? 0;
|
|
225
|
+
// Resolve key to a character: try token first, then single char
|
|
226
|
+
const tokenCode = PETSCII_TOKENS[parsed.key.toUpperCase()];
|
|
227
|
+
const keyByte = tokenCode !== undefined
|
|
228
|
+
? tokenCode
|
|
229
|
+
: parsed.key.length === 1
|
|
230
|
+
? parsed.key.charCodeAt(0) & 0xff
|
|
231
|
+
: (() => {
|
|
232
|
+
throw new ToolValidationError(`Unrecognised key: ${parsed.key}. Use a single character or a PETSCII token name.`, { path: "$.key" });
|
|
233
|
+
})();
|
|
234
|
+
for (let i = 0; i < count; i++) {
|
|
235
|
+
await ctx.client.injectKeyboardQueue(Uint8Array.of(keyByte));
|
|
236
|
+
if (durationMs > 0) {
|
|
237
|
+
await new Promise((res) => setTimeout(res, durationMs));
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
ctx.logger.info("Pressed key", { key: parsed.key, count, durationMs });
|
|
241
|
+
return textResult(`Pressed '${parsed.key}' ${count} time${count === 1 ? "" : "s"}.`, { success: true, key: parsed.key, count, durationMs });
|
|
242
|
+
}
|
|
243
|
+
catch (error) {
|
|
244
|
+
if (error instanceof ToolValidationError)
|
|
245
|
+
return toolErrorResult(error);
|
|
246
|
+
return unknownErrorResult(error);
|
|
247
|
+
}
|
|
248
|
+
},
|
|
249
|
+
joystick: async (args, ctx) => {
|
|
250
|
+
try {
|
|
251
|
+
const parsed = joystickArgsSchema.parse(args);
|
|
252
|
+
const port = parsed.port;
|
|
253
|
+
const addr = JOYSTICK_PORT_ADDRESS[port];
|
|
254
|
+
const durationMs = parsed.durationMs ?? 80;
|
|
255
|
+
const pressedByte = joystickByte(parsed.controls);
|
|
256
|
+
const releasedByte = 0xff;
|
|
257
|
+
if (parsed.action === "release") {
|
|
258
|
+
await ctx.client.viceMemSet(addr, Uint8Array.of(releasedByte));
|
|
259
|
+
ctx.logger.info("Released joystick", { port, address: `$${addr.toString(16).toUpperCase()}` });
|
|
260
|
+
return textResult(`Joystick port ${port} released.`, { success: true, port, action: "release" });
|
|
261
|
+
}
|
|
262
|
+
if (parsed.action === "press") {
|
|
263
|
+
await ctx.client.viceMemSet(addr, Uint8Array.of(pressedByte));
|
|
264
|
+
ctx.logger.info("Pressed joystick", { port, controls: parsed.controls, byte: pressedByte });
|
|
265
|
+
return textResult(`Joystick port ${port} pressed: ${parsed.controls.join(", ") || "none"}.`, { success: true, port, action: "press", controls: parsed.controls });
|
|
266
|
+
}
|
|
267
|
+
// tap: press → wait → release
|
|
268
|
+
await ctx.client.viceMemSet(addr, Uint8Array.of(pressedByte));
|
|
269
|
+
await new Promise((res) => setTimeout(res, durationMs));
|
|
270
|
+
await ctx.client.viceMemSet(addr, Uint8Array.of(releasedByte));
|
|
271
|
+
ctx.logger.info("Tapped joystick", { port, controls: parsed.controls, durationMs });
|
|
272
|
+
return textResult(`Joystick port ${port} tapped: ${parsed.controls.join(", ") || "none"} for ${durationMs}ms.`, { success: true, port, action: "tap", controls: parsed.controls, durationMs });
|
|
273
|
+
}
|
|
274
|
+
catch (error) {
|
|
275
|
+
if (error instanceof ToolValidationError)
|
|
276
|
+
return toolErrorResult(error);
|
|
277
|
+
return unknownErrorResult(error);
|
|
278
|
+
}
|
|
279
|
+
},
|
|
280
|
+
};
|
|
281
|
+
// ---------------------------------------------------------------------------
|
|
282
|
+
// Module definition
|
|
283
|
+
// ---------------------------------------------------------------------------
|
|
284
|
+
export const inputModule = defineToolModule({
|
|
285
|
+
domain: "input",
|
|
286
|
+
summary: "Cross-platform keyboard input plus VICE-only joystick simulation.",
|
|
287
|
+
supportedPlatforms: ["c64u", "vice"],
|
|
288
|
+
resources: ["c64://specs/assembly", "c64://specs/memory-map"],
|
|
289
|
+
prompts: [],
|
|
290
|
+
defaultTags: ["input"],
|
|
291
|
+
workflowHints: [
|
|
292
|
+
"Use write_text with {RETURN} tokens to automate BASIC entry; prefer key for individual control keys.",
|
|
293
|
+
"write_text and key inject through the KERNAL keyboard queue ($0277/$00C6) so they work on both C64U and VICE.",
|
|
294
|
+
"Joystick tap is suitable for one-shot moves; use press/release pairs for timed holds. Joystick is VICE-only.",
|
|
295
|
+
],
|
|
296
|
+
tools: [
|
|
297
|
+
{
|
|
298
|
+
name: "c64_input",
|
|
299
|
+
description: "Keyboard buffer injection (cross-platform) and joystick simulation (VICE only).",
|
|
300
|
+
summary: "Types text, taps keys, and simulates joystick movements.",
|
|
301
|
+
inputSchema: {
|
|
302
|
+
type: "object",
|
|
303
|
+
description: "Input operations: write_text, key, joystick.",
|
|
304
|
+
oneOf: [
|
|
305
|
+
writeTextArgsSchema.jsonSchema,
|
|
306
|
+
keyArgsSchema.jsonSchema,
|
|
307
|
+
joystickArgsSchema.jsonSchema,
|
|
308
|
+
],
|
|
309
|
+
discriminator: { propertyName: "op" },
|
|
310
|
+
},
|
|
311
|
+
operationPlatforms: { joystick: ["vice"] },
|
|
312
|
+
tags: ["input", "keyboard", "joystick"],
|
|
313
|
+
examples: [
|
|
314
|
+
{
|
|
315
|
+
name: "Type BASIC line",
|
|
316
|
+
description: "Enter a BASIC line and press RETURN",
|
|
317
|
+
arguments: { op: "write_text", text: "PRINT \"HELLO\"{RETURN}" },
|
|
318
|
+
},
|
|
319
|
+
{
|
|
320
|
+
name: "Press F1",
|
|
321
|
+
description: "Send the F1 function key",
|
|
322
|
+
arguments: { op: "key", key: "F1" },
|
|
323
|
+
},
|
|
324
|
+
{
|
|
325
|
+
name: "Tap joystick right",
|
|
326
|
+
description: "Brief rightward tap on joystick port 2",
|
|
327
|
+
arguments: { op: "joystick", port: 2, controls: ["right"], action: "tap", durationMs: 80 },
|
|
328
|
+
},
|
|
329
|
+
{
|
|
330
|
+
name: "Press fire on port 1",
|
|
331
|
+
description: "Hold fire button on joystick port 1",
|
|
332
|
+
arguments: { op: "joystick", port: 1, controls: ["fire"], action: "press" },
|
|
333
|
+
},
|
|
334
|
+
],
|
|
335
|
+
execute: createOperationDispatcher("c64_input", inputOperationHandlers),
|
|
336
|
+
},
|
|
337
|
+
],
|
|
338
|
+
});
|
package/dist/tools/memory.js
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
import { Buffer } from "node:buffer";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
2
4
|
import { defineToolModule, OPERATION_DISCRIMINATOR, } from "./types.js";
|
|
3
5
|
import { booleanSchema, numberSchema, objectSchema, optionalSchema, stringSchema } from "./schema.js";
|
|
4
|
-
import { textResult } from "./responses.js";
|
|
6
|
+
import { jsonResult, textResult } from "./responses.js";
|
|
5
7
|
import { ToolError, ToolExecutionError, ToolValidationError, toolErrorResult, unknownErrorResult, } from "./errors.js";
|
|
8
|
+
import { disassemble, formatDisassembly } from "./disassembler.js";
|
|
9
|
+
import { getViceSymbols } from "./symbolRegistry.js";
|
|
10
|
+
import { resolveAddressSymbol } from "../knowledge.js";
|
|
6
11
|
function toRecord(details) {
|
|
7
12
|
if (details && typeof details === "object") {
|
|
8
13
|
return details;
|
|
@@ -79,6 +84,18 @@ function supportsMachinePause(ctx) {
|
|
|
79
84
|
const platformId = ctx.platform?.id;
|
|
80
85
|
return platformId !== "vice";
|
|
81
86
|
}
|
|
87
|
+
export const __memoryHelpersForTests = {
|
|
88
|
+
toRecord,
|
|
89
|
+
normaliseFailure,
|
|
90
|
+
cleanHex,
|
|
91
|
+
parseUserHex,
|
|
92
|
+
parseFirmwareHex,
|
|
93
|
+
formatByte,
|
|
94
|
+
resolveAddressLabel,
|
|
95
|
+
resolveLength,
|
|
96
|
+
supportsMachinePause,
|
|
97
|
+
stripOperationDiscriminator,
|
|
98
|
+
};
|
|
82
99
|
const readScreenArgsSchema = objectSchema({
|
|
83
100
|
description: "No arguments are required for reading the current screen contents.",
|
|
84
101
|
properties: {},
|
|
@@ -352,11 +369,310 @@ async function executeWriteMemory(rawArgs, ctx) {
|
|
|
352
369
|
return unknownErrorResult(error);
|
|
353
370
|
}
|
|
354
371
|
}
|
|
372
|
+
const disassembleArgsSchema = objectSchema({
|
|
373
|
+
description: "Parameters for disassembling a block of VICE memory into 6502/6510 instructions.",
|
|
374
|
+
properties: {
|
|
375
|
+
address: stringSchema({
|
|
376
|
+
description: "Start address as $HHHH, decimal, or symbol name.",
|
|
377
|
+
minLength: 1,
|
|
378
|
+
}),
|
|
379
|
+
length: numberSchema({
|
|
380
|
+
description: "Number of bytes to read and disassemble.",
|
|
381
|
+
integer: true,
|
|
382
|
+
minimum: 1,
|
|
383
|
+
maximum: 4096,
|
|
384
|
+
default: 64,
|
|
385
|
+
}),
|
|
386
|
+
instructionCount: optionalSchema(numberSchema({
|
|
387
|
+
description: "Stop after this many instructions (overrides length as a termination condition).",
|
|
388
|
+
integer: true,
|
|
389
|
+
minimum: 1,
|
|
390
|
+
maximum: 512,
|
|
391
|
+
})),
|
|
392
|
+
},
|
|
393
|
+
required: ["address"],
|
|
394
|
+
additionalProperties: false,
|
|
395
|
+
});
|
|
396
|
+
async function executeDisassemble(rawArgs, ctx) {
|
|
397
|
+
try {
|
|
398
|
+
const parsed = disassembleArgsSchema.parse(rawArgs ?? {});
|
|
399
|
+
const length = parsed.length ?? 64;
|
|
400
|
+
ctx.logger.info("Disassembling VICE memory", { address: parsed.address, length });
|
|
401
|
+
const address = parseAddressArg(parsed.address, "$.address");
|
|
402
|
+
const bytes = await ctx.client.readMemoryRaw(address, length);
|
|
403
|
+
const symbols = ctx.platform?.id === "vice" ? getViceSymbols() : undefined;
|
|
404
|
+
const lines = disassemble(bytes, address, parsed.instructionCount, symbols);
|
|
405
|
+
const text = formatDisassembly(lines);
|
|
406
|
+
const addrHex = `$${address.toString(16).toUpperCase().padStart(4, "0")}`;
|
|
407
|
+
return textResult(`Disassembly of ${lines.length} instructions starting at ${addrHex}:\n\n${text}`, {
|
|
408
|
+
success: true,
|
|
409
|
+
address: addrHex,
|
|
410
|
+
bytesRead: bytes.length,
|
|
411
|
+
instructionCount: lines.length,
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
catch (error) {
|
|
415
|
+
if (error instanceof ToolError) {
|
|
416
|
+
return toolErrorResult(error);
|
|
417
|
+
}
|
|
418
|
+
return unknownErrorResult(error);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
// ---------------------------------------------------------------------------
|
|
422
|
+
// Shared address-parsing helper
|
|
423
|
+
// ---------------------------------------------------------------------------
|
|
424
|
+
function parseAddressArg(raw, fieldPath) {
|
|
425
|
+
const trimmed = raw.trim();
|
|
426
|
+
let addr;
|
|
427
|
+
if (/^\$[0-9A-Fa-f]+$/.test(trimmed)) {
|
|
428
|
+
addr = parseInt(trimmed.slice(1), 16);
|
|
429
|
+
}
|
|
430
|
+
else if (/^0[xX][0-9A-Fa-f]+$/.test(trimmed)) {
|
|
431
|
+
addr = parseInt(trimmed.slice(2), 16);
|
|
432
|
+
}
|
|
433
|
+
else if (/^[0-9]+$/.test(trimmed)) {
|
|
434
|
+
addr = parseInt(trimmed, 10);
|
|
435
|
+
}
|
|
436
|
+
else {
|
|
437
|
+
const resolved = resolveAddressSymbol(trimmed);
|
|
438
|
+
if (resolved === undefined) {
|
|
439
|
+
throw new ToolValidationError(`Unknown address or symbol: ${trimmed}`, { path: fieldPath });
|
|
440
|
+
}
|
|
441
|
+
addr = resolved;
|
|
442
|
+
}
|
|
443
|
+
if (!Number.isInteger(addr) || addr < 0 || addr > 0xffff) {
|
|
444
|
+
throw new ToolValidationError("Address must be in range $0000-$FFFF", { path: fieldPath });
|
|
445
|
+
}
|
|
446
|
+
return addr;
|
|
447
|
+
}
|
|
448
|
+
function fmtAddr(addr) {
|
|
449
|
+
return `$${addr.toString(16).toUpperCase().padStart(4, "0")}`;
|
|
450
|
+
}
|
|
451
|
+
function validateRangeWithinAddressSpace(start, length, fieldPath) {
|
|
452
|
+
const end = start + length - 1;
|
|
453
|
+
if (end > 0xffff) {
|
|
454
|
+
throw new ToolValidationError(`${fieldPath} range exceeds address space; ${fmtAddr(start)} + ${length} bytes would pass $FFFF.`, { path: fieldPath });
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
// ---------------------------------------------------------------------------
|
|
458
|
+
// copy_memory
|
|
459
|
+
// ---------------------------------------------------------------------------
|
|
460
|
+
const copyMemoryArgsSchema = objectSchema({
|
|
461
|
+
description: "Copy a region of RAM from one address to another.",
|
|
462
|
+
properties: {
|
|
463
|
+
source: stringSchema({ description: "Source start address ($HHHH, decimal, or symbol).", minLength: 1 }),
|
|
464
|
+
dest: stringSchema({ description: "Destination start address.", minLength: 1 }),
|
|
465
|
+
length: numberSchema({ description: "Number of bytes to copy (1–16384).", integer: true, minimum: 1, maximum: 16384 }),
|
|
466
|
+
},
|
|
467
|
+
required: ["source", "dest", "length"],
|
|
468
|
+
additionalProperties: false,
|
|
469
|
+
});
|
|
470
|
+
async function executeCopyMemory(rawArgs, ctx) {
|
|
471
|
+
try {
|
|
472
|
+
const parsed = copyMemoryArgsSchema.parse(rawArgs ?? {});
|
|
473
|
+
const src = parseAddressArg(parsed.source, "$.source");
|
|
474
|
+
const dst = parseAddressArg(parsed.dest, "$.dest");
|
|
475
|
+
const len = parsed.length;
|
|
476
|
+
validateRangeWithinAddressSpace(src, len, "$.source");
|
|
477
|
+
validateRangeWithinAddressSpace(dst, len, "$.dest");
|
|
478
|
+
ctx.logger.info("Copying memory", { src: fmtAddr(src), dst: fmtAddr(dst), len });
|
|
479
|
+
const bytes = await ctx.client.readMemoryRaw(src, len);
|
|
480
|
+
await ctx.client.writeMemoryRaw(dst, bytes);
|
|
481
|
+
return textResult(`Copied ${bytes.length} bytes from ${fmtAddr(src)} to ${fmtAddr(dst)}.`, {
|
|
482
|
+
success: true, source: fmtAddr(src), dest: fmtAddr(dst), bytesCopied: bytes.length,
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
catch (error) {
|
|
486
|
+
if (error instanceof ToolError)
|
|
487
|
+
return toolErrorResult(error);
|
|
488
|
+
return unknownErrorResult(error);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
// ---------------------------------------------------------------------------
|
|
492
|
+
// fill_memory
|
|
493
|
+
// ---------------------------------------------------------------------------
|
|
494
|
+
const fillMemoryArgsSchema = objectSchema({
|
|
495
|
+
description: "Fill a memory region with a repeating byte pattern.",
|
|
496
|
+
properties: {
|
|
497
|
+
address: stringSchema({ description: "Start address ($HHHH, decimal, or symbol).", minLength: 1 }),
|
|
498
|
+
length: numberSchema({ description: "Number of bytes to fill (1–16384).", integer: true, minimum: 1, maximum: 16384 }),
|
|
499
|
+
pattern: stringSchema({
|
|
500
|
+
description: "Hex bytes to repeat, e.g. 'FF' or 'AA 55'. Space-separated or run together.",
|
|
501
|
+
minLength: 2,
|
|
502
|
+
pattern: /^[\s0-9A-Fa-f$]+$/,
|
|
503
|
+
}),
|
|
504
|
+
},
|
|
505
|
+
required: ["address", "length", "pattern"],
|
|
506
|
+
additionalProperties: false,
|
|
507
|
+
});
|
|
508
|
+
async function executeFillMemory(rawArgs, ctx) {
|
|
509
|
+
try {
|
|
510
|
+
const parsed = fillMemoryArgsSchema.parse(rawArgs ?? {});
|
|
511
|
+
const addr = parseAddressArg(parsed.address, "$.address");
|
|
512
|
+
validateRangeWithinAddressSpace(addr, parsed.length, "$.address");
|
|
513
|
+
const { bytes: patternBytes } = parseUserHex(parsed.pattern, "$.pattern");
|
|
514
|
+
if (patternBytes.length === 0)
|
|
515
|
+
throw new ToolValidationError("Pattern must not be empty", { path: "$.pattern" });
|
|
516
|
+
const buf = new Uint8Array(parsed.length);
|
|
517
|
+
for (let i = 0; i < parsed.length; i++)
|
|
518
|
+
buf[i] = patternBytes[i % patternBytes.length];
|
|
519
|
+
await ctx.client.writeMemoryRaw(addr, buf);
|
|
520
|
+
ctx.logger.info("Filled memory", { address: fmtAddr(addr), length: parsed.length, pattern: parsed.pattern });
|
|
521
|
+
return textResult(`Filled ${parsed.length} bytes at ${fmtAddr(addr)} with pattern ${parsed.pattern.trim()}.`, {
|
|
522
|
+
success: true, address: fmtAddr(addr), length: parsed.length, pattern: parsed.pattern.trim(),
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
catch (error) {
|
|
526
|
+
if (error instanceof ToolError)
|
|
527
|
+
return toolErrorResult(error);
|
|
528
|
+
return unknownErrorResult(error);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
// ---------------------------------------------------------------------------
|
|
532
|
+
// search_memory
|
|
533
|
+
// ---------------------------------------------------------------------------
|
|
534
|
+
const searchMemoryArgsSchema = objectSchema({
|
|
535
|
+
description: "Search for a byte pattern within a memory range.",
|
|
536
|
+
properties: {
|
|
537
|
+
startAddress: stringSchema({ description: "Start of search range ($HHHH, decimal, or symbol).", minLength: 1 }),
|
|
538
|
+
endAddress: stringSchema({ description: "End of search range (inclusive).", minLength: 1 }),
|
|
539
|
+
pattern: stringSchema({
|
|
540
|
+
description: "Hex bytes to find, e.g. 'A9 00' for LDA #$00.",
|
|
541
|
+
minLength: 2,
|
|
542
|
+
pattern: /^[\s0-9A-Fa-f$]+$/,
|
|
543
|
+
}),
|
|
544
|
+
maxResults: optionalSchema(numberSchema({ description: "Maximum matches to return (default 10).", integer: true, minimum: 1, maximum: 100, default: 10 })),
|
|
545
|
+
},
|
|
546
|
+
required: ["startAddress", "endAddress", "pattern"],
|
|
547
|
+
additionalProperties: false,
|
|
548
|
+
});
|
|
549
|
+
async function executeSearchMemory(rawArgs, ctx) {
|
|
550
|
+
try {
|
|
551
|
+
const parsed = searchMemoryArgsSchema.parse(rawArgs ?? {});
|
|
552
|
+
const start = parseAddressArg(parsed.startAddress, "$.startAddress");
|
|
553
|
+
const end = parseAddressArg(parsed.endAddress, "$.endAddress");
|
|
554
|
+
if (end < start)
|
|
555
|
+
throw new ToolValidationError("endAddress must be ≥ startAddress", { path: "$.endAddress" });
|
|
556
|
+
const { bytes: needle } = parseUserHex(parsed.pattern, "$.pattern");
|
|
557
|
+
if (needle.length === 0)
|
|
558
|
+
throw new ToolValidationError("Pattern must not be empty", { path: "$.pattern" });
|
|
559
|
+
const maxResults = parsed.maxResults ?? 10;
|
|
560
|
+
const len = end - start + 1;
|
|
561
|
+
ctx.logger.info("Searching memory", { start: fmtAddr(start), end: fmtAddr(end), pattern: parsed.pattern });
|
|
562
|
+
const haystack = await ctx.client.readMemoryRaw(start, len);
|
|
563
|
+
const matches = [];
|
|
564
|
+
outer: for (let i = 0; i <= haystack.length - needle.length && matches.length < maxResults; i++) {
|
|
565
|
+
for (let j = 0; j < needle.length; j++) {
|
|
566
|
+
if (haystack[i + j] !== needle[j])
|
|
567
|
+
continue outer;
|
|
568
|
+
}
|
|
569
|
+
matches.push(fmtAddr(start + i));
|
|
570
|
+
}
|
|
571
|
+
return jsonResult({ found: matches.length, matches, pattern: parsed.pattern.trim(), range: { start: fmtAddr(start), end: fmtAddr(end) } }, { success: true, found: matches.length });
|
|
572
|
+
}
|
|
573
|
+
catch (error) {
|
|
574
|
+
if (error instanceof ToolError)
|
|
575
|
+
return toolErrorResult(error);
|
|
576
|
+
return unknownErrorResult(error);
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
// ---------------------------------------------------------------------------
|
|
580
|
+
// compare_memory
|
|
581
|
+
// ---------------------------------------------------------------------------
|
|
582
|
+
const compareMemoryArgsSchema = objectSchema({
|
|
583
|
+
description: "Compare two memory regions and report differing bytes.",
|
|
584
|
+
properties: {
|
|
585
|
+
address1: stringSchema({ description: "First region start address.", minLength: 1 }),
|
|
586
|
+
address2: stringSchema({ description: "Second region start address.", minLength: 1 }),
|
|
587
|
+
length: numberSchema({ description: "Number of bytes to compare (1–16384).", integer: true, minimum: 1, maximum: 16384 }),
|
|
588
|
+
maxDiffs: optionalSchema(numberSchema({ description: "Max differences to report (default 10).", integer: true, minimum: 1, maximum: 200, default: 10 })),
|
|
589
|
+
},
|
|
590
|
+
required: ["address1", "address2", "length"],
|
|
591
|
+
additionalProperties: false,
|
|
592
|
+
});
|
|
593
|
+
async function executeCompareMemory(rawArgs, ctx) {
|
|
594
|
+
try {
|
|
595
|
+
const parsed = compareMemoryArgsSchema.parse(rawArgs ?? {});
|
|
596
|
+
const a1 = parseAddressArg(parsed.address1, "$.address1");
|
|
597
|
+
const a2 = parseAddressArg(parsed.address2, "$.address2");
|
|
598
|
+
const len = parsed.length;
|
|
599
|
+
const maxDiffs = parsed.maxDiffs ?? 10;
|
|
600
|
+
ctx.logger.info("Comparing memory regions", { a1: fmtAddr(a1), a2: fmtAddr(a2), len });
|
|
601
|
+
const [r1, r2] = await Promise.all([
|
|
602
|
+
ctx.client.readMemoryRaw(a1, len),
|
|
603
|
+
ctx.client.readMemoryRaw(a2, len),
|
|
604
|
+
]);
|
|
605
|
+
const diffs = [];
|
|
606
|
+
for (let i = 0; i < len && diffs.length < maxDiffs; i++) {
|
|
607
|
+
if (r1[i] !== r2[i]) {
|
|
608
|
+
diffs.push({
|
|
609
|
+
offset: i,
|
|
610
|
+
address1: fmtAddr(a1 + i),
|
|
611
|
+
address2: fmtAddr(a2 + i),
|
|
612
|
+
value1: formatByte(r1[i] ?? 0),
|
|
613
|
+
value2: formatByte(r2[i] ?? 0),
|
|
614
|
+
});
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
const identical = diffs.length === 0;
|
|
618
|
+
return jsonResult({ identical, diffCount: diffs.length, diffs, region1: fmtAddr(a1), region2: fmtAddr(a2), length: len }, { success: true, identical, diffCount: diffs.length });
|
|
619
|
+
}
|
|
620
|
+
catch (error) {
|
|
621
|
+
if (error instanceof ToolError)
|
|
622
|
+
return toolErrorResult(error);
|
|
623
|
+
return unknownErrorResult(error);
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
// ---------------------------------------------------------------------------
|
|
627
|
+
// save_memory
|
|
628
|
+
// ---------------------------------------------------------------------------
|
|
629
|
+
const saveMemoryArgsSchema = objectSchema({
|
|
630
|
+
description: "Dump a memory region to a local file, optionally with a PRG load-address header.",
|
|
631
|
+
properties: {
|
|
632
|
+
startAddress: stringSchema({ description: "Start address of region to save.", minLength: 1 }),
|
|
633
|
+
endAddress: stringSchema({ description: "End address (inclusive).", minLength: 1 }),
|
|
634
|
+
filePath: stringSchema({ description: "Absolute or relative path for the output file.", minLength: 1 }),
|
|
635
|
+
asPrg: optionalSchema(booleanSchema({ description: "Prepend a 2-byte little-endian load-address header (PRG format). Default: true.", default: true })),
|
|
636
|
+
},
|
|
637
|
+
required: ["startAddress", "endAddress", "filePath"],
|
|
638
|
+
additionalProperties: false,
|
|
639
|
+
});
|
|
640
|
+
async function executeSaveMemory(rawArgs, ctx) {
|
|
641
|
+
try {
|
|
642
|
+
const parsed = saveMemoryArgsSchema.parse(rawArgs ?? {});
|
|
643
|
+
const start = parseAddressArg(parsed.startAddress, "$.startAddress");
|
|
644
|
+
const end = parseAddressArg(parsed.endAddress, "$.endAddress");
|
|
645
|
+
if (end < start)
|
|
646
|
+
throw new ToolValidationError("endAddress must be ≥ startAddress", { path: "$.endAddress" });
|
|
647
|
+
const asPrg = parsed.asPrg !== false;
|
|
648
|
+
const len = end - start + 1;
|
|
649
|
+
const resolvedPath = path.resolve(parsed.filePath);
|
|
650
|
+
ctx.logger.info("Saving memory to file", { start: fmtAddr(start), end: fmtAddr(end), filePath: resolvedPath, asPrg });
|
|
651
|
+
const bytes = await ctx.client.readMemoryRaw(start, len);
|
|
652
|
+
const header = asPrg ? Buffer.from([start & 0xff, (start >> 8) & 0xff]) : Buffer.alloc(0);
|
|
653
|
+
await fs.promises.mkdir(path.dirname(resolvedPath), { recursive: true });
|
|
654
|
+
await fs.promises.writeFile(resolvedPath, Buffer.concat([header, Buffer.from(bytes)]));
|
|
655
|
+
const fileSize = bytes.length + header.length;
|
|
656
|
+
return textResult(`Saved ${bytes.length} bytes (${fmtAddr(start)}–${fmtAddr(end)}) to ${resolvedPath}${asPrg ? " (PRG, with load address header)" : ""}.`, { success: true, filePath: resolvedPath, bytesRead: bytes.length, fileSize, asPrg, startAddress: fmtAddr(start), endAddress: fmtAddr(end) });
|
|
657
|
+
}
|
|
658
|
+
catch (error) {
|
|
659
|
+
if (error instanceof ToolError)
|
|
660
|
+
return toolErrorResult(error);
|
|
661
|
+
return unknownErrorResult(error);
|
|
662
|
+
}
|
|
663
|
+
}
|
|
355
664
|
export const memoryOperationHandlers = {
|
|
356
665
|
read: async (args, ctx) => executeReadMemory(stripOperationDiscriminator(args), ctx),
|
|
357
666
|
write: async (args, ctx) => executeWriteMemory(stripOperationDiscriminator(args), ctx),
|
|
358
667
|
read_screen: async (args, ctx) => executeReadScreen(stripOperationDiscriminator(args), ctx),
|
|
668
|
+
disassemble: async (args, ctx) => executeDisassemble(stripOperationDiscriminator(args), ctx),
|
|
669
|
+
copy_memory: async (args, ctx) => executeCopyMemory(stripOperationDiscriminator(args), ctx),
|
|
670
|
+
fill_memory: async (args, ctx) => executeFillMemory(stripOperationDiscriminator(args), ctx),
|
|
671
|
+
search_memory: async (args, ctx) => executeSearchMemory(stripOperationDiscriminator(args), ctx),
|
|
672
|
+
compare_memory: async (args, ctx) => executeCompareMemory(stripOperationDiscriminator(args), ctx),
|
|
673
|
+
save_memory: async (args, ctx) => executeSaveMemory(stripOperationDiscriminator(args), ctx),
|
|
359
674
|
};
|
|
675
|
+
export { disassembleArgsSchema, copyMemoryArgsSchema, fillMemoryArgsSchema, searchMemoryArgsSchema, compareMemoryArgsSchema, saveMemoryArgsSchema, };
|
|
360
676
|
export const memoryModule = defineToolModule({
|
|
361
677
|
domain: "memory",
|
|
362
678
|
summary: "Screen, main memory, and low-level inspection utilities.",
|