c64-debug-mcp 1.0.2 → 1.0.7
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 +61 -3
- package/dist/http.cjs +444 -140
- package/dist/http.js +444 -140
- package/dist/stdio.cjs +444 -140
- package/dist/stdio.js +444 -140
- package/package.json +1 -1
package/dist/http.cjs
CHANGED
|
@@ -152,9 +152,193 @@ var import_zod3 = require("zod");
|
|
|
152
152
|
// src/schemas.ts
|
|
153
153
|
var import_zod2 = require("zod");
|
|
154
154
|
var warningSchema = warningItemSchema;
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
155
|
+
function parseAddress16(input) {
|
|
156
|
+
if (typeof input === "number") {
|
|
157
|
+
if (!Number.isInteger(input) || input < 0 || input > 65535) {
|
|
158
|
+
throw new import_zod2.z.ZodError([
|
|
159
|
+
{
|
|
160
|
+
code: "custom",
|
|
161
|
+
message: `Address must be an integer between 0 and 65535 (0xFFFF), got ${input}`,
|
|
162
|
+
path: []
|
|
163
|
+
}
|
|
164
|
+
]);
|
|
165
|
+
}
|
|
166
|
+
return input;
|
|
167
|
+
}
|
|
168
|
+
if (typeof input !== "string") {
|
|
169
|
+
throw new import_zod2.z.ZodError([
|
|
170
|
+
{
|
|
171
|
+
code: "custom",
|
|
172
|
+
message: `Address must be a number or string, got ${typeof input}`,
|
|
173
|
+
path: []
|
|
174
|
+
}
|
|
175
|
+
]);
|
|
176
|
+
}
|
|
177
|
+
const trimmed = input.trim();
|
|
178
|
+
let hexString;
|
|
179
|
+
let format;
|
|
180
|
+
if (trimmed.startsWith("$")) {
|
|
181
|
+
hexString = trimmed.slice(1);
|
|
182
|
+
format = "C64 hex ($)";
|
|
183
|
+
} else if (trimmed.toLowerCase().startsWith("0x")) {
|
|
184
|
+
hexString = trimmed.slice(2);
|
|
185
|
+
format = "C hex (0x)";
|
|
186
|
+
} else {
|
|
187
|
+
throw new import_zod2.z.ZodError([
|
|
188
|
+
{
|
|
189
|
+
code: "custom",
|
|
190
|
+
message: `Invalid address format: "${input}". Expected formats: decimal number (53248), hex with $ ($D000), or hex with 0x (0xD000). Bare hex not supported to avoid ambiguity.`,
|
|
191
|
+
path: []
|
|
192
|
+
}
|
|
193
|
+
]);
|
|
194
|
+
}
|
|
195
|
+
if (!/^[0-9A-Fa-f]{1,4}$/.test(hexString)) {
|
|
196
|
+
throw new import_zod2.z.ZodError([
|
|
197
|
+
{
|
|
198
|
+
code: "custom",
|
|
199
|
+
message: `Invalid ${format} address: "${input}". Hex portion must be 1-4 hex digits (0-9, A-F)`,
|
|
200
|
+
path: []
|
|
201
|
+
}
|
|
202
|
+
]);
|
|
203
|
+
}
|
|
204
|
+
const parsed = parseInt(hexString, 16);
|
|
205
|
+
if (isNaN(parsed) || parsed < 0 || parsed > 65535) {
|
|
206
|
+
throw new import_zod2.z.ZodError([
|
|
207
|
+
{
|
|
208
|
+
code: "custom",
|
|
209
|
+
message: `Address out of range: "${input}" (${parsed}). Must be 0x0000-0xFFFF (0-65535)`,
|
|
210
|
+
path: []
|
|
211
|
+
}
|
|
212
|
+
]);
|
|
213
|
+
}
|
|
214
|
+
return parsed;
|
|
215
|
+
}
|
|
216
|
+
var address16Schema = import_zod2.z.preprocess(parseAddress16, import_zod2.z.number().int().min(0).max(65535)).describe("16-bit C64 address: decimal (53248) or hex string with prefix ($D000, 0xD000)");
|
|
217
|
+
function parseByte(input) {
|
|
218
|
+
if (typeof input === "number") {
|
|
219
|
+
if (!Number.isInteger(input) || input < 0 || input > 255) {
|
|
220
|
+
throw new import_zod2.z.ZodError([
|
|
221
|
+
{
|
|
222
|
+
code: "custom",
|
|
223
|
+
message: `Byte value must be an integer between 0 and 255 (0xFF), got ${input}`,
|
|
224
|
+
path: []
|
|
225
|
+
}
|
|
226
|
+
]);
|
|
227
|
+
}
|
|
228
|
+
return input;
|
|
229
|
+
}
|
|
230
|
+
if (typeof input !== "string") {
|
|
231
|
+
throw new import_zod2.z.ZodError([
|
|
232
|
+
{
|
|
233
|
+
code: "custom",
|
|
234
|
+
message: `Byte value must be a number or string, got ${typeof input}`,
|
|
235
|
+
path: []
|
|
236
|
+
}
|
|
237
|
+
]);
|
|
238
|
+
}
|
|
239
|
+
const trimmed = input.trim();
|
|
240
|
+
if (trimmed.startsWith("$")) {
|
|
241
|
+
const hexString = trimmed.slice(1);
|
|
242
|
+
if (!/^[0-9A-Fa-f]{1,2}$/.test(hexString)) {
|
|
243
|
+
throw new import_zod2.z.ZodError([
|
|
244
|
+
{
|
|
245
|
+
code: "custom",
|
|
246
|
+
message: `Invalid C64 hex ($) byte value: "${input}". Hex portion must be 1-2 hex digits (0-9, A-F)`,
|
|
247
|
+
path: []
|
|
248
|
+
}
|
|
249
|
+
]);
|
|
250
|
+
}
|
|
251
|
+
const parsed = parseInt(hexString, 16);
|
|
252
|
+
if (isNaN(parsed) || parsed < 0 || parsed > 255) {
|
|
253
|
+
throw new import_zod2.z.ZodError([
|
|
254
|
+
{
|
|
255
|
+
code: "custom",
|
|
256
|
+
message: `Byte value out of range: "${input}" (${parsed}). Must be 0x00-0xFF (0-255)`,
|
|
257
|
+
path: []
|
|
258
|
+
}
|
|
259
|
+
]);
|
|
260
|
+
}
|
|
261
|
+
return parsed;
|
|
262
|
+
}
|
|
263
|
+
if (trimmed.toLowerCase().startsWith("0x")) {
|
|
264
|
+
const hexString = trimmed.slice(2);
|
|
265
|
+
if (!/^[0-9A-Fa-f]{1,2}$/.test(hexString)) {
|
|
266
|
+
throw new import_zod2.z.ZodError([
|
|
267
|
+
{
|
|
268
|
+
code: "custom",
|
|
269
|
+
message: `Invalid C hex (0x) byte value: "${input}". Hex portion must be 1-2 hex digits (0-9, A-F)`,
|
|
270
|
+
path: []
|
|
271
|
+
}
|
|
272
|
+
]);
|
|
273
|
+
}
|
|
274
|
+
const parsed = parseInt(hexString, 16);
|
|
275
|
+
if (isNaN(parsed) || parsed < 0 || parsed > 255) {
|
|
276
|
+
throw new import_zod2.z.ZodError([
|
|
277
|
+
{
|
|
278
|
+
code: "custom",
|
|
279
|
+
message: `Byte value out of range: "${input}" (${parsed}). Must be 0x00-0xFF (0-255)`,
|
|
280
|
+
path: []
|
|
281
|
+
}
|
|
282
|
+
]);
|
|
283
|
+
}
|
|
284
|
+
return parsed;
|
|
285
|
+
}
|
|
286
|
+
if (trimmed.startsWith("%")) {
|
|
287
|
+
const binString = trimmed.slice(1);
|
|
288
|
+
if (!/^[01]{1,8}$/.test(binString)) {
|
|
289
|
+
throw new import_zod2.z.ZodError([
|
|
290
|
+
{
|
|
291
|
+
code: "custom",
|
|
292
|
+
message: `Invalid C64 binary (%) byte value: "${input}". Binary portion must be 1-8 binary digits (0-1)`,
|
|
293
|
+
path: []
|
|
294
|
+
}
|
|
295
|
+
]);
|
|
296
|
+
}
|
|
297
|
+
const parsed = parseInt(binString, 2);
|
|
298
|
+
if (isNaN(parsed) || parsed < 0 || parsed > 255) {
|
|
299
|
+
throw new import_zod2.z.ZodError([
|
|
300
|
+
{
|
|
301
|
+
code: "custom",
|
|
302
|
+
message: `Byte value out of range: "${input}" (${parsed}). Must be 0b00000000-0b11111111 (0-255)`,
|
|
303
|
+
path: []
|
|
304
|
+
}
|
|
305
|
+
]);
|
|
306
|
+
}
|
|
307
|
+
return parsed;
|
|
308
|
+
}
|
|
309
|
+
if (trimmed.toLowerCase().startsWith("0b")) {
|
|
310
|
+
const binString = trimmed.slice(2);
|
|
311
|
+
if (!/^[01]{1,8}$/.test(binString)) {
|
|
312
|
+
throw new import_zod2.z.ZodError([
|
|
313
|
+
{
|
|
314
|
+
code: "custom",
|
|
315
|
+
message: `Invalid C binary (0b) byte value: "${input}". Binary portion must be 1-8 binary digits (0-1)`,
|
|
316
|
+
path: []
|
|
317
|
+
}
|
|
318
|
+
]);
|
|
319
|
+
}
|
|
320
|
+
const parsed = parseInt(binString, 2);
|
|
321
|
+
if (isNaN(parsed) || parsed < 0 || parsed > 255) {
|
|
322
|
+
throw new import_zod2.z.ZodError([
|
|
323
|
+
{
|
|
324
|
+
code: "custom",
|
|
325
|
+
message: `Byte value out of range: "${input}" (${parsed}). Must be 0b00000000-0b11111111 (0-255)`,
|
|
326
|
+
path: []
|
|
327
|
+
}
|
|
328
|
+
]);
|
|
329
|
+
}
|
|
330
|
+
return parsed;
|
|
331
|
+
}
|
|
332
|
+
throw new import_zod2.z.ZodError([
|
|
333
|
+
{
|
|
334
|
+
code: "custom",
|
|
335
|
+
message: `Invalid byte format: "${input}". Expected formats: decimal (255), hex with prefix ($FF, 0xFF), or binary with prefix (%11111111, 0b11111111). Bare hex/binary not supported to avoid ambiguity.`,
|
|
336
|
+
path: []
|
|
337
|
+
}
|
|
338
|
+
]);
|
|
339
|
+
}
|
|
340
|
+
var byteValueSchema = import_zod2.z.preprocess(parseByte, import_zod2.z.number().int().min(0).max(255)).describe("8-bit byte value: decimal (255), hex with prefix ($FF, 0xFF), or binary with prefix (%11111111, 0b11111111)");
|
|
341
|
+
var byteArraySchema = import_zod2.z.array(byteValueSchema).describe('Array of byte values in mixed formats: [255, "$FF", "%11111111", 42]');
|
|
158
342
|
var c64RegisterValueSchema = import_zod2.z.object(
|
|
159
343
|
Object.fromEntries(
|
|
160
344
|
C64_REGISTER_DEFINITIONS.map((register) => [
|
|
@@ -391,10 +575,15 @@ function publicMessageFor(error) {
|
|
|
391
575
|
case "program_file_missing":
|
|
392
576
|
case "program_file_invalid":
|
|
393
577
|
return error.message;
|
|
578
|
+
case "binary_not_found":
|
|
579
|
+
case "spawn_failed":
|
|
580
|
+
case "emulator_crashed_on_startup":
|
|
581
|
+
return error.message;
|
|
394
582
|
case "port_allocation_failed":
|
|
395
583
|
case "port_in_use":
|
|
396
|
-
case "monitor_timeout":
|
|
397
584
|
return "The server could not start a usable emulator session. Check the emulator configuration and try again.";
|
|
585
|
+
case "monitor_timeout":
|
|
586
|
+
return error.message;
|
|
398
587
|
case "not_connected":
|
|
399
588
|
case "connection_closed":
|
|
400
589
|
case "socket_write_failed":
|
|
@@ -425,14 +614,21 @@ function publicMessageFor(error) {
|
|
|
425
614
|
}
|
|
426
615
|
}
|
|
427
616
|
function publicDetailsFor(error) {
|
|
428
|
-
switch (error.
|
|
429
|
-
case "
|
|
430
|
-
case "
|
|
431
|
-
case "
|
|
432
|
-
case "io":
|
|
617
|
+
switch (error.code) {
|
|
618
|
+
case "binary_not_found":
|
|
619
|
+
case "spawn_failed":
|
|
620
|
+
case "emulator_crashed_on_startup":
|
|
433
621
|
return error.details;
|
|
434
622
|
default:
|
|
435
|
-
|
|
623
|
+
switch (error.category) {
|
|
624
|
+
case "validation":
|
|
625
|
+
case "session_state":
|
|
626
|
+
case "unsupported":
|
|
627
|
+
case "io":
|
|
628
|
+
return error.details;
|
|
629
|
+
default:
|
|
630
|
+
return void 0;
|
|
631
|
+
}
|
|
436
632
|
}
|
|
437
633
|
}
|
|
438
634
|
function normalizeToolError(error) {
|
|
@@ -1550,6 +1746,10 @@ var ViceSession = class {
|
|
|
1550
1746
|
this.#syncMonitorRuntimeState();
|
|
1551
1747
|
});
|
|
1552
1748
|
}
|
|
1749
|
+
async getSessionState() {
|
|
1750
|
+
await this.#ensureReady();
|
|
1751
|
+
return this.snapshot();
|
|
1752
|
+
}
|
|
1553
1753
|
snapshot() {
|
|
1554
1754
|
return {
|
|
1555
1755
|
transportState: this.#transportState,
|
|
@@ -1786,6 +1986,17 @@ var ViceSession = class {
|
|
|
1786
1986
|
async continueExecution(waitUntilRunningStable = false) {
|
|
1787
1987
|
return this.#withExecutionLock(async () => {
|
|
1788
1988
|
await this.#ensureReady();
|
|
1989
|
+
if (this.#executionState === "running") {
|
|
1990
|
+
const runtime2 = this.#client.runtimeState();
|
|
1991
|
+
const debugState2 = this.#lastRegisters == null ? await this.#readDebugState() : this.#buildDebugState(this.#lastRegisters);
|
|
1992
|
+
return {
|
|
1993
|
+
executionState: "running",
|
|
1994
|
+
lastStopReason: this.#lastStopReason,
|
|
1995
|
+
programCounter: runtime2.programCounter ?? debugState2.programCounter,
|
|
1996
|
+
registers: debugState2.registers,
|
|
1997
|
+
warnings: []
|
|
1998
|
+
};
|
|
1999
|
+
}
|
|
1789
2000
|
if (this.#executionState !== "stopped") {
|
|
1790
2001
|
debuggerNotPausedError("execute resume", {
|
|
1791
2002
|
executionState: this.#executionState,
|
|
@@ -1964,7 +2175,6 @@ var ViceSession = class {
|
|
|
1964
2175
|
async programLoad(options) {
|
|
1965
2176
|
const filePath = import_node_path.default.resolve(options.filePath);
|
|
1966
2177
|
await this.#assertReadableProgramFile(filePath);
|
|
1967
|
-
await this.#ensureRunning("program_load");
|
|
1968
2178
|
this.#explicitPauseActive = false;
|
|
1969
2179
|
const result = await this.autostartProgram(filePath, options.autoStart ?? true, options.fileIndex ?? 0);
|
|
1970
2180
|
return {
|
|
@@ -2215,134 +2425,137 @@ var ViceSession = class {
|
|
|
2215
2425
|
}
|
|
2216
2426
|
}
|
|
2217
2427
|
async writeText(text) {
|
|
2218
|
-
await this.#
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2428
|
+
return await this.#withAutoResumeForInput("write_text", async () => {
|
|
2429
|
+
const encoded = decodeWriteTextToPetscii(text);
|
|
2430
|
+
if (encoded.length > MAX_WRITE_TEXT_BYTES) {
|
|
2431
|
+
validationError("write_text exceeds the maximum allowed byte length for one request", {
|
|
2432
|
+
length: encoded.length,
|
|
2433
|
+
max: MAX_WRITE_TEXT_BYTES
|
|
2434
|
+
});
|
|
2435
|
+
}
|
|
2436
|
+
this.#writeProcessLogLine(`[tx] write_text length=${encoded.length} text=${JSON.stringify(text)}`);
|
|
2437
|
+
await this.#client.sendKeys(Buffer.from(encoded).toString("binary"));
|
|
2438
|
+
await this.#settleInputState("write_text", "running");
|
|
2439
|
+
return {
|
|
2440
|
+
sent: true,
|
|
2441
|
+
length: encoded.length
|
|
2442
|
+
};
|
|
2443
|
+
});
|
|
2233
2444
|
}
|
|
2234
2445
|
async keyboardInput(action, keys, durationMs) {
|
|
2235
|
-
await this.#
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
}
|
|
2239
|
-
const resolvedKeys = keys.map((key) => resolveKeyboardInputKey(key));
|
|
2240
|
-
const normalizedKeys = resolvedKeys.map((key) => key.canonical);
|
|
2241
|
-
this.#writeProcessLogLine(
|
|
2242
|
-
`[tx] keyboard_input action=${action} keys=${normalizedKeys.join(",")}${durationMs == null ? "" : ` durationMs=${durationMs}`}`
|
|
2243
|
-
);
|
|
2244
|
-
switch (action) {
|
|
2245
|
-
case "tap": {
|
|
2246
|
-
const duration = clampTapDuration(durationMs);
|
|
2247
|
-
const bytes = Uint8Array.from(resolvedKeys.flatMap((key) => Array.from(key.bytes)));
|
|
2248
|
-
await this.#client.sendKeys(Buffer.from(bytes).toString("binary"));
|
|
2249
|
-
await this.#settleInputState("keyboard_input", "running");
|
|
2250
|
-
await sleep(duration);
|
|
2251
|
-
return {
|
|
2252
|
-
action,
|
|
2253
|
-
keys: normalizedKeys,
|
|
2254
|
-
applied: true,
|
|
2255
|
-
held: false,
|
|
2256
|
-
mode: "buffered_text"
|
|
2257
|
-
};
|
|
2446
|
+
return await this.#withAutoResumeForInput("keyboard_input", async () => {
|
|
2447
|
+
if (!Array.isArray(keys) || keys.length === 0 || keys.length > 4) {
|
|
2448
|
+
validationError("keyboard_input requires between 1 and 4 keys", { keys });
|
|
2258
2449
|
}
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
}
|
|
2450
|
+
const resolvedKeys = keys.map((key) => resolveKeyboardInputKey(key));
|
|
2451
|
+
const normalizedKeys = resolvedKeys.map((key) => key.canonical);
|
|
2452
|
+
this.#writeProcessLogLine(
|
|
2453
|
+
`[tx] keyboard_input action=${action} keys=${normalizedKeys.join(",")}${durationMs == null ? "" : ` durationMs=${durationMs}`}`
|
|
2454
|
+
);
|
|
2455
|
+
switch (action) {
|
|
2456
|
+
case "tap": {
|
|
2457
|
+
const duration = clampTapDuration(durationMs);
|
|
2458
|
+
const bytes = Uint8Array.from(resolvedKeys.flatMap((key) => Array.from(key.bytes)));
|
|
2459
|
+
await this.#client.sendKeys(Buffer.from(bytes).toString("binary"));
|
|
2460
|
+
await this.#settleInputState("keyboard_input", "running");
|
|
2461
|
+
await sleep(duration);
|
|
2462
|
+
return {
|
|
2463
|
+
action,
|
|
2464
|
+
keys: normalizedKeys,
|
|
2465
|
+
applied: true,
|
|
2466
|
+
held: false,
|
|
2467
|
+
mode: "buffered_text"
|
|
2468
|
+
};
|
|
2279
2469
|
}
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2470
|
+
case "press": {
|
|
2471
|
+
const singleByteKeys = resolvedKeys.map((key) => {
|
|
2472
|
+
if (key.bytes.length !== 1) {
|
|
2473
|
+
unsupportedError("keyboard_input press/release only supports keys that map to a single PETSCII byte.", {
|
|
2474
|
+
key: key.canonical
|
|
2475
|
+
});
|
|
2476
|
+
}
|
|
2477
|
+
return key.bytes[0];
|
|
2478
|
+
});
|
|
2479
|
+
for (let index = 0; index < normalizedKeys.length; index += 1) {
|
|
2480
|
+
const heldKey = normalizedKeys[index];
|
|
2481
|
+
const byte = singleByteKeys[index];
|
|
2482
|
+
if (!this.#heldKeyboardIntervals.has(heldKey)) {
|
|
2483
|
+
await this.#client.sendKeys(Buffer.from([byte]).toString("binary"));
|
|
2484
|
+
await this.#settleInputState("keyboard_input", "running");
|
|
2485
|
+
const interval = setInterval(() => {
|
|
2486
|
+
void this.#client.sendKeys(Buffer.from([byte]).toString("binary")).then(() => this.#settleInputState("keyboard_input", "running")).catch(() => void 0);
|
|
2487
|
+
}, DEFAULT_KEYBOARD_REPEAT_MS);
|
|
2488
|
+
this.#heldKeyboardIntervals.set(heldKey, interval);
|
|
2489
|
+
}
|
|
2294
2490
|
}
|
|
2491
|
+
return {
|
|
2492
|
+
action,
|
|
2493
|
+
keys: normalizedKeys,
|
|
2494
|
+
applied: true,
|
|
2495
|
+
held: true,
|
|
2496
|
+
mode: "buffered_text_repeat"
|
|
2497
|
+
};
|
|
2295
2498
|
}
|
|
2296
|
-
|
|
2297
|
-
const
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2499
|
+
case "release": {
|
|
2500
|
+
for (const key of resolvedKeys) {
|
|
2501
|
+
if (key.bytes.length !== 1) {
|
|
2502
|
+
unsupportedError("keyboard_input press/release only supports keys that map to a single PETSCII byte.", {
|
|
2503
|
+
key: key.canonical
|
|
2504
|
+
});
|
|
2505
|
+
}
|
|
2301
2506
|
}
|
|
2507
|
+
for (const heldKey of normalizedKeys) {
|
|
2508
|
+
const interval = this.#heldKeyboardIntervals.get(heldKey);
|
|
2509
|
+
if (interval) {
|
|
2510
|
+
clearInterval(interval);
|
|
2511
|
+
this.#heldKeyboardIntervals.delete(heldKey);
|
|
2512
|
+
}
|
|
2513
|
+
}
|
|
2514
|
+
return {
|
|
2515
|
+
action,
|
|
2516
|
+
keys: normalizedKeys,
|
|
2517
|
+
applied: true,
|
|
2518
|
+
held: false,
|
|
2519
|
+
mode: "buffered_text_repeat"
|
|
2520
|
+
};
|
|
2302
2521
|
}
|
|
2303
|
-
return {
|
|
2304
|
-
action,
|
|
2305
|
-
keys: normalizedKeys,
|
|
2306
|
-
applied: true,
|
|
2307
|
-
held: false,
|
|
2308
|
-
mode: "buffered_text_repeat"
|
|
2309
|
-
};
|
|
2310
2522
|
}
|
|
2311
|
-
}
|
|
2523
|
+
});
|
|
2312
2524
|
}
|
|
2313
2525
|
async joystickInput(port2, action, control, durationMs) {
|
|
2314
|
-
await this.#
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
}
|
|
2320
|
-
this.#writeProcessLogLine(
|
|
2321
|
-
`[tx] joystick_input port=${port2} action=${action} control=${control}${durationMs == null ? "" : ` durationMs=${durationMs}`}`
|
|
2322
|
-
);
|
|
2323
|
-
switch (action) {
|
|
2324
|
-
case "tap": {
|
|
2325
|
-
const duration = clampTapDuration(durationMs);
|
|
2326
|
-
await this.#applyJoystickMask(port2, this.#getJoystickMask(port2) & ~bit);
|
|
2327
|
-
await sleep(duration);
|
|
2328
|
-
await this.#applyJoystickMask(port2, this.#getJoystickMask(port2) | bit);
|
|
2329
|
-
break;
|
|
2526
|
+
return await this.#withAutoResumeForInput("joystick_input", async () => {
|
|
2527
|
+
const previousExecutionState = this.#executionState;
|
|
2528
|
+
const bit = JOYSTICK_CONTROL_BITS[control];
|
|
2529
|
+
if (bit == null) {
|
|
2530
|
+
validationError("Unsupported joystick control", { control });
|
|
2330
2531
|
}
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2532
|
+
this.#writeProcessLogLine(
|
|
2533
|
+
`[tx] joystick_input port=${port2} action=${action} control=${control}${durationMs == null ? "" : ` durationMs=${durationMs}`}`
|
|
2534
|
+
);
|
|
2535
|
+
switch (action) {
|
|
2536
|
+
case "tap": {
|
|
2537
|
+
const duration = clampTapDuration(durationMs);
|
|
2538
|
+
await this.#applyJoystickMask(port2, this.#getJoystickMask(port2) & ~bit);
|
|
2539
|
+
await sleep(duration);
|
|
2540
|
+
await this.#applyJoystickMask(port2, this.#getJoystickMask(port2) | bit);
|
|
2541
|
+
break;
|
|
2542
|
+
}
|
|
2543
|
+
case "press":
|
|
2544
|
+
await this.#applyJoystickMask(port2, this.#getJoystickMask(port2) & ~bit);
|
|
2545
|
+
break;
|
|
2546
|
+
case "release":
|
|
2547
|
+
await this.#applyJoystickMask(port2, this.#getJoystickMask(port2) | bit);
|
|
2548
|
+
break;
|
|
2549
|
+
}
|
|
2550
|
+
await this.#settleInputState("joystick_input", previousExecutionState);
|
|
2551
|
+
return {
|
|
2552
|
+
port: port2,
|
|
2553
|
+
action,
|
|
2554
|
+
control,
|
|
2555
|
+
applied: true,
|
|
2556
|
+
state: this.#describeJoystickState(port2)
|
|
2557
|
+
};
|
|
2558
|
+
});
|
|
2346
2559
|
}
|
|
2347
2560
|
async waitForState(targetState, timeoutMs = 5e3, stableMs = targetState === "running" ? INPUT_RUNNING_STABLE_MS : 0) {
|
|
2348
2561
|
await this.#ensureReady();
|
|
@@ -2405,6 +2618,39 @@ var ViceSession = class {
|
|
|
2405
2618
|
});
|
|
2406
2619
|
}
|
|
2407
2620
|
}
|
|
2621
|
+
async #withAutoResumeForInput(commandName, operation) {
|
|
2622
|
+
await this.#ensureReady();
|
|
2623
|
+
this.#syncMonitorRuntimeState();
|
|
2624
|
+
if (this.#executionState === "unknown") {
|
|
2625
|
+
throw new ViceMcpError(
|
|
2626
|
+
"emulator_state_unknown",
|
|
2627
|
+
`${commandName} requires a known execution state (running or stopped)`,
|
|
2628
|
+
"session_state",
|
|
2629
|
+
false,
|
|
2630
|
+
{
|
|
2631
|
+
executionState: this.#executionState,
|
|
2632
|
+
lastStopReason: this.#lastStopReason
|
|
2633
|
+
}
|
|
2634
|
+
);
|
|
2635
|
+
}
|
|
2636
|
+
const wasRunning = this.#executionState === "running";
|
|
2637
|
+
const wasPaused = this.#explicitPauseActive;
|
|
2638
|
+
if (this.#executionState === "stopped") {
|
|
2639
|
+
this.#writeProcessLogLine(`[${commandName}] auto-resuming for input operation`);
|
|
2640
|
+
await this.#client.continueExecution();
|
|
2641
|
+
await this.waitForState("running", 5e3, INPUT_RUNNING_STABLE_MS);
|
|
2642
|
+
}
|
|
2643
|
+
try {
|
|
2644
|
+
return await operation();
|
|
2645
|
+
} finally {
|
|
2646
|
+
if (!wasRunning && wasPaused) {
|
|
2647
|
+
this.#writeProcessLogLine(`[${commandName}] restoring paused state after input`);
|
|
2648
|
+
await this.#client.ping();
|
|
2649
|
+
await this.waitForState("stopped", 5e3, 0);
|
|
2650
|
+
this.#explicitPauseActive = true;
|
|
2651
|
+
}
|
|
2652
|
+
}
|
|
2653
|
+
}
|
|
2408
2654
|
async #ensureHealthyConnection() {
|
|
2409
2655
|
if (this.#recoveryPromise) {
|
|
2410
2656
|
await this.#recoveryPromise;
|
|
@@ -2453,6 +2699,16 @@ var ViceSession = class {
|
|
|
2453
2699
|
const port2 = await this.#portAllocator.allocate();
|
|
2454
2700
|
await this.#portAllocator.ensureFree(port2, host2);
|
|
2455
2701
|
const binary = config.binaryPath ?? DEFAULT_C64_BINARY;
|
|
2702
|
+
const binaryCheck = await checkBinaryExists(binary);
|
|
2703
|
+
if (!binaryCheck.exists) {
|
|
2704
|
+
throw new ViceMcpError(
|
|
2705
|
+
"binary_not_found",
|
|
2706
|
+
`VICE emulator binary '${binary}' not found. Please install VICE or configure the correct path using the 'binaryPath' setting.`,
|
|
2707
|
+
"process_launch",
|
|
2708
|
+
false,
|
|
2709
|
+
{ binary, searchedPath: process.env.PATH }
|
|
2710
|
+
);
|
|
2711
|
+
}
|
|
2456
2712
|
const args = ["-autostartprgmode", "1", "-binarymonitor", "-binarymonitoraddress", `${host2}:${port2}`];
|
|
2457
2713
|
if (config.arguments) {
|
|
2458
2714
|
args.push(...splitCommandLine(config.arguments));
|
|
@@ -2470,11 +2726,15 @@ var ViceSession = class {
|
|
|
2470
2726
|
this.#lastRuntimeEventType = "unknown";
|
|
2471
2727
|
this.#lastRuntimeProgramCounter = null;
|
|
2472
2728
|
const env = await buildViceLaunchEnv();
|
|
2729
|
+
let spawnError = void 0;
|
|
2473
2730
|
const child = (0, import_node_child_process.spawn)(binary, args, {
|
|
2474
2731
|
cwd: config.workingDirectory ? import_node_path.default.resolve(config.workingDirectory) : void 0,
|
|
2475
2732
|
env,
|
|
2476
2733
|
stdio: ["ignore", "pipe", "pipe"]
|
|
2477
2734
|
});
|
|
2735
|
+
child.once("error", (err) => {
|
|
2736
|
+
spawnError = err;
|
|
2737
|
+
});
|
|
2478
2738
|
this.#process = child;
|
|
2479
2739
|
this.#attachProcessLogging(child, binary, args);
|
|
2480
2740
|
this.#bindProcessLifecycle(child);
|
|
@@ -2482,6 +2742,16 @@ var ViceSession = class {
|
|
|
2482
2742
|
this.#transportState = "waiting_for_monitor";
|
|
2483
2743
|
try {
|
|
2484
2744
|
await waitForMonitor(host2, port2, 5e3);
|
|
2745
|
+
if (spawnError !== void 0) {
|
|
2746
|
+
const err = spawnError;
|
|
2747
|
+
throw new ViceMcpError(
|
|
2748
|
+
"spawn_failed",
|
|
2749
|
+
`Failed to start VICE emulator '${binary}': ${err.message}`,
|
|
2750
|
+
"process_launch",
|
|
2751
|
+
false,
|
|
2752
|
+
{ binary, error: err.message, resolvedPath: binaryCheck.path }
|
|
2753
|
+
);
|
|
2754
|
+
}
|
|
2485
2755
|
await this.#client.connect(host2, port2);
|
|
2486
2756
|
this.#transportState = "connected";
|
|
2487
2757
|
this.#connectedSince = nowIso();
|
|
@@ -2494,6 +2764,19 @@ var ViceSession = class {
|
|
|
2494
2764
|
} catch (error) {
|
|
2495
2765
|
this.#processState = "crashed";
|
|
2496
2766
|
this.#transportState = "faulted";
|
|
2767
|
+
if (error instanceof ViceMcpError && error.code === "monitor_timeout" && spawnError !== void 0) {
|
|
2768
|
+
const err = spawnError;
|
|
2769
|
+
const enhancedError = new ViceMcpError(
|
|
2770
|
+
"emulator_crashed_on_startup",
|
|
2771
|
+
`VICE emulator '${binary}' crashed during startup: ${err.message}`,
|
|
2772
|
+
"process_launch",
|
|
2773
|
+
false,
|
|
2774
|
+
{ binary, error: err.message, resolvedPath: binaryCheck.path }
|
|
2775
|
+
);
|
|
2776
|
+
this.#warnings = [...this.#warnings.filter((warning) => warning.code !== "launch_failed"), makeWarning(enhancedError.message, "launch_failed")];
|
|
2777
|
+
await this.#stopManagedProcess(true);
|
|
2778
|
+
throw enhancedError;
|
|
2779
|
+
}
|
|
2497
2780
|
this.#warnings = [...this.#warnings.filter((warning) => warning.code !== "launch_failed"), makeWarning(String(error.message ?? error), "launch_failed")];
|
|
2498
2781
|
await this.#stopManagedProcess(true);
|
|
2499
2782
|
throw error;
|
|
@@ -3234,6 +3517,27 @@ function splitCommandLine(input) {
|
|
|
3234
3517
|
}
|
|
3235
3518
|
return result;
|
|
3236
3519
|
}
|
|
3520
|
+
async function checkBinaryExists(binaryPath) {
|
|
3521
|
+
if (import_node_path.default.isAbsolute(binaryPath)) {
|
|
3522
|
+
try {
|
|
3523
|
+
await import_promises.default.access(binaryPath, import_promises.default.constants.X_OK);
|
|
3524
|
+
return { exists: true, path: binaryPath };
|
|
3525
|
+
} catch {
|
|
3526
|
+
return { exists: false };
|
|
3527
|
+
}
|
|
3528
|
+
}
|
|
3529
|
+
const pathEnv = process.env.PATH || "";
|
|
3530
|
+
const pathDirs = pathEnv.split(import_node_path.default.delimiter);
|
|
3531
|
+
for (const dir of pathDirs) {
|
|
3532
|
+
const fullPath = import_node_path.default.join(dir, binaryPath);
|
|
3533
|
+
try {
|
|
3534
|
+
await import_promises.default.access(fullPath, import_promises.default.constants.X_OK);
|
|
3535
|
+
return { exists: true, path: fullPath };
|
|
3536
|
+
} catch {
|
|
3537
|
+
}
|
|
3538
|
+
}
|
|
3539
|
+
return { exists: false };
|
|
3540
|
+
}
|
|
3237
3541
|
async function waitForMonitor(host2, port2, timeoutMs) {
|
|
3238
3542
|
const deadline = Date.now() + timeoutMs;
|
|
3239
3543
|
while (Date.now() < deadline) {
|
|
@@ -3242,7 +3546,7 @@ async function waitForMonitor(host2, port2, timeoutMs) {
|
|
|
3242
3546
|
}
|
|
3243
3547
|
await sleep(100);
|
|
3244
3548
|
}
|
|
3245
|
-
throw new ViceMcpError("monitor_timeout", `Debugger monitor did not open on ${host2}:${port2}
|
|
3549
|
+
throw new ViceMcpError("monitor_timeout", `Debugger monitor did not open on ${host2}:${port2}. The emulator may have failed to start or crashed during startup.`, "timeout", true, {
|
|
3246
3550
|
host: host2,
|
|
3247
3551
|
port: port2
|
|
3248
3552
|
});
|
|
@@ -3307,7 +3611,7 @@ var getSessionStateTool = createViceTool({
|
|
|
3307
3611
|
description: "Returns emulator session state including transport/process status, auto-resume state, and the most recent hit checkpoint.",
|
|
3308
3612
|
inputSchema: noInputSchema,
|
|
3309
3613
|
dataSchema: sessionStateResultSchema,
|
|
3310
|
-
execute: async () => c64Session.
|
|
3614
|
+
execute: async () => await c64Session.getSessionState()
|
|
3311
3615
|
});
|
|
3312
3616
|
var getRegistersTool = createViceTool({
|
|
3313
3617
|
id: "get_registers",
|
|
@@ -3332,9 +3636,9 @@ var setRegistersTool = createViceTool({
|
|
|
3332
3636
|
});
|
|
3333
3637
|
var readMemoryTool = createViceTool({
|
|
3334
3638
|
id: "memory_read",
|
|
3335
|
-
description: "Reads a memory chunk by start address and length
|
|
3639
|
+
description: "Reads a memory chunk by start address and length. Address can be decimal (53248) or hex string with prefix ($D000, 0xD000). Returns byte values as decimal numbers.",
|
|
3336
3640
|
inputSchema: import_zod4.z.object({
|
|
3337
|
-
address: address16Schema.describe("Start address
|
|
3641
|
+
address: address16Schema.describe("Start address: decimal (53248) or hex string with prefix ($D000, 0xD000)"),
|
|
3338
3642
|
length: import_zod4.z.number().int().positive().max(65535).describe("Size of the data chunk to read in bytes")
|
|
3339
3643
|
}).refine((input) => input.address + input.length <= 65536, {
|
|
3340
3644
|
message: "address + length must stay within the 64K address space",
|
|
@@ -3356,10 +3660,10 @@ var readMemoryTool = createViceTool({
|
|
|
3356
3660
|
});
|
|
3357
3661
|
var writeMemoryTool = createViceTool({
|
|
3358
3662
|
id: "memory_write",
|
|
3359
|
-
description:
|
|
3663
|
+
description: "Writes raw byte values into C64 memory. Address and byte values support decimal, hex ($FF, 0xFF), and binary (%11111111, 0b11111111) formats. Requires emulator to be stopped.",
|
|
3360
3664
|
inputSchema: import_zod4.z.object({
|
|
3361
|
-
address: address16Schema.describe("Start address
|
|
3362
|
-
data: byteArraySchema.min(1).describe("
|
|
3665
|
+
address: address16Schema.describe("Start address: decimal (53248) or hex string with prefix ($D000, 0xD000)"),
|
|
3666
|
+
data: byteArraySchema.min(1).describe("Bytes to write: decimal (255), hex ($FF, 0xFF), or binary (%11111111, 0b11111111). Mixed formats allowed.")
|
|
3363
3667
|
}).refine((input) => input.address + input.data.length - 1 <= 65535, {
|
|
3364
3668
|
message: "address + data.length must stay within the 16-bit address space",
|
|
3365
3669
|
path: ["data"]
|
|
@@ -3373,7 +3677,7 @@ var writeMemoryTool = createViceTool({
|
|
|
3373
3677
|
});
|
|
3374
3678
|
var executeTool = createViceTool({
|
|
3375
3679
|
id: "execute",
|
|
3376
|
-
description: "Controls execution with pause, resume, step, step_over, step_out, or reset.
|
|
3680
|
+
description: "Controls execution with pause, resume, step, step_over, step_out, or reset. Pause and resume are idempotent (safe to call multiple times).",
|
|
3377
3681
|
inputSchema: import_zod4.z.object({
|
|
3378
3682
|
action: import_zod4.z.enum(["pause", "resume", "step", "step_over", "step_out", "reset"]),
|
|
3379
3683
|
count: import_zod4.z.number().int().positive().default(1).describe("Instruction count for step and step_over actions"),
|
|
@@ -3415,10 +3719,10 @@ var listBreakpointsTool = createViceTool({
|
|
|
3415
3719
|
});
|
|
3416
3720
|
var breakpointSetTool = createViceTool({
|
|
3417
3721
|
id: "breakpoint_set",
|
|
3418
|
-
description: "Creates an execution breakpoint or read/write watchpoint.",
|
|
3722
|
+
description: "Creates an execution breakpoint or read/write watchpoint. Address can be decimal (53248) or hex string with prefix ($D000, 0xD000).",
|
|
3419
3723
|
inputSchema: import_zod4.z.object({
|
|
3420
3724
|
kind: breakpointKindSchema,
|
|
3421
|
-
address: address16Schema.describe("Start address
|
|
3725
|
+
address: address16Schema.describe("Start address: decimal (53248) or hex string with prefix ($D000, 0xD000)"),
|
|
3422
3726
|
length: import_zod4.z.number().int().positive().default(1).describe("Size of the breakpoint range in bytes"),
|
|
3423
3727
|
condition: import_zod4.z.string().optional(),
|
|
3424
3728
|
label: import_zod4.z.string().optional(),
|
|
@@ -3495,7 +3799,7 @@ var getDisplayTextTool = createViceTool({
|
|
|
3495
3799
|
});
|
|
3496
3800
|
var writeTextTool = createViceTool({
|
|
3497
3801
|
id: "write_text",
|
|
3498
|
-
description:
|
|
3802
|
+
description: "Types text into the C64. Automatically resumes if stopped and restores pause state after. Supports escaped characters and PETSCII brace tokens like {RETURN}, {CLR}, {HOME}, {PI}, and color names. Limit 64 bytes per request.",
|
|
3499
3803
|
inputSchema: import_zod4.z.object({
|
|
3500
3804
|
text: import_zod4.z.string()
|
|
3501
3805
|
}),
|
|
@@ -3507,7 +3811,7 @@ var writeTextTool = createViceTool({
|
|
|
3507
3811
|
});
|
|
3508
3812
|
var keyboardInputTool = createViceTool({
|
|
3509
3813
|
id: "keyboard_input",
|
|
3510
|
-
description:
|
|
3814
|
+
description: "Sends one to four keys or PETSCII tokens to the C64. Automatically resumes if stopped and restores pause state after. Use for key presses, releases, and taps.",
|
|
3511
3815
|
inputSchema: import_zod4.z.object({
|
|
3512
3816
|
action: inputActionSchema.describe("Use tap for a single key event or press/release for repeated buffered input"),
|
|
3513
3817
|
keys: import_zod4.z.array(import_zod4.z.string().min(1)).min(1).max(4).describe("One to four literal keys or PETSCII token names such as RETURN, CLR, HOME, PI, LEFT, RED, or F1"),
|
|
@@ -3518,7 +3822,7 @@ var keyboardInputTool = createViceTool({
|
|
|
3518
3822
|
});
|
|
3519
3823
|
var joystickInputTool = createViceTool({
|
|
3520
3824
|
id: "joystick_input",
|
|
3521
|
-
description:
|
|
3825
|
+
description: "Sends joystick input to C64 joystick port 1 or 2. Automatically resumes if stopped and restores pause state after.",
|
|
3522
3826
|
inputSchema: import_zod4.z.object({
|
|
3523
3827
|
port: joystickPortSchema.describe("Joystick port number"),
|
|
3524
3828
|
action: inputActionSchema.describe("Joystick action to apply"),
|