c64-debug-mcp 0.1.0

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 ADDED
@@ -0,0 +1,3580 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/http.ts
4
+ import http from "http";
5
+
6
+ // src/server.ts
7
+ import { createTool } from "@mastra/core/tools";
8
+ import { MCPServer } from "@mastra/mcp";
9
+ import { z as z3 } from "zod";
10
+
11
+ // src/contracts.ts
12
+ import { z } from "zod";
13
+ var VICE_API_VERSION = 2;
14
+ var VICE_STX = 2;
15
+ var VICE_BROADCAST_REQUEST_ID = 4294967295;
16
+ var DEFAULT_MONITOR_HOST = "127.0.0.1";
17
+ var DEFAULT_C64_BINARY = "x64sc";
18
+ var DEFAULT_FORBIDDEN_PORTS = /* @__PURE__ */ new Set([6502]);
19
+ var transportStateSchema = z.enum([
20
+ "not_started",
21
+ "starting",
22
+ "waiting_for_monitor",
23
+ "connecting",
24
+ "connected",
25
+ "reconnecting",
26
+ "disconnected",
27
+ "stopped",
28
+ "faulted"
29
+ ]);
30
+ var processStateSchema = z.enum(["not_applicable", "launching", "running", "exited", "crashed"]);
31
+ var executionStateSchema = z.enum(["unknown", "running", "stopped"]);
32
+ var stopReasonSchema = z.enum([
33
+ "none",
34
+ "breakpoint",
35
+ "watchpoint_read",
36
+ "watchpoint_write",
37
+ "step_complete",
38
+ "reset",
39
+ "monitor_entry",
40
+ "program_end",
41
+ "error",
42
+ "unknown"
43
+ ]);
44
+ var sessionHealthSchema = z.enum(["not_configured", "starting", "ready", "recovering", "stopped", "error"]);
45
+ var breakpointKindSchema = z.enum(["exec", "read", "write", "read_write"]);
46
+ var resetModeSchema = z.enum(["soft", "hard"]);
47
+ var inputActionSchema = z.enum(["press", "release", "tap"]);
48
+ var joystickControlSchema = z.enum(["up", "down", "left", "right", "fire"]);
49
+ var joystickPortSchema = z.union([z.literal(1), z.literal(2)]);
50
+ var toolErrorCategorySchema = z.enum([
51
+ "validation",
52
+ "configuration",
53
+ "session_state",
54
+ "process_launch",
55
+ "connection",
56
+ "protocol",
57
+ "timeout",
58
+ "io",
59
+ "unsupported",
60
+ "internal"
61
+ ]);
62
+ var warningItemSchema = z.object({
63
+ code: z.string(),
64
+ message: z.string()
65
+ });
66
+ var c64ConfigSchema = z.object({
67
+ binaryPath: z.string().optional(),
68
+ workingDirectory: z.string().optional(),
69
+ arguments: z.string().optional()
70
+ });
71
+ var responseMetaSchema = z.object({
72
+ freshEmulator: z.boolean(),
73
+ launchId: z.number().int().nonnegative(),
74
+ restartCount: z.number().int().nonnegative()
75
+ });
76
+ var C64_REGISTER_DEFINITIONS = [
77
+ { fieldName: "PC", viceName: "PC", widthBits: 16, min: 0, max: 65535, description: "Program counter register" },
78
+ { fieldName: "A", viceName: "A", widthBits: 8, min: 0, max: 255, description: "Accumulator register" },
79
+ { fieldName: "X", viceName: "X", widthBits: 8, min: 0, max: 255, description: "X index register" },
80
+ { fieldName: "Y", viceName: "Y", widthBits: 8, min: 0, max: 255, description: "Y index register" },
81
+ { fieldName: "SP", viceName: "SP", widthBits: 8, min: 0, max: 255, description: "Stack pointer register" },
82
+ { fieldName: "FL", viceName: "FL", widthBits: 8, min: 0, max: 255, description: "CPU flags register" },
83
+ { fieldName: "00", viceName: "00", widthBits: 8, min: 0, max: 255, description: "Zero-page processor port register 00" },
84
+ { fieldName: "01", viceName: "01", widthBits: 8, min: 0, max: 255, description: "Zero-page processor port register 01" },
85
+ { fieldName: "LIN", viceName: "LIN", widthBits: 16, min: 0, max: 65535, description: "Current raster line register" },
86
+ { fieldName: "CYC", viceName: "CYC", widthBits: 16, min: 0, max: 65535, description: "Current cycle position register" }
87
+ ];
88
+ var toolErrorSchema = z.object({
89
+ code: z.string(),
90
+ message: z.string(),
91
+ category: toolErrorCategorySchema,
92
+ retryable: z.boolean(),
93
+ details: z.record(z.unknown()).optional()
94
+ });
95
+ function mainMemSpaceToProtocol() {
96
+ return 0;
97
+ }
98
+ function breakpointKindToOperation(kind) {
99
+ switch (kind) {
100
+ case "read":
101
+ return 1;
102
+ case "write":
103
+ return 2;
104
+ case "read_write":
105
+ return 3;
106
+ case "exec":
107
+ return 4;
108
+ }
109
+ }
110
+ function cpuOperationToBreakpointKind(operation) {
111
+ if ((operation & 4) === 4) {
112
+ return "exec";
113
+ }
114
+ if ((operation & 1) === 1 && (operation & 2) === 2) {
115
+ return "read_write";
116
+ }
117
+ if ((operation & 1) === 1) {
118
+ return "read";
119
+ }
120
+ if ((operation & 2) === 2) {
121
+ return "write";
122
+ }
123
+ return "exec";
124
+ }
125
+
126
+ // src/errors.ts
127
+ import { ZodError } from "zod";
128
+
129
+ // src/schemas.ts
130
+ import { z as z2 } from "zod";
131
+ var warningSchema = warningItemSchema;
132
+ var address16Schema = z2.number().int().min(0).max(65535);
133
+ var byteValueSchema = z2.number().int().min(0).max(255).describe("Single raw byte value");
134
+ var byteArraySchema = z2.array(byteValueSchema).describe("Raw bytes as a JSON array");
135
+ var c64RegisterValueSchema = z2.object(
136
+ Object.fromEntries(
137
+ C64_REGISTER_DEFINITIONS.map((register) => [
138
+ register.fieldName,
139
+ z2.number().int().min(register.min).max(register.max).describe(register.description)
140
+ ])
141
+ )
142
+ );
143
+ var c64PartialRegisterValueSchema = z2.object(
144
+ Object.fromEntries(
145
+ C64_REGISTER_DEFINITIONS.map((register) => [
146
+ register.fieldName,
147
+ z2.number().int().min(register.min).max(register.max).describe(register.description).optional()
148
+ ])
149
+ )
150
+ );
151
+ var debugStateSchema = z2.object({
152
+ executionState: executionStateSchema.describe("Current execution state of the emulator"),
153
+ lastStopReason: stopReasonSchema.describe("Reason the emulator most recently stopped in the monitor"),
154
+ programCounter: address16Schema.describe("Current program counter"),
155
+ registers: c64RegisterValueSchema
156
+ });
157
+ var monitorStateSchema = z2.object({
158
+ executionState: executionStateSchema.describe("Current execution state reported by the monitor"),
159
+ lastStopReason: stopReasonSchema.describe("Reason the monitor most recently reported a stop-like state"),
160
+ runtimeKnown: z2.boolean().describe("Whether the monitor has reported a runtime event in this session"),
161
+ programCounter: address16Schema.nullable().describe("Program counter from the latest monitor event, or null if unknown")
162
+ });
163
+ var sessionStateResultSchema = z2.object({
164
+ transportState: z2.enum([
165
+ "not_started",
166
+ "starting",
167
+ "waiting_for_monitor",
168
+ "connecting",
169
+ "connected",
170
+ "reconnecting",
171
+ "disconnected",
172
+ "stopped",
173
+ "faulted"
174
+ ]),
175
+ processState: z2.enum(["not_applicable", "launching", "running", "exited", "crashed"]),
176
+ executionState: executionStateSchema.describe("Current execution state of the emulator session"),
177
+ lastStopReason: stopReasonSchema.describe("Reason the emulator most recently stopped in the monitor"),
178
+ idleAutoResumeArmed: z2.boolean().describe("Whether the idle auto-resume timer is currently armed"),
179
+ explicitPauseActive: z2.boolean().describe("Whether execution was explicitly paused by the caller"),
180
+ lastCheckpointId: z2.number().int().nullable().describe("Most recent hit checkpoint/watchpoint id when known"),
181
+ lastCheckpointKind: breakpointKindSchema.nullable().describe("Most recent hit checkpoint/watchpoint kind when known"),
182
+ recoveryInProgress: z2.boolean(),
183
+ launchId: z2.number().int().nonnegative(),
184
+ restartCount: z2.number().int().nonnegative(),
185
+ freshEmulatorPending: z2.boolean(),
186
+ connectedSince: z2.string().nullable(),
187
+ lastResponseAt: z2.string().nullable(),
188
+ processId: z2.number().int().nullable(),
189
+ warnings: z2.array(warningSchema)
190
+ });
191
+ var breakpointSchema = z2.object({
192
+ id: z2.number().int().describe("Breakpoint identifier"),
193
+ address: address16Schema.describe("Start address of the breakpoint range"),
194
+ length: z2.number().int().positive().describe("Size of the breakpoint range in bytes"),
195
+ enabled: z2.boolean().describe("Whether the breakpoint is enabled"),
196
+ temporary: z2.boolean().describe("Whether the breakpoint is temporary"),
197
+ hasCondition: z2.boolean().describe("Whether the breakpoint has a condition expression"),
198
+ kind: breakpointKindSchema.describe("Breakpoint trigger kind"),
199
+ label: z2.string().nullable().optional().describe("Optional caller-provided label")
200
+ });
201
+ var joystickStateSchema = z2.object({
202
+ up: z2.boolean().describe("Whether up is currently held on the selected joystick port"),
203
+ down: z2.boolean().describe("Whether down is currently held on the selected joystick port"),
204
+ left: z2.boolean().describe("Whether left is currently held on the selected joystick port"),
205
+ right: z2.boolean().describe("Whether right is currently held on the selected joystick port"),
206
+ fire: z2.boolean().describe("Whether fire is currently held on the selected joystick port")
207
+ });
208
+ var programLoadResultSchema = z2.object({
209
+ filePath: z2.string().describe("Absolute path to the program file that was loaded"),
210
+ autoStart: z2.boolean().describe("Whether the loaded program was requested to start immediately after loading"),
211
+ fileIndex: z2.number().int().nonnegative().describe("Autostart file index inside the image, when applicable"),
212
+ executionState: executionStateSchema.describe("Execution state after the monitor-driven load request")
213
+ });
214
+ var captureDisplayResultSchema = z2.object({
215
+ imagePath: z2.string().describe("Absolute path to the rendered PNG image"),
216
+ width: z2.number().int().positive().describe("Width of the visible rendered screen image"),
217
+ height: z2.number().int().positive().describe("Height of the visible rendered screen image"),
218
+ debugWidth: z2.number().int().positive().describe("Width of the full uncropped debug display buffer"),
219
+ debugHeight: z2.number().int().positive().describe("Height of the full uncropped debug display buffer"),
220
+ debugOffsetX: z2.number().int().nonnegative().describe("X offset of the visible inner area within the debug display buffer"),
221
+ debugOffsetY: z2.number().int().nonnegative().describe("Y offset of the visible inner area within the debug display buffer"),
222
+ bitsPerPixel: z2.number().int().positive().describe("Bits per pixel reported by the emulator display payload")
223
+ });
224
+ var graphicsModeSchema = z2.enum([
225
+ "standard_text",
226
+ "multicolor_text",
227
+ "standard_bitmap",
228
+ "multicolor_bitmap",
229
+ "extended_background_color_text",
230
+ "invalid_text_mode",
231
+ "invalid_bitmap_mode_1",
232
+ "invalid_bitmap_mode_2"
233
+ ]);
234
+ var displayStateResultSchema = z2.object({
235
+ graphicsMode: graphicsModeSchema.describe("Decoded VIC-II graphics mode from D011/D016"),
236
+ extendedColorMode: z2.boolean().describe("Whether extended background color mode is enabled"),
237
+ bitmapMode: z2.boolean().describe("Whether bitmap mode is enabled"),
238
+ multicolorMode: z2.boolean().describe("Whether multicolor mode is enabled"),
239
+ vicBankAddress: address16Schema.describe("Base address of the active 16K VIC bank"),
240
+ screenRamAddress: address16Schema.describe("Base address of the active 1000-byte screen matrix"),
241
+ characterMemoryAddress: address16Schema.nullable().describe("Base address of the active character memory when the current mode uses character data"),
242
+ bitmapMemoryAddress: address16Schema.nullable().describe("Base address of the active bitmap memory when the current mode uses bitmap data"),
243
+ colorRamAddress: address16Schema.describe("Base address of the 1000-byte color RAM area"),
244
+ borderColor: z2.number().int().min(0).max(15).describe("Current border color value"),
245
+ backgroundColor0: z2.number().int().min(0).max(15).describe("Current background color 0 value"),
246
+ backgroundColor1: z2.number().int().min(0).max(15).describe("Current background color 1 value"),
247
+ backgroundColor2: z2.number().int().min(0).max(15).describe("Current background color 2 value"),
248
+ backgroundColor3: z2.number().int().min(0).max(15).describe("Current background color 3 value"),
249
+ vicRegisters: z2.object({
250
+ d011: byteValueSchema.describe("Raw VIC-II register $D011"),
251
+ d016: byteValueSchema.describe("Raw VIC-II register $D016"),
252
+ d018: byteValueSchema.describe("Raw VIC-II register $D018"),
253
+ dd00: byteValueSchema.describe("Raw CIA2/VIC bank register $DD00"),
254
+ d020: byteValueSchema.describe("Raw border color register $D020"),
255
+ d021: byteValueSchema.describe("Raw background color register $D021"),
256
+ d022: byteValueSchema.describe("Raw background color register $D022"),
257
+ d023: byteValueSchema.describe("Raw background color register $D023"),
258
+ d024: byteValueSchema.describe("Raw background color register $D024")
259
+ }),
260
+ screenRam: byteArraySchema.describe("Raw 1000-byte screen matrix contents"),
261
+ colorRam: byteArraySchema.describe("Raw 1000-byte color RAM contents, masked to the low nybble")
262
+ });
263
+ var displayTextResultSchema = z2.object({
264
+ graphicsMode: graphicsModeSchema.describe("Decoded VIC-II graphics mode from D011/D016"),
265
+ textMode: z2.boolean().describe("Whether the current graphics mode supports direct screen-text decoding"),
266
+ lossy: z2.boolean().describe("Whether the screen-code to ASCII translation may lose C64-specific glyph information"),
267
+ columns: z2.number().int().positive().describe("Number of text columns decoded per row"),
268
+ rows: z2.number().int().positive().describe("Number of decoded text rows"),
269
+ screenRamAddress: address16Schema.describe("Base address of the active 1000-byte screen matrix"),
270
+ textLines: z2.array(z2.string()).describe("Decoded text rows from screen RAM, trimmed on the right"),
271
+ tokenLines: z2.array(z2.string()).optional().describe("Optional richer tokenized rows, included only when non-ASCII or ambiguous C64 glyphs are present")
272
+ });
273
+ var keyboardInputResultSchema = z2.object({
274
+ action: inputActionSchema.describe("Keyboard action that was applied"),
275
+ keys: z2.array(z2.string()).min(1).max(4).describe("Normalized symbolic key names"),
276
+ applied: z2.boolean().describe("Whether the request was accepted and applied"),
277
+ held: z2.boolean().describe("Whether the keys are still treated as held after this request"),
278
+ mode: z2.enum(["buffered_text", "buffered_text_repeat"]).describe("Keyboard delivery mode supported by the emulator debug connection")
279
+ });
280
+ var joystickInputResultSchema = z2.object({
281
+ port: joystickPortSchema.describe("Joystick port that received the input"),
282
+ action: inputActionSchema.describe("Joystick action that was applied"),
283
+ control: joystickControlSchema.describe("Joystick control that was applied"),
284
+ applied: z2.boolean().describe("Whether the request was accepted and applied"),
285
+ state: joystickStateSchema
286
+ });
287
+ var waitForStateResultSchema = z2.object({
288
+ executionState: executionStateSchema.describe("Current execution state after waiting"),
289
+ lastStopReason: stopReasonSchema.describe("Reason the emulator most recently stopped in the monitor"),
290
+ runtimeKnown: z2.boolean().describe("Whether the monitor has reported a runtime event in this session"),
291
+ programCounter: address16Schema.nullable().describe("Program counter from the latest monitor event, or null if unknown"),
292
+ reachedTarget: z2.boolean().describe("Whether the requested target state was reached before timeout"),
293
+ waitedMs: z2.number().int().nonnegative().describe("Milliseconds spent waiting")
294
+ });
295
+ function toolOutputSchema(dataSchema) {
296
+ return z2.object({
297
+ meta: responseMetaSchema,
298
+ data: dataSchema
299
+ });
300
+ }
301
+
302
+ // src/errors.ts
303
+ var ViceMcpError = class extends Error {
304
+ code;
305
+ category;
306
+ retryable;
307
+ details;
308
+ constructor(code, message, category, retryable = false, details) {
309
+ super(message);
310
+ this.name = "ViceMcpError";
311
+ this.code = code;
312
+ this.category = category;
313
+ this.retryable = retryable;
314
+ this.details = details;
315
+ }
316
+ };
317
+ function validationError(message, details) {
318
+ throw new ViceMcpError("validation_error", message, "validation", false, details);
319
+ }
320
+ function debuggerNotPausedError(commandName, details) {
321
+ throw new ViceMcpError(
322
+ "debugger_not_paused",
323
+ `${commandName} can only be executed when the emulator is stopped.`,
324
+ "session_state",
325
+ false,
326
+ { commandName, requiredState: "stopped", ...details }
327
+ );
328
+ }
329
+ function emulatorNotRunningError(commandName, details) {
330
+ throw new ViceMcpError(
331
+ "emulator_not_running",
332
+ `${commandName} can only be executed when the emulator is running.`,
333
+ "session_state",
334
+ false,
335
+ { commandName, requiredState: "running", ...details }
336
+ );
337
+ }
338
+ function unsupportedError(message, details) {
339
+ throw new ViceMcpError("unsupported", message, "unsupported", false, details);
340
+ }
341
+ function asToolError(error) {
342
+ return toolErrorSchema.parse({
343
+ code: error.code,
344
+ message: error.message,
345
+ category: error.category,
346
+ retryable: error.retryable,
347
+ details: error.details
348
+ });
349
+ }
350
+ function zodDetails(error) {
351
+ return {
352
+ issues: error.issues.map((issue) => ({
353
+ code: issue.code,
354
+ message: issue.message,
355
+ path: issue.path
356
+ }))
357
+ };
358
+ }
359
+ function publicMessageFor(error) {
360
+ switch (error.code) {
361
+ case "debugger_not_paused":
362
+ return error.message;
363
+ case "emulator_not_running":
364
+ return error.message;
365
+ case "validation_error":
366
+ case "invalid_prg":
367
+ case "unsupported":
368
+ case "program_file_missing":
369
+ case "program_file_invalid":
370
+ return error.message;
371
+ case "port_allocation_failed":
372
+ case "port_in_use":
373
+ case "monitor_timeout":
374
+ return "The server could not start a usable emulator session. Check the emulator configuration and try again.";
375
+ case "not_connected":
376
+ case "connection_closed":
377
+ case "socket_write_failed":
378
+ case "timeout":
379
+ return "The server could not communicate with the emulator. Try the request again.";
380
+ case "protocol_invalid_stx":
381
+ case "emulator_protocol_error":
382
+ return "The emulator returned an unexpected debugger response. Try the request again.";
383
+ default:
384
+ switch (error.category) {
385
+ case "validation":
386
+ case "session_state":
387
+ case "unsupported":
388
+ return error.message;
389
+ case "configuration":
390
+ case "process_launch":
391
+ return "The server could not start the emulator with the current configuration.";
392
+ case "connection":
393
+ case "timeout":
394
+ return "The server could not communicate with the emulator. Try the request again.";
395
+ case "protocol":
396
+ return "The emulator returned an unexpected debugger response. Try the request again.";
397
+ case "io":
398
+ return "The requested file operation could not be completed.";
399
+ case "internal":
400
+ return "The server hit an unexpected error.";
401
+ }
402
+ }
403
+ }
404
+ function publicDetailsFor(error) {
405
+ switch (error.category) {
406
+ case "validation":
407
+ case "session_state":
408
+ case "unsupported":
409
+ case "io":
410
+ return error.details;
411
+ default:
412
+ return void 0;
413
+ }
414
+ }
415
+ function normalizeToolError(error) {
416
+ if (error instanceof ViceMcpError) {
417
+ const normalized = asToolError(error);
418
+ return new ViceMcpError(
419
+ normalized.code,
420
+ publicMessageFor(error),
421
+ normalized.category,
422
+ normalized.retryable,
423
+ publicDetailsFor(error)
424
+ );
425
+ }
426
+ if (error instanceof ZodError) {
427
+ return new ViceMcpError(
428
+ "validation_error",
429
+ error.issues.map((issue) => issue.message).join("; ") || "Validation failed",
430
+ "validation",
431
+ false,
432
+ zodDetails(error)
433
+ );
434
+ }
435
+ if (error instanceof Error) {
436
+ return new ViceMcpError("internal_error", "The server hit an unexpected error.", "internal", false);
437
+ }
438
+ return new ViceMcpError("internal_error", "The server hit an unexpected error.", "internal", false);
439
+ }
440
+
441
+ // src/session.ts
442
+ import fs from "fs/promises";
443
+ import { createWriteStream } from "fs";
444
+ import net2 from "net";
445
+ import os from "os";
446
+ import path from "path";
447
+ import { spawn } from "child_process";
448
+ import zlib from "zlib";
449
+
450
+ // src/vice-protocol.ts
451
+ import { EventEmitter } from "events";
452
+ import net from "net";
453
+ function parseLittleEndianVariableWidth(bytes) {
454
+ let value = 0;
455
+ for (let index = 0; index < bytes.length; index += 1) {
456
+ value += (bytes[index] ?? 0) * 2 ** (index * 8);
457
+ }
458
+ return value;
459
+ }
460
+ function encodeHeader(commandType, requestId, body) {
461
+ const header = Buffer.alloc(11);
462
+ header[0] = VICE_STX;
463
+ header[1] = VICE_API_VERSION;
464
+ header.writeUInt32LE(body.length, 2);
465
+ header.writeUInt32LE(requestId, 6);
466
+ header[10] = commandType;
467
+ return Buffer.concat([header, body]);
468
+ }
469
+ function parseBuffer(buffer) {
470
+ const responses = [];
471
+ let offset = 0;
472
+ while (offset + 12 <= buffer.length) {
473
+ if (buffer[offset] !== VICE_STX) {
474
+ throw new ViceMcpError("protocol_invalid_stx", "Invalid response prefix from emulator debug connection", "protocol");
475
+ }
476
+ const bodyLength = buffer.readUInt32LE(offset + 2);
477
+ const frameLength = 12 + bodyLength;
478
+ if (offset + frameLength > buffer.length) {
479
+ break;
480
+ }
481
+ const body = buffer.subarray(offset + 12, offset + frameLength);
482
+ const responseType = buffer[offset + 6];
483
+ const errorCode = buffer[offset + 7];
484
+ const requestId = buffer.readUInt32LE(offset + 8);
485
+ responses.push(parseResponse(responseType, errorCode, requestId, body));
486
+ offset += frameLength;
487
+ }
488
+ return { responses, remainder: buffer.subarray(offset) };
489
+ }
490
+ function parseResponse(responseType, errorCode, requestId, body) {
491
+ switch (responseType) {
492
+ case 1 /* MemoryGet */: {
493
+ const length = errorCode === 0 /* OK */ ? body.readUInt16LE(0) : 0;
494
+ return { type: "memory_get", requestId, errorCode, bytes: body.subarray(2, 2 + length) };
495
+ }
496
+ case 49 /* RegisterInfo */: {
497
+ const count = errorCode === 0 /* OK */ ? body.readUInt16LE(0) : 0;
498
+ const registers = Array.from({ length: count }, (_, index) => {
499
+ const start = 2 + index * 4;
500
+ return { id: body[start + 1], value: body.readUInt16LE(start + 2) };
501
+ });
502
+ return { type: "registers", requestId, errorCode, registers };
503
+ }
504
+ case 131 /* RegistersAvailable */: {
505
+ const count = errorCode === 0 /* OK */ ? body.readUInt16LE(0) : 0;
506
+ let offset = 2;
507
+ const registers = [];
508
+ for (let index = 0; index < count; index += 1) {
509
+ const itemSize = body[offset];
510
+ const id = body[offset + 1];
511
+ const size = body[offset + 2];
512
+ const nameLength = body[offset + 3];
513
+ const name = body.subarray(offset + 4, offset + 4 + nameLength).toString("ascii");
514
+ registers.push({ id, size, name });
515
+ offset += itemSize + 1;
516
+ }
517
+ return { type: "registers_available", requestId, errorCode, registers };
518
+ }
519
+ case 133 /* Info */: {
520
+ const mainVersionLength = body[0] ?? 0;
521
+ const version = Array.from(body.subarray(1, 1 + mainVersionLength));
522
+ const svnLengthOffset = 1 + mainVersionLength;
523
+ const svnLength = body[svnLengthOffset] ?? 0;
524
+ const svnBytes = body.subarray(svnLengthOffset + 1, svnLengthOffset + 1 + svnLength);
525
+ return {
526
+ type: "info",
527
+ requestId,
528
+ errorCode,
529
+ version,
530
+ versionString: version.join("."),
531
+ svnVersion: parseLittleEndianVariableWidth(svnBytes)
532
+ };
533
+ }
534
+ case 17 /* CheckpointInfo */: {
535
+ const operation = body[11] ?? 4;
536
+ const checkpoint = {
537
+ id: body.readUInt32LE(0),
538
+ currentlyHit: body[4] === 1,
539
+ start: body.readUInt16LE(5),
540
+ end: body.readUInt16LE(7),
541
+ stopWhenHit: body[9] === 1,
542
+ enabled: body[10] === 1,
543
+ kind: cpuOperationToBreakpointKind(operation),
544
+ temporary: body[12] === 1,
545
+ hitCount: body.readUInt32LE(13),
546
+ ignoreCount: body.readUInt32LE(17),
547
+ hasCondition: body[21] === 1
548
+ };
549
+ return { type: "checkpoint_info", requestId, errorCode, checkpoint };
550
+ }
551
+ case 20 /* CheckpointList */: {
552
+ return {
553
+ type: "checkpoint_list",
554
+ requestId,
555
+ errorCode,
556
+ total: errorCode === 0 /* OK */ ? body.readUInt32LE(0) : 0,
557
+ checkpoints: []
558
+ };
559
+ }
560
+ case 132 /* DisplayGet */: {
561
+ const infoLength = errorCode === 0 /* OK */ ? body.readUInt32LE(0) : 0;
562
+ const imageLength = errorCode === 0 /* OK */ ? body.readUInt32LE(17) : 0;
563
+ return {
564
+ type: "display",
565
+ requestId,
566
+ errorCode,
567
+ debugWidth: body.readUInt16LE(4),
568
+ debugHeight: body.readUInt16LE(6),
569
+ debugOffsetX: body.readUInt16LE(8),
570
+ debugOffsetY: body.readUInt16LE(10),
571
+ innerWidth: body.readUInt16LE(12),
572
+ innerHeight: body.readUInt16LE(14),
573
+ bitsPerPixel: body[16] ?? 0,
574
+ imageBytes: body.subarray(infoLength + 4, infoLength + 4 + imageLength)
575
+ };
576
+ }
577
+ case 145 /* PaletteGet */: {
578
+ const count = errorCode === 0 /* OK */ ? body.readUInt16LE(0) : 0;
579
+ let offset = 2;
580
+ const items = [];
581
+ for (let index = 0; index < count; index += 1) {
582
+ const itemSize = body[offset] ?? 0;
583
+ items.push({
584
+ index,
585
+ red: body[offset + 1] ?? 0,
586
+ green: body[offset + 2] ?? 0,
587
+ blue: body[offset + 3] ?? 0
588
+ });
589
+ offset += itemSize + 1;
590
+ }
591
+ return { type: "palette", requestId, errorCode, items };
592
+ }
593
+ case 98 /* Stopped */:
594
+ return { type: "stopped", requestId, errorCode, programCounter: body.readUInt16LE(0) };
595
+ case 99 /* Resumed */:
596
+ return { type: "resumed", requestId, errorCode, programCounter: body.readUInt16LE(0) };
597
+ case 97 /* Jam */:
598
+ return { type: "jam", requestId, errorCode, programCounter: body.readUInt16LE(0) };
599
+ case 66 /* Undump */:
600
+ return { type: "undump", requestId, errorCode, programCounter: body.readUInt16LE(0) };
601
+ default:
602
+ return { type: "empty", requestId, errorCode, responseType };
603
+ }
604
+ }
605
+ var ViceMonitorClient = class extends EventEmitter {
606
+ #socket = null;
607
+ #buffer = Buffer.alloc(0);
608
+ #nextRequestId = 1;
609
+ #pending = /* @__PURE__ */ new Map();
610
+ #chain = Promise.resolve();
611
+ #host = null;
612
+ #port = null;
613
+ #runtimeState = {
614
+ connected: false,
615
+ runtimeKnown: false,
616
+ lastEventType: "unknown",
617
+ programCounter: null
618
+ };
619
+ get connected() {
620
+ return this.#socket != null && !this.#socket.destroyed;
621
+ }
622
+ runtimeState() {
623
+ return { ...this.#runtimeState };
624
+ }
625
+ async connect(host2, port2) {
626
+ if (this.connected && this.#host === host2 && this.#port === port2) {
627
+ return;
628
+ }
629
+ await this.disconnect();
630
+ this.#host = host2;
631
+ this.#port = port2;
632
+ this.#buffer = Buffer.alloc(0);
633
+ await new Promise((resolve, reject) => {
634
+ const socket = net.createConnection({ host: host2, port: port2 }, () => {
635
+ this.#socket = socket;
636
+ this.#runtimeState = {
637
+ connected: true,
638
+ runtimeKnown: false,
639
+ lastEventType: "unknown",
640
+ programCounter: null
641
+ };
642
+ resolve();
643
+ });
644
+ socket.on("data", (chunk) => this.#onData(chunk));
645
+ socket.on("close", () => this.#onClose());
646
+ socket.on("error", (error) => {
647
+ if (!this.#socket) {
648
+ reject(error);
649
+ return;
650
+ }
651
+ this.emit("transport-error", error);
652
+ });
653
+ });
654
+ }
655
+ async disconnect() {
656
+ for (const pending of this.#pending.values()) {
657
+ clearTimeout(pending.timer);
658
+ pending.reject(new ViceMcpError("connection_closed", "Emulator debug connection closed", "connection", true));
659
+ }
660
+ this.#pending.clear();
661
+ if (!this.#socket) {
662
+ return;
663
+ }
664
+ const socket = this.#socket;
665
+ this.#socket = null;
666
+ this.#runtimeState = {
667
+ connected: false,
668
+ runtimeKnown: false,
669
+ lastEventType: "unknown",
670
+ programCounter: null
671
+ };
672
+ await new Promise((resolve) => {
673
+ socket.once("close", () => resolve());
674
+ socket.destroy();
675
+ });
676
+ }
677
+ async ping(timeoutMs = 2e3) {
678
+ await this.send(129 /* Ping */, Buffer.alloc(0), timeoutMs);
679
+ }
680
+ async getInfo() {
681
+ return this.send(133 /* Info */, Buffer.alloc(0));
682
+ }
683
+ async captureDisplay(useVic = true) {
684
+ return this.send(132 /* DisplayGet */, Buffer.from([useVic ? 1 : 0, 0]));
685
+ }
686
+ async getPalette(useVic = true) {
687
+ return this.send(145 /* PaletteGet */, Buffer.from([useVic ? 1 : 0]));
688
+ }
689
+ async getRegistersAvailable() {
690
+ return this.send(131 /* RegistersAvailable */, Buffer.from([mainMemSpaceToProtocol()]));
691
+ }
692
+ async getRegisters() {
693
+ return this.send(49 /* RegistersGet */, Buffer.from([mainMemSpaceToProtocol()]));
694
+ }
695
+ async setRegisters(registers) {
696
+ const body = Buffer.alloc(3 + registers.length * 4);
697
+ body[0] = mainMemSpaceToProtocol();
698
+ body.writeUInt16LE(registers.length, 1);
699
+ registers.forEach((register, index) => {
700
+ const offset = 3 + index * 4;
701
+ body[offset] = 3;
702
+ body[offset + 1] = register.id;
703
+ body.writeUInt16LE(register.value, offset + 2);
704
+ });
705
+ return this.send(50 /* RegistersSet */, body);
706
+ }
707
+ async readMemory(start, end, bankId = 0) {
708
+ const body = Buffer.alloc(8);
709
+ body[0] = 0;
710
+ body.writeUInt16LE(start, 1);
711
+ body.writeUInt16LE(end, 3);
712
+ body[5] = mainMemSpaceToProtocol();
713
+ body.writeUInt16LE(bankId, 6);
714
+ return this.send(1 /* MemoryGet */, body);
715
+ }
716
+ async writeMemory(start, bytes, bankId = 0) {
717
+ const body = Buffer.alloc(8 + bytes.length);
718
+ body[0] = 0;
719
+ body.writeUInt16LE(start, 1);
720
+ body.writeUInt16LE(start + bytes.length - 1, 3);
721
+ body[5] = mainMemSpaceToProtocol();
722
+ body.writeUInt16LE(bankId, 6);
723
+ Buffer.from(bytes).copy(body, 8);
724
+ return this.send(2 /* MemorySet */, body);
725
+ }
726
+ async continueExecution() {
727
+ return this.send(170 /* Exit */, Buffer.alloc(0));
728
+ }
729
+ async stepInstruction(count = 1, stepOver = false) {
730
+ const body = Buffer.alloc(3);
731
+ body[0] = stepOver ? 1 : 0;
732
+ body.writeUInt16LE(count, 1);
733
+ return this.send(113 /* AdvanceInstruction */, body);
734
+ }
735
+ async stepOut() {
736
+ return this.send(115 /* ExecuteUntilReturn */, Buffer.alloc(0));
737
+ }
738
+ async reset(mode) {
739
+ const body = Buffer.from([mode === "hard" ? 1 : 0]);
740
+ return this.send(204 /* Reset */, body);
741
+ }
742
+ async setBreakpoint(options) {
743
+ const body = Buffer.alloc(8);
744
+ body.writeUInt16LE(options.start, 0);
745
+ body.writeUInt16LE(options.end ?? options.start, 2);
746
+ body[4] = options.stopWhenHit === false ? 0 : 1;
747
+ body[5] = options.enabled === false ? 0 : 1;
748
+ body[6] = breakpointKindToOperation(options.kind);
749
+ body[7] = options.temporary ? 1 : 0;
750
+ const response = await this.send(18 /* CheckpointSet */, body);
751
+ if (options.condition && response.type === "checkpoint_info") {
752
+ const conditionBody = Buffer.alloc(5 + options.condition.length);
753
+ conditionBody.writeUInt32LE(response.checkpoint.id, 0);
754
+ conditionBody[4] = options.condition.length;
755
+ conditionBody.write(options.condition, 5, "ascii");
756
+ await this.send(34 /* ConditionSet */, conditionBody);
757
+ return {
758
+ ...response,
759
+ checkpoint: {
760
+ ...response.checkpoint,
761
+ hasCondition: true
762
+ }
763
+ };
764
+ }
765
+ return response;
766
+ }
767
+ async getBreakpoint(id) {
768
+ const body = Buffer.alloc(4);
769
+ body.writeUInt32LE(id, 0);
770
+ return this.send(17 /* CheckpointGet */, body);
771
+ }
772
+ async listBreakpoints() {
773
+ return this.send(20 /* CheckpointList */, Buffer.alloc(0));
774
+ }
775
+ async deleteBreakpoint(id) {
776
+ const body = Buffer.alloc(4);
777
+ body.writeUInt32LE(id, 0);
778
+ return this.send(19 /* CheckpointDelete */, body);
779
+ }
780
+ async toggleBreakpoint(id, enabled) {
781
+ const body = Buffer.alloc(5);
782
+ body.writeUInt32LE(id, 0);
783
+ body[4] = enabled ? 1 : 0;
784
+ return this.send(21 /* CheckpointToggle */, body);
785
+ }
786
+ async setBreakpointCondition(id, condition) {
787
+ const conditionBytes = Buffer.from(condition, "ascii");
788
+ const body = Buffer.alloc(5 + conditionBytes.length);
789
+ body.writeUInt32LE(id, 0);
790
+ body[4] = conditionBytes.length;
791
+ conditionBytes.copy(body, 5);
792
+ return this.send(34 /* ConditionSet */, body);
793
+ }
794
+ async autostartProgram(filename, autoStart, fileIndex = 0) {
795
+ const body = Buffer.alloc(4 + Buffer.byteLength(filename));
796
+ body[0] = autoStart ? 1 : 0;
797
+ body.writeUInt16LE(fileIndex, 1);
798
+ body[3] = Buffer.byteLength(filename);
799
+ body.write(filename, 4, "ascii");
800
+ return this.send(221 /* AutoStart */, body);
801
+ }
802
+ async quit() {
803
+ return this.send(187 /* Quit */, Buffer.alloc(0));
804
+ }
805
+ async sendKeys(text) {
806
+ const encoded = Buffer.from(text, "binary");
807
+ const body = Buffer.alloc(1 + encoded.length);
808
+ body[0] = encoded.length;
809
+ encoded.copy(body, 1);
810
+ return this.send(114 /* KeyboardFeed */, body);
811
+ }
812
+ async setJoyport(port2, value) {
813
+ const body = Buffer.alloc(4);
814
+ body.writeUInt16LE(port2, 0);
815
+ body.writeUInt16LE(value, 2);
816
+ return this.send(162 /* JoyportSet */, body);
817
+ }
818
+ async send(commandType, body, timeoutMs = 5e3) {
819
+ const next = this.#chain.catch(() => void 0).then(async () => this.#execute(commandType, body, timeoutMs));
820
+ this.#chain = next.then(
821
+ () => void 0,
822
+ () => void 0
823
+ );
824
+ return next;
825
+ }
826
+ async #execute(commandType, body, timeoutMs) {
827
+ if (!this.#socket || this.#socket.destroyed) {
828
+ throw new ViceMcpError("not_connected", "Emulator debug connection is not connected", "connection", true);
829
+ }
830
+ const requestId = this.#nextRequestId++;
831
+ const packet = encodeHeader(commandType, requestId, body);
832
+ return await new Promise((resolve, reject) => {
833
+ const timer = setTimeout(() => {
834
+ this.#pending.delete(requestId);
835
+ reject(new ViceMcpError("timeout", `Emulator debug command timed out (0x${commandType.toString(16)})`, "timeout", true));
836
+ }, timeoutMs);
837
+ this.#pending.set(requestId, {
838
+ type: commandType,
839
+ timer,
840
+ resolve: (response) => resolve(response),
841
+ reject,
842
+ linkedCheckpointInfo: commandType === 20 /* CheckpointList */ ? [] : void 0
843
+ });
844
+ this.#socket.write(packet, (error) => {
845
+ if (error) {
846
+ clearTimeout(timer);
847
+ this.#pending.delete(requestId);
848
+ reject(new ViceMcpError("socket_write_failed", error.message, "connection", true));
849
+ }
850
+ });
851
+ });
852
+ }
853
+ #onData(chunk) {
854
+ this.#buffer = Buffer.concat([this.#buffer, chunk]);
855
+ const { responses, remainder } = parseBuffer(this.#buffer);
856
+ this.#buffer = Buffer.from(remainder);
857
+ for (const response of responses) {
858
+ this.emit("response", response);
859
+ if (response.requestId === VICE_BROADCAST_REQUEST_ID) {
860
+ this.#applyRuntimeResponse(response);
861
+ this.emit("event", response);
862
+ continue;
863
+ }
864
+ const pending = this.#pending.get(response.requestId);
865
+ if (!pending) {
866
+ this.emit("event", response);
867
+ continue;
868
+ }
869
+ if (pending.type === 20 /* CheckpointList */ && response.type === "checkpoint_info") {
870
+ pending.linkedCheckpointInfo?.push(response);
871
+ continue;
872
+ }
873
+ clearTimeout(pending.timer);
874
+ this.#pending.delete(response.requestId);
875
+ if (response.errorCode !== 0 /* OK */) {
876
+ pending.reject(
877
+ new ViceMcpError("emulator_protocol_error", `Emulator returned error ${response.errorCode}`, "protocol", false, {
878
+ commandType: pending.type,
879
+ requestId: response.requestId,
880
+ emulatorErrorCode: response.errorCode
881
+ })
882
+ );
883
+ continue;
884
+ }
885
+ if (pending.type === 20 /* CheckpointList */ && response.type === "checkpoint_list") {
886
+ if (!pending.linkedCheckpointInfo) {
887
+ pending.linkedCheckpointInfo = [];
888
+ }
889
+ response.checkpoints = pending.linkedCheckpointInfo.map((entry) => entry.checkpoint);
890
+ }
891
+ pending.resolve(response);
892
+ }
893
+ }
894
+ #onClose() {
895
+ const pendingError = new ViceMcpError("connection_closed", "Emulator debug connection closed", "connection", true);
896
+ for (const pending of this.#pending.values()) {
897
+ clearTimeout(pending.timer);
898
+ pending.reject(pendingError);
899
+ }
900
+ this.#pending.clear();
901
+ this.#socket = null;
902
+ this.#runtimeState = {
903
+ connected: false,
904
+ runtimeKnown: false,
905
+ lastEventType: "unknown",
906
+ programCounter: null
907
+ };
908
+ this.emit("close");
909
+ }
910
+ #applyRuntimeResponse(response) {
911
+ switch (response.type) {
912
+ case "resumed":
913
+ case "stopped":
914
+ case "jam":
915
+ this.#runtimeState = {
916
+ connected: this.connected,
917
+ runtimeKnown: true,
918
+ lastEventType: response.type,
919
+ programCounter: response.programCounter
920
+ };
921
+ break;
922
+ default:
923
+ break;
924
+ }
925
+ }
926
+ };
927
+
928
+ // src/session.ts
929
+ function sleep(ms) {
930
+ return new Promise((resolve) => setTimeout(resolve, ms));
931
+ }
932
+ function nowIso() {
933
+ return (/* @__PURE__ */ new Date()).toISOString();
934
+ }
935
+ function defaultC64Config() {
936
+ return c64ConfigSchema.parse({});
937
+ }
938
+ async function buildViceLaunchEnv() {
939
+ const env = { ...process.env };
940
+ const uid = os.userInfo().uid;
941
+ const runtimeDir = env.XDG_RUNTIME_DIR || `/run/user/${uid}`;
942
+ env.XDG_RUNTIME_DIR ||= runtimeDir;
943
+ if (!env.WAYLAND_DISPLAY) {
944
+ const waylandDisplay = await firstRuntimeEntry(runtimeDir, /^wayland-\d+$/);
945
+ if (waylandDisplay) {
946
+ env.WAYLAND_DISPLAY = waylandDisplay;
947
+ }
948
+ }
949
+ if (!env.XAUTHORITY) {
950
+ const xauthority = await firstRuntimeEntry(runtimeDir, /^\.mutter-Xwaylandauth\./, true);
951
+ if (xauthority) {
952
+ env.XAUTHORITY = xauthority;
953
+ }
954
+ }
955
+ if (!env.DISPLAY) {
956
+ env.DISPLAY = ":0";
957
+ }
958
+ return env;
959
+ }
960
+ async function firstRuntimeEntry(runtimeDir, pattern, returnAbsolutePath = false) {
961
+ try {
962
+ const entries = await fs.readdir(runtimeDir);
963
+ const match = entries.find((entry) => pattern.test(entry));
964
+ if (!match) {
965
+ return null;
966
+ }
967
+ return returnAbsolutePath ? path.join(runtimeDir, match) : match;
968
+ } catch {
969
+ return null;
970
+ }
971
+ }
972
+ function makeWarning(message, code = "warning") {
973
+ return { code, message };
974
+ }
975
+ function lowNibble(value) {
976
+ return value & 15;
977
+ }
978
+ function decodeVicBankAddress(dd00) {
979
+ return ((dd00 ^ 3) & 3) * 16384;
980
+ }
981
+ function decodeGraphicsMode(d011, d016) {
982
+ const extendedColorMode = (d011 & 64) !== 0;
983
+ const bitmapMode = (d011 & 32) !== 0;
984
+ const multicolorMode = (d016 & 16) !== 0;
985
+ let graphicsMode;
986
+ if (!extendedColorMode && !bitmapMode && !multicolorMode) {
987
+ graphicsMode = "standard_text";
988
+ } else if (!extendedColorMode && !bitmapMode && multicolorMode) {
989
+ graphicsMode = "multicolor_text";
990
+ } else if (!extendedColorMode && bitmapMode && !multicolorMode) {
991
+ graphicsMode = "standard_bitmap";
992
+ } else if (!extendedColorMode && bitmapMode && multicolorMode) {
993
+ graphicsMode = "multicolor_bitmap";
994
+ } else if (extendedColorMode && !bitmapMode && !multicolorMode) {
995
+ graphicsMode = "extended_background_color_text";
996
+ } else if (extendedColorMode && !bitmapMode && multicolorMode) {
997
+ graphicsMode = "invalid_text_mode";
998
+ } else if (extendedColorMode && bitmapMode && !multicolorMode) {
999
+ graphicsMode = "invalid_bitmap_mode_1";
1000
+ } else {
1001
+ graphicsMode = "invalid_bitmap_mode_2";
1002
+ }
1003
+ return {
1004
+ graphicsMode,
1005
+ extendedColorMode,
1006
+ bitmapMode,
1007
+ multicolorMode
1008
+ };
1009
+ }
1010
+ function isTextGraphicsMode(graphicsMode) {
1011
+ return graphicsMode === "standard_text" || graphicsMode === "multicolor_text" || graphicsMode === "extended_background_color_text" || graphicsMode === "invalid_text_mode";
1012
+ }
1013
+ function petsciiToScreenCode(value) {
1014
+ if (value === 255) {
1015
+ return 94;
1016
+ }
1017
+ if (value < 32) {
1018
+ return value ^ 128;
1019
+ }
1020
+ if (value < 96) {
1021
+ return value & 63;
1022
+ }
1023
+ if (value < 128) {
1024
+ return value & 95;
1025
+ }
1026
+ if (value < 160) {
1027
+ return value | 64;
1028
+ }
1029
+ if (value < 192) {
1030
+ return value ^ 192;
1031
+ }
1032
+ if (value < 255) {
1033
+ return value ^ 128;
1034
+ }
1035
+ return 94;
1036
+ }
1037
+ function decodeScreenCodeCell(code) {
1038
+ if (code === 32 || code === 160) {
1039
+ return { ascii: " ", lossy: false };
1040
+ }
1041
+ if (code >= 1 && code <= 26) {
1042
+ return { ascii: String.fromCharCode(64 + code), lossy: false };
1043
+ }
1044
+ if (code >= 48 && code <= 57) {
1045
+ return { ascii: String.fromCharCode(code), lossy: false };
1046
+ }
1047
+ switch (code) {
1048
+ case 0:
1049
+ return { ascii: "@", lossy: false };
1050
+ case 27:
1051
+ return { ascii: "[", lossy: false };
1052
+ case 28:
1053
+ return { ascii: "\xA3", lossy: false };
1054
+ case 29:
1055
+ return { ascii: "]", lossy: false };
1056
+ case 34:
1057
+ return { ascii: '"', lossy: false };
1058
+ case 35:
1059
+ return { ascii: "#", lossy: false };
1060
+ case 36:
1061
+ return { ascii: "$", lossy: false };
1062
+ case 37:
1063
+ return { ascii: "%", lossy: false };
1064
+ case 38:
1065
+ return { ascii: "&", lossy: false };
1066
+ case 39:
1067
+ return { ascii: "'", lossy: false };
1068
+ case 40:
1069
+ return { ascii: "(", lossy: false };
1070
+ case 41:
1071
+ return { ascii: ")", lossy: false };
1072
+ case 42:
1073
+ return { ascii: "*", lossy: false };
1074
+ case 43:
1075
+ return { ascii: "+", lossy: false };
1076
+ case 44:
1077
+ return { ascii: ",", lossy: false };
1078
+ case 45:
1079
+ return { ascii: "-", lossy: false };
1080
+ case 46:
1081
+ return { ascii: ".", lossy: false };
1082
+ case 47:
1083
+ return { ascii: "/", lossy: false };
1084
+ case 58:
1085
+ return { ascii: ":", lossy: false };
1086
+ case 59:
1087
+ return { ascii: ";", lossy: false };
1088
+ case 60:
1089
+ return { ascii: "\u2191", lossy: false };
1090
+ case 61:
1091
+ return { ascii: "=", lossy: false };
1092
+ case 62:
1093
+ return { ascii: "\u2190", lossy: false };
1094
+ case 63:
1095
+ return { ascii: "?", lossy: false };
1096
+ case 94:
1097
+ return { ascii: "\u03C0", lossy: false };
1098
+ default:
1099
+ return {
1100
+ ascii: "\uFFFD",
1101
+ token: SCREEN_CODE_TOKEN_MAP.get(code) ?? `<SC:${code}>`,
1102
+ lossy: true
1103
+ };
1104
+ }
1105
+ }
1106
+ function uint32(value) {
1107
+ const buffer = Buffer.alloc(4);
1108
+ buffer.writeUInt32BE(value, 0);
1109
+ return buffer;
1110
+ }
1111
+ var CRC_TABLE = new Uint32Array(256).map((_, index) => {
1112
+ let c = index;
1113
+ for (let k = 0; k < 8; k += 1) {
1114
+ c = (c & 1) === 1 ? 3988292384 ^ c >>> 1 : c >>> 1;
1115
+ }
1116
+ return c >>> 0;
1117
+ });
1118
+ function crc32(buffer) {
1119
+ let crc = 4294967295;
1120
+ for (const byte of buffer) {
1121
+ crc = CRC_TABLE[(crc ^ byte) & 255] ^ crc >>> 8;
1122
+ }
1123
+ return (crc ^ 4294967295) >>> 0;
1124
+ }
1125
+ function pngChunk(type, data) {
1126
+ const typeBuffer = Buffer.from(type, "ascii");
1127
+ const crc = crc32(Buffer.concat([typeBuffer, data]));
1128
+ return Buffer.concat([uint32(data.length), typeBuffer, data, uint32(crc >>> 0)]);
1129
+ }
1130
+ function encodePngRgb(width, height, pixels) {
1131
+ const stride = width * 3;
1132
+ const rows = Buffer.alloc((stride + 1) * height);
1133
+ for (let y = 0; y < height; y += 1) {
1134
+ const rowOffset = y * (stride + 1);
1135
+ rows[rowOffset] = 0;
1136
+ Buffer.from(pixels.subarray(y * stride, y * stride + stride)).copy(rows, rowOffset + 1);
1137
+ }
1138
+ const signature = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
1139
+ const chunks = [
1140
+ pngChunk(
1141
+ "IHDR",
1142
+ Buffer.concat([
1143
+ uint32(width),
1144
+ uint32(height),
1145
+ Buffer.from([8, 2, 0, 0, 0])
1146
+ ])
1147
+ ),
1148
+ pngChunk("IDAT", zlib.deflateSync(rows)),
1149
+ pngChunk("IEND", Buffer.alloc(0))
1150
+ ];
1151
+ return Buffer.concat([signature, ...chunks]);
1152
+ }
1153
+ var PETSCII_TOKEN_DEFINITIONS = [
1154
+ { canonical: "RETURN", bytes: [13], aliases: ["ENTER"] },
1155
+ { canonical: "SHIFT RETURN", bytes: [141], aliases: ["SHIFT ENTER", "SH RETURN", "SH ENTER", "STRET"] },
1156
+ { canonical: "SPACE", bytes: [32], aliases: ["SPC"] },
1157
+ { canonical: "SHIFT SPACE", bytes: [160], aliases: ["SH SPACE"] },
1158
+ { canonical: "HOME", bytes: [19], aliases: ["CURSOR HOME", "CUR HOME"] },
1159
+ { canonical: "CLEAR", bytes: [147], aliases: ["CLR", "CLEAR SCREEN"] },
1160
+ { canonical: "DELETE", bytes: [20], aliases: ["DEL", "BACKSPACE"] },
1161
+ { canonical: "INSERT", bytes: [148], aliases: ["INST", "INS"] },
1162
+ { canonical: "DOWN", bytes: [17], aliases: ["CURSOR DOWN", "CUR DOWN"] },
1163
+ { canonical: "UP", bytes: [145], aliases: ["CURSOR UP", "CUR UP"] },
1164
+ { canonical: "LEFT", bytes: [157], aliases: ["CURSOR LEFT", "CUR LEFT"] },
1165
+ { canonical: "RIGHT", bytes: [29], aliases: ["CURSOR RIGHT", "CUR RIGHT"] },
1166
+ { canonical: "REVERSE ON", bytes: [18], aliases: ["RVS ON", "RVON", "RVRS ON"] },
1167
+ { canonical: "REVERSE OFF", bytes: [146], aliases: ["RVS OFF", "RVOF", "RVRS OFF"] },
1168
+ { canonical: "BLACK", bytes: [144], aliases: ["BLK"] },
1169
+ { canonical: "WHITE", bytes: [5], aliases: ["WHT"] },
1170
+ { canonical: "RED", bytes: [28], aliases: [] },
1171
+ { canonical: "CYAN", bytes: [159], aliases: ["CYN"] },
1172
+ { canonical: "PURPLE", bytes: [156], aliases: ["PUR"] },
1173
+ { canonical: "GREEN", bytes: [30], aliases: ["GRN"] },
1174
+ { canonical: "BLUE", bytes: [31], aliases: ["BLU"] },
1175
+ { canonical: "YELLOW", bytes: [158], aliases: ["YEL"] },
1176
+ { canonical: "ORANGE", bytes: [129], aliases: ["ORNG"] },
1177
+ { canonical: "BROWN", bytes: [149], aliases: ["BRN"] },
1178
+ { canonical: "LIGHT RED", bytes: [150], aliases: ["LRED", "PINK", "LT RED"] },
1179
+ { canonical: "DARK GRAY", bytes: [151], aliases: ["DARK GREY", "GRAY1", "GREY1", "GRY1"] },
1180
+ { canonical: "GRAY", bytes: [152], aliases: ["GREY", "GRAY2", "GREY2", "GRY2", "MEDIUM GRAY", "MEDIUM GREY"] },
1181
+ { canonical: "LIGHT GREEN", bytes: [153], aliases: ["LGRN", "LT GREEN"] },
1182
+ { canonical: "LIGHT BLUE", bytes: [154], aliases: ["LBLU", "LT BLUE"] },
1183
+ { canonical: "LIGHT GRAY", bytes: [155], aliases: ["LIGHT GREY", "GRAY3", "GREY3", "GRY3"] },
1184
+ { canonical: "F1", bytes: [133], aliases: [] },
1185
+ { canonical: "F2", bytes: [137], aliases: [] },
1186
+ { canonical: "F3", bytes: [134], aliases: [] },
1187
+ { canonical: "F4", bytes: [138], aliases: [] },
1188
+ { canonical: "F5", bytes: [135], aliases: [] },
1189
+ { canonical: "F6", bytes: [139], aliases: [] },
1190
+ { canonical: "F7", bytes: [136], aliases: [] },
1191
+ { canonical: "F8", bytes: [140], aliases: [] },
1192
+ { canonical: "STOP", bytes: [3], aliases: ["RUN STOP", "RUNSTOP"] },
1193
+ { canonical: "LOWER", bytes: [14], aliases: ["LOWERCASE", "SWLC"] },
1194
+ { canonical: "UPPER", bytes: [142], aliases: ["UPPERCASE", "SWUC"] },
1195
+ { canonical: "POUND", bytes: [92], aliases: ["GBP", "UK POUND"] },
1196
+ { canonical: "UP ARROW", bytes: [94], aliases: ["ARROW UP"] },
1197
+ { canonical: "LEFT ARROW", bytes: [95], aliases: ["ARROW LEFT", "BACK ARROW"] },
1198
+ { canonical: "PI", bytes: [255], aliases: [] }
1199
+ ];
1200
+ var PETSCII_TOKEN_MAP = /* @__PURE__ */ new Map();
1201
+ for (const definition of PETSCII_TOKEN_DEFINITIONS) {
1202
+ PETSCII_TOKEN_MAP.set(definition.canonical, definition);
1203
+ for (const alias of definition.aliases) {
1204
+ PETSCII_TOKEN_MAP.set(alias, definition);
1205
+ }
1206
+ }
1207
+ var SCREEN_CODE_TOKEN_MAP = /* @__PURE__ */ new Map();
1208
+ for (const definition of PETSCII_TOKEN_DEFINITIONS) {
1209
+ for (const byte of definition.bytes) {
1210
+ const screenCode = petsciiToScreenCode(byte);
1211
+ if (!SCREEN_CODE_TOKEN_MAP.has(screenCode)) {
1212
+ SCREEN_CODE_TOKEN_MAP.set(screenCode, `<${definition.canonical}>`);
1213
+ }
1214
+ }
1215
+ }
1216
+ var DIRECT_PETSCII_CHAR_BYTES = /* @__PURE__ */ new Map([
1217
+ ["\xA3", 92],
1218
+ ["\u2191", 94],
1219
+ ["\u2190", 95],
1220
+ ["\u03C0", 255],
1221
+ ["\u03A0", 255]
1222
+ ]);
1223
+ function normalizePetsciiTokenName(token) {
1224
+ return token.trim().toUpperCase().replace(/[\s_-]+/g, " ");
1225
+ }
1226
+ function lookupPetsciiToken(token) {
1227
+ const normalized = normalizePetsciiTokenName(token);
1228
+ const definition = PETSCII_TOKEN_MAP.get(normalized);
1229
+ if (!definition) {
1230
+ unsupportedError("Requested keyboard token is not representable through PETSCII keyboard-buffer input.", {
1231
+ token,
1232
+ normalizedToken: normalized
1233
+ });
1234
+ }
1235
+ return definition;
1236
+ }
1237
+ function encodeLiteralPetsciiChar(char) {
1238
+ if (char === "\n" || char === "\r") {
1239
+ return 13;
1240
+ }
1241
+ if (char === " ") {
1242
+ return 32;
1243
+ }
1244
+ const direct = DIRECT_PETSCII_CHAR_BYTES.get(char);
1245
+ if (direct != null) {
1246
+ return direct;
1247
+ }
1248
+ const code = char.codePointAt(0);
1249
+ if (code == null) {
1250
+ validationError("write_text received an empty character while encoding PETSCII");
1251
+ }
1252
+ if (code >= 97 && code <= 122) {
1253
+ return code - 32;
1254
+ }
1255
+ if (code >= 32 && code <= 93) {
1256
+ return code;
1257
+ }
1258
+ validationError("write_text only supports characters representable in the supported PETSCII subset", {
1259
+ character: char,
1260
+ codePoint: code
1261
+ });
1262
+ }
1263
+ function decodeWriteTextToPetscii(input) {
1264
+ const bytes = [];
1265
+ for (let index = 0; index < input.length; index += 1) {
1266
+ const char = input[index];
1267
+ if (char === "\\") {
1268
+ const next = input[index + 1];
1269
+ if (next == null) {
1270
+ validationError("write_text received a trailing backslash escape with no character after it");
1271
+ }
1272
+ switch (next) {
1273
+ case "n":
1274
+ bytes.push(13);
1275
+ break;
1276
+ case "r":
1277
+ bytes.push(13);
1278
+ break;
1279
+ case "t":
1280
+ bytes.push(32);
1281
+ break;
1282
+ case "\\":
1283
+ bytes.push(encodeLiteralPetsciiChar("\\"));
1284
+ break;
1285
+ case '"':
1286
+ bytes.push(encodeLiteralPetsciiChar('"'));
1287
+ break;
1288
+ case "'":
1289
+ bytes.push(encodeLiteralPetsciiChar("'"));
1290
+ break;
1291
+ default:
1292
+ validationError("write_text received an unsupported escape sequence", {
1293
+ escape: `\\${next}`
1294
+ });
1295
+ }
1296
+ index += 1;
1297
+ continue;
1298
+ }
1299
+ if (char === "{") {
1300
+ const end = input.indexOf("}", index + 1);
1301
+ if (end === -1) {
1302
+ validationError("write_text received an opening brace without a closing brace", {
1303
+ position: index
1304
+ });
1305
+ }
1306
+ const rawToken = input.slice(index + 1, end);
1307
+ if (!rawToken.trim()) {
1308
+ validationError("write_text received an empty brace token", {
1309
+ position: index
1310
+ });
1311
+ }
1312
+ const definition = lookupPetsciiToken(rawToken);
1313
+ bytes.push(...definition.bytes);
1314
+ index = end;
1315
+ continue;
1316
+ }
1317
+ bytes.push(encodeLiteralPetsciiChar(char));
1318
+ }
1319
+ return Uint8Array.from(bytes);
1320
+ }
1321
+ function resolveKeyboardInputKey(key) {
1322
+ const trimmed = key.trim();
1323
+ if (!trimmed) {
1324
+ validationError("keyboard_input requires a non-empty key name");
1325
+ }
1326
+ if (trimmed.length === 1) {
1327
+ return {
1328
+ canonical: normalizePetsciiTokenName(trimmed),
1329
+ bytes: Uint8Array.from([encodeLiteralPetsciiChar(trimmed)])
1330
+ };
1331
+ }
1332
+ const definition = lookupPetsciiToken(trimmed);
1333
+ return {
1334
+ canonical: definition.canonical,
1335
+ bytes: Uint8Array.from(definition.bytes)
1336
+ };
1337
+ }
1338
+ var JOYSTICK_CONTROL_BITS = {
1339
+ up: 1,
1340
+ down: 2,
1341
+ left: 4,
1342
+ right: 8,
1343
+ fire: 16
1344
+ };
1345
+ var JOYSTICK_RELEASED_MASK = 31;
1346
+ var DEFAULT_INPUT_TAP_MS = 75;
1347
+ var DEFAULT_KEYBOARD_REPEAT_MS = 100;
1348
+ var VICE_PROCESS_LOG_PATH = path.join(os.tmpdir(), "c64-debug-mcp-x64sc.log");
1349
+ var DISPLAY_CAPTURE_DIR = path.resolve(process.cwd(), ".vice-debug-mcp-artifacts");
1350
+ var MIRROR_EMULATOR_LOGS_TO_STDERR = /^(1|true|yes|on)$/i.test(process.env.C64_DEBUG_CONSOLE_LOGS ?? "");
1351
+ var EXECUTION_EVENT_WAIT_MS = 1e3;
1352
+ var EXECUTION_SETTLE_DELAY_MS = 2e3;
1353
+ var BOOTSTRAP_INITIAL_DELAY_MS = 2e3;
1354
+ var BOOTSTRAP_SETTLE_TIMEOUT_MS = 15e3;
1355
+ var BOOTSTRAP_POLL_MS = 250;
1356
+ var BOOTSTRAP_RUNNING_STABLE_MS = 3e3;
1357
+ var BOOTSTRAP_RESUME_COOLDOWN_MS = 500;
1358
+ var PROGRAM_LOAD_SETTLE_TIMEOUT_MS = 15e3;
1359
+ var PROGRAM_LOAD_SETTLE_POLL_MS = 250;
1360
+ var PROGRAM_LOAD_RUNNING_STABLE_MS = 3e3;
1361
+ var PROGRAM_LOAD_RESUME_COOLDOWN_MS = 500;
1362
+ var DISPLAY_SETTLE_TIMEOUT_MS = 5e3;
1363
+ var DISPLAY_SETTLE_POLL_MS = 100;
1364
+ var DISPLAY_PAUSE_TIMEOUT_MS = 5e3;
1365
+ var DISPLAY_RUNNING_STABLE_MS = 750;
1366
+ var MAX_WRITE_TEXT_BYTES = 64;
1367
+ var DISPLAY_RESUME_COOLDOWN_MS = 250;
1368
+ var INPUT_SETTLE_TIMEOUT_MS = 5e3;
1369
+ var INPUT_SETTLE_POLL_MS = 100;
1370
+ var INPUT_RUNNING_STABLE_MS = 750;
1371
+ var INPUT_RESUME_COOLDOWN_MS = 250;
1372
+ var CHECKPOINT_HIT_SETTLE_MS = 1e3;
1373
+ var STOPPED_IDLE_TIMEOUT_MS = 2e4;
1374
+ var MIN_TAP_DURATION_MS = 10;
1375
+ var MAX_TAP_DURATION_MS = 1e4;
1376
+ var RESET_GRACE_PERIOD_MS = 150;
1377
+ function clampTapDuration(durationMs) {
1378
+ if (durationMs == null) {
1379
+ return DEFAULT_INPUT_TAP_MS;
1380
+ }
1381
+ if (!Number.isInteger(durationMs)) {
1382
+ validationError("durationMs must be an integer", { durationMs });
1383
+ }
1384
+ return Math.max(MIN_TAP_DURATION_MS, Math.min(MAX_TAP_DURATION_MS, durationMs));
1385
+ }
1386
+ function joystickPortToProtocol(port2) {
1387
+ return port2 - 1;
1388
+ }
1389
+ var PortAllocator = class {
1390
+ #forbiddenPorts;
1391
+ constructor(forbiddenPorts) {
1392
+ this.#forbiddenPorts = new Set(forbiddenPorts ?? DEFAULT_FORBIDDEN_PORTS);
1393
+ }
1394
+ get forbiddenPorts() {
1395
+ return [...this.#forbiddenPorts].sort((left, right) => left - right);
1396
+ }
1397
+ assertAllowed(port2) {
1398
+ if (this.#forbiddenPorts.has(port2)) {
1399
+ validationError("Standard/default debug monitor ports are forbidden in managed mode", {
1400
+ port: port2,
1401
+ forbiddenPorts: this.forbiddenPorts
1402
+ });
1403
+ }
1404
+ }
1405
+ async allocate() {
1406
+ for (let attempts = 0; attempts < 30; attempts += 1) {
1407
+ const candidate = await this.#probeEphemeralPort();
1408
+ if (candidate < 1024 || this.#forbiddenPorts.has(candidate)) {
1409
+ continue;
1410
+ }
1411
+ return candidate;
1412
+ }
1413
+ throw new ViceMcpError("port_allocation_failed", "Could not allocate a non-default monitor port", "configuration");
1414
+ }
1415
+ async ensureFree(port2, host2 = DEFAULT_MONITOR_HOST) {
1416
+ this.assertAllowed(port2);
1417
+ const available = await isPortAvailable(host2, port2);
1418
+ if (!available) {
1419
+ throw new ViceMcpError("port_in_use", `Monitor port ${port2} is already in use`, "configuration", false, {
1420
+ host: host2,
1421
+ port: port2
1422
+ });
1423
+ }
1424
+ }
1425
+ async #probeEphemeralPort() {
1426
+ return await new Promise((resolve, reject) => {
1427
+ const server = net2.createServer();
1428
+ server.once("error", reject);
1429
+ server.listen(0, DEFAULT_MONITOR_HOST, () => {
1430
+ const address = server.address();
1431
+ if (address && typeof address === "object") {
1432
+ const port2 = address.port;
1433
+ server.close((error) => {
1434
+ if (error) {
1435
+ reject(error);
1436
+ return;
1437
+ }
1438
+ resolve(port2);
1439
+ });
1440
+ return;
1441
+ }
1442
+ reject(new Error("Failed to allocate port"));
1443
+ });
1444
+ });
1445
+ }
1446
+ };
1447
+ async function isPortAvailable(host2, port2) {
1448
+ return await new Promise((resolve) => {
1449
+ const socket = net2.connect({ host: host2, port: port2 });
1450
+ socket.once("connect", () => {
1451
+ socket.destroy();
1452
+ resolve(false);
1453
+ });
1454
+ socket.once("error", () => resolve(true));
1455
+ });
1456
+ }
1457
+ var ViceSession = class {
1458
+ #client = new ViceMonitorClient();
1459
+ #portAllocator;
1460
+ #transportState = "not_started";
1461
+ #processState = "not_applicable";
1462
+ #executionState = "unknown";
1463
+ #lastStopReason = "none";
1464
+ #host = null;
1465
+ #port = null;
1466
+ #connectedSince = null;
1467
+ #lastResponseAt = null;
1468
+ #process = null;
1469
+ #processLogStream = null;
1470
+ #stdoutMirrorBuffer = "";
1471
+ #stderrMirrorBuffer = "";
1472
+ #warnings = [];
1473
+ #lastExecutionIntent = "unknown";
1474
+ #lastRegisters = null;
1475
+ #lastRuntimeEventType = "unknown";
1476
+ #lastRuntimeProgramCounter = null;
1477
+ #config = null;
1478
+ #recoveryInProgress = false;
1479
+ #recoveryPromise = null;
1480
+ #freshEmulatorPending = false;
1481
+ #launchId = 0;
1482
+ #restartCount = 0;
1483
+ #suppressRecovery = false;
1484
+ #shuttingDown = false;
1485
+ #heldKeyboardIntervals = /* @__PURE__ */ new Map();
1486
+ #heldJoystickMasks = /* @__PURE__ */ new Map();
1487
+ #breakpointLabels = /* @__PURE__ */ new Map();
1488
+ #stoppedAt = null;
1489
+ #autoResumeTimer = null;
1490
+ #explicitPauseActive = false;
1491
+ #pendingCheckpointHit = null;
1492
+ #lastCheckpointHit = null;
1493
+ #checkpointQueryPending = false;
1494
+ #executionOperationLock = null;
1495
+ #displayOperationLock = null;
1496
+ constructor(portAllocator = new PortAllocator()) {
1497
+ this.#portAllocator = portAllocator;
1498
+ this.#client.on("response", (response) => {
1499
+ this.#lastResponseAt = nowIso();
1500
+ this.#writeProcessLogLine(`[monitor-response] type=${response.type} requestId=${response.requestId} errorCode=${response.errorCode}`);
1501
+ });
1502
+ this.#client.on("close", () => {
1503
+ this.#writeProcessLogLine("[monitor-close] debugger connection closed");
1504
+ if (this.#transportState !== "stopped") {
1505
+ this.#transportState = "disconnected";
1506
+ }
1507
+ this.#syncMonitorRuntimeState();
1508
+ if (!this.#suppressRecovery && !this.#shuttingDown && this.#config) {
1509
+ void this.#scheduleRecovery();
1510
+ }
1511
+ });
1512
+ this.#client.on("event", (event) => {
1513
+ if (event.type === "checkpoint_info" && event.checkpoint.currentlyHit) {
1514
+ this.#pendingCheckpointHit = {
1515
+ id: event.checkpoint.id,
1516
+ kind: event.checkpoint.kind,
1517
+ observedAt: Date.now()
1518
+ };
1519
+ }
1520
+ const programCounter = "programCounter" in event && typeof event.programCounter === "number" ? event.programCounter : null;
1521
+ this.#writeProcessLogLine(
1522
+ `[monitor-event] type=${event.type}${programCounter == null ? "" : ` pc=$${programCounter.toString(16).padStart(4, "0")}`}`
1523
+ );
1524
+ this.#syncMonitorRuntimeState();
1525
+ });
1526
+ }
1527
+ snapshot() {
1528
+ return {
1529
+ transportState: this.#transportState,
1530
+ processState: this.#processState,
1531
+ executionState: this.#executionState,
1532
+ lastStopReason: this.#lastStopReason,
1533
+ idleAutoResumeArmed: this.#autoResumeTimer != null,
1534
+ explicitPauseActive: this.#explicitPauseActive,
1535
+ lastCheckpointId: this.#lastCheckpointHit?.id ?? null,
1536
+ lastCheckpointKind: this.#lastCheckpointHit?.kind ?? null,
1537
+ recoveryInProgress: this.#recoveryInProgress,
1538
+ launchId: this.#launchId,
1539
+ restartCount: this.#restartCount,
1540
+ freshEmulatorPending: this.#freshEmulatorPending,
1541
+ connectedSince: this.#connectedSince,
1542
+ lastResponseAt: this.#lastResponseAt,
1543
+ processId: this.#process?.pid ?? null,
1544
+ warnings: [...this.#warnings]
1545
+ };
1546
+ }
1547
+ async getMonitorState() {
1548
+ await this.#ensureReady();
1549
+ this.#syncMonitorRuntimeState();
1550
+ const runtime = this.#client.runtimeState();
1551
+ return {
1552
+ executionState: this.#executionState,
1553
+ lastStopReason: this.#lastStopReason,
1554
+ runtimeKnown: runtime.runtimeKnown,
1555
+ programCounter: runtime.programCounter
1556
+ };
1557
+ }
1558
+ async getRegisters() {
1559
+ await this.#ensurePausedForDebug("get_registers");
1560
+ return {
1561
+ registers: await this.#readRegisters()
1562
+ };
1563
+ }
1564
+ async shutdown() {
1565
+ if (this.#shuttingDown) {
1566
+ return;
1567
+ }
1568
+ this.#shuttingDown = true;
1569
+ this.#clearIdleAutoResume();
1570
+ this.#suppressRecovery = true;
1571
+ this.#config = null;
1572
+ this.#recoveryPromise = null;
1573
+ this.#recoveryInProgress = false;
1574
+ this.#freshEmulatorPending = false;
1575
+ this.#clearHeldInputState();
1576
+ this.#breakpointLabels.clear();
1577
+ try {
1578
+ await this.#resumeBeforeShutdown();
1579
+ await this.#stopManagedProcess(true);
1580
+ } finally {
1581
+ this.#transportState = "stopped";
1582
+ this.#processState = "not_applicable";
1583
+ this.#executionState = "unknown";
1584
+ this.#lastStopReason = "none";
1585
+ this.#explicitPauseActive = false;
1586
+ this.#pendingCheckpointHit = null;
1587
+ this.#lastCheckpointHit = null;
1588
+ this.#lastRuntimeEventType = "unknown";
1589
+ this.#lastRuntimeProgramCounter = null;
1590
+ this.#host = null;
1591
+ this.#port = null;
1592
+ this.#connectedSince = null;
1593
+ this.#lastRegisters = null;
1594
+ }
1595
+ }
1596
+ takeResponseMeta() {
1597
+ const meta = {
1598
+ freshEmulator: this.#freshEmulatorPending,
1599
+ launchId: this.#launchId,
1600
+ restartCount: this.#restartCount
1601
+ };
1602
+ this.#freshEmulatorPending = false;
1603
+ return meta;
1604
+ }
1605
+ async execute(action, count = 1, resetMode = "soft", waitUntilRunningStable = false) {
1606
+ switch (action) {
1607
+ case "pause":
1608
+ return await this.pauseExecution();
1609
+ case "resume":
1610
+ return await this.continueExecution(waitUntilRunningStable);
1611
+ case "step":
1612
+ return await this.stepInstruction(count, false);
1613
+ case "step_over":
1614
+ return await this.stepInstruction(count, true);
1615
+ case "step_out":
1616
+ return await this.stepOut();
1617
+ case "reset":
1618
+ return await this.resetMachine(resetMode);
1619
+ }
1620
+ }
1621
+ async setRegisters(registers) {
1622
+ await this.#ensurePausedForDebug("set_registers");
1623
+ const metadata = await this.#client.getRegistersAvailable();
1624
+ const metadataByName = new Map(metadata.registers.map((item) => [item.name.toUpperCase(), item]));
1625
+ const payload = Object.entries(registers).map(([fieldName, value]) => {
1626
+ const definition = C64_REGISTER_DEFINITIONS.find((item) => item.fieldName === fieldName);
1627
+ if (!definition) {
1628
+ validationError(`Unknown register ${fieldName}`, { registerName: fieldName });
1629
+ }
1630
+ if (!Number.isInteger(value)) {
1631
+ validationError(`Register ${fieldName} must be an integer`, { registerName: fieldName, value });
1632
+ }
1633
+ if (value < definition.min || value > definition.max) {
1634
+ validationError(`Register ${fieldName} must be between ${definition.min} and ${definition.max}`, {
1635
+ registerName: fieldName,
1636
+ min: definition.min,
1637
+ max: definition.max,
1638
+ value
1639
+ });
1640
+ }
1641
+ const meta = metadataByName.get(definition.viceName.toUpperCase());
1642
+ if (!meta) {
1643
+ validationError(`Required C64 register is missing from the emulator: ${definition.viceName}`, {
1644
+ registerName: definition.fieldName,
1645
+ viceName: definition.viceName
1646
+ });
1647
+ }
1648
+ return {
1649
+ id: meta.id,
1650
+ value
1651
+ };
1652
+ });
1653
+ const response = await this.#client.setRegisters(payload);
1654
+ const updatedById = new Map(response.registers.map((register) => [register.id, register.value]));
1655
+ this.#lastRegisters = this.#mergeRegisters(
1656
+ this.#lastRegisters,
1657
+ Object.fromEntries(
1658
+ C64_REGISTER_DEFINITIONS.flatMap((definition) => {
1659
+ const meta = metadataByName.get(definition.viceName.toUpperCase());
1660
+ if (!meta) {
1661
+ return [];
1662
+ }
1663
+ const value = updatedById.get(meta.id);
1664
+ if (value == null) {
1665
+ return [];
1666
+ }
1667
+ return [[definition.fieldName, value]];
1668
+ })
1669
+ )
1670
+ );
1671
+ return {
1672
+ updated: Object.fromEntries(
1673
+ C64_REGISTER_DEFINITIONS.flatMap((definition) => {
1674
+ const meta = metadataByName.get(definition.viceName.toUpperCase());
1675
+ if (!meta) {
1676
+ return [];
1677
+ }
1678
+ const value = updatedById.get(meta.id);
1679
+ if (value == null) {
1680
+ return [];
1681
+ }
1682
+ return [[definition.fieldName, value]];
1683
+ })
1684
+ ),
1685
+ executionState: this.#executionState
1686
+ };
1687
+ }
1688
+ async readMemory(start, end, bank = 0) {
1689
+ await this.#ensurePausedForDebug("memory_read");
1690
+ this.#validateRange(start, end);
1691
+ const response = await this.#client.readMemory(start, end, bank);
1692
+ return {
1693
+ length: response.bytes.length,
1694
+ data: Array.from(response.bytes)
1695
+ };
1696
+ }
1697
+ async writeMemory(start, data, bank = 0) {
1698
+ await this.#ensurePausedForDebug("memory_write");
1699
+ const bytes = Uint8Array.from(data);
1700
+ if (bytes.length === 0) {
1701
+ validationError("write_memory requires at least one byte");
1702
+ }
1703
+ if (data.some((value) => !Number.isInteger(value) || value < 0 || value > 255)) {
1704
+ validationError("write_memory data must contain only integer byte values between 0 and 255");
1705
+ }
1706
+ await this.#client.writeMemory(start, bytes, bank);
1707
+ const debugState = await this.#readDebugState();
1708
+ return {
1709
+ address: start,
1710
+ length: bytes.length,
1711
+ worked: true,
1712
+ executionState: debugState.executionState,
1713
+ lastStopReason: debugState.lastStopReason,
1714
+ programCounter: debugState.programCounter,
1715
+ registers: debugState.registers
1716
+ };
1717
+ }
1718
+ async pauseExecution() {
1719
+ return this.#withExecutionLock(async () => {
1720
+ await this.#ensureReady();
1721
+ this.#syncMonitorRuntimeState();
1722
+ if (this.#executionState === "stopped") {
1723
+ this.#explicitPauseActive = true;
1724
+ const debugState2 = await this.#readDebugState();
1725
+ return {
1726
+ executionState: debugState2.executionState,
1727
+ lastStopReason: debugState2.lastStopReason,
1728
+ programCounter: debugState2.programCounter,
1729
+ registers: debugState2.registers,
1730
+ warnings: []
1731
+ };
1732
+ }
1733
+ if (this.#executionState !== "running") {
1734
+ emulatorNotRunningError("execute pause", {
1735
+ executionState: this.#executionState,
1736
+ lastStopReason: this.#lastStopReason
1737
+ });
1738
+ }
1739
+ this.#explicitPauseActive = true;
1740
+ this.#lastExecutionIntent = "monitor_entry";
1741
+ this.#writeProcessLogLine("[tx] execute pause");
1742
+ await this.#client.ping();
1743
+ const paused = await this.waitForState("stopped", 5e3, 0);
1744
+ if (!paused.reachedTarget) {
1745
+ throw new ViceMcpError("pause_timeout", "execute pause could not reach a stopped state before timeout.", "timeout", true, {
1746
+ executionState: paused.executionState,
1747
+ lastStopReason: paused.lastStopReason
1748
+ });
1749
+ }
1750
+ const debugState = await this.#readDebugState();
1751
+ return {
1752
+ executionState: debugState.executionState,
1753
+ lastStopReason: debugState.lastStopReason,
1754
+ programCounter: debugState.programCounter,
1755
+ registers: debugState.registers,
1756
+ warnings: []
1757
+ };
1758
+ });
1759
+ }
1760
+ async continueExecution(waitUntilRunningStable = false) {
1761
+ return this.#withExecutionLock(async () => {
1762
+ await this.#ensureReady();
1763
+ if (this.#executionState !== "stopped") {
1764
+ debuggerNotPausedError("execute resume", {
1765
+ executionState: this.#executionState,
1766
+ lastStopReason: this.#lastStopReason
1767
+ });
1768
+ }
1769
+ const debugState = this.#lastRegisters == null ? await this.#readDebugState() : this.#buildDebugState(this.#lastRegisters);
1770
+ this.#lastExecutionIntent = "unknown";
1771
+ this.#writeProcessLogLine("[tx] execute resume");
1772
+ const executionEvent = this.#waitForExecutionEvent(1e3);
1773
+ await this.#client.continueExecution();
1774
+ const event = await executionEvent;
1775
+ if (!event || event.type !== "resumed") {
1776
+ this.#writeProcessLogLine(`[execute-resume] no resumed event within 1000ms (got ${event?.type ?? "nothing"})`);
1777
+ }
1778
+ if (waitUntilRunningStable) {
1779
+ await this.waitForState("running", 5e3, INPUT_RUNNING_STABLE_MS);
1780
+ } else {
1781
+ this.#syncMonitorRuntimeState();
1782
+ }
1783
+ this.#explicitPauseActive = false;
1784
+ const runtime = this.#client.runtimeState();
1785
+ const warnings = [];
1786
+ const currentExecutionState = this.#executionState;
1787
+ if (!waitUntilRunningStable && currentExecutionState !== "running") {
1788
+ warnings.push(
1789
+ makeWarning("Resume acknowledged but state transition not yet confirmed; use wait_for_state for authoritative state.", "resume_async")
1790
+ );
1791
+ }
1792
+ return {
1793
+ executionState: currentExecutionState,
1794
+ lastStopReason: this.#lastStopReason,
1795
+ programCounter: runtime.programCounter ?? debugState.programCounter,
1796
+ registers: debugState.registers,
1797
+ warnings
1798
+ };
1799
+ });
1800
+ }
1801
+ async stepInstruction(count = 1, stepOver = false) {
1802
+ await this.#ensurePausedForDebug(stepOver ? "execute step_over" : "execute step");
1803
+ this.#lastExecutionIntent = "step_complete";
1804
+ this.#writeProcessLogLine(`[tx] ${stepOver ? "execute step_over" : "execute step"} count=${count}`);
1805
+ await this.#client.stepInstruction(count, stepOver);
1806
+ this.#syncMonitorRuntimeState();
1807
+ const debugState = await this.#readDebugState();
1808
+ return {
1809
+ executionState: debugState.executionState,
1810
+ lastStopReason: debugState.lastStopReason,
1811
+ programCounter: debugState.programCounter,
1812
+ registers: debugState.registers,
1813
+ stepsExecuted: count,
1814
+ warnings: []
1815
+ };
1816
+ }
1817
+ async stepOut() {
1818
+ await this.#ensurePausedForDebug("execute step_out");
1819
+ this.#lastExecutionIntent = "step_complete";
1820
+ this.#writeProcessLogLine("[tx] execute step_out");
1821
+ await this.#client.stepOut();
1822
+ this.#syncMonitorRuntimeState();
1823
+ const debugState = await this.#readDebugState();
1824
+ return {
1825
+ executionState: debugState.executionState,
1826
+ lastStopReason: debugState.lastStopReason,
1827
+ programCounter: debugState.programCounter,
1828
+ registers: debugState.registers,
1829
+ warnings: []
1830
+ };
1831
+ }
1832
+ async resetMachine(mode) {
1833
+ return this.#withExecutionLock(async () => {
1834
+ await this.#ensureReady();
1835
+ const wasPaused = this.#explicitPauseActive;
1836
+ this.#lastExecutionIntent = "reset";
1837
+ this.#writeProcessLogLine(`[tx] execute reset mode=${mode}`);
1838
+ await this.#client.reset(mode);
1839
+ await sleep(RESET_GRACE_PERIOD_MS);
1840
+ this.#explicitPauseActive = wasPaused;
1841
+ this.#syncMonitorRuntimeState();
1842
+ await sleep(50);
1843
+ this.#syncMonitorRuntimeState();
1844
+ const debugState = await this.#readDebugState();
1845
+ return {
1846
+ executionState: debugState.executionState,
1847
+ lastStopReason: debugState.lastStopReason,
1848
+ programCounter: debugState.programCounter,
1849
+ registers: debugState.registers,
1850
+ warnings: []
1851
+ };
1852
+ });
1853
+ }
1854
+ async listBreakpoints(includeDisabled = true) {
1855
+ await this.#ensureReady();
1856
+ this.#writeProcessLogLine(`[tx] breakpoint_list includeDisabled=${includeDisabled}`);
1857
+ const response = await this.#client.listBreakpoints();
1858
+ this.#pruneBreakpointLabels(response.checkpoints.map((breakpoint) => breakpoint.id));
1859
+ return {
1860
+ breakpoints: response.checkpoints.filter((breakpoint) => includeDisabled ? true : breakpoint.enabled).map((breakpoint) => this.#attachBreakpointLabel(breakpoint))
1861
+ };
1862
+ }
1863
+ async setBreakpoint(options) {
1864
+ await this.#ensureReady();
1865
+ this.#writeProcessLogLine(
1866
+ `[tx] breakpoint_set kind=${options.kind} start=$${options.start.toString(16).padStart(4, "0")}${options.end == null ? "" : ` end=$${options.end.toString(16).padStart(4, "0")}`}${options.temporary ? " temporary=true" : ""}${options.enabled === false ? " enabled=false" : ""}`
1867
+ );
1868
+ const response = await this.#client.setBreakpoint({
1869
+ start: options.start,
1870
+ end: options.end,
1871
+ kind: options.kind,
1872
+ condition: options.condition,
1873
+ temporary: options.temporary,
1874
+ enabled: options.enabled,
1875
+ stopWhenHit: true
1876
+ });
1877
+ if (options.label?.trim()) {
1878
+ this.#breakpointLabels.set(response.checkpoint.id, options.label.trim());
1879
+ } else {
1880
+ this.#breakpointLabels.delete(response.checkpoint.id);
1881
+ }
1882
+ return {
1883
+ breakpoint: this.#attachBreakpointLabel(response.checkpoint),
1884
+ executionState: this.#executionState,
1885
+ lastStopReason: this.#lastStopReason,
1886
+ programCounter: this.#lastRegisters?.PC ?? null,
1887
+ registers: this.#lastRegisters
1888
+ };
1889
+ }
1890
+ async deleteBreakpoint(breakpointId) {
1891
+ await this.#ensureReady();
1892
+ this.#writeProcessLogLine(`[tx] breakpoint_clear id=${breakpointId}`);
1893
+ try {
1894
+ await this.#client.deleteBreakpoint(breakpointId);
1895
+ } catch (error) {
1896
+ if (error instanceof ViceMcpError && error.code === "emulator_protocol_error" && error.details?.emulatorErrorCode === 1) {
1897
+ return {
1898
+ cleared: false,
1899
+ breakpointId,
1900
+ executionState: this.#executionState,
1901
+ lastStopReason: this.#lastStopReason,
1902
+ programCounter: this.#lastRegisters?.PC ?? null,
1903
+ registers: this.#lastRegisters
1904
+ };
1905
+ }
1906
+ throw error;
1907
+ }
1908
+ this.#breakpointLabels.delete(breakpointId);
1909
+ return {
1910
+ cleared: true,
1911
+ breakpointId,
1912
+ executionState: this.#executionState,
1913
+ lastStopReason: this.#lastStopReason,
1914
+ programCounter: this.#lastRegisters?.PC ?? null,
1915
+ registers: this.#lastRegisters
1916
+ };
1917
+ }
1918
+ async breakpointSet(options) {
1919
+ const length = options.length ?? 1;
1920
+ if (!Number.isInteger(length) || length <= 0) {
1921
+ validationError("Breakpoint length must be a positive integer", { length });
1922
+ }
1923
+ const end = options.address + length - 1;
1924
+ this.#validateRange(options.address, end);
1925
+ return await this.setBreakpoint({
1926
+ kind: options.kind,
1927
+ start: options.address,
1928
+ end,
1929
+ condition: options.condition,
1930
+ label: options.label,
1931
+ temporary: options.temporary,
1932
+ enabled: options.enabled
1933
+ });
1934
+ }
1935
+ async breakpointClear(breakpointId) {
1936
+ return await this.deleteBreakpoint(breakpointId);
1937
+ }
1938
+ async programLoad(options) {
1939
+ const filePath = path.resolve(options.filePath);
1940
+ await this.#assertReadableProgramFile(filePath);
1941
+ await this.#ensureRunning("program_load");
1942
+ this.#explicitPauseActive = false;
1943
+ const result = await this.autostartProgram(filePath, options.autoStart ?? true, options.fileIndex ?? 0);
1944
+ return {
1945
+ filePath: result.filePath,
1946
+ autoStart: result.autoStart,
1947
+ fileIndex: result.fileIndex,
1948
+ executionState: result.executionState
1949
+ };
1950
+ }
1951
+ async captureDisplay(useVic = true) {
1952
+ return this.#withDisplayLock(async () => {
1953
+ await this.#ensureReady();
1954
+ this.#syncMonitorRuntimeState();
1955
+ const previousExecutionState = this.#executionState;
1956
+ this.#writeProcessLogLine(`[tx] capture_display useVic=${useVic}`);
1957
+ const display = await this.#client.captureDisplay(useVic);
1958
+ const palette = await this.#client.getPalette(useVic);
1959
+ if (display.bitsPerPixel !== 8) {
1960
+ throw new ViceMcpError(
1961
+ "display_bpp_unsupported",
1962
+ `capture_display only supports 8-bit indexed display payloads, got ${display.bitsPerPixel}.`,
1963
+ "unsupported",
1964
+ false,
1965
+ { bitsPerPixel: display.bitsPerPixel }
1966
+ );
1967
+ }
1968
+ const expectedLength = display.debugWidth * display.debugHeight;
1969
+ if (display.imageBytes.length !== expectedLength) {
1970
+ throw new ViceMcpError(
1971
+ "display_payload_invalid",
1972
+ "capture_display received a display payload whose length does not match the reported debug dimensions.",
1973
+ "protocol",
1974
+ false,
1975
+ {
1976
+ debugWidth: display.debugWidth,
1977
+ debugHeight: display.debugHeight,
1978
+ expectedLength,
1979
+ actualLength: display.imageBytes.length
1980
+ }
1981
+ );
1982
+ }
1983
+ if (palette.items.length === 0) {
1984
+ throw new ViceMcpError("display_palette_missing", "capture_display received an empty display palette.", "protocol", false);
1985
+ }
1986
+ if (display.debugOffsetX + display.innerWidth > display.debugWidth || display.debugOffsetY + display.innerHeight > display.debugHeight) {
1987
+ throw new ViceMcpError(
1988
+ "display_crop_invalid",
1989
+ "capture_display received crop geometry outside the display buffer bounds.",
1990
+ "protocol",
1991
+ false,
1992
+ {
1993
+ debugWidth: display.debugWidth,
1994
+ debugHeight: display.debugHeight,
1995
+ debugOffsetX: display.debugOffsetX,
1996
+ debugOffsetY: display.debugOffsetY,
1997
+ innerWidth: display.innerWidth,
1998
+ innerHeight: display.innerHeight
1999
+ }
2000
+ );
2001
+ }
2002
+ const rgbPixels = new Uint8Array(display.innerWidth * display.innerHeight * 3);
2003
+ for (let y = 0; y < display.innerHeight; y += 1) {
2004
+ for (let x = 0; x < display.innerWidth; x += 1) {
2005
+ const sourceX = display.debugOffsetX + x;
2006
+ const sourceY = display.debugOffsetY + y;
2007
+ const sourceIndex = sourceY * display.debugWidth + sourceX;
2008
+ const paletteIndex = display.imageBytes[sourceIndex];
2009
+ const color = palette.items[paletteIndex];
2010
+ if (!color) {
2011
+ throw new ViceMcpError(
2012
+ "display_palette_index_invalid",
2013
+ "capture_display encountered a pixel index that is outside the current palette.",
2014
+ "protocol",
2015
+ false,
2016
+ { paletteIndex, paletteSize: palette.items.length }
2017
+ );
2018
+ }
2019
+ const targetIndex = (y * display.innerWidth + x) * 3;
2020
+ rgbPixels[targetIndex] = color.red;
2021
+ rgbPixels[targetIndex + 1] = color.green;
2022
+ rgbPixels[targetIndex + 2] = color.blue;
2023
+ }
2024
+ }
2025
+ await fs.mkdir(DISPLAY_CAPTURE_DIR, { recursive: true });
2026
+ const imagePath = path.join(DISPLAY_CAPTURE_DIR, `capture-${Date.now()}-${process.pid}.png`);
2027
+ await fs.writeFile(imagePath, encodePngRgb(display.innerWidth, display.innerHeight, rgbPixels));
2028
+ await this.#settleDisplayToolState("capture_display", previousExecutionState);
2029
+ return {
2030
+ imagePath,
2031
+ width: display.innerWidth,
2032
+ height: display.innerHeight,
2033
+ debugWidth: display.debugWidth,
2034
+ debugHeight: display.debugHeight,
2035
+ debugOffsetX: display.debugOffsetX,
2036
+ debugOffsetY: display.debugOffsetY,
2037
+ bitsPerPixel: display.bitsPerPixel
2038
+ };
2039
+ });
2040
+ }
2041
+ async getDisplayState() {
2042
+ await this.#ensureReady();
2043
+ this.#syncMonitorRuntimeState();
2044
+ const previousExecutionState = this.#executionState;
2045
+ await this.#pauseForDisplayInspection("get_display_state", previousExecutionState);
2046
+ try {
2047
+ this.#writeProcessLogLine("[tx] get_display_state");
2048
+ const vicPrimary = await this.#client.readMemory(53265, 53272, 0);
2049
+ const cia2Bank = await this.#client.readMemory(56576, 56576, 0);
2050
+ const colors = await this.#client.readMemory(53280, 53284, 0);
2051
+ const d011 = vicPrimary.bytes[0] ?? 0;
2052
+ const d016 = vicPrimary.bytes[5] ?? 0;
2053
+ const d018 = vicPrimary.bytes[7] ?? 0;
2054
+ const dd00 = cia2Bank.bytes[0] ?? 0;
2055
+ const d020 = colors.bytes[0] ?? 0;
2056
+ const d021 = colors.bytes[1] ?? 0;
2057
+ const d022 = colors.bytes[2] ?? 0;
2058
+ const d023 = colors.bytes[3] ?? 0;
2059
+ const d024 = colors.bytes[4] ?? 0;
2060
+ const vicBankAddress = decodeVicBankAddress(dd00);
2061
+ const screenRamAddress = vicBankAddress + (d018 >> 4 & 15) * 1024;
2062
+ const characterMemoryAddress = vicBankAddress + (d018 >> 1 & 7) * 2048;
2063
+ const bitmapMemoryAddress = vicBankAddress + (d018 >> 3 & 1) * 8192;
2064
+ const { graphicsMode, extendedColorMode, bitmapMode, multicolorMode } = decodeGraphicsMode(d011, d016);
2065
+ const screenRam = await this.#client.readMemory(screenRamAddress, screenRamAddress + 999, 0);
2066
+ const colorRam = await this.#client.readMemory(55296, 55296 + 999, 0);
2067
+ return {
2068
+ graphicsMode,
2069
+ extendedColorMode,
2070
+ bitmapMode,
2071
+ multicolorMode,
2072
+ vicBankAddress,
2073
+ screenRamAddress,
2074
+ characterMemoryAddress: bitmapMode ? null : characterMemoryAddress,
2075
+ bitmapMemoryAddress: bitmapMode ? bitmapMemoryAddress : null,
2076
+ colorRamAddress: 55296,
2077
+ borderColor: lowNibble(d020),
2078
+ backgroundColor0: lowNibble(d021),
2079
+ backgroundColor1: lowNibble(d022),
2080
+ backgroundColor2: lowNibble(d023),
2081
+ backgroundColor3: lowNibble(d024),
2082
+ vicRegisters: {
2083
+ d011,
2084
+ d016,
2085
+ d018,
2086
+ dd00,
2087
+ d020,
2088
+ d021,
2089
+ d022,
2090
+ d023,
2091
+ d024
2092
+ },
2093
+ screenRam: Array.from(screenRam.bytes),
2094
+ colorRam: Array.from(colorRam.bytes, (value) => lowNibble(value))
2095
+ };
2096
+ } finally {
2097
+ await this.#settleDisplayToolState("get_display_state", previousExecutionState);
2098
+ }
2099
+ }
2100
+ async getDisplayText() {
2101
+ let displayState = await this.getDisplayState();
2102
+ if (displayState.screenRamAddress === 0 && displayState.graphicsMode === "standard_text") {
2103
+ this.#writeProcessLogLine("[display] screen RAM address still zero in text mode, retrying display state after short settle");
2104
+ await sleep(250);
2105
+ displayState = await this.getDisplayState();
2106
+ }
2107
+ if (!isTextGraphicsMode(displayState.graphicsMode)) {
2108
+ unsupportedError("get_display_text is only available when the current graphics mode is a text mode.", {
2109
+ graphicsMode: displayState.graphicsMode
2110
+ });
2111
+ }
2112
+ const columns = 40;
2113
+ const rows = 25;
2114
+ let hasDetailedTokens = false;
2115
+ const textLines = Array.from({ length: rows }, (_, row) => {
2116
+ const start = row * columns;
2117
+ return displayState.screenRam.slice(start, start + columns).map((code) => {
2118
+ const decoded = decodeScreenCodeCell(code);
2119
+ hasDetailedTokens ||= decoded.lossy;
2120
+ return decoded.ascii;
2121
+ }).join("").replace(/\s+$/, "");
2122
+ });
2123
+ const tokenLines = hasDetailedTokens ? Array.from({ length: rows }, (_, row) => {
2124
+ const start = row * columns;
2125
+ return displayState.screenRam.slice(start, start + columns).map((code) => {
2126
+ const decoded = decodeScreenCodeCell(code);
2127
+ return decoded.token ?? decoded.ascii;
2128
+ }).join("").replace(/\s+$/, "");
2129
+ }) : void 0;
2130
+ return {
2131
+ graphicsMode: displayState.graphicsMode,
2132
+ textMode: true,
2133
+ lossy: hasDetailedTokens,
2134
+ columns,
2135
+ rows,
2136
+ screenRamAddress: displayState.screenRamAddress,
2137
+ textLines,
2138
+ ...tokenLines ? { tokenLines } : {}
2139
+ };
2140
+ }
2141
+ async autostartProgram(filePath, autoStart = true, fileIndex = 0) {
2142
+ await this.#ensureReady();
2143
+ const absolutePath = path.resolve(filePath);
2144
+ const previousExecutionState = this.#executionState;
2145
+ const previousStopReason = this.#lastStopReason;
2146
+ const executionEvent = this.#waitForExecutionEvent(EXECUTION_EVENT_WAIT_MS);
2147
+ this.#lastExecutionIntent = autoStart ? "none" : "monitor_entry";
2148
+ try {
2149
+ this.#writeProcessLogLine(`[tx] program_load filePath=${absolutePath} autoStart=${autoStart} fileIndex=${fileIndex}`);
2150
+ await this.#client.autostartProgram(absolutePath, autoStart, fileIndex);
2151
+ } catch (error) {
2152
+ const event2 = await executionEvent;
2153
+ const accepted = this.#autostartWasAcceptedAfterError(error, event2, previousExecutionState, previousStopReason);
2154
+ if (!accepted) {
2155
+ throw error;
2156
+ }
2157
+ }
2158
+ const event = await executionEvent;
2159
+ if (event) {
2160
+ } else {
2161
+ this.#writeProcessLogLine(
2162
+ `[autostart] no runtime event observed within ${EXECUTION_EVENT_WAIT_MS}ms, waiting ${EXECUTION_SETTLE_DELAY_MS}ms for emulator settle`
2163
+ );
2164
+ await sleep(EXECUTION_SETTLE_DELAY_MS);
2165
+ }
2166
+ await this.#settleProgramLoadState(autoStart);
2167
+ return {
2168
+ filePath: absolutePath,
2169
+ autoStart,
2170
+ fileIndex,
2171
+ executionState: this.#executionState
2172
+ };
2173
+ }
2174
+ async #assertReadableProgramFile(filePath) {
2175
+ let stats;
2176
+ try {
2177
+ await fs.access(filePath);
2178
+ stats = await fs.stat(filePath);
2179
+ } catch (error) {
2180
+ throw new ViceMcpError("program_file_missing", `Program file does not exist or is not readable: ${filePath}`, "io", false, {
2181
+ filePath,
2182
+ cause: error instanceof Error ? error.message : String(error)
2183
+ });
2184
+ }
2185
+ if (!stats.isFile()) {
2186
+ throw new ViceMcpError("program_file_invalid", `Program path is not a regular file: ${filePath}`, "io", false, {
2187
+ filePath
2188
+ });
2189
+ }
2190
+ }
2191
+ 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
+ };
2207
+ }
2208
+ 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
+ };
2232
+ }
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
+ }
2253
+ }
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
+ });
2268
+ }
2269
+ }
2270
+ for (const heldKey of normalizedKeys) {
2271
+ const interval = this.#heldKeyboardIntervals.get(heldKey);
2272
+ if (interval) {
2273
+ clearInterval(interval);
2274
+ this.#heldKeyboardIntervals.delete(heldKey);
2275
+ }
2276
+ }
2277
+ return {
2278
+ action,
2279
+ keys: normalizedKeys,
2280
+ applied: true,
2281
+ held: false,
2282
+ mode: "buffered_text_repeat"
2283
+ };
2284
+ }
2285
+ }
2286
+ }
2287
+ 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;
2304
+ }
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
+ };
2320
+ }
2321
+ async waitForState(targetState, timeoutMs = 5e3, stableMs = targetState === "running" ? INPUT_RUNNING_STABLE_MS : 0) {
2322
+ await this.#ensureReady();
2323
+ const startedAt = Date.now();
2324
+ const deadline = startedAt + timeoutMs;
2325
+ let matchingSince = null;
2326
+ while (true) {
2327
+ this.#syncMonitorRuntimeState();
2328
+ if (this.#executionState === targetState) {
2329
+ matchingSince ??= Date.now();
2330
+ if (Date.now() - matchingSince >= stableMs) {
2331
+ const runtime = this.#client.runtimeState();
2332
+ return {
2333
+ executionState: this.#executionState,
2334
+ lastStopReason: this.#lastStopReason,
2335
+ runtimeKnown: runtime.runtimeKnown,
2336
+ programCounter: runtime.programCounter,
2337
+ reachedTarget: true,
2338
+ waitedMs: Date.now() - startedAt
2339
+ };
2340
+ }
2341
+ } else {
2342
+ matchingSince = null;
2343
+ }
2344
+ if (Date.now() >= deadline) {
2345
+ const runtime = this.#client.runtimeState();
2346
+ return {
2347
+ executionState: this.#executionState,
2348
+ lastStopReason: this.#lastStopReason,
2349
+ runtimeKnown: runtime.runtimeKnown,
2350
+ programCounter: runtime.programCounter,
2351
+ reachedTarget: false,
2352
+ waitedMs: Date.now() - startedAt
2353
+ };
2354
+ }
2355
+ await sleep(INPUT_SETTLE_POLL_MS);
2356
+ }
2357
+ }
2358
+ async #ensureReady() {
2359
+ this.#ensureConfig();
2360
+ await this.#ensureHealthyConnection();
2361
+ }
2362
+ async #ensurePausedForDebug(commandName) {
2363
+ await this.#ensureReady();
2364
+ this.#syncMonitorRuntimeState();
2365
+ if (this.#executionState !== "stopped") {
2366
+ debuggerNotPausedError(commandName, {
2367
+ executionState: this.#executionState,
2368
+ lastStopReason: this.#lastStopReason
2369
+ });
2370
+ }
2371
+ }
2372
+ async #ensureRunning(commandName) {
2373
+ await this.#ensureReady();
2374
+ this.#syncMonitorRuntimeState();
2375
+ if (this.#executionState !== "running") {
2376
+ emulatorNotRunningError(commandName, {
2377
+ executionState: this.#executionState,
2378
+ lastStopReason: this.#lastStopReason
2379
+ });
2380
+ }
2381
+ }
2382
+ async #ensureHealthyConnection() {
2383
+ if (this.#recoveryPromise) {
2384
+ await this.#recoveryPromise;
2385
+ }
2386
+ const processAlive = this.#process != null && this.#process.exitCode == null && !this.#process.killed;
2387
+ if (processAlive && this.#client.connected) {
2388
+ this.#syncMonitorRuntimeState();
2389
+ return;
2390
+ }
2391
+ await this.#scheduleRecovery();
2392
+ }
2393
+ async #scheduleRecovery() {
2394
+ this.#ensureConfig();
2395
+ if (this.#recoveryPromise) {
2396
+ return await this.#recoveryPromise;
2397
+ }
2398
+ this.#recoveryPromise = (async () => {
2399
+ this.#recoveryInProgress = true;
2400
+ try {
2401
+ const processAlive = this.#process != null && this.#process.exitCode == null && !this.#process.killed;
2402
+ if (processAlive && this.#host && this.#port) {
2403
+ this.#transportState = "reconnecting";
2404
+ await this.#client.connect(this.#host, this.#port);
2405
+ this.#transportState = "connected";
2406
+ if (!this.#connectedSince) {
2407
+ this.#connectedSince = nowIso();
2408
+ }
2409
+ await this.#hydrateExecutionState();
2410
+ return;
2411
+ }
2412
+ await this.#launchManagedEmulator("restart");
2413
+ } finally {
2414
+ this.#recoveryInProgress = false;
2415
+ this.#recoveryPromise = null;
2416
+ }
2417
+ })();
2418
+ return await this.#recoveryPromise;
2419
+ }
2420
+ async #replaceManagedEmulator(mode) {
2421
+ await this.#stopManagedProcess(true);
2422
+ await this.#launchManagedEmulator(mode);
2423
+ }
2424
+ async #launchManagedEmulator(mode) {
2425
+ const config = this.#ensureConfig();
2426
+ const host2 = DEFAULT_MONITOR_HOST;
2427
+ const port2 = await this.#portAllocator.allocate();
2428
+ await this.#portAllocator.ensureFree(port2, host2);
2429
+ const binary = config.binaryPath ?? DEFAULT_C64_BINARY;
2430
+ const args = ["-autostartprgmode", "1", "-binarymonitor", "-binarymonitoraddress", `${host2}:${port2}`];
2431
+ if (config.arguments) {
2432
+ args.push(...splitCommandLine(config.arguments));
2433
+ }
2434
+ this.#transportState = "starting";
2435
+ this.#processState = "launching";
2436
+ this.#host = host2;
2437
+ this.#port = port2;
2438
+ this.#connectedSince = null;
2439
+ this.#executionState = "unknown";
2440
+ this.#breakpointLabels.clear();
2441
+ this.#explicitPauseActive = false;
2442
+ this.#pendingCheckpointHit = null;
2443
+ this.#lastCheckpointHit = null;
2444
+ this.#lastRuntimeEventType = "unknown";
2445
+ this.#lastRuntimeProgramCounter = null;
2446
+ const env = await buildViceLaunchEnv();
2447
+ const child = spawn(binary, args, {
2448
+ cwd: config.workingDirectory ? path.resolve(config.workingDirectory) : void 0,
2449
+ env,
2450
+ stdio: ["ignore", "pipe", "pipe"]
2451
+ });
2452
+ this.#process = child;
2453
+ this.#attachProcessLogging(child, binary, args);
2454
+ this.#bindProcessLifecycle(child);
2455
+ this.#processState = "running";
2456
+ this.#transportState = "waiting_for_monitor";
2457
+ try {
2458
+ await waitForMonitor(host2, port2, 5e3);
2459
+ await this.#client.connect(host2, port2);
2460
+ this.#transportState = "connected";
2461
+ this.#connectedSince = nowIso();
2462
+ this.#launchId += 1;
2463
+ if (mode === "restart") {
2464
+ this.#restartCount += 1;
2465
+ }
2466
+ this.#freshEmulatorPending = true;
2467
+ await this.#hydrateExecutionState();
2468
+ } catch (error) {
2469
+ this.#processState = "crashed";
2470
+ this.#transportState = "faulted";
2471
+ this.#warnings = [...this.#warnings.filter((warning) => warning.code !== "launch_failed"), makeWarning(String(error.message ?? error), "launch_failed")];
2472
+ await this.#stopManagedProcess(true);
2473
+ throw error;
2474
+ }
2475
+ }
2476
+ #ensureConfig() {
2477
+ if (!this.#config) {
2478
+ this.#config = defaultC64Config();
2479
+ }
2480
+ return this.#config;
2481
+ }
2482
+ #bindProcessLifecycle(child) {
2483
+ child.once("exit", (code, signal) => {
2484
+ if (this.#process !== child) {
2485
+ return;
2486
+ }
2487
+ this.#closeProcessLog(child, `process exit (${code ?? "null"} / ${signal ?? "null"})`);
2488
+ this.#process = null;
2489
+ this.#processState = code === 0 ? "exited" : "crashed";
2490
+ this.#transportState = "disconnected";
2491
+ this.#warnings = [
2492
+ ...this.#warnings.filter((warning) => warning.code !== "process_exit"),
2493
+ makeWarning(`C64 emulator process exited (${code ?? "null"} / ${signal ?? "null"})`, "process_exit")
2494
+ ];
2495
+ if (!this.#suppressRecovery && !this.#shuttingDown && this.#config) {
2496
+ void this.#scheduleRecovery();
2497
+ }
2498
+ });
2499
+ child.once("error", (error) => {
2500
+ if (this.#process !== child) {
2501
+ return;
2502
+ }
2503
+ this.#closeProcessLog(child, `process error (${error.message})`);
2504
+ this.#process = null;
2505
+ this.#processState = "crashed";
2506
+ this.#transportState = "faulted";
2507
+ this.#warnings = [...this.#warnings.filter((warning) => warning.code !== "process_error"), makeWarning(error.message, "process_error")];
2508
+ if (!this.#suppressRecovery && !this.#shuttingDown && this.#config) {
2509
+ void this.#scheduleRecovery();
2510
+ }
2511
+ });
2512
+ }
2513
+ #attachProcessLogging(child, binary, args) {
2514
+ const logStream = createWriteStream(VICE_PROCESS_LOG_PATH, { flags: "a" });
2515
+ this.#processLogStream = logStream;
2516
+ this.#stdoutMirrorBuffer = "";
2517
+ this.#stderrMirrorBuffer = "";
2518
+ logStream.write(`
2519
+ === Emulator launch ${nowIso()} ===
2520
+ `);
2521
+ logStream.write(`binary: ${binary}
2522
+ `);
2523
+ logStream.write(`args: ${args.join(" ")}
2524
+ `);
2525
+ child.stdout?.pipe(logStream, { end: false });
2526
+ child.stderr?.pipe(logStream, { end: false });
2527
+ if (MIRROR_EMULATOR_LOGS_TO_STDERR) {
2528
+ child.stdout?.on("data", (chunk) => {
2529
+ this.#mirrorViceOutputChunk("stdout", chunk);
2530
+ });
2531
+ child.stderr?.on("data", (chunk) => {
2532
+ this.#mirrorViceOutputChunk("stderr", chunk);
2533
+ });
2534
+ }
2535
+ }
2536
+ #closeProcessLog(child, reason) {
2537
+ const logStream = this.#processLogStream;
2538
+ if (!logStream) {
2539
+ return;
2540
+ }
2541
+ child.stdout?.unpipe(logStream);
2542
+ child.stderr?.unpipe(logStream);
2543
+ this.#flushViceOutputMirror("stdout");
2544
+ this.#flushViceOutputMirror("stderr");
2545
+ logStream.write(`
2546
+ === Emulator stream closed ${nowIso()} (${reason}) ===
2547
+ `);
2548
+ logStream.end();
2549
+ this.#processLogStream = null;
2550
+ }
2551
+ #mirrorViceOutputChunk(stream, chunk) {
2552
+ const text = String(chunk);
2553
+ const buffer = stream === "stdout" ? this.#stdoutMirrorBuffer : this.#stderrMirrorBuffer;
2554
+ const combined = buffer + text;
2555
+ const lines = combined.split(/\r?\n/);
2556
+ const remainder = lines.pop() ?? "";
2557
+ for (const line of lines) {
2558
+ process.stderr.write(`[vice ${stream}] ${line}
2559
+ `);
2560
+ }
2561
+ if (stream === "stdout") {
2562
+ this.#stdoutMirrorBuffer = remainder;
2563
+ } else {
2564
+ this.#stderrMirrorBuffer = remainder;
2565
+ }
2566
+ }
2567
+ #flushViceOutputMirror(stream) {
2568
+ const remainder = stream === "stdout" ? this.#stdoutMirrorBuffer : this.#stderrMirrorBuffer;
2569
+ if (!remainder) {
2570
+ return;
2571
+ }
2572
+ process.stderr.write(`[vice ${stream}] ${remainder}
2573
+ `);
2574
+ if (stream === "stdout") {
2575
+ this.#stdoutMirrorBuffer = "";
2576
+ } else {
2577
+ this.#stderrMirrorBuffer = "";
2578
+ }
2579
+ }
2580
+ #writeProcessLogLine(line) {
2581
+ this.#processLogStream?.write(`${nowIso()} ${line}
2582
+ `);
2583
+ if (MIRROR_EMULATOR_LOGS_TO_STDERR) {
2584
+ process.stderr.write(`[vice monitor] ${line}
2585
+ `);
2586
+ }
2587
+ }
2588
+ async #stopManagedProcess(fullReset) {
2589
+ this.#suppressRecovery = true;
2590
+ try {
2591
+ this.#clearHeldInputState();
2592
+ const processId = this.#process?.pid ?? null;
2593
+ this.#breakpointLabels.clear();
2594
+ this.#explicitPauseActive = false;
2595
+ this.#pendingCheckpointHit = null;
2596
+ this.#lastCheckpointHit = null;
2597
+ if (this.#client.connected) {
2598
+ try {
2599
+ await this.#client.quit();
2600
+ } catch {
2601
+ if (this.#process) {
2602
+ this.#process.kill("SIGTERM");
2603
+ }
2604
+ }
2605
+ } else if (this.#process) {
2606
+ this.#process.kill("SIGTERM");
2607
+ }
2608
+ if (this.#process) {
2609
+ await waitForProcessExit(this.#process, 1e3).catch(() => {
2610
+ this.#process?.kill("SIGKILL");
2611
+ });
2612
+ }
2613
+ await this.#client.disconnect();
2614
+ this.#process = null;
2615
+ if (fullReset) {
2616
+ this.#transportState = "stopped";
2617
+ this.#processState = processId == null ? "not_applicable" : "exited";
2618
+ }
2619
+ } finally {
2620
+ this.#suppressRecovery = false;
2621
+ }
2622
+ }
2623
+ async #resumeBeforeShutdown() {
2624
+ if (!this.#client.connected) {
2625
+ return;
2626
+ }
2627
+ this.#syncMonitorRuntimeState();
2628
+ if (this.#executionState !== "stopped") {
2629
+ return;
2630
+ }
2631
+ this.#writeProcessLogLine("[shutdown] emulator stopped in monitor, resuming before quit");
2632
+ try {
2633
+ this.#lastExecutionIntent = "unknown";
2634
+ await this.#client.continueExecution();
2635
+ await this.#waitForExecutionEvent(1e3);
2636
+ this.#syncMonitorRuntimeState();
2637
+ } catch (error) {
2638
+ this.#writeProcessLogLine(
2639
+ `[shutdown] resume before quit failed: ${error instanceof Error ? error.message : String(error)}`
2640
+ );
2641
+ }
2642
+ }
2643
+ async #hydrateExecutionState() {
2644
+ await this.#stabilizeLaunchExecutionState();
2645
+ }
2646
+ async #readRegisters() {
2647
+ const metadata = await this.#client.getRegistersAvailable();
2648
+ const values = await this.#client.getRegisters();
2649
+ const registers = this.#mapC64Registers(metadata.registers, values.registers);
2650
+ this.#lastRegisters = registers;
2651
+ return registers;
2652
+ }
2653
+ async #readDebugState() {
2654
+ const registers = await this.#readRegisters();
2655
+ return this.#buildDebugState(registers);
2656
+ }
2657
+ #buildDebugState(registers) {
2658
+ this.#lastRegisters = registers;
2659
+ return {
2660
+ executionState: this.#executionState,
2661
+ lastStopReason: this.#lastStopReason,
2662
+ programCounter: registers.PC,
2663
+ registers
2664
+ };
2665
+ }
2666
+ #mergeRegisters(current, updated) {
2667
+ if (!current) {
2668
+ return null;
2669
+ }
2670
+ return {
2671
+ ...current,
2672
+ ...updated
2673
+ };
2674
+ }
2675
+ #validateRange(start, end) {
2676
+ if (end < start) {
2677
+ validationError("End address must be greater than or equal to start address", { start, end });
2678
+ }
2679
+ if (start < 0 || end > 65535) {
2680
+ validationError("Address range must fit in 16-bit address space", { start, end });
2681
+ }
2682
+ }
2683
+ #mapC64Registers(metadata, values) {
2684
+ const metadataByName = new Map(metadata.map((item) => [item.name.toUpperCase(), item]));
2685
+ const valuesById = new Map(values.map((item) => [item.id, item.value]));
2686
+ return Object.fromEntries(
2687
+ C64_REGISTER_DEFINITIONS.map((definition) => {
2688
+ const meta = metadataByName.get(definition.viceName.toUpperCase());
2689
+ if (!meta) {
2690
+ validationError(`Required C64 register is missing from the emulator: ${definition.viceName}`, {
2691
+ registerName: definition.fieldName,
2692
+ viceName: definition.viceName
2693
+ });
2694
+ }
2695
+ const value = valuesById.get(meta.id);
2696
+ if (value == null) {
2697
+ validationError(`Required C64 register value is missing from the emulator: ${definition.viceName}`, {
2698
+ registerName: definition.fieldName,
2699
+ viceName: definition.viceName
2700
+ });
2701
+ }
2702
+ return [definition.fieldName, value];
2703
+ })
2704
+ );
2705
+ }
2706
+ #getJoystickMask(port2) {
2707
+ return this.#heldJoystickMasks.get(port2) ?? JOYSTICK_RELEASED_MASK;
2708
+ }
2709
+ async #applyJoystickMask(port2, mask) {
2710
+ const normalizedMask = mask & JOYSTICK_RELEASED_MASK;
2711
+ this.#heldJoystickMasks.set(port2, normalizedMask);
2712
+ await this.#client.setJoyport(joystickPortToProtocol(port2), normalizedMask);
2713
+ }
2714
+ #describeJoystickState(port2) {
2715
+ const mask = this.#getJoystickMask(port2);
2716
+ return {
2717
+ up: (mask & JOYSTICK_CONTROL_BITS.up) === 0,
2718
+ down: (mask & JOYSTICK_CONTROL_BITS.down) === 0,
2719
+ left: (mask & JOYSTICK_CONTROL_BITS.left) === 0,
2720
+ right: (mask & JOYSTICK_CONTROL_BITS.right) === 0,
2721
+ fire: (mask & JOYSTICK_CONTROL_BITS.fire) === 0
2722
+ };
2723
+ }
2724
+ #clearHeldInputState() {
2725
+ for (const interval of this.#heldKeyboardIntervals.values()) {
2726
+ clearInterval(interval);
2727
+ }
2728
+ this.#heldKeyboardIntervals.clear();
2729
+ this.#heldJoystickMasks.clear();
2730
+ }
2731
+ #attachBreakpointLabel(breakpoint) {
2732
+ return {
2733
+ ...breakpoint,
2734
+ label: this.#breakpointLabels.get(breakpoint.id) ?? null
2735
+ };
2736
+ }
2737
+ #pruneBreakpointLabels(activeBreakpointIds) {
2738
+ const active = new Set(activeBreakpointIds);
2739
+ for (const breakpointId of this.#breakpointLabels.keys()) {
2740
+ if (!active.has(breakpointId)) {
2741
+ this.#breakpointLabels.delete(breakpointId);
2742
+ }
2743
+ }
2744
+ }
2745
+ async #waitForExecutionEvent(timeoutMs) {
2746
+ return await new Promise((resolve) => {
2747
+ const timer = setTimeout(() => {
2748
+ this.#client.off("event", onEvent);
2749
+ resolve(null);
2750
+ }, timeoutMs);
2751
+ const onEvent = (event) => {
2752
+ if (event.type !== "resumed" && event.type !== "stopped" && event.type !== "jam") {
2753
+ return;
2754
+ }
2755
+ clearTimeout(timer);
2756
+ this.#client.off("event", onEvent);
2757
+ resolve({ type: event.type });
2758
+ };
2759
+ this.#client.on("event", onEvent);
2760
+ });
2761
+ }
2762
+ async #stabilizeLaunchExecutionState() {
2763
+ this.#writeProcessLogLine(`[bootstrap] waiting ${BOOTSTRAP_INITIAL_DELAY_MS}ms before probing launch state`);
2764
+ await sleep(BOOTSTRAP_INITIAL_DELAY_MS);
2765
+ const deadline = Date.now() + BOOTSTRAP_SETTLE_TIMEOUT_MS;
2766
+ let runningSince = null;
2767
+ let lastResumeAt = 0;
2768
+ while (true) {
2769
+ this.#syncMonitorRuntimeState();
2770
+ if (this.#executionState === "running") {
2771
+ runningSince ??= Date.now();
2772
+ if (Date.now() - runningSince >= BOOTSTRAP_RUNNING_STABLE_MS) {
2773
+ this.#writeProcessLogLine("[bootstrap] emulator reached stable running state after launch");
2774
+ return;
2775
+ }
2776
+ } else {
2777
+ runningSince = null;
2778
+ }
2779
+ if (this.#executionState !== "running" && Date.now() - lastResumeAt >= BOOTSTRAP_RESUME_COOLDOWN_MS) {
2780
+ this.#writeProcessLogLine(`[bootstrap] observed ${this.#executionState} state after launch, sending resume`);
2781
+ const previousExecutionState = this.#executionState;
2782
+ const previousStopReason = this.#lastStopReason;
2783
+ const executionEvent = this.#waitForExecutionEvent(EXECUTION_EVENT_WAIT_MS);
2784
+ this.#lastExecutionIntent = "unknown";
2785
+ try {
2786
+ await this.#client.continueExecution();
2787
+ } catch (error) {
2788
+ const event2 = await executionEvent;
2789
+ const accepted = this.#resumeWasAcceptedAfterError(error, event2, previousExecutionState, previousStopReason);
2790
+ if (!accepted) {
2791
+ this.#writeProcessLogLine(
2792
+ `[bootstrap] resume after launch failed without runtime transition: ${error instanceof Error ? error.message : String(error)}`
2793
+ );
2794
+ throw error;
2795
+ }
2796
+ }
2797
+ const event = await executionEvent;
2798
+ if (!event) {
2799
+ this.#writeProcessLogLine(
2800
+ `[bootstrap] no runtime event observed within ${EXECUTION_EVENT_WAIT_MS}ms after resume, waiting ${BOOTSTRAP_POLL_MS}ms`
2801
+ );
2802
+ await sleep(BOOTSTRAP_POLL_MS);
2803
+ }
2804
+ lastResumeAt = Date.now();
2805
+ continue;
2806
+ }
2807
+ if (Date.now() >= deadline) {
2808
+ this.#writeProcessLogLine(
2809
+ `[bootstrap] settle timeout reached after ${BOOTSTRAP_SETTLE_TIMEOUT_MS}ms with executionState=${this.#executionState}`
2810
+ );
2811
+ throw new ViceMcpError(
2812
+ "bootstrap_timeout",
2813
+ "Emulator launch did not reach a stable running state before timeout.",
2814
+ "timeout",
2815
+ true,
2816
+ {
2817
+ executionState: this.#executionState,
2818
+ lastStopReason: this.#lastStopReason
2819
+ }
2820
+ );
2821
+ }
2822
+ await sleep(BOOTSTRAP_POLL_MS);
2823
+ }
2824
+ }
2825
+ async #withExecutionLock(operation) {
2826
+ while (this.#executionOperationLock) {
2827
+ await this.#executionOperationLock;
2828
+ }
2829
+ let resolve;
2830
+ this.#executionOperationLock = new Promise((r) => resolve = r);
2831
+ try {
2832
+ return await operation();
2833
+ } finally {
2834
+ this.#executionOperationLock = null;
2835
+ resolve();
2836
+ }
2837
+ }
2838
+ async #withDisplayLock(operation) {
2839
+ while (this.#displayOperationLock) {
2840
+ await this.#displayOperationLock;
2841
+ }
2842
+ let resolve;
2843
+ this.#displayOperationLock = new Promise((r) => resolve = r);
2844
+ try {
2845
+ return await operation();
2846
+ } finally {
2847
+ this.#displayOperationLock = null;
2848
+ resolve();
2849
+ }
2850
+ }
2851
+ async #settleProgramLoadState(autoStart) {
2852
+ const deadline = Date.now() + PROGRAM_LOAD_SETTLE_TIMEOUT_MS;
2853
+ let runningSince = null;
2854
+ let lastResumeAt = 0;
2855
+ while (true) {
2856
+ this.#syncMonitorRuntimeState();
2857
+ if (this.#executionState === "running") {
2858
+ runningSince ??= Date.now();
2859
+ if (Date.now() - runningSince >= PROGRAM_LOAD_RUNNING_STABLE_MS) {
2860
+ return;
2861
+ }
2862
+ } else {
2863
+ runningSince = null;
2864
+ }
2865
+ if (this.#executionState === "stopped" && Date.now() - lastResumeAt >= PROGRAM_LOAD_RESUME_COOLDOWN_MS) {
2866
+ this.#writeProcessLogLine(
2867
+ `[autostart] observed stopped state after load with autoStart=${autoStart}, sending resume`
2868
+ );
2869
+ this.#explicitPauseActive = false;
2870
+ this.#lastExecutionIntent = "unknown";
2871
+ await this.#client.continueExecution();
2872
+ lastResumeAt = Date.now();
2873
+ }
2874
+ if (Date.now() >= deadline) {
2875
+ this.#writeProcessLogLine(
2876
+ `[autostart] settle timeout reached after ${PROGRAM_LOAD_SETTLE_TIMEOUT_MS}ms with executionState=${this.#executionState}`
2877
+ );
2878
+ return;
2879
+ }
2880
+ await sleep(PROGRAM_LOAD_SETTLE_POLL_MS);
2881
+ }
2882
+ }
2883
+ async #pauseForDisplayInspection(commandName, previousExecutionState) {
2884
+ if (previousExecutionState === "stopped") {
2885
+ return;
2886
+ }
2887
+ if (previousExecutionState !== "running") {
2888
+ emulatorNotRunningError(commandName, {
2889
+ executionState: previousExecutionState,
2890
+ lastStopReason: this.#lastStopReason
2891
+ });
2892
+ }
2893
+ this.#writeProcessLogLine(`[display] ${commandName} started while running, waiting for a temporary stop`);
2894
+ await this.#client.setBreakpoint({
2895
+ start: 0,
2896
+ end: 65535,
2897
+ kind: "exec",
2898
+ temporary: true,
2899
+ enabled: true,
2900
+ stopWhenHit: true
2901
+ });
2902
+ const deadline = Date.now() + DISPLAY_PAUSE_TIMEOUT_MS;
2903
+ while (true) {
2904
+ this.#syncMonitorRuntimeState();
2905
+ if (this.#executionState === "stopped") {
2906
+ return;
2907
+ }
2908
+ if (Date.now() >= deadline) {
2909
+ throw new ViceMcpError(
2910
+ "display_pause_timeout",
2911
+ `${commandName} could not reach a temporary stopped state before timeout.`,
2912
+ "timeout",
2913
+ true,
2914
+ {
2915
+ commandName,
2916
+ executionState: this.#executionState,
2917
+ lastStopReason: this.#lastStopReason
2918
+ }
2919
+ );
2920
+ }
2921
+ await sleep(DISPLAY_SETTLE_POLL_MS);
2922
+ }
2923
+ }
2924
+ async #settleDisplayToolState(commandName, previousExecutionState) {
2925
+ this.#syncMonitorRuntimeState();
2926
+ if (previousExecutionState !== "running") {
2927
+ return;
2928
+ }
2929
+ const deadline = Date.now() + DISPLAY_SETTLE_TIMEOUT_MS;
2930
+ let runningSince = null;
2931
+ let lastResumeAt = 0;
2932
+ while (true) {
2933
+ this.#syncMonitorRuntimeState();
2934
+ if (this.#executionState === "running") {
2935
+ runningSince ??= Date.now();
2936
+ if (Date.now() - runningSince >= DISPLAY_RUNNING_STABLE_MS) {
2937
+ return;
2938
+ }
2939
+ } else {
2940
+ runningSince = null;
2941
+ }
2942
+ if (this.#executionState === "stopped" && Date.now() - lastResumeAt >= DISPLAY_RESUME_COOLDOWN_MS) {
2943
+ this.#writeProcessLogLine(`[display] observed stopped state after ${commandName}, sending resume`);
2944
+ this.#lastExecutionIntent = "unknown";
2945
+ await this.#client.continueExecution();
2946
+ lastResumeAt = Date.now();
2947
+ }
2948
+ if (Date.now() >= deadline) {
2949
+ this.#writeProcessLogLine(
2950
+ `[display] settle timeout reached after ${DISPLAY_SETTLE_TIMEOUT_MS}ms after ${commandName} with executionState=${this.#executionState}`
2951
+ );
2952
+ if (this.#executionState !== "running") {
2953
+ emulatorNotRunningError(commandName, {
2954
+ executionState: this.#executionState,
2955
+ lastStopReason: this.#lastStopReason
2956
+ });
2957
+ }
2958
+ return;
2959
+ }
2960
+ await sleep(DISPLAY_SETTLE_POLL_MS);
2961
+ }
2962
+ }
2963
+ async #settleInputState(commandName, previousExecutionState) {
2964
+ if (previousExecutionState !== "running") {
2965
+ return;
2966
+ }
2967
+ const deadline = Date.now() + INPUT_SETTLE_TIMEOUT_MS;
2968
+ let runningSince = null;
2969
+ let lastResumeAt = 0;
2970
+ while (true) {
2971
+ this.#syncMonitorRuntimeState();
2972
+ if (this.#executionState === "running") {
2973
+ runningSince ??= Date.now();
2974
+ if (Date.now() - runningSince >= INPUT_RUNNING_STABLE_MS) {
2975
+ return;
2976
+ }
2977
+ } else {
2978
+ runningSince = null;
2979
+ }
2980
+ if (this.#executionState === "stopped" && Date.now() - lastResumeAt >= INPUT_RESUME_COOLDOWN_MS) {
2981
+ this.#writeProcessLogLine(`[input] observed stopped state after ${commandName}, sending resume`);
2982
+ this.#explicitPauseActive = false;
2983
+ this.#lastExecutionIntent = "unknown";
2984
+ await this.#client.continueExecution();
2985
+ lastResumeAt = Date.now();
2986
+ }
2987
+ if (Date.now() >= deadline) {
2988
+ this.#writeProcessLogLine(
2989
+ `[input] settle timeout reached after ${INPUT_SETTLE_TIMEOUT_MS}ms with executionState=${this.#executionState}`
2990
+ );
2991
+ if (this.#executionState !== "running") {
2992
+ emulatorNotRunningError(commandName, {
2993
+ executionState: this.#executionState,
2994
+ lastStopReason: this.#lastStopReason
2995
+ });
2996
+ }
2997
+ return;
2998
+ }
2999
+ await sleep(INPUT_SETTLE_POLL_MS);
3000
+ }
3001
+ }
3002
+ #autostartWasAcceptedAfterError(error, event, previousExecutionState, previousStopReason) {
3003
+ if (!(error instanceof ViceMcpError)) {
3004
+ return false;
3005
+ }
3006
+ if (!["emulator_protocol_error", "timeout", "connection_closed"].includes(error.code)) {
3007
+ return false;
3008
+ }
3009
+ return this.#executionState !== previousExecutionState && (this.#executionState === "running" || this.#executionState === "stopped") && this.#lastStopReason !== previousStopReason && event != null;
3010
+ }
3011
+ #resumeWasAcceptedAfterError(error, event, previousExecutionState, previousStopReason) {
3012
+ if (!(error instanceof ViceMcpError)) {
3013
+ return false;
3014
+ }
3015
+ if (!["emulator_protocol_error", "timeout", "connection_closed"].includes(error.code)) {
3016
+ return false;
3017
+ }
3018
+ if (event) {
3019
+ this.#writeProcessLogLine(
3020
+ `[bootstrap] resume probe accepted after ${error.code} because monitor reported ${event.type}`
3021
+ );
3022
+ return true;
3023
+ }
3024
+ return this.#executionState !== previousExecutionState || this.#lastStopReason !== previousStopReason;
3025
+ }
3026
+ #syncMonitorRuntimeState() {
3027
+ const runtime = this.#client.runtimeState();
3028
+ const eventType = runtime.lastEventType;
3029
+ this.#executionState = runtime.runtimeKnown && eventType === "resumed" ? "running" : runtime.runtimeKnown ? "stopped" : "unknown";
3030
+ if (eventType !== this.#lastRuntimeEventType || runtime.programCounter !== this.#lastRuntimeProgramCounter) {
3031
+ this.#lastStopReason = this.#stopReasonFromMonitorEvent(eventType);
3032
+ this.#lastRuntimeEventType = eventType;
3033
+ this.#lastRuntimeProgramCounter = runtime.programCounter;
3034
+ }
3035
+ this.#writeProcessLogLine(
3036
+ `[monitor-state] executionState=${this.#executionState} lastStopReason=${this.#lastStopReason} runtimeKnown=${runtime.runtimeKnown} lastEventType=${eventType}${runtime.programCounter == null ? "" : ` pc=$${runtime.programCounter.toString(16).padStart(4, "0")}`}`
3037
+ );
3038
+ this.#scheduleIdleAutoResume();
3039
+ }
3040
+ #stopReasonFromMonitorEvent(eventType) {
3041
+ switch (eventType) {
3042
+ case "resumed":
3043
+ this.#pendingCheckpointHit = null;
3044
+ return "none";
3045
+ case "stopped":
3046
+ if (this.#pendingCheckpointHit && Date.now() - this.#pendingCheckpointHit.observedAt <= CHECKPOINT_HIT_SETTLE_MS) {
3047
+ this.#lastCheckpointHit = this.#pendingCheckpointHit;
3048
+ const checkpointReason = this.#pendingCheckpointHit.kind === "exec" ? "breakpoint" : this.#pendingCheckpointHit.kind === "read" ? "watchpoint_read" : "watchpoint_write";
3049
+ this.#pendingCheckpointHit = null;
3050
+ return checkpointReason;
3051
+ }
3052
+ if (this.#lastExecutionIntent === "unknown" && !this.#checkpointQueryPending) {
3053
+ void this.#scheduleCheckpointHitQuery();
3054
+ }
3055
+ return this.#lastExecutionIntent;
3056
+ case "jam":
3057
+ this.#pendingCheckpointHit = null;
3058
+ return "error";
3059
+ case "unknown":
3060
+ this.#pendingCheckpointHit = null;
3061
+ return "unknown";
3062
+ }
3063
+ }
3064
+ async #scheduleCheckpointHitQuery() {
3065
+ if (this.#checkpointQueryPending) {
3066
+ return;
3067
+ }
3068
+ this.#checkpointQueryPending = true;
3069
+ try {
3070
+ await sleep(200);
3071
+ if (this.#lastStopReason !== "unknown" && this.#lastStopReason !== this.#lastExecutionIntent) {
3072
+ return;
3073
+ }
3074
+ this.#writeProcessLogLine("[checkpoint-query] probing for hit checkpoint after ambiguous stop");
3075
+ const response = await this.#client.listBreakpoints();
3076
+ const hit = response.checkpoints.find((cp) => cp.currentlyHit);
3077
+ if (hit) {
3078
+ this.#lastCheckpointHit = {
3079
+ id: hit.id,
3080
+ kind: hit.kind,
3081
+ observedAt: Date.now()
3082
+ };
3083
+ const reason = hit.kind === "exec" ? "breakpoint" : hit.kind === "read" ? "watchpoint_read" : "watchpoint_write";
3084
+ this.#lastStopReason = reason;
3085
+ this.#writeProcessLogLine(`[checkpoint-query] retroactively identified stop reason: ${reason} (id=${hit.id})`);
3086
+ }
3087
+ } catch (error) {
3088
+ this.#writeProcessLogLine(`[checkpoint-query] failed: ${error instanceof Error ? error.message : String(error)}`);
3089
+ } finally {
3090
+ this.#checkpointQueryPending = false;
3091
+ }
3092
+ }
3093
+ #autoResumeAllowed() {
3094
+ return !this.#shuttingDown && this.#client.connected && !this.#explicitPauseActive;
3095
+ }
3096
+ #scheduleIdleAutoResume() {
3097
+ if (this.#executionState !== "stopped" || !this.#autoResumeAllowed()) {
3098
+ this.#clearIdleAutoResume();
3099
+ return;
3100
+ }
3101
+ this.#stoppedAt = Date.now();
3102
+ if (this.#autoResumeTimer) {
3103
+ clearTimeout(this.#autoResumeTimer);
3104
+ }
3105
+ this.#autoResumeTimer = setTimeout(() => {
3106
+ this.#autoResumeTimer = null;
3107
+ void this.#autoResumeDueToIdle();
3108
+ }, STOPPED_IDLE_TIMEOUT_MS);
3109
+ }
3110
+ #clearIdleAutoResume() {
3111
+ this.#stoppedAt = null;
3112
+ if (this.#autoResumeTimer) {
3113
+ clearTimeout(this.#autoResumeTimer);
3114
+ this.#autoResumeTimer = null;
3115
+ }
3116
+ }
3117
+ async #autoResumeDueToIdle() {
3118
+ if (!this.#autoResumeAllowed() || this.#executionState !== "stopped") {
3119
+ this.#stoppedAt = null;
3120
+ return;
3121
+ }
3122
+ const stoppedMs = this.#stoppedAt ? Date.now() - this.#stoppedAt : STOPPED_IDLE_TIMEOUT_MS;
3123
+ if (stoppedMs < STOPPED_IDLE_TIMEOUT_MS) {
3124
+ this.#scheduleIdleAutoResume();
3125
+ return;
3126
+ }
3127
+ this.#writeProcessLogLine(`[auto-resume] emulator stopped for ${STOPPED_IDLE_TIMEOUT_MS}ms, resuming to stay responsive`);
3128
+ try {
3129
+ this.#lastExecutionIntent = "unknown";
3130
+ await this.#client.continueExecution();
3131
+ } catch (error) {
3132
+ this.#writeProcessLogLine(
3133
+ `[auto-resume] resume attempt failed: ${error instanceof Error ? error.message : String(error)}`
3134
+ );
3135
+ } finally {
3136
+ this.#stoppedAt = null;
3137
+ }
3138
+ this.#syncMonitorRuntimeState();
3139
+ }
3140
+ };
3141
+ function splitCommandLine(input) {
3142
+ const result = [];
3143
+ let current = "";
3144
+ let quote = null;
3145
+ for (const char of input) {
3146
+ if (quote) {
3147
+ if (char === quote) {
3148
+ quote = null;
3149
+ } else {
3150
+ current += char;
3151
+ }
3152
+ continue;
3153
+ }
3154
+ if (char === '"' || char === "'") {
3155
+ quote = char;
3156
+ continue;
3157
+ }
3158
+ if (/\s/.test(char)) {
3159
+ if (current) {
3160
+ result.push(current);
3161
+ current = "";
3162
+ }
3163
+ continue;
3164
+ }
3165
+ current += char;
3166
+ }
3167
+ if (current) {
3168
+ result.push(current);
3169
+ }
3170
+ return result;
3171
+ }
3172
+ async function waitForMonitor(host2, port2, timeoutMs) {
3173
+ const deadline = Date.now() + timeoutMs;
3174
+ while (Date.now() < deadline) {
3175
+ if (!await isPortAvailable(host2, port2)) {
3176
+ return;
3177
+ }
3178
+ await sleep(100);
3179
+ }
3180
+ throw new ViceMcpError("monitor_timeout", `Debugger monitor did not open on ${host2}:${port2}`, "timeout", true, {
3181
+ host: host2,
3182
+ port: port2
3183
+ });
3184
+ }
3185
+ async function waitForProcessExit(process2, timeoutMs) {
3186
+ if (process2.exitCode != null) {
3187
+ return;
3188
+ }
3189
+ await new Promise((resolve, reject) => {
3190
+ const timer = setTimeout(() => reject(new Error("Timed out waiting for process exit")), timeoutMs);
3191
+ process2.once("exit", () => {
3192
+ clearTimeout(timer);
3193
+ resolve();
3194
+ });
3195
+ });
3196
+ }
3197
+
3198
+ // src/server.ts
3199
+ var c64Session = new ViceSession();
3200
+ var noInputSchema = z3.object({}).strict();
3201
+ function createViceTool(options) {
3202
+ return createTool({
3203
+ id: options.id,
3204
+ description: options.description,
3205
+ ...options.inputSchema ? { inputSchema: options.inputSchema } : {},
3206
+ outputSchema: toolOutputSchema(options.dataSchema),
3207
+ mcp: options.mcp,
3208
+ execute: async (input) => {
3209
+ try {
3210
+ const data = await options.execute(input);
3211
+ return {
3212
+ meta: c64Session.takeResponseMeta(),
3213
+ data
3214
+ };
3215
+ } catch (error) {
3216
+ throw normalizeToolError(error);
3217
+ }
3218
+ }
3219
+ });
3220
+ }
3221
+ function normalizeBreakpoint(breakpoint) {
3222
+ return {
3223
+ id: breakpoint.id,
3224
+ address: breakpoint.start,
3225
+ length: breakpoint.end - breakpoint.start + 1,
3226
+ enabled: breakpoint.enabled,
3227
+ temporary: breakpoint.temporary,
3228
+ hasCondition: breakpoint.hasCondition,
3229
+ kind: breakpoint.kind,
3230
+ label: breakpoint.label ?? null
3231
+ };
3232
+ }
3233
+ var getMonitorStateTool = createViceTool({
3234
+ id: "get_monitor_state",
3235
+ description: "Returns whether the C64 is running or stopped, along with the current stop reason and program counter when available.",
3236
+ inputSchema: noInputSchema,
3237
+ dataSchema: monitorStateSchema,
3238
+ execute: async () => await c64Session.getMonitorState()
3239
+ });
3240
+ var getSessionStateTool = createViceTool({
3241
+ id: "get_session_state",
3242
+ description: "Returns richer emulator session state including transport/process status, auto-resume state, and the most recent hit checkpoint when known.",
3243
+ inputSchema: noInputSchema,
3244
+ dataSchema: sessionStateResultSchema,
3245
+ execute: async () => c64Session.snapshot()
3246
+ });
3247
+ var getRegistersTool = createViceTool({
3248
+ id: "get_registers",
3249
+ description: "Returns the current C64 register snapshot. This requires the emulator to already be stopped.",
3250
+ inputSchema: noInputSchema,
3251
+ dataSchema: z3.object({
3252
+ registers: c64RegisterValueSchema
3253
+ }),
3254
+ execute: async () => await c64Session.getRegisters()
3255
+ });
3256
+ var setRegistersTool = createViceTool({
3257
+ id: "set_registers",
3258
+ description: "Sets one or more C64 registers by field name.",
3259
+ inputSchema: z3.object({
3260
+ registers: c64PartialRegisterValueSchema
3261
+ }),
3262
+ dataSchema: z3.object({
3263
+ updated: c64PartialRegisterValueSchema,
3264
+ executionState: executionStateSchema
3265
+ }),
3266
+ execute: async (input) => await c64Session.setRegisters(input.registers)
3267
+ });
3268
+ var readMemoryTool = createViceTool({
3269
+ id: "memory_read",
3270
+ description: "Reads a memory chunk by start address and length and returns raw bytes as a JSON array.",
3271
+ inputSchema: z3.object({
3272
+ address: address16Schema.describe("Start address in the 16-bit C64 address space"),
3273
+ length: z3.number().int().positive().max(65535).describe("Size of the data chunk to read in bytes")
3274
+ }).refine((input) => input.address + input.length <= 65536, {
3275
+ message: "address + length must stay within the 64K address space",
3276
+ path: ["length"]
3277
+ }),
3278
+ dataSchema: z3.object({
3279
+ address: address16Schema.describe("Start address of the returned memory chunk"),
3280
+ length: z3.number().int().min(0).describe("Number of bytes returned"),
3281
+ data: byteArraySchema.describe("Raw bytes returned from memory")
3282
+ }),
3283
+ execute: async (input) => {
3284
+ const result = await c64Session.readMemory(input.address, input.address + input.length - 1);
3285
+ return {
3286
+ address: input.address,
3287
+ length: result.length,
3288
+ data: result.data
3289
+ };
3290
+ }
3291
+ });
3292
+ var writeMemoryTool = createViceTool({
3293
+ id: "memory_write",
3294
+ description: "Writes raw byte values into the active C64 memory space.",
3295
+ inputSchema: z3.object({
3296
+ address: address16Schema.describe("Start address in the 16-bit C64 address space"),
3297
+ data: byteArraySchema.min(1).describe("Raw bytes to write into memory")
3298
+ }).refine((input) => input.address + input.data.length - 1 <= 65535, {
3299
+ message: "address + data.length must stay within the 16-bit address space",
3300
+ path: ["data"]
3301
+ }),
3302
+ dataSchema: z3.object({
3303
+ worked: z3.boolean().describe("Whether the write operation completed successfully"),
3304
+ address: address16Schema.describe("Start address where the bytes were written"),
3305
+ length: z3.number().int().min(1).describe("Number of bytes written")
3306
+ }).extend(debugStateSchema.shape),
3307
+ execute: async (input) => await c64Session.writeMemory(input.address, input.data)
3308
+ });
3309
+ var executeTool = createViceTool({
3310
+ id: "execute",
3311
+ description: "Controls execution with pause, resume, step, step_over, step_out, or reset. Resume can optionally wait until running becomes stable.",
3312
+ inputSchema: z3.object({
3313
+ action: z3.enum(["pause", "resume", "step", "step_over", "step_out", "reset"]),
3314
+ count: z3.number().int().positive().default(1).describe("Instruction count for step and step_over actions"),
3315
+ resetMode: resetModeSchema.default("soft").describe("Reset mode when action is reset"),
3316
+ waitUntilRunningStable: z3.boolean().default(false).describe("When action is resume, wait until running becomes stable before returning")
3317
+ }),
3318
+ dataSchema: debugStateSchema.extend({
3319
+ stepsExecuted: z3.number().int().positive().optional(),
3320
+ warnings: z3.array(warningSchema)
3321
+ }),
3322
+ execute: async (input) => await c64Session.execute(input.action, input.count, input.resetMode, input.waitUntilRunningStable)
3323
+ });
3324
+ var waitForStateTool = createViceTool({
3325
+ id: "wait_for_state",
3326
+ description: "Waits for the emulator to reach a target execution state and optionally remain there for a stability window.",
3327
+ inputSchema: z3.object({
3328
+ executionState: z3.enum(["running", "stopped"]).describe("Target execution state to wait for"),
3329
+ timeoutMs: z3.number().int().positive().default(5e3).describe("Maximum time to wait before returning"),
3330
+ stableMs: z3.number().int().nonnegative().optional().describe("Optional stability window the target state must remain true before returning")
3331
+ }),
3332
+ dataSchema: waitForStateResultSchema,
3333
+ execute: async (input) => await c64Session.waitForState(input.executionState, input.timeoutMs, input.stableMs)
3334
+ });
3335
+ var listBreakpointsTool = createViceTool({
3336
+ id: "list_breakpoints",
3337
+ description: "Lists current breakpoints and watchpoints.",
3338
+ inputSchema: z3.object({
3339
+ includeDisabled: z3.boolean().default(true)
3340
+ }),
3341
+ dataSchema: z3.object({
3342
+ breakpoints: z3.array(breakpointSchema)
3343
+ }),
3344
+ execute: async (input) => {
3345
+ const result = await c64Session.listBreakpoints(input.includeDisabled);
3346
+ return {
3347
+ breakpoints: result.breakpoints.map((breakpoint) => normalizeBreakpoint(breakpoint))
3348
+ };
3349
+ }
3350
+ });
3351
+ var breakpointSetTool = createViceTool({
3352
+ id: "breakpoint_set",
3353
+ description: "Creates an execution breakpoint or read/write watchpoint.",
3354
+ inputSchema: z3.object({
3355
+ kind: breakpointKindSchema,
3356
+ address: address16Schema.describe("Start address of the breakpoint range"),
3357
+ length: z3.number().int().positive().default(1).describe("Size of the breakpoint range in bytes"),
3358
+ condition: z3.string().optional(),
3359
+ label: z3.string().optional(),
3360
+ temporary: z3.boolean().default(false),
3361
+ enabled: z3.boolean().default(true)
3362
+ }),
3363
+ dataSchema: z3.object({
3364
+ breakpoint: breakpointSchema,
3365
+ executionState: executionStateSchema,
3366
+ lastStopReason: stopReasonSchema,
3367
+ programCounter: address16Schema.nullable(),
3368
+ registers: c64PartialRegisterValueSchema.nullable()
3369
+ }),
3370
+ execute: async (input) => {
3371
+ const result = await c64Session.breakpointSet(input);
3372
+ return {
3373
+ breakpoint: normalizeBreakpoint(result.breakpoint),
3374
+ executionState: result.executionState,
3375
+ lastStopReason: result.lastStopReason,
3376
+ programCounter: result.programCounter,
3377
+ registers: result.registers
3378
+ };
3379
+ }
3380
+ });
3381
+ var breakpointClearTool = createViceTool({
3382
+ id: "breakpoint_clear",
3383
+ description: "Deletes a breakpoint by numeric id.",
3384
+ inputSchema: z3.object({
3385
+ breakpointId: z3.number().int().nonnegative()
3386
+ }),
3387
+ dataSchema: z3.object({
3388
+ executionState: executionStateSchema,
3389
+ lastStopReason: stopReasonSchema,
3390
+ programCounter: address16Schema.nullable(),
3391
+ registers: c64PartialRegisterValueSchema.nullable(),
3392
+ cleared: z3.boolean(),
3393
+ breakpointId: z3.number().int()
3394
+ }),
3395
+ execute: async (input) => await c64Session.breakpointClear(input.breakpointId)
3396
+ });
3397
+ var programLoadTool = createViceTool({
3398
+ id: "program_load",
3399
+ description: "Loads a C64 program and optionally starts it.",
3400
+ inputSchema: z3.object({
3401
+ filePath: z3.string(),
3402
+ autoStart: z3.boolean().default(true).describe("Whether the loaded program should be started immediately after loading"),
3403
+ fileIndex: z3.number().int().nonnegative().default(0).describe("Autostart file index inside the image, when applicable")
3404
+ }),
3405
+ dataSchema: programLoadResultSchema,
3406
+ execute: async (input) => await c64Session.programLoad(input)
3407
+ });
3408
+ var captureDisplayTool = createViceTool({
3409
+ id: "capture_display",
3410
+ description: "Captures the current screen to a PNG file and returns the saved image path. If the emulator was running, the server restores running state before returning, subject to timeout.",
3411
+ inputSchema: z3.object({
3412
+ useVic: z3.boolean().default(true).describe("Whether to capture the VIC-II display when supported")
3413
+ }),
3414
+ dataSchema: captureDisplayResultSchema,
3415
+ execute: async (input) => await c64Session.captureDisplay(input.useVic)
3416
+ });
3417
+ var getDisplayStateTool = createViceTool({
3418
+ id: "get_display_state",
3419
+ description: "Returns screen RAM, color RAM, the current graphics mode, screen memory addresses, and the current border and background colors. If the emulator was running, the server restores running state before returning, subject to timeout.",
3420
+ inputSchema: noInputSchema,
3421
+ dataSchema: displayStateResultSchema,
3422
+ execute: async () => await c64Session.getDisplayState()
3423
+ });
3424
+ var getDisplayTextTool = createViceTool({
3425
+ id: "get_display_text",
3426
+ description: "Returns the current text screen as readable text when the C64 is in a text mode. If the emulator was running, the server restores running state before returning, subject to timeout.",
3427
+ inputSchema: noInputSchema,
3428
+ dataSchema: displayTextResultSchema,
3429
+ execute: async () => await c64Session.getDisplayText()
3430
+ });
3431
+ var writeTextTool = createViceTool({
3432
+ id: "write_text",
3433
+ description: "Types text into the C64 while it is running. Supports escaped characters and PETSCII brace tokens like {RETURN}, {CLR}, {HOME}, {PI}, and color names; limit each request to 64 bytes.",
3434
+ inputSchema: z3.object({
3435
+ text: z3.string()
3436
+ }),
3437
+ dataSchema: z3.object({
3438
+ sent: z3.boolean(),
3439
+ length: z3.number().int()
3440
+ }),
3441
+ execute: async (input) => await c64Session.writeText(input.text)
3442
+ });
3443
+ var keyboardInputTool = createViceTool({
3444
+ id: "keyboard_input",
3445
+ description: "Sends one to four keys or PETSCII tokens to the C64 while it is running. Use this for key presses, releases, and taps. If the emulator transiently stops, the server restores running state before returning, subject to timeout.",
3446
+ inputSchema: z3.object({
3447
+ action: inputActionSchema.describe("Use tap for a single key event or press/release for repeated buffered input"),
3448
+ 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"),
3449
+ durationMs: z3.number().int().positive().optional().describe("Tap duration in milliseconds")
3450
+ }),
3451
+ dataSchema: keyboardInputResultSchema,
3452
+ execute: async (input) => await c64Session.keyboardInput(input.action, input.keys, input.durationMs)
3453
+ });
3454
+ var joystickInputTool = createViceTool({
3455
+ id: "joystick_input",
3456
+ description: "Sends joystick input to C64 joystick port 1 or 2 while the C64 is running. If the emulator transiently stops, the server restores running state before returning, subject to timeout.",
3457
+ inputSchema: z3.object({
3458
+ port: joystickPortSchema.describe("Joystick port number"),
3459
+ action: inputActionSchema.describe("Joystick action to apply"),
3460
+ control: joystickControlSchema.describe("Joystick direction or fire control"),
3461
+ durationMs: z3.number().int().optional().describe("Tap duration in milliseconds (will be clamped to reasonable range)")
3462
+ }),
3463
+ dataSchema: joystickInputResultSchema,
3464
+ execute: async (input) => await c64Session.joystickInput(input.port, input.action, input.control, input.durationMs)
3465
+ });
3466
+ var c64DebugServer = new MCPServer({
3467
+ id: "c64-debug-mcp",
3468
+ name: "c64 debugger",
3469
+ version: "0.1.0",
3470
+ description: "MCP server for C64 debugging and interaction.",
3471
+ instructions: "This server is for C64 debugging. Use the tools directly to inspect, control, and interact with the C64.",
3472
+ tools: {
3473
+ get_monitor_state: getMonitorStateTool,
3474
+ get_session_state: getSessionStateTool,
3475
+ get_registers: getRegistersTool,
3476
+ set_registers: setRegistersTool,
3477
+ memory_read: readMemoryTool,
3478
+ memory_write: writeMemoryTool,
3479
+ execute: executeTool,
3480
+ wait_for_state: waitForStateTool,
3481
+ list_breakpoints: listBreakpointsTool,
3482
+ breakpoint_set: breakpointSetTool,
3483
+ breakpoint_clear: breakpointClearTool,
3484
+ program_load: programLoadTool,
3485
+ capture_display: captureDisplayTool,
3486
+ get_display_state: getDisplayStateTool,
3487
+ get_display_text: getDisplayTextTool,
3488
+ write_text: writeTextTool,
3489
+ keyboard_input: keyboardInputTool,
3490
+ joystick_input: joystickInputTool
3491
+ }
3492
+ });
3493
+
3494
+ // src/http.ts
3495
+ var host = process.env.C64_DEBUG_HTTP_HOST?.trim() || "127.0.0.1";
3496
+ var port = Number.parseInt(process.env.C64_DEBUG_HTTP_PORT?.trim() || "39080", 10);
3497
+ var mcpPath = process.env.C64_DEBUG_HTTP_PATH?.trim() || "/mcp";
3498
+ var healthPath = process.env.C64_DEBUG_HTTP_HEALTH_PATH?.trim() || "/healthz";
3499
+ var shuttingDown = false;
3500
+ var httpServer = http.createServer(async (req, res) => {
3501
+ const url = new URL(req.url || "/", `http://${host}:${port}`);
3502
+ if (url.pathname === healthPath) {
3503
+ res.writeHead(200, { "content-type": "application/json" });
3504
+ res.end(JSON.stringify({ ok: true }));
3505
+ return;
3506
+ }
3507
+ if (url.pathname !== mcpPath) {
3508
+ res.writeHead(404, { "content-type": "application/json" });
3509
+ res.end(JSON.stringify({ error: "not_found" }));
3510
+ return;
3511
+ }
3512
+ try {
3513
+ await c64DebugServer.startHTTP({
3514
+ url,
3515
+ httpPath: mcpPath,
3516
+ req,
3517
+ res,
3518
+ options: {
3519
+ enableJsonResponse: true
3520
+ }
3521
+ });
3522
+ } catch (error) {
3523
+ if (!res.headersSent) {
3524
+ res.writeHead(500, { "content-type": "application/json" });
3525
+ res.end(
3526
+ JSON.stringify({
3527
+ error: "mcp_http_start_failed",
3528
+ message: error instanceof Error ? error.message : String(error)
3529
+ })
3530
+ );
3531
+ return;
3532
+ }
3533
+ res.end();
3534
+ }
3535
+ });
3536
+ async function closeHttpServer() {
3537
+ await new Promise((resolve, reject) => {
3538
+ httpServer.close((error) => {
3539
+ if (error) {
3540
+ reject(error);
3541
+ return;
3542
+ }
3543
+ resolve();
3544
+ });
3545
+ });
3546
+ }
3547
+ async function shutdown(exitCode = 0, error) {
3548
+ if (shuttingDown) {
3549
+ return;
3550
+ }
3551
+ shuttingDown = true;
3552
+ if (error) {
3553
+ console.error("C64 Debug MCP HTTP server failed:", error);
3554
+ }
3555
+ try {
3556
+ await closeHttpServer().catch(() => void 0);
3557
+ await c64DebugServer.close().catch(() => void 0);
3558
+ await c64Session.shutdown();
3559
+ } catch (shutdownError) {
3560
+ console.error("C64 Debug MCP HTTP shutdown failed:", shutdownError);
3561
+ exitCode = exitCode === 0 ? 1 : exitCode;
3562
+ } finally {
3563
+ process.exit(exitCode);
3564
+ }
3565
+ }
3566
+ process.once("SIGINT", () => {
3567
+ void shutdown(0);
3568
+ });
3569
+ process.once("SIGTERM", () => {
3570
+ void shutdown(0);
3571
+ });
3572
+ process.once("uncaughtException", (error) => {
3573
+ void shutdown(1, error);
3574
+ });
3575
+ process.once("unhandledRejection", (reason) => {
3576
+ void shutdown(1, reason);
3577
+ });
3578
+ httpServer.listen(port, host, () => {
3579
+ console.error(`C64 Debug MCP HTTP listening on http://${host}:${port}${mcpPath}`);
3580
+ });