c64bridge 1.0.4 → 1.0.6

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 (63) hide show
  1. package/README.md +2 -2
  2. package/data/embeddings_basic.json +1 -1
  3. package/data/embeddings_hardware.json +1 -1
  4. package/data/embeddings_mixed.json +1 -1
  5. package/data/embeddings_other.json +1 -1
  6. package/data/graphics/character-set.csv +64 -64
  7. package/data/sound/sidwave.md +113 -36
  8. package/data/sound/sidwave.schema.json +138 -28
  9. package/dist/.tsbuildinfo +1 -1
  10. package/dist/c64Client.js +144 -150
  11. package/dist/config.js +93 -39
  12. package/dist/device.js +252 -130
  13. package/dist/diagnostics.js +10 -2
  14. package/dist/knowledge.js +2 -1
  15. package/dist/mcp-server.js +12 -4
  16. package/dist/petscii.js +18 -15
  17. package/dist/petsciiArt.js +40 -14
  18. package/dist/rag/indexer.js +2 -1
  19. package/dist/sidwave.js +453 -61
  20. package/dist/sidwaveCompiler.js +1424 -275
  21. package/dist/tools/audio.js +97 -18
  22. package/dist/tools/debug.js +9 -9
  23. package/dist/tools/developer.js +7 -7
  24. package/dist/tools/errors.js +19 -1
  25. package/dist/tools/graphics.js +4 -4
  26. package/dist/tools/hex.js +9 -0
  27. package/dist/tools/input.js +21 -18
  28. package/dist/tools/machineControl.js +6 -6
  29. package/dist/tools/memory.js +11 -10
  30. package/dist/tools/meta/audio.js +8 -2
  31. package/dist/tools/meta/background.js +6 -1
  32. package/dist/tools/meta/filesystem.js +37 -10
  33. package/dist/tools/meta/graphics.js +11 -16
  34. package/dist/tools/meta/memory.js +16 -22
  35. package/dist/tools/meta/program.js +28 -9
  36. package/dist/tools/pollValidator.js +30 -2
  37. package/dist/tools/printer.js +4 -4
  38. package/dist/tools/programRunners.js +10 -9
  39. package/dist/tools/registry/disk.js +7 -5
  40. package/dist/tools/storage.js +21 -17
  41. package/dist/tools/streaming.js +2 -2
  42. package/dist/tools/translation/assembler.js +192 -57
  43. package/dist/tools/translation/basicTokenizer.js +44 -10
  44. package/dist/tools/translation/opcodeInventory.js +1 -1
  45. package/dist/tools/types.js +11 -4
  46. package/dist/tools/vice.js +1 -1
  47. package/dist/vicBitmap.js +16 -0
  48. package/dist/vice/mockServer.js +99 -8
  49. package/dist/vice/readiness.js +4 -9
  50. package/dist/vice/viceClient.js +34 -6
  51. package/mcp/schemas/c64_disk.schema.json +2 -2
  52. package/mcp/schemas/c64_input.schema.json +3 -3
  53. package/mcp/schemas/c64_program.schema.json +2 -2
  54. package/mcp/schemas/c64_system.schema.json +6 -4
  55. package/mcp/schemas/c64_vice.schema.json +3 -3
  56. package/mcp/server.json +1 -1
  57. package/mcp/tools.json +20 -14
  58. package/mcp.json +1 -1
  59. package/package.json +1 -1
  60. package/scripts/mockC64Server.mjs +113 -20
  61. package/scripts/run-coverage.mjs +31 -1
  62. package/scripts/update-readme.ts +3 -2
  63. package/server.json +2 -2
