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
@@ -0,0 +1,371 @@
1
+ import { Buffer } from "node:buffer";
2
+ import fs from "node:fs";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ let state = null;
6
+ let handlersInstalled = false;
7
+ let spanCounter = 0;
8
+ const DEFAULT_OUTPUT_TAIL_CHARS = 4096;
9
+ export function installProcessDiagnostics(component) {
10
+ const info = ensureState(component).info;
11
+ writeDiagnosticEvent("session_start", {
12
+ argv: process.argv.slice(2),
13
+ cwd: process.cwd(),
14
+ node: process.version,
15
+ pid: process.pid,
16
+ platform: process.platform,
17
+ });
18
+ installGlobalHandlers();
19
+ return info;
20
+ }
21
+ export function getDiagnosticsSessionInfo() {
22
+ return state?.info ?? null;
23
+ }
24
+ export function getDiagnosticsDirectory() {
25
+ return resolveDiagnosticsDirectory();
26
+ }
27
+ export function writeDiagnosticEvent(event, details) {
28
+ if (!isDiagnosticsEnabled()) {
29
+ return;
30
+ }
31
+ const current = ensureState("runtime");
32
+ const record = {
33
+ ts: new Date().toISOString(),
34
+ pid: process.pid,
35
+ sessionId: current.info.sessionId,
36
+ component: current.info.component,
37
+ event,
38
+ details: sanitize(details),
39
+ };
40
+ try {
41
+ fs.appendFileSync(current.info.filePath, `${JSON.stringify(record)}\n`, "utf8");
42
+ }
43
+ catch (error) {
44
+ safeStderrWrite(`[diagnostics] failed to append event ${event}: ${error instanceof Error ? error.message : String(error)}\n`);
45
+ }
46
+ }
47
+ export function createOutputTailCapture(name, maxChars = DEFAULT_OUTPUT_TAIL_CHARS) {
48
+ let stdoutTail = "";
49
+ let stderrTail = "";
50
+ let stdoutBytes = 0;
51
+ let stderrBytes = 0;
52
+ return {
53
+ pushStdout(chunk) {
54
+ const text = normalizeChunk(chunk);
55
+ stdoutBytes += Buffer.byteLength(text, "utf8");
56
+ stdoutTail = keepTail(stdoutTail, text, maxChars);
57
+ },
58
+ pushStderr(chunk) {
59
+ const text = normalizeChunk(chunk);
60
+ stderrBytes += Buffer.byteLength(text, "utf8");
61
+ stderrTail = keepTail(stderrTail, text, maxChars);
62
+ },
63
+ snapshot() {
64
+ return {
65
+ name,
66
+ stdoutBytes,
67
+ stderrBytes,
68
+ stdoutTail: stdoutTail.trim(),
69
+ stderrTail: stderrTail.trim(),
70
+ };
71
+ },
72
+ };
73
+ }
74
+ export function beginDiagnosticSpan(category, name, details) {
75
+ const spanId = `${Date.now().toString(36)}-${process.pid}-${++spanCounter}`;
76
+ const startedAt = process.hrtime.bigint();
77
+ let closed = false;
78
+ writeDiagnosticEvent("perf_span_start", {
79
+ spanId,
80
+ category,
81
+ name,
82
+ details,
83
+ });
84
+ return {
85
+ spanId,
86
+ category,
87
+ name,
88
+ end(outcome = "ok", details) {
89
+ if (closed) {
90
+ return;
91
+ }
92
+ closed = true;
93
+ writeDiagnosticEvent("perf_span_end", {
94
+ spanId,
95
+ category,
96
+ name,
97
+ outcome,
98
+ durationMs: roundDurationMs(startedAt),
99
+ details,
100
+ });
101
+ },
102
+ };
103
+ }
104
+ export async function withDiagnosticSpan(category, name, details, fn) {
105
+ const span = beginDiagnosticSpan(category, name, details);
106
+ try {
107
+ const result = await fn();
108
+ span.end("ok");
109
+ return result;
110
+ }
111
+ catch (error) {
112
+ span.end("error", { error });
113
+ throw error;
114
+ }
115
+ }
116
+ export function listDiagnosticSessionFiles(directory = resolveDiagnosticsDirectory()) {
117
+ if (!fs.existsSync(directory)) {
118
+ return [];
119
+ }
120
+ return fs.readdirSync(directory)
121
+ .filter((entry) => entry.endsWith(".ndjson"))
122
+ .map((entry) => path.join(directory, entry))
123
+ .sort((left, right) => right.localeCompare(left));
124
+ }
125
+ export function resolveDiagnosticSessionFile(scope = "current") {
126
+ if (scope === "current") {
127
+ return getDiagnosticsSessionInfo()?.filePath ?? null;
128
+ }
129
+ const [latest] = listDiagnosticSessionFiles();
130
+ return latest ?? null;
131
+ }
132
+ export function readDiagnosticRecords(filePath) {
133
+ if (!fs.existsSync(filePath)) {
134
+ return [];
135
+ }
136
+ const text = fs.readFileSync(filePath, "utf8").trim();
137
+ if (!text) {
138
+ return [];
139
+ }
140
+ return text.split("\n")
141
+ .map((line) => JSON.parse(line));
142
+ }
143
+ export function summarizeDiagnosticsSession(filePath, options = {}) {
144
+ const records = readDiagnosticRecords(filePath);
145
+ const maxEntries = Math.max(1, Math.min(200, Math.trunc(options.maxEntries ?? 25)));
146
+ const spanGroups = new Map();
147
+ const toolCalls = new Map();
148
+ for (const record of records) {
149
+ if (record.event === "perf_span_end") {
150
+ const details = (record.details ?? {});
151
+ const category = String(details.category ?? "unknown");
152
+ const name = String(details.name ?? "unknown");
153
+ const key = `${category}:${name}`;
154
+ const existing = spanGroups.get(key) ?? {
155
+ category,
156
+ name,
157
+ count: 0,
158
+ totalDurationMs: 0,
159
+ maxDurationMs: 0,
160
+ lastOutcome: undefined,
161
+ };
162
+ const durationMs = Number(details.durationMs ?? 0);
163
+ existing.count += 1;
164
+ existing.totalDurationMs += durationMs;
165
+ existing.maxDurationMs = Math.max(existing.maxDurationMs, durationMs);
166
+ existing.lastOutcome = typeof details.outcome === "string" ? details.outcome : existing.lastOutcome;
167
+ spanGroups.set(key, existing);
168
+ continue;
169
+ }
170
+ if (record.event === "mcp_call_tool_ok" || record.event === "mcp_call_tool_failed") {
171
+ const details = (record.details ?? {});
172
+ const name = String(details.name ?? "unknown");
173
+ const existing = toolCalls.get(name) ?? {
174
+ name,
175
+ count: 0,
176
+ errorCount: 0,
177
+ totalLatencyMs: 0,
178
+ maxLatencyMs: 0,
179
+ };
180
+ const latencyMs = Number(details.latencyMs ?? 0);
181
+ existing.count += 1;
182
+ existing.totalLatencyMs += latencyMs;
183
+ existing.maxLatencyMs = Math.max(existing.maxLatencyMs, latencyMs);
184
+ if (record.event === "mcp_call_tool_failed" || details.isError === true) {
185
+ existing.errorCount += 1;
186
+ }
187
+ toolCalls.set(name, existing);
188
+ }
189
+ }
190
+ const topSpans = Array.from(spanGroups.values())
191
+ .map((entry) => ({
192
+ category: entry.category,
193
+ name: entry.name,
194
+ count: entry.count,
195
+ totalDurationMs: roundNumber(entry.totalDurationMs),
196
+ avgDurationMs: roundNumber(entry.totalDurationMs / Math.max(1, entry.count)),
197
+ maxDurationMs: roundNumber(entry.maxDurationMs),
198
+ lastOutcome: entry.lastOutcome,
199
+ }))
200
+ .sort((left, right) => right.totalDurationMs - left.totalDurationMs)
201
+ .slice(0, maxEntries);
202
+ const summarizedToolCalls = Array.from(toolCalls.values())
203
+ .map((entry) => ({
204
+ name: entry.name,
205
+ count: entry.count,
206
+ errorCount: entry.errorCount,
207
+ totalLatencyMs: roundNumber(entry.totalLatencyMs),
208
+ avgLatencyMs: roundNumber(entry.totalLatencyMs / Math.max(1, entry.count)),
209
+ maxLatencyMs: roundNumber(entry.maxLatencyMs),
210
+ }))
211
+ .sort((left, right) => right.totalLatencyMs - left.totalLatencyMs);
212
+ const startedAt = records[0]?.ts ?? null;
213
+ const endedAt = records.length > 0 ? (records[records.length - 1]?.ts ?? null) : null;
214
+ const durationMs = startedAt && endedAt
215
+ ? roundNumber(new Date(endedAt).getTime() - new Date(startedAt).getTime())
216
+ : 0;
217
+ const timeline = options.includeTimeline
218
+ ? records.slice(-maxEntries).map((record) => ({
219
+ ts: record.ts,
220
+ event: record.event,
221
+ details: record.details,
222
+ }))
223
+ : undefined;
224
+ return {
225
+ filePath,
226
+ sessionId: records[0]?.sessionId ?? null,
227
+ component: records[0]?.component ?? null,
228
+ eventCount: records.length,
229
+ startedAt,
230
+ endedAt,
231
+ durationMs,
232
+ topSpans,
233
+ toolCalls: summarizedToolCalls,
234
+ ...(timeline ? { timeline } : {}),
235
+ };
236
+ }
237
+ function ensureState(component) {
238
+ if (state) {
239
+ return state;
240
+ }
241
+ const info = createSessionInfo(component);
242
+ state = { info };
243
+ return state;
244
+ }
245
+ function createSessionInfo(component) {
246
+ const directory = resolveDiagnosticsDirectory();
247
+ fs.mkdirSync(directory, { recursive: true });
248
+ const startedAt = new Date().toISOString().replace(/[:.]/g, "-");
249
+ const safeComponent = component.replace(/[^a-z0-9_-]+/gi, "-").replace(/^-+|-+$/g, "") || "runtime";
250
+ const sessionId = `${startedAt}-${process.pid}`;
251
+ const filePath = path.join(directory, `${sessionId}-${safeComponent}.ndjson`);
252
+ return { component: safeComponent, directory, filePath, sessionId };
253
+ }
254
+ function resolveDiagnosticsDirectory() {
255
+ const configured = process.env.C64BRIDGE_DIAGNOSTICS_DIR?.trim();
256
+ if (configured) {
257
+ return path.resolve(configured);
258
+ }
259
+ return path.join(os.homedir(), ".c64bridge", "diagnostics");
260
+ }
261
+ function installGlobalHandlers() {
262
+ if (!isDiagnosticsEnabled() || handlersInstalled) {
263
+ return;
264
+ }
265
+ handlersInstalled = true;
266
+ process.on("warning", (warning) => {
267
+ writeDiagnosticEvent("process_warning", warning);
268
+ });
269
+ process.on("unhandledRejection", (reason, promise) => {
270
+ writeDiagnosticEvent("unhandled_rejection", {
271
+ reason,
272
+ promise: Object.prototype.toString.call(promise),
273
+ });
274
+ });
275
+ process.on("uncaughtExceptionMonitor", (error, origin) => {
276
+ writeDiagnosticEvent("uncaught_exception", { error, origin });
277
+ });
278
+ process.on("beforeExit", (code) => {
279
+ writeDiagnosticEvent("before_exit", { code });
280
+ });
281
+ process.on("exit", (code) => {
282
+ writeDiagnosticEvent("process_exit", { code });
283
+ });
284
+ }
285
+ function isDiagnosticsEnabled() {
286
+ if (process.env.C64BRIDGE_DISABLE_DIAGNOSTICS === "1") {
287
+ return false;
288
+ }
289
+ if (process.env.NODE_ENV === "test" && process.env.C64BRIDGE_ENABLE_TEST_DIAGNOSTICS !== "1") {
290
+ return false;
291
+ }
292
+ return true;
293
+ }
294
+ function sanitize(value, depth = 0) {
295
+ if (value instanceof Error) {
296
+ const extra = value;
297
+ return {
298
+ name: value.name,
299
+ message: value.message,
300
+ stack: limitString(value.stack ?? "", 8000),
301
+ code: extra.code,
302
+ cause: depth < 3 ? sanitize(extra.cause, depth + 1) : undefined,
303
+ };
304
+ }
305
+ if (typeof value === "string") {
306
+ return limitString(value, 8000);
307
+ }
308
+ if (typeof value === "number" || typeof value === "boolean" || value === null || value === undefined) {
309
+ return value;
310
+ }
311
+ if (typeof value === "bigint") {
312
+ return value.toString();
313
+ }
314
+ if (Buffer.isBuffer(value)) {
315
+ return {
316
+ type: "Buffer",
317
+ byteLength: value.length,
318
+ previewHex: value.subarray(0, 64).toString("hex"),
319
+ };
320
+ }
321
+ if (ArrayBuffer.isView(value)) {
322
+ return {
323
+ type: value.constructor.name,
324
+ byteLength: value.byteLength,
325
+ };
326
+ }
327
+ if (Array.isArray(value)) {
328
+ if (depth >= 3) {
329
+ return `[Array(${value.length})]`;
330
+ }
331
+ return value.slice(0, 25).map((entry) => sanitize(entry, depth + 1));
332
+ }
333
+ if (typeof value === "object") {
334
+ if (depth >= 3) {
335
+ return `[Object ${value?.constructor?.name ?? "Object"}]`;
336
+ }
337
+ const entries = Object.entries(value).slice(0, 25);
338
+ return Object.fromEntries(entries.map(([key, entry]) => [key, sanitize(entry, depth + 1)]));
339
+ }
340
+ return String(value);
341
+ }
342
+ function normalizeChunk(chunk) {
343
+ return typeof chunk === "string" ? chunk : Buffer.from(chunk).toString("utf8");
344
+ }
345
+ function keepTail(existing, next, maxChars) {
346
+ const combined = `${existing}${next}`;
347
+ if (combined.length <= maxChars) {
348
+ return combined;
349
+ }
350
+ return combined.slice(combined.length - maxChars);
351
+ }
352
+ function limitString(value, maxChars) {
353
+ if (value.length <= maxChars) {
354
+ return value;
355
+ }
356
+ return `${value.slice(0, maxChars)}…[truncated ${value.length - maxChars} chars]`;
357
+ }
358
+ function roundDurationMs(startedAt) {
359
+ return roundNumber(Number(process.hrtime.bigint() - startedAt) / 1000000);
360
+ }
361
+ function roundNumber(value) {
362
+ return Number(value.toFixed(3));
363
+ }
364
+ function safeStderrWrite(text) {
365
+ try {
366
+ process.stderr.write(text);
367
+ }
368
+ catch {
369
+ // Ignore stderr write failures during crash handling.
370
+ }
371
+ }
@@ -0,0 +1,39 @@
1
+ import { readFileSync } from "node:fs";
2
+ function readJsonFile(url) {
3
+ return JSON.parse(readFileSync(url, "utf8"));
4
+ }
5
+ const packageJson = readJsonFile(new URL("../../package.json", import.meta.url));
6
+ const manifestJson = readJsonFile(new URL("../../mcp.json", import.meta.url));
7
+ function normalizeRepositoryUrl(repository) {
8
+ if (!repository) {
9
+ return undefined;
10
+ }
11
+ if (typeof repository === "string") {
12
+ return repository.trim().length > 0 ? repository.trim() : undefined;
13
+ }
14
+ if (typeof repository === "object"
15
+ && repository !== null
16
+ && typeof repository.url === "string"
17
+ && repository.url.trim().length > 0) {
18
+ return repository.url.trim();
19
+ }
20
+ return undefined;
21
+ }
22
+ const implementationInfo = Object.freeze({
23
+ name: manifestJson.name ?? packageJson.name ?? "c64bridge",
24
+ version: packageJson.version ?? manifestJson.version ?? "0.0.0",
25
+ });
26
+ const projectMetadata = Object.freeze({
27
+ name: implementationInfo.name,
28
+ version: implementationInfo.version,
29
+ description: manifestJson.description ?? packageJson.description ?? "",
30
+ license: manifestJson.license ?? packageJson.license ?? "",
31
+ repository: normalizeRepositoryUrl(manifestJson.repository ?? packageJson.repository),
32
+ transports: manifestJson.type === "stdio" ? ["stdio"] : ["stdio"],
33
+ });
34
+ export function getMcpServerImplementationInfo() {
35
+ return implementationInfo;
36
+ }
37
+ export function getMcpProjectMetadata() {
38
+ return projectMetadata;
39
+ }