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.
Files changed (107) 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 +62 -24
  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 +83 -9
  99. package/package.json +8 -3
  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/run-coverage.mjs +284 -0
  105. package/scripts/run-tests.mjs +95 -39
  106. package/scripts/run-tests.ts +378 -73
  107. package/scripts/update-readme.ts +147 -13
package/dist/rag/init.js CHANGED
@@ -4,6 +4,7 @@ GPL-2.0-only
4
4
  */
5
5
  import fs from "node:fs/promises";
6
6
  import path from "node:path";
7
+ import { setTimeout as delay } from "node:timers/promises";
7
8
  import { LocalMiniHashEmbedding } from "./embeddings.js";
8
9
  import { buildAllIndexes, loadIndexes } from "./indexer.js";
9
10
  import { LocalRagRetriever } from "./retriever.js";
@@ -12,7 +13,7 @@ const EXTERNAL_DIR = path.resolve("external");
12
13
  const BASIC_DATA_DIR = path.resolve("data/basic/examples");
13
14
  const ASM_DATA_DIR = path.resolve("data/assembly/examples");
14
15
  const DOC_DIR = path.resolve("doc");
15
- const CONTEXT_DIR = path.resolve("doc/context");
16
+ const CONTEXT_DIR = path.resolve("data/context");
16
17
  const BOOTSTRAP_PATH = path.join(CONTEXT_DIR, "bootstrap.md");
17
18
  const AGENTS_PATH = path.resolve("AGENTS.md");
18
19
  const PROMPTS_DIR = path.resolve(".github/prompts");
@@ -31,7 +32,42 @@ function embeddingIndexPaths() {
31
32
  other: path.join(dir, "embeddings_other.json"),
32
33
  };
33
34
  }
