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.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.category) {
406
- case "validation":
407
- case "session_state":
408
- case "unsupported":
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
- return void 0;
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.#ensureRunning("write_text");
2193
- const encoded = decodeWriteTextToPetscii(text);
2194
- if (encoded.length > MAX_WRITE_TEXT_BYTES) {
2195
- validationError("write_text exceeds the maximum allowed byte length for one request", {
2196
- length: encoded.length,
2197
- max: MAX_WRITE_TEXT_BYTES
2198
- });
2199
- }
2200
- this.#writeProcessLogLine(`[tx] write_text length=${encoded.length} text=${JSON.stringify(text)}`);
2201
- await this.#client.sendKeys(Buffer.from(encoded).toString("binary"));
2202
- await this.#settleInputState("write_text", "running");
2203
- return {
2204
- sent: true,
2205
- length: encoded.length
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.#ensureRunning("keyboard_input");
2210
- if (!Array.isArray(keys) || keys.length === 0 || keys.length > 4) {
2211
- validationError("keyboard_input requires between 1 and 4 keys", { keys });
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
- case "press": {
2234
- const singleByteKeys = resolvedKeys.map((key) => {
2235
- if (key.bytes.length !== 1) {
2236
- unsupportedError("keyboard_input press/release only supports keys that map to a single PETSCII byte.", {
2237
- key: key.canonical
2238
- });
2239
- }
2240
- return key.bytes[0];
2241
- });
2242
- for (let index = 0; index < normalizedKeys.length; index += 1) {
2243
- const heldKey = normalizedKeys[index];
2244
- const byte = singleByteKeys[index];
2245
- if (!this.#heldKeyboardIntervals.has(heldKey)) {
2246
- await this.#client.sendKeys(Buffer.from([byte]).toString("binary"));
2247
- await this.#settleInputState("keyboard_input", "running");
2248
- const interval = setInterval(() => {
2249
- void this.#client.sendKeys(Buffer.from([byte]).toString("binary")).then(() => this.#settleInputState("keyboard_input", "running")).catch(() => void 0);
2250
- }, DEFAULT_KEYBOARD_REPEAT_MS);
2251
- this.#heldKeyboardIntervals.set(heldKey, interval);
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
- return {
2255
- action,
2256
- keys: normalizedKeys,
2257
- applied: true,
2258
- held: true,
2259
- mode: "buffered_text_repeat"
2260
- };
2261
- }
2262
- case "release": {
2263
- for (const key of resolvedKeys) {
2264
- if (key.bytes.length !== 1) {
2265
- unsupportedError("keyboard_input press/release only supports keys that map to a single PETSCII byte.", {
2266
- key: key.canonical
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
- for (const heldKey of normalizedKeys) {
2271
- const interval = this.#heldKeyboardIntervals.get(heldKey);
2272
- if (interval) {
2273
- clearInterval(interval);
2274
- this.#heldKeyboardIntervals.delete(heldKey);
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.#ensureRunning("joystick_input");
2289
- const previousExecutionState = this.#executionState;
2290
- const bit = JOYSTICK_CONTROL_BITS[control];
2291
- if (bit == null) {
2292
- validationError("Unsupported joystick control", { control });
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
- case "press":
2306
- await this.#applyJoystickMask(port2, this.#getJoystickMask(port2) & ~bit);
2307
- break;
2308
- case "release":
2309
- await this.#applyJoystickMask(port2, this.#getJoystickMask(port2) | bit);
2310
- break;
2311
- }
2312
- await this.#settleInputState("joystick_input", previousExecutionState);
2313
- return {
2314
- port: port2,
2315
- action,
2316
- control,
2317
- applied: true,
2318
- state: this.#describeJoystickState(port2)
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}`, "timeout", true, {
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.snapshot()
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. Resume can optionally wait until running becomes stable.",
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: 'Types text into the C64. Requires emulator to be running - call execute(action="resume") first if stopped. Supports escaped characters and PETSCII brace tokens like {RETURN}, {CLR}, {HOME}, {PI}, and color names. Limit 64 bytes per request.',
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: 'Sends one to four keys or PETSCII tokens to the C64. Requires emulator to be running - call execute(action="resume") first if stopped. Use for key presses, releases, and taps.',
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: 'Sends joystick input to C64 joystick port 1 or 2. Requires emulator to be running - call execute(action="resume") first if stopped.',
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"),