c64bridge 0.7.4 → 0.8.1

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.
Files changed (109) hide show
  1. package/.github/prompts/README.md +14 -4
  2. package/.github/prompts/assembly-routine-session.prompt.md +9 -9
  3. package/.github/prompts/basic-program-session.prompt.md +11 -9
  4. package/.github/prompts/drive-management-session.prompt.md +9 -9
  5. package/.github/prompts/hello-world-session.prompt.md +17 -0
  6. package/.github/prompts/petscii-art-session.prompt.md +9 -9
  7. package/.github/prompts/play-classic-game.prompt.md +9 -9
  8. package/.github/prompts/pr-converge.prompt.md +13 -0
  9. package/.github/prompts/sid-music-session.prompt.md +9 -9
  10. package/AGENTS.md +59 -60
  11. package/README.md +550 -191
  12. package/data/assembly/assembly-spec.md +290 -1
  13. package/data/context/bootstrap.md +26 -2
  14. package/data/context/fast-paths.md +44 -0
  15. package/data/graphics/petscii-style-guide.md +6 -2
  16. package/data/graphics/sprite-charset-best-practices.md +5 -5
  17. package/dist/.tsbuildinfo +1 -1
  18. package/dist/c64Client.js +990 -26
  19. package/dist/config.js +5 -1
  20. package/dist/device.js +680 -57
  21. package/dist/diagnostics.js +371 -0
  22. package/dist/mcp/metadata.js +39 -0
  23. package/dist/mcp-server.js +170 -20
  24. package/dist/platform.js +10 -2
  25. package/dist/prompts/registry.js +166 -309
  26. package/dist/rag/init.js +37 -1
  27. package/dist/rag/knowledgeIndex.js +18 -6
  28. package/dist/rag/rateLimiter.js +2 -2
  29. package/dist/streamCapture.js +135 -0
  30. package/dist/tools/audio.js +3 -1
  31. package/dist/tools/debug.js +470 -0
  32. package/dist/tools/developer.js +10 -4
  33. package/dist/tools/graphics.js +183 -17
  34. package/dist/tools/machineControl.js +11 -5
  35. package/dist/tools/memory.js +19 -10
  36. package/dist/tools/meta/audio.js +240 -1
  37. package/dist/tools/meta/config.js +20 -28
  38. package/dist/tools/meta/configInventory.js +104 -0
  39. package/dist/tools/meta/diagnostics.js +45 -2
  40. package/dist/tools/meta/graphics.js +14 -6
  41. package/dist/tools/meta/index.js +1 -0
  42. package/dist/tools/meta/memory.js +20 -6
  43. package/dist/tools/meta/program.js +473 -0
  44. package/dist/tools/pollValidator.js +4 -2
  45. package/dist/tools/programRunners.js +8 -4
  46. package/dist/tools/registry/config.js +17 -0
  47. package/dist/tools/registry/disk.js +7 -0
  48. package/dist/tools/registry/drive.js +3 -0
  49. package/dist/tools/registry/graphics.js +111 -27
  50. package/dist/tools/registry/index.js +6 -0
  51. package/dist/tools/registry/memory.js +1 -0
  52. package/dist/tools/registry/platform.js +121 -0
  53. package/dist/tools/registry/program.js +26 -8
  54. package/dist/tools/registry/sound.js +96 -0
  55. package/dist/tools/registry/system.js +15 -2
  56. package/dist/tools/storage.js +1 -0
  57. package/dist/tools/streaming.js +1 -0
  58. package/dist/tools/types.js +38 -4
  59. package/dist/tools/vice.js +160 -0
  60. package/dist/vicBitmap.js +317 -0
  61. package/dist/vice/mockServer.js +439 -0
  62. package/dist/vice/process.js +310 -0
  63. package/dist/vice/readiness.js +18 -2
  64. package/dist/vice/viceClient.js +382 -22
  65. package/doc/developer.md +71 -25
  66. package/doc/img/backend-switch/hello-c64u.png +0 -0
  67. package/doc/img/backend-switch/hello-vice-and-c64u.png +0 -0
  68. package/doc/img/backend-switch/hello-vice.png +0 -0
  69. package/doc/img/backend-switch/metadata.json +50 -0
  70. package/doc/plans/vice-backend-switch/plan.md +486 -0
  71. package/doc/plans/vice-backend-switch-2/plan.md +48 -0
  72. package/doc/plans/vice-support/notes.md +1 -0
  73. package/doc/plans/vice-support/plan.md +5 -0
  74. package/doc/plans/vice-support/rollout.md +23 -17
  75. package/doc/reviews/vice-support-review.md +185 -0
  76. package/doc/troubleshooting-mcp.md +35 -6
  77. package/doc/vice/support-matrix.md +54 -0
  78. package/mcp/prompts.json +292 -0
  79. package/mcp/protocol-examples.json +46 -0
  80. package/mcp/resources.json +940 -0
  81. package/mcp/schemas/c64_config.schema.json +350 -0
  82. package/mcp/schemas/c64_debug.schema.json +380 -0
  83. package/mcp/schemas/c64_disk.schema.json +278 -0
  84. package/mcp/schemas/c64_drive.schema.json +120 -0
  85. package/mcp/schemas/c64_extract.schema.json +302 -0
  86. package/mcp/schemas/c64_graphics.schema.json +333 -0
  87. package/mcp/schemas/c64_memory.schema.json +155 -0
  88. package/mcp/schemas/c64_printer.schema.json +218 -0
  89. package/mcp/schemas/c64_program.schema.json +391 -0
  90. package/mcp/schemas/c64_rag.schema.json +67 -0
  91. package/mcp/schemas/c64_select_backend.schema.json +32 -0
  92. package/mcp/schemas/c64_sound.schema.json +644 -0
  93. package/mcp/schemas/c64_stream.schema.json +61 -0
  94. package/mcp/schemas/c64_system.schema.json +227 -0
  95. package/mcp/schemas/c64_vice.schema.json +63 -0
  96. package/mcp/server.json +15 -0
  97. package/mcp/tools.json +4517 -0
  98. package/mcp.json +90 -15
  99. package/package.json +11 -4
  100. package/scripts/capture-backend-switch-screenshots.mjs +276 -0
  101. package/scripts/generate-mcp-interface.ts +289 -0
  102. package/scripts/invoke-bun.mjs +67 -21
  103. package/scripts/mockC64Server.mjs +162 -6
  104. package/scripts/prepare-release.mjs +59 -2
  105. package/scripts/run-coverage.mjs +284 -0
  106. package/scripts/run-tests.mjs +95 -39
  107. package/scripts/run-tests.ts +378 -73
  108. package/scripts/update-readme.ts +147 -13
  109. package/server.json +94 -0