35
+ export function createLazyRagRetriever(factory, hooks = {}) {
36
+ let resolved;
37
+ let pending;
38
+ const ensure = async () => {
39
+ if (resolved) {
40
+ return resolved;
41
+ }
42
+ if (!pending) {
43
+ hooks.onInitStart?.();
44
+ pending = factory().then((retriever) => {
45
+ resolved = retriever;
46
+ hooks.onInitComplete?.();
47
+ return retriever;
48
+ }).catch((error) => {
49
+ pending = undefined;
50
+ hooks.onInitError?.(error);
51
+ throw error;
52
+ });
53
+ }
54
+ return pending;
55
+ };
56
+ return {
57
+ async retrieve(query, topK, filterLanguage) {
58
+ const retriever = await ensure();
59
+ return retriever.retrieve(query, topK, filterLanguage);
60
+ },
61
+ async warmup() {
62
+ await ensure();
63
+ },
64
+ };
65
+ }
34
66
  export async function initRag() {
67
+ const initDelayMs = Number(process.env.RAG_INIT_DELAY_MS ?? 0);
68
+ if (Number.isFinite(initDelayMs) && initDelayMs > 0) {
69
+ await delay(initDelayMs);
70
+ }
35
71
  const model = new LocalMiniHashEmbedding(384);
36
72
  // Build on start only when explicitly requested
37
73
  const buildOnStart = String(process.env.RAG_BUILD_ON_START ?? "").trim().toLowerCase();
@@ -37,8 +37,8 @@ const KNOWLEDGE_BUNDLES = [
37
37
  id: "orientation",
38
38
  title: "Workflow & Orientation",
39
39
  summary: "Mandatory workflow and safety guidance before issuing any C64 commands.",
40
- prompts: ["basic-program", "assembly-program", "memory-debug"],
41
- tools: ["upload_run_basic", "upload_run_asm", "read_screen", "read"],
40
+ prompts: ["basic-program", "assembly-program", "memory-debug", "cross-platform-demo", "preset-music-demo"],
41
+ tools: ["upload_run_basic", "upload_run_asm", "cross_platform_greeting", "music_play_preset", "read_screen", "read"],
42
42
  resources: [
43
43
  {
44
44
  uri: "c64://context/bootstrap",
@@ -47,10 +47,22 @@ const KNOWLEDGE_BUNDLES = [
47
47
  relativePath: "data/context/bootstrap.md",
48
48
  priority: "critical",
49
49
  summary: "Step-by-step rules for safe automation, verification, and rollback on the C64.",
50
- prompts: ["basic-program", "assembly-program", "memory-debug"],
51
- tools: ["upload_run_basic", "upload_run_asm", "read_screen", "read"],
50
+ prompts: ["basic-program", "assembly-program", "memory-debug", "cross-platform-demo", "preset-music-demo"],
51
+ tools: ["upload_run_basic", "upload_run_asm", "cross_platform_greeting", "music_play_preset", "read_screen", "read"],
52
52
  tags: ["workflow", "safety"],
53
53
  },
54
+ {
55
+ uri: "c64://context/fast-paths",
56
+ name: "Fast Path Workflows",
57
+ description: "Shortest reliable workflows for common C64U and VICE tasks, including dual-backend demos.",
58
+ relativePath: "data/context/fast-paths.md",
59
+ priority: "critical",
60
+ summary: "Condensed routing guide for one-call demos, backend switching, and when to prefer orchestration over manual tool composition.",
61
+ prompts: ["basic-program", "graphics-demo", "cross-platform-demo", "sid-music", "preset-music-demo"],
62
+ tools: ["c64_program", "c64_sound", "c64_select_backend", "c64_memory", "c64_graphics"],
63
+ relatedResources: ["c64://context/bootstrap", "c64://platform/status"],
64
+ tags: ["workflow", "fast-path", "routing"],
65
+ },
54
66
  ],
55
67
  },
56
68
  {
@@ -445,12 +457,12 @@ const INDEX_RESOURCE = {
445
457
  domain: "overview",
446
458
  priority: "critical",
447
459
  summary: "Explains how to approach each knowledge bundle and when to consult it.",
448
- prompts: ["basic-program", "assembly-program", "sid-music", "graphics-demo", "printer-job", "memory-debug"],
460
+ prompts: ["basic-program", "assembly-program", "sid-music", "graphics-demo", "printer-job", "memory-debug", "cross-platform-demo", "preset-music-demo"],
449
461
  tools: [
450
462
  "upload_run_basic",
451
463
  "upload_run_asm",
452
464
  "music_generate",
453
- "render_petscii_screen",
465
+ "render_petscii_text",
454
466
  "printer_guide",
455
467
  "read",
456
468
  ],
@@ -25,7 +25,7 @@ export class SlidingWindowRateLimiter {
25
25
  if (!arr)
26
26
  return;
27
27
  let startIdx = 0;
28
- while (startIdx < arr.length && arr[startIdx] < windowStart)
28
+ while (startIdx < arr.length && arr[startIdx] <= windowStart)
29
29
  startIdx++;
30
30
  if (startIdx > 0)
31
31
  arr.splice(0, startIdx);
@@ -74,7 +74,7 @@ export class AdaptiveRateLimiter {
74
74
  return;
75
75
  const cutoff = now - 1000;
76
76
  let i = 0;
77
- while (i < arr.length && arr[i] < cutoff)
77
+ while (i < arr.length && arr[i] <= cutoff)
78
78
  i++;
79
79
  if (i > 0)
80
80
  arr.splice(0, i);
@@ -0,0 +1,135 @@
1
+ export const VIDEO_STREAM_HEADER_BYTES = 12;
2
+ export const AUDIO_STREAM_HEADER_BYTES = 2;
3
+ export const DEFAULT_VIDEO_STREAM_PORT = 11000;
4
+ export const DEFAULT_AUDIO_STREAM_PORT = 11001;
5
+ export const C64U_PAL_AUDIO_SAMPLE_RATE = 47982.8869047619;
6
+ export const C64U_NTSC_AUDIO_SAMPLE_RATE = 47940.3408482143;
7
+ export function parseVideoPacket(payload) {
8
+ if (payload.length < VIDEO_STREAM_HEADER_BYTES) {
9
+ throw new Error(`Video packet too small: ${payload.length}`);
10
+ }
11
+ const sequence = payload.readUInt16LE(0);
12
+ const frameNumber = payload.readUInt16LE(2);
13
+ const lineField = payload.readUInt16LE(4);
14
+ const lineNumber = lineField & 0x7fff;
15
+ const isLastPacket = (lineField & 0x8000) !== 0;
16
+ const pixelsPerLine = payload.readUInt16LE(6);
17
+ const linesPerPacket = payload.readUInt8(8);
18
+ const bitsPerPixel = payload.readUInt8(9);
19
+ const encodingType = payload.readUInt16LE(10);
20
+ return {
21
+ sequence,
22
+ frameNumber,
23
+ lineNumber,
24
+ isLastPacket,
25
+ pixelsPerLine,
26
+ linesPerPacket,
27
+ bitsPerPixel,
28
+ encodingType,
29
+ payload: payload.subarray(VIDEO_STREAM_HEADER_BYTES),
30
+ };
31
+ }
32
+ export function parseAudioPacket(payload) {
33
+ if (payload.length < AUDIO_STREAM_HEADER_BYTES) {
34
+ throw new Error(`Audio packet too small: ${payload.length}`);
35
+ }
36
+ const sequence = payload.readUInt16LE(0);
37
+ const audioBytes = payload.subarray(AUDIO_STREAM_HEADER_BYTES);
38
+ if (audioBytes.length % 2 !== 0) {
39
+ throw new Error(`Audio payload has odd byte count: ${audioBytes.length}`);
40
+ }
41
+ const samples = new Int16Array(audioBytes.length / 2);
42
+ for (let index = 0; index < samples.length; index += 1) {
43
+ samples[index] = audioBytes.readInt16LE(index * 2);
44
+ }
45
+ return { sequence, samples };
46
+ }
47
+ export function collectCompleteVideoFrames(packets, count) {
48
+ const completed = [];
49
+ for (const group of groupVideoPackets(packets)) {
50
+ if (!isVideoFrameGroupComplete(group)) {
51
+ continue;
52
+ }
53
+ completed.push(reconstructVideoFrame(group.packets));
54
+ if (completed.length >= count) {
55
+ break;
56
+ }
57
+ }
58
+ return completed;
59
+ }
60
+ export function reconstructVideoFrame(packets) {
61
+ if (packets.length === 0) {
62
+ throw new Error("No video packets available");
63
+ }
64
+ const reference = packets[0];
65
+ const width = reference.pixelsPerLine;
66
+ const linesPerPacket = Math.max(1, reference.linesPerPacket);
67
+ const bytesPerLine = Math.ceil((width * reference.bitsPerPixel) / 8);
68
+ const maxLine = Math.max(...packets.map((packet) => packet.lineNumber + packet.linesPerPacket));
69
+ const height = maxLine;
70
+ const pixels = new Uint8Array(width * height);
71
+ const filledLines = new Set();
72
+ for (const packet of packets) {
73
+ const expectedPayloadBytes = bytesPerLine * linesPerPacket;
74
+ const packetPayload = packet.payload.subarray(0, expectedPayloadBytes);
75
+ for (let lineOffset = 0; lineOffset < linesPerPacket; lineOffset += 1) {
76
+ const lineIndex = packet.lineNumber + lineOffset;
77
+ if (lineIndex >= height) {
78
+ continue;
79
+ }
80
+ const srcStart = lineOffset * bytesPerLine;
81
+ const srcLine = packetPayload.subarray(srcStart, srcStart + bytesPerLine);
82
+ unpack4BitPixelsToRow(srcLine, pixels, lineIndex * width, width);
83
+ filledLines.add(lineIndex);
84
+ }
85
+ }
86
+ return {
87
+ frameNumber: reference.frameNumber,
88
+ width,
89
+ height,
90
+ bitsPerPixel: 4,
91
+ pixels,
92
+ complete: height > 0 && filledLines.size === height,
93
+ };
94
+ }
95
+ function groupVideoPackets(packets) {
96
+ const byFrame = new Map();
97
+ for (const packet of packets) {
98
+ const framePackets = byFrame.get(packet.frameNumber) ?? [];
99
+ framePackets.push(packet);
100
+ byFrame.set(packet.frameNumber, framePackets);
101
+ }
102
+ return Array.from(byFrame.entries())
103
+ .sort((left, right) => left[0] - right[0])
104
+ .map(([frameNumber, framePackets]) => ({
105
+ frameNumber,
106
+ packets: framePackets.slice().sort((left, right) => left.lineNumber - right.lineNumber),
107
+ }));
108
+ }
109
+ function isVideoFrameGroupComplete(group) {
110
+ const lastPacket = group.packets.find((packet) => packet.isLastPacket);
111
+ if (!lastPacket) {
112
+ return false;
113
+ }
114
+ const expectedHeight = lastPacket.lineNumber + lastPacket.linesPerPacket;
115
+ if (expectedHeight <= 0) {
116
+ return false;
117
+ }
118
+ const coveredLines = new Set();
119
+ for (const packet of group.packets) {
120
+ for (let offset = 0; offset < packet.linesPerPacket; offset += 1) {
121
+ coveredLines.add(packet.lineNumber + offset);
122
+ }
123
+ }
124
+ return coveredLines.size >= expectedHeight;
125
+ }
126
+ function unpack4BitPixelsToRow(srcLine, dstPixels, dstOffset, width) {
127
+ let cursor = dstOffset;
128
+ for (let index = 0; index < srcLine.length && cursor < dstOffset + width; index += 1) {
129
+ const packed = srcLine[index];
130
+ dstPixels[cursor++] = packed & 0x0f;
131
+ if (cursor < dstOffset + width) {
132
+ dstPixels[cursor++] = (packed >> 4) & 0x0f;
133
+ }
134
+ }
135
+ }
@@ -403,6 +403,7 @@ export const audioModule = defineToolModule({
403
403
  ],
404
404
  prompts: ["sid-music"],
405
405
  defaultTags: ["sid", "audio"],
406
+ supportedPlatforms: ["c64u", "vice"],
406
407
  workflowHints: [
407
408
  "Reach for SID helpers when the user talks about sound design, playback quality, or stuck notes.",
408
409
  "After changing playback state, suggest verify-by-ear steps such as analyze_audio so the user gets concrete feedback.",
@@ -1006,7 +1007,8 @@ export const audioModule = defineToolModule({
1006
1007
  }
1007
1008
  const duration = parsed.durationSeconds ?? 3;
1008
1009
  ctx.logger.info("Auto-analyzing SID audio", { durationSeconds: duration });
1009
- const analysis = await recordAndAnalyzeAudio({
1010
+ const analyzer = resolveAnalyzer(ctx);
1011
+ const analysis = await analyzer({
1010
1012
  durationSeconds: duration,
1011
1013
  expectedSidwave: normaliseSidwaveInput(parsed.expectedSidwave),
1012
1014
  });