c64bridge 0.9.0 → 0.9.2

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 (61) hide show
  1. package/README.md +161 -135
  2. package/data/assembly/assembly-spec.md +1 -1
  3. package/data/context/bootstrap.md +3 -3
  4. package/data/context/fast-paths.md +3 -1
  5. package/data/graphics/petscii-style-guide.md +1 -1
  6. package/data/graphics/sprite-charset-best-practices.md +14 -13
  7. package/data/sound/sid-file-structure.md +5 -5
  8. package/data/sound/sid-programming-best-practices.md +5 -5
  9. package/data/vice/vice-binary-monitor-spec.md +26 -10
  10. package/dist/.tsbuildinfo +1 -1
  11. package/dist/c64Client.js +107 -8
  12. package/dist/device.js +43 -3
  13. package/dist/rag/indexer.js +18 -8
  14. package/dist/sidwaveCompiler.js +1 -1
  15. package/dist/tools/debug.js +158 -5
  16. package/dist/tools/input.js +336 -0
  17. package/dist/tools/memory.js +326 -1
  18. package/dist/tools/programRunners.js +45 -6
  19. package/dist/tools/registry/batch.js +167 -0
  20. package/dist/tools/registry/graphics.js +101 -1
  21. package/dist/tools/registry/index.js +4 -0
  22. package/dist/tools/registry/input.js +2 -0
  23. package/dist/tools/registry/memory.js +61 -1
  24. package/dist/tools/responses.js +14 -1
  25. package/dist/tools/schema.js +4 -5
  26. package/dist/{assemblyConverter.js → tools/translation/assembler.js} +66 -163
  27. package/dist/{basicConverter.js → tools/translation/basicTokenizer.js} +1 -1
  28. package/dist/tools/translation/disassembler.js +150 -0
  29. package/dist/tools/translation/opcodeInventory.js +126 -0
  30. package/dist/tools/translation/symbolRegistry.js +49 -0
  31. package/dist/tools/types.js +19 -4
  32. package/dist/vice/process.js +43 -2
  33. package/dist/vice/viceClient.js +5 -1
  34. package/doc/reviews/audit.md +143 -0
  35. package/mcp/schemas/c64_batch.schema.json +53 -0
  36. package/mcp/schemas/c64_config.schema.json +461 -2
  37. package/mcp/schemas/c64_debug.schema.json +641 -3
  38. package/mcp/schemas/c64_disk.schema.json +340 -2
  39. package/mcp/schemas/c64_drive.schema.json +157 -2
  40. package/mcp/schemas/c64_extract.schema.json +359 -2
  41. package/mcp/schemas/c64_graphics.schema.json +415 -3
  42. package/mcp/schemas/c64_input.schema.json +254 -0
  43. package/mcp/schemas/c64_memory.schema.json +562 -3
  44. package/mcp/schemas/c64_printer.schema.json +253 -2
  45. package/mcp/schemas/c64_program.schema.json +499 -2
  46. package/mcp/schemas/c64_rag.schema.json +82 -2
  47. package/mcp/schemas/c64_select_backend.schema.json +35 -2
  48. package/mcp/schemas/c64_sound.schema.json +793 -2
  49. package/mcp/schemas/c64_stream.schema.json +73 -2
  50. package/mcp/schemas/c64_system.schema.json +301 -2
  51. package/mcp/schemas/c64_vice.schema.json +71 -2
  52. package/mcp/server.json +1 -1
  53. package/mcp/tools.json +7509 -1929
  54. package/mcp.json +1 -1
  55. package/package.json +2 -2
  56. package/scripts/c64-cli.mjs +2 -2
  57. package/scripts/run-coverage.mjs +61 -3
  58. package/scripts/run-tests.mjs +1 -0
  59. package/scripts/run-tests.ts +3 -0
  60. package/scripts/update-readme.ts +79 -2
  61. package/server.json +2 -2
package/mcp.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "c64bridge",
3
- "version": "0.9.0",
3
+ "version": "0.9.2",
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": "0.9.0",
3
+ "version": "0.9.2",
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
  "type": "module",
