c64-debug-mcp 1.0.1 → 1.0.6
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/dist/http.cjs +280 -130
- package/dist/http.js +280 -130
- package/dist/stdio.cjs +280 -130
- package/dist/stdio.js +280 -130
- package/package.json +1 -1
package/dist/http.cjs
CHANGED
|
@@ -391,10 +391,15 @@ function publicMessageFor(error) {
|
|
|
391
391
|
case "program_file_missing":
|
|
392
392
|
case "program_file_invalid":
|
|
393
393
|
return error.message;
|
|
394
|
+
case "binary_not_found":
|
|
395
|
+
case "spawn_failed":
|
|
396
|
+
case "emulator_crashed_on_startup":
|
|
397
|
+
return error.message;
|
|
394
398
|
case "port_allocation_failed":
|
|
395
399
|
case "port_in_use":
|
|
396
|
-
case "monitor_timeout":
|
|
397
400
|
return "The server could not start a usable emulator session. Check the emulator configuration and try again.";
|
|
401
|
+
case "monitor_timeout":
|
|
402
|
+
return error.message;
|
|
398
403
|
case "not_connected":
|
|
399
404
|
case "connection_closed":
|
|
400
405
|
case "socket_write_failed":
|
|
@@ -425,14 +430,21 @@ function publicMessageFor(error) {
|
|
|
425
430
|
}
|
|
426
431
|
}
|
|
427
432
|
function publicDetailsFor(error) {
|
|
428
|
-
switch (error.
|
|
429
|
-
case "
|
|
430
|
-
case "
|
|
431
|
-
case "
|
|
432
|
-
case "io":
|
|
433
|
+
switch (error.code) {
|
|
434
|
+
case "binary_not_found":
|
|
435
|
+
case "spawn_failed":
|
|
436
|
+
case "emulator_crashed_on_startup":
|
|
433
437
|
return error.details;
|
|
434
438
|
default:
|
|
435
|
-
|
|
439
|
+
switch (error.category) {
|
|
440
|
+
case "validation":
|
|
441
|
+
case "session_state":
|
|
442
|
+
case "unsupported":
|
|
443
|
+
case "io":
|
|
444
|
+
return error.details;
|
|
445
|
+
default:
|
|
446
|
+
return void 0;
|
|
447
|
+
}
|
|
436
448
|
}
|
|
437
449
|
}
|
|
438
450
|
function normalizeToolError(error) {
|
|
@@ -1370,6 +1382,8 @@ var DEFAULT_INPUT_TAP_MS = 75;
|
|
|
1370
1382
|
var DEFAULT_KEYBOARD_REPEAT_MS = 100;
|
|
1371
1383
|
var VICE_PROCESS_LOG_PATH = import_node_path.default.join(import_node_os.default.tmpdir(), "c64-debug-mcp-x64sc.log");
|
|
1372
1384
|
var DISPLAY_CAPTURE_DIR = import_node_path.default.resolve(process.cwd(), ".vice-debug-mcp-artifacts");
|
|
1385
|
+
var CLEANUP_ENABLED = !/^(0|false|no|off)$/i.test(process.env.C64_CLEANUP_SCREENSHOTS ?? "");
|
|
1386
|
+
var CLEANUP_MAX_AGE_MINUTES = Number.parseInt(process.env.C64_CLEANUP_MAX_AGE_MINUTES ?? "20", 10);
|
|
1373
1387
|
var MIRROR_EMULATOR_LOGS_TO_STDERR = /^(1|true|yes|on)$/i.test(process.env.C64_DEBUG_CONSOLE_LOGS ?? "");
|
|
1374
1388
|
var EXECUTION_EVENT_WAIT_MS = 1e3;
|
|
1375
1389
|
var EXECUTION_SETTLE_DELAY_MS = 2e3;
|
|
@@ -1518,6 +1532,7 @@ var ViceSession = class {
|
|
|
1518
1532
|
#displayOperationLock = null;
|
|
1519
1533
|
constructor(portAllocator = new PortAllocator()) {
|
|
1520
1534
|
this.#portAllocator = portAllocator;
|
|
1535
|
+
void this.#cleanupOldScreenshots();
|
|
1521
1536
|
this.#client.on("response", (response) => {
|
|
1522
1537
|
this.#lastResponseAt = nowIso();
|
|
1523
1538
|
this.#writeProcessLogLine(`[monitor-response] type=${response.type} requestId=${response.requestId} errorCode=${response.errorCode}`);
|
|
@@ -1547,6 +1562,10 @@ var ViceSession = class {
|
|
|
1547
1562
|
this.#syncMonitorRuntimeState();
|
|
1548
1563
|
});
|
|
1549
1564
|
}
|
|
1565
|
+
async getSessionState() {
|
|
1566
|
+
await this.#ensureReady();
|
|
1567
|
+
return this.snapshot();
|
|
1568
|
+
}
|
|
1550
1569
|
snapshot() {
|
|
1551
1570
|
return {
|
|
1552
1571
|
transportState: this.#transportState,
|
|
@@ -1783,6 +1802,17 @@ var ViceSession = class {
|
|
|
1783
1802
|
async continueExecution(waitUntilRunningStable = false) {
|
|
1784
1803
|
return this.#withExecutionLock(async () => {
|
|
1785
1804
|
await this.#ensureReady();
|
|
1805
|
+
if (this.#executionState === "running") {
|
|
1806
|
+
const runtime2 = this.#client.runtimeState();
|
|
1807
|
+
const debugState2 = this.#lastRegisters == null ? await this.#readDebugState() : this.#buildDebugState(this.#lastRegisters);
|
|
1808
|
+
return {
|
|
1809
|
+
executionState: "running",
|
|
1810
|
+
lastStopReason: this.#lastStopReason,
|
|
1811
|
+
programCounter: runtime2.programCounter ?? debugState2.programCounter,
|
|
1812
|
+
registers: debugState2.registers,
|
|
1813
|
+
warnings: []
|
|
1814
|
+
};
|
|
1815
|
+
}
|
|
1786
1816
|
if (this.#executionState !== "stopped") {
|
|
1787
1817
|
debuggerNotPausedError("execute resume", {
|
|
1788
1818
|
executionState: this.#executionState,
|
|
@@ -1961,7 +1991,6 @@ var ViceSession = class {
|
|
|
1961
1991
|
async programLoad(options) {
|
|
1962
1992
|
const filePath = import_node_path.default.resolve(options.filePath);
|
|
1963
1993
|
await this.#assertReadableProgramFile(filePath);
|
|
1964
|
-
await this.#ensureRunning("program_load");
|
|
1965
1994
|
this.#explicitPauseActive = false;
|
|
1966
1995
|
const result = await this.autostartProgram(filePath, options.autoStart ?? true, options.fileIndex ?? 0);
|
|
1967
1996
|
return {
|
|
@@ -2212,134 +2241,137 @@ var ViceSession = class {
|
|
|
2212
2241
|
}
|
|
2213
2242
|
}
|
|
2214
2243
|
async writeText(text) {
|
|
2215
|
-
await this.#
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2244
|
+
return await this.#withAutoResumeForInput("write_text", async () => {
|
|
2245
|
+
const encoded = decodeWriteTextToPetscii(text);
|
|
2246
|
+
if (encoded.length > MAX_WRITE_TEXT_BYTES) {
|
|
2247
|
+
validationError("write_text exceeds the maximum allowed byte length for one request", {
|
|
2248
|
+
length: encoded.length,
|
|
2249
|
+
max: MAX_WRITE_TEXT_BYTES
|
|
2250
|
+
});
|
|
2251
|
+
}
|
|
2252
|
+
this.#writeProcessLogLine(`[tx] write_text length=${encoded.length} text=${JSON.stringify(text)}`);
|
|
2253
|
+
await this.#client.sendKeys(Buffer.from(encoded).toString("binary"));
|
|
2254
|
+
await this.#settleInputState("write_text", "running");
|
|
2255
|
+
return {
|
|
2256
|
+
sent: true,
|
|
2257
|
+
length: encoded.length
|
|
2258
|
+
};
|
|
2259
|
+
});
|
|
2230
2260
|
}
|
|
2231
2261
|
async keyboardInput(action, keys, durationMs) {
|
|
2232
|
-
await this.#
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
}
|
|
2236
|
-
const resolvedKeys = keys.map((key) => resolveKeyboardInputKey(key));
|
|
2237
|
-
const normalizedKeys = resolvedKeys.map((key) => key.canonical);
|
|
2238
|
-
this.#writeProcessLogLine(
|
|
2239
|
-
`[tx] keyboard_input action=${action} keys=${normalizedKeys.join(",")}${durationMs == null ? "" : ` durationMs=${durationMs}`}`
|
|
2240
|
-
);
|
|
2241
|
-
switch (action) {
|
|
2242
|
-
case "tap": {
|
|
2243
|
-
const duration = clampTapDuration(durationMs);
|
|
2244
|
-
const bytes = Uint8Array.from(resolvedKeys.flatMap((key) => Array.from(key.bytes)));
|
|
2245
|
-
await this.#client.sendKeys(Buffer.from(bytes).toString("binary"));
|
|
2246
|
-
await this.#settleInputState("keyboard_input", "running");
|
|
2247
|
-
await sleep(duration);
|
|
2248
|
-
return {
|
|
2249
|
-
action,
|
|
2250
|
-
keys: normalizedKeys,
|
|
2251
|
-
applied: true,
|
|
2252
|
-
held: false,
|
|
2253
|
-
mode: "buffered_text"
|
|
2254
|
-
};
|
|
2262
|
+
return await this.#withAutoResumeForInput("keyboard_input", async () => {
|
|
2263
|
+
if (!Array.isArray(keys) || keys.length === 0 || keys.length > 4) {
|
|
2264
|
+
validationError("keyboard_input requires between 1 and 4 keys", { keys });
|
|
2255
2265
|
}
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
}
|
|
2266
|
+
const resolvedKeys = keys.map((key) => resolveKeyboardInputKey(key));
|
|
2267
|
+
const normalizedKeys = resolvedKeys.map((key) => key.canonical);
|
|
2268
|
+
this.#writeProcessLogLine(
|
|
2269
|
+
`[tx] keyboard_input action=${action} keys=${normalizedKeys.join(",")}${durationMs == null ? "" : ` durationMs=${durationMs}`}`
|
|
2270
|
+
);
|
|
2271
|
+
switch (action) {
|
|
2272
|
+
case "tap": {
|
|
2273
|
+
const duration = clampTapDuration(durationMs);
|
|
2274
|
+
const bytes = Uint8Array.from(resolvedKeys.flatMap((key) => Array.from(key.bytes)));
|
|
2275
|
+
await this.#client.sendKeys(Buffer.from(bytes).toString("binary"));
|
|
2276
|
+
await this.#settleInputState("keyboard_input", "running");
|
|
2277
|
+
await sleep(duration);
|
|
2278
|
+
return {
|
|
2279
|
+
action,
|
|
2280
|
+
keys: normalizedKeys,
|
|
2281
|
+
applied: true,
|
|
2282
|
+
held: false,
|
|
2283
|
+
mode: "buffered_text"
|
|
2284
|
+
};
|
|
2276
2285
|
}
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2286
|
+
case "press": {
|
|
2287
|
+
const singleByteKeys = resolvedKeys.map((key) => {
|
|
2288
|
+
if (key.bytes.length !== 1) {
|
|
2289
|
+
unsupportedError("keyboard_input press/release only supports keys that map to a single PETSCII byte.", {
|
|
2290
|
+
key: key.canonical
|
|
2291
|
+
});
|
|
2292
|
+
}
|
|
2293
|
+
return key.bytes[0];
|
|
2294
|
+
});
|
|
2295
|
+
for (let index = 0; index < normalizedKeys.length; index += 1) {
|
|
2296
|
+
const heldKey = normalizedKeys[index];
|
|
2297
|
+
const byte = singleByteKeys[index];
|
|
2298
|
+
if (!this.#heldKeyboardIntervals.has(heldKey)) {
|
|
2299
|
+
await this.#client.sendKeys(Buffer.from([byte]).toString("binary"));
|
|
2300
|
+
await this.#settleInputState("keyboard_input", "running");
|
|
2301
|
+
const interval = setInterval(() => {
|
|
2302
|
+
void this.#client.sendKeys(Buffer.from([byte]).toString("binary")).then(() => this.#settleInputState("keyboard_input", "running")).catch(() => void 0);
|
|
2303
|
+
}, DEFAULT_KEYBOARD_REPEAT_MS);
|
|
2304
|
+
this.#heldKeyboardIntervals.set(heldKey, interval);
|
|
2305
|
+
}
|
|
2291
2306
|
}
|
|
2307
|
+
return {
|
|
2308
|
+
action,
|
|
2309
|
+
keys: normalizedKeys,
|
|
2310
|
+
applied: true,
|
|
2311
|
+
held: true,
|
|
2312
|
+
mode: "buffered_text_repeat"
|
|
2313
|
+
};
|
|
2292
2314
|
}
|
|
2293
|
-
|
|
2294
|
-
const
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2315
|
+
case "release": {
|
|
2316
|
+
for (const key of resolvedKeys) {
|
|
2317
|
+
if (key.bytes.length !== 1) {
|
|
2318
|
+
unsupportedError("keyboard_input press/release only supports keys that map to a single PETSCII byte.", {
|
|
2319
|
+
key: key.canonical
|
|
2320
|
+
});
|
|
2321
|
+
}
|
|
2298
2322
|
}
|
|
2323
|
+
for (const heldKey of normalizedKeys) {
|
|
2324
|
+
const interval = this.#heldKeyboardIntervals.get(heldKey);
|
|
2325
|
+
if (interval) {
|
|
2326
|
+
clearInterval(interval);
|
|
2327
|
+
this.#heldKeyboardIntervals.delete(heldKey);
|
|
2328
|
+
}
|
|
2329
|
+
}
|
|
2330
|
+
return {
|
|
2331
|
+
action,
|
|
2332
|
+
keys: normalizedKeys,
|
|
2333
|
+
applied: true,
|
|
2334
|
+
held: false,
|
|
2335
|
+
mode: "buffered_text_repeat"
|
|
2336
|
+
};
|
|
2299
2337
|
}
|
|
2300
|
-
return {
|
|
2301
|
-
action,
|
|
2302
|
-
keys: normalizedKeys,
|
|
2303
|
-
applied: true,
|
|
2304
|
-
held: false,
|
|
2305
|
-
mode: "buffered_text_repeat"
|
|
2306
|
-
};
|
|
2307
2338
|
}
|
|
2308
|
-
}
|
|
2339
|
+
});
|
|
2309
2340
|
}
|
|
2310
2341
|
async joystickInput(port2, action, control, durationMs) {
|
|
2311
|
-
await this.#
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
}
|
|
2317
|
-
this.#writeProcessLogLine(
|
|
2318
|
-
`[tx] joystick_input port=${port2} action=${action} control=${control}${durationMs == null ? "" : ` durationMs=${durationMs}`}`
|
|
2319
|
-
);
|
|
2320
|
-
switch (action) {
|
|
2321
|
-
case "tap": {
|
|
2322
|
-
const duration = clampTapDuration(durationMs);
|
|
2323
|
-
await this.#applyJoystickMask(port2, this.#getJoystickMask(port2) & ~bit);
|
|
2324
|
-
await sleep(duration);
|
|
2325
|
-
await this.#applyJoystickMask(port2, this.#getJoystickMask(port2) | bit);
|
|
2326
|
-
break;
|
|
2342
|
+
return await this.#withAutoResumeForInput("joystick_input", async () => {
|
|
2343
|
+
const previousExecutionState = this.#executionState;
|
|
2344
|
+
const bit = JOYSTICK_CONTROL_BITS[control];
|
|
2345
|
+
if (bit == null) {
|
|
2346
|
+
validationError("Unsupported joystick control", { control });
|
|
2327
2347
|
}
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2348
|
+
this.#writeProcessLogLine(
|
|
2349
|
+
`[tx] joystick_input port=${port2} action=${action} control=${control}${durationMs == null ? "" : ` durationMs=${durationMs}`}`
|
|
2350
|
+
);
|
|
2351
|
+
switch (action) {
|
|
2352
|
+
case "tap": {
|
|
2353
|
+
const duration = clampTapDuration(durationMs);
|
|
2354
|
+
await this.#applyJoystickMask(port2, this.#getJoystickMask(port2) & ~bit);
|
|
2355
|
+
await sleep(duration);
|
|
2356
|
+
await this.#applyJoystickMask(port2, this.#getJoystickMask(port2) | bit);
|
|
2357
|
+
break;
|
|
2358
|
+
}
|
|
2359
|
+
case "press":
|
|
2360
|
+
await this.#applyJoystickMask(port2, this.#getJoystickMask(port2) & ~bit);
|
|
2361
|
+
break;
|
|
2362
|
+
case "release":
|
|
2363
|
+
await this.#applyJoystickMask(port2, this.#getJoystickMask(port2) | bit);
|
|
2364
|
+
break;
|
|
2365
|
+
}
|
|
2366
|
+
await this.#settleInputState("joystick_input", previousExecutionState);
|
|
2367
|
+
return {
|
|
2368
|
+
port: port2,
|
|
2369
|
+
action,
|
|
2370
|
+
control,
|
|
2371
|
+
applied: true,
|
|
2372
|
+
state: this.#describeJoystickState(port2)
|
|
2373
|
+
};
|
|
2374
|
+
});
|
|
2343
2375
|
}
|
|
2344
2376
|
async waitForState(targetState, timeoutMs = 5e3, stableMs = targetState === "running" ? INPUT_RUNNING_STABLE_MS : 0) {
|
|
2345
2377
|
await this.#ensureReady();
|
|
@@ -2402,6 +2434,27 @@ var ViceSession = class {
|
|
|
2402
2434
|
});
|
|
2403
2435
|
}
|
|
2404
2436
|
}
|
|
2437
|
+
async #withAutoResumeForInput(commandName, operation) {
|
|
2438
|
+
await this.#ensureReady();
|
|
2439
|
+
this.#syncMonitorRuntimeState();
|
|
2440
|
+
const wasRunning = this.#executionState === "running";
|
|
2441
|
+
const wasPaused = this.#explicitPauseActive;
|
|
2442
|
+
if (!wasRunning) {
|
|
2443
|
+
this.#writeProcessLogLine(`[${commandName}] auto-resuming for input operation`);
|
|
2444
|
+
await this.#client.continueExecution();
|
|
2445
|
+
await this.waitForState("running", 5e3, INPUT_RUNNING_STABLE_MS);
|
|
2446
|
+
}
|
|
2447
|
+
try {
|
|
2448
|
+
return await operation();
|
|
2449
|
+
} finally {
|
|
2450
|
+
if (!wasRunning && wasPaused) {
|
|
2451
|
+
this.#writeProcessLogLine(`[${commandName}] restoring paused state after input`);
|
|
2452
|
+
await this.#client.ping();
|
|
2453
|
+
await this.waitForState("stopped", 5e3, 0);
|
|
2454
|
+
this.#explicitPauseActive = true;
|
|
2455
|
+
}
|
|
2456
|
+
}
|
|
2457
|
+
}
|
|
2405
2458
|
async #ensureHealthyConnection() {
|
|
2406
2459
|
if (this.#recoveryPromise) {
|
|
2407
2460
|
await this.#recoveryPromise;
|
|
@@ -2450,6 +2503,16 @@ var ViceSession = class {
|
|
|
2450
2503
|
const port2 = await this.#portAllocator.allocate();
|
|
2451
2504
|
await this.#portAllocator.ensureFree(port2, host2);
|
|
2452
2505
|
const binary = config.binaryPath ?? DEFAULT_C64_BINARY;
|
|
2506
|
+
const binaryCheck = await checkBinaryExists(binary);
|
|
2507
|
+
if (!binaryCheck.exists) {
|
|
2508
|
+
throw new ViceMcpError(
|
|
2509
|
+
"binary_not_found",
|
|
2510
|
+
`VICE emulator binary '${binary}' not found. Please install VICE or configure the correct path using the 'binaryPath' setting.`,
|
|
2511
|
+
"process_launch",
|
|
2512
|
+
false,
|
|
2513
|
+
{ binary, searchedPath: process.env.PATH }
|
|
2514
|
+
);
|
|
2515
|
+
}
|
|
2453
2516
|
const args = ["-autostartprgmode", "1", "-binarymonitor", "-binarymonitoraddress", `${host2}:${port2}`];
|
|
2454
2517
|
if (config.arguments) {
|
|
2455
2518
|
args.push(...splitCommandLine(config.arguments));
|
|
@@ -2467,11 +2530,15 @@ var ViceSession = class {
|
|
|
2467
2530
|
this.#lastRuntimeEventType = "unknown";
|
|
2468
2531
|
this.#lastRuntimeProgramCounter = null;
|
|
2469
2532
|
const env = await buildViceLaunchEnv();
|
|
2533
|
+
let spawnError = void 0;
|
|
2470
2534
|
const child = (0, import_node_child_process.spawn)(binary, args, {
|
|
2471
2535
|
cwd: config.workingDirectory ? import_node_path.default.resolve(config.workingDirectory) : void 0,
|
|
2472
2536
|
env,
|
|
2473
2537
|
stdio: ["ignore", "pipe", "pipe"]
|
|
2474
2538
|
});
|
|
2539
|
+
child.once("error", (err) => {
|
|
2540
|
+
spawnError = err;
|
|
2541
|
+
});
|
|
2475
2542
|
this.#process = child;
|
|
2476
2543
|
this.#attachProcessLogging(child, binary, args);
|
|
2477
2544
|
this.#bindProcessLifecycle(child);
|
|
@@ -2479,6 +2546,16 @@ var ViceSession = class {
|
|
|
2479
2546
|
this.#transportState = "waiting_for_monitor";
|
|
2480
2547
|
try {
|
|
2481
2548
|
await waitForMonitor(host2, port2, 5e3);
|
|
2549
|
+
if (spawnError !== void 0) {
|
|
2550
|
+
const err = spawnError;
|
|
2551
|
+
throw new ViceMcpError(
|
|
2552
|
+
"spawn_failed",
|
|
2553
|
+
`Failed to start VICE emulator '${binary}': ${err.message}`,
|
|
2554
|
+
"process_launch",
|
|
2555
|
+
false,
|
|
2556
|
+
{ binary, error: err.message, resolvedPath: binaryCheck.path }
|
|
2557
|
+
);
|
|
2558
|
+
}
|
|
2482
2559
|
await this.#client.connect(host2, port2);
|
|
2483
2560
|
this.#transportState = "connected";
|
|
2484
2561
|
this.#connectedSince = nowIso();
|
|
@@ -2491,6 +2568,19 @@ var ViceSession = class {
|
|
|
2491
2568
|
} catch (error) {
|
|
2492
2569
|
this.#processState = "crashed";
|
|
2493
2570
|
this.#transportState = "faulted";
|
|
2571
|
+
if (error instanceof ViceMcpError && error.code === "monitor_timeout" && spawnError !== void 0) {
|
|
2572
|
+
const err = spawnError;
|
|
2573
|
+
const enhancedError = new ViceMcpError(
|
|
2574
|
+
"emulator_crashed_on_startup",
|
|
2575
|
+
`VICE emulator '${binary}' crashed during startup: ${err.message}`,
|
|
2576
|
+
"process_launch",
|
|
2577
|
+
false,
|
|
2578
|
+
{ binary, error: err.message, resolvedPath: binaryCheck.path }
|
|
2579
|
+
);
|
|
2580
|
+
this.#warnings = [...this.#warnings.filter((warning) => warning.code !== "launch_failed"), makeWarning(enhancedError.message, "launch_failed")];
|
|
2581
|
+
await this.#stopManagedProcess(true);
|
|
2582
|
+
throw enhancedError;
|
|
2583
|
+
}
|
|
2494
2584
|
this.#warnings = [...this.#warnings.filter((warning) => warning.code !== "launch_failed"), makeWarning(String(error.message ?? error), "launch_failed")];
|
|
2495
2585
|
await this.#stopManagedProcess(true);
|
|
2496
2586
|
throw error;
|
|
@@ -3160,6 +3250,45 @@ var ViceSession = class {
|
|
|
3160
3250
|
}
|
|
3161
3251
|
this.#syncMonitorRuntimeState();
|
|
3162
3252
|
}
|
|
3253
|
+
async #cleanupOldScreenshots() {
|
|
3254
|
+
if (!CLEANUP_ENABLED) {
|
|
3255
|
+
return;
|
|
3256
|
+
}
|
|
3257
|
+
try {
|
|
3258
|
+
const maxAgeMinutes = Math.max(1, Math.min(525600, CLEANUP_MAX_AGE_MINUTES));
|
|
3259
|
+
const maxAgeMs = maxAgeMinutes * 60 * 1e3;
|
|
3260
|
+
const cutoffTime = Date.now() - maxAgeMs;
|
|
3261
|
+
this.#writeProcessLogLine(`[cleanup] scanning ${DISPLAY_CAPTURE_DIR} for screenshots older than ${maxAgeMinutes}m`);
|
|
3262
|
+
let entries;
|
|
3263
|
+
try {
|
|
3264
|
+
entries = await import_promises.default.readdir(DISPLAY_CAPTURE_DIR);
|
|
3265
|
+
} catch (error) {
|
|
3266
|
+
if (error.code === "ENOENT") {
|
|
3267
|
+
return;
|
|
3268
|
+
}
|
|
3269
|
+
throw error;
|
|
3270
|
+
}
|
|
3271
|
+
const pngFiles = entries.filter((name) => name.endsWith(".png") && name.startsWith("capture-"));
|
|
3272
|
+
let deletedCount = 0;
|
|
3273
|
+
let errorCount = 0;
|
|
3274
|
+
for (const filename of pngFiles) {
|
|
3275
|
+
try {
|
|
3276
|
+
const filePath = import_node_path.default.join(DISPLAY_CAPTURE_DIR, filename);
|
|
3277
|
+
const stats = await import_promises.default.stat(filePath);
|
|
3278
|
+
if (stats.mtime.getTime() < cutoffTime) {
|
|
3279
|
+
await import_promises.default.unlink(filePath);
|
|
3280
|
+
deletedCount++;
|
|
3281
|
+
}
|
|
3282
|
+
} catch (error) {
|
|
3283
|
+
errorCount++;
|
|
3284
|
+
this.#writeProcessLogLine(`[cleanup] failed to delete ${filename}: ${error instanceof Error ? error.message : String(error)}`);
|
|
3285
|
+
}
|
|
3286
|
+
}
|
|
3287
|
+
this.#writeProcessLogLine(`[cleanup] completed: ${deletedCount} deleted, ${errorCount} errors, ${pngFiles.length - deletedCount - errorCount} retained`);
|
|
3288
|
+
} catch (error) {
|
|
3289
|
+
this.#writeProcessLogLine(`[cleanup] failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
3290
|
+
}
|
|
3291
|
+
}
|
|
3163
3292
|
};
|
|
3164
3293
|
function splitCommandLine(input) {
|
|
3165
3294
|
const result = [];
|
|
@@ -3192,6 +3321,27 @@ function splitCommandLine(input) {
|
|
|
3192
3321
|
}
|
|
3193
3322
|
return result;
|
|
3194
3323
|
}
|
|
3324
|
+
async function checkBinaryExists(binaryPath) {
|
|
3325
|
+
if (import_node_path.default.isAbsolute(binaryPath)) {
|
|
3326
|
+
try {
|
|
3327
|
+
await import_promises.default.access(binaryPath, import_promises.default.constants.X_OK);
|
|
3328
|
+
return { exists: true, path: binaryPath };
|
|
3329
|
+
} catch {
|
|
3330
|
+
return { exists: false };
|
|
3331
|
+
}
|
|
3332
|
+
}
|
|
3333
|
+
const pathEnv = process.env.PATH || "";
|
|
3334
|
+
const pathDirs = pathEnv.split(import_node_path.default.delimiter);
|
|
3335
|
+
for (const dir of pathDirs) {
|
|
3336
|
+
const fullPath = import_node_path.default.join(dir, binaryPath);
|
|
3337
|
+
try {
|
|
3338
|
+
await import_promises.default.access(fullPath, import_promises.default.constants.X_OK);
|
|
3339
|
+
return { exists: true, path: fullPath };
|
|
3340
|
+
} catch {
|
|
3341
|
+
}
|
|
3342
|
+
}
|
|
3343
|
+
return { exists: false };
|
|
3344
|
+
}
|
|
3195
3345
|
async function waitForMonitor(host2, port2, timeoutMs) {
|
|
3196
3346
|
const deadline = Date.now() + timeoutMs;
|
|
3197
3347
|
while (Date.now() < deadline) {
|
|
@@ -3200,7 +3350,7 @@ async function waitForMonitor(host2, port2, timeoutMs) {
|
|
|
3200
3350
|
}
|
|
3201
3351
|
await sleep(100);
|
|
3202
3352
|
}
|
|
3203
|
-
throw new ViceMcpError("monitor_timeout", `Debugger monitor did not open on ${host2}:${port2}
|
|
3353
|
+
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, {
|
|
3204
3354
|
host: host2,
|
|
3205
3355
|
port: port2
|
|
3206
3356
|
});
|
|
@@ -3265,7 +3415,7 @@ var getSessionStateTool = createViceTool({
|
|
|
3265
3415
|
description: "Returns emulator session state including transport/process status, auto-resume state, and the most recent hit checkpoint.",
|
|
3266
3416
|
inputSchema: noInputSchema,
|
|
3267
3417
|
dataSchema: sessionStateResultSchema,
|
|
3268
|
-
execute: async () => c64Session.
|
|
3418
|
+
execute: async () => await c64Session.getSessionState()
|
|
3269
3419
|
});
|
|
3270
3420
|
var getRegistersTool = createViceTool({
|
|
3271
3421
|
id: "get_registers",
|
|
@@ -3331,7 +3481,7 @@ var writeMemoryTool = createViceTool({
|
|
|
3331
3481
|
});
|
|
3332
3482
|
var executeTool = createViceTool({
|
|
3333
3483
|
id: "execute",
|
|
3334
|
-
description: "Controls execution with pause, resume, step, step_over, step_out, or reset.
|
|
3484
|
+
description: "Controls execution with pause, resume, step, step_over, step_out, or reset. Pause and resume are idempotent (safe to call multiple times).",
|
|
3335
3485
|
inputSchema: import_zod4.z.object({
|
|
3336
3486
|
action: import_zod4.z.enum(["pause", "resume", "step", "step_over", "step_out", "reset"]),
|
|
3337
3487
|
count: import_zod4.z.number().int().positive().default(1).describe("Instruction count for step and step_over actions"),
|
|
@@ -3453,7 +3603,7 @@ var getDisplayTextTool = createViceTool({
|
|
|
3453
3603
|
});
|
|
3454
3604
|
var writeTextTool = createViceTool({
|
|
3455
3605
|
id: "write_text",
|
|
3456
|
-
description:
|
|
3606
|
+
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.",
|
|
3457
3607
|
inputSchema: import_zod4.z.object({
|
|
3458
3608
|
text: import_zod4.z.string()
|
|
3459
3609
|
}),
|
|
@@ -3465,7 +3615,7 @@ var writeTextTool = createViceTool({
|
|
|
3465
3615
|
});
|
|
3466
3616
|
var keyboardInputTool = createViceTool({
|
|
3467
3617
|
id: "keyboard_input",
|
|
3468
|
-
description:
|
|
3618
|
+
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.",
|
|
3469
3619
|
inputSchema: import_zod4.z.object({
|
|
3470
3620
|
action: inputActionSchema.describe("Use tap for a single key event or press/release for repeated buffered input"),
|
|
3471
3621
|
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"),
|
|
@@ -3476,7 +3626,7 @@ var keyboardInputTool = createViceTool({
|
|
|
3476
3626
|
});
|
|
3477
3627
|
var joystickInputTool = createViceTool({
|
|
3478
3628
|
id: "joystick_input",
|
|
3479
|
-
description:
|
|
3629
|
+
description: "Sends joystick input to C64 joystick port 1 or 2. Automatically resumes if stopped and restores pause state after.",
|
|
3480
3630
|
inputSchema: import_zod4.z.object({
|
|
3481
3631
|
port: joystickPortSchema.describe("Joystick port number"),
|
|
3482
3632
|
action: inputActionSchema.describe("Joystick action to apply"),
|