c64bridge 1.0.0 → 1.0.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.
- package/README.md +5 -5
- package/dist/.tsbuildinfo +1 -1
- package/dist/c64Client.js +151 -37
- package/dist/config.js +18 -10
- package/dist/device.js +123 -87
- package/dist/knowledge.js +1 -1
- package/dist/loggingHttpClient.js +15 -3
- package/dist/mcp-server.js +56 -15
- package/dist/rag/indexer.js +11 -8
- package/dist/rag/init.js +11 -8
- package/dist/sidwaveCompiler.js +2 -2
- package/dist/tools/audio.js +26 -5
- package/dist/tools/input.js +9 -14
- package/dist/tools/memory.js +20 -5
- package/dist/tools/meta/artifacts.js +20 -8
- package/dist/tools/meta/background.js +11 -1
- package/dist/tools/pollValidator.js +19 -51
- package/dist/tools/printer.js +1 -0
- package/dist/tools/registry/platform.js +2 -0
- package/dist/tools/registry/printer.js +1 -0
- package/dist/vice/mockServer.js +33 -0
- package/dist/vice/process.js +11 -1
- package/dist/vice/viceClient.js +35 -6
- package/doc/reviews/hardening/01-fable/prompt-handover.md +125 -0
- package/doc/reviews/hardening/01-fable/prompt.md +98 -0
- package/doc/reviews/hardening/01-fable/review-prompt.md +468 -0
- package/doc/reviews/hardening/01-fable/review.md +1234 -0
- package/mcp/schemas/c64_input.schema.json +5 -5
- package/mcp/server.json +1 -1
- package/mcp/tools.json +7 -6
- package/mcp.json +1 -1
- package/package.json +1 -1
- package/scripts/mockC64Server.mjs +131 -2
- package/server.json +2 -2
package/dist/device.js
CHANGED
|
@@ -4,13 +4,13 @@
|
|
|
4
4
|
import axios from "axios";
|
|
5
5
|
import { Buffer } from "node:buffer";
|
|
6
6
|
import fs from "node:fs";
|
|
7
|
-
import os from "node:os";
|
|
8
7
|
import path from "node:path";
|
|
9
8
|
import { Api } from "../generated/c64/index.js";
|
|
10
9
|
import { createLoggingHttpClient } from "./loggingHttpClient.js";
|
|
11
10
|
import { ViceClient } from "./vice/viceClient.js";
|
|
12
11
|
import { waitForBasicReady } from "./vice/readiness.js";
|
|
13
12
|
import { startViceProcess } from "./vice/process.js";
|
|
13
|
+
import { loadConfig } from "./config.js";
|
|
14
14
|
const DEFAULT_C64U_HOST = "c64u";
|
|
15
15
|
const DEFAULT_C64U_PORT = 80;
|
|
16
16
|
const DEFAULT_VICE_HOST = "127.0.0.1";
|
|
@@ -53,44 +53,12 @@ function coalesceMemoryWriteBlocks(blocks) {
|
|
|
53
53
|
return merged;
|
|
54
54
|
}
|
|
55
55
|
function readConfigFile() {
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
const here = path.resolve(path.dirname(new URL(import.meta.url).pathname), "..", ".c64bridge.json");
|
|
63
|
-
candidates.push(here);
|
|
64
|
-
}
|
|
65
|
-
catch { }
|
|
66
|
-
const home = process.env.HOME || os.homedir();
|
|
67
|
-
if (home)
|
|
68
|
-
candidates.push(path.join(home, ".c64bridge.json"));
|
|
69
|
-
let foundCandidate = false;
|
|
70
|
-
const merged = {};
|
|
71
|
-
for (const p of candidates) {
|
|
72
|
-
try {
|
|
73
|
-
if (fs.existsSync(p)) {
|
|
74
|
-
foundCandidate = true;
|
|
75
|
-
const text = fs.readFileSync(p, "utf8");
|
|
76
|
-
const json = JSON.parse(text);
|
|
77
|
-
if (!merged.c64u && json?.c64u && typeof json.c64u === "object" && !Array.isArray(json.c64u)) {
|
|
78
|
-
merged.c64u = json.c64u;
|
|
79
|
-
}
|
|
80
|
-
if (!merged.u2 && json?.u2 && typeof json.u2 === "object" && !Array.isArray(json.u2)) {
|
|
81
|
-
merged.u2 = json.u2;
|
|
82
|
-
}
|
|
83
|
-
if (!merged.vice && json?.vice && typeof json.vice === "object" && !Array.isArray(json.vice)) {
|
|
84
|
-
merged.vice = json.vice;
|
|
85
|
-
}
|
|
86
|
-
if ((merged.c64u || merged.u2) && merged.vice) {
|
|
87
|
-
break;
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
catch { }
|
|
92
|
-
}
|
|
93
|
-
return foundCandidate ? merged : null;
|
|
56
|
+
const sections = loadConfig().backendConfig;
|
|
57
|
+
return Object.keys(sections).length === 0 ? null : {
|
|
58
|
+
c64u: sections.c64u,
|
|
59
|
+
u2: sections.u2,
|
|
60
|
+
vice: sections.vice,
|
|
61
|
+
};
|
|
94
62
|
}
|
|
95
63
|
class C64uBackend {
|
|
96
64
|
constructor(config, type = "c64u") {
|
|
@@ -117,6 +85,14 @@ class C64uBackend {
|
|
|
117
85
|
}
|
|
118
86
|
}
|
|
119
87
|
getBaseUrl() { return this.baseUrl; }
|
|
88
|
+
actionResult(data) {
|
|
89
|
+
const errors = data && typeof data === "object" && Array.isArray(data.errors)
|
|
90
|
+
? data.errors.filter((error) => error !== undefined && error !== null && String(error).trim() !== "")
|
|
91
|
+
: [];
|
|
92
|
+
return errors.length > 0
|
|
93
|
+
? { success: false, details: { response: data, errors: errors.map(String) } }
|
|
94
|
+
: { success: true, details: data };
|
|
95
|
+
}
|
|
120
96
|
async ping() {
|
|
121
97
|
try {
|
|
122
98
|
const res = await axios.get(this.baseUrl, {
|
|
@@ -132,30 +108,35 @@ class C64uBackend {
|
|
|
132
108
|
async runPrg(prg) {
|
|
133
109
|
const payload = Buffer.isBuffer(prg) ? prg : Buffer.from(prg);
|
|
134
110
|
const res = await this.api.v1.runnersRunPrgCreate(":run_prg", payload, { headers: { "Content-Type": "application/octet-stream" } });
|
|
135
|
-
return
|
|
111
|
+
return this.actionResult(res.data);
|
|
112
|
+
}
|
|
113
|
+
async loadPrg(prg) {
|
|
114
|
+
const payload = Buffer.isBuffer(prg) ? prg : Buffer.from(prg);
|
|
115
|
+
const res = await this.api.v1.runnersLoadPrgCreate(":load_prg", payload, { headers: { "Content-Type": "application/octet-stream" } });
|
|
116
|
+
return this.actionResult(res.data);
|
|
136
117
|
}
|
|
137
118
|
async loadPrgFile(pathStr) {
|
|
138
119
|
const res = await this.api.v1.runnersLoadPrgUpdate(":load_prg", { file: pathStr });
|
|
139
|
-
return
|
|
120
|
+
return this.actionResult(res.data);
|
|
140
121
|
}
|
|
141
122
|
async runPrgFile(pathStr) {
|
|
142
123
|
const res = await this.api.v1.runnersRunPrgUpdate(":run_prg", { file: pathStr });
|
|
143
|
-
return
|
|
124
|
+
return this.actionResult(res.data);
|
|
144
125
|
}
|
|
145
126
|
async runCrtFile(pathStr) {
|
|
146
127
|
const res = await this.api.v1.runnersRunCrtUpdate(":run_crt", { file: pathStr });
|
|
147
|
-
return
|
|
128
|
+
return this.actionResult(res.data);
|
|
148
129
|
}
|
|
149
130
|
async sidplayFile(pathStr, songnr) {
|
|
150
131
|
const res = await this.api.v1.runnersSidplayUpdate(":sidplay", { file: pathStr, songnr });
|
|
151
|
-
return
|
|
132
|
+
return this.actionResult(res.data);
|
|
152
133
|
}
|
|
153
134
|
async sidplayAttachment(sid, options) {
|
|
154
135
|
const form = { sid: Buffer.isBuffer(sid) ? sid : Buffer.from(sid) };
|
|
155
136
|
if (options?.songlengths)
|
|
156
137
|
form.songlengths = Buffer.isBuffer(options.songlengths) ? options.songlengths : Buffer.from(options.songlengths);
|
|
157
138
|
const res = await this.api.v1.runnersSidplayCreate(":sidplay", form, options?.songnr !== undefined ? { songnr: options.songnr } : undefined);
|
|
158
|
-
return
|
|
139
|
+
return this.actionResult(res.data);
|
|
159
140
|
}
|
|
160
141
|
async readMemory(address, length) {
|
|
161
142
|
const addrStr = address.toString(16).toUpperCase().padStart(4, "0");
|
|
@@ -189,13 +170,13 @@ class C64uBackend {
|
|
|
189
170
|
const mergedBlocks = coalesceMemoryWriteBlocks(blocks);
|
|
190
171
|
await Promise.all(mergedBlocks.map(({ address, bytes }) => this.writeMemory(address, bytes)));
|
|
191
172
|
}
|
|
192
|
-
async reset() { const res = await this.api.v1.machineResetUpdate(":reset"); return
|
|
193
|
-
async reboot() { const res = await this.api.v1.machineRebootUpdate(":reboot"); return
|
|
194
|
-
async pause() { const res = await this.api.v1.machinePauseUpdate(":pause"); return
|
|
195
|
-
async resume() { const res = await this.api.v1.machineResumeUpdate(":resume"); return
|
|
196
|
-
async poweroff() { this.requireC64u("poweroff"); const res = await this.api.v1.machinePoweroffUpdate(":poweroff"); return
|
|
173
|
+
async reset() { const res = await this.api.v1.machineResetUpdate(":reset"); return this.actionResult(res.data); }
|
|
174
|
+
async reboot() { const res = await this.api.v1.machineRebootUpdate(":reboot"); return this.actionResult(res.data); }
|
|
175
|
+
async pause() { const res = await this.api.v1.machinePauseUpdate(":pause"); return this.actionResult(res.data); }
|
|
176
|
+
async resume() { const res = await this.api.v1.machineResumeUpdate(":resume"); return this.actionResult(res.data); }
|
|
177
|
+
async poweroff() { this.requireC64u("poweroff"); const res = await this.api.v1.machinePoweroffUpdate(":poweroff"); return this.actionResult(res.data); }
|
|
197
178
|
async powerCycle() { this.requireC64u("powerCycle"); throw unsupported("powerCycle must be coordinated by C64Client"); }
|
|
198
|
-
async menuButton() { const res = await this.api.v1.machineMenuButtonUpdate(":menu_button"); return
|
|
179
|
+
async menuButton() { const res = await this.api.v1.machineMenuButtonUpdate(":menu_button"); return this.actionResult(res.data); }
|
|
199
180
|
async readMenuScreen() {
|
|
200
181
|
const response = await this.api.v1.machineMenuScreenList(":menu_screen", { format: "arraybuffer", headers: { Accept: "application/octet-stream" } });
|
|
201
182
|
const body = response.data;
|
|
@@ -215,29 +196,42 @@ class C64uBackend {
|
|
|
215
196
|
async debugregWrite(value) { this.requireC64u("debugregWrite"); const res = await this.api.v1.machineDebugregUpdate(":debugreg", { value }); return { success: true, value: res.data.value, details: res.data }; }
|
|
216
197
|
async version() { const res = await this.api.v1.versionList(); return res.data; }
|
|
217
198
|
async info() { const res = await this.api.v1.infoList(); return res.data; }
|
|
218
|
-
async drivesList() {
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
199
|
+
async drivesList() {
|
|
200
|
+
const res = await this.api.v1.drivesList();
|
|
201
|
+
const raw = res.data;
|
|
202
|
+
const entries = Array.isArray(raw.drives)
|
|
203
|
+
? raw.drives
|
|
204
|
+
: raw.drives && typeof raw.drives === "object" ? [raw.drives] : [];
|
|
205
|
+
return entries.flatMap((entry) => Object.entries(entry).map(([id, info]) => ({
|
|
206
|
+
id,
|
|
207
|
+
power: info.enabled === true || info.enabled === "on" || info.power === "on" ? "on" : "off",
|
|
208
|
+
image: (info.image_path ?? info.image_file ?? info.image ?? null),
|
|
209
|
+
type: info.type ?? null,
|
|
210
|
+
raw: info,
|
|
211
|
+
})));
|
|
212
|
+
}
|
|
213
|
+
async driveMount(d, img, options) { const res = await this.api.v1.drivesMountUpdate(d, ":mount", { image: img, type: options?.type, mode: options?.mode }); return this.actionResult(res.data); }
|
|
214
|
+
async driveRemove(d) { const res = await this.api.v1.drivesRemoveUpdate(d, ":remove"); return this.actionResult(res.data); }
|
|
215
|
+
async driveReset(d) { const res = await this.api.v1.drivesResetUpdate(d, ":reset"); return this.actionResult(res.data); }
|
|
216
|
+
async driveOn(d) { const res = await this.api.v1.drivesOnUpdate(d, ":on"); return this.actionResult(res.data); }
|
|
217
|
+
async driveOff(d) { const res = await this.api.v1.drivesOffUpdate(d, ":off"); return this.actionResult(res.data); }
|
|
218
|
+
async driveSetMode(d, mode) { const res = await this.api.v1.drivesSetModeUpdate(d, ":set_mode", { mode }); return this.actionResult(res.data); }
|
|
219
|
+
async driveLoadRom(d, romPath) { const res = await this.api.v1.drivesLoadRomUpdate(d, ":load_rom", { file: romPath }); return this.actionResult(res.data); }
|
|
220
|
+
async streamStart(s, ip) { this.requireC64u("streamStart"); const res = await this.api.v1.streamsStartUpdate(s, ":start", { ip }); return this.actionResult(res.data); }
|
|
221
|
+
async streamStop(s) { this.requireC64u("streamStop"); const res = await this.api.v1.streamsStopUpdate(s, ":stop"); return this.actionResult(res.data); }
|
|
228
222
|
async configsList() { const res = await this.api.v1.configsList(); return res.data; }
|
|
229
223
|
async configGet(cat, item) { const res = item ? await this.api.v1.configsDetail2(cat, item) : await this.api.v1.configsDetail(cat); return res.data; }
|
|
230
|
-
async configSet(cat, item, value) { const res = await this.api.v1.configsUpdate(cat, item, { value }); return
|
|
231
|
-
async configBatchUpdate(payload) { const res = await this.api.v1.configsCreate(payload); return
|
|
232
|
-
async configLoadFromFlash() { const res = await this.api.v1.configsLoadFromFlashUpdate(":load_from_flash"); return
|
|
233
|
-
async configSaveToFlash() { const res = await this.api.v1.configsSaveToFlashUpdate(":save_to_flash"); return
|
|
234
|
-
async configResetToDefault() { const res = await this.api.v1.configsResetToDefaultUpdate(":reset_to_default"); return
|
|
235
|
-
async filesInfo(p) { const res = await this.api.v1.filesInfoDetail(
|
|
236
|
-
async filesCreateD64(p, options) { const res = await this.api.v1.filesCreateD64Update(
|
|
237
|
-
async filesCreateD71(p, options) { const res = await this.api.v1.filesCreateD71Update(
|
|
238
|
-
async filesCreateD81(p, options) { const res = await this.api.v1.filesCreateD81Update(
|
|
239
|
-
async filesCreateDnp(p, tracks, options) { const res = await this.api.v1.filesCreateDnpUpdate(
|
|
240
|
-
async modplayFile(pathStr) { const res = await this.api.v1.runnersModplayUpdate(":modplay", { file: pathStr }); return
|
|
224
|
+
async configSet(cat, item, value) { const res = await this.api.v1.configsUpdate(cat, item, { value }); return this.actionResult(res.data); }
|
|
225
|
+
async configBatchUpdate(payload) { const res = await this.api.v1.configsCreate(payload); return this.actionResult(res.data); }
|
|
226
|
+
async configLoadFromFlash() { const res = await this.api.v1.configsLoadFromFlashUpdate(":load_from_flash"); return this.actionResult(res.data); }
|
|
227
|
+
async configSaveToFlash() { const res = await this.api.v1.configsSaveToFlashUpdate(":save_to_flash"); return this.actionResult(res.data); }
|
|
228
|
+
async configResetToDefault() { const res = await this.api.v1.configsResetToDefaultUpdate(":reset_to_default"); return this.actionResult(res.data); }
|
|
229
|
+
async filesInfo(p) { const res = await this.api.v1.filesInfoDetail(encodeDevicePath(p), ":info"); return res.data; }
|
|
230
|
+
async filesCreateD64(p, options) { const res = await this.api.v1.filesCreateD64Update(encodeDevicePath(p), ":create_d64", { tracks: options?.tracks, diskname: options?.diskname }); return this.actionResult(res.data); }
|
|
231
|
+
async filesCreateD71(p, options) { const res = await this.api.v1.filesCreateD71Update(encodeDevicePath(p), ":create_d71", { diskname: options?.diskname }); return this.actionResult(res.data); }
|
|
232
|
+
async filesCreateD81(p, options) { const res = await this.api.v1.filesCreateD81Update(encodeDevicePath(p), ":create_d81", { diskname: options?.diskname }); return this.actionResult(res.data); }
|
|
233
|
+
async filesCreateDnp(p, tracks, options) { const res = await this.api.v1.filesCreateDnpUpdate(encodeDevicePath(p), ":create_dnp", { tracks, diskname: options?.diskname }); return this.actionResult(res.data); }
|
|
234
|
+
async modplayFile(pathStr) { const res = await this.api.v1.runnersModplayUpdate(":modplay", { file: pathStr }); return this.actionResult(res.data); }
|
|
241
235
|
}
|
|
242
236
|
export class ViceBackend {
|
|
243
237
|
constructor(config) {
|
|
@@ -402,13 +396,19 @@ export class ViceBackend {
|
|
|
402
396
|
return;
|
|
403
397
|
}
|
|
404
398
|
ViceBackend.cleanupRegistered = true;
|
|
405
|
-
|
|
406
|
-
process.once("exit", () => {
|
|
399
|
+
const cleanupSync = () => {
|
|
407
400
|
for (const [, handle] of ViceBackend.supervisors) {
|
|
408
|
-
handle.
|
|
401
|
+
handle.stopSync();
|
|
409
402
|
}
|
|
410
403
|
ViceBackend.supervisors.clear();
|
|
411
|
-
}
|
|
404
|
+
};
|
|
405
|
+
process.once("exit", cleanupSync);
|
|
406
|
+
for (const signal of ["SIGINT", "SIGTERM", "SIGHUP"]) {
|
|
407
|
+
process.once(signal, () => {
|
|
408
|
+
cleanupSync();
|
|
409
|
+
process.exit(signal === "SIGINT" ? 130 : 143);
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
412
|
}
|
|
413
413
|
async withClient(fn, options) {
|
|
414
414
|
const previous = this.monitorQueue;
|
|
@@ -472,7 +472,15 @@ export class ViceBackend {
|
|
|
472
472
|
}
|
|
473
473
|
return false;
|
|
474
474
|
}
|
|
475
|
-
|
|
475
|
+
/**
|
|
476
|
+
* Reset, wait for a usable BASIC prompt, and load PRG bytes into memory.
|
|
477
|
+
* `finish` decides what happens next: a genuine BASIC PRG needs its
|
|
478
|
+
* pointers set and RUN typed, but machine code must never receive either
|
|
479
|
+
* — RUN would misinterpret raw opcodes as tokenized BASIC text, and
|
|
480
|
+
* rewriting TXTTAB/VARTAB/ARYTAB/STREND corrupts the BASIC workspace for
|
|
481
|
+
* an origin that isn't BASIC program text at all.
|
|
482
|
+
*/
|
|
483
|
+
async loadPrgAndFinish(buffer, finish) {
|
|
476
484
|
if (buffer.length < 2)
|
|
477
485
|
throw new Error("PRG data too short");
|
|
478
486
|
const loadAddress = buffer.readUInt16LE(0);
|
|
@@ -482,7 +490,12 @@ export class ViceBackend {
|
|
|
482
490
|
await waitForBasicReady(client, { timeoutMs: 10000, ensurePrompt: true });
|
|
483
491
|
if (body.length > 0)
|
|
484
492
|
await client.memSet(loadAddress, body);
|
|
485
|
-
|
|
493
|
+
await finish(client, loadAddress, loadAddress + body.length);
|
|
494
|
+
await client.exitMonitor();
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
async injectPrg(buffer) {
|
|
498
|
+
await this.loadPrgAndFinish(buffer, async (client, loadAddress, programEnd) => {
|
|
486
499
|
const ptrs = Buffer.alloc(8);
|
|
487
500
|
ptrs.writeUInt16LE(loadAddress, 0);
|
|
488
501
|
ptrs.writeUInt16LE(programEnd, 2);
|
|
@@ -490,7 +503,6 @@ export class ViceBackend {
|
|
|
490
503
|
ptrs.writeUInt16LE(programEnd, 6);
|
|
491
504
|
await client.memSet(0x002B, ptrs);
|
|
492
505
|
await client.keyboardFeed("RUN\r");
|
|
493
|
-
await client.exitMonitor();
|
|
494
506
|
});
|
|
495
507
|
}
|
|
496
508
|
async runPrg(prg) {
|
|
@@ -498,6 +510,13 @@ export class ViceBackend {
|
|
|
498
510
|
await this.injectPrg(buffer);
|
|
499
511
|
return { success: true };
|
|
500
512
|
}
|
|
513
|
+
/** Load PRG bytes without starting them — the caller decides how to enter
|
|
514
|
+
* the code (e.g. a subsequent typed `SYS <entry>` for machine code). */
|
|
515
|
+
async loadPrg(prg) {
|
|
516
|
+
const buffer = Buffer.isBuffer(prg) ? prg : Buffer.from(prg);
|
|
517
|
+
await this.loadPrgAndFinish(buffer, async () => { });
|
|
518
|
+
return { success: true };
|
|
519
|
+
}
|
|
501
520
|
async loadPrgFile(_path) { throw unsupported("loadPrgFile"); }
|
|
502
521
|
async runPrgFile(prgPath) {
|
|
503
522
|
const data = fs.readFileSync(prgPath);
|
|
@@ -540,22 +559,29 @@ export class ViceBackend {
|
|
|
540
559
|
});
|
|
541
560
|
}
|
|
542
561
|
async reset() {
|
|
562
|
+
let readiness;
|
|
563
|
+
// Overridable only for deterministic tests of the readiness-failure path;
|
|
564
|
+
// production always uses the 20s default.
|
|
565
|
+
const resetReadinessTimeoutMs = Number(process.env.C64BRIDGE_VICE_RESET_TIMEOUT_MS) || 20000;
|
|
543
566
|
await this.withClient(async (client) => {
|
|
544
567
|
await client.reset();
|
|
545
568
|
const opts = this.debugEnabled
|
|
546
569
|
? {
|
|
547
|
-
timeoutMs:
|
|
570
|
+
timeoutMs: resetReadinessTimeoutMs,
|
|
548
571
|
ensurePrompt: true,
|
|
549
572
|
onPointersSample: (p) => {
|
|
550
573
|
console.error("[vice-backend] BASIC pointers", p);
|
|
551
574
|
},
|
|
552
575
|
}
|
|
553
|
-
: { timeoutMs:
|
|
554
|
-
|
|
576
|
+
: { timeoutMs: resetReadinessTimeoutMs, ensurePrompt: true };
|
|
577
|
+
readiness = await waitForBasicReady(client, opts);
|
|
555
578
|
if (this.debugEnabled)
|
|
556
579
|
console.error("[vice-backend] waitForBasicReady result", readiness);
|
|
557
580
|
});
|
|
558
|
-
|
|
581
|
+
const ready = Boolean(readiness?.pointersOk && readiness?.promptOk);
|
|
582
|
+
return ready
|
|
583
|
+
? { success: true, details: readiness }
|
|
584
|
+
: { success: false, details: { readiness, message: "VICE reset completed but BASIC READY was not observed. Reset again or inspect the emulator monitor." } };
|
|
559
585
|
}
|
|
560
586
|
async reboot() { return this.reset(); }
|
|
561
587
|
async pause() {
|
|
@@ -742,9 +768,12 @@ export class ViceBackend {
|
|
|
742
768
|
});
|
|
743
769
|
}
|
|
744
770
|
async configSet(_category, item, value) {
|
|
745
|
-
|
|
746
|
-
const parsed = !isNaN(numValue) && value.trim() !== "" ? numValue : value;
|
|
771
|
+
let parsed = value;
|
|
747
772
|
await this.withClient(async (client) => {
|
|
773
|
+
const current = await client.resourceGet(item);
|
|
774
|
+
parsed = current.type === "int" ? Number(value) : value;
|
|
775
|
+
if (current.type === "int" && !Number.isFinite(parsed))
|
|
776
|
+
throw new Error(`VICE resource '${item}' requires an integer value`);
|
|
748
777
|
await client.resourceSet(item, parsed);
|
|
749
778
|
});
|
|
750
779
|
return { success: true, details: { item, value: parsed } };
|
|
@@ -756,8 +785,10 @@ export class ViceBackend {
|
|
|
756
785
|
for (const [item, value] of Object.entries(items)) {
|
|
757
786
|
try {
|
|
758
787
|
const str = String(value ?? "");
|
|
759
|
-
const
|
|
760
|
-
const parsed =
|
|
788
|
+
const current = await client.resourceGet(item);
|
|
789
|
+
const parsed = current.type === "int" ? Number(str) : str;
|
|
790
|
+
if (current.type === "int" && !Number.isFinite(parsed))
|
|
791
|
+
throw new Error(`VICE resource '${item}' requires an integer value`);
|
|
761
792
|
await client.resourceSet(item, parsed);
|
|
762
793
|
results.push({ item: `${category}/${item}`, success: true });
|
|
763
794
|
}
|
|
@@ -784,6 +815,11 @@ export class ViceBackend {
|
|
|
784
815
|
ViceBackend.supervisors = new Map();
|
|
785
816
|
ViceBackend.cleanupRegistered = false;
|
|
786
817
|
function unsupported(name) { const err = new Error(`Operation '${name}' is not supported by the VICE backend in phase one`); err.code = "UNSUPPORTED"; return err; }
|
|
818
|
+
/** Ultimate file APIs treat slash as a path separator. Encode each name once,
|
|
819
|
+
* preserving leading and nested separators for firmware routing. */
|
|
820
|
+
function encodeDevicePath(value) {
|
|
821
|
+
return value.split("/").map((segment) => encodeURIComponent(segment)).join("/");
|
|
822
|
+
}
|
|
787
823
|
function parseDriveNumber(drive) {
|
|
788
824
|
const match = /\d+/.exec(drive);
|
|
789
825
|
const n = match ? parseInt(match[0], 10) : NaN;
|
package/dist/knowledge.js
CHANGED
|
@@ -82,7 +82,7 @@ Key SID Registers (decimal addresses):
|
|
|
82
82
|
**PAL vs NTSC Clock Differences:**
|
|
83
83
|
- PAL: 985,248 Hz SID clock
|
|
84
84
|
- NTSC: 1,022,727 Hz SID clock
|
|
85
|
-
- Formula: freq_value = (frequency_Hz ×
|
|
85
|
+
- Formula: freq_value = round((frequency_Hz × 16777216) / clock_rate) (SID has a 24-bit phase accumulator)
|
|
86
86
|
|
|
87
87
|
**Complete SID Frequency Lookup Table (All Notes, Multiple Octaves):**
|
|
88
88
|
|
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
import { HttpClient } from "../generated/c64/index.js";
|
|
2
2
|
import { formatErrorMessage, formatPayloadForDebug, loggerFor, payloadByteLength } from "./logger.js";
|
|
3
|
+
/** Headers are useful in diagnostics, but credentials must never leave the
|
|
4
|
+
* transport layer. Axios normalises some keys and preserves others, so do a
|
|
5
|
+
* case-insensitive pass instead of relying on one spelling. */
|
|
6
|
+
function redactHeaders(headers) {
|
|
7
|
+
if (!headers)
|
|
8
|
+
return headers;
|
|
9
|
+
const sensitive = /^(x-password|authorization|proxy-authorization|cookie|set-cookie|x-api-key)$/i;
|
|
10
|
+
return Object.fromEntries(Object.entries(headers).map(([key, value]) => [
|
|
11
|
+
key,
|
|
12
|
+
sensitive.test(key) ? "***" : value,
|
|
13
|
+
]));
|
|
14
|
+
}
|
|
3
15
|
export function createLoggingHttpClient(config, logger = loggerFor("c64u")) {
|
|
4
16
|
const http = new HttpClient(config);
|
|
5
17
|
http.instance.interceptors.request.use((request) => {
|
|
@@ -9,7 +21,7 @@ export function createLoggingHttpClient(config, logger = loggerFor("c64u")) {
|
|
|
9
21
|
startedAt: Date.now(),
|
|
10
22
|
method,
|
|
11
23
|
path,
|
|
12
|
-
headers: request.headers
|
|
24
|
+
headers: redactHeaders(request.headers),
|
|
13
25
|
params: request.params ? { ...request.params } : undefined,
|
|
14
26
|
body: request.data,
|
|
15
27
|
};
|
|
@@ -40,7 +52,7 @@ function handleResponse(response, logger) {
|
|
|
40
52
|
});
|
|
41
53
|
logger.debug(`response ${method} ${path}`, {
|
|
42
54
|
status: response.status,
|
|
43
|
-
headers: response.headers
|
|
55
|
+
headers: redactHeaders(response.headers),
|
|
44
56
|
body: formatPayloadForDebug(response.data),
|
|
45
57
|
});
|
|
46
58
|
}
|
|
@@ -68,7 +80,7 @@ function handleError(error, logger) {
|
|
|
68
80
|
});
|
|
69
81
|
logger.debug(`error ${method} ${path}`, {
|
|
70
82
|
status,
|
|
71
|
-
headers: response?.headers
|
|
83
|
+
headers: redactHeaders(response?.headers),
|
|
72
84
|
body: response ? formatPayloadForDebug(response.data) : null,
|
|
73
85
|
message,
|
|
74
86
|
});
|
package/dist/mcp-server.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import "./bootstrap/stdio-logger.js";
|
|
3
|
+
import { createServer } from "node:http";
|
|
3
4
|
import { dirname, join } from "node:path";
|
|
4
5
|
import { fileURLToPath } from "node:url";
|
|
5
6
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
6
7
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
8
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
7
9
|
import { CallToolRequestSchema, ListResourcesRequestSchema, ListToolsRequestSchema, ReadResourceRequestSchema, ListPromptsRequestSchema, GetPromptRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
8
10
|
import { loadConfig } from "./config.js";
|
|
9
11
|
import { C64Client } from "./c64Client.js";
|
|
@@ -32,7 +34,7 @@ function createPromptRegistryGetter() {
|
|
|
32
34
|
return registry;
|
|
33
35
|
};
|
|
34
36
|
}
|
|
35
|
-
function parseCliOptions(argv) {
|
|
37
|
+
export function parseCliOptions(argv) {
|
|
36
38
|
const httpIndex = argv.indexOf("--http");
|
|
37
39
|
if (httpIndex === -1) {
|
|
38
40
|
return { mode: "stdio" };
|
|
@@ -420,12 +422,34 @@ async function main() {
|
|
|
420
422
|
throw error;
|
|
421
423
|
}
|
|
422
424
|
});
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
425
|
+
const cli = parseCliOptions(process.argv.slice(2));
|
|
426
|
+
if (cli.mode === "http") {
|
|
427
|
+
const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
|
|
428
|
+
await server.connect(transport);
|
|
429
|
+
const port = cli.port ?? 3000;
|
|
430
|
+
const httpServer = createServer((request, response) => {
|
|
431
|
+
if (request.url?.split("?")[0] !== "/mcp") {
|
|
432
|
+
response.statusCode = 404;
|
|
433
|
+
response.end("Not found");
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
void transport.handleRequest(request, response).catch((error) => {
|
|
437
|
+
if (!response.headersSent)
|
|
438
|
+
response.statusCode = 500;
|
|
439
|
+
response.end(error instanceof Error ? error.message : "MCP transport error");
|
|
440
|
+
});
|
|
441
|
+
});
|
|
442
|
+
await new Promise((resolve, reject) => httpServer.listen(port, "127.0.0.1", resolve).once("error", reject));
|
|
443
|
+
writeDiagnosticEvent("mcp_transport_connected", { mode: "http", port });
|
|
444
|
+
console.error(`c64bridge MCP server running on HTTP at http://127.0.0.1:${port}/mcp`);
|
|
445
|
+
}
|
|
446
|
+
else {
|
|
447
|
+
const transport = new StdioServerTransport();
|
|
448
|
+
await server.connect(transport);
|
|
449
|
+
writeDiagnosticEvent("mcp_transport_connected", { mode: "stdio" });
|
|
450
|
+
console.error("c64bridge MCP server running on stdio");
|
|
451
|
+
}
|
|
427
452
|
await logConnectivity(client, baseUrl);
|
|
428
|
-
console.error("c64bridge MCP server running on stdio");
|
|
429
453
|
writeDiagnosticEvent("server_ready", {
|
|
430
454
|
diagnosticsFile: diagnostics.filePath,
|
|
431
455
|
});
|
|
@@ -447,11 +471,18 @@ function createPlatformResourceDescriptor() {
|
|
|
447
471
|
},
|
|
448
472
|
};
|
|
449
473
|
}
|
|
450
|
-
async function renderPlatformStatusMarkdown(client) {
|
|
474
|
+
export async function renderPlatformStatusMarkdown(client) {
|
|
451
475
|
const status = getPlatformStatus();
|
|
452
476
|
const availableBackends = client.getAvailableBackends();
|
|
453
477
|
const capabilities = describePlatformCapabilities(toolRegistry.list());
|
|
454
|
-
|
|
478
|
+
// Resource reads must remain responsive while an Ultimate boots or is
|
|
479
|
+
// unreachable. The probe itself remains retryable (unknown is not cached).
|
|
480
|
+
const nativeEndpoints = await Promise.race([
|
|
481
|
+
client.getNativeEndpointCapabilities(),
|
|
482
|
+
new Promise((resolve) => {
|
|
483
|
+
setTimeout(() => resolve({ machineInput: "unknown", machineMenuScreen: "unknown" }), 750).unref();
|
|
484
|
+
}),
|
|
485
|
+
]);
|
|
455
486
|
const lines = [
|
|
456
487
|
"# MCP Platform Status",
|
|
457
488
|
"",
|
|
@@ -590,11 +621,21 @@ function toPromptMessage(segment) {
|
|
|
590
621
|
},
|
|
591
622
|
};
|
|
592
623
|
}
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
624
|
+
// The documented entrypoint (src/index.ts / dist/index.js, per package.json
|
|
625
|
+
// "main"/"bin") always imports this module as a side effect rather than
|
|
626
|
+
// invoking it as the process's own script, so process.argv[1] never points
|
|
627
|
+
// here — an entrypoint-identity check would wrongly skip startup in every
|
|
628
|
+
// real deployment. Test code that only needs a pure export (e.g.
|
|
629
|
+
// parseCliOptions, renderPlatformStatusMarkdown) sets this opt-out instead,
|
|
630
|
+
// mirroring the existing C64BRIDGE_START_SKIP_AUTO_LAUNCH convention in
|
|
631
|
+
// scripts/start.mjs.
|
|
632
|
+
if (process.env.C64BRIDGE_SKIP_AUTO_START !== "1") {
|
|
633
|
+
main().catch((error) => {
|
|
634
|
+
writeDiagnosticEvent("server_fatal", {
|
|
635
|
+
diagnosticsFile: getDiagnosticsSessionInfo()?.filePath,
|
|
636
|
+
error,
|
|
637
|
+
});
|
|
638
|
+
console.error("Fatal error in MCP server:", error);
|
|
639
|
+
process.exit(1);
|
|
597
640
|
});
|
|
598
|
-
|
|
599
|
-
process.exit(1);
|
|
600
|
-
});
|
|
641
|
+
}
|
package/dist/rag/indexer.js
CHANGED
|
@@ -4,16 +4,19 @@ GPL-2.0-only
|
|
|
4
4
|
*/
|
|
5
5
|
import fs from "node:fs/promises";
|
|
6
6
|
import path from "node:path";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
7
8
|
import fsSync from "node:fs";
|
|
8
|
-
const
|
|
9
|
-
const
|
|
10
|
-
const
|
|
11
|
-
const
|
|
9
|
+
const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..");
|
|
10
|
+
const ASSET_ROOT = process.env.C64BRIDGE_ASSET_ROOT ? path.resolve(process.env.C64BRIDGE_ASSET_ROOT) : PACKAGE_ROOT;
|
|
11
|
+
const BASIC_DIR = path.join(ASSET_ROOT, "data/basic/examples");
|
|
12
|
+
const ASM_DIR = path.join(ASSET_ROOT, "data/assembly/examples");
|
|
13
|
+
const EXTERNAL_DIR = path.join(ASSET_ROOT, "external");
|
|
14
|
+
const DOC_ROOT = path.join(ASSET_ROOT, "doc");
|
|
12
15
|
const DOC_EXCLUDE_DIRS = new Set(["plans"]);
|
|
13
|
-
const CONTEXT_DIR = path.
|
|
16
|
+
const CONTEXT_DIR = path.join(ASSET_ROOT, "data/context");
|
|
14
17
|
const BOOTSTRAP_PATH = path.join(CONTEXT_DIR, "bootstrap.md");
|
|
15
|
-
const AGENTS_PATH = path.
|
|
16
|
-
const PROMPTS_DIR = path.
|
|
18
|
+
const AGENTS_PATH = path.join(ASSET_ROOT, "AGENTS.md");
|
|
19
|
+
const PROMPTS_DIR = path.join(ASSET_ROOT, ".github/prompts");
|
|
17
20
|
const CHAT_PATH = path.join(CONTEXT_DIR, "chat.md");
|
|
18
21
|
// RAG_DOC_FILES env var can add extra specific files, comma-separated
|
|
19
22
|
const ENV_DOC_FILES = (process.env.RAG_DOC_FILES ?? "")
|
|
@@ -34,7 +37,7 @@ function shouldExcludeDocFile(file) {
|
|
|
34
37
|
return firstSegment !== undefined && DOC_EXCLUDE_DIRS.has(firstSegment);
|
|
35
38
|
}
|
|
36
39
|
function resolveEmbeddingsDir(override) {
|
|
37
|
-
return path.resolve(override ?? process.env.RAG_EMBEDDINGS_DIR ?? "data");
|
|
40
|
+
return path.resolve(override ?? process.env.RAG_EMBEDDINGS_DIR ?? path.join(ASSET_ROOT, "data"));
|
|
38
41
|
}
|
|
39
42
|
function embeddingIndexPaths(dir) {
|
|
40
43
|
return {
|
package/dist/rag/init.js
CHANGED
|
@@ -4,22 +4,25 @@ GPL-2.0-only
|
|
|
4
4
|
*/
|
|
5
5
|
import fs from "node:fs/promises";
|
|
6
6
|
import path from "node:path";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
7
8
|
import { setTimeout as delay } from "node:timers/promises";
|
|
8
9
|
import { LocalMiniHashEmbedding } from "./embeddings.js";
|
|
9
10
|
import { buildAllIndexes, loadIndexes } from "./indexer.js";
|
|
10
11
|
import { LocalRagRetriever } from "./retriever.js";
|
|
11
12
|
import { LoggingRagRetriever } from "./loggingRetriever.js";
|
|
12
|
-
const
|
|
13
|
-
const
|
|
14
|
-
const
|
|
15
|
-
const
|
|
16
|
-
const
|
|
13
|
+
const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..");
|
|
14
|
+
const assetRoot = process.env.C64BRIDGE_ASSET_ROOT ? path.resolve(process.env.C64BRIDGE_ASSET_ROOT) : PACKAGE_ROOT;
|
|
15
|
+
const EXTERNAL_DIR = path.join(assetRoot, "external");
|
|
16
|
+
const BASIC_DATA_DIR = path.join(assetRoot, "data/basic/examples");
|
|
17
|
+
const ASM_DATA_DIR = path.join(assetRoot, "data/assembly/examples");
|
|
18
|
+
const DOC_DIR = path.join(assetRoot, "doc");
|
|
19
|
+
const CONTEXT_DIR = path.join(assetRoot, "data/context");
|
|
17
20
|
const BOOTSTRAP_PATH = path.join(CONTEXT_DIR, "bootstrap.md");
|
|
18
|
-
const AGENTS_PATH = path.
|
|
19
|
-
const PROMPTS_DIR = path.
|
|
21
|
+
const AGENTS_PATH = path.join(assetRoot, "AGENTS.md");
|
|
22
|
+
const PROMPTS_DIR = path.join(assetRoot, ".github/prompts");
|
|
20
23
|
const CHAT_PATH = path.join(CONTEXT_DIR, "chat.md");
|
|
21
24
|
function resolveEmbeddingsDir() {
|
|
22
|
-
return path.resolve(process.env.RAG_EMBEDDINGS_DIR ?? "data");
|
|
25
|
+
return path.resolve(process.env.RAG_EMBEDDINGS_DIR ?? path.join(assetRoot, "data"));
|
|
23
26
|
}
|
|
24
27
|
function embeddingIndexPaths() {
|
|
25
28
|
const dir = resolveEmbeddingsDir();
|
package/dist/sidwaveCompiler.js
CHANGED
|
@@ -169,9 +169,9 @@ export function compileSidwaveToSid(doc, prg, options) {
|
|
|
169
169
|
const body = Buffer.isBuffer(prg) ? prg : Buffer.from(prg);
|
|
170
170
|
return { sid: Buffer.concat([header, body]) };
|
|
171
171
|
}
|
|
172
|
-
function hzToSidFrequency(hz, system) {
|
|
172
|
+
export function hzToSidFrequency(hz, system) {
|
|
173
173
|
const phi2 = system === "PAL" ? 985248 : 1022727;
|
|
174
|
-
const value = Math.round((hz *
|
|
174
|
+
const value = Math.round((hz * 16777216) / phi2);
|
|
175
175
|
return Math.max(0, Math.min(0xffff, value));
|
|
176
176
|
}
|
|
177
177
|
function waveformToCtrl(w) {
|