@@ -0,0 +1,310 @@
1
+ import { spawn } from "node:child_process";
2
+ import fs from "node:fs";
3
+ import net from "node:net";
4
+ import { createOutputTailCapture, getDiagnosticsSessionInfo, writeDiagnosticEvent } from "../diagnostics.js";
5
+ const DEFAULT_DISPLAY = ":99";
6
+ export function delay(ms) {
7
+ return new Promise((resolve) => setTimeout(resolve, ms));
8
+ }
9
+ function hasGraphicalSession() {
10
+ return configuredDisplay() !== undefined || configuredWaylandDisplay() !== undefined;
11
+ }
12
+ function configuredDisplay() {
13
+ const display = process.env.DISPLAY?.trim();
14
+ return display ? display : undefined;
15
+ }
16
+ function configuredWaylandDisplay() {
17
+ const display = process.env.WAYLAND_DISPLAY?.trim();
18
+ return display ? display : undefined;
19
+ }
20
+ function currentDisplayLabel() {
21
+ return configuredDisplay() ?? configuredWaylandDisplay() ?? DEFAULT_DISPLAY;
22
+ }
23
+ export function shouldUseXvfb(visible) {
24
+ if (process.env.FORCE_XVFB === "1") {
25
+ return { useXvfb: true, display: process.env.VICE_XVFB_DISPLAY ?? DEFAULT_DISPLAY };
26
+ }
27
+ if (visible === false) {
28
+ if (process.env.DISABLE_XVFB === "1") {
29
+ return { useXvfb: false, display: currentDisplayLabel() };
30
+ }
31
+ return { useXvfb: true, display: process.env.VICE_XVFB_DISPLAY ?? DEFAULT_DISPLAY };
32
+ }
33
+ if (hasGraphicalSession()) {
34
+ return { useXvfb: false, display: currentDisplayLabel() };
35
+ }
36
+ if (process.env.DISABLE_XVFB === "1") {
37
+ return { useXvfb: false, display: currentDisplayLabel() };
38
+ }
39
+ const ci = (process.env.CI || "").toLowerCase();
40
+ if (ci === "true" || ci === "1" || ci === "yes") {
41
+ return { useXvfb: true, display: process.env.VICE_XVFB_DISPLAY ?? DEFAULT_DISPLAY };
42
+ }
43
+ return { useXvfb: true, display: process.env.VICE_XVFB_DISPLAY ?? DEFAULT_DISPLAY };
44
+ }
45
+ export async function waitForPort(host, port, timeoutMs = 4000) {
46
+ const start = Date.now();
47
+ while (Date.now() - start < timeoutMs) {
48
+ try {
49
+ await new Promise((resolve, reject) => {
50
+ const socket = net.connect({ host, port }, () => {
51
+ socket.end();
52
+ resolve();
53
+ });
54
+ socket.on("error", reject);
55
+ socket.setTimeout(300, () => {
56
+ socket.destroy(new Error("timeout"));
57
+ });
58
+ });
59
+ return;
60
+ }
61
+ catch {
62
+ await delay(50);
63
+ }
64
+ }
65
+ throw new Error(`Timeout waiting for VICE monitor at ${host}:${port}`);
66
+ }
67
+ async function waitForXvfb(display, timeoutMs = 5000) {
68
+ const match = /^:([0-9]+)/.exec(display.trim());
69
+ if (!match) {
70
+ await delay(500);
71
+ return;
72
+ }
73
+ const socketPath = `/tmp/.X11-unix/X${match[1]}`;
74
+ const start = Date.now();
75
+ while (Date.now() - start < timeoutMs) {
76
+ try {
77
+ if (fs.existsSync(socketPath)) {
78
+ return;
79
+ }
80
+ }
81
+ catch {
82
+ // Ignore transient fs errors while polling for the display socket.
83
+ }
84
+ await delay(50);
85
+ }
86
+ throw new Error(`Timeout waiting for Xvfb display socket ${socketPath}`);
87
+ }
88
+ export function waitForExit(child, timeoutMs) {
89
+ if (child.exitCode !== null || child.signalCode !== null)
90
+ return Promise.resolve();
91
+ return new Promise((resolve) => {
92
+ const timer = setTimeout(() => {
93
+ child.removeListener("exit", onExit);
94
+ resolve();
95
+ }, Math.max(0, timeoutMs));
96
+ const onExit = () => {
97
+ clearTimeout(timer);
98
+ resolve();
99
+ };
100
+ child.once("exit", onExit);
101
+ });
102
+ }
103
+ export async function terminateProcess(child, signal = "SIGTERM", timeoutMs = 1000) {
104
+ if (!child)
105
+ return;
106
+ if (child.exitCode !== null || child.signalCode !== null)
107
+ return;
108
+ try {
109
+ child.kill(signal);
110
+ }
111
+ catch { }
112
+ await waitForExit(child, timeoutMs);
113
+ }
114
+ export async function startViceProcess(options) {
115
+ const debugEnabled = process.env.VICE_DEVICE_TEST_DEBUG === "1";
116
+ const { useXvfb, display } = shouldUseXvfb(options.visible);
117
+ const viceEnv = { ...process.env };
118
+ let xvfb = null;
119
+ const xvfbOutput = createOutputTailCapture("xvfb");
120
+ const viceOutput = createOutputTailCapture("vice");
121
+ writeDiagnosticEvent("vice_process_start_requested", {
122
+ binary: options.binary,
123
+ directory: options.directory,
124
+ display,
125
+ extraArgs: options.extraArgs ?? [],
126
+ host: options.host,
127
+ port: options.port,
128
+ useXvfb,
129
+ visible: options.visible === true,
130
+ warp: options.warp !== false,
131
+ });
132
+ if (useXvfb) {
133
+ ensureXvfbSocketDir(debugEnabled);
134
+ if (debugEnabled) {
135
+ console.error("[vice-process] launching Xvfb", { display });
136
+ }
137
+ xvfb = spawn("Xvfb", [display, "-screen", "0", "640x480x24"], { stdio: ["ignore", "pipe", "pipe"] });
138
+ if (debugEnabled) {
139
+ console.error("[vice-process] Xvfb pid", { pid: xvfb.pid });
140
+ }
141
+ xvfb.stdout?.on("data", (chunk) => {
142
+ xvfbOutput.pushStdout(chunk);
143
+ if (debugEnabled)
144
+ console.error("[vice-process][xvfb stdout]", chunk.toString().trim());
145
+ });
146
+ xvfb.stderr?.on("data", (chunk) => {
147
+ xvfbOutput.pushStderr(chunk);
148
+ if (debugEnabled)
149
+ console.error("[vice-process][xvfb stderr]", chunk.toString().trim());
150
+ });
151
+ xvfb.once("exit", (code, signal) => {
152
+ writeDiagnosticEvent("xvfb_exit", {
153
+ code,
154
+ signal,
155
+ output: xvfbOutput.snapshot(),
156
+ });
157
+ });
158
+ viceEnv.DISPLAY = display;
159
+ await waitForXvfb(display);
160
+ }
161
+ const args = [
162
+ "-binarymonitor",
163
+ "-binarymonitoraddress", `${options.host}:${options.port}`,
164
+ "-sounddev", "dummy",
165
+ "-config", "/dev/null",
166
+ ...(options.directory ? ["-directory", options.directory] : []),
167
+ ...(options.extraArgs ?? []),
168
+ ];
169
+ if (options.warp !== false)
170
+ args.push("-warp");
171
+ if (debugEnabled) {
172
+ console.error("[vice-process] starting VICE", {
173
+ binary: options.binary,
174
+ args,
175
+ display: viceEnv.DISPLAY,
176
+ warp: options.warp !== false,
177
+ });
178
+ }
179
+ const spawnOptions = { stdio: ["ignore", "pipe", "pipe"], env: viceEnv };
180
+ let spawnError = null;
181
+ const child = spawn(options.binary, args, spawnOptions);
182
+ if (debugEnabled) {
183
+ console.error("[vice-process] VICE pid", { pid: child.pid });
184
+ }
185
+ child.stdout?.on("data", (chunk) => {
186
+ viceOutput.pushStdout(chunk);
187
+ if (debugEnabled)
188
+ console.error("[vice-process][vice stdout]", chunk.toString().trim());
189
+ });
190
+ child.stderr?.on("data", (chunk) => {
191
+ viceOutput.pushStderr(chunk);
192
+ if (debugEnabled)
193
+ console.error("[vice-process][vice stderr]", chunk.toString().trim());
194
+ });
195
+ child.once("exit", (code, signal) => {
196
+ writeDiagnosticEvent("vice_process_exit", {
197
+ code,
198
+ signal,
199
+ output: viceOutput.snapshot(),
200
+ });
201
+ });
202
+ child.once("error", (err) => { spawnError = err; });
203
+ try {
204
+ await new Promise((resolve, reject) => {
205
+ const onExit = (code, signal) => {
206
+ child.removeListener("error", onError);
207
+ if (spawnError)
208
+ reject(spawnError);
209
+ else
210
+ reject(new Error(`VICE process exited before monitor became ready (code=${code}, signal=${signal ?? "null"})`));
211
+ };
212
+ const onError = (err) => {
213
+ child.removeListener("exit", onExit);
214
+ reject(err);
215
+ };
216
+ child.once("exit", onExit);
217
+ child.once("error", onError);
218
+ waitForPort(options.host, options.port)
219
+ .then(() => {
220
+ if (debugEnabled) {
221
+ console.error("[vice-process] monitor port is ready", { host: options.host, port: options.port });
222
+ }
223
+ writeDiagnosticEvent("vice_process_monitor_ready", {
224
+ host: options.host,
225
+ port: options.port,
226
+ pid: child.pid,
227
+ });
228
+ child.removeListener("exit", onExit);
229
+ child.removeListener("error", onError);
230
+ resolve();
231
+ })
232
+ .catch((err) => {
233
+ child.removeListener("exit", onExit);
234
+ child.removeListener("error", onError);
235
+ reject(err);
236
+ });
237
+ });
238
+ }
239
+ catch (err) {
240
+ const diagnosticsFile = getDiagnosticsSessionInfo()?.filePath;
241
+ const detail = {
242
+ diagnosticsFile,
243
+ display,
244
+ host: options.host,
245
+ output: {
246
+ vice: viceOutput.snapshot(),
247
+ xvfb: xvfbOutput.snapshot(),
248
+ },
249
+ pid: child.pid,
250
+ useXvfb,
251
+ error: err,
252
+ };
253
+ writeDiagnosticEvent("vice_process_start_failed", detail);
254
+ if (debugEnabled) {
255
+ console.error("[vice-process] failed to start VICE", err instanceof Error ? err : new Error(String(err)));
256
+ }
257
+ await terminateProcess(child, "SIGTERM", 500);
258
+ await terminateProcess(child, "SIGKILL", 200);
259
+ await terminateProcess(xvfb, "SIGTERM", 500);
260
+ await terminateProcess(xvfb, "SIGKILL", 200);
261
+ const stderrTail = viceOutput.snapshot().stderrTail || xvfbOutput.snapshot().stderrTail;
262
+ const suffix = diagnosticsFile
263
+ ? ` Diagnostics: ${diagnosticsFile}`
264
+ : "";
265
+ const error = err instanceof Error ? err : new Error(String(err));
266
+ if (stderrTail) {
267
+ error.message = `${error.message}${suffix} Last stderr: ${stderrTail}`;
268
+ }
269
+ else if (suffix) {
270
+ error.message = `${error.message}${suffix}`;
271
+ }
272
+ throw error;
273
+ }
274
+ const stop = async () => {
275
+ if (debugEnabled) {
276
+ console.error("[vice-process] stopping VICE/xvfb");
277
+ }
278
+ writeDiagnosticEvent("vice_process_stop_requested", {
279
+ pid: child.pid,
280
+ useXvfb,
281
+ });
282
+ await terminateProcess(child, "SIGTERM", 750);
283
+ if (child.exitCode === null && child.signalCode === null) {
284
+ await terminateProcess(child, "SIGKILL", 300);
285
+ }
286
+ await terminateProcess(xvfb, "SIGTERM", 500);
287
+ if (xvfb && xvfb.exitCode === null && xvfb.signalCode === null) {
288
+ await terminateProcess(xvfb, "SIGKILL", 200);
289
+ }
290
+ };
291
+ child.once("exit", async () => {
292
+ await terminateProcess(xvfb, "SIGTERM", 0);
293
+ });
294
+ return { host: options.host, port: options.port, process: child, stop };
295
+ }
296
+ export function ensureXvfbSocketDir(debugEnabled) {
297
+ const socketDir = "/tmp/.X11-unix";
298
+ try {
299
+ if (!fs.existsSync(socketDir)) {
300
+ fs.mkdirSync(socketDir, { mode: 0o1777 });
301
+ }
302
+ fs.chmodSync(socketDir, 0o1777);
303
+ }
304
+ catch (error) {
305
+ writeDiagnosticEvent("xvfb_socket_dir_failed", { socketDir, error });
306
+ if (debugEnabled) {
307
+ console.error("[vice-process] failed to prepare Xvfb socket dir", error);
308
+ }
309
+ }
310
+ }
@@ -1,5 +1,13 @@
1
1
  function nowNs() { return process.hrtime.bigint(); }
2
2
  function msSince(start) { return Number((process.hrtime.bigint() - start) / 1000000n); }
3
+ async function exitMonitorIfNeeded(bm) {
4
+ try {
5
+ await bm.exitMonitor();
6
+ }
7
+ catch {
8
+ // Ignore errors while resuming; monitor may already be running.
9
+ }
10
+ }
3
11
  export function buildReadyPattern() {
4
12
  // Screen-codes for "READY." in power-on uppercase
5
13
  return Buffer.from([0x12, 0x05, 0x01, 0x04, 0x19, 0x2E]);
@@ -14,10 +22,12 @@ export async function waitForScreenPattern(bm, pattern, timeoutMs, tickMs = 50,
14
22
  idx = screen.indexOf(pattern);
15
23
  if (idx >= 0)
16
24
  break;
25
+ await exitMonitorIfNeeded(bm);
17
26
  if (between)
18
27
  await between();
19
28
  await new Promise((r) => setTimeout(r, Math.max(1, tickMs)));
20
29
  }
30
+ await exitMonitorIfNeeded(bm);
21
31
  return idx;
22
32
  }
23
33
  /** Wait until any non-blank (non-0x00/0x20) character appears on the text screen. */
@@ -33,10 +43,12 @@ export async function waitForAnyScreenText(bm, timeoutMs, tickMs = 50, onRead, b
33
43
  return true;
34
44
  }
35
45
  }
46
+ await exitMonitorIfNeeded(bm);
36
47
  if (between)
37
48
  await between();
38
49
  await new Promise((r) => setTimeout(r, Math.max(1, tickMs)));
39
50
  }
51
+ await exitMonitorIfNeeded(bm);
40
52
  return false;
41
53
  }
42
54
  export async function waitForBasicReady(bm, options) {
@@ -57,15 +69,19 @@ export async function waitForBasicReady(bm, options) {
57
69
  pointersOk = true;
58
70
  break;
59
71
  }
72
+ await exitMonitorIfNeeded(bm);
60
73
  await new Promise((r) => setTimeout(r, 50));
61
74
  }
62
75
  if (!pointersOk)
63
76
  return { pointersOk: false, promptOk: false };
64
- if (!options?.ensurePrompt)
77
+ if (!options?.ensurePrompt) {
78
+ await exitMonitorIfNeeded(bm);
65
79
  return { pointersOk: true, promptOk: false };
80
+ }
66
81
  // Coax READY. by hitting RETURN, then poll for READY.
67
82
  await bm.keyboardFeed("\r");
68
- const promptIdx = await waitForScreenPattern(bm, buildReadyPattern(), timeoutMs, 50, options?.onScreenRead);
83
+ await exitMonitorIfNeeded(bm);
84
+ const promptIdx = await waitForScreenPattern(bm, buildReadyPattern(), timeoutMs, 50, options?.onScreenRead, async () => { await exitMonitorIfNeeded(bm); });
69
85
  return { pointersOk: true, promptOk: promptIdx >= 0 };
70
86
  }
71
87
  export function asciiToScreenCodes(text) {