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