c64bridge 0.9.3 → 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/AGENTS.md +19 -3
- package/README.md +190 -148
- package/dist/.tsbuildinfo +1 -1
- package/dist/c64Client.js +277 -41
- package/dist/config.js +30 -18
- package/dist/device.js +191 -125
- package/dist/knowledge.js +1 -1
- package/dist/loggingHttpClient.js +15 -3
- package/dist/mcp-server.js +65 -16
- package/dist/platform.js +13 -2
- 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 +27 -6
- package/dist/tools/developer.js +6 -6
- package/dist/tools/graphics.js +4 -4
- package/dist/tools/input.js +136 -24
- package/dist/tools/machineControl.js +85 -6
- package/dist/tools/memory.js +21 -6
- package/dist/tools/meta/artifacts.js +20 -8
- package/dist/tools/meta/background.js +11 -1
- package/dist/tools/meta/index.js +1 -1
- package/dist/tools/pollValidator.js +19 -51
- package/dist/tools/printer.js +1 -0
- package/dist/tools/programRunners.js +5 -5
- package/dist/tools/rag.js +1 -1
- package/dist/tools/registry/batch.js +1 -1
- package/dist/tools/registry/config.js +5 -5
- package/dist/tools/registry/disk.js +2 -2
- package/dist/tools/registry/drive.js +2 -2
- package/dist/tools/registry/graphics.js +2 -2
- package/dist/tools/registry/memory.js +2 -2
- package/dist/tools/registry/platform.js +9 -5
- package/dist/tools/registry/printer.js +1 -0
- package/dist/tools/registry/program.js +5 -5
- package/dist/tools/registry/rag.js +1 -1
- package/dist/tools/registry/sound.js +4 -4
- package/dist/tools/registry/system.js +13 -3
- package/dist/tools/storage.js +1 -1
- package/dist/vice/mockServer.js +33 -0
- package/dist/vice/process.js +11 -1
- package/dist/vice/viceClient.js +35 -6
- package/doc/c64u/c64-openapi.yaml +1130 -774
- 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/doc/u2/u2-openapi.yaml +1192 -0
- package/generated/c64/index.js +142 -9
- package/generated/openapi/c64u/index.js +1117 -0
- package/generated/openapi/u2/index.js +929 -0
- package/mcp/schemas/c64_graphics.schema.json +3 -3
- package/mcp/schemas/c64_input.schema.json +249 -8
- package/mcp/schemas/c64_memory.schema.json +3 -3
- package/mcp/schemas/c64_program.schema.json +3 -3
- package/mcp/schemas/c64_select_backend.schema.json +2 -0
- package/mcp/schemas/c64_system.schema.json +41 -1
- package/mcp/server.json +1 -1
- package/mcp/tools.json +386 -44
- package/mcp.json +14 -2
- package/package.json +4 -2
- package/scripts/mockC64Server.mjs +171 -2
- package/scripts/prepare-release.mjs +13 -2
- package/scripts/update-readme.ts +18 -30
- package/server.json +3 -3
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,54 +53,25 @@ 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.vice && json?.vice && typeof json.vice === "object" && !Array.isArray(json.vice)) {
|
|
81
|
-
merged.vice = json.vice;
|
|
82
|
-
}
|
|
83
|
-
if (merged.c64u && merged.vice) {
|
|
84
|
-
break;
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
catch { }
|
|
89
|
-
}
|
|
90
|
-
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
|
+
};
|
|
91
62
|
}
|
|
92
63
|
class C64uBackend {
|
|
93
|
-
constructor(config) {
|
|
94
|
-
this.type =
|
|
95
|
-
const envHost = configuredString(process.env.C64U_HOST);
|
|
96
|
-
const envPort = configuredPort(process.env.C64U_PORT);
|
|
64
|
+
constructor(config, type = "c64u") {
|
|
65
|
+
this.type = type;
|
|
66
|
+
const envHost = configuredString(type === "u2" ? process.env.U2_HOST ?? process.env.C64U_HOST : process.env.C64U_HOST);
|
|
67
|
+
const envPort = configuredPort(type === "u2" ? process.env.U2_PORT ?? process.env.C64U_PORT : process.env.C64U_PORT);
|
|
97
68
|
const configBaseUrl = normaliseBaseUrl(config.baseUrl);
|
|
98
69
|
const parsedConfigBaseUrl = configBaseUrl ? parseEndpoint(configBaseUrl) : {};
|
|
99
70
|
const baseUrl = envHost !== undefined || envPort !== undefined
|
|
100
71
|
? buildBaseUrl(firstDefined(envHost, configuredString(config.host), configuredString(config.hostname), parsedConfigBaseUrl.hostname) ?? DEFAULT_C64U_HOST, firstDefined(envPort, configuredPort(config.port), parsedConfigBaseUrl.port) ?? DEFAULT_C64U_PORT)
|
|
101
72
|
: resolveBaseUrl(config);
|
|
102
73
|
this.baseUrl = baseUrl;
|
|
103
|
-
this.networkPassword = firstDefined(configuredString(process.env.C64U_PASSWORD), configuredString(config.networkPassword));
|
|
74
|
+
this.networkPassword = firstDefined(configuredString(type === "u2" ? process.env.U2_PASSWORD ?? process.env.C64U_PASSWORD : process.env.C64U_PASSWORD), configuredString(config.networkPassword));
|
|
104
75
|
const http = createLoggingHttpClient({
|
|
105
76
|
baseURL: baseUrl,
|
|
106
77
|
timeout: 10000,
|
|
@@ -108,7 +79,20 @@ class C64uBackend {
|
|
|
108
79
|
});
|
|
109
80
|
this.api = new Api(http);
|
|
110
81
|
}
|
|
82
|
+
requireC64u(feature) {
|
|
83
|
+
if (this.type === "u2") {
|
|
84
|
+
throw unsupported(`${feature} is unavailable on U2-family cartridges`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
111
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
|
+
}
|
|
112
96
|
async ping() {
|
|
113
97
|
try {
|
|
114
98
|
const res = await axios.get(this.baseUrl, {
|
|
@@ -124,30 +108,35 @@ class C64uBackend {
|
|
|
124
108
|
async runPrg(prg) {
|
|
125
109
|
const payload = Buffer.isBuffer(prg) ? prg : Buffer.from(prg);
|
|
126
110
|
const res = await this.api.v1.runnersRunPrgCreate(":run_prg", payload, { headers: { "Content-Type": "application/octet-stream" } });
|
|
127
|
-
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);
|
|
128
117
|
}
|
|
129
118
|
async loadPrgFile(pathStr) {
|
|
130
119
|
const res = await this.api.v1.runnersLoadPrgUpdate(":load_prg", { file: pathStr });
|
|
131
|
-
return
|
|
120
|
+
return this.actionResult(res.data);
|
|
132
121
|
}
|
|
133
122
|
async runPrgFile(pathStr) {
|
|
134
123
|
const res = await this.api.v1.runnersRunPrgUpdate(":run_prg", { file: pathStr });
|
|
135
|
-
return
|
|
124
|
+
return this.actionResult(res.data);
|
|
136
125
|
}
|
|
137
126
|
async runCrtFile(pathStr) {
|
|
138
127
|
const res = await this.api.v1.runnersRunCrtUpdate(":run_crt", { file: pathStr });
|
|
139
|
-
return
|
|
128
|
+
return this.actionResult(res.data);
|
|
140
129
|
}
|
|
141
130
|
async sidplayFile(pathStr, songnr) {
|
|
142
131
|
const res = await this.api.v1.runnersSidplayUpdate(":sidplay", { file: pathStr, songnr });
|
|
143
|
-
return
|
|
132
|
+
return this.actionResult(res.data);
|
|
144
133
|
}
|
|
145
134
|
async sidplayAttachment(sid, options) {
|
|
146
135
|
const form = { sid: Buffer.isBuffer(sid) ? sid : Buffer.from(sid) };
|
|
147
136
|
if (options?.songlengths)
|
|
148
137
|
form.songlengths = Buffer.isBuffer(options.songlengths) ? options.songlengths : Buffer.from(options.songlengths);
|
|
149
138
|
const res = await this.api.v1.runnersSidplayCreate(":sidplay", form, options?.songnr !== undefined ? { songnr: options.songnr } : undefined);
|
|
150
|
-
return
|
|
139
|
+
return this.actionResult(res.data);
|
|
151
140
|
}
|
|
152
141
|
async readMemory(address, length) {
|
|
153
142
|
const addrStr = address.toString(16).toUpperCase().padStart(4, "0");
|
|
@@ -181,39 +170,68 @@ class C64uBackend {
|
|
|
181
170
|
const mergedBlocks = coalesceMemoryWriteBlocks(blocks);
|
|
182
171
|
await Promise.all(mergedBlocks.map(({ address, bytes }) => this.writeMemory(address, bytes)));
|
|
183
172
|
}
|
|
184
|
-
async reset() { const res = await this.api.v1.machineResetUpdate(":reset"); return
|
|
185
|
-
async reboot() { const res = await this.api.v1.machineRebootUpdate(":reboot"); return
|
|
186
|
-
async pause() { const res = await this.api.v1.machinePauseUpdate(":pause"); return
|
|
187
|
-
async resume() { const res = await this.api.v1.machineResumeUpdate(":resume"); return
|
|
188
|
-
async poweroff() { const res = await this.api.v1.machinePoweroffUpdate(":poweroff"); return
|
|
189
|
-
async
|
|
190
|
-
async
|
|
191
|
-
async
|
|
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); }
|
|
178
|
+
async powerCycle() { this.requireC64u("powerCycle"); throw unsupported("powerCycle must be coordinated by C64Client"); }
|
|
179
|
+
async menuButton() { const res = await this.api.v1.machineMenuButtonUpdate(":menu_button"); return this.actionResult(res.data); }
|
|
180
|
+
async readMenuScreen() {
|
|
181
|
+
const response = await this.api.v1.machineMenuScreenList(":menu_screen", { format: "arraybuffer", headers: { Accept: "application/octet-stream" } });
|
|
182
|
+
const body = response.data;
|
|
183
|
+
return body instanceof ArrayBuffer ? new Uint8Array(body) : extractBytes(body);
|
|
184
|
+
}
|
|
185
|
+
async getInputState() {
|
|
186
|
+
this.requireC64u("getInputState");
|
|
187
|
+
const response = await this.api.v1.machineInputList(":input");
|
|
188
|
+
return response.data;
|
|
189
|
+
}
|
|
190
|
+
async sendInputEvents(batch) {
|
|
191
|
+
this.requireC64u("sendInputEvents");
|
|
192
|
+
const response = await this.api.v1.machineInputCreate(":input", batch);
|
|
193
|
+
return response.data;
|
|
194
|
+
}
|
|
195
|
+
async debugregRead() { this.requireC64u("debugregRead"); const res = await this.api.v1.machineDebugregList(":debugreg"); return { success: true, value: res.data.value, details: res.data }; }
|
|
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 }; }
|
|
192
197
|
async version() { const res = await this.api.v1.versionList(); return res.data; }
|
|
193
198
|
async info() { const res = await this.api.v1.infoList(); return res.data; }
|
|
194
|
-
async drivesList() {
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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); }
|
|
204
222
|
async configsList() { const res = await this.api.v1.configsList(); return res.data; }
|
|
205
223
|
async configGet(cat, item) { const res = item ? await this.api.v1.configsDetail2(cat, item) : await this.api.v1.configsDetail(cat); return res.data; }
|
|
206
|
-
async configSet(cat, item, value) { const res = await this.api.v1.configsUpdate(cat, item, { value }); return
|
|
207
|
-
async configBatchUpdate(payload) { const res = await this.api.v1.configsCreate(payload); return
|
|
208
|
-
async configLoadFromFlash() { const res = await this.api.v1.configsLoadFromFlashUpdate(":load_from_flash"); return
|
|
209
|
-
async configSaveToFlash() { const res = await this.api.v1.configsSaveToFlashUpdate(":save_to_flash"); return
|
|
210
|
-
async configResetToDefault() { const res = await this.api.v1.configsResetToDefaultUpdate(":reset_to_default"); return
|
|
211
|
-
async filesInfo(p) { const res = await this.api.v1.filesInfoDetail(
|
|
212
|
-
async filesCreateD64(p, options) { const res = await this.api.v1.filesCreateD64Update(
|
|
213
|
-
async filesCreateD71(p, options) { const res = await this.api.v1.filesCreateD71Update(
|
|
214
|
-
async filesCreateD81(p, options) { const res = await this.api.v1.filesCreateD81Update(
|
|
215
|
-
async filesCreateDnp(p, tracks, options) { const res = await this.api.v1.filesCreateDnpUpdate(
|
|
216
|
-
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); }
|
|
217
235
|
}
|
|
218
236
|
export class ViceBackend {
|
|
219
237
|
constructor(config) {
|
|
@@ -378,13 +396,19 @@ export class ViceBackend {
|
|
|
378
396
|
return;
|
|
379
397
|
}
|
|
380
398
|
ViceBackend.cleanupRegistered = true;
|
|
381
|
-
|
|
382
|
-
process.once("exit", () => {
|
|
399
|
+
const cleanupSync = () => {
|
|
383
400
|
for (const [, handle] of ViceBackend.supervisors) {
|
|
384
|
-
handle.
|
|
401
|
+
handle.stopSync();
|
|
385
402
|
}
|
|
386
403
|
ViceBackend.supervisors.clear();
|
|
387
|
-
}
|
|
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
|
+
}
|
|
388
412
|
}
|
|
389
413
|
async withClient(fn, options) {
|
|
390
414
|
const previous = this.monitorQueue;
|
|
@@ -448,7 +472,15 @@ export class ViceBackend {
|
|
|
448
472
|
}
|
|
449
473
|
return false;
|
|
450
474
|
}
|
|
451
|
-
|
|
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) {
|
|
452
484
|
if (buffer.length < 2)
|
|
453
485
|
throw new Error("PRG data too short");
|
|
454
486
|
const loadAddress = buffer.readUInt16LE(0);
|
|
@@ -458,7 +490,12 @@ export class ViceBackend {
|
|
|
458
490
|
await waitForBasicReady(client, { timeoutMs: 10000, ensurePrompt: true });
|
|
459
491
|
if (body.length > 0)
|
|
460
492
|
await client.memSet(loadAddress, body);
|
|
461
|
-
|
|
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) => {
|
|
462
499
|
const ptrs = Buffer.alloc(8);
|
|
463
500
|
ptrs.writeUInt16LE(loadAddress, 0);
|
|
464
501
|
ptrs.writeUInt16LE(programEnd, 2);
|
|
@@ -466,7 +503,6 @@ export class ViceBackend {
|
|
|
466
503
|
ptrs.writeUInt16LE(programEnd, 6);
|
|
467
504
|
await client.memSet(0x002B, ptrs);
|
|
468
505
|
await client.keyboardFeed("RUN\r");
|
|
469
|
-
await client.exitMonitor();
|
|
470
506
|
});
|
|
471
507
|
}
|
|
472
508
|
async runPrg(prg) {
|
|
@@ -474,6 +510,13 @@ export class ViceBackend {
|
|
|
474
510
|
await this.injectPrg(buffer);
|
|
475
511
|
return { success: true };
|
|
476
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
|
+
}
|
|
477
520
|
async loadPrgFile(_path) { throw unsupported("loadPrgFile"); }
|
|
478
521
|
async runPrgFile(prgPath) {
|
|
479
522
|
const data = fs.readFileSync(prgPath);
|
|
@@ -516,22 +559,29 @@ export class ViceBackend {
|
|
|
516
559
|
});
|
|
517
560
|
}
|
|
518
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;
|
|
519
566
|
await this.withClient(async (client) => {
|
|
520
567
|
await client.reset();
|
|
521
568
|
const opts = this.debugEnabled
|
|
522
569
|
? {
|
|
523
|
-
timeoutMs:
|
|
570
|
+
timeoutMs: resetReadinessTimeoutMs,
|
|
524
571
|
ensurePrompt: true,
|
|
525
572
|
onPointersSample: (p) => {
|
|
526
573
|
console.error("[vice-backend] BASIC pointers", p);
|
|
527
574
|
},
|
|
528
575
|
}
|
|
529
|
-
: { timeoutMs:
|
|
530
|
-
|
|
576
|
+
: { timeoutMs: resetReadinessTimeoutMs, ensurePrompt: true };
|
|
577
|
+
readiness = await waitForBasicReady(client, opts);
|
|
531
578
|
if (this.debugEnabled)
|
|
532
579
|
console.error("[vice-backend] waitForBasicReady result", readiness);
|
|
533
580
|
});
|
|
534
|
-
|
|
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." } };
|
|
535
585
|
}
|
|
536
586
|
async reboot() { return this.reset(); }
|
|
537
587
|
async pause() {
|
|
@@ -580,6 +630,7 @@ export class ViceBackend {
|
|
|
580
630
|
return { success: false, details };
|
|
581
631
|
}
|
|
582
632
|
}
|
|
633
|
+
async powerCycle() { return this.nuclearReset(); }
|
|
583
634
|
async nuclearReset() {
|
|
584
635
|
const poweroffResult = await this.poweroff();
|
|
585
636
|
if (!poweroffResult.success) {
|
|
@@ -606,6 +657,9 @@ export class ViceBackend {
|
|
|
606
657
|
}
|
|
607
658
|
}
|
|
608
659
|
async menuButton() { throw unsupported("menuButton"); }
|
|
660
|
+
async readMenuScreen() { throw unsupported("readMenuScreen"); }
|
|
661
|
+
async getInputState() { throw unsupported("getInputState"); }
|
|
662
|
+
async sendInputEvents(_batch) { throw unsupported("sendInputEvents"); }
|
|
609
663
|
async debugregRead() { throw unsupported("debugregRead"); }
|
|
610
664
|
async debugregWrite(_v) { throw unsupported("debugregWrite"); }
|
|
611
665
|
async version() { return { emulator: "vice", host: this.host, port: this.port }; }
|
|
@@ -714,9 +768,12 @@ export class ViceBackend {
|
|
|
714
768
|
});
|
|
715
769
|
}
|
|
716
770
|
async configSet(_category, item, value) {
|
|
717
|
-
|
|
718
|
-
const parsed = !isNaN(numValue) && value.trim() !== "" ? numValue : value;
|
|
771
|
+
let parsed = value;
|
|
719
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`);
|
|
720
777
|
await client.resourceSet(item, parsed);
|
|
721
778
|
});
|
|
722
779
|
return { success: true, details: { item, value: parsed } };
|
|
@@ -728,8 +785,10 @@ export class ViceBackend {
|
|
|
728
785
|
for (const [item, value] of Object.entries(items)) {
|
|
729
786
|
try {
|
|
730
787
|
const str = String(value ?? "");
|
|
731
|
-
const
|
|
732
|
-
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`);
|
|
733
792
|
await client.resourceSet(item, parsed);
|
|
734
793
|
results.push({ item: `${category}/${item}`, success: true });
|
|
735
794
|
}
|
|
@@ -756,6 +815,11 @@ export class ViceBackend {
|
|
|
756
815
|
ViceBackend.supervisors = new Map();
|
|
757
816
|
ViceBackend.cleanupRegistered = false;
|
|
758
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
|
+
}
|
|
759
823
|
function parseDriveNumber(drive) {
|
|
760
824
|
const match = /\d+/.exec(drive);
|
|
761
825
|
const n = match ? parseInt(match[0], 10) : NaN;
|
|
@@ -885,46 +949,53 @@ export function __buildViceProcessOptionsForTests(input) {
|
|
|
885
949
|
return buildManagedViceProcessOptions(input);
|
|
886
950
|
}
|
|
887
951
|
export async function createFacade(logger, options) {
|
|
952
|
+
const envMode = (process.env.C64_MODE || "").toLowerCase().trim();
|
|
888
953
|
// Caller-forced preference: use c64u with provided base URL (used by tests and server wiring)
|
|
889
954
|
if (options?.preferredC64uBaseUrl) {
|
|
955
|
+
const type = envMode === "u2" ? "u2" : "c64u";
|
|
890
956
|
const backend = new C64uBackend({
|
|
891
957
|
baseUrl: options.preferredC64uBaseUrl,
|
|
892
958
|
networkPassword: options.preferredC64uNetworkPassword,
|
|
893
|
-
});
|
|
894
|
-
logger?.info?.(
|
|
895
|
-
return { facade: backend, selected:
|
|
959
|
+
}, type);
|
|
960
|
+
logger?.info?.(`Active backend: ${type} (forced by caller)`);
|
|
961
|
+
return { facade: backend, selected: type, reason: "forced by caller", details: { baseUrl: options.preferredC64uBaseUrl } };
|
|
896
962
|
}
|
|
897
963
|
const cfg = readConfigFile();
|
|
898
|
-
const envMode = (process.env.C64_MODE || "").toLowerCase().trim();
|
|
899
964
|
const hasC64u = Boolean(cfg?.c64u);
|
|
965
|
+
const hasU2 = Boolean(cfg?.u2);
|
|
900
966
|
const hasVice = Boolean(cfg?.vice);
|
|
901
967
|
if (envMode === "c64u") {
|
|
902
968
|
const backend = new C64uBackend(cfg?.c64u ?? {});
|
|
903
969
|
logger?.info?.("Active backend: c64u (from env override)");
|
|
904
970
|
return { facade: backend, selected: "c64u", reason: "env override", details: { baseUrl: backend.getBaseUrl?.() } };
|
|
905
971
|
}
|
|
972
|
+
if (envMode === "u2") {
|
|
973
|
+
const backend = new C64uBackend(cfg?.u2 ?? {}, "u2");
|
|
974
|
+
logger?.info?.("Active backend: u2 (from env override)");
|
|
975
|
+
return { facade: backend, selected: "u2", reason: "env override", details: { baseUrl: backend.getBaseUrl?.() } };
|
|
976
|
+
}
|
|
906
977
|
if (envMode === "vice") {
|
|
907
978
|
const backend = new ViceBackend(cfg?.vice ?? {});
|
|
908
979
|
logger?.info?.("Active backend: vice (from env override)");
|
|
909
980
|
const endpoint = backend.getEndpoint();
|
|
910
981
|
return { facade: backend, selected: "vice", reason: "env override", details: { host: endpoint.host, port: endpoint.port } };
|
|
911
982
|
}
|
|
912
|
-
if (hasC64u
|
|
983
|
+
if (hasC64u) {
|
|
913
984
|
const backend = new C64uBackend(cfg.c64u);
|
|
914
985
|
logger?.info?.("Active backend: c64u (from config)");
|
|
915
|
-
return { facade: backend, selected: "c64u", reason: "config only", details: { baseUrl: backend.getBaseUrl?.() } };
|
|
986
|
+
return { facade: backend, selected: "c64u", reason: hasVice ? "both defined (prefer c64u)" : "config only", details: { baseUrl: backend.getBaseUrl?.() } };
|
|
987
|
+
}
|
|
988
|
+
if (hasU2) {
|
|
989
|
+
const backend = new C64uBackend(cfg.u2, "u2");
|
|
990
|
+
logger?.info?.("Active backend: u2 (from config)");
|
|
991
|
+
return { facade: backend, selected: "u2", reason: "config only", details: { baseUrl: backend.getBaseUrl?.() } };
|
|
916
992
|
}
|
|
917
|
-
if (
|
|
993
|
+
if (hasVice) {
|
|
918
994
|
const backend = new ViceBackend(cfg.vice);
|
|
919
995
|
logger?.info?.("Active backend: vice (from config)");
|
|
920
996
|
const endpoint = backend.getEndpoint();
|
|
921
997
|
return { facade: backend, selected: "vice", reason: "config only", details: { host: endpoint.host, port: endpoint.port } };
|
|
922
998
|
}
|
|
923
|
-
if (hasC64u && hasVice) {
|
|
924
|
-
const backend = new C64uBackend(cfg.c64u);
|
|
925
|
-
logger?.info?.("Active backend: c64u (both defined; default preference)");
|
|
926
|
-
return { facade: backend, selected: "c64u", reason: "both defined (prefer c64u)", details: { baseUrl: backend.getBaseUrl?.() } };
|
|
927
|
-
}
|
|
928
999
|
// No configuration
|
|
929
1000
|
const probeBase = resolveBaseUrl({});
|
|
930
1001
|
try {
|
|
@@ -942,35 +1013,30 @@ export async function createFacade(logger, options) {
|
|
|
942
1013
|
return { facade: backend, selected: "vice", reason: "fallback (hardware unavailable)", details: { host: endpoint.host, port: endpoint.port } };
|
|
943
1014
|
}
|
|
944
1015
|
export async function createAllFacades(logger, options) {
|
|
945
|
-
|
|
1016
|
+
// C64U/VICE preserve config-file selection semantics. U2 needs the
|
|
1017
|
+
// caller-provided endpoint because it is selected through C64_MODE=u2.
|
|
1018
|
+
const primary = await createFacade(logger, process.env.C64_MODE?.toLowerCase().trim() === "u2" ? options : undefined);
|
|
946
1019
|
const config = readConfigFile();
|
|
947
|
-
const
|
|
948
|
-
if (
|
|
949
|
-
|
|
1020
|
+
const facades = new Map([[primary.selected, primary.facade]]);
|
|
1021
|
+
if (config?.c64u && primary.selected !== "c64u") {
|
|
1022
|
+
facades.set("c64u", new C64uBackend(config.c64u));
|
|
1023
|
+
}
|
|
1024
|
+
if (config?.u2 && primary.selected !== "u2") {
|
|
1025
|
+
facades.set("u2", new C64uBackend(config.u2, "u2"));
|
|
1026
|
+
}
|
|
1027
|
+
if (config?.vice && primary.selected !== "vice") {
|
|
1028
|
+
facades.set("vice", new ViceBackend(config.vice));
|
|
1029
|
+
}
|
|
1030
|
+
if (!facades.has("c64u") && !config?.u2 && shouldProvisionSecondaryC64u(config, options) && primary.selected !== "u2") {
|
|
1031
|
+
facades.set("c64u", new C64uBackend({
|
|
950
1032
|
...(config?.c64u ?? {}),
|
|
951
1033
|
...(!config?.c64u?.baseUrl && options?.preferredC64uBaseUrl ? { baseUrl: options.preferredC64uBaseUrl } : {}),
|
|
952
1034
|
...(!config?.c64u?.networkPassword && options?.preferredC64uNetworkPassword
|
|
953
1035
|
? { networkPassword: options.preferredC64uNetworkPassword }
|
|
954
1036
|
: {}),
|
|
955
|
-
};
|
|
956
|
-
return {
|
|
957
|
-
primary,
|
|
958
|
-
secondary: new C64uBackend(secondaryConfig),
|
|
959
|
-
secondaryType,
|
|
960
|
-
};
|
|
961
|
-
}
|
|
962
|
-
if (secondaryType === "vice" && config?.vice) {
|
|
963
|
-
return {
|
|
964
|
-
primary,
|
|
965
|
-
secondary: new ViceBackend(config.vice),
|
|
966
|
-
secondaryType,
|
|
967
|
-
};
|
|
1037
|
+
}));
|
|
968
1038
|
}
|
|
969
|
-
return {
|
|
970
|
-
primary,
|
|
971
|
-
secondary: null,
|
|
972
|
-
secondaryType: null,
|
|
973
|
-
};
|
|
1039
|
+
return { primary, facades };
|
|
974
1040
|
}
|
|
975
1041
|
function shouldProvisionSecondaryC64u(config, options) {
|
|
976
1042
|
return Boolean(config?.c64u
|
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
|
});
|