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/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.category) {
403
- case "validation":
404
- case "session_state":
405
- case "unsupported":
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
- return void 0;
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.#ensureRunning("write_text");
2190
- const encoded = decodeWriteTextToPetscii(text);
2191
- if (encoded.length > MAX_WRITE_TEXT_BYTES) {
2192
- validationError("write_text exceeds the maximum allowed byte length for one request", {
2193
- length: encoded.length,
2194
- max: MAX_WRITE_TEXT_BYTES
2195
- });
2196
- }
2197
- this.#writeProcessLogLine(`[tx] write_text length=${encoded.length} text=${JSON.stringify(text)}`);
2198
- await this.#client.sendKeys(Buffer.from(encoded).toString("binary"));
2199
- await this.#settleInputState("write_text", "running");
2200
- return {
2201
- sent: true,
2202
- length: encoded.length
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.#ensureRunning("keyboard_input");
2207
- if (!Array.isArray(keys) || keys.length === 0 || keys.length > 4) {
2208
- validationError("keyboard_input requires between 1 and 4 keys", { keys });
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
- case "press": {
2231
- const singleByteKeys = resolvedKeys.map((key) => {
2232
- if (key.bytes.length !== 1) {
2233
- unsupportedError("keyboard_input press/release only supports keys that map to a single PETSCII byte.", {
2234
- key: key.canonical
2235
- });
2236
- }
2237
- return key.bytes[0];
2238
- });
2239
- for (let index = 0; index < normalizedKeys.length; index += 1) {
2240
- const heldKey = normalizedKeys[index];
2241
- const byte = singleByteKeys[index];
2242
- if (!this.#heldKeyboardIntervals.has(heldKey)) {
2243
- await this.#client.sendKeys(Buffer.from([byte]).toString("binary"));
2244
- await this.#settleInputState("keyboard_input", "running");
2245
- const interval = setInterval(() => {
2246
- void this.#client.sendKeys(Buffer.from([byte]).toString("binary")).then(() => this.#settleInputState("keyboard_input", "running")).catch(() => void 0);
2247
- }, DEFAULT_KEYBOARD_REPEAT_MS);
2248
- this.#heldKeyboardIntervals.set(heldKey, interval);
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
- return {
2252
- action,
2253
- keys: normalizedKeys,
2254
- applied: true,
2255
- held: true,
2256
- mode: "buffered_text_repeat"
2257
- };
2258
- }
2259
- case "release": {
2260
- for (const key of resolvedKeys) {
2261
- if (key.bytes.length !== 1) {
2262
- unsupportedError("keyboard_input press/release only supports keys that map to a single PETSCII byte.", {
2263
- key: key.canonical
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
- for (const heldKey of normalizedKeys) {
2268
- const interval = this.#heldKeyboardIntervals.get(heldKey);
2269
- if (interval) {
2270
- clearInterval(interval);
2271
- this.#heldKeyboardIntervals.delete(heldKey);
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.#ensureRunning("joystick_input");
2286
- const previousExecutionState = this.#executionState;
2287
- const bit = JOYSTICK_CONTROL_BITS[control];
2288
- if (bit == null) {
2289
- validationError("Unsupported joystick control", { control });
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
- case "press":
2303
- await this.#applyJoystickMask(port, this.#getJoystickMask(port) & ~bit);
2304
- break;
2305
- case "release":
2306
- await this.#applyJoystickMask(port, this.#getJoystickMask(port) | bit);
2307
- break;
2308
- }
2309
- await this.#settleInputState("joystick_input", previousExecutionState);
2310
- return {
2311
- port,
2312
- action,
2313
- control,
2314
- applied: true,
2315
- state: this.#describeJoystickState(port)
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}`, "timeout", true, {
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.snapshot()
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. Resume can optionally wait until running becomes stable.",
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: '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.',
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: '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.',
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: 'Sends joystick input to C64 joystick port 1 or 2. Requires emulator to be running - call execute(action="resume") first if stopped.',
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"),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "c64-debug-mcp",
3
- "version": "1.0.1",
3
+ "version": "1.0.6",
4
4
  "description": "Model Context Protocol server for C64 debugging via VICE emulator",
5
5
  "type": "module",
6
6
  "keywords": [