c64-debug-mcp 1.0.2 → 1.0.7

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 CHANGED
@@ -152,9 +152,193 @@ var import_zod3 = require("zod");
152
152
  // src/schemas.ts
153
153
  var import_zod2 = require("zod");
154
154
  var warningSchema = warningItemSchema;
155
- var address16Schema = import_zod2.z.number().int().min(0).max(65535);
156
- var byteValueSchema = import_zod2.z.number().int().min(0).max(255).describe("Single raw byte value");
157
- var byteArraySchema = import_zod2.z.array(byteValueSchema).describe("Raw bytes as a JSON array");
155
+ function parseAddress16(input) {
156
+ if (typeof input === "number") {
157
+ if (!Number.isInteger(input) || input < 0 || input > 65535) {
158
+ throw new import_zod2.z.ZodError([
159
+ {
160
+ code: "custom",
161
+ message: `Address must be an integer between 0 and 65535 (0xFFFF), got ${input}`,
162
+ path: []
163
+ }
164
+ ]);
165
+ }
166
+ return input;
167
+ }
168
+ if (typeof input !== "string") {
169
+ throw new import_zod2.z.ZodError([
170
+ {
171
+ code: "custom",
172
+ message: `Address must be a number or string, got ${typeof input}`,
173
+ path: []
174
+ }
175
+ ]);
176
+ }
177
+ const trimmed = input.trim();
178
+ let hexString;
179
+ let format;
180
+ if (trimmed.startsWith("$")) {
181
+ hexString = trimmed.slice(1);
182
+ format = "C64 hex ($)";
183
+ } else if (trimmed.toLowerCase().startsWith("0x")) {
184
+ hexString = trimmed.slice(2);
185
+ format = "C hex (0x)";
186
+ } else {
187
+ throw new import_zod2.z.ZodError([
188
+ {
189
+ code: "custom",
190
+ message: `Invalid address format: "${input}". Expected formats: decimal number (53248), hex with $ ($D000), or hex with 0x (0xD000). Bare hex not supported to avoid ambiguity.`,
191
+ path: []
192
+ }
193
+ ]);
194
+ }
195
+ if (!/^[0-9A-Fa-f]{1,4}$/.test(hexString)) {
196
+ throw new import_zod2.z.ZodError([
197
+ {
198
+ code: "custom",
199
+ message: `Invalid ${format} address: "${input}". Hex portion must be 1-4 hex digits (0-9, A-F)`,
200
+ path: []
201
+ }
202
+ ]);
203
+ }
204
+ const parsed = parseInt(hexString, 16);
205
+ if (isNaN(parsed) || parsed < 0 || parsed > 65535) {
206
+ throw new import_zod2.z.ZodError([
207
+ {
208
+ code: "custom",
209
+ message: `Address out of range: "${input}" (${parsed}). Must be 0x0000-0xFFFF (0-65535)`,
210
+ path: []
211
+ }
212
+ ]);
213
+ }
214
+ return parsed;
215
+ }
216
+ var address16Schema = import_zod2.z.preprocess(parseAddress16, import_zod2.z.number().int().min(0).max(65535)).describe("16-bit C64 address: decimal (53248) or hex string with prefix ($D000, 0xD000)");
217
+ function parseByte(input) {
218
+ if (typeof input === "number") {
219
+ if (!Number.isInteger(input) || input < 0 || input > 255) {
220
+ throw new import_zod2.z.ZodError([
221
+ {
222
+ code: "custom",
223
+ message: `Byte value must be an integer between 0 and 255 (0xFF), got ${input}`,
224
+ path: []
225
+ }
226
+ ]);
227
+ }
228
+ return input;
229
+ }
230
+ if (typeof input !== "string") {
231
+ throw new import_zod2.z.ZodError([
232
+ {
233
+ code: "custom",
234
+ message: `Byte value must be a number or string, got ${typeof input}`,
235
+ path: []
236
+ }
237
+ ]);
238
+ }
239
+ const trimmed = input.trim();
240
+ if (trimmed.startsWith("$")) {
241
+ const hexString = trimmed.slice(1);
242
+ if (!/^[0-9A-Fa-f]{1,2}$/.test(hexString)) {
243
+ throw new import_zod2.z.ZodError([
244
+ {
245
+ code: "custom",
246
+ message: `Invalid C64 hex ($) byte value: "${input}". Hex portion must be 1-2 hex digits (0-9, A-F)`,
247
+ path: []
248
+ }
249
+ ]);
250
+ }
251
+ const parsed = parseInt(hexString, 16);
252
+ if (isNaN(parsed) || parsed < 0 || parsed > 255) {
253
+ throw new import_zod2.z.ZodError([
254
+ {
255
+ code: "custom",
256
+ message: `Byte value out of range: "${input}" (${parsed}). Must be 0x00-0xFF (0-255)`,
257
+ path: []
258
+ }
259
+ ]);
260
+ }
261
+ return parsed;
262
+ }
263
+ if (trimmed.toLowerCase().startsWith("0x")) {
264
+ const hexString = trimmed.slice(2);
265
+ if (!/^[0-9A-Fa-f]{1,2}$/.test(hexString)) {
266
+ throw new import_zod2.z.ZodError([
267
+ {
268
+ code: "custom",
269
+ message: `Invalid C hex (0x) byte value: "${input}". Hex portion must be 1-2 hex digits (0-9, A-F)`,
270
+ path: []
271
+ }
272
+ ]);
273
+ }
274
+ const parsed = parseInt(hexString, 16);
275
+ if (isNaN(parsed) || parsed < 0 || parsed > 255) {
276
+ throw new import_zod2.z.ZodError([
277
+ {
278
+ code: "custom",
279
+ message: `Byte value out of range: "${input}" (${parsed}). Must be 0x00-0xFF (0-255)`,
280
+ path: []
281
+ }
282
+ ]);
283
+ }
284
+ return parsed;
285
+ }
286
+ if (trimmed.startsWith("%")) {
287
+ const binString = trimmed.slice(1);
288
+ if (!/^[01]{1,8}$/.test(binString)) {
289
+ throw new import_zod2.z.ZodError([
290
+ {
291
+ code: "custom",
292
+ message: `Invalid C64 binary (%) byte value: "${input}". Binary portion must be 1-8 binary digits (0-1)`,
293
+ path: []
294
+ }
295
+ ]);
296
+ }
297
+ const parsed = parseInt(binString, 2);
298
+ if (isNaN(parsed) || parsed < 0 || parsed > 255) {
299
+ throw new import_zod2.z.ZodError([
300
+ {
301
+ code: "custom",
302
+ message: `Byte value out of range: "${input}" (${parsed}). Must be 0b00000000-0b11111111 (0-255)`,
303
+ path: []
304
+ }
305
+ ]);
306
+ }
307
+ return parsed;
308
+ }
309
+ if (trimmed.toLowerCase().startsWith("0b")) {
310
+ const binString = trimmed.slice(2);
311
+ if (!/^[01]{1,8}$/.test(binString)) {
312
+ throw new import_zod2.z.ZodError([
313
+ {
314
+ code: "custom",
315
+ message: `Invalid C binary (0b) byte value: "${input}". Binary portion must be 1-8 binary digits (0-1)`,
316
+ path: []
317
+ }
318
+ ]);
319
+ }
320
+ const parsed = parseInt(binString, 2);
321
+ if (isNaN(parsed) || parsed < 0 || parsed > 255) {
322
+ throw new import_zod2.z.ZodError([
323
+ {
324
+ code: "custom",
325
+ message: `Byte value out of range: "${input}" (${parsed}). Must be 0b00000000-0b11111111 (0-255)`,
326
+ path: []
327
+ }
328
+ ]);
329
+ }
330
+ return parsed;
331
+ }
332
+ throw new import_zod2.z.ZodError([
333
+ {
334
+ code: "custom",
335
+ message: `Invalid byte format: "${input}". Expected formats: decimal (255), hex with prefix ($FF, 0xFF), or binary with prefix (%11111111, 0b11111111). Bare hex/binary not supported to avoid ambiguity.`,
336
+ path: []
337
+ }
338
+ ]);
339
+ }
340
+ var byteValueSchema = import_zod2.z.preprocess(parseByte, import_zod2.z.number().int().min(0).max(255)).describe("8-bit byte value: decimal (255), hex with prefix ($FF, 0xFF), or binary with prefix (%11111111, 0b11111111)");
341
+ var byteArraySchema = import_zod2.z.array(byteValueSchema).describe('Array of byte values in mixed formats: [255, "$FF", "%11111111", 42]');
158
342
  var c64RegisterValueSchema = import_zod2.z.object(
159
343
  Object.fromEntries(
160
344
  C64_REGISTER_DEFINITIONS.map((register) => [
@@ -391,10 +575,15 @@ function publicMessageFor(error) {
391
575
  case "program_file_missing":
392
576
  case "program_file_invalid":
393
577
  return error.message;
578
+ case "binary_not_found":
579
+ case "spawn_failed":
580
+ case "emulator_crashed_on_startup":
581
+ return error.message;
394
582
  case "port_allocation_failed":
395
583
  case "port_in_use":
396
- case "monitor_timeout":
397
584
  return "The server could not start a usable emulator session. Check the emulator configuration and try again.";
585
+ case "monitor_timeout":
586
+ return error.message;
398
587
  case "not_connected":
399
588
  case "connection_closed":
400
589
  case "socket_write_failed":
@@ -425,14 +614,21 @@ function publicMessageFor(error) {
425
614
  }
426
615
  }
427
616
  function publicDetailsFor(error) {
428
- switch (error.category) {
429
- case "validation":
430
- case "session_state":
431
- case "unsupported":
432
- case "io":
617
+ switch (error.code) {
618
+ case "binary_not_found":
619
+ case "spawn_failed":
620
+ case "emulator_crashed_on_startup":
433
621
  return error.details;
434
622
  default:
435
- return void 0;
623
+ switch (error.category) {
624
+ case "validation":
625
+ case "session_state":
626
+ case "unsupported":
627
+ case "io":
628
+ return error.details;
629
+ default:
630
+ return void 0;
631
+ }
436
632
  }
437
633
  }
438
634
  function normalizeToolError(error) {
@@ -1550,6 +1746,10 @@ var ViceSession = class {
1550
1746
  this.#syncMonitorRuntimeState();
1551
1747
  });
1552
1748
  }
1749
+ async getSessionState() {
1750
+ await this.#ensureReady();
1751
+ return this.snapshot();
1752
+ }
1553
1753
  snapshot() {
1554
1754
  return {
1555
1755
  transportState: this.#transportState,
@@ -1786,6 +1986,17 @@ var ViceSession = class {
1786
1986
  async continueExecution(waitUntilRunningStable = false) {
1787
1987
  return this.#withExecutionLock(async () => {
1788
1988
  await this.#ensureReady();
1989
+ if (this.#executionState === "running") {
1990
+ const runtime2 = this.#client.runtimeState();
1991
+ const debugState2 = this.#lastRegisters == null ? await this.#readDebugState() : this.#buildDebugState(this.#lastRegisters);
1992
+ return {
1993
+ executionState: "running",
1994
+ lastStopReason: this.#lastStopReason,
1995
+ programCounter: runtime2.programCounter ?? debugState2.programCounter,
1996
+ registers: debugState2.registers,
1997
+ warnings: []
1998
+ };
1999
+ }
1789
2000
  if (this.#executionState !== "stopped") {
1790
2001
  debuggerNotPausedError("execute resume", {
1791
2002
  executionState: this.#executionState,
@@ -1964,7 +2175,6 @@ var ViceSession = class {
1964
2175
  async programLoad(options) {
1965
2176
  const filePath = import_node_path.default.resolve(options.filePath);
1966
2177
  await this.#assertReadableProgramFile(filePath);
1967
- await this.#ensureRunning("program_load");
1968
2178
  this.#explicitPauseActive = false;
1969
2179
  const result = await this.autostartProgram(filePath, options.autoStart ?? true, options.fileIndex ?? 0);
1970
2180
  return {
@@ -2215,134 +2425,137 @@ var ViceSession = class {
2215
2425
  }
2216
2426
  }
2217
2427
  async writeText(text) {
2218
- await this.#ensureRunning("write_text");
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
- };
2428
+ return await this.#withAutoResumeForInput("write_text", async () => {
2429
+ const encoded = decodeWriteTextToPetscii(text);
2430
+ if (encoded.length > MAX_WRITE_TEXT_BYTES) {
2431
+ validationError("write_text exceeds the maximum allowed byte length for one request", {
2432
+ length: encoded.length,
2433
+ max: MAX_WRITE_TEXT_BYTES
2434
+ });
2435
+ }
2436
+ this.#writeProcessLogLine(`[tx] write_text length=${encoded.length} text=${JSON.stringify(text)}`);
2437
+ await this.#client.sendKeys(Buffer.from(encoded).toString("binary"));
2438
+ await this.#settleInputState("write_text", "running");
2439
+ return {
2440
+ sent: true,
2441
+ length: encoded.length
2442
+ };
2443
+ });
2233
2444
  }
2234
2445
  async keyboardInput(action, keys, durationMs) {
2235
- await this.#ensureRunning("keyboard_input");
2236
- if (!Array.isArray(keys) || keys.length === 0 || keys.length > 4) {
2237
- validationError("keyboard_input requires between 1 and 4 keys", { keys });
2238
- }
2239
- const resolvedKeys = keys.map((key) => resolveKeyboardInputKey(key));
2240
- const normalizedKeys = resolvedKeys.map((key) => key.canonical);
2241
- this.#writeProcessLogLine(
2242
- `[tx] keyboard_input action=${action} keys=${normalizedKeys.join(",")}${durationMs == null ? "" : ` durationMs=${durationMs}`}`
2243
- );
2244
- switch (action) {
2245
- case "tap": {
2246
- const duration = clampTapDuration(durationMs);
2247
- const bytes = Uint8Array.from(resolvedKeys.flatMap((key) => Array.from(key.bytes)));
2248
- await this.#client.sendKeys(Buffer.from(bytes).toString("binary"));
2249
- await this.#settleInputState("keyboard_input", "running");
2250
- await sleep(duration);
2251
- return {
2252
- action,
2253
- keys: normalizedKeys,
2254
- applied: true,
2255
- held: false,
2256
- mode: "buffered_text"
2257
- };
2446
+ return await this.#withAutoResumeForInput("keyboard_input", async () => {
2447
+ if (!Array.isArray(keys) || keys.length === 0 || keys.length > 4) {
2448
+ validationError("keyboard_input requires between 1 and 4 keys", { keys });
2258
2449
  }
2259
- case "press": {
2260
- const singleByteKeys = resolvedKeys.map((key) => {
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
- });
2265
- }
2266
- return key.bytes[0];
2267
- });
2268
- for (let index = 0; index < normalizedKeys.length; index += 1) {
2269
- const heldKey = normalizedKeys[index];
2270
- const byte = singleByteKeys[index];
2271
- if (!this.#heldKeyboardIntervals.has(heldKey)) {
2272
- await this.#client.sendKeys(Buffer.from([byte]).toString("binary"));
2273
- await this.#settleInputState("keyboard_input", "running");
2274
- const interval = setInterval(() => {
2275
- void this.#client.sendKeys(Buffer.from([byte]).toString("binary")).then(() => this.#settleInputState("keyboard_input", "running")).catch(() => void 0);
2276
- }, DEFAULT_KEYBOARD_REPEAT_MS);
2277
- this.#heldKeyboardIntervals.set(heldKey, interval);
2278
- }
2450
+ const resolvedKeys = keys.map((key) => resolveKeyboardInputKey(key));
2451
+ const normalizedKeys = resolvedKeys.map((key) => key.canonical);
2452
+ this.#writeProcessLogLine(
2453
+ `[tx] keyboard_input action=${action} keys=${normalizedKeys.join(",")}${durationMs == null ? "" : ` durationMs=${durationMs}`}`
2454
+ );
2455
+ switch (action) {
2456
+ case "tap": {
2457
+ const duration = clampTapDuration(durationMs);
2458
+ const bytes = Uint8Array.from(resolvedKeys.flatMap((key) => Array.from(key.bytes)));
2459
+ await this.#client.sendKeys(Buffer.from(bytes).toString("binary"));
2460
+ await this.#settleInputState("keyboard_input", "running");
2461
+ await sleep(duration);
2462
+ return {
2463
+ action,
2464
+ keys: normalizedKeys,
2465
+ applied: true,
2466
+ held: false,
2467
+ mode: "buffered_text"
2468
+ };
2279
2469
  }
2280
- return {
2281
- action,
2282
- keys: normalizedKeys,
2283
- applied: true,
2284
- held: true,
2285
- mode: "buffered_text_repeat"
2286
- };
2287
- }
2288
- case "release": {
2289
- for (const key of resolvedKeys) {
2290
- if (key.bytes.length !== 1) {
2291
- unsupportedError("keyboard_input press/release only supports keys that map to a single PETSCII byte.", {
2292
- key: key.canonical
2293
- });
2470
+ case "press": {
2471
+ const singleByteKeys = resolvedKeys.map((key) => {
2472
+ if (key.bytes.length !== 1) {
2473
+ unsupportedError("keyboard_input press/release only supports keys that map to a single PETSCII byte.", {
2474
+ key: key.canonical
2475
+ });
2476
+ }
2477
+ return key.bytes[0];
2478
+ });
2479
+ for (let index = 0; index < normalizedKeys.length; index += 1) {
2480
+ const heldKey = normalizedKeys[index];
2481
+ const byte = singleByteKeys[index];
2482
+ if (!this.#heldKeyboardIntervals.has(heldKey)) {
2483
+ await this.#client.sendKeys(Buffer.from([byte]).toString("binary"));
2484
+ await this.#settleInputState("keyboard_input", "running");
2485
+ const interval = setInterval(() => {
2486
+ void this.#client.sendKeys(Buffer.from([byte]).toString("binary")).then(() => this.#settleInputState("keyboard_input", "running")).catch(() => void 0);
2487
+ }, DEFAULT_KEYBOARD_REPEAT_MS);
2488
+ this.#heldKeyboardIntervals.set(heldKey, interval);
2489
+ }
2294
2490
  }
2491
+ return {
2492
+ action,
2493
+ keys: normalizedKeys,
2494
+ applied: true,
2495
+ held: true,
2496
+ mode: "buffered_text_repeat"
2497
+ };
2295
2498
  }
2296
- for (const heldKey of normalizedKeys) {
2297
- const interval = this.#heldKeyboardIntervals.get(heldKey);
2298
- if (interval) {
2299
- clearInterval(interval);
2300
- this.#heldKeyboardIntervals.delete(heldKey);
2499
+ case "release": {
2500
+ for (const key of resolvedKeys) {
2501
+ if (key.bytes.length !== 1) {
2502
+ unsupportedError("keyboard_input press/release only supports keys that map to a single PETSCII byte.", {
2503
+ key: key.canonical
2504
+ });
2505
+ }
2301
2506
  }
2507
+ for (const heldKey of normalizedKeys) {
2508
+ const interval = this.#heldKeyboardIntervals.get(heldKey);
2509
+ if (interval) {
2510
+ clearInterval(interval);
2511
+ this.#heldKeyboardIntervals.delete(heldKey);
2512
+ }
2513
+ }
2514
+ return {
2515
+ action,
2516
+ keys: normalizedKeys,
2517
+ applied: true,
2518
+ held: false,
2519
+ mode: "buffered_text_repeat"
2520
+ };
2302
2521
  }
2303
- return {
2304
- action,
2305
- keys: normalizedKeys,
2306
- applied: true,
2307
- held: false,
2308
- mode: "buffered_text_repeat"
2309
- };
2310
2522
  }
2311
- }
2523
+ });
2312
2524
  }
2313
2525
  async joystickInput(port2, action, control, durationMs) {
2314
- await this.#ensureRunning("joystick_input");
2315
- const previousExecutionState = this.#executionState;
2316
- const bit = JOYSTICK_CONTROL_BITS[control];
2317
- if (bit == null) {
2318
- validationError("Unsupported joystick control", { control });
2319
- }
2320
- this.#writeProcessLogLine(
2321
- `[tx] joystick_input port=${port2} action=${action} control=${control}${durationMs == null ? "" : ` durationMs=${durationMs}`}`
2322
- );
2323
- switch (action) {
2324
- case "tap": {
2325
- const duration = clampTapDuration(durationMs);
2326
- await this.#applyJoystickMask(port2, this.#getJoystickMask(port2) & ~bit);
2327
- await sleep(duration);
2328
- await this.#applyJoystickMask(port2, this.#getJoystickMask(port2) | bit);
2329
- break;
2526
+ return await this.#withAutoResumeForInput("joystick_input", async () => {
2527
+ const previousExecutionState = this.#executionState;
2528
+ const bit = JOYSTICK_CONTROL_BITS[control];
2529
+ if (bit == null) {
2530
+ validationError("Unsupported joystick control", { control });
2330
2531
  }
2331
- case "press":
2332
- await this.#applyJoystickMask(port2, this.#getJoystickMask(port2) & ~bit);
2333
- break;
2334
- case "release":
2335
- await this.#applyJoystickMask(port2, this.#getJoystickMask(port2) | bit);
2336
- break;
2337
- }
2338
- await this.#settleInputState("joystick_input", previousExecutionState);
2339
- return {
2340
- port: port2,
2341
- action,
2342
- control,
2343
- applied: true,
2344
- state: this.#describeJoystickState(port2)
2345
- };
2532
+ this.#writeProcessLogLine(
2533
+ `[tx] joystick_input port=${port2} action=${action} control=${control}${durationMs == null ? "" : ` durationMs=${durationMs}`}`
2534
+ );
2535
+ switch (action) {
2536
+ case "tap": {
2537
+ const duration = clampTapDuration(durationMs);
2538
+ await this.#applyJoystickMask(port2, this.#getJoystickMask(port2) & ~bit);
2539
+ await sleep(duration);
2540
+ await this.#applyJoystickMask(port2, this.#getJoystickMask(port2) | bit);
2541
+ break;
2542
+ }
2543
+ case "press":
2544
+ await this.#applyJoystickMask(port2, this.#getJoystickMask(port2) & ~bit);
2545
+ break;
2546
+ case "release":
2547
+ await this.#applyJoystickMask(port2, this.#getJoystickMask(port2) | bit);
2548
+ break;
2549
+ }
2550
+ await this.#settleInputState("joystick_input", previousExecutionState);
2551
+ return {
2552
+ port: port2,
2553
+ action,
2554
+ control,
2555
+ applied: true,
2556
+ state: this.#describeJoystickState(port2)
2557
+ };
2558
+ });
2346
2559
  }
2347
2560
  async waitForState(targetState, timeoutMs = 5e3, stableMs = targetState === "running" ? INPUT_RUNNING_STABLE_MS : 0) {
2348
2561
  await this.#ensureReady();
@@ -2405,6 +2618,39 @@ var ViceSession = class {
2405
2618
  });
2406
2619
  }
2407
2620
  }
2621
+ async #withAutoResumeForInput(commandName, operation) {
2622
+ await this.#ensureReady();
2623
+ this.#syncMonitorRuntimeState();
2624
+ if (this.#executionState === "unknown") {
2625
+ throw new ViceMcpError(
2626
+ "emulator_state_unknown",
2627
+ `${commandName} requires a known execution state (running or stopped)`,
2628
+ "session_state",
2629
+ false,
2630
+ {
2631
+ executionState: this.#executionState,
2632
+ lastStopReason: this.#lastStopReason
2633
+ }
2634
+ );
2635
+ }
2636
+ const wasRunning = this.#executionState === "running";
2637
+ const wasPaused = this.#explicitPauseActive;
2638
+ if (this.#executionState === "stopped") {
2639
+ this.#writeProcessLogLine(`[${commandName}] auto-resuming for input operation`);
2640
+ await this.#client.continueExecution();
2641
+ await this.waitForState("running", 5e3, INPUT_RUNNING_STABLE_MS);
2642
+ }
2643
+ try {
2644
+ return await operation();
2645
+ } finally {
2646
+ if (!wasRunning && wasPaused) {
2647
+ this.#writeProcessLogLine(`[${commandName}] restoring paused state after input`);
2648
+ await this.#client.ping();
2649
+ await this.waitForState("stopped", 5e3, 0);
2650
+ this.#explicitPauseActive = true;
2651
+ }
2652
+ }
2653
+ }
2408
2654
  async #ensureHealthyConnection() {
2409
2655
  if (this.#recoveryPromise) {
2410
2656
  await this.#recoveryPromise;
@@ -2453,6 +2699,16 @@ var ViceSession = class {
2453
2699
  const port2 = await this.#portAllocator.allocate();
2454
2700
  await this.#portAllocator.ensureFree(port2, host2);
2455
2701
  const binary = config.binaryPath ?? DEFAULT_C64_BINARY;
2702
+ const binaryCheck = await checkBinaryExists(binary);
2703
+ if (!binaryCheck.exists) {
2704
+ throw new ViceMcpError(
2705
+ "binary_not_found",
2706
+ `VICE emulator binary '${binary}' not found. Please install VICE or configure the correct path using the 'binaryPath' setting.`,
2707
+ "process_launch",
2708
+ false,
2709
+ { binary, searchedPath: process.env.PATH }
2710
+ );
2711
+ }
2456
2712
  const args = ["-autostartprgmode", "1", "-binarymonitor", "-binarymonitoraddress", `${host2}:${port2}`];
2457
2713
  if (config.arguments) {
2458
2714
  args.push(...splitCommandLine(config.arguments));
@@ -2470,11 +2726,15 @@ var ViceSession = class {
2470
2726
  this.#lastRuntimeEventType = "unknown";
2471
2727
  this.#lastRuntimeProgramCounter = null;
2472
2728
  const env = await buildViceLaunchEnv();
2729
+ let spawnError = void 0;
2473
2730
  const child = (0, import_node_child_process.spawn)(binary, args, {
2474
2731
  cwd: config.workingDirectory ? import_node_path.default.resolve(config.workingDirectory) : void 0,
2475
2732
  env,
2476
2733
  stdio: ["ignore", "pipe", "pipe"]
2477
2734
  });
2735
+ child.once("error", (err) => {
2736
+ spawnError = err;
2737
+ });
2478
2738
  this.#process = child;
2479
2739
  this.#attachProcessLogging(child, binary, args);
2480
2740
  this.#bindProcessLifecycle(child);
@@ -2482,6 +2742,16 @@ var ViceSession = class {
2482
2742
  this.#transportState = "waiting_for_monitor";
2483
2743
  try {
2484
2744
  await waitForMonitor(host2, port2, 5e3);
2745
+ if (spawnError !== void 0) {
2746
+ const err = spawnError;
2747
+ throw new ViceMcpError(
2748
+ "spawn_failed",
2749
+ `Failed to start VICE emulator '${binary}': ${err.message}`,
2750
+ "process_launch",
2751
+ false,
2752
+ { binary, error: err.message, resolvedPath: binaryCheck.path }
2753
+ );
2754
+ }
2485
2755
  await this.#client.connect(host2, port2);
2486
2756
  this.#transportState = "connected";
2487
2757
  this.#connectedSince = nowIso();
@@ -2494,6 +2764,19 @@ var ViceSession = class {
2494
2764
  } catch (error) {
2495
2765
  this.#processState = "crashed";
2496
2766
  this.#transportState = "faulted";
2767
+ if (error instanceof ViceMcpError && error.code === "monitor_timeout" && spawnError !== void 0) {
2768
+ const err = spawnError;
2769
+ const enhancedError = new ViceMcpError(
2770
+ "emulator_crashed_on_startup",
2771
+ `VICE emulator '${binary}' crashed during startup: ${err.message}`,
2772
+ "process_launch",
2773
+ false,
2774
+ { binary, error: err.message, resolvedPath: binaryCheck.path }
2775
+ );
2776
+ this.#warnings = [...this.#warnings.filter((warning) => warning.code !== "launch_failed"), makeWarning(enhancedError.message, "launch_failed")];
2777
+ await this.#stopManagedProcess(true);
2778
+ throw enhancedError;
2779
+ }
2497
2780
  this.#warnings = [...this.#warnings.filter((warning) => warning.code !== "launch_failed"), makeWarning(String(error.message ?? error), "launch_failed")];
2498
2781
  await this.#stopManagedProcess(true);
2499
2782
  throw error;
@@ -3234,6 +3517,27 @@ function splitCommandLine(input) {
3234
3517
  }
3235
3518
  return result;
3236
3519
  }
3520
+ async function checkBinaryExists(binaryPath) {
3521
+ if (import_node_path.default.isAbsolute(binaryPath)) {
3522
+ try {
3523
+ await import_promises.default.access(binaryPath, import_promises.default.constants.X_OK);
3524
+ return { exists: true, path: binaryPath };
3525
+ } catch {
3526
+ return { exists: false };
3527
+ }
3528
+ }
3529
+ const pathEnv = process.env.PATH || "";
3530
+ const pathDirs = pathEnv.split(import_node_path.default.delimiter);
3531
+ for (const dir of pathDirs) {
3532
+ const fullPath = import_node_path.default.join(dir, binaryPath);
3533
+ try {
3534
+ await import_promises.default.access(fullPath, import_promises.default.constants.X_OK);
3535
+ return { exists: true, path: fullPath };
3536
+ } catch {
3537
+ }
3538
+ }
3539
+ return { exists: false };
3540
+ }
3237
3541
  async function waitForMonitor(host2, port2, timeoutMs) {
3238
3542
  const deadline = Date.now() + timeoutMs;
3239
3543
  while (Date.now() < deadline) {
@@ -3242,7 +3546,7 @@ async function waitForMonitor(host2, port2, timeoutMs) {
3242
3546
  }
3243
3547
  await sleep(100);
3244
3548
  }
3245
- throw new ViceMcpError("monitor_timeout", `Debugger monitor did not open on ${host2}:${port2}`, "timeout", true, {
3549
+ 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, {
3246
3550
  host: host2,
3247
3551
  port: port2
3248
3552
  });
@@ -3307,7 +3611,7 @@ var getSessionStateTool = createViceTool({
3307
3611
  description: "Returns emulator session state including transport/process status, auto-resume state, and the most recent hit checkpoint.",
3308
3612
  inputSchema: noInputSchema,
3309
3613
  dataSchema: sessionStateResultSchema,
3310
- execute: async () => c64Session.snapshot()
3614
+ execute: async () => await c64Session.getSessionState()
3311
3615
  });
3312
3616
  var getRegistersTool = createViceTool({
3313
3617
  id: "get_registers",
@@ -3332,9 +3636,9 @@ var setRegistersTool = createViceTool({
3332
3636
  });
3333
3637
  var readMemoryTool = createViceTool({
3334
3638
  id: "memory_read",
3335
- description: "Reads a memory chunk by start address and length and returns raw bytes as a JSON array.",
3639
+ description: "Reads a memory chunk by start address and length. Address can be decimal (53248) or hex string with prefix ($D000, 0xD000). Returns byte values as decimal numbers.",
3336
3640
  inputSchema: import_zod4.z.object({
3337
- address: address16Schema.describe("Start address in the 16-bit C64 address space"),
3641
+ address: address16Schema.describe("Start address: decimal (53248) or hex string with prefix ($D000, 0xD000)"),
3338
3642
  length: import_zod4.z.number().int().positive().max(65535).describe("Size of the data chunk to read in bytes")
3339
3643
  }).refine((input) => input.address + input.length <= 65536, {
3340
3644
  message: "address + length must stay within the 64K address space",
@@ -3356,10 +3660,10 @@ var readMemoryTool = createViceTool({
3356
3660
  });
3357
3661
  var writeMemoryTool = createViceTool({
3358
3662
  id: "memory_write",
3359
- description: 'Writes raw byte values into the active C64 memory space. Requires emulator to be stopped - call execute(action="pause") first if running.',
3663
+ description: "Writes raw byte values into C64 memory. Address and byte values support decimal, hex ($FF, 0xFF), and binary (%11111111, 0b11111111) formats. Requires emulator to be stopped.",
3360
3664
  inputSchema: import_zod4.z.object({
3361
- address: address16Schema.describe("Start address in the 16-bit C64 address space"),
3362
- data: byteArraySchema.min(1).describe("Raw bytes to write into memory")
3665
+ address: address16Schema.describe("Start address: decimal (53248) or hex string with prefix ($D000, 0xD000)"),
3666
+ data: byteArraySchema.min(1).describe("Bytes to write: decimal (255), hex ($FF, 0xFF), or binary (%11111111, 0b11111111). Mixed formats allowed.")
3363
3667
  }).refine((input) => input.address + input.data.length - 1 <= 65535, {
3364
3668
  message: "address + data.length must stay within the 16-bit address space",
3365
3669
  path: ["data"]
@@ -3373,7 +3677,7 @@ var writeMemoryTool = createViceTool({
3373
3677
  });
3374
3678
  var executeTool = createViceTool({
3375
3679
  id: "execute",
3376
- description: "Controls execution with pause, resume, step, step_over, step_out, or reset. Resume can optionally wait until running becomes stable.",
3680
+ description: "Controls execution with pause, resume, step, step_over, step_out, or reset. Pause and resume are idempotent (safe to call multiple times).",
3377
3681
  inputSchema: import_zod4.z.object({
3378
3682
  action: import_zod4.z.enum(["pause", "resume", "step", "step_over", "step_out", "reset"]),
3379
3683
  count: import_zod4.z.number().int().positive().default(1).describe("Instruction count for step and step_over actions"),
@@ -3415,10 +3719,10 @@ var listBreakpointsTool = createViceTool({
3415
3719
  });
3416
3720
  var breakpointSetTool = createViceTool({
3417
3721
  id: "breakpoint_set",
3418
- description: "Creates an execution breakpoint or read/write watchpoint.",
3722
+ description: "Creates an execution breakpoint or read/write watchpoint. Address can be decimal (53248) or hex string with prefix ($D000, 0xD000).",
3419
3723
  inputSchema: import_zod4.z.object({
3420
3724
  kind: breakpointKindSchema,
3421
- address: address16Schema.describe("Start address of the breakpoint range"),
3725
+ address: address16Schema.describe("Start address: decimal (53248) or hex string with prefix ($D000, 0xD000)"),
3422
3726
  length: import_zod4.z.number().int().positive().default(1).describe("Size of the breakpoint range in bytes"),
3423
3727
  condition: import_zod4.z.string().optional(),
3424
3728
  label: import_zod4.z.string().optional(),
@@ -3495,7 +3799,7 @@ var getDisplayTextTool = createViceTool({
3495
3799
  });
3496
3800
  var writeTextTool = createViceTool({
3497
3801
  id: "write_text",
3498
- 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.',
3802
+ 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.",
3499
3803
  inputSchema: import_zod4.z.object({
3500
3804
  text: import_zod4.z.string()
3501
3805
  }),
@@ -3507,7 +3811,7 @@ var writeTextTool = createViceTool({
3507
3811
  });
3508
3812
  var keyboardInputTool = createViceTool({
3509
3813
  id: "keyboard_input",
3510
- 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.',
3814
+ 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.",
3511
3815
  inputSchema: import_zod4.z.object({
3512
3816
  action: inputActionSchema.describe("Use tap for a single key event or press/release for repeated buffered input"),
3513
3817
  keys: import_zod4.z.array(import_zod4.z.string().min(1)).min(1).max(4).describe("One to four literal keys or PETSCII token names such as RETURN, CLR, HOME, PI, LEFT, RED, or F1"),
@@ -3518,7 +3822,7 @@ var keyboardInputTool = createViceTool({
3518
3822
  });
3519
3823
  var joystickInputTool = createViceTool({
3520
3824
  id: "joystick_input",
3521
- description: 'Sends joystick input to C64 joystick port 1 or 2. Requires emulator to be running - call execute(action="resume") first if stopped.',
3825
+ description: "Sends joystick input to C64 joystick port 1 or 2. Automatically resumes if stopped and restores pause state after.",
3522
3826
  inputSchema: import_zod4.z.object({
3523
3827
  port: joystickPortSchema.describe("Joystick port number"),
3524
3828
  action: inputActionSchema.describe("Joystick action to apply"),