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.js
CHANGED
|
@@ -368,10 +368,15 @@ function publicMessageFor(error) {
|
|
|
368
368
|
case "program_file_missing":
|
|
369
369
|
case "program_file_invalid":
|
|
370
370
|
return error.message;
|
|
371
|
+
case "binary_not_found":
|
|
372
|
+
case "spawn_failed":
|
|
373
|
+
case "emulator_crashed_on_startup":
|
|
374
|
+
return error.message;
|
|
371
375
|
case "port_allocation_failed":
|
|
372
376
|
case "port_in_use":
|
|
373
|
-
case "monitor_timeout":
|
|
374
377
|
return "The server could not start a usable emulator session. Check the emulator configuration and try again.";
|
|
378
|
+
case "monitor_timeout":
|
|
379
|
+
return error.message;
|
|
375
380
|
case "not_connected":
|
|
376
381
|
case "connection_closed":
|
|
377
382
|
case "socket_write_failed":
|
|
@@ -402,14 +407,21 @@ function publicMessageFor(error) {
|
|
|
402
407
|
}
|
|
403
408
|
}
|
|
404
409
|
function publicDetailsFor(error) {
|
|
405
|
-
switch (error.
|
|
406
|
-
case "
|
|
407
|
-
case "
|
|
408
|
-
case "
|
|
409
|
-
case "io":
|
|
410
|
+
switch (error.code) {
|
|
411
|
+
case "binary_not_found":
|
|
412
|
+
case "spawn_failed":
|
|
413
|
+
case "emulator_crashed_on_startup":
|
|
410
414
|
return error.details;
|
|
411
415
|
default:
|
|
412
|
-
|
|
416
|
+
switch (error.category) {
|
|
417
|
+
case "validation":
|
|
418
|
+
case "session_state":
|
|
419
|
+
case "unsupported":
|
|
420
|
+
case "io":
|
|
421
|
+
return error.details;
|
|
422
|
+
default:
|
|
423
|
+
return void 0;
|
|
424
|
+
}
|
|
413
425
|
}
|
|
414
426
|
}
|
|
415
427
|
function normalizeToolError(error) {
|
|
@@ -1347,6 +1359,8 @@ var DEFAULT_INPUT_TAP_MS = 75;
|
|
|
1347
1359
|
var DEFAULT_KEYBOARD_REPEAT_MS = 100;
|
|
1348
1360
|
var VICE_PROCESS_LOG_PATH = path.join(os.tmpdir(), "c64-debug-mcp-x64sc.log");
|
|
1349
1361
|
var DISPLAY_CAPTURE_DIR = path.resolve(process.cwd(), ".vice-debug-mcp-artifacts");
|
|
1362
|
+
var CLEANUP_ENABLED = !/^(0|false|no|off)$/i.test(process.env.C64_CLEANUP_SCREENSHOTS ?? "");
|
|
1363
|
+
var CLEANUP_MAX_AGE_MINUTES = Number.parseInt(process.env.C64_CLEANUP_MAX_AGE_MINUTES ?? "20", 10);
|
|
1350
1364
|
var MIRROR_EMULATOR_LOGS_TO_STDERR = /^(1|true|yes|on)$/i.test(process.env.C64_DEBUG_CONSOLE_LOGS ?? "");
|
|
1351
1365
|
var EXECUTION_EVENT_WAIT_MS = 1e3;
|
|
1352
1366
|
var EXECUTION_SETTLE_DELAY_MS = 2e3;
|
|
@@ -1495,6 +1509,7 @@ var ViceSession = class {
|
|
|
1495
1509
|
#displayOperationLock = null;
|
|
1496
1510
|
constructor(portAllocator = new PortAllocator()) {
|
|
1497
1511
|
this.#portAllocator = portAllocator;
|
|
1512
|
+
void this.#cleanupOldScreenshots();
|
|
1498
1513
|
this.#client.on("response", (response) => {
|
|
1499
1514
|
this.#lastResponseAt = nowIso();
|
|
1500
1515
|
this.#writeProcessLogLine(`[monitor-response] type=${response.type} requestId=${response.requestId} errorCode=${response.errorCode}`);
|
|
@@ -1524,6 +1539,10 @@ var ViceSession = class {
|
|
|
1524
1539
|
this.#syncMonitorRuntimeState();
|
|
1525
1540
|
});
|
|
1526
1541
|
}
|
|
1542
|
+
async getSessionState() {
|
|
1543
|
+
await this.#ensureReady();
|
|
1544
|
+
return this.snapshot();
|
|
1545
|
+
}
|
|
1527
1546
|
snapshot() {
|
|
1528
1547
|
return {
|
|
1529
1548
|
transportState: this.#transportState,
|
|
@@ -1760,6 +1779,17 @@ var ViceSession = class {
|
|
|
1760
1779
|
async continueExecution(waitUntilRunningStable = false) {
|
|
1761
1780
|
return this.#withExecutionLock(async () => {
|
|
1762
1781
|
await this.#ensureReady();
|
|
1782
|
+
if (this.#executionState === "running") {
|
|
1783
|
+
const runtime2 = this.#client.runtimeState();
|
|
1784
|
+
const debugState2 = this.#lastRegisters == null ? await this.#readDebugState() : this.#buildDebugState(this.#lastRegisters);
|
|
1785
|
+
return {
|
|
1786
|
+
executionState: "running",
|
|
1787
|
+
lastStopReason: this.#lastStopReason,
|
|
1788
|
+
programCounter: runtime2.programCounter ?? debugState2.programCounter,
|
|
1789
|
+
registers: debugState2.registers,
|
|
1790
|
+
warnings: []
|
|
1791
|
+
};
|
|
1792
|
+
}
|
|
1763
1793
|
if (this.#executionState !== "stopped") {
|
|
1764
1794
|
debuggerNotPausedError("execute resume", {
|
|
1765
1795
|
executionState: this.#executionState,
|
|
@@ -1938,7 +1968,6 @@ var ViceSession = class {
|
|
|
1938
1968
|
async programLoad(options) {
|
|
1939
1969
|
const filePath = path.resolve(options.filePath);
|
|
1940
1970
|
await this.#assertReadableProgramFile(filePath);
|
|
1941
|
-
await this.#ensureRunning("program_load");
|
|
1942
1971
|
this.#explicitPauseActive = false;
|
|
1943
1972
|
const result = await this.autostartProgram(filePath, options.autoStart ?? true, options.fileIndex ?? 0);
|
|
1944
1973
|
return {
|
|
@@ -2189,134 +2218,137 @@ var ViceSession = class {
|
|
|
2189
2218
|
}
|
|
2190
2219
|
}
|
|
2191
2220
|
async writeText(text) {
|
|
2192
|
-
await this.#
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2221
|
+
return await this.#withAutoResumeForInput("write_text", async () => {
|
|
2222
|
+
const encoded = decodeWriteTextToPetscii(text);
|
|
2223
|
+
if (encoded.length > MAX_WRITE_TEXT_BYTES) {
|
|
2224
|
+
validationError("write_text exceeds the maximum allowed byte length for one request", {
|
|
2225
|
+
length: encoded.length,
|
|
2226
|
+
max: MAX_WRITE_TEXT_BYTES
|
|
2227
|
+
});
|
|
2228
|
+
}
|
|
2229
|
+
this.#writeProcessLogLine(`[tx] write_text length=${encoded.length} text=${JSON.stringify(text)}`);
|
|
2230
|
+
await this.#client.sendKeys(Buffer.from(encoded).toString("binary"));
|
|
2231
|
+
await this.#settleInputState("write_text", "running");
|
|
2232
|
+
return {
|
|
2233
|
+
sent: true,
|
|
2234
|
+
length: encoded.length
|
|
2235
|
+
};
|
|
2236
|
+
});
|
|
2207
2237
|
}
|
|
2208
2238
|
async keyboardInput(action, keys, durationMs) {
|
|
2209
|
-
await this.#
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
}
|
|
2213
|
-
const resolvedKeys = keys.map((key) => resolveKeyboardInputKey(key));
|
|
2214
|
-
const normalizedKeys = resolvedKeys.map((key) => key.canonical);
|
|
2215
|
-
this.#writeProcessLogLine(
|
|
2216
|
-
`[tx] keyboard_input action=${action} keys=${normalizedKeys.join(",")}${durationMs == null ? "" : ` durationMs=${durationMs}`}`
|
|
2217
|
-
);
|
|
2218
|
-
switch (action) {
|
|
2219
|
-
case "tap": {
|
|
2220
|
-
const duration = clampTapDuration(durationMs);
|
|
2221
|
-
const bytes = Uint8Array.from(resolvedKeys.flatMap((key) => Array.from(key.bytes)));
|
|
2222
|
-
await this.#client.sendKeys(Buffer.from(bytes).toString("binary"));
|
|
2223
|
-
await this.#settleInputState("keyboard_input", "running");
|
|
2224
|
-
await sleep(duration);
|
|
2225
|
-
return {
|
|
2226
|
-
action,
|
|
2227
|
-
keys: normalizedKeys,
|
|
2228
|
-
applied: true,
|
|
2229
|
-
held: false,
|
|
2230
|
-
mode: "buffered_text"
|
|
2231
|
-
};
|
|
2239
|
+
return await this.#withAutoResumeForInput("keyboard_input", async () => {
|
|
2240
|
+
if (!Array.isArray(keys) || keys.length === 0 || keys.length > 4) {
|
|
2241
|
+
validationError("keyboard_input requires between 1 and 4 keys", { keys });
|
|
2232
2242
|
}
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
}
|
|
2243
|
+
const resolvedKeys = keys.map((key) => resolveKeyboardInputKey(key));
|
|
2244
|
+
const normalizedKeys = resolvedKeys.map((key) => key.canonical);
|
|
2245
|
+
this.#writeProcessLogLine(
|
|
2246
|
+
`[tx] keyboard_input action=${action} keys=${normalizedKeys.join(",")}${durationMs == null ? "" : ` durationMs=${durationMs}`}`
|
|
2247
|
+
);
|
|
2248
|
+
switch (action) {
|
|
2249
|
+
case "tap": {
|
|
2250
|
+
const duration = clampTapDuration(durationMs);
|
|
2251
|
+
const bytes = Uint8Array.from(resolvedKeys.flatMap((key) => Array.from(key.bytes)));
|
|
2252
|
+
await this.#client.sendKeys(Buffer.from(bytes).toString("binary"));
|
|
2253
|
+
await this.#settleInputState("keyboard_input", "running");
|
|
2254
|
+
await sleep(duration);
|
|
2255
|
+
return {
|
|
2256
|
+
action,
|
|
2257
|
+
keys: normalizedKeys,
|
|
2258
|
+
applied: true,
|
|
2259
|
+
held: false,
|
|
2260
|
+
mode: "buffered_text"
|
|
2261
|
+
};
|
|
2253
2262
|
}
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2263
|
+
case "press": {
|
|
2264
|
+
const singleByteKeys = resolvedKeys.map((key) => {
|
|
2265
|
+
if (key.bytes.length !== 1) {
|
|
2266
|
+
unsupportedError("keyboard_input press/release only supports keys that map to a single PETSCII byte.", {
|
|
2267
|
+
key: key.canonical
|
|
2268
|
+
});
|
|
2269
|
+
}
|
|
2270
|
+
return key.bytes[0];
|
|
2271
|
+
});
|
|
2272
|
+
for (let index = 0; index < normalizedKeys.length; index += 1) {
|
|
2273
|
+
const heldKey = normalizedKeys[index];
|
|
2274
|
+
const byte = singleByteKeys[index];
|
|
2275
|
+
if (!this.#heldKeyboardIntervals.has(heldKey)) {
|
|
2276
|
+
await this.#client.sendKeys(Buffer.from([byte]).toString("binary"));
|
|
2277
|
+
await this.#settleInputState("keyboard_input", "running");
|
|
2278
|
+
const interval = setInterval(() => {
|
|
2279
|
+
void this.#client.sendKeys(Buffer.from([byte]).toString("binary")).then(() => this.#settleInputState("keyboard_input", "running")).catch(() => void 0);
|
|
2280
|
+
}, DEFAULT_KEYBOARD_REPEAT_MS);
|
|
2281
|
+
this.#heldKeyboardIntervals.set(heldKey, interval);
|
|
2282
|
+
}
|
|
2268
2283
|
}
|
|
2284
|
+
return {
|
|
2285
|
+
action,
|
|
2286
|
+
keys: normalizedKeys,
|
|
2287
|
+
applied: true,
|
|
2288
|
+
held: true,
|
|
2289
|
+
mode: "buffered_text_repeat"
|
|
2290
|
+
};
|
|
2269
2291
|
}
|
|
2270
|
-
|
|
2271
|
-
const
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2292
|
+
case "release": {
|
|
2293
|
+
for (const key of resolvedKeys) {
|
|
2294
|
+
if (key.bytes.length !== 1) {
|
|
2295
|
+
unsupportedError("keyboard_input press/release only supports keys that map to a single PETSCII byte.", {
|
|
2296
|
+
key: key.canonical
|
|
2297
|
+
});
|
|
2298
|
+
}
|
|
2275
2299
|
}
|
|
2300
|
+
for (const heldKey of normalizedKeys) {
|
|
2301
|
+
const interval = this.#heldKeyboardIntervals.get(heldKey);
|
|
2302
|
+
if (interval) {
|
|
2303
|
+
clearInterval(interval);
|
|
2304
|
+
this.#heldKeyboardIntervals.delete(heldKey);
|
|
2305
|
+
}
|
|
2306
|
+
}
|
|
2307
|
+
return {
|
|
2308
|
+
action,
|
|
2309
|
+
keys: normalizedKeys,
|
|
2310
|
+
applied: true,
|
|
2311
|
+
held: false,
|
|
2312
|
+
mode: "buffered_text_repeat"
|
|
2313
|
+
};
|
|
2276
2314
|
}
|
|
2277
|
-
return {
|
|
2278
|
-
action,
|
|
2279
|
-
keys: normalizedKeys,
|
|
2280
|
-
applied: true,
|
|
2281
|
-
held: false,
|
|
2282
|
-
mode: "buffered_text_repeat"
|
|
2283
|
-
};
|
|
2284
2315
|
}
|
|
2285
|
-
}
|
|
2316
|
+
});
|
|
2286
2317
|
}
|
|
2287
2318
|
async joystickInput(port2, action, control, durationMs) {
|
|
2288
|
-
await this.#
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
}
|
|
2294
|
-
this.#writeProcessLogLine(
|
|
2295
|
-
`[tx] joystick_input port=${port2} action=${action} control=${control}${durationMs == null ? "" : ` durationMs=${durationMs}`}`
|
|
2296
|
-
);
|
|
2297
|
-
switch (action) {
|
|
2298
|
-
case "tap": {
|
|
2299
|
-
const duration = clampTapDuration(durationMs);
|
|
2300
|
-
await this.#applyJoystickMask(port2, this.#getJoystickMask(port2) & ~bit);
|
|
2301
|
-
await sleep(duration);
|
|
2302
|
-
await this.#applyJoystickMask(port2, this.#getJoystickMask(port2) | bit);
|
|
2303
|
-
break;
|
|
2319
|
+
return await this.#withAutoResumeForInput("joystick_input", async () => {
|
|
2320
|
+
const previousExecutionState = this.#executionState;
|
|
2321
|
+
const bit = JOYSTICK_CONTROL_BITS[control];
|
|
2322
|
+
if (bit == null) {
|
|
2323
|
+
validationError("Unsupported joystick control", { control });
|
|
2304
2324
|
}
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2325
|
+
this.#writeProcessLogLine(
|
|
2326
|
+
`[tx] joystick_input port=${port2} action=${action} control=${control}${durationMs == null ? "" : ` durationMs=${durationMs}`}`
|
|
2327
|
+
);
|
|
2328
|
+
switch (action) {
|
|
2329
|
+
case "tap": {
|
|
2330
|
+
const duration = clampTapDuration(durationMs);
|
|
2331
|
+
await this.#applyJoystickMask(port2, this.#getJoystickMask(port2) & ~bit);
|
|
2332
|
+
await sleep(duration);
|
|
2333
|
+
await this.#applyJoystickMask(port2, this.#getJoystickMask(port2) | bit);
|
|
2334
|
+
break;
|
|
2335
|
+
}
|
|
2336
|
+
case "press":
|
|
2337
|
+
await this.#applyJoystickMask(port2, this.#getJoystickMask(port2) & ~bit);
|
|
2338
|
+
break;
|
|
2339
|
+
case "release":
|
|
2340
|
+
await this.#applyJoystickMask(port2, this.#getJoystickMask(port2) | bit);
|
|
2341
|
+
break;
|
|
2342
|
+
}
|
|
2343
|
+
await this.#settleInputState("joystick_input", previousExecutionState);
|
|
2344
|
+
return {
|
|
2345
|
+
port: port2,
|
|
2346
|
+
action,
|
|
2347
|
+
control,
|
|
2348
|
+
applied: true,
|
|
2349
|
+
state: this.#describeJoystickState(port2)
|
|
2350
|
+
};
|
|
2351
|
+
});
|
|
2320
2352
|
}
|
|
2321
2353
|
async waitForState(targetState, timeoutMs = 5e3, stableMs = targetState === "running" ? INPUT_RUNNING_STABLE_MS : 0) {
|
|
2322
2354
|
await this.#ensureReady();
|
|
@@ -2379,6 +2411,27 @@ var ViceSession = class {
|
|
|
2379
2411
|
});
|
|
2380
2412
|
}
|
|
2381
2413
|
}
|
|
2414
|
+
async #withAutoResumeForInput(commandName, operation) {
|
|
2415
|
+
await this.#ensureReady();
|
|
2416
|
+
this.#syncMonitorRuntimeState();
|
|
2417
|
+
const wasRunning = this.#executionState === "running";
|
|
2418
|
+
const wasPaused = this.#explicitPauseActive;
|
|
2419
|
+
if (!wasRunning) {
|
|
2420
|
+
this.#writeProcessLogLine(`[${commandName}] auto-resuming for input operation`);
|
|
2421
|
+
await this.#client.continueExecution();
|
|
2422
|
+
await this.waitForState("running", 5e3, INPUT_RUNNING_STABLE_MS);
|
|
2423
|
+
}
|
|
2424
|
+
try {
|
|
2425
|
+
return await operation();
|
|
2426
|
+
} finally {
|
|
2427
|
+
if (!wasRunning && wasPaused) {
|
|
2428
|
+
this.#writeProcessLogLine(`[${commandName}] restoring paused state after input`);
|
|
2429
|
+
await this.#client.ping();
|
|
2430
|
+
await this.waitForState("stopped", 5e3, 0);
|
|
2431
|
+
this.#explicitPauseActive = true;
|
|
2432
|
+
}
|
|
2433
|
+
}
|
|
2434
|
+
}
|
|
2382
2435
|
async #ensureHealthyConnection() {
|
|
2383
2436
|
if (this.#recoveryPromise) {
|
|
2384
2437
|
await this.#recoveryPromise;
|
|
@@ -2427,6 +2480,16 @@ var ViceSession = class {
|
|
|
2427
2480
|
const port2 = await this.#portAllocator.allocate();
|
|
2428
2481
|
await this.#portAllocator.ensureFree(port2, host2);
|
|
2429
2482
|
const binary = config.binaryPath ?? DEFAULT_C64_BINARY;
|
|
2483
|
+
const binaryCheck = await checkBinaryExists(binary);
|
|
2484
|
+
if (!binaryCheck.exists) {
|
|
2485
|
+
throw new ViceMcpError(
|
|
2486
|
+
"binary_not_found",
|
|
2487
|
+
`VICE emulator binary '${binary}' not found. Please install VICE or configure the correct path using the 'binaryPath' setting.`,
|
|
2488
|
+
"process_launch",
|
|
2489
|
+
false,
|
|
2490
|
+
{ binary, searchedPath: process.env.PATH }
|
|
2491
|
+
);
|
|
2492
|
+
}
|
|
2430
2493
|
const args = ["-autostartprgmode", "1", "-binarymonitor", "-binarymonitoraddress", `${host2}:${port2}`];
|
|
2431
2494
|
if (config.arguments) {
|
|
2432
2495
|
args.push(...splitCommandLine(config.arguments));
|
|
@@ -2444,11 +2507,15 @@ var ViceSession = class {
|
|
|
2444
2507
|
this.#lastRuntimeEventType = "unknown";
|
|
2445
2508
|
this.#lastRuntimeProgramCounter = null;
|
|
2446
2509
|
const env = await buildViceLaunchEnv();
|
|
2510
|
+
let spawnError = void 0;
|
|
2447
2511
|
const child = spawn(binary, args, {
|
|
2448
2512
|
cwd: config.workingDirectory ? path.resolve(config.workingDirectory) : void 0,
|
|
2449
2513
|
env,
|
|
2450
2514
|
stdio: ["ignore", "pipe", "pipe"]
|
|
2451
2515
|
});
|
|
2516
|
+
child.once("error", (err) => {
|
|
2517
|
+
spawnError = err;
|
|
2518
|
+
});
|
|
2452
2519
|
this.#process = child;
|
|
2453
2520
|
this.#attachProcessLogging(child, binary, args);
|
|
2454
2521
|
this.#bindProcessLifecycle(child);
|
|
@@ -2456,6 +2523,16 @@ var ViceSession = class {
|
|
|
2456
2523
|
this.#transportState = "waiting_for_monitor";
|
|
2457
2524
|
try {
|
|
2458
2525
|
await waitForMonitor(host2, port2, 5e3);
|
|
2526
|
+
if (spawnError !== void 0) {
|
|
2527
|
+
const err = spawnError;
|
|
2528
|
+
throw new ViceMcpError(
|
|
2529
|
+
"spawn_failed",
|
|
2530
|
+
`Failed to start VICE emulator '${binary}': ${err.message}`,
|
|
2531
|
+
"process_launch",
|
|
2532
|
+
false,
|
|
2533
|
+
{ binary, error: err.message, resolvedPath: binaryCheck.path }
|
|
2534
|
+
);
|
|
2535
|
+
}
|
|
2459
2536
|
await this.#client.connect(host2, port2);
|
|
2460
2537
|
this.#transportState = "connected";
|
|
2461
2538
|
this.#connectedSince = nowIso();
|
|
@@ -2468,6 +2545,19 @@ var ViceSession = class {
|
|
|
2468
2545
|
} catch (error) {
|
|
2469
2546
|
this.#processState = "crashed";
|
|
2470
2547
|
this.#transportState = "faulted";
|
|
2548
|
+
if (error instanceof ViceMcpError && error.code === "monitor_timeout" && spawnError !== void 0) {
|
|
2549
|
+
const err = spawnError;
|
|
2550
|
+
const enhancedError = new ViceMcpError(
|
|
2551
|
+
"emulator_crashed_on_startup",
|
|
2552
|
+
`VICE emulator '${binary}' crashed during startup: ${err.message}`,
|
|
2553
|
+
"process_launch",
|
|
2554
|
+
false,
|
|
2555
|
+
{ binary, error: err.message, resolvedPath: binaryCheck.path }
|
|
2556
|
+
);
|
|
2557
|
+
this.#warnings = [...this.#warnings.filter((warning) => warning.code !== "launch_failed"), makeWarning(enhancedError.message, "launch_failed")];
|
|
2558
|
+
await this.#stopManagedProcess(true);
|
|
2559
|
+
throw enhancedError;
|
|
2560
|
+
}
|
|
2471
2561
|
this.#warnings = [...this.#warnings.filter((warning) => warning.code !== "launch_failed"), makeWarning(String(error.message ?? error), "launch_failed")];
|
|
2472
2562
|
await this.#stopManagedProcess(true);
|
|
2473
2563
|
throw error;
|
|
@@ -3137,6 +3227,45 @@ var ViceSession = class {
|
|
|
3137
3227
|
}
|
|
3138
3228
|
this.#syncMonitorRuntimeState();
|
|
3139
3229
|
}
|
|
3230
|
+
async #cleanupOldScreenshots() {
|
|
3231
|
+
if (!CLEANUP_ENABLED) {
|
|
3232
|
+
return;
|
|
3233
|
+
}
|
|
3234
|
+
try {
|
|
3235
|
+
const maxAgeMinutes = Math.max(1, Math.min(525600, CLEANUP_MAX_AGE_MINUTES));
|
|
3236
|
+
const maxAgeMs = maxAgeMinutes * 60 * 1e3;
|
|
3237
|
+
const cutoffTime = Date.now() - maxAgeMs;
|
|
3238
|
+
this.#writeProcessLogLine(`[cleanup] scanning ${DISPLAY_CAPTURE_DIR} for screenshots older than ${maxAgeMinutes}m`);
|
|
3239
|
+
let entries;
|
|
3240
|
+
try {
|
|
3241
|
+
entries = await fs.readdir(DISPLAY_CAPTURE_DIR);
|
|
3242
|
+
} catch (error) {
|
|
3243
|
+
if (error.code === "ENOENT") {
|
|
3244
|
+
return;
|
|
3245
|
+
}
|
|
3246
|
+
throw error;
|
|
3247
|
+
}
|
|
3248
|
+
const pngFiles = entries.filter((name) => name.endsWith(".png") && name.startsWith("capture-"));
|
|
3249
|
+
let deletedCount = 0;
|
|
3250
|
+
let errorCount = 0;
|
|
3251
|
+
for (const filename of pngFiles) {
|
|
3252
|
+
try {
|
|
3253
|
+
const filePath = path.join(DISPLAY_CAPTURE_DIR, filename);
|
|
3254
|
+
const stats = await fs.stat(filePath);
|
|
3255
|
+
if (stats.mtime.getTime() < cutoffTime) {
|
|
3256
|
+
await fs.unlink(filePath);
|
|
3257
|
+
deletedCount++;
|
|
3258
|
+
}
|
|
3259
|
+
} catch (error) {
|
|
3260
|
+
errorCount++;
|
|
3261
|
+
this.#writeProcessLogLine(`[cleanup] failed to delete ${filename}: ${error instanceof Error ? error.message : String(error)}`);
|
|
3262
|
+
}
|
|
3263
|
+
}
|
|
3264
|
+
this.#writeProcessLogLine(`[cleanup] completed: ${deletedCount} deleted, ${errorCount} errors, ${pngFiles.length - deletedCount - errorCount} retained`);
|
|
3265
|
+
} catch (error) {
|
|
3266
|
+
this.#writeProcessLogLine(`[cleanup] failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
3267
|
+
}
|
|
3268
|
+
}
|
|
3140
3269
|
};
|
|
3141
3270
|
function splitCommandLine(input) {
|
|
3142
3271
|
const result = [];
|
|
@@ -3169,6 +3298,27 @@ function splitCommandLine(input) {
|
|
|
3169
3298
|
}
|
|
3170
3299
|
return result;
|
|
3171
3300
|
}
|
|
3301
|
+
async function checkBinaryExists(binaryPath) {
|
|
3302
|
+
if (path.isAbsolute(binaryPath)) {
|
|
3303
|
+
try {
|
|
3304
|
+
await fs.access(binaryPath, fs.constants.X_OK);
|
|
3305
|
+
return { exists: true, path: binaryPath };
|
|
3306
|
+
} catch {
|
|
3307
|
+
return { exists: false };
|
|
3308
|
+
}
|
|
3309
|
+
}
|
|
3310
|
+
const pathEnv = process.env.PATH || "";
|
|
3311
|
+
const pathDirs = pathEnv.split(path.delimiter);
|
|
3312
|
+
for (const dir of pathDirs) {
|
|
3313
|
+
const fullPath = path.join(dir, binaryPath);
|
|
3314
|
+
try {
|
|
3315
|
+
await fs.access(fullPath, fs.constants.X_OK);
|
|
3316
|
+
return { exists: true, path: fullPath };
|
|
3317
|
+
} catch {
|
|
3318
|
+
}
|
|
3319
|
+
}
|
|
3320
|
+
return { exists: false };
|
|
3321
|
+
}
|
|
3172
3322
|
async function waitForMonitor(host2, port2, timeoutMs) {
|
|
3173
3323
|
const deadline = Date.now() + timeoutMs;
|
|
3174
3324
|
while (Date.now() < deadline) {
|
|
@@ -3177,7 +3327,7 @@ async function waitForMonitor(host2, port2, timeoutMs) {
|
|
|
3177
3327
|
}
|
|
3178
3328
|
await sleep(100);
|
|
3179
3329
|
}
|
|
3180
|
-
throw new ViceMcpError("monitor_timeout", `Debugger monitor did not open on ${host2}:${port2}
|
|
3330
|
+
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, {
|
|
3181
3331
|
host: host2,
|
|
3182
3332
|
port: port2
|
|
3183
3333
|
});
|
|
@@ -3242,7 +3392,7 @@ var getSessionStateTool = createViceTool({
|
|
|
3242
3392
|
description: "Returns emulator session state including transport/process status, auto-resume state, and the most recent hit checkpoint.",
|
|
3243
3393
|
inputSchema: noInputSchema,
|
|
3244
3394
|
dataSchema: sessionStateResultSchema,
|
|
3245
|
-
execute: async () => c64Session.
|
|
3395
|
+
execute: async () => await c64Session.getSessionState()
|
|
3246
3396
|
});
|
|
3247
3397
|
var getRegistersTool = createViceTool({
|
|
3248
3398
|
id: "get_registers",
|
|
@@ -3308,7 +3458,7 @@ var writeMemoryTool = createViceTool({
|
|
|
3308
3458
|
});
|
|
3309
3459
|
var executeTool = createViceTool({
|
|
3310
3460
|
id: "execute",
|
|
3311
|
-
description: "Controls execution with pause, resume, step, step_over, step_out, or reset.
|
|
3461
|
+
description: "Controls execution with pause, resume, step, step_over, step_out, or reset. Pause and resume are idempotent (safe to call multiple times).",
|
|
3312
3462
|
inputSchema: z3.object({
|
|
3313
3463
|
action: z3.enum(["pause", "resume", "step", "step_over", "step_out", "reset"]),
|
|
3314
3464
|
count: z3.number().int().positive().default(1).describe("Instruction count for step and step_over actions"),
|
|
@@ -3430,7 +3580,7 @@ var getDisplayTextTool = createViceTool({
|
|
|
3430
3580
|
});
|
|
3431
3581
|
var writeTextTool = createViceTool({
|
|
3432
3582
|
id: "write_text",
|
|
3433
|
-
description:
|
|
3583
|
+
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.",
|
|
3434
3584
|
inputSchema: z3.object({
|
|
3435
3585
|
text: z3.string()
|
|
3436
3586
|
}),
|
|
@@ -3442,7 +3592,7 @@ var writeTextTool = createViceTool({
|
|
|
3442
3592
|
});
|
|
3443
3593
|
var keyboardInputTool = createViceTool({
|
|
3444
3594
|
id: "keyboard_input",
|
|
3445
|
-
description:
|
|
3595
|
+
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.",
|
|
3446
3596
|
inputSchema: z3.object({
|
|
3447
3597
|
action: inputActionSchema.describe("Use tap for a single key event or press/release for repeated buffered input"),
|
|
3448
3598
|
keys: z3.array(z3.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"),
|
|
@@ -3453,7 +3603,7 @@ var keyboardInputTool = createViceTool({
|
|
|
3453
3603
|
});
|
|
3454
3604
|
var joystickInputTool = createViceTool({
|
|
3455
3605
|
id: "joystick_input",
|
|
3456
|
-
description:
|
|
3606
|
+
description: "Sends joystick input to C64 joystick port 1 or 2. Automatically resumes if stopped and restores pause state after.",
|
|
3457
3607
|
inputSchema: z3.object({
|
|
3458
3608
|
port: joystickPortSchema.describe("Joystick port number"),
|
|
3459
3609
|
action: inputActionSchema.describe("Joystick action to apply"),
|