c64bridge 0.5.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.
Files changed (138) hide show
  1. package/.github/prompts/README.md +6 -0
  2. package/.github/prompts/assembly-routine-session.prompt.md +13 -0
  3. package/.github/prompts/basic-program-session.prompt.md +13 -0
  4. package/.github/prompts/drive-management-session.prompt.md +13 -0
  5. package/.github/prompts/petscii-art-session.prompt.md +13 -0
  6. package/.github/prompts/play-classic-game.prompt.md +13 -0
  7. package/.github/prompts/sid-music-session.prompt.md +13 -0
  8. package/AGENTS.md +173 -0
  9. package/LICENSE +339 -0
  10. package/README.md +582 -0
  11. package/data/README.md +16 -0
  12. package/data/api/basic-api-spec.md +154 -0
  13. package/data/api/kernal-api-spec.md +68 -0
  14. package/data/assembly/assembly-spec.md +196 -0
  15. package/data/assembly/examples/effects/sinewave.asm +5 -0
  16. package/data/assembly/examples/graphics/sprite/mover.asm +15 -0
  17. package/data/assembly/examples/sid-file-structure.md +8 -0
  18. package/data/assembly/examples/sid-overview.md +11 -0
  19. package/data/audio/examples/Biosphere.sid +0 -0
  20. package/data/audio/sid-file-structure.md +91 -0
  21. package/data/audio/sid-programming-best-practices.md +198 -0
  22. package/data/audio/sid-spec.md +166 -0
  23. package/data/audio/sidwave.md +131 -0
  24. package/data/audio/sidwave.schema.json +64 -0
  25. package/data/basic/basic-spec.md +230 -0
  26. package/data/basic/examples/audio/alle-meine-entchen-expressive.bas +41 -0
  27. package/data/basic/examples/audio/c-major-scale-pleasant.bas +29 -0
  28. package/data/basic/examples/io/joystick.bas +5 -0
  29. package/data/basic/examples/video/bounce.bas +8 -0
  30. package/data/basic/examples/video/entchen-petscii.bas +16 -0
  31. package/data/basic/examples/video/hello-world.bas +4 -0
  32. package/data/basic/examples/video/wave.bas +7 -0
  33. package/data/context/bootstrap.md +148 -0
  34. package/data/context/chat.md +36 -0
  35. package/data/drive/drive-spec.md +215 -0
  36. package/data/embeddings_asm.json +1 -0
  37. package/data/embeddings_basic.json +1 -0
  38. package/data/embeddings_hardware.json +1 -0
  39. package/data/embeddings_mixed.json +1 -0
  40. package/data/embeddings_other.json +1 -0
  41. package/data/io/cia-spec.md +115 -0
  42. package/data/io/io-spec.md +124 -0
  43. package/data/memory/kernal-memory-map.md +227 -0
  44. package/data/memory/low-memory-map.md +133 -0
  45. package/data/memory/memory-map.md +186 -0
  46. package/data/printer/printer-commodore-bitmap.md +31 -0
  47. package/data/printer/printer-commodore.md +28 -0
  48. package/data/printer/printer-epson-bitmap.md +29 -0
  49. package/data/printer/printer-epson.md +30 -0
  50. package/data/printer/printer-prompts.md +29 -0
  51. package/data/printer/printer-spec.md +180 -0
  52. package/data/video/character-set.csv +257 -0
  53. package/data/video/vic-spec.md +276 -0
  54. package/dist/assemblyConverter.js +1299 -0
  55. package/dist/audio/record_and_analyze_audio.js +292 -0
  56. package/dist/basicConverter.js +193 -0
  57. package/dist/bootstrap/stdio-logger.js +32 -0
  58. package/dist/c64Client.js +1052 -0
  59. package/dist/chargen.js +197 -0
  60. package/dist/config.js +142 -0
  61. package/dist/context.js +157 -0
  62. package/dist/device.js +413 -0
  63. package/dist/index.js +7 -0
  64. package/dist/knowledge.js +454 -0
  65. package/dist/logger.js +182 -0
  66. package/dist/loggingHttpClient.js +88 -0
  67. package/dist/mcp-server.js +426 -0
  68. package/dist/petscii.js +135 -0
  69. package/dist/petsciiArt.js +356 -0
  70. package/dist/platform.js +82 -0
  71. package/dist/prompts/registry.js +560 -0
  72. package/dist/rag/discover.config.json +41 -0
  73. package/dist/rag/discover.js +401 -0
  74. package/dist/rag/embeddings.js +60 -0
  75. package/dist/rag/externalFetcher.js +934 -0
  76. package/dist/rag/indexer.js +770 -0
  77. package/dist/rag/init.js +139 -0
  78. package/dist/rag/knowledgeIndex.js +361 -0
  79. package/dist/rag/loggingRetriever.js +46 -0
  80. package/dist/rag/rateLimiter.js +127 -0
  81. package/dist/rag/retriever.js +74 -0
  82. package/dist/rag/sources.csv +31 -0
  83. package/dist/rag/types.js +5 -0
  84. package/dist/rag/urlUtils.js +38 -0
  85. package/dist/sidplayRunner.js +240 -0
  86. package/dist/sidwave.js +101 -0
  87. package/dist/sidwaveCompiler.js +329 -0
  88. package/dist/tools/audio.js +944 -0
  89. package/dist/tools/developer.js +515 -0
  90. package/dist/tools/errors.js +59 -0
  91. package/dist/tools/graphics.js +469 -0
  92. package/dist/tools/machineControl.js +267 -0
  93. package/dist/tools/memory.js +224 -0
  94. package/dist/tools/printer.js +482 -0
  95. package/dist/tools/programRunners.js +315 -0
  96. package/dist/tools/rag.js +153 -0
  97. package/dist/tools/registry.js +58 -0
  98. package/dist/tools/responses.js +36 -0
  99. package/dist/tools/schema.js +304 -0
  100. package/dist/tools/storage.js +740 -0
  101. package/dist/tools/streaming.js +132 -0
  102. package/dist/tools/types.js +99 -0
  103. package/dist/viceRunner.js +239 -0
  104. package/doc/MCP_SETUP.md +153 -0
  105. package/doc/README.md +7 -0
  106. package/doc/developer.md +312 -0
  107. package/doc/img/logo.png +0 -0
  108. package/doc/img/prompts/duck_petscii.png +0 -0
  109. package/doc/img/prompts/duck_song.png +0 -0
  110. package/doc/img/vscode/vscode-copilot-c64-chat-mode.png +0 -0
  111. package/doc/img/vscode/vscode-copilot-hello-world.png +0 -0
  112. package/doc/prompts/README.md +96 -0
  113. package/doc/prompts/user-prompt-ideas.md +147 -0
  114. package/doc/rest/c64-openapi.yaml +979 -0
  115. package/doc/rest/c64-rest-api.md +106 -0
  116. package/doc/troubleshooting-mcp.md +177 -0
  117. package/generated/c64/index.js +857 -0
  118. package/mcp.json +106 -0
  119. package/package.json +79 -0
  120. package/scripts/c64-cli.mjs +142 -0
  121. package/scripts/c64-tool.sh +83 -0
  122. package/scripts/check-package.mjs +89 -0
  123. package/scripts/cli.js +61 -0
  124. package/scripts/fetch-external-sources.mjs +67 -0
  125. package/scripts/generate-changelog.mjs +131 -0
  126. package/scripts/postbuild.mjs +101 -0
  127. package/scripts/prepare-release.mjs +58 -0
  128. package/scripts/rag-rebuild.mjs +53 -0
  129. package/scripts/register-ts-node.mjs +6 -0
  130. package/scripts/run-mcp-check.sh +104 -0
  131. package/scripts/run-tests.mjs +195 -0
  132. package/scripts/sid2wav.sh +94 -0
  133. package/scripts/smoke-run.sh +65 -0
  134. package/scripts/start-mock-c64.mjs +51 -0
  135. package/scripts/start-mock.mjs +31 -0
  136. package/scripts/start.mjs +72 -0
  137. package/scripts/update-readme.ts +164 -0
  138. package/scripts/verify-package.mjs +147 -0
