c64bridge 0.7.4 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. package/.github/prompts/README.md +14 -4
  2. package/.github/prompts/assembly-routine-session.prompt.md +9 -9
  3. package/.github/prompts/basic-program-session.prompt.md +11 -9
  4. package/.github/prompts/drive-management-session.prompt.md +9 -9
  5. package/.github/prompts/hello-world-session.prompt.md +17 -0
  6. package/.github/prompts/petscii-art-session.prompt.md +9 -9
  7. package/.github/prompts/play-classic-game.prompt.md +9 -9
  8. package/.github/prompts/pr-converge.prompt.md +13 -0
  9. package/.github/prompts/sid-music-session.prompt.md +9 -9
  10. package/AGENTS.md +59 -60
  11. package/README.md +550 -191
  12. package/data/assembly/assembly-spec.md +290 -1
  13. package/data/context/bootstrap.md +26 -2
  14. package/data/context/fast-paths.md +44 -0
  15. package/data/graphics/petscii-style-guide.md +6 -2
  16. package/data/graphics/sprite-charset-best-practices.md +5 -5
  17. package/dist/.tsbuildinfo +1 -1
  18. package/dist/c64Client.js +990 -26
  19. package/dist/config.js +5 -1
  20. package/dist/device.js +680 -57
  21. package/dist/diagnostics.js +371 -0
  22. package/dist/mcp/metadata.js +39 -0
  23. package/dist/mcp-server.js +170 -20
  24. package/dist/platform.js +10 -2
  25. package/dist/prompts/registry.js +166 -309
  26. package/dist/rag/init.js +37 -1
  27. package/dist/rag/knowledgeIndex.js +18 -6
  28. package/dist/rag/rateLimiter.js +2 -2
  29. package/dist/streamCapture.js +135 -0
  30. package/dist/tools/audio.js +3 -1
  31. package/dist/tools/debug.js +470 -0
  32. package/dist/tools/developer.js +10 -4
  33. package/dist/tools/graphics.js +183 -17
  34. package/dist/tools/machineControl.js +11 -5
  35. package/dist/tools/memory.js +19 -10
  36. package/dist/tools/meta/audio.js +240 -1
  37. package/dist/tools/meta/config.js +20 -28
  38. package/dist/tools/meta/configInventory.js +104 -0
  39. package/dist/tools/meta/diagnostics.js +45 -2
  40. package/dist/tools/meta/graphics.js +14 -6
  41. package/dist/tools/meta/index.js +1 -0
  42. package/dist/tools/meta/memory.js +20 -6
  43. package/dist/tools/meta/program.js +473 -0
  44. package/dist/tools/pollValidator.js +4 -2
  45. package/dist/tools/programRunners.js +8 -4
  46. package/dist/tools/registry/config.js +17 -0
  47. package/dist/tools/registry/disk.js +7 -0
  48. package/dist/tools/registry/drive.js +3 -0
  49. package/dist/tools/registry/graphics.js +111 -27
  50. package/dist/tools/registry/index.js +6 -0
  51. package/dist/tools/registry/memory.js +1 -0
  52. package/dist/tools/registry/platform.js +121 -0
  53. package/dist/tools/registry/program.js +26 -8
  54. package/dist/tools/registry/sound.js +96 -0
  55. package/dist/tools/registry/system.js +15 -2
  56. package/dist/tools/storage.js +1 -0
  57. package/dist/tools/streaming.js +1 -0
  58. package/dist/tools/types.js +38 -4
  59. package/dist/tools/vice.js +160 -0
  60. package/dist/vicBitmap.js +317 -0
  61. package/dist/vice/mockServer.js +439 -0
  62. package/dist/vice/process.js +310 -0
  63. package/dist/vice/readiness.js +18 -2
  64. package/dist/vice/viceClient.js +382 -22
  65. package/doc/developer.md +71 -25
  66. package/doc/img/backend-switch/hello-c64u.png +0 -0
  67. package/doc/img/backend-switch/hello-vice-and-c64u.png +0 -0
  68. package/doc/img/backend-switch/hello-vice.png +0 -0
  69. package/doc/img/backend-switch/metadata.json +50 -0
  70. package/doc/plans/vice-backend-switch/plan.md +486 -0
  71. package/doc/plans/vice-backend-switch-2/plan.md +48 -0
  72. package/doc/plans/vice-support/notes.md +1 -0
  73. package/doc/plans/vice-support/plan.md +5 -0
  74. package/doc/plans/vice-support/rollout.md +23 -17
  75. package/doc/reviews/vice-support-review.md +185 -0
  76. package/doc/troubleshooting-mcp.md +35 -6
  77. package/doc/vice/support-matrix.md +54 -0
  78. package/mcp/prompts.json +292 -0
  79. package/mcp/protocol-examples.json +46 -0
  80. package/mcp/resources.json +940 -0
  81. package/mcp/schemas/c64_config.schema.json +350 -0
  82. package/mcp/schemas/c64_debug.schema.json +380 -0
  83. package/mcp/schemas/c64_disk.schema.json +278 -0
  84. package/mcp/schemas/c64_drive.schema.json +120 -0
  85. package/mcp/schemas/c64_extract.schema.json +302 -0
  86. package/mcp/schemas/c64_graphics.schema.json +333 -0
  87. package/mcp/schemas/c64_memory.schema.json +155 -0
  88. package/mcp/schemas/c64_printer.schema.json +218 -0
  89. package/mcp/schemas/c64_program.schema.json +391 -0
  90. package/mcp/schemas/c64_rag.schema.json +67 -0
  91. package/mcp/schemas/c64_select_backend.schema.json +32 -0
  92. package/mcp/schemas/c64_sound.schema.json +644 -0
  93. package/mcp/schemas/c64_stream.schema.json +61 -0
  94. package/mcp/schemas/c64_system.schema.json +227 -0
  95. package/mcp/schemas/c64_vice.schema.json +63 -0
  96. package/mcp/server.json +15 -0
  97. package/mcp/tools.json +4517 -0
  98. package/mcp.json +90 -15
  99. package/package.json +11 -4
  100. package/scripts/capture-backend-switch-screenshots.mjs +276 -0
  101. package/scripts/generate-mcp-interface.ts +289 -0
  102. package/scripts/invoke-bun.mjs +67 -21
  103. package/scripts/mockC64Server.mjs +162 -6
  104. package/scripts/prepare-release.mjs +59 -2
  105. package/scripts/run-coverage.mjs +284 -0
  106. package/scripts/run-tests.mjs +95 -39
  107. package/scripts/run-tests.ts +378 -73
  108. package/scripts/update-readme.ts +147 -13
  109. package/server.json +94 -0
