c64bridge 0.7.4 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/prompts/README.md +14 -4
- package/.github/prompts/assembly-routine-session.prompt.md +9 -9
- package/.github/prompts/basic-program-session.prompt.md +11 -9
- package/.github/prompts/drive-management-session.prompt.md +9 -9
- package/.github/prompts/hello-world-session.prompt.md +17 -0
- package/.github/prompts/petscii-art-session.prompt.md +9 -9
- package/.github/prompts/play-classic-game.prompt.md +9 -9
- package/.github/prompts/pr-converge.prompt.md +13 -0
- package/.github/prompts/sid-music-session.prompt.md +9 -9
- package/AGENTS.md +59 -60
- package/README.md +550 -191
- package/data/assembly/assembly-spec.md +290 -1
- package/data/context/bootstrap.md +26 -2
- package/data/context/fast-paths.md +44 -0
- package/data/graphics/petscii-style-guide.md +6 -2
- package/data/graphics/sprite-charset-best-practices.md +5 -5
- package/dist/.tsbuildinfo +1 -1
- package/dist/c64Client.js +990 -26
- package/dist/config.js +5 -1
- package/dist/device.js +680 -57
- package/dist/diagnostics.js +371 -0
- package/dist/mcp/metadata.js +39 -0
- package/dist/mcp-server.js +170 -20
- package/dist/platform.js +10 -2
- package/dist/prompts/registry.js +166 -309
- package/dist/rag/init.js +37 -1
- package/dist/rag/knowledgeIndex.js +18 -6
- package/dist/rag/rateLimiter.js +2 -2
- package/dist/streamCapture.js +135 -0
- package/dist/tools/audio.js +3 -1
- package/dist/tools/debug.js +470 -0
- package/dist/tools/developer.js +10 -4
- package/dist/tools/graphics.js +183 -17
- package/dist/tools/machineControl.js +11 -5
- package/dist/tools/memory.js +19 -10
- package/dist/tools/meta/audio.js +240 -1
- package/dist/tools/meta/config.js +20 -28
- package/dist/tools/meta/configInventory.js +104 -0
- package/dist/tools/meta/diagnostics.js +45 -2
- package/dist/tools/meta/graphics.js +14 -6
- package/dist/tools/meta/index.js +1 -0
- package/dist/tools/meta/memory.js +20 -6
- package/dist/tools/meta/program.js +473 -0
- package/dist/tools/pollValidator.js +4 -2
- package/dist/tools/programRunners.js +8 -4
- package/dist/tools/registry/config.js +17 -0
- package/dist/tools/registry/disk.js +7 -0
- package/dist/tools/registry/drive.js +3 -0
- package/dist/tools/registry/graphics.js +111 -27
- package/dist/tools/registry/index.js +6 -0
- package/dist/tools/registry/memory.js +1 -0
- package/dist/tools/registry/platform.js +121 -0
- package/dist/tools/registry/program.js +26 -8
- package/dist/tools/registry/sound.js +96 -0
- package/dist/tools/registry/system.js +15 -2
- package/dist/tools/storage.js +1 -0
- package/dist/tools/streaming.js +1 -0
- package/dist/tools/types.js +38 -4
- package/dist/tools/vice.js +160 -0
- package/dist/vicBitmap.js +317 -0
- package/dist/vice/mockServer.js +439 -0
- package/dist/vice/process.js +310 -0
- package/dist/vice/readiness.js +18 -2
- package/dist/vice/viceClient.js +382 -22
- package/doc/developer.md +62 -24
- package/doc/img/backend-switch/hello-c64u.png +0 -0
- package/doc/img/backend-switch/hello-vice-and-c64u.png +0 -0
- package/doc/img/backend-switch/hello-vice.png +0 -0
- package/doc/img/backend-switch/metadata.json +50 -0
- package/doc/plans/vice-backend-switch/plan.md +486 -0
- package/doc/plans/vice-backend-switch-2/plan.md +48 -0
- package/doc/plans/vice-support/notes.md +1 -0
- package/doc/plans/vice-support/plan.md +5 -0
- package/doc/plans/vice-support/rollout.md +23 -17
- package/doc/reviews/vice-support-review.md +185 -0
- package/doc/troubleshooting-mcp.md +35 -6
- package/doc/vice/support-matrix.md +54 -0
- package/mcp/prompts.json +292 -0
- package/mcp/protocol-examples.json +46 -0
- package/mcp/resources.json +940 -0
- package/mcp/schemas/c64_config.schema.json +350 -0
- package/mcp/schemas/c64_debug.schema.json +380 -0
- package/mcp/schemas/c64_disk.schema.json +278 -0
- package/mcp/schemas/c64_drive.schema.json +120 -0
- package/mcp/schemas/c64_extract.schema.json +302 -0
- package/mcp/schemas/c64_graphics.schema.json +333 -0
- package/mcp/schemas/c64_memory.schema.json +155 -0
- package/mcp/schemas/c64_printer.schema.json +218 -0
- package/mcp/schemas/c64_program.schema.json +391 -0
- package/mcp/schemas/c64_rag.schema.json +67 -0
- package/mcp/schemas/c64_select_backend.schema.json +32 -0
- package/mcp/schemas/c64_sound.schema.json +644 -0
- package/mcp/schemas/c64_stream.schema.json +61 -0
- package/mcp/schemas/c64_system.schema.json +227 -0
- package/mcp/schemas/c64_vice.schema.json +63 -0
- package/mcp/server.json +15 -0
- package/mcp/tools.json +4517 -0
- package/mcp.json +83 -9
- package/package.json +8 -3
- package/scripts/capture-backend-switch-screenshots.mjs +276 -0
- package/scripts/generate-mcp-interface.ts +289 -0
- package/scripts/invoke-bun.mjs +67 -21
- package/scripts/mockC64Server.mjs +162 -6
- package/scripts/run-coverage.mjs +284 -0
- package/scripts/run-tests.mjs +95 -39
- package/scripts/run-tests.ts +378 -73
- package/scripts/update-readme.ts +147 -13
package/dist/vice/viceClient.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*
|
|
2
|
-
*
|
|
2
|
+
* VICE Binary Monitor client with debugger, resource, and display helpers.
|
|
3
3
|
*/
|
|
4
4
|
import net from "node:net";
|
|
5
5
|
export class ViceClient {
|
|
@@ -7,25 +7,56 @@ export class ViceClient {
|
|
|
7
7
|
this.buffer = Buffer.alloc(0);
|
|
8
8
|
this.nextReqId = 1;
|
|
9
9
|
this.pending = new Map();
|
|
10
|
+
this.closing = false;
|
|
10
11
|
}
|
|
11
12
|
async connect(port, host = "127.0.0.1") {
|
|
12
13
|
this.socket = net.connect({ host, port });
|
|
14
|
+
this.closing = false;
|
|
13
15
|
this.socket.setNoDelay(true);
|
|
14
16
|
await new Promise((resolve, reject) => {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
+
const onConnect = () => {
|
|
18
|
+
this.socket.off("error", onError);
|
|
19
|
+
resolve();
|
|
20
|
+
};
|
|
21
|
+
const onError = (error) => {
|
|
22
|
+
this.socket.off("connect", onConnect);
|
|
23
|
+
reject(error);
|
|
24
|
+
};
|
|
25
|
+
this.socket.once("connect", onConnect);
|
|
26
|
+
this.socket.once("error", onError);
|
|
17
27
|
});
|
|
18
28
|
this.socket.on("data", (chunk) => this.onData(chunk));
|
|
19
29
|
this.socket.on("error", (err) => {
|
|
20
|
-
|
|
21
|
-
p.reject(err);
|
|
22
|
-
this.pending.clear();
|
|
30
|
+
this.rejectAllPending(err);
|
|
23
31
|
});
|
|
32
|
+
this.socket.on("close", (hadError) => {
|
|
33
|
+
if (this.pending.size === 0 || this.closing) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
const reason = hadError ? new Error("VICE monitor connection closed due to error") : new Error("VICE monitor connection closed");
|
|
37
|
+
this.rejectAllPending(reason);
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
close() {
|
|
41
|
+
const reason = new Error("VICE monitor connection closed");
|
|
42
|
+
try {
|
|
43
|
+
this.closing = true;
|
|
44
|
+
this.rejectAllPending(reason);
|
|
45
|
+
this.buffer = Buffer.alloc(0);
|
|
46
|
+
this.socket?.removeAllListeners();
|
|
47
|
+
this.socket?.destroy();
|
|
48
|
+
}
|
|
49
|
+
catch { }
|
|
24
50
|
}
|
|
25
|
-
|
|
26
|
-
this.
|
|
51
|
+
rejectAllPending(reason) {
|
|
52
|
+
for (const [, pending] of this.pending) {
|
|
53
|
+
try {
|
|
54
|
+
pending.reject(reason);
|
|
55
|
+
}
|
|
56
|
+
catch { }
|
|
57
|
+
}
|
|
58
|
+
this.pending.clear();
|
|
27
59
|
}
|
|
28
|
-
catch { } }
|
|
29
60
|
onData(chunk) {
|
|
30
61
|
const data = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
31
62
|
this.buffer = Buffer.concat([this.buffer, data]);
|
|
@@ -42,21 +73,33 @@ export class ViceClient {
|
|
|
42
73
|
return;
|
|
43
74
|
const frame = this.buffer.subarray(0, total);
|
|
44
75
|
this.buffer = this.buffer.subarray(total);
|
|
45
|
-
const
|
|
76
|
+
const responseType = frame[6];
|
|
46
77
|
const err = frame[7];
|
|
47
78
|
const reqId = frame.readUInt32LE(8);
|
|
48
|
-
if (reqId === 0xffffffff)
|
|
49
|
-
continue; // unsolicited event
|
|
79
|
+
if (reqId === 0xffffffff) {
|
|
80
|
+
continue; // unsolicited event
|
|
81
|
+
}
|
|
50
82
|
const pending = this.pending.get(reqId);
|
|
51
|
-
if (!pending)
|
|
83
|
+
if (!pending) {
|
|
52
84
|
continue;
|
|
85
|
+
}
|
|
53
86
|
if (err !== 0x00) {
|
|
54
87
|
pending.reject(new Error(`BM error 0x${err.toString(16)}`));
|
|
55
88
|
this.pending.delete(reqId);
|
|
56
89
|
continue;
|
|
57
90
|
}
|
|
58
|
-
if (pending.cmd !==
|
|
59
|
-
|
|
91
|
+
if (pending.cmd !== responseType) {
|
|
92
|
+
if (pending.onFrame) {
|
|
93
|
+
try {
|
|
94
|
+
pending.onFrame(responseType, frame);
|
|
95
|
+
}
|
|
96
|
+
catch (error) {
|
|
97
|
+
pending.reject(error);
|
|
98
|
+
this.pending.delete(reqId);
|
|
99
|
+
}
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
pending.reject(new Error(`BM mismatched response: expected 0x${pending.cmd.toString(16)} got 0x${responseType.toString(16)}`));
|
|
60
103
|
this.pending.delete(reqId);
|
|
61
104
|
continue;
|
|
62
105
|
}
|
|
@@ -64,19 +107,30 @@ export class ViceClient {
|
|
|
64
107
|
this.pending.delete(reqId);
|
|
65
108
|
}
|
|
66
109
|
}
|
|
67
|
-
send(cmd, body) {
|
|
110
|
+
send(cmd, body, options) {
|
|
111
|
+
if (!this.socket || this.socket.destroyed) {
|
|
112
|
+
return Promise.reject(new Error("VICE monitor socket is not connected"));
|
|
113
|
+
}
|
|
68
114
|
const reqId = this.nextReqId++;
|
|
69
|
-
const
|
|
70
|
-
const header = Buffer.alloc(
|
|
115
|
+
const payload = body ?? Buffer.alloc(0);
|
|
116
|
+
const header = Buffer.alloc(11);
|
|
71
117
|
header[0] = 0x02; // STX
|
|
72
118
|
header[1] = 0x02; // API v2
|
|
73
|
-
header.writeUInt32LE(
|
|
119
|
+
header.writeUInt32LE(payload.length, 2);
|
|
74
120
|
header.writeUInt32LE(reqId, 6);
|
|
75
121
|
header[10] = cmd;
|
|
76
|
-
const packet = Buffer.concat([header,
|
|
77
|
-
const
|
|
122
|
+
const packet = Buffer.concat([header, payload]);
|
|
123
|
+
const expectedCmd = options?.responseType ?? cmd;
|
|
124
|
+
const promise = new Promise((resolve, reject) => {
|
|
125
|
+
this.pending.set(reqId, {
|
|
126
|
+
cmd: expectedCmd,
|
|
127
|
+
resolve,
|
|
128
|
+
reject,
|
|
129
|
+
onFrame: options?.onFrame,
|
|
130
|
+
});
|
|
131
|
+
});
|
|
78
132
|
this.socket.write(packet);
|
|
79
|
-
return
|
|
133
|
+
return promise;
|
|
80
134
|
}
|
|
81
135
|
async info() { await this.send(0x85); }
|
|
82
136
|
async resetSoft() { await this.send(0xCC, Buffer.from([0x00])); }
|
|
@@ -120,4 +174,310 @@ export class ViceClient {
|
|
|
120
174
|
const body = Buffer.concat([Buffer.from([bytes.length & 0xff]), bytes]);
|
|
121
175
|
await this.send(0x72, body);
|
|
122
176
|
}
|
|
177
|
+
async checkpointGet(id) {
|
|
178
|
+
const body = Buffer.alloc(4);
|
|
179
|
+
body.writeUInt32LE(id >>> 0, 0);
|
|
180
|
+
const frame = await this.send(0x11, body, { responseType: 0x11 });
|
|
181
|
+
return this.parseCheckpoint(frame);
|
|
182
|
+
}
|
|
183
|
+
async checkpointCreate(options) {
|
|
184
|
+
const start = options.start & 0xffff;
|
|
185
|
+
const end = (options.end ?? options.start) & 0xffff;
|
|
186
|
+
const stop = options.stopOnHit !== false;
|
|
187
|
+
const enabled = options.enabled !== false;
|
|
188
|
+
const ops = options.operations ?? { execute: true };
|
|
189
|
+
const mask = (ops.load ? 0x01 : 0) | (ops.store ? 0x02 : 0) | (ops.execute === false ? 0 : 0x04);
|
|
190
|
+
const temporary = options.temporary === true;
|
|
191
|
+
const memspace = options.memspace ?? 0;
|
|
192
|
+
const hasMemspace = options.memspace !== undefined;
|
|
193
|
+
const body = Buffer.alloc(hasMemspace ? 9 : 8);
|
|
194
|
+
body.writeUInt16LE(start, 0);
|
|
195
|
+
body.writeUInt16LE(end, 2);
|
|
196
|
+
body[4] = stop ? 1 : 0;
|
|
197
|
+
body[5] = enabled ? 1 : 0;
|
|
198
|
+
body[6] = mask;
|
|
199
|
+
body[7] = temporary ? 1 : 0;
|
|
200
|
+
if (hasMemspace) {
|
|
201
|
+
body[8] = memspace & 0xff;
|
|
202
|
+
}
|
|
203
|
+
const frame = await this.send(0x12, body, { responseType: 0x11 });
|
|
204
|
+
return this.parseCheckpoint(frame);
|
|
205
|
+
}
|
|
206
|
+
async checkpointDelete(id) {
|
|
207
|
+
const body = Buffer.alloc(4);
|
|
208
|
+
body.writeUInt32LE(id >>> 0, 0);
|
|
209
|
+
await this.send(0x13, body);
|
|
210
|
+
}
|
|
211
|
+
async checkpointToggle(id, enabled) {
|
|
212
|
+
const body = Buffer.alloc(5);
|
|
213
|
+
body.writeUInt32LE(id >>> 0, 0);
|
|
214
|
+
body[4] = enabled ? 1 : 0;
|
|
215
|
+
await this.send(0x15, body);
|
|
216
|
+
}
|
|
217
|
+
async checkpointSetCondition(id, expression) {
|
|
218
|
+
const expr = Buffer.from(expression ?? "", "utf8");
|
|
219
|
+
if (expr.length > 255) {
|
|
220
|
+
throw new Error("Condition expressions are limited to 255 bytes");
|
|
221
|
+
}
|
|
222
|
+
const body = Buffer.alloc(5 + expr.length);
|
|
223
|
+
body.writeUInt32LE(id >>> 0, 0);
|
|
224
|
+
body[4] = expr.length & 0xff;
|
|
225
|
+
expr.copy(body, 5);
|
|
226
|
+
await this.send(0x22, body);
|
|
227
|
+
}
|
|
228
|
+
async checkpointList() {
|
|
229
|
+
const checkpoints = [];
|
|
230
|
+
await this.send(0x14, undefined, {
|
|
231
|
+
responseType: 0x14,
|
|
232
|
+
onFrame: (type, frame) => {
|
|
233
|
+
if (type === 0x11) {
|
|
234
|
+
checkpoints.push(this.parseCheckpoint(frame));
|
|
235
|
+
}
|
|
236
|
+
},
|
|
237
|
+
});
|
|
238
|
+
return checkpoints;
|
|
239
|
+
}
|
|
240
|
+
async registersAvailable(memspace = 0) {
|
|
241
|
+
const frame = await this.send(0x83, Buffer.from([memspace & 0xff]), { responseType: 0x83 });
|
|
242
|
+
return this.parseRegisterInfo(frame);
|
|
243
|
+
}
|
|
244
|
+
async registersGet(memspace = 0) {
|
|
245
|
+
const frame = await this.send(0x31, Buffer.from([memspace & 0xff]), { responseType: 0x31 });
|
|
246
|
+
return this.parseRegisterValues(frame);
|
|
247
|
+
}
|
|
248
|
+
async registersSet(writes, options) {
|
|
249
|
+
if (!writes || writes.length === 0) {
|
|
250
|
+
throw new Error("At least one register write must be provided");
|
|
251
|
+
}
|
|
252
|
+
const memspace = options?.memspace ?? 0;
|
|
253
|
+
const metadata = options?.metadata ?? (await this.registersAvailable(memspace));
|
|
254
|
+
const entries = [];
|
|
255
|
+
for (const write of writes) {
|
|
256
|
+
const target = this.resolveRegisterTarget(write, metadata);
|
|
257
|
+
const value = write.value & 0xffff;
|
|
258
|
+
const entry = Buffer.alloc(4);
|
|
259
|
+
entry[0] = 3;
|
|
260
|
+
entry[1] = target.id & 0xff;
|
|
261
|
+
entry.writeUInt16LE(value, 2);
|
|
262
|
+
entries.push(entry);
|
|
263
|
+
}
|
|
264
|
+
const header = Buffer.alloc(3);
|
|
265
|
+
header[0] = memspace & 0xff;
|
|
266
|
+
header.writeUInt16LE(entries.length & 0xffff, 1);
|
|
267
|
+
const frame = await this.send(0x32, Buffer.concat([header, ...entries]), { responseType: 0x31 });
|
|
268
|
+
return this.parseRegisterValues(frame);
|
|
269
|
+
}
|
|
270
|
+
async stepInstructions(count = 1, options) {
|
|
271
|
+
const body = Buffer.alloc(3);
|
|
272
|
+
body[0] = options?.stepOver ? 1 : 0;
|
|
273
|
+
body.writeUInt16LE(Math.max(1, Math.min(count, 0xffff)), 1);
|
|
274
|
+
await this.send(0x71, body);
|
|
275
|
+
}
|
|
276
|
+
async stepReturn() {
|
|
277
|
+
await this.send(0x73);
|
|
278
|
+
}
|
|
279
|
+
async displayGet(options) {
|
|
280
|
+
const alt = options?.alternateCanvas ? 1 : 0;
|
|
281
|
+
const format = options?.format ?? 0;
|
|
282
|
+
const frame = await this.send(0x84, Buffer.from([alt & 0xff, format & 0xff]), { responseType: 0x84 });
|
|
283
|
+
return this.parseDisplay(frame);
|
|
284
|
+
}
|
|
285
|
+
async resourceGet(name) {
|
|
286
|
+
const trimmed = name.trim();
|
|
287
|
+
if (!trimmed) {
|
|
288
|
+
throw new Error("Resource name must not be empty");
|
|
289
|
+
}
|
|
290
|
+
const nameBytes = Buffer.from(trimmed, "utf8");
|
|
291
|
+
if (nameBytes.length === 0 || nameBytes.length > 255) {
|
|
292
|
+
throw new Error("Resource names must be 1-255 bytes");
|
|
293
|
+
}
|
|
294
|
+
const body = Buffer.concat([Buffer.from([nameBytes.length & 0xff]), nameBytes]);
|
|
295
|
+
const frame = await this.send(0x51, body, { responseType: 0x51 });
|
|
296
|
+
return this.parseResource(frame);
|
|
297
|
+
}
|
|
298
|
+
async resourceSet(name, value) {
|
|
299
|
+
const trimmed = name.trim();
|
|
300
|
+
if (!trimmed) {
|
|
301
|
+
throw new Error("Resource name must not be empty");
|
|
302
|
+
}
|
|
303
|
+
const nameBytes = Buffer.from(trimmed, "utf8");
|
|
304
|
+
if (nameBytes.length === 0 || nameBytes.length > 255) {
|
|
305
|
+
throw new Error("Resource names must be 1-255 bytes");
|
|
306
|
+
}
|
|
307
|
+
let body;
|
|
308
|
+
if (typeof value === "number") {
|
|
309
|
+
const header = Buffer.from([1, nameBytes.length & 0xff]);
|
|
310
|
+
const intPayload = Buffer.alloc(4);
|
|
311
|
+
intPayload.writeInt32LE(value | 0, 0);
|
|
312
|
+
body = Buffer.concat([header, nameBytes, Buffer.from([4]), intPayload]);
|
|
313
|
+
}
|
|
314
|
+
else {
|
|
315
|
+
const valueBytes = Buffer.from(value, "utf8");
|
|
316
|
+
if (valueBytes.length > 255) {
|
|
317
|
+
throw new Error("String resource values are limited to 255 bytes");
|
|
318
|
+
}
|
|
319
|
+
const header = Buffer.from([0, nameBytes.length & 0xff]);
|
|
320
|
+
body = Buffer.concat([header, nameBytes, Buffer.from([valueBytes.length & 0xff]), valueBytes]);
|
|
321
|
+
}
|
|
322
|
+
await this.send(0x52, body);
|
|
323
|
+
}
|
|
324
|
+
parseCheckpoint(frame) {
|
|
325
|
+
const body = frame.subarray(12);
|
|
326
|
+
if (body.length < 23) {
|
|
327
|
+
throw new Error("Checkpoint response too short");
|
|
328
|
+
}
|
|
329
|
+
const id = body.readUInt32LE(0);
|
|
330
|
+
const hit = body[4] === 1;
|
|
331
|
+
const start = body.readUInt16LE(5);
|
|
332
|
+
const end = body.readUInt16LE(7);
|
|
333
|
+
const stopOnHit = body[9] === 1;
|
|
334
|
+
const enabled = body[10] === 1;
|
|
335
|
+
const mask = body[11] ?? 0;
|
|
336
|
+
const temporary = body[12] === 1;
|
|
337
|
+
const hitCount = body.readUInt32LE(13);
|
|
338
|
+
const ignoreCount = body.readUInt32LE(17);
|
|
339
|
+
const hasCondition = body[21] === 1;
|
|
340
|
+
const memspace = (body[22] ?? 0) & 0xff;
|
|
341
|
+
return {
|
|
342
|
+
id,
|
|
343
|
+
hit,
|
|
344
|
+
start,
|
|
345
|
+
end,
|
|
346
|
+
stopOnHit,
|
|
347
|
+
enabled,
|
|
348
|
+
temporary,
|
|
349
|
+
operations: {
|
|
350
|
+
load: (mask & 0x01) !== 0,
|
|
351
|
+
store: (mask & 0x02) !== 0,
|
|
352
|
+
execute: (mask & 0x04) !== 0,
|
|
353
|
+
},
|
|
354
|
+
hitCount,
|
|
355
|
+
ignoreCount,
|
|
356
|
+
hasCondition,
|
|
357
|
+
memspace: memspace,
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
parseRegisterValues(frame) {
|
|
361
|
+
const body = frame.subarray(12);
|
|
362
|
+
if (body.length < 2)
|
|
363
|
+
return [];
|
|
364
|
+
const count = body.readUInt16LE(0);
|
|
365
|
+
const values = [];
|
|
366
|
+
let offset = 2;
|
|
367
|
+
for (let i = 0; i < count && offset < body.length; i += 1) {
|
|
368
|
+
const entrySize = body[offset] ?? 0;
|
|
369
|
+
const entryLength = entrySize + 1;
|
|
370
|
+
if (entryLength < 3 || offset + entryLength > body.length) {
|
|
371
|
+
break;
|
|
372
|
+
}
|
|
373
|
+
const id = body[offset + 1] ?? 0;
|
|
374
|
+
const valueBytes = Math.max(0, entryLength - 2);
|
|
375
|
+
let value = 0;
|
|
376
|
+
if (valueBytes >= 2) {
|
|
377
|
+
value = body.readUInt16LE(offset + 2);
|
|
378
|
+
}
|
|
379
|
+
else if (valueBytes === 1) {
|
|
380
|
+
value = body[offset + 2] ?? 0;
|
|
381
|
+
}
|
|
382
|
+
values.push({ id, size: Math.max(1, valueBytes), value });
|
|
383
|
+
offset += entryLength;
|
|
384
|
+
}
|
|
385
|
+
return values;
|
|
386
|
+
}
|
|
387
|
+
parseRegisterInfo(frame) {
|
|
388
|
+
const body = frame.subarray(12);
|
|
389
|
+
if (body.length < 2)
|
|
390
|
+
return [];
|
|
391
|
+
const count = body.readUInt16LE(0);
|
|
392
|
+
const info = [];
|
|
393
|
+
let offset = 2;
|
|
394
|
+
for (let i = 0; i < count && offset < body.length; i += 1) {
|
|
395
|
+
const entrySize = body[offset] ?? 0;
|
|
396
|
+
const entryLength = entrySize + 1;
|
|
397
|
+
if (entryLength < 4 || offset + entryLength > body.length) {
|
|
398
|
+
break;
|
|
399
|
+
}
|
|
400
|
+
const id = body[offset + 1] ?? 0;
|
|
401
|
+
const bits = body[offset + 2] ?? 0;
|
|
402
|
+
const nameLen = body[offset + 3] ?? 0;
|
|
403
|
+
const nameStart = offset + 4;
|
|
404
|
+
const nameEnd = Math.min(nameStart + nameLen, offset + entryLength);
|
|
405
|
+
const name = body.toString("utf8", nameStart, nameEnd);
|
|
406
|
+
const size = Math.max(1, Math.ceil(Math.min(bits || 8, 16) / 8));
|
|
407
|
+
info.push({ id, name, bits, size });
|
|
408
|
+
offset += entryLength;
|
|
409
|
+
}
|
|
410
|
+
return info;
|
|
411
|
+
}
|
|
412
|
+
parseDisplay(frame) {
|
|
413
|
+
const body = frame.subarray(12);
|
|
414
|
+
if (body.length < 17) {
|
|
415
|
+
throw new Error("Display response too short");
|
|
416
|
+
}
|
|
417
|
+
const infoLength = body.readUInt32LE(0);
|
|
418
|
+
if (body.length < 4 + infoLength) {
|
|
419
|
+
throw new Error("Display response length mismatch");
|
|
420
|
+
}
|
|
421
|
+
const debugWidth = body.readUInt16LE(4);
|
|
422
|
+
const debugHeight = body.readUInt16LE(6);
|
|
423
|
+
const offsetX = body.readUInt16LE(8);
|
|
424
|
+
const offsetY = body.readUInt16LE(10);
|
|
425
|
+
const innerWidth = body.readUInt16LE(12);
|
|
426
|
+
const innerHeight = body.readUInt16LE(14);
|
|
427
|
+
const bitsPerPixel = body[16] ?? 8;
|
|
428
|
+
const pixelLength = body.readUInt32LE(17);
|
|
429
|
+
const pixelStart = 4 + infoLength;
|
|
430
|
+
if (pixelStart + pixelLength > body.length) {
|
|
431
|
+
throw new Error("Display pixel buffer truncated");
|
|
432
|
+
}
|
|
433
|
+
const pixels = Buffer.from(body.subarray(pixelStart, pixelStart + pixelLength));
|
|
434
|
+
return {
|
|
435
|
+
debugWidth,
|
|
436
|
+
debugHeight,
|
|
437
|
+
offsetX,
|
|
438
|
+
offsetY,
|
|
439
|
+
innerWidth,
|
|
440
|
+
innerHeight,
|
|
441
|
+
bitsPerPixel,
|
|
442
|
+
pixels,
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
parseResource(frame) {
|
|
446
|
+
const body = frame.subarray(12);
|
|
447
|
+
if (body.length < 2) {
|
|
448
|
+
throw new Error("Resource response too short");
|
|
449
|
+
}
|
|
450
|
+
const type = body[0] ?? 0;
|
|
451
|
+
const length = body[1] ?? 0;
|
|
452
|
+
if (type === 0) {
|
|
453
|
+
const value = body.subarray(2, 2 + length).toString("utf8");
|
|
454
|
+
return { type: "string", value };
|
|
455
|
+
}
|
|
456
|
+
if (type === 1) {
|
|
457
|
+
if (length < 1 || length > 4) {
|
|
458
|
+
throw new Error("Unsupported integer resource width");
|
|
459
|
+
}
|
|
460
|
+
const temp = Buffer.alloc(4);
|
|
461
|
+
body.subarray(2, 2 + length).copy(temp, 0);
|
|
462
|
+
return { type: "int", value: temp.readInt32LE(0) };
|
|
463
|
+
}
|
|
464
|
+
throw new Error(`Unsupported resource type 0x${type.toString(16)}`);
|
|
465
|
+
}
|
|
466
|
+
resolveRegisterTarget(write, metadata) {
|
|
467
|
+
if (write.id !== undefined) {
|
|
468
|
+
const entry = metadata.find((meta) => meta.id === write.id);
|
|
469
|
+
if (!entry) {
|
|
470
|
+
throw new Error(`Unknown register id ${write.id}`);
|
|
471
|
+
}
|
|
472
|
+
return entry;
|
|
473
|
+
}
|
|
474
|
+
if (write.name) {
|
|
475
|
+
const entry = metadata.find((meta) => meta.name.toLowerCase() === write.name.toLowerCase());
|
|
476
|
+
if (!entry) {
|
|
477
|
+
throw new Error(`Unknown register name ${write.name}`);
|
|
478
|
+
}
|
|
479
|
+
return entry;
|
|
480
|
+
}
|
|
481
|
+
throw new Error("Register write requires id or name");
|
|
482
|
+
}
|
|
123
483
|
}
|
package/doc/developer.md
CHANGED
|
@@ -11,28 +11,52 @@ Focused reference for maintainers and contributors. User-facing setup lives in [
|
|
|
11
11
|
Install once:
|
|
12
12
|
|
|
13
13
|
```bash
|
|
14
|
+
./build install # install via the project build helper
|
|
15
|
+
# or directly:
|
|
14
16
|
npm install # reliable everywhere
|
|
15
|
-
# or
|
|
16
17
|
bun install # faster workflow (respects package-lock)
|
|
17
18
|
```
|
|
18
19
|
|
|
19
20
|
## 2. Core Workflows
|
|
20
21
|
|
|
22
|
+
Most day-to-day tasks are run through the [`./build`](../build) helper at the project root. It wraps every `npm run` script behind a consistent, self-documented interface.
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
./build --help # full command reference
|
|
26
|
+
```
|
|
27
|
+
|
|
21
28
|
| Task | Command |
|
|
22
29
|
| --- | --- |
|
|
23
30
|
| Launch MCP server (TS-aware) | `npm start` |
|
|
24
31
|
| Run TypeScript entry directly | `npm run mcp` (Bun) · `npm run mcp:node` (dist only) |
|
|
25
|
-
| Build + refresh generated docs |
|
|
26
|
-
| Tests (mock backend) |
|
|
27
|
-
| Tests against hardware |
|
|
28
|
-
|
|
|
29
|
-
|
|
|
30
|
-
|
|
|
31
|
-
|
|
|
32
|
+
| Build + refresh generated docs | `./build build` |
|
|
33
|
+
| Tests (mock backend) | `./build test` |
|
|
34
|
+
| Tests against hardware | `./build test --real [--base-url http://host]` |
|
|
35
|
+
| Full test matrix | `./build test:matrix` |
|
|
36
|
+
| Coverage report | `./build coverage` (merged matrix LCOV in `coverage/lcov.info`) |
|
|
37
|
+
| End-to-end smoke (local/npm) | `./build check:run-local` · `./build check:run-npm` |
|
|
38
|
+
| Node-only sanity | `./build check:node-compat` |
|
|
39
|
+
| VICE smoke (readiness + HELLO) | `./build vice:smoke` |
|
|
32
40
|
|
|
33
41
|
`scripts/invoke-bun.mjs` automatically delegates npm scripts to Bun when available; stay on the npm variants if Bun is not installed.
|
|
34
42
|
|
|
35
|
-
|
|
43
|
+
### Test Matrix
|
|
44
|
+
|
|
45
|
+
| Platform | Target | Command | Environment |
|
|
46
|
+
| --- | --- | --- | --- |
|
|
47
|
+
| c64u | mock | `./build test --platform c64u --target mock` | `C64_MODE=c64u C64_TEST_TARGET=mock` |
|
|
48
|
+
| c64u | device | `./build test --platform c64u --target device --base-url http://c64u` | `C64_MODE=c64u C64_TEST_TARGET=real C64_TEST_BASE_URL=http://c64u` |
|
|
49
|
+
| vice | mock | `./build test --platform vice --target mock` | `C64_MODE=vice VICE_TEST_TARGET=mock` |
|
|
50
|
+
| vice | device | `./build test --platform vice --target device` | `C64_MODE=vice VICE_TEST_TARGET=vice` |
|
|
51
|
+
| mixed | aggregate (no real c64u) | `./build test:matrix` | see individual rows |
|
|
52
|
+
|
|
53
|
+
The runner still accepts legacy flags (`--mock`, `--real`) for c64u workflows, but the explicit matrix keeps cross-platform runs symmetrical.
|
|
54
|
+
|
|
55
|
+
`./build coverage` executes the same three matrix legs, then merges the resulting LCOV reports while filtering to the same source scope configured in `.c8rc.json`. Use `./build coverage:single --platform <p> --target <t>` for a one-leg Bun report when debugging a local coverage regression.
|
|
56
|
+
|
|
57
|
+
> **Note**: Only suites that explicitly opt-in touch real hardware. Today that is `test/device.test.mjs` (real VICE via `target=device`) and the "C64Client against real C64" block in `test/c64Client.test.mjs` when `C64_TEST_TARGET=real`; all other tests continue to run against mocks even under a hardware matrix leg.
|
|
58
|
+
|
|
59
|
+
Subsequent `./build build` invocations reuse the incremental cache stored in `dist/.tsbuildinfo`; delete that file (or `dist/`) for a fully clean rebuild when needed.
|
|
36
60
|
|
|
37
61
|
## 3. Repository Layout (Essentials)
|
|
38
62
|
|
|
@@ -52,67 +76,81 @@ Subsequent `npm run build` invocations reuse the incremental cache stored in `di
|
|
|
52
76
|
|
|
53
77
|
- **Tools**: Implement under `src/tools/<domain>/`, export from [`src/tools/registry/index.ts`](../src/tools/registry/index.ts). Share helpers via [`src/tools/registry/utils.ts`](../src/tools/registry/utils.ts). Add coverage in [`test/`](../test/).
|
|
54
78
|
- **Prompts**: Author in [`src/prompts/`](../src/prompts/), register via [`src/prompts/registry.ts`](../src/prompts/registry.ts), mirror description updates in [AGENTS.md](../AGENTS.md) and `.github/prompts/`.
|
|
55
|
-
- **REST surface**: Keep [`doc/c64u/c64-openapi.yaml`](c64u/c64-openapi.yaml) current. Regenerate the typed client with
|
|
56
|
-
- **Docs**:
|
|
79
|
+
- **REST surface**: Keep [`doc/c64u/c64-openapi.yaml`](c64u/c64-openapi.yaml) current. Regenerate the typed client with `./build api:generate` when endpoints change.
|
|
80
|
+
- **Docs**: `./build build` calls [`scripts/update-readme.ts`](../scripts/update-readme.ts); never hand-edit the `<!-- AUTO-GENERATED:MCP-DOCS-* -->` block in the README.
|
|
57
81
|
|
|
58
82
|
## 5. Configuration & Backends
|
|
59
83
|
|
|
60
|
-
Resolution order: `C64BRIDGE_CONFIG` → `./.c64bridge.json` → `~/.c64bridge.json` → defaults (`host=c64u`, `port=80`). Supports hardware (`c64u`) and
|
|
84
|
+
Resolution order: `C64BRIDGE_CONFIG` → `./.c64bridge.json` → `~/.c64bridge.json` → defaults (`host=c64u`, `port=80`). Supports hardware (`c64u`) and phase-one VICE. For secured Ultimate firmware, set `c64u.networkPassword` in the config file to send `X-Password` on every REST request.
|
|
85
|
+
|
|
86
|
+
The authoritative VICE contract lives in [doc/vice/support-matrix.md](vice/support-matrix.md). Treat that file plus the generated README compatibility tables as release-blocking artifacts: if runtime, tests, and docs disagree, the contract is broken.
|
|
61
87
|
|
|
62
88
|
Key env flags:
|
|
63
89
|
|
|
64
90
|
- `C64_MODE=c64u|vice` — force backend
|
|
65
91
|
- `LOG_LEVEL=debug` — verbose logging (stderr)
|
|
92
|
+
- `C64BRIDGE_DIAGNOSTICS_DIR=/path` — persistent NDJSON crash and request diagnostics (default: `~/.c64bridge/diagnostics`)
|
|
93
|
+
- `C64BRIDGE_DISABLE_DIAGNOSTICS=1` — disable persistent diagnostics if they interfere with a constrained environment
|
|
66
94
|
- `C64_TEST_TARGET` / `C64_TEST_BASE_URL` — influence test harness
|
|
95
|
+
- `VICE_TEST_TARGET=mock|vice` — test selection for VICE (default: auto-detect real VICE; set `mock` to run against the BM stub during tests)
|
|
67
96
|
|
|
68
|
-
|
|
97
|
+
Managed VICE launch is currently Linux/X11-oriented. The launcher and smoke workflow assume Unix facilities such as `Xvfb`, `/tmp/.X11-unix`, and `/dev/null`; on other hosts, connect to an already running Binary Monitor endpoint or use the mock backend instead of assuming supervised startup will work.
|
|
98
|
+
|
|
99
|
+
For MCP stability work, always check the latest file under `~/.c64bridge/diagnostics/` before assuming a VS Code or Copilot crash came from repository logic. The file captures MCP request flow plus VICE/Xvfb stderr tails, which is usually enough to separate a repo fault from an editor or extension-host crash.
|
|
100
|
+
|
|
101
|
+
Public VICE support excludes mock-only behaviors. If a capability only works in the BM stub or an internal backend experiment, keep it out of grouped tool support tables until it is explicitly promoted in [doc/vice/support-matrix.md](vice/support-matrix.md).
|
|
69
102
|
|
|
70
103
|
## 6. RAG Maintenance
|
|
71
104
|
|
|
72
105
|
- Indices live under [`data/embeddings_*.json`](../data/)
|
|
73
|
-
- Rebuild:
|
|
74
|
-
- Fetch external sources:
|
|
75
|
-
- Discover sources (experimental):
|
|
106
|
+
- Rebuild: `./build rag:rebuild`
|
|
107
|
+
- Fetch external sources: `./build rag:fetch` (writes to [`external/`](../external/))
|
|
108
|
+
- Discover sources (experimental): `./build rag:discover` with `GITHUB_TOKEN`
|
|
76
109
|
|
|
77
110
|
Environment knobs: `RAG_EMBEDDINGS_DIR`, `RAG_BUILD_ON_START`, `RAG_REINDEX_INTERVAL_MS`, `RAG_DOC_FILES`.
|
|
78
111
|
|
|
79
112
|
## 7. Optional Services
|
|
80
113
|
|
|
81
|
-
- **HTTP bridge**: Disabled by default; enable with `npm start -- --http [port]` for manual curl experiments. Details in [`doc/troubleshooting-mcp.md`](troubleshooting-mcp.md).
|
|
114
|
+
- **HTTP bridge**: Disabled by default; enable with `npm start -- --http [port]` for manual curl experiments (the MCP server start is not managed by `./build`). Details in [`doc/troubleshooting-mcp.md`](troubleshooting-mcp.md).
|
|
82
115
|
- **Docker image**: [`Dockerfile`](../Dockerfile) builds Ubuntu 24.04 + Node 24 + Bun for reproducible environments.
|
|
83
116
|
- **Audio pipeline**: SID analysis uses [`naudiodon`](https://www.npmjs.com/package/naudiodon); see [`src/audio/`](../src/audio/) and tests like [`test/audioAnalysis.test.mjs`](../test/audioAnalysis.test.mjs).
|
|
84
117
|
|
|
85
118
|
## 8. Release & Packaging
|
|
86
119
|
|
|
87
|
-
-
|
|
88
|
-
-
|
|
89
|
-
-
|
|
120
|
+
- `./build check` — build + test matrix in one pass
|
|
121
|
+
- `./build changelog` — update CHANGELOG draft
|
|
122
|
+
- `./build release --version <version>` — bump version, regenerate manifest, prepend changelog notes
|
|
90
123
|
- Published package ships [`dist/`](../dist/), [`doc/`](../doc/), [`data/`](../data/), [`scripts/`](../scripts/), [`generated/`](../generated/), and [`mcp.json`](../mcp.json)
|
|
91
124
|
|
|
92
125
|
## 9. Troubleshooting Cheatsheet
|
|
93
126
|
|
|
94
|
-
- Missing entrypoint? Ensure dev deps are installed or run
|
|
127
|
+
- Missing entrypoint? Ensure dev deps are installed or run `./build build` so [`dist/index.js`](../dist/index.js) exists.
|
|
95
128
|
- Tool not exposed? Confirm registry wiring (`src/tools/registry/index.ts`) and rebuild.
|
|
96
129
|
- Real-device tests flaky? Verify `C64_TEST_BASE_URL` reachability; replay curl probes while the HTTP bridge is active.
|
|
97
|
-
- Empty RAG answers? Rebuild embeddings (
|
|
130
|
+
- Empty RAG answers? Rebuild embeddings (`./build rag:rebuild`) and confirm `RAG_EMBEDDINGS_DIR` points at committed data.
|
|
98
131
|
- Logs quiet? Remember all server logs emit to stderr to keep stdout dedicated to MCP.
|
|
99
132
|
|
|
100
133
|
Stay in lockstep with the [README](../README.md) and [`AGENTS.md`](../AGENTS.md) when introducing features so external docs remain accurate.
|
|
134
|
+
|
|
101
135
|
### VICE smoke test (Binary Monitor)
|
|
102
136
|
|
|
103
137
|
Quick sanity that VICE is reachable and BASIC readiness + simple run works end‑to‑end.
|
|
104
138
|
|
|
105
139
|
```bash
|
|
106
|
-
|
|
140
|
+
./build vice:smoke
|
|
107
141
|
```
|
|
108
142
|
|
|
109
143
|
Env knobs:
|
|
110
144
|
|
|
111
|
-
- `VICE_BINARY=/path/to/x64sc` — pick emulator binary
|
|
145
|
+
- `VICE_BINARY=/path/to/x64sc` — pick emulator binary on Unix-like hosts
|
|
112
146
|
- `VICE_VISIBLE=1` — show the VICE window (recommended during dev)
|
|
113
147
|
- `VICE_WARP=0` — disable warp so you can observe visible output
|
|
114
148
|
- `VICE_KEEP_OPEN=1` — keep window open after success
|
|
115
149
|
(The smoke test registers exit/signal handlers and tears down spawned processes; with `VICE_KEEP_OPEN=1` only your visible VICE is left running.)
|
|
150
|
+
- `VICE_TEST_TARGET=mock` — run the smoke test against the BM stub instead of launching VICE (useful on CI or platforms without VICE installed)
|
|
151
|
+
- `FORCE_XVFB=1` — run headless via Xvfb even if a display is detected (default on CI)
|
|
152
|
+
- `DISABLE_XVFB=1` — skip Xvfb and use the current `$DISPLAY` even in headless environments
|
|
153
|
+
- `VICE_XVFB_DISPLAY=:nn` — override the Xvfb display number (defaults to `:99`)
|
|
116
154
|
|
|
117
155
|
Implementation tip: programmatic shutdown
|
|
118
156
|
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"prompts": {
|
|
3
|
+
"c64u": "c64u: write a small BASIC program that clears the screen and prints HELLO C64U",
|
|
4
|
+
"vice": "vice: write a small BASIC program that clears the screen and prints HELLO VICE"
|
|
5
|
+
},
|
|
6
|
+
"ocr": {
|
|
7
|
+
"c64u": {
|
|
8
|
+
"expected": "HELLO C64U",
|
|
9
|
+
"recognized": "HELLO C64U",
|
|
10
|
+
"origin": {
|
|
11
|
+
"x": 32,
|
|
12
|
+
"y": 35
|
|
13
|
+
},
|
|
14
|
+
"score": 0,
|
|
15
|
+
"characterScores": [
|
|
16
|
+
0,
|
|
17
|
+
0,
|
|
18
|
+
0,
|
|
19
|
+
0,
|
|
20
|
+
0,
|
|
21
|
+
0,
|
|
22
|
+
0,
|
|
23
|
+
0,
|
|
24
|
+
0,
|
|
25
|
+
0
|
|
26
|
+
]
|
|
27
|
+
},
|
|
28
|
+
"vice": {
|
|
29
|
+
"expected": "HELLO VICE",
|
|
30
|
+
"recognized": "HELLO VICE",
|
|
31
|
+
"origin": {
|
|
32
|
+
"x": 32,
|
|
33
|
+
"y": 35
|
|
34
|
+
},
|
|
35
|
+
"score": 0,
|
|
36
|
+
"characterScores": [
|
|
37
|
+
0,
|
|
38
|
+
0,
|
|
39
|
+
0,
|
|
40
|
+
0,
|
|
41
|
+
0,
|
|
42
|
+
0,
|
|
43
|
+
0,
|
|
44
|
+
0,
|
|
45
|
+
0,
|
|
46
|
+
0
|
|
47
|
+
]
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|