package/mcp/tools.json CHANGED
@@ -66,7 +66,7 @@
66
66
  "properties": {
67
67
  "type": {
68
68
  "type": "string",
69
- "description": "Assertion type",
69
+ "description": "Assertion type; sid_silent needs the C64 Ultimate audio stream (c64u backend).",
70
70
  "enum": [
71
71
  "screen_contains",
72
72
  "memory_equals",
@@ -468,7 +468,7 @@
468
468
  "properties": {
469
469
  "type": {
470
470
  "type": "string",
471
- "description": "Assertion type",
471
+ "description": "Assertion type; sid_silent needs the C64 Ultimate audio stream (c64u backend).",
472
472
  "enum": [
473
473
  "screen_contains",
474
474
  "memory_equals",
@@ -2872,8 +2872,9 @@
2872
2872
  },
2873
2873
  "name": {
2874
2874
  "type": "string",
2875
- "description": "Unique task name",
2876
- "minLength": 1
2875
+ "description": "Unique task name (letters, digits, spaces, '_', '-', '.'; must not start with '.' or a space).",
2876
+ "minLength": 1,
2877
+ "pattern": "^[A-Za-z0-9_][A-Za-z0-9_. -]*$"
2877
2878
  },
2878
2879
  "operation": {
2879
2880
  "type": "string",
@@ -3114,8 +3115,9 @@
3114
3115
  "properties": {
3115
3116
  "name": {
3116
3117
  "type": "string",
3117
- "description": "Unique task name",
3118
- "minLength": 1
3118
+ "description": "Unique task name (letters, digits, spaces, '_', '-', '.'; must not start with '.' or a space).",
3119
+ "minLength": 1,
3120
+ "pattern": "^[A-Za-z0-9_][A-Za-z0-9_. -]*$"
3119
3121
  },
3120
3122
  "operation": {
3121
3123
  "type": "string",
@@ -4481,7 +4483,7 @@
4481
4483
  "integer",
4482
4484
  "null"
4483
4485
  ],
4484
- "description": "Track count (D64 supports 35 or 40; DNP requires explicit tracks).",
4486
+ "description": "Track count (D64 supports 35 to 41; DNP requires explicit tracks).",
4485
4487
  "minimum": 1,
4486
4488
  "maximum": 255
4487
4489
  },
@@ -4783,7 +4785,7 @@
4783
4785
  "integer",
4784
4786
  "null"
4785
4787
  ],
4786
- "description": "Track count (D64 supports 35 or 40; DNP requires explicit tracks).",
4788
+ "description": "Track count (D64 supports 35 to 41; DNP requires explicit tracks).",
4787
4789
  "minimum": 1,
4788
4790
  "maximum": 255
4789
4791
  },
@@ -4947,6 +4949,10 @@
4947
4949
  "vice"
4948
4950
  ],
4949
4951
  "operationPlatforms": {
4952
+ "unmount": [
4953
+ "c64u",
4954
+ "u2"
4955
+ ],
4950
4956
  "file_info": [
4951
4957
  "c64u",
4952
4958
  "u2"
@@ -8073,7 +8079,7 @@
8073
8079
  },
8074
8080
  "name": {
8075
8081
  "type": "string",
8076
- "description": "VICE resource name (example: SidEngine, C64Model).",
8082
+ "description": "VICE resource name (example: SidEngine, SidModel).",
8077
8083
  "minLength": 1,
8078
8084
  "maxLength": 64,
8079
8085
  "pattern": "^[A-Z][A-Za-z0-9_.-]{0,63}$"
@@ -8108,7 +8114,7 @@
8108
8114
  },
8109
8115
  "name": {
8110
8116
  "type": "string",
8111
- "description": "VICE resource name (example: SidEngine, C64Model).",
8117
+ "description": "VICE resource name (example: SidEngine, SidModel).",
8112
8118
  "minLength": 1,
8113
8119
  "maxLength": 64,
8114
8120
  "pattern": "^[A-Z][A-Za-z0-9_.-]{0,63}$"
@@ -8138,7 +8144,7 @@
8138
8144
  },
8139
8145
  "name": {
8140
8146
  "type": "string",
8141
- "description": "VICE resource name (example: SidEngine, C64Model).",
8147
+ "description": "VICE resource name (example: SidEngine, SidModel).",
8142
8148
  "minLength": 1,
8143
8149
  "maxLength": 64,
8144
8150
  "pattern": "^[A-Z][A-Za-z0-9_.-]{0,63}$"
@@ -8390,7 +8396,7 @@
8390
8396
  "required": [
8391
8397
  "op"
8392
8398
  ],
8393
- "description": "Input operations: write_text, key, keyboard, joystick, release_all, state.\n\nOperations:\n- write_text: Send a text string to the keyboard buffer, with PETSCII token expansion. Required inputs: text. Optional inputs: delayMs.\n- key: Tap a single key or hold it for a duration. Required inputs: key. Optional inputs: durationMs, count.\n- joystick: Simulate joystick input. On C64U/U64 this uses machine:input; VICE uses Binary Monitor Joyport Set. Required inputs: port, controls, action. Optional inputs: durationMs.\n- keyboard: Send physical C64 keyboard matrix events through machine:input (a C64U firmware version that provides it, or U64 3.15+). Required inputs: inputs, transition.\n- release_all: Release every key and joystick control injected through machine:input (a C64U firmware version that provides it, or U64 3.15+). Required inputs: none.\n- state: Read the keys and joystick controls held through machine:input (a C64U firmware version that provides it, or U64 3.15+). Required inputs: none.",
8399
+ "description": "Input operations: write_text, key, keyboard, joystick, release_all, state.\n\nOperations:\n- write_text: Send a text string to the keyboard buffer, with PETSCII token expansion. Required inputs: text. Optional inputs: delayMs.\n- key: Tap a single key or hold it for a duration. Required inputs: key. Optional inputs: durationMs, count.\n- joystick: Simulate joystick input. On C64U/U64 this uses machine:input. On VICE it attaches VICE's I/O simulation joyport device while pressed; that device also pulls CIA lines 5-7 low, so port 1 produces phantom keys (e.g. SHIFT+C=) and port 2 reads $1E for up instead of $7E. Prefer port 2 on VICE and test bits 0-4. Required inputs: port, controls, action. Optional inputs: durationMs.\n- keyboard: Send physical C64 keyboard matrix events through machine:input (a C64U firmware version that provides it, or U64 3.15+). Required inputs: inputs, transition.\n- release_all: Release every key and joystick control injected through machine:input (a C64U firmware version that provides it, or U64 3.15+). Required inputs: none.\n- state: Read the keys and joystick controls held through machine:input (a C64U firmware version that provides it, or U64 3.15+). Required inputs: none.",
8394
8400
  "additionalProperties": false,
8395
8401
  "x-c64bridge-operations": [
8396
8402
  {
@@ -8486,7 +8492,7 @@
8486
8492
  },
8487
8493
  {
8488
8494
  "op": "joystick",
8489
- "description": "Simulate joystick input. On C64U/U64 this uses machine:input; VICE uses Binary Monitor Joyport Set.",
8495
+ "description": "Simulate joystick input. On C64U/U64 this uses machine:input. On VICE it attaches VICE's I/O simulation joyport device while pressed; that device also pulls CIA lines 5-7 low, so port 1 produces phantom keys (e.g. SHIFT+C=) and port 2 reads $1E for up instead of $7E. Prefer port 2 on VICE and test bits 0-4.",
8490
8496
  "required": [
8491
8497
  "port",
8492
8498
  "controls",
@@ -8497,7 +8503,7 @@
8497
8503
  ],
8498
8504
  "inputSchema": {
8499
8505
  "type": "object",
8500
- "description": "Simulate joystick input. On C64U/U64 this uses machine:input; VICE uses Binary Monitor Joyport Set.",
8506
+ "description": "Simulate joystick input. On C64U/U64 this uses machine:input. On VICE it attaches VICE's I/O simulation joyport device while pressed; that device also pulls CIA lines 5-7 low, so port 1 produces phantom keys (e.g. SHIFT+C=) and port 2 reads $1E for up instead of $7E. Prefer port 2 on VICE and test bits 0-4.",
8501
8507
  "properties": {
8502
8508
  "op": {
8503
8509
  "const": "joystick"
package/mcp.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "c64bridge",
3
- "version": "1.0.4",
3
+ "version": "1.0.6",
4
4
  "description": "Stdio-based Model Context Protocol (MCP) server that bridges Commodore 64 Ultimate hardware with AI agents.",
5
5
  "entry": "dist/index.js",
6
6
  "type": "stdio",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "c64bridge",
3
- "version": "1.0.4",
3
+ "version": "1.0.6",
4
4
  "mcpName": "io.github.chrisgleissner/c64bridge",
5
5
  "description": "C64 Bridge MCP server for Commodore 64 Ultimate hardware and VICE emulator control",
6
6
  "repository": {
@@ -250,6 +250,14 @@ function cancelHeartbeatOnWrite(state, address, length) {
250
250
  }
251
251
 
252
252
  function seedReadyPrompt(state) {
253
+ // After a reset the KERNAL clears screen RAM to spaces and BASIC sets up an
254
+ // empty program: TXTTAB=$0801, VARTAB/ARYTAB/STREND=$0803, FRETOP/MEMSIZ=$A000
255
+ // (as read from an Ultimate 64 after machine:reset).
256
+ state.memory.fill(toScreenCode(" "), 0x0400, 0x0400 + 1000);
257
+ state.memory.set([0x01, 0x08, 0x03, 0x08, 0x03, 0x08, 0x03, 0x08, 0x00, 0xa0], 0x2b);
258
+ state.memory.set([0x00, 0xa0], 0x37);
259
+ state.memory.set([0x00, 0x00], 0x0801);
260
+
253
261
  const text = "READY.";
254
262
  const buffer = new Uint8Array(text.length + 1);
255
263
  for (let i = 0; i < text.length; i += 1) {
@@ -259,6 +267,26 @@ function seedReadyPrompt(state) {
259
267
  state.memory.set(buffer, 0x0400);
260
268
  }
261
269
 
270
+ /**
271
+ * BASIC LINKPRG ($A533): walk the line chain from $0801 and rewrite each link
272
+ * pointer to the byte after the line's terminating zero. Stops at a zero link
273
+ * high byte, as the ROM does; the step limit only guards the mock.
274
+ */
275
+ function relinkBasicProgram(memory) {
276
+ let line = 0x0801;
277
+ for (let steps = 0; steps < 0x10000 && line < 0xfffb; steps += 1) {
278
+ if (memory[line + 1] === 0) return;
279
+ let y = 4;
280
+ do {
281
+ y += 1;
282
+ } while (line + y < 0xffff && memory[line + y] !== 0);
283
+ const next = line + y + 1;
284
+ memory[line] = next & 0xff;
285
+ memory[line + 1] = (next >> 8) & 0xff;
286
+ line = next;
287
+ }
288
+ }
289
+
262
290
  export async function startMockC64Server(options = {}) {
263
291
  const state = createInitialState();
264
292
  const streamRuntime = createStreamRuntime();
@@ -269,9 +297,14 @@ export async function startMockC64Server(options = {}) {
269
297
  // seed memory with READY prompt at $0400 using screen codes
270
298
  seedReadyPrompt(state);
271
299
 
300
+ // Firmware drive ids (route_drives.h DriveToSubsys): a and b are the
301
+ // emulated drives, softiec the IEC file system; anything else is rejected.
302
+ const FIRMWARE_DRIVE_BUS_IDS = { a: 8, b: 9, softiec: 11 };
303
+
272
304
  function ensureDriveState(id) {
273
305
  if (!state.drives[id]) {
274
306
  state.drives[id] = {
307
+ busId: FIRMWARE_DRIVE_BUS_IDS[id] ?? null,
275
308
  mountedImage: null,
276
309
  mode: "1541",
277
310
  power: "off",
@@ -282,14 +315,20 @@ export async function startMockC64Server(options = {}) {
282
315
  return state.drives[id];
283
316
  }
284
317
 
285
- ensureDriveState("drive8");
318
+ function seedDrives() {
319
+ ensureDriveState("a");
320
+ ensureDriveState("b");
321
+ ensureDriveState("softiec");
322
+ }
323
+
324
+ seedDrives();
286
325
 
287
326
  function resetState() {
288
327
  stopAllStreams();
289
328
  const fresh = createInitialState();
290
329
  Object.assign(state, fresh);
291
330
  seedReadyPrompt(state);
292
- ensureDriveState("drive8");
331
+ seedDrives();
293
332
  }
294
333
 
295
334
  function stopAllStreams() {
@@ -401,18 +440,32 @@ export async function startMockC64Server(options = {}) {
401
440
  }
402
441
 
403
442
  if (method === "GET" && url === "/v1/drives") {
404
- const drives = {};
405
- for (const [driveId, driveState] of Object.entries(state.drives)) {
406
- drives[driveId] = {
407
- enabled: driveState.power !== "off",
408
- power: driveState.power,
409
- mode: driveState.mode,
410
- // Mirror the real firmware's DriveInfo shape (image_path is a
411
- // plain string), not the internal mount-params object.
412
- image_path: driveState.mountedImage?.image ?? null,
413
- type: driveState.mountedImage?.type ?? driveState.mode ?? null,
443
+ // Firmware shape (route_drives.cc drive_info, IecInterface::info): a list
444
+ // of single-key objects. Power is `enabled`; the mounted image is split
445
+ // into its directory (image_path) and file name (image_file). The soft
446
+ // IEC drive is listed as "IEC Drive" although its route id is softiec,
447
+ // and the printer is listed but is not a drive.
448
+ const splitImage = (image) => {
449
+ if (!image) return { image_file: "", image_path: "" };
450
+ const slash = image.lastIndexOf("/");
451
+ return { image_file: image.slice(slash + 1), image_path: slash > 0 ? image.slice(0, slash) : "/" };
452
+ };
453
+ const drives = ["a", "b"].map((driveId) => {
454
+ const driveState = state.drives[driveId];
455
+ return {
456
+ [driveId]: {
457
+ enabled: driveState.power !== "off",
458
+ bus_id: driveState.busId,
459
+ type: driveState.mode,
460
+ rom: `${driveState.mode}.rom`,
461
+ ...splitImage(driveState.mountedImage?.image),
462
+ },
414
463
  };
415
- }
464
+ });
465
+ drives.push(
466
+ { "IEC Drive": { bus_id: state.drives.softiec.busId, enabled: state.drives.softiec.power !== "off", type: "DOS emulation", last_error: "00, OK, 00, 00", partitions: [{ id: 1, path: "/Temp/" }] } },
467
+ { "Printer Emulation": { bus_id: 4, enabled: false } },
468
+ );
416
469
  sendJson(res, { drives });
417
470
  return;
418
471
  }
@@ -526,11 +579,20 @@ export async function startMockC64Server(options = {}) {
526
579
  state.lastLoadedPrg = prg;
527
580
  state.loadCount += 1;
528
581
 
529
- // Mirror real firmware: place the PRG body at its embedded load
530
- // address without starting it (no RUN, no BASIC pointer changes).
582
+ // Mirror Ultimate 64 firmware 3.15: the PRG body is placed at its
583
+ // embedded load address without starting it, and, as after a BASIC
584
+ // LOAD, VARTAB/ARYTAB/STREND are set to the end address and the BASIC
585
+ // program at $0801 is relinked (which rewrites raw code at $0801).
531
586
  if (prg.length >= 2) {
532
587
  const loadAddress = prg.readUInt16LE(0);
533
- state.memory.set(prg.subarray(2), loadAddress);
588
+ const body = prg.subarray(2);
589
+ state.memory.set(body.subarray(0, 0x10000 - loadAddress), loadAddress);
590
+ const endAddress = Math.min(0xffff, loadAddress + body.length);
591
+ for (const pointer of [0x2d, 0x2f, 0x31]) {
592
+ state.memory[pointer] = endAddress & 0xff;
593
+ state.memory[pointer + 1] = endAddress >> 8;
594
+ }
595
+ relinkBasicProgram(state.memory);
534
596
  }
535
597
 
536
598
  sendJson(res, { result: "ok", bytes: prg.length });
@@ -564,9 +626,22 @@ export async function startMockC64Server(options = {}) {
564
626
  }
565
627
 
566
628
  const attachment = Buffer.concat(chunks);
629
+ // Like the firmware, a multipart upload is a list of files in part
630
+ // order: the SID first, optional song lengths second. A text field
631
+ // (for example a JSON-serialised Buffer) is not a SID upload.
632
+ const contentType = String(req.headers["content-type"] ?? "");
633
+ const files = contentType.startsWith("multipart/form-data")
634
+ ? [...(await new Response(attachment, { headers: { "content-type": contentType } }).formData()).values()]
635
+ : null;
636
+ if (files && (files.length === 0 || files.some((part) => typeof part === "string"))) {
637
+ sendJson(res, { errors: ["Upload of file failed."] }, 400);
638
+ return;
639
+ }
640
+ const sid = files ? Buffer.from(await files[0].arrayBuffer()) : attachment;
641
+ const songlengths = files?.[1] ? Buffer.from(await files[1].arrayBuffer()) : null;
567
642
  state.sidAttachmentCount += 1;
568
- state.lastSidAttachment = { songnr, bytes: attachment.length };
569
- sendJson(res, { result: "sidplay_attachment", bytes: attachment.length, songnr });
643
+ state.lastSidAttachment = { songnr, bytes: sid.length, sid, songlengths };
644
+ sendJson(res, { result: "sidplay_attachment", bytes: sid.length, songnr });
570
645
  return;
571
646
  }
572
647
  }
@@ -610,7 +685,21 @@ export async function startMockC64Server(options = {}) {
610
685
  const lengthValue = routeUrl.searchParams.get("length") ?? "256";
611
686
  const address = parseNumeric(addressValue);
612
687
  const length = Math.max(0, parseNumeric(lengthValue, 10));
613
- const bytes = readMemoryWithHeartbeat(state, address, length);
688
+ // Same checks as the firmware (route_machine.cc readmem).
689
+ if (length < 1 || length > 65536) {
690
+ sendJson(res, { errors: ["Invalid length"] }, 400);
691
+ return;
692
+ }
693
+ if (address + length > 65536) {
694
+ sendJson(res, { errors: ["Memory read exceeds location $FFFF"] }, 400);
695
+ return;
696
+ }
697
+ const bytes = Uint8Array.from(readMemoryWithHeartbeat(state, address, length));
698
+ // SID registers $D400-$D418 are write-only; the Ultimate 64 reads them as $00.
699
+ for (let i = 0; i < bytes.length; i += 1) {
700
+ const target = address + i;
701
+ if (target >= 0xd400 && target <= 0xd418) bytes[i] = 0;
702
+ }
614
703
 
615
704
  const accept = String(req.headers["accept"] || "");
616
705
  if (accept.includes("application/octet-stream")) {
@@ -816,8 +905,12 @@ export async function startMockC64Server(options = {}) {
816
905
  if (routeUrl) {
817
906
  const match = /^\/v1\/drives\/([^:]+):(mount|remove|reset|on|off|load_rom|set_mode)$/.exec(routeUrl.pathname);
818
907
  if (match) {
819
- const driveId = decodeURIComponent(match[1]);
908
+ const driveId = decodeURIComponent(match[1]).toLowerCase();
820
909
  const action = match[2];
910
+ if (!Object.hasOwn(FIRMWARE_DRIVE_BUS_IDS, driveId)) {
911
+ sendJson(res, { errors: [`Invalid Drive '${decodeURIComponent(match[1])}'`] }, 400);
912
+ return;
913
+ }
821
914
  const driveState = ensureDriveState(driveId);
822
915
 
823
916
  const respond = (payload) => {
@@ -98,7 +98,10 @@ export async function main() {
98
98
  legOutputs.push(legPath);
99
99
  }
100
100
 
101
- const finalReport = mergeReports(await Promise.all(legOutputs.map(readReport)));
101
+ const finalReport = await dropNonCodeLines(
102
+ mergeReports(await Promise.all(legOutputs.map(readReport))),
103
+ (relativePath) => fs.readFile(path.join(repoRoot, relativePath), "utf8"),
104
+ );
102
105
  const finalPath = path.join(coverageDir, "lcov.info");
103
106
  await fs.writeFile(finalPath, serializeReport(finalReport), "utf8");
104
107
 
@@ -265,6 +268,33 @@ function parseReport(text) {
265
268
  return report;
266
269
  }
267
270
 
271
+ /**
272
+ * Bun's LCOV output lists blank and comment-only lines inside executed
273
+ * functions as executable lines that were never hit. Drop them, using each
274
+ * file's source, so coverage counts only code.
275
+ */
276
+ export async function dropNonCodeLines(report, readSource) {
277
+ const result = new Map();
278
+ for (const [filePath, record] of report.entries()) {
279
+ let sourceLines;
280
+ try {
281
+ sourceLines = (await readSource(filePath)).split(/\r?\n/);
282
+ } catch {
283
+ result.set(filePath, record);
284
+ continue;
285
+ }
286
+ const lines = new Map();
287
+ for (const [lineNo, hits] of record.lines.entries()) {
288
+ const text = (sourceLines[lineNo - 1] ?? "").trim();
289
+ const isComment = text.startsWith("//") || text.startsWith("/*") || text.startsWith("*");
290
+ if (text === "" || isComment) continue;
291
+ lines.set(lineNo, hits);
292
+ }
293
+ result.set(filePath, { ...record, lines });
294
+ }
295
+ return result;
296
+ }
297
+
268
298
  function mergeReports(reports) {
269
299
  const merged = new Map();
270
300
  for (const report of reports) {
@@ -485,8 +485,9 @@ export async function updateReadme(): Promise<boolean> {
485
485
  const generatedDocs = `\n\n${buildDocumentation()}\n\n`;
486
486
  const generatedEnv = `\n\n${buildEnvironmentDocumentation()}\n\n`;
487
487
  const nextReadme = readme
488
- .replace(docsPattern, `${START_MARKER}${generatedDocs}${END_MARKER}`)
489
- .replace(envPattern, `${ENV_START_MARKER}${generatedEnv}${ENV_END_MARKER}`);
488
+ // Replacement functions, not strings: generated text such as "$1E" must not be read as "$1".
489
+ .replace(docsPattern, () => `${START_MARKER}${generatedDocs}${END_MARKER}`)
490
+ .replace(envPattern, () => `${ENV_START_MARKER}${generatedEnv}${ENV_END_MARKER}`);
490
491
 
491
492
  if (nextReadme === readme) {
492
493
  return false;
package/server.json CHANGED
@@ -8,12 +8,12 @@
8
8
  "source": "github"
9
9
  },
10
10
  "websiteUrl": "https://github.com/chrisgleissner/c64bridge",
11
- "version": "1.0.4",
11
+ "version": "1.0.6",
12
12
  "packages": [
13
13
  {
14
14
  "registryType": "npm",
15
15
  "identifier": "c64bridge",
16
- "version": "1.0.4",
16
+ "version": "1.0.6",
17
17
  "transport": {
18
18
  "type": "stdio"
19
19
  },