@@ -18,7 +18,7 @@
18
18
  "mcp": "node scripts/invoke-bun.mjs src/mcp-server.ts",
19
19
  "mcp:node": "node dist/mcp-server.js",
20
20
  "mcp:build": "npm run build && node dist/mcp-server.js",
21
- "build": "node scripts/prebuild.mjs && tsc -p tsconfig.json && node scripts/postbuild.mjs && node scripts/invoke-bun.mjs scripts/update-readme.ts && node scripts/invoke-bun.mjs scripts/generate-mcp-interface.ts",
21
+ "build": "VICE_VISIBLE=false node scripts/prebuild.mjs && VICE_VISIBLE=false tsc -p tsconfig.json && VICE_VISIBLE=false node scripts/postbuild.mjs && VICE_VISIBLE=false node scripts/invoke-bun.mjs scripts/update-readme.ts && VICE_VISIBLE=false node scripts/invoke-bun.mjs scripts/generate-mcp-interface.ts",
22
22
  "lint": "npm run check",
23
23
  "test": "node scripts/invoke-bun.mjs scripts/run-tests.ts",
24
24
  "test:node": "node scripts/run-tests.mjs",
@@ -18,7 +18,7 @@ async function loadCoreModules() {
18
18
  }
19
19
 
20
20
  if (typeof globalThis.Bun !== "undefined") {
21
- ({ basicToPrg } = await import("../src/basicConverter.ts"));
21
+ ({ basicToPrg } = await import("../src/tools/translation/basicTokenizer.ts"));
22
22
  ({ loadConfig } = await import("../src/config.ts"));
23
23
  ({ C64Client } = await import("../src/c64Client.ts"));
24
24
  return;
@@ -26,7 +26,7 @@ async function loadCoreModules() {
26
26
 
27
27
  const distPath = resolve(projectRoot, "dist");
28
28
  try {
29
- ({ basicToPrg } = await import(pathToFileURL(resolve(distPath, "basicConverter.js")).href));
29
+ ({ basicToPrg } = await import(pathToFileURL(resolve(distPath, "tools/translation/basicTokenizer.js")).href));
30
30
  ({ loadConfig } = await import(pathToFileURL(resolve(distPath, "config.js")).href));
31
31
  ({ C64Client } = await import(pathToFileURL(resolve(distPath, "c64Client.js")).href));
32
32
  } catch (error) {
@@ -18,6 +18,8 @@ const DEFAULT_VICE_MOCK_TEST_FILES = [
18
18
  "test/viceIntegration.test.mjs",
19
19
  "test/viceModule.test.mjs",
20
20
  "test/groupedToolsShims.test.mjs",
21
+ "test/inputModule.test.mjs",
22
+ "test/batchAndDisplayState.test.mjs",
21
23
  "test/toolsTypes.test.mjs",
22
24
  "test/platformRegistry.test.mjs",
23
25
  "test/meta/program.test.mjs",
@@ -40,6 +42,11 @@ const supplementalTests = [
40
42
  "test/toolsCoverage.test.mjs",
41
43
  "test/audioModule.test.mjs",
42
44
  "test/updateReadme.test.mjs",
45
+ "test/toolsSchema.test.mjs",
46
+ "test/symbolRegistry.test.mjs",
47
+ "test/programRunnersModule.test.mjs",
48
+ "test/batchAndDisplayState.test.mjs",
49
+ "test/inputModule.test.mjs",
43
50
  "test/viceIntegration.test.mjs",
44
51
  ];
45
52
 
@@ -155,7 +162,8 @@ async function listRepoTestFiles(root) {
155
162
  return files;
156
163
  }
157
164
 
158
- function runCoverageLeg(leg, legDir, label, files) {
165
+ async function runCoverageLeg(leg, legDir, label, files) {
166
+ const knownTempFiles = new Set(await listCoverageTempFiles(coverageDir));
159
167
  return new Promise((resolve, reject) => {
160
168
  const outputPath = path.join(legDir, `${label}.lcov.info`);
161
169
  const args = [runner, "scripts/run-tests.ts", "--coverage", ...leg.args, ...files];
@@ -171,7 +179,7 @@ function runCoverageLeg(leg, legDir, label, files) {
171
179
  return;
172
180
  }
173
181
  try {
174
- await fs.copyFile(path.join(coverageDir, "lcov.info"), outputPath);
182
+ await copyCoverageOutput(coverageDir, outputPath, knownTempFiles);
175
183
  resolve(outputPath);
176
184
  } catch (error) {
177
185
  reject(error);
@@ -180,6 +188,56 @@ function runCoverageLeg(leg, legDir, label, files) {
180
188
  });
181
189
  }
182
190
 
191
+ export async function copyCoverageOutput(sourceDir, outputPath, knownTempFiles = new Set()) {
192
+ const stablePath = path.join(sourceDir, "lcov.info");
193
+ try {
194
+ await fs.copyFile(stablePath, outputPath);
195
+ return outputPath;
196
+ } catch (error) {
197
+ if (!isNodeErrorCode(error, "ENOENT")) {
198
+ throw error;
199
+ }
200
+ }
201
+
202
+ const tempFiles = await listCoverageTempFiles(sourceDir);
203
+ const newTempFiles = tempFiles.filter((filePath) => !knownTempFiles.has(filePath));
204
+ if (newTempFiles.length === 0) {
205
+ throw new Error(`Coverage run completed but no LCOV report was produced in ${sourceDir}`);
206
+ }
207
+
208
+ const newest = await newestFile(newTempFiles);
209
+ await fs.copyFile(newest, outputPath);
210
+ return outputPath;
211
+ }
212
+
213
+ async function listCoverageTempFiles(dir) {
214
+ let entries;
215
+ try {
216
+ entries = await fs.readdir(dir);
217
+ } catch (error) {
218
+ if (isNodeErrorCode(error, "ENOENT")) {
219
+ return [];
220
+ }
221
+ throw error;
222
+ }
223
+ return entries
224
+ .filter((entry) => /^\.lcov\.info\.[^.]+\.tmp$/.test(entry))
225
+ .map((entry) => path.join(dir, entry));
226
+ }
227
+
228
+ async function newestFile(filePaths) {
229
+ const stats = await Promise.all(filePaths.map(async (filePath) => ({
230
+ filePath,
231
+ mtimeMs: (await fs.stat(filePath)).mtimeMs,
232
+ })));
233
+ stats.sort((a, b) => b.mtimeMs - a.mtimeMs || a.filePath.localeCompare(b.filePath));
234
+ return stats[0].filePath;
235
+ }
236
+
237
+ function isNodeErrorCode(error, code) {
238
+ return Boolean(error && typeof error === "object" && error.code === code);
239
+ }
240
+
183
241
  async function readReport(filePath) {
184
242
  const text = await fs.readFile(filePath, "utf8");
185
243
  return parseReport(text);
@@ -306,4 +364,4 @@ function globToRegExp(glob) {
306
364
  }
307
365
  regex += "$";
308
366
  return new RegExp(regex);
309
- }
367
+ }
@@ -18,6 +18,7 @@ if (!fs.existsSync(defaultEmbeddingsDir)) {
18
18
  const env = {
19
19
  ...process.env,
20
20
  C64_TEST_TARGET: target,
21
+ VICE_VISIBLE: "false",
21
22
  };
22
23
 
23
24
  if (!env.RAG_EMBEDDINGS_DIR) {
@@ -16,6 +16,8 @@ const DEFAULT_VICE_MOCK_TEST_FILES = [
16
16
  "test/viceIntegration.test.mjs",
17
17
  "test/viceModule.test.mjs",
18
18
  "test/groupedToolsShims.test.mjs",
19
+ "test/inputModule.test.mjs",
20
+ "test/batchAndDisplayState.test.mjs",
19
21
  "test/toolsTypes.test.mjs",
20
22
  "test/platformRegistry.test.mjs",
21
23
  "test/meta/program.test.mjs",
@@ -140,6 +142,7 @@ export function buildMatrixEnv(
140
142
  const env: Record<string, string> = { ...envSource } as Record<string, string>;
141
143
  env.C64_MODE = platform;
142
144
  env.C64_TEST_TARGET = target === "device" ? "real" : "mock";
145
+ env.VICE_VISIBLE = "false";
143
146
 
144
147
  if (platform === "vice") {
145
148
  const useViceMock = target !== "device";
@@ -142,6 +142,8 @@ type GroupedOperation = {
142
142
  readonly op: string;
143
143
  readonly description: string;
144
144
  readonly required: readonly string[];
145
+ readonly optional: readonly string[];
146
+ readonly properties: Readonly<Record<string, JsonSchema | undefined>>;
145
147
  readonly notes: readonly string[];
146
148
  };
147
149
 
@@ -165,11 +167,27 @@ function toTableValue(values: readonly string[]): string {
165
167
  return values.join(", ");
166
168
  }
167
169
 
170
+ function formatInputName(name: string, schema?: JsonSchema): string {
171
+ const defaultSuffix = schema && Object.prototype.hasOwnProperty.call(schema, "default")
172
+ ? `=${JSON.stringify(schema.default)}`
173
+ : "";
174
+ return `\`${name}${defaultSuffix}\``;
175
+ }
176
+
177
+ function formatInputs(names: readonly string[], properties: Readonly<Record<string, JsonSchema | undefined>>): string {
178
+ return toTableValue(names.map((name) => formatInputName(name, properties[name])));
179
+ }
180
+
168
181
  function collectGroupedOperations(schema?: JsonSchema): readonly GroupedOperation[] {
169
182
  if (!schema || !isObject(schema)) {
170
183
  return [];
171
184
  }
172
185
 
186
+ const metadataOperations = collectFromOperationMetadata(schema);
187
+ if (metadataOperations.length > 0) {
188
+ return metadataOperations;
189
+ }
190
+
173
191
  const discriminator = (schema as JsonSchema & { discriminator?: { propertyName?: string } }).discriminator;
174
192
  const variants = (schema as JsonSchema & { oneOf?: readonly JsonSchema[] }).oneOf;
175
193
 
@@ -180,6 +198,57 @@ function collectGroupedOperations(schema?: JsonSchema): readonly GroupedOperatio
180
198
  return collectFromFlattenedSchema(schema);
181
199
  }
182
200
 
201
+ function collectFromOperationMetadata(schema: JsonSchema): readonly GroupedOperation[] {
202
+ const metadata = (schema as JsonSchema & {
203
+ readonly "x-c64bridge-operations"?: readonly unknown[];
204
+ })["x-c64bridge-operations"];
205
+
206
+ if (!Array.isArray(metadata)) {
207
+ return [];
208
+ }
209
+
210
+ const operations: GroupedOperation[] = [];
211
+
212
+ for (const entry of metadata) {
213
+ if (!isObject(entry)) {
214
+ continue;
215
+ }
216
+
217
+ const opName = getString(entry.op);
218
+ if (!opName) {
219
+ continue;
220
+ }
221
+
222
+ const inputSchema = isObject(entry.inputSchema) ? (entry.inputSchema as JsonSchema) : undefined;
223
+ const properties = (inputSchema?.properties ?? {}) as Record<string, JsonSchema | undefined>;
224
+ const requiredProps = Array.isArray(entry.required)
225
+ ? entry.required.filter((name): name is string => typeof name === "string" && name !== "op")
226
+ : [];
227
+ const optionalProps = Array.isArray(entry.optional)
228
+ ? entry.optional.filter((name): name is string => typeof name === "string" && name !== "op")
229
+ : Object.keys(properties).filter((name) => name !== "op" && !requiredProps.includes(name));
230
+
231
+ const notes: string[] = [];
232
+ const hasVerificationProperty = Object.keys(properties).some((name) =>
233
+ name.toLowerCase().startsWith("verify"),
234
+ );
235
+ if (hasVerificationProperty) {
236
+ notes.push("supports verify");
237
+ }
238
+
239
+ operations.push({
240
+ op: opName,
241
+ description: getString(entry.description, `Operation ${opName}`),
242
+ required: requiredProps,
243
+ optional: optionalProps,
244
+ properties,
245
+ notes,
246
+ });
247
+ }
248
+
249
+ return operations;
250
+ }
251
+
183
252
  function collectFromVariantList(variants: readonly JsonSchema[]): readonly GroupedOperation[] {
184
253
  const operations: GroupedOperation[] = [];
185
254
 
@@ -224,6 +293,8 @@ function collectFromVariantList(variants: readonly JsonSchema[]): readonly Group
224
293
  op: opName,
225
294
  description,
226
295
  required: requiredProps,
296
+ optional: Object.keys(properties).filter((name) => name !== "op" && !requiredProps.includes(name)),
297
+ properties,
227
298
  notes,
228
299
  });
229
300
  }
@@ -258,6 +329,8 @@ function collectFromFlattenedSchema(schema: JsonSchema): readonly GroupedOperati
258
329
  op: opName,
259
330
  description: descriptionMap.get(opName) ?? `Operation ${opName}`,
260
331
  required: [] as readonly string[],
332
+ optional: Object.keys(properties).filter((name) => name !== "op"),
333
+ properties,
261
334
  notes,
262
335
  }));
263
336
  }
@@ -302,6 +375,9 @@ export function renderToolsSection(modules: readonly ToolModuleDescriptor[] = de
302
375
  if (operations.length === 0) {
303
376
  lines.push("_No operations defined._");
304
377
  } else {
378
+ if (!lines.includes("_Address range convention: `address` + `length` means start address plus byte count; `startAddress` + `endAddress` means inclusive bounds._")) {
379
+ lines.splice(2, 0, "_Address range convention: `address` + `length` means start address plus byte count; `startAddress` + `endAddress` means inclusive bounds._", "");
380
+ }
305
381
  const toolPlatforms: readonly string[] = tool.metadata.platforms ?? ["c64u"];
306
382
  const opPlatformOverrides: Record<string, readonly string[]> = tool.metadata.operationPlatforms ?? {};
307
383
  const operationRows = operations
@@ -312,14 +388,15 @@ export function renderToolsSection(modules: readonly ToolModuleDescriptor[] = de
312
388
  return [
313
389
  `\`${operation.op}\``,
314
390
  escapeCell(operation.description),
315
- escapeCell(toTableValue(operation.required.map((name) => `\`${name}\``))),
391
+ escapeCell(formatInputs(operation.required, operation.properties)),
392
+ escapeCell(formatInputs(operation.optional, operation.properties)),
316
393
  operation.notes.length ? escapeCell(operation.notes.join(", ")) : "—",
317
394
  effectivePlatforms.includes("c64u") ? "✅" : "",
318
395
  effectivePlatforms.includes("vice") ? "✅" : "",
319
396
  ];
320
397
  });
321
398
 
322
- lines.push(renderTable(["Operation", "Description", "Required Inputs", "Notes", "C64U", "VICE"], operationRows));
399
+ lines.push(renderTable(["Operation", "Description", "Required Inputs", "Optional Inputs", "Notes", "C64U", "VICE"], operationRows));
323
400
  }
324
401
 
325
402
  lines.push("");
package/server.json CHANGED
@@ -8,12 +8,12 @@
8
8
  "source": "github"
9
9
  },
10
10
  "websiteUrl": "https://github.com/chrisgleissner/c64bridge",
11
- "version": "0.9.0",
11
+ "version": "0.9.2",
12
12
  "packages": [
13
13
  {
14
14
  "registryType": "npm",
15
15
  "identifier": "c64bridge",
16
- "version": "0.9.0",
16
+ "version": "0.9.2",
17
17
  "transport": {
18
18
  "type": "stdio"
19
19
  },