@@ -1,5 +1,5 @@
1
1
  /*
2
- * Minimal VICE Binary Monitor client with length-aware framing.
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
- this.socket.once("connect", () => resolve());
16
- this.socket.once("error", reject);
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
- for (const [, p] of this.pending)
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
- close() { try {
26
- this.socket?.destroy();
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 respType = frame[6];
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 — ignore
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 !== respType) {
59
- pending.reject(new Error(`BM mismatched response: expected 0x${pending.cmd.toString(16)} got 0x${respType.toString(16)}`));
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 b = body ?? Buffer.alloc(0);
70
- const header = Buffer.alloc(2 + 4 + 4 + 1);
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(b.length, 2);
119
+ header.writeUInt32LE(payload.length, 2);
74
120
  header.writeUInt32LE(reqId, 6);
75
121
  header[10] = cmd;
76
- const packet = Buffer.concat([header, b]);
77
- const p = new Promise((resolve, reject) => this.pending.set(reqId, { cmd, resolve, reject }));
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 p;
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 | `npm run build` |
26
- | Tests (mock backend) | `npm test` |
27
- | Tests against hardware | `npm test -- --real [--base-url=http://host]` |
28
- | Coverage report | `npm run coverage` (emits `coverage/lcov.info`, minimum 90%) |
29
- | End-to-end smoke (local/npm) | `npm run check:run-local` · `npm run check:run-npm` |
30
- | Node-only sanity | `npm run check:node-compat` |
31
- | VICE smoke (readiness + HELLO) | `npm run vice:smoke` |
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
- Subsequent `npm run build` invocations reuse the incremental cache stored in `dist/.tsbuildinfo`; delete that file (or `dist/`) for a fully clean rebuild when needed.
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,89 @@ 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 `npm run api:generate` when endpoints change.
56
- - **Docs**: `npm run build` calls [`scripts/update-readme.ts`](../scripts/update-readme.ts); never hand-edit the `<!-- AUTO-GENERATED:MCP-DOCS-* -->` block in the README.
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 experimental VICE (`vice.exe`). For secured Ultimate firmware, set `c64u.networkPassword` in the config file to send `X-Password` on every REST request.
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
- ### VICE smoke test (Binary Monitor)
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: `npm run rag:rebuild`
74
- - Fetch external sources: `npm run rag:fetch` (writes to [`external/`](../external/))
75
- - Discover sources (experimental): `npm run rag:discover` with `GITHUB_TOKEN`
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
- - `npm run check` — build + test in one pass (mock backend)
88
- - `npm run changelog:generate` — update CHANGELOG draft
89
- - `npm run release:prepare -- <version>` — pre-publish sanity checks
90
- - Published package ships [`dist/`](../dist/), [`doc/`](../doc/), [`data/`](../data/), [`scripts/`](../scripts/), [`generated/`](../generated/), and [`mcp.json`](../mcp.json)
120
+ - `./build check` — build + test matrix in one pass
121
+ - `./build changelog` — update CHANGELOG draft
122
+ - `./build release --version <version>` — bump version, refresh [`mcp.json`](../mcp.json) and [`server.json`](../server.json), prepend changelog notes
123
+ - Published package ships [`dist/`](../dist/), [`doc/`](../doc/), [`data/`](../data/), [`scripts/`](../scripts/), [`generated/`](../generated/), [`mcp.json`](../mcp.json), and [`server.json`](../server.json)
124
+
125
+ Tagging a release through the GitHub release UI triggers [`.github/workflows/release.yaml`](../.github/workflows/release.yaml), which publishes the npm package and then publishes the same version to the MCP Registry.
126
+
127
+ - Required GitHub secret: `NPM_TOKEN`
128
+ - No dedicated MCP Registry secret is required for the GitHub-driven flow; the workflow uses `mcp-publisher login github-oidc`
129
+ - The release workflow must grant `id-token: write` so GitHub Actions can mint the short-lived OIDC token used by `mcp-publisher`
130
+ - Keep [`package.json`](../package.json) `mcpName` aligned with [`server.json`](../server.json) `name`
131
+ - The root [`server.json`](../server.json) is the MCP Registry manifest; [`mcp/server.json`](../mcp/server.json) remains generated MCP interface output
91
132
 
92
133
  ## 9. Troubleshooting Cheatsheet
93
134
 
94
- - Missing entrypoint? Ensure dev deps are installed or run `npm run build` so [`dist/index.js`](../dist/index.js) exists.
135
+ - Missing entrypoint? Ensure dev deps are installed or run `./build build` so [`dist/index.js`](../dist/index.js) exists.
95
136
  - Tool not exposed? Confirm registry wiring (`src/tools/registry/index.ts`) and rebuild.
96
137
  - 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 (`npm run rag:rebuild`) and confirm `RAG_EMBEDDINGS_DIR` points at committed data.
138
+ - Empty RAG answers? Rebuild embeddings (`./build rag:rebuild`) and confirm `RAG_EMBEDDINGS_DIR` points at committed data.
98
139
  - Logs quiet? Remember all server logs emit to stderr to keep stdout dedicated to MCP.
99
140
 
100
141
  Stay in lockstep with the [README](../README.md) and [`AGENTS.md`](../AGENTS.md) when introducing features so external docs remain accurate.
142
+
101
143
  ### VICE smoke test (Binary Monitor)
102
144
 
103
145
  Quick sanity that VICE is reachable and BASIC readiness + simple run works end‑to‑end.
104
146
 
105
147
  ```bash
106
- npm run vice:smoke
148
+ ./build vice:smoke
107
149
  ```
108
150
 
109
151
  Env knobs:
110
152
 
111
- - `VICE_BINARY=/path/to/x64sc` — pick emulator binary
153
+ - `VICE_BINARY=/path/to/x64sc` — pick emulator binary on Unix-like hosts
112
154
  - `VICE_VISIBLE=1` — show the VICE window (recommended during dev)
113
155
  - `VICE_WARP=0` — disable warp so you can observe visible output
114
156
  - `VICE_KEEP_OPEN=1` — keep window open after success
115
157
  (The smoke test registers exit/signal handlers and tears down spawned processes; with `VICE_KEEP_OPEN=1` only your visible VICE is left running.)
158
+ - `VICE_TEST_TARGET=mock` — run the smoke test against the BM stub instead of launching VICE (useful on CI or platforms without VICE installed)
159
+ - `FORCE_XVFB=1` — run headless via Xvfb even if a display is detected (default on CI)
160
+ - `DISABLE_XVFB=1` — skip Xvfb and use the current `$DISPLAY` even in headless environments
161
+ - `VICE_XVFB_DISPLAY=:nn` — override the Xvfb display number (defaults to `:99`)
116
162
 
117
163
  Implementation tip: programmatic shutdown
118
164