package/dist/device.js ADDED
@@ -0,0 +1,413 @@
1
+ /*
2
+ * Unified C64 abstraction and backend selection
3
+ */
4
+ import axios from "axios";
5
+ import { Buffer } from "node:buffer";
6
+ import { spawn } from "node:child_process";
7
+ import fs from "node:fs";
8
+ import os from "node:os";
9
+ import path from "node:path";
10
+ import { Api } from "../generated/c64/index.js";
11
+ import { createLoggingHttpClient } from "./loggingHttpClient.js";
12
+ const DEFAULT_C64U_HOST = "c64u";
13
+ const DEFAULT_C64U_PORT = 80;
14
+ function readConfigFile() {
15
+ const envPath = process.env.C64BRIDGE_CONFIG;
16
+ const candidates = [];
17
+ if (envPath)
18
+ candidates.push(envPath);
19
+ // Repo root
20
+ try {
21
+ const here = path.resolve(path.dirname(new URL(import.meta.url).pathname), "..", ".c64bridge.json");
22
+ candidates.push(here);
23
+ }
24
+ catch { }
25
+ const home = process.env.HOME || os.homedir();
26
+ if (home)
27
+ candidates.push(path.join(home, ".c64bridge.json"));
28
+ for (const p of candidates) {
29
+ try {
30
+ if (fs.existsSync(p)) {
31
+ const text = fs.readFileSync(p, "utf8");
32
+ const json = JSON.parse(text);
33
+ return json ?? null;
34
+ }
35
+ }
36
+ catch { }
37
+ }
38
+ return null;
39
+ }
40
+ class C64uBackend {
41
+ constructor(config) {
42
+ this.type = "c64u";
43
+ const baseUrl = resolveBaseUrl(config);
44
+ this.baseUrl = baseUrl;
45
+ const http = createLoggingHttpClient({ baseURL: baseUrl, timeout: 10000 });
46
+ this.api = new Api(http);
47
+ }
48
+ getBaseUrl() { return this.baseUrl; }
49
+ async ping() {
50
+ try {
51
+ const res = await axios.get(this.baseUrl, { timeout: 2000 });
52
+ return res.status >= 200 && res.status < 500;
53
+ }
54
+ catch {
55
+ return false;
56
+ }
57
+ }
58
+ async runPrg(prg) {
59
+ const payload = Buffer.isBuffer(prg) ? prg : Buffer.from(prg);
60
+ const res = await this.api.v1.runnersRunPrgCreate(":run_prg", payload, { headers: { "Content-Type": "application/octet-stream" } });
61
+ return { success: true, details: res.data };
62
+ }
63
+ async loadPrgFile(pathStr) {
64
+ const res = await this.api.v1.runnersLoadPrgUpdate(":load_prg", { file: pathStr });
65
+ return { success: true, details: res.data };
66
+ }
67
+ async runPrgFile(pathStr) {
68
+ const res = await this.api.v1.runnersRunPrgUpdate(":run_prg", { file: pathStr });
69
+ return { success: true, details: res.data };
70
+ }
71
+ async runCrtFile(pathStr) {
72
+ const res = await this.api.v1.runnersRunCrtUpdate(":run_crt", { file: pathStr });
73
+ return { success: true, details: res.data };
74
+ }
75
+ async sidplayFile(pathStr, songnr) {
76
+ const res = await this.api.v1.runnersSidplayUpdate(":sidplay", { file: pathStr, songnr });
77
+ return { success: true, details: res.data };
78
+ }
79
+ async sidplayAttachment(sid, options) {
80
+ const form = { sid: Buffer.isBuffer(sid) ? sid : Buffer.from(sid) };
81
+ if (options?.songlengths)
82
+ form.songlengths = Buffer.isBuffer(options.songlengths) ? options.songlengths : Buffer.from(options.songlengths);
83
+ const res = await this.api.v1.runnersSidplayCreate(":sidplay", form, options?.songnr !== undefined ? { songnr: options.songnr } : undefined);
84
+ return { success: true, details: res.data };
85
+ }
86
+ async readMemory(address, length) {
87
+ const addrStr = address.toString(16).toUpperCase().padStart(4, "0");
88
+ const response = await this.api.v1.machineReadmemList(":readmem", { address: addrStr, length }, { format: "arraybuffer", headers: { Accept: "application/octet-stream, application/json" } });
89
+ const contentType = (response.headers?.["content-type"] ?? "").toString().toLowerCase();
90
+ const body = response.data;
91
+ if (contentType.includes("application/json")) {
92
+ const text = Buffer.from(body).toString("utf8");
93
+ try {
94
+ const parsed = JSON.parse(text);
95
+ return extractBytes(parsed?.data ?? parsed);
96
+ }
97
+ catch {
98
+ return extractBytes(text);
99
+ }
100
+ }
101
+ if (body instanceof ArrayBuffer)
102
+ return new Uint8Array(body);
103
+ return extractBytes(body);
104
+ }
105
+ async writeMemory(address, bytes) {
106
+ const addrStr = address.toString(16).toUpperCase().padStart(4, "0");
107
+ if (bytes.length <= 128) {
108
+ await this.api.v1.machineWritememUpdate(":writemem", { address: addrStr, data: Buffer.from(bytes).toString("hex").toUpperCase() });
109
+ }
110
+ else {
111
+ await this.api.v1.machineWritememCreate(":writemem", { address: addrStr }, Buffer.from(bytes), { headers: { "Content-Type": "application/octet-stream" } });
112
+ }
113
+ }
114
+ async reset() { const res = await this.api.v1.machineResetUpdate(":reset"); return { success: true, details: res.data }; }
115
+ async reboot() { const res = await this.api.v1.machineRebootUpdate(":reboot"); return { success: true, details: res.data }; }
116
+ async pause() { const res = await this.api.v1.machinePauseUpdate(":pause"); return { success: true, details: res.data }; }
117
+ async resume() { const res = await this.api.v1.machineResumeUpdate(":resume"); return { success: true, details: res.data }; }
118
+ async poweroff() { const res = await this.api.v1.machinePoweroffUpdate(":poweroff"); return { success: true, details: res.data }; }
119
+ async menuButton() { const res = await this.api.v1.machineMenuButtonUpdate(":menu_button"); return { success: true, details: res.data }; }
120
+ async debugregRead() { const res = await this.api.v1.machineDebugregList(":debugreg"); return { success: true, value: res.data.value, details: res.data }; }
121
+ async debugregWrite(value) { const res = await this.api.v1.machineDebugregUpdate(":debugreg", { value }); return { success: true, value: res.data.value, details: res.data }; }
122
+ async version() { const res = await this.api.v1.versionList(); return res.data; }
123
+ async info() { const res = await this.api.v1.infoList(); return res.data; }
124
+ async drivesList() { const res = await this.api.v1.drivesList(); return res.data; }
125
+ async driveMount(d, img, options) { const res = await this.api.v1.drivesMountUpdate(d, ":mount", { image: img, type: options?.type, mode: options?.mode }); return { success: true, details: res.data }; }
126
+ async driveRemove(d) { const res = await this.api.v1.drivesRemoveUpdate(d, ":remove"); return { success: true, details: res.data }; }
127
+ async driveReset(d) { const res = await this.api.v1.drivesResetUpdate(d, ":reset"); return { success: true, details: res.data }; }
128
+ async driveOn(d) { const res = await this.api.v1.drivesOnUpdate(d, ":on"); return { success: true, details: res.data }; }
129
+ async driveOff(d) { const res = await this.api.v1.drivesOffUpdate(d, ":off"); return { success: true, details: res.data }; }
130
+ async driveSetMode(d, mode) { const res = await this.api.v1.drivesSetModeUpdate(d, ":set_mode", { mode }); return { success: true, details: res.data }; }
131
+ async driveLoadRom(d, romPath) { const res = await this.api.v1.drivesLoadRomUpdate(d, ":load_rom", { file: romPath }); return { success: true, details: res.data }; }
132
+ async streamStart(s, ip) { const res = await this.api.v1.streamsStartUpdate(s, ":start", { ip }); return { success: true, details: res.data }; }
133
+ async streamStop(s) { const res = await this.api.v1.streamsStopUpdate(s, ":stop"); return { success: true, details: res.data }; }
134
+ async configsList() { const res = await this.api.v1.configsList(); return res.data; }
135
+ async configGet(cat, item) { const res = item ? await this.api.v1.configsDetail2(cat, item) : await this.api.v1.configsDetail(cat); return res.data; }
136
+ async configSet(cat, item, value) { const res = await this.api.v1.configsUpdate(cat, item, { value }); return { success: true, details: res.data }; }
137
+ async configBatchUpdate(payload) { const res = await this.api.v1.configsCreate(payload); return { success: true, details: res.data }; }
138
+ async configLoadFromFlash() { const res = await this.api.v1.configsLoadFromFlashUpdate(":load_from_flash"); return { success: true, details: res.data }; }
139
+ async configSaveToFlash() { const res = await this.api.v1.configsSaveToFlashUpdate(":save_to_flash"); return { success: true, details: res.data }; }
140
+ async configResetToDefault() { const res = await this.api.v1.configsResetToDefaultUpdate(":reset_to_default"); return { success: true, details: res.data }; }
141
+ async filesInfo(p) { const res = await this.api.v1.filesInfoDetail(encodeURIComponent(p), ":info"); return res.data; }
142
+ async filesCreateD64(p, options) { const res = await this.api.v1.filesCreateD64Update(encodeURIComponent(p), ":create_d64", { tracks: options?.tracks, diskname: options?.diskname }); return { success: true, details: res.data }; }
143
+ async filesCreateD71(p, options) { const res = await this.api.v1.filesCreateD71Update(encodeURIComponent(p), ":create_d71", { diskname: options?.diskname }); return { success: true, details: res.data }; }
144
+ async filesCreateD81(p, options) { const res = await this.api.v1.filesCreateD81Update(encodeURIComponent(p), ":create_d81", { diskname: options?.diskname }); return { success: true, details: res.data }; }
145
+ async filesCreateDnp(p, tracks, options) { const res = await this.api.v1.filesCreateDnpUpdate(encodeURIComponent(p), ":create_dnp", { tracks, diskname: options?.diskname }); return { success: true, details: res.data }; }
146
+ async modplayFile(pathStr) { const res = await this.api.v1.runnersModplayUpdate(":modplay", { file: pathStr }); return { success: true, details: res.data }; }
147
+ }
148
+ class ViceBackend {
149
+ constructor(config) {
150
+ this.type = "vice";
151
+ this.exe = config.exe || which("x64sc") || which("x64") || "x64sc";
152
+ }
153
+ async ping() { return Boolean(which(this.exe)); }
154
+ async runPrg(prg) {
155
+ const tmp = writeTempPrg(prg);
156
+ try {
157
+ const args = ["-silent", "-warp", "-autostart", tmp];
158
+ const exitCode = await spawnWithTimeout(this.exe, args, resolveTimeout());
159
+ return { success: exitCode === 0, details: { command: [this.exe, ...args].join(" "), exitCode } };
160
+ }
161
+ finally {
162
+ try {
163
+ fs.unlinkSync(tmp);
164
+ }
165
+ catch { }
166
+ }
167
+ }
168
+ async loadPrgFile(_path) { throw unsupported("loadPrgFile"); }
169
+ async runPrgFile(prgPath) {
170
+ const args = ["-silent", "-warp", "-autostart", prgPath];
171
+ const exitCode = await spawnWithTimeout(this.exe, args, resolveTimeout());
172
+ return { success: exitCode === 0, details: { command: [this.exe, ...args].join(" "), exitCode } };
173
+ }
174
+ async runCrtFile(_path) { throw unsupported("runCrtFile"); }
175
+ async sidplayFile(_p) { throw unsupported("sidplayFile"); }
176
+ async sidplayAttachment(_sid) { throw unsupported("sidplayAttachment"); }
177
+ async readMemory(_a) { throw unsupported("readMemory"); }
178
+ async writeMemory(_a, _b) { throw unsupported("writeMemory"); }
179
+ async reset() { throw unsupported("reset"); }
180
+ async reboot() { throw unsupported("reboot"); }
181
+ async pause() { throw unsupported("pause"); }
182
+ async resume() { throw unsupported("resume"); }
183
+ async poweroff() { throw unsupported("poweroff"); }
184
+ async menuButton() { throw unsupported("menuButton"); }
185
+ async debugregRead() { throw unsupported("debugregRead"); }
186
+ async debugregWrite(_v) { throw unsupported("debugregWrite"); }
187
+ async version() { return { emulator: "vice" }; }
188
+ async info() { return { emulator: "vice", phase: 1 }; }
189
+ async drivesList() { throw unsupported("drivesList"); }
190
+ async driveMount() { throw unsupported("driveMount"); }
191
+ async driveRemove() { throw unsupported("driveRemove"); }
192
+ async driveReset() { throw unsupported("driveReset"); }
193
+ async driveOn() { throw unsupported("driveOn"); }
194
+ async driveOff() { throw unsupported("driveOff"); }
195
+ async driveSetMode() { throw unsupported("driveSetMode"); }
196
+ async driveLoadRom() { throw unsupported("driveLoadRom"); }
197
+ async streamStart() { throw unsupported("streamStart"); }
198
+ async streamStop() { throw unsupported("streamStop"); }
199
+ async configsList() { throw unsupported("configsList"); }
200
+ async configGet() { throw unsupported("configGet"); }
201
+ async configSet() { throw unsupported("configSet"); }
202
+ async configBatchUpdate() { throw unsupported("configBatchUpdate"); }
203
+ async configLoadFromFlash() { throw unsupported("configLoadFromFlash"); }
204
+ async configSaveToFlash() { throw unsupported("configSaveToFlash"); }
205
+ async configResetToDefault() { throw unsupported("configResetToDefault"); }
206
+ async filesInfo() { throw unsupported("filesInfo"); }
207
+ async filesCreateD64() { throw unsupported("filesCreateD64"); }
208
+ async filesCreateD71() { throw unsupported("filesCreateD71"); }
209
+ async filesCreateD81() { throw unsupported("filesCreateD81"); }
210
+ async filesCreateDnp() { throw unsupported("filesCreateDnp"); }
211
+ }
212
+ function unsupported(name) { const err = new Error(`Operation '${name}' is not supported by the VICE backend in phase one`); err.code = "UNSUPPORTED"; return err; }
213
+ function extractBytes(data) {
214
+ if (!data)
215
+ return new Uint8Array();
216
+ if (data instanceof ArrayBuffer)
217
+ return new Uint8Array(data);
218
+ if (Buffer.isBuffer(data))
219
+ return new Uint8Array(data);
220
+ if (data instanceof Uint8Array)
221
+ return data;
222
+ if (typeof data === "string") {
223
+ try {
224
+ return Uint8Array.from(Buffer.from(data, "base64"));
225
+ }
226
+ catch {
227
+ return Uint8Array.from(Buffer.from(data, "hex"));
228
+ }
229
+ }
230
+ if (Array.isArray(data?.data))
231
+ return Uint8Array.from((data.data) ?? []);
232
+ if (Array.isArray(data))
233
+ return Uint8Array.from(data);
234
+ if (typeof data === "object" && data !== null) {
235
+ const maybe = data.data;
236
+ if (typeof maybe === "string")
237
+ return Uint8Array.from(Buffer.from(maybe, "base64"));
238
+ if (Array.isArray(maybe))
239
+ return Uint8Array.from(maybe);
240
+ }
241
+ return new Uint8Array();
242
+ }
243
+ function which(binary) {
244
+ const hasSep = binary.includes("/") || binary.includes("\\");
245
+ if (hasSep) {
246
+ try {
247
+ if (fs.existsSync(binary))
248
+ return binary;
249
+ }
250
+ catch { }
251
+ return null;
252
+ }
253
+ const envPath = process.env.PATH || "";
254
+ for (const dir of envPath.split(path.delimiter)) {
255
+ if (!dir)
256
+ continue;
257
+ const candidate = path.join(dir, binary);
258
+ try {
259
+ if (fs.existsSync(candidate))
260
+ return candidate;
261
+ }
262
+ catch { }
263
+ }
264
+ return null;
265
+ }
266
+ function writeTempPrg(prg) {
267
+ const dir = process.env.TMPDIR || process.env.TEMP || "/tmp";
268
+ const file = path.join(dir, `c64bridge-${Date.now()}-${Math.random().toString(16).slice(2)}.prg`);
269
+ const buf = Buffer.isBuffer(prg) ? prg : Buffer.from(prg);
270
+ fs.writeFileSync(file, buf);
271
+ return file;
272
+ }
273
+ function resolveTimeout() { const n = Number(process.env.VICE_RUN_TIMEOUT_MS); return Number.isFinite(n) && n > 0 ? Math.floor(n) : 10000; }
274
+ async function spawnWithTimeout(file, args, timeoutMs) {
275
+ return await new Promise((resolve) => {
276
+ const child = spawn(file, args, { stdio: ["ignore", "ignore", "pipe"] });
277
+ const timer = setTimeout(() => { try {
278
+ child.kill("SIGKILL");
279
+ }
280
+ catch { } }, Math.max(1000, timeoutMs));
281
+ child.on("close", (code) => { clearTimeout(timer); resolve(code ?? 0); });
282
+ child.on("error", () => { clearTimeout(timer); resolve(127); });
283
+ });
284
+ }
285
+ export async function createFacade(logger, options) {
286
+ // Caller-forced preference: use c64u with provided base URL (used by tests and server wiring)
287
+ if (options?.preferredC64uBaseUrl) {
288
+ const backend = new C64uBackend({ baseUrl: options.preferredC64uBaseUrl });
289
+ logger?.info?.("Active backend: c64u (forced by caller)");
290
+ return { facade: backend, selected: "c64u", reason: "forced by caller", details: { baseUrl: options.preferredC64uBaseUrl } };
291
+ }
292
+ const cfg = readConfigFile();
293
+ const envMode = (process.env.C64_MODE || "").toLowerCase().trim();
294
+ const hasC64u = Boolean(cfg?.c64u);
295
+ const hasVice = Boolean(cfg?.vice);
296
+ if (envMode === "c64u") {
297
+ const backend = new C64uBackend(cfg?.c64u ?? {});
298
+ logger?.info?.("Active backend: c64u (from env override)");
299
+ return { facade: backend, selected: "c64u", reason: "env override", details: { baseUrl: backend.getBaseUrl?.() } };
300
+ }
301
+ if (envMode === "vice") {
302
+ const backend = new ViceBackend(cfg?.vice ?? {});
303
+ logger?.info?.("Active backend: vice (from env override)");
304
+ return { facade: backend, selected: "vice", reason: "env override", details: { exe: (cfg?.vice?.exe || which("x64sc") || "x64sc") } };
305
+ }
306
+ if (hasC64u && !hasVice) {
307
+ const backend = new C64uBackend(cfg.c64u);
308
+ logger?.info?.("Active backend: c64u (from config)");
309
+ return { facade: backend, selected: "c64u", reason: "config only", details: { baseUrl: backend.getBaseUrl?.() } };
310
+ }
311
+ if (!hasC64u && hasVice) {
312
+ const backend = new ViceBackend(cfg.vice);
313
+ logger?.info?.("Active backend: vice (from config)");
314
+ return { facade: backend, selected: "vice", reason: "config only", details: { exe: (cfg.vice.exe || which("x64sc") || "x64sc") } };
315
+ }
316
+ if (hasC64u && hasVice) {
317
+ const backend = new C64uBackend(cfg.c64u);
318
+ logger?.info?.("Active backend: c64u (both defined; default preference)");
319
+ return { facade: backend, selected: "c64u", reason: "both defined (prefer c64u)", details: { baseUrl: backend.getBaseUrl?.() } };
320
+ }
321
+ // No configuration
322
+ const probeBase = resolveBaseUrl({});
323
+ try {
324
+ const res = await axios.get(probeBase, { timeout: 1500 });
325
+ if (res.status >= 200 && res.status < 500) {
326
+ const backend = new C64uBackend({ baseUrl: probeBase });
327
+ logger?.info?.("Active backend: c64u (fallback – hardware reachable)");
328
+ return { facade: backend, selected: "c64u", reason: "fallback (reachable)", details: { baseUrl: probeBase } };
329
+ }
330
+ }
331
+ catch { }
332
+ const backend = new ViceBackend(cfg?.vice ?? {});
333
+ logger?.info?.("Active backend: vice (fallback – hardware unavailable)");
334
+ return { facade: backend, selected: "vice", reason: "fallback (hardware unavailable)", details: { exe: (cfg?.vice?.exe || which("x64sc") || "x64sc") } };
335
+ }
336
+ function resolveBaseUrl(config) {
337
+ const explicit = normaliseBaseUrl(config.baseUrl);
338
+ if (explicit)
339
+ return explicit;
340
+ const hostEntries = [configuredString(config.host), configuredString(config.hostname)];
341
+ for (const entry of hostEntries) {
342
+ if (!entry)
343
+ continue;
344
+ const parsed = parseEndpoint(entry);
345
+ if (parsed.hostname) {
346
+ const port = firstDefined(configuredPort(config.port), parsed.port) ?? DEFAULT_C64U_PORT;
347
+ return buildBaseUrl(parsed.hostname, port);
348
+ }
349
+ }
350
+ return buildBaseUrl(DEFAULT_C64U_HOST, DEFAULT_C64U_PORT);
351
+ }
352
+ function configuredString(value) {
353
+ if (typeof value === "string") {
354
+ const trimmed = value.trim();
355
+ return trimmed ? trimmed : undefined;
356
+ }
357
+ return undefined;
358
+ }
359
+ function configuredPort(value) {
360
+ if (typeof value === "number" && Number.isInteger(value) && value > 0 && value <= 65535) {
361
+ return value;
362
+ }
363
+ if (typeof value === "string") {
364
+ const trimmed = value.trim();
365
+ if (!trimmed)
366
+ return undefined;
367
+ const parsed = Number(trimmed);
368
+ if (Number.isInteger(parsed) && parsed > 0 && parsed <= 65535) {
369
+ return parsed;
370
+ }
371
+ }
372
+ return undefined;
373
+ }
374
+ function normaliseBaseUrl(value) {
375
+ const input = configuredString(value);
376
+ if (!input)
377
+ return undefined;
378
+ if (!/^[a-z][a-z0-9+.-]*:\/\//i.test(input)) {
379
+ return `http://${input}`;
380
+ }
381
+ return stripTrailingSlash(input);
382
+ }
383
+ function parseEndpoint(value) {
384
+ try {
385
+ const hasScheme = /^[a-z][a-z0-9+.-]*:\/\//i.test(value);
386
+ const url = new URL(hasScheme ? value : `http://${value}`);
387
+ const hostname = url.hostname || undefined;
388
+ const port = url.port ? configuredPort(url.port) : undefined;
389
+ return { hostname, port };
390
+ }
391
+ catch {
392
+ return {};
393
+ }
394
+ }
395
+ function buildBaseUrl(host, port) {
396
+ const normalizedPort = Number.isInteger(port) && port > 0 ? port : DEFAULT_C64U_PORT;
397
+ const hostPart = formatHost(host);
398
+ const suffix = normalizedPort === DEFAULT_C64U_PORT ? "" : `:${normalizedPort}`;
399
+ return `http://${hostPart}${suffix}`;
400
+ }
401
+ function formatHost(host) {
402
+ return host.includes(":") && !host.startsWith("[") ? `[${host}]` : host;
403
+ }
404
+ function stripTrailingSlash(input) {
405
+ return input.replace(/\/+$/, "");
406
+ }
407
+ function firstDefined(...values) {
408
+ for (const value of values) {
409
+ if (value !== undefined && value !== null)
410
+ return value;
411
+ }
412
+ return undefined;
413
+ }
package/dist/index.js ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * MCP server entry point used by the CLI launcher and npm package main field.
4
+ * This thin wrapper delegates to the real implementation in mcp-server.ts.
5
+ */
6
+ import "./bootstrap/stdio-logger.js";
7
+ import "./mcp-server.js";