c64bridge 0.8.0 → 0.9.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 (79) hide show
  1. package/.github/prompts/steer.prompt.md +55 -0
  2. package/AGENTS.md +9 -7
  3. package/README.md +139 -95
  4. package/data/embeddings_asm.json +1 -1
  5. package/data/embeddings_basic.json +1 -1
  6. package/data/embeddings_hardware.json +1 -1
  7. package/data/embeddings_mixed.json +1 -1
  8. package/data/embeddings_other.json +1 -1
  9. package/data/graphics/petscii-style-guide.md +4 -4
  10. package/data/graphics/sprite-charset-best-practices.md +9 -9
  11. package/data/vice/vice-binary-monitor-spec.md +921 -0
  12. package/dist/.tsbuildinfo +1 -1
  13. package/dist/device.js +86 -10
  14. package/dist/mcp-server.js +1 -1
  15. package/dist/prompts/registry.js +27 -25
  16. package/dist/rag/knowledgeIndex.js +112 -79
  17. package/dist/rag/resourceUris.js +93 -0
  18. package/dist/tools/audio.js +11 -11
  19. package/dist/tools/debug.js +1 -1
  20. package/dist/tools/developer.js +13 -13
  21. package/dist/tools/graphics.js +10 -10
  22. package/dist/tools/machineControl.js +5 -5
  23. package/dist/tools/memory.js +19 -11
  24. package/dist/tools/meta/index.js +1 -1
  25. package/dist/tools/printer.js +11 -12
  26. package/dist/tools/programRunners.js +16 -16
  27. package/dist/tools/rag.js +9 -9
  28. package/dist/tools/registry/config.js +1 -1
  29. package/dist/tools/registry/disk.js +1 -1
  30. package/dist/tools/registry/drive.js +1 -1
  31. package/dist/tools/registry/extract.js +1 -1
  32. package/dist/tools/registry/graphics.js +1 -1
  33. package/dist/tools/registry/memory.js +1 -1
  34. package/dist/tools/registry/platform.js +1 -1
  35. package/dist/tools/registry/printer.js +1 -1
  36. package/dist/tools/registry/program.js +1 -1
  37. package/dist/tools/registry/rag.js +1 -1
  38. package/dist/tools/registry/sound.js +1 -1
  39. package/dist/tools/registry/stream.js +1 -1
  40. package/dist/tools/registry/system.js +1 -1
  41. package/dist/tools/storage.js +15 -15
  42. package/dist/tools/streaming.js +3 -3
  43. package/dist/tools/types.js +41 -4
  44. package/dist/tools/vice.js +2 -5
  45. package/dist/vice/process.js +79 -2
  46. package/dist/vice/readiness.js +35 -0
  47. package/dist/vice/smokeOptions.js +44 -0
  48. package/dist/viceRunner.js +2 -0
  49. package/doc/developer.md +10 -2
  50. package/doc/plans/prompts/README.md +7 -7
  51. package/mcp/prompts.json +42 -59
  52. package/mcp/protocol-examples.json +2 -2
  53. package/mcp/resources.json +179 -166
  54. package/mcp/schemas/c64_config.schema.json +109 -326
  55. package/mcp/schemas/c64_debug.schema.json +158 -337
  56. package/mcp/schemas/c64_disk.schema.json +180 -262
  57. package/mcp/schemas/c64_drive.schema.json +31 -111
  58. package/mcp/schemas/c64_extract.schema.json +193 -291
  59. package/mcp/schemas/c64_graphics.schema.json +208 -308
  60. package/mcp/schemas/c64_memory.schema.json +94 -144
  61. package/mcp/schemas/c64_printer.schema.json +151 -205
  62. package/mcp/schemas/c64_program.schema.json +229 -367
  63. package/mcp/schemas/c64_rag.schema.json +25 -59
  64. package/mcp/schemas/c64_select_backend.schema.json +19 -26
  65. package/mcp/schemas/c64_sound.schema.json +346 -622
  66. package/mcp/schemas/c64_stream.schema.json +25 -54
  67. package/mcp/schemas/c64_system.schema.json +79 -212
  68. package/mcp/schemas/c64_vice.schema.json +24 -55
  69. package/mcp/server.json +1 -1
  70. package/mcp/tools.json +1935 -3443
  71. package/mcp.json +10 -9
  72. package/package.json +20 -15
  73. package/scripts/fetch-external-sources.mjs +1 -2
  74. package/scripts/prepare-release.mjs +59 -2
  75. package/scripts/run-coverage.mjs +26 -1
  76. package/scripts/run-tests.mjs +2 -2
  77. package/scripts/run-tests.ts +273 -52
  78. package/scripts/update-readme.ts +54 -2
  79. package/server.json +94 -0
@@ -252,7 +252,7 @@ export const diskModuleGroup = defineToolModule({
252
252
  domain: "storage",
253
253
  summary: "Grouped disk image management, mounting, and discovery tools.",
254
254
  supportedPlatforms: ["c64u", "vice"],
255
- resources: ["c64://context/bootstrap"],
255
+ resources: ["c64://guide/bootstrap"],
256
256
  prompts: ["drive-management"],
257
257
  defaultTags: ["storage", "drive"],
258
258
  workflowHints: [
@@ -34,7 +34,7 @@ export const driveModuleGroup = defineToolModule({
34
34
  domain: "drive",
35
35
  summary: "Grouped drive power, reset, ROM, and mode helpers.",
36
36
  supportedPlatforms: ["c64u", "vice"],
37
- resources: ["c64://context/bootstrap"],
37
+ resources: ["c64://guide/bootstrap"],
38
38
  defaultTags: ["drive", "hardware"],
39
39
  workflowHints: [
40
40
  "State the resulting power/mode/ROM so the user can reconcile IEC behaviour.",
@@ -33,7 +33,7 @@ const extractOperationHandlers = createOperationHandlers(extractOperations);
33
33
  export const extractModule = defineToolModule({
34
34
  domain: "extract",
35
35
  summary: "Grouped extraction helpers for sprites, charsets, memory dumps, and diagnostics.",
36
- resources: ["c64://context/bootstrap", "c64://specs/basic", "c64://specs/assembly"],
36
+ resources: ["c64://guide/bootstrap", "c64://basic/spec", "c64://assembly/6510-spec"],
37
37
  defaultTags: ["extract", "diagnostics"],
38
38
  workflowHints: [
39
39
  "Pause the machine when advised so dumps and sprite scans remain stable.",
@@ -110,7 +110,7 @@ const graphicsOperationHandlers = createOperationHandlers(graphicsOperations);
110
110
  export const graphicsModuleGroup = defineToolModule({
111
111
  domain: "graphics",
112
112
  summary: "Grouped frame capture and graphics rendering helpers.",
113
- resources: ["c64://specs/vic", "c64://specs/basic", "c64://specs/assembly"],
113
+ resources: ["c64://graphics/vic/spec", "c64://basic/spec", "c64://assembly/6510-spec"],
114
114
  prompts: ["graphics-demo", "basic-program", "assembly-program"],
115
115
  defaultTags: ["graphics", "vic"],
116
116
  workflowHints: [
@@ -31,7 +31,7 @@ export const memoryModuleGroup = defineToolModule({
31
31
  domain: "memory",
32
32
  summary: "Grouped memory, screen, and polling operations.",
33
33
  supportedPlatforms: ["c64u", "vice"],
34
- resources: ["c64://context/bootstrap", "c64://specs/basic", "c64://specs/assembly"],
34
+ resources: ["c64://guide/bootstrap", "c64://basic/spec", "c64://assembly/6510-spec"],
35
35
  prompts: ["memory-debug", "basic-program", "assembly-program"],
36
36
  defaultTags: ["memory", "debug"],
37
37
  workflowHints: [
@@ -92,7 +92,7 @@ export const platformModuleGroup = defineToolModule({
92
92
  domain: "platform",
93
93
  summary: "Runtime backend selection and platform-status coordination.",
94
94
  supportedPlatforms: ["c64u", "vice"],
95
- resources: ["c64://platform/status", "c64://context/bootstrap"],
95
+ resources: ["c64://platform/status", "c64://guide/bootstrap"],
96
96
  prompts: [],
97
97
  defaultTags: ["platform", "backend", "grouped"],
98
98
  workflowHints: [
@@ -117,7 +117,7 @@ function invokePrintTool(toolName, rawArgs, ctx) {
117
117
  export const printerModuleGroup = defineToolModule({
118
118
  domain: "printer",
119
119
  summary: "Grouped printer text, bitmap, and character definition helpers.",
120
- resources: ["c64://context/bootstrap"],
120
+ resources: ["c64://guide/bootstrap"],
121
121
  defaultTags: ["printer", "device"],
122
122
  workflowHints: [
123
123
  "Mention device/secondary addresses so the user knows which printer workflow ran.",
@@ -50,7 +50,7 @@ const programOperationHandlers = createOperationHandlers(programOperations);
50
50
  export const programModule = defineToolModule({
51
51
  domain: "programs",
52
52
  summary: "Grouped program upload, run, and orchestration workflows.",
53
- resources: ["c64://context/bootstrap", "c64://specs/basic", "c64://specs/assembly"],
53
+ resources: ["c64://guide/bootstrap", "c64://basic/spec", "c64://assembly/6510-spec"],
54
54
  prompts: ["basic-program", "assembly-program"],
55
55
  defaultTags: ["programs", "execution"],
56
56
  workflowHints: [
@@ -18,7 +18,7 @@ const ragOperationHandlers = createOperationHandlers(ragOperations);
18
18
  export const ragModuleGroup = defineToolModule({
19
19
  domain: "rag",
20
20
  summary: "Grouped retrieval helpers for BASIC and assembly references.",
21
- resources: ["c64://specs/basic", "c64://specs/assembly", "c64://context/bootstrap"],
21
+ resources: ["c64://basic/spec", "c64://assembly/6510-spec", "c64://guide/bootstrap"],
22
22
  prompts: ["basic-program", "assembly-program"],
23
23
  defaultTags: ["rag", "search"],
24
24
  workflowHints: [
@@ -140,7 +140,7 @@ export const soundModuleGroup = defineToolModule({
140
140
  domain: "audio",
141
141
  summary: "Grouped SID control, playback, composition, and analysis operations.",
142
142
  supportedPlatforms: ["c64u", "vice"],
143
- resources: ["c64://specs/sid", "c64://specs/sidwave", "c64://docs/sid/file-structure"],
143
+ resources: ["c64://sound/sid/spec", "c64://sound/sidwave/spec", "c64://sound/sid/file-format"],
144
144
  prompts: ["sid-music"],
145
145
  defaultTags: ["sid", "audio"],
146
146
  workflowHints: [
@@ -18,7 +18,7 @@ const streamOperationHandlers = createOperationHandlers(streamOperations);
18
18
  export const streamModule = defineToolModule({
19
19
  domain: "stream",
20
20
  summary: "Grouped streaming helpers for starting and stopping Ultimate capture sessions.",
21
- resources: ["c64://docs/index", "c64://specs/sid"],
21
+ resources: ["c64://guide/index", "c64://sound/sid/spec"],
22
22
  prompts: ["sid-music"],
23
23
  defaultTags: ["stream", "monitor"],
24
24
  workflowHints: [
@@ -66,7 +66,7 @@ export const systemModuleGroup = defineToolModule({
66
66
  domain: "system",
67
67
  summary: "Grouped machine control and background task orchestration.",
68
68
  supportedPlatforms: ["c64u", "vice"],
69
- resources: ["c64://context/bootstrap"],
69
+ resources: ["c64://guide/bootstrap"],
70
70
  prompts: ["memory-debug"],
71
71
  defaultTags: ["system", "control"],
72
72
  workflowHints: [
@@ -144,7 +144,7 @@ export const storageModule = defineToolModule({
144
144
  domain: "storage",
145
145
  summary: "Drive management, disk image creation, and file inspection utilities.",
146
146
  supportedPlatforms: ["c64u", "vice"],
147
- resources: ["c64://context/bootstrap"],
147
+ resources: ["c64://guide/bootstrap"],
148
148
  prompts: ["memory-debug"],
149
149
  defaultTags: ["drive", "storage"],
150
150
  workflowHints: [
@@ -154,10 +154,10 @@ export const storageModule = defineToolModule({
154
154
  tools: [
155
155
  {
156
156
  name: "drives_list",
157
- description: "List Ultimate drive slots and their currently mounted images. Read c64://context/bootstrap for drive safety.",
157
+ description: "List Ultimate drive slots and their currently mounted images. Read c64://guide/bootstrap for drive safety.",
158
158
  summary: "Fetches firmware drive status including mounted images, power state, and modes.",
159
159
  inputSchema: drivesListArgsSchema.jsonSchema,
160
- relatedResources: ["c64://context/bootstrap"],
160
+ relatedResources: ["c64://guide/bootstrap"],
161
161
  tags: ["drive", "status"],
162
162
  prerequisites: [],
163
163
  examples: [
@@ -193,7 +193,7 @@ export const storageModule = defineToolModule({
193
193
  description: "Mount a disk image onto a specific Ultimate drive slot.",
194
194
  summary: "Validates parameters and instructs the firmware to mount an image with optional type/mode overrides.",
195
195
  inputSchema: driveMountArgsSchema.jsonSchema,
196
- relatedResources: ["c64://context/bootstrap"],
196
+ relatedResources: ["c64://guide/bootstrap"],
197
197
  tags: ["drive", "mount"],
198
198
  prerequisites: ["drives_list"],
199
199
  examples: [
@@ -250,7 +250,7 @@ export const storageModule = defineToolModule({
250
250
  description: "Remove the currently mounted disk image from an Ultimate drive slot.",
251
251
  summary: "Ejects the image and leaves the drive empty without powering it off.",
252
252
  inputSchema: driveOnlyArgsSchema.jsonSchema,
253
- relatedResources: ["c64://context/bootstrap"],
253
+ relatedResources: ["c64://guide/bootstrap"],
254
254
  tags: ["drive", "unmount"],
255
255
  prerequisites: ["drives_list"],
256
256
  examples: [
@@ -292,7 +292,7 @@ export const storageModule = defineToolModule({
292
292
  description: "Reset the selected Ultimate drive slot.",
293
293
  summary: "Issues a IEC reset for the chosen drive without cycling the entire machine.",
294
294
  inputSchema: driveOnlyArgsSchema.jsonSchema,
295
- relatedResources: ["c64://context/bootstrap"],
295
+ relatedResources: ["c64://guide/bootstrap"],
296
296
  tags: ["drive", "reset"],
297
297
  prerequisites: ["drives_list"],
298
298
  examples: [
@@ -334,7 +334,7 @@ export const storageModule = defineToolModule({
334
334
  description: "Power on a specific Ultimate drive slot.",
335
335
  summary: "Ensures the selected drive is reported as powered and ready on the IEC bus.",
336
336
  inputSchema: driveOnlyArgsSchema.jsonSchema,
337
- relatedResources: ["c64://context/bootstrap"],
337
+ relatedResources: ["c64://guide/bootstrap"],
338
338
  tags: ["drive", "power"],
339
339
  prerequisites: ["drives_list"],
340
340
  examples: [
@@ -377,7 +377,7 @@ export const storageModule = defineToolModule({
377
377
  description: "Power off a specific Ultimate drive slot.",
378
378
  summary: "Disables the drive without altering mounted media so it can be re-enabled later.",
379
379
  inputSchema: driveOnlyArgsSchema.jsonSchema,
380
- relatedResources: ["c64://context/bootstrap"],
380
+ relatedResources: ["c64://guide/bootstrap"],
381
381
  tags: ["drive", "power"],
382
382
  prerequisites: ["drives_list"],
383
383
  examples: [
@@ -420,7 +420,7 @@ export const storageModule = defineToolModule({
420
420
  description: "Temporarily load a custom ROM into an Ultimate drive slot.",
421
421
  summary: "Points the drive at an alternative ROM file without flashing permanent storage.",
422
422
  inputSchema: driveLoadRomArgsSchema.jsonSchema,
423
- relatedResources: ["c64://context/bootstrap"],
423
+ relatedResources: ["c64://guide/bootstrap"],
424
424
  tags: ["drive", "rom"],
425
425
  prerequisites: ["drives_list"],
426
426
  examples: [
@@ -463,7 +463,7 @@ export const storageModule = defineToolModule({
463
463
  description: "Set the emulation mode for an Ultimate drive slot (1541/1571/1581).",
464
464
  summary: "Switches the drive hardware profile to match the mounted media and desired behaviour.",
465
465
  inputSchema: driveModeArgsSchema.jsonSchema,
466
- relatedResources: ["c64://context/bootstrap"],
466
+ relatedResources: ["c64://guide/bootstrap"],
467
467
  tags: ["drive", "mode"],
468
468
  prerequisites: ["drives_list"],
469
469
  examples: [
@@ -506,7 +506,7 @@ export const storageModule = defineToolModule({
506
506
  description: "Inspect metadata for a file on the Ultimate filesystem.",
507
507
  summary: "Queries the REST API for file size, type, and other details useful when managing disk images.",
508
508
  inputSchema: fileInfoArgsSchema.jsonSchema,
509
- relatedResources: ["c64://context/bootstrap"],
509
+ relatedResources: ["c64://guide/bootstrap"],
510
510
  tags: ["storage", "info"],
511
511
  prerequisites: [],
512
512
  examples: [
@@ -543,7 +543,7 @@ export const storageModule = defineToolModule({
543
543
  description: "Create a blank D64 disk image on the Ultimate filesystem.",
544
544
  summary: "Allocates a 35 or 40 track D64 image with an optional disk name.",
545
545
  inputSchema: createD64ArgsSchema.jsonSchema,
546
- relatedResources: ["c64://context/bootstrap"],
546
+ relatedResources: ["c64://guide/bootstrap"],
547
547
  tags: ["disk", "create"],
548
548
  prerequisites: ["drives_list"],
549
549
  examples: [
@@ -594,7 +594,7 @@ export const storageModule = defineToolModule({
594
594
  description: "Create a blank D71 disk image on the Ultimate filesystem.",
595
595
  summary: "Allocates a 1571-compatible disk image with optional labelling.",
596
596
  inputSchema: createD71ArgsSchema.jsonSchema,
597
- relatedResources: ["c64://context/bootstrap"],
597
+ relatedResources: ["c64://guide/bootstrap"],
598
598
  tags: ["disk", "create"],
599
599
  prerequisites: ["drives_list"],
600
600
  examples: [
@@ -642,7 +642,7 @@ export const storageModule = defineToolModule({
642
642
  description: "Create a blank D81 disk image on the Ultimate filesystem.",
643
643
  summary: "Allocates a 1581-compatible disk image with optional labelling.",
644
644
  inputSchema: createD81ArgsSchema.jsonSchema,
645
- relatedResources: ["c64://context/bootstrap"],
645
+ relatedResources: ["c64://guide/bootstrap"],
646
646
  tags: ["disk", "create"],
647
647
  prerequisites: ["drives_list"],
648
648
  examples: [
@@ -690,7 +690,7 @@ export const storageModule = defineToolModule({
690
690
  description: "Create a blank DNP disk image on the Ultimate filesystem.",
691
691
  summary: "Allocates a CMD-native DNP image with custom track count and optional label.",
692
692
  inputSchema: createDnpArgsSchema.jsonSchema,
693
- relatedResources: ["c64://context/bootstrap"],
693
+ relatedResources: ["c64://guide/bootstrap"],
694
694
  tags: ["disk", "create"],
695
695
  prerequisites: [
696
696
  "drives_list"
@@ -36,8 +36,8 @@ export const streamingModule = defineToolModule({
36
36
  domain: "streaming",
37
37
  summary: "Long-running or streaming workflows such as audio capture or SID playback monitoring.",
38
38
  resources: [
39
- "c64://specs/sid",
40
- "c64://docs/index",
39
+ "c64://sound/sid/spec",
40
+ "c64://guide/index",
41
41
  ],
42
42
  prompts: ["sid-music"],
43
43
  defaultLifecycle: "stream",
@@ -50,7 +50,7 @@ export const streamingModule = defineToolModule({
50
50
  tools: [
51
51
  {
52
52
  name: "stream_start",
53
- description: "Start an Ultimate streaming session (video/audio/debug) targeting a host:port destination. See c64://docs/index for usage notes.",
53
+ description: "Start an Ultimate streaming session (video/audio/debug) targeting a host:port destination. See c64://guide/index for usage notes.",
54
54
  summary: "Validates stream arguments and forwards the request to firmware, returning status metadata.",
55
55
  inputSchema: streamStartArgsSchema.jsonSchema,
56
56
  tags: ["stream", "start"],
@@ -30,13 +30,50 @@ export function discriminatedUnionSchema(options) {
30
30
  if (!variants || variants.length === 0) {
31
31
  throw new Error("Discriminated union schemas require at least one variant.");
32
32
  }
33
+ // Flatten the discriminated union into a single object schema.
34
+ // Claude's API does not support oneOf/allOf/anyOf at the top level,
35
+ // so we merge all variant properties and use an enum for the discriminator.
36
+ const opValues = [];
37
+ const mergedProperties = {};
38
+ const opDescriptions = [];
39
+ for (const variant of variants) {
40
+ const variantProps = variant.properties ?? {};
41
+ const discProp = variantProps[discriminator];
42
+ const opValue = discProp?.const;
43
+ if (opValue) {
44
+ opValues.push(opValue);
45
+ const variantDesc = variant.description ?? discProp?.description ?? "";
46
+ if (variantDesc) {
47
+ opDescriptions.push(`${opValue}: ${variantDesc}`);
48
+ }
49
+ }
50
+ for (const [key, propSchema] of Object.entries(variantProps)) {
51
+ if (key === discriminator)
52
+ continue;
53
+ if (!mergedProperties[key]) {
54
+ mergedProperties[key] = propSchema;
55
+ }
56
+ }
57
+ }
58
+ const opPropertySchema = opValues.length > 0
59
+ ? { type: "string", enum: opValues, description: `Selects the operation.` }
60
+ : { type: "string", description: `Selects the operation.` };
61
+ const fullDescription = description
62
+ ? (opDescriptions.length > 0
63
+ ? `${description}\n\nOperations:\n${opDescriptions.map((d) => `- ${d}`).join("\n")}`
64
+ : description)
65
+ : (opDescriptions.length > 0
66
+ ? `Operations:\n${opDescriptions.map((d) => `- ${d}`).join("\n")}`
67
+ : undefined);
33
68
  const schema = {
34
69
  type: "object",
35
- ...(description ? { description } : {}),
36
- oneOf: [...variants],
37
- discriminator: {
38
- propertyName: discriminator,
70
+ ...(fullDescription ? { description: fullDescription } : {}),
71
+ properties: {
72
+ [discriminator]: opPropertySchema,
73
+ ...mergedProperties,
39
74
  },
75
+ required: [discriminator],
76
+ additionalProperties: false,
40
77
  };
41
78
  return schema;
42
79
  }
@@ -15,11 +15,8 @@ const SAFE_RESOURCE_PREFIXES = [
15
15
  ];
16
16
  const resourceValueSchema = {
17
17
  jsonSchema: {
18
+ type: ["string", "integer"],
18
19
  description: "VICE resource value (string or integer).",
19
- oneOf: [
20
- { type: "string" },
21
- { type: "integer" },
22
- ],
23
20
  },
24
21
  parse(value, path) {
25
22
  const resolvedPath = path ?? "$.value";
@@ -110,7 +107,7 @@ const viceOperationDispatcher = createOperationDispatcher("c64_vice", viceOperat
110
107
  export const viceModuleGroup = defineToolModule({
111
108
  domain: "vice",
112
109
  summary: "VICE emulator helpers for safe runtime resource access.",
113
- resources: ["c64://specs/memory-map"],
110
+ resources: ["c64://memory/map"],
114
111
  prompts: ["assembly-program", "memory-debug"],
115
112
  defaultTags: ["vice", "emulator"],
116
113
  workflowHints: [
@@ -3,10 +3,37 @@ import fs from "node:fs";
3
3
  import net from "node:net";
4
4
  import { createOutputTailCapture, getDiagnosticsSessionInfo, writeDiagnosticEvent } from "../diagnostics.js";
5
5
  const DEFAULT_DISPLAY = ":99";
6
+ function hasDisplayArtifacts(display) {
7
+ const match = /^:([0-9]+)$/.exec(display.trim());
8
+ if (!match) {
9
+ return false;
10
+ }
11
+ const displayNumber = match[1];
12
+ return fs.existsSync(`/tmp/.X11-unix/X${displayNumber}`) || fs.existsSync(`/tmp/.X${displayNumber}-lock`);
13
+ }
14
+ export function resolveXvfbDisplayForLaunch(display) {
15
+ const explicitDisplay = process.env.VICE_XVFB_DISPLAY?.trim();
16
+ if (explicitDisplay) {
17
+ return explicitDisplay;
18
+ }
19
+ if (display !== DEFAULT_DISPLAY || !hasDisplayArtifacts(display)) {
20
+ return display;
21
+ }
22
+ const baseDisplay = Number.parseInt(display.slice(1), 10);
23
+ for (let candidate = baseDisplay + 1; candidate < baseDisplay + 100; candidate += 1) {
24
+ const candidateDisplay = `:${candidate}`;
25
+ if (!hasDisplayArtifacts(candidateDisplay)) {
26
+ return candidateDisplay;
27
+ }
28
+ }
29
+ return display;
30
+ }
6
31
  export function delay(ms) {
7
32
  return new Promise((resolve) => setTimeout(resolve, ms));
8
33
  }
9
34
  function hasGraphicalSession() {
35
+ if (process.platform === "darwin" || process.platform === "win32")
36
+ return true;
10
37
  return configuredDisplay() !== undefined || configuredWaylandDisplay() !== undefined;
11
38
  }
12
39
  function configuredDisplay() {
@@ -64,6 +91,53 @@ export async function waitForPort(host, port, timeoutMs = 4000) {
64
91
  }
65
92
  throw new Error(`Timeout waiting for VICE monitor at ${host}:${port}`);
66
93
  }
94
+ async function sendResumeMonitorCommand(host, port, timeoutMs = 1500) {
95
+ const deadline = Date.now() + timeoutMs;
96
+ let lastError;
97
+ while (Date.now() < deadline) {
98
+ try {
99
+ await new Promise((resolve, reject) => {
100
+ const socket = net.connect({ host, port });
101
+ const cleanup = () => {
102
+ socket.removeAllListeners();
103
+ socket.destroy();
104
+ };
105
+ const packet = Buffer.alloc(11);
106
+ packet[0] = 0x02;
107
+ packet[1] = 0x02;
108
+ packet.writeUInt32LE(0, 2);
109
+ packet.writeUInt32LE(1, 6);
110
+ packet[10] = 0xaa;
111
+ socket.setTimeout(500, () => {
112
+ cleanup();
113
+ reject(new Error(`Timed out sending VICE monitor resume command to ${host}:${port}`));
114
+ });
115
+ socket.once("error", (error) => {
116
+ cleanup();
117
+ reject(error);
118
+ });
119
+ socket.once("connect", () => {
120
+ socket.write(packet, (error) => {
121
+ cleanup();
122
+ if (error) {
123
+ reject(error);
124
+ return;
125
+ }
126
+ resolve();
127
+ });
128
+ });
129
+ });
130
+ return;
131
+ }
132
+ catch (error) {
133
+ lastError = error;
134
+ await delay(50);
135
+ }
136
+ }
137
+ throw lastError instanceof Error
138
+ ? lastError
139
+ : new Error(`Failed to resume VICE monitor at ${host}:${port}`);
140
+ }
67
141
  async function waitForXvfb(display, timeoutMs = 5000) {
68
142
  const match = /^:([0-9]+)/.exec(display.trim());
69
143
  if (!match) {
@@ -113,7 +187,8 @@ export async function terminateProcess(child, signal = "SIGTERM", timeoutMs = 10
113
187
  }
114
188
  export async function startViceProcess(options) {
115
189
  const debugEnabled = process.env.VICE_DEVICE_TEST_DEBUG === "1";
116
- const { useXvfb, display } = shouldUseXvfb(options.visible);
190
+ const { useXvfb, display: requestedDisplay } = shouldUseXvfb(options.visible);
191
+ const display = useXvfb ? resolveXvfbDisplayForLaunch(requestedDisplay) : requestedDisplay;
117
192
  const viceEnv = { ...process.env };
118
193
  let xvfb = null;
119
194
  const xvfbOutput = createOutputTailCapture("xvfb");
@@ -157,6 +232,7 @@ export async function startViceProcess(options) {
157
232
  });
158
233
  viceEnv.DISPLAY = display;
159
234
  await waitForXvfb(display);
235
+ await delay(300);
160
236
  }
161
237
  const args = [
162
238
  "-binarymonitor",
@@ -216,7 +292,8 @@ export async function startViceProcess(options) {
216
292
  child.once("exit", onExit);
217
293
  child.once("error", onError);
218
294
  waitForPort(options.host, options.port)
219
- .then(() => {
295
+ .then(async () => {
296
+ await sendResumeMonitorCommand(options.host, options.port);
220
297
  if (debugEnabled) {
221
298
  console.error("[vice-process] monitor port is ready", { host: options.host, port: options.port });
222
299
  }
@@ -30,6 +30,41 @@ export async function waitForScreenPattern(bm, pattern, timeoutMs, tickMs = 50,
30
30
  await exitMonitorIfNeeded(bm);
31
31
  return idx;
32
32
  }
33
+ export async function waitForStableScreenPattern(bm, pattern, timeoutMs, tickMs = 50, stableReads = 2, onRead, between) {
34
+ const requiredStableReads = Math.max(1, stableReads);
35
+ const start = nowNs();
36
+ let idx = -1;
37
+ let stableIdx = -1;
38
+ let consecutiveMatches = 0;
39
+ while (msSince(start) < timeoutMs) {
40
+ const t = nowNs();
41
+ const screen = await bm.memGet(0x0400, 0x0400 + 999);
42
+ onRead?.("bm_read_screen", t);
43
+ idx = screen.indexOf(pattern);
44
+ if (idx >= 0) {
45
+ if (idx === stableIdx) {
46
+ consecutiveMatches += 1;
47
+ }
48
+ else {
49
+ stableIdx = idx;
50
+ consecutiveMatches = 1;
51
+ }
52
+ if (consecutiveMatches >= requiredStableReads) {
53
+ break;
54
+ }
55
+ }
56
+ else {
57
+ stableIdx = -1;
58
+ consecutiveMatches = 0;
59
+ }
60
+ await exitMonitorIfNeeded(bm);
61
+ if (between)
62
+ await between();
63
+ await new Promise((r) => setTimeout(r, Math.max(1, tickMs)));
64
+ }
65
+ await exitMonitorIfNeeded(bm);
66
+ return consecutiveMatches >= requiredStableReads ? idx : -1;
67
+ }
33
68
  /** Wait until any non-blank (non-0x00/0x20) character appears on the text screen. */
34
69
  export async function waitForAnyScreenText(bm, timeoutMs, tickMs = 50, onRead, between) {
35
70
  const start = nowNs();
@@ -0,0 +1,44 @@
1
+ const TRUE_VALUES = new Set(["1", "true", "yes", "on"]);
2
+ const FALSE_VALUES = new Set(["0", "false", "no", "off"]);
3
+ function parseVicePort(value) {
4
+ const normalized = value?.trim();
5
+ if (!normalized) {
6
+ return { configuredPort: 6502, hasExplicitPort: false };
7
+ }
8
+ const parsed = Number(normalized);
9
+ if (!Number.isInteger(parsed) || parsed < 1 || parsed > 65535) {
10
+ return { configuredPort: 6502, hasExplicitPort: false };
11
+ }
12
+ return { configuredPort: parsed, hasExplicitPort: true };
13
+ }
14
+ export function parseEnvBoolean(value) {
15
+ const normalized = value?.trim().toLowerCase();
16
+ if (!normalized) {
17
+ return undefined;
18
+ }
19
+ if (TRUE_VALUES.has(normalized)) {
20
+ return true;
21
+ }
22
+ if (FALSE_VALUES.has(normalized)) {
23
+ return false;
24
+ }
25
+ return undefined;
26
+ }
27
+ export function resolveViceSmokeOptions(env = process.env, argv = process.argv.slice(2)) {
28
+ const visibleDemo = argv.includes("--visible-demo") || parseEnvBoolean(env.VICE_VISIBLE_DEMO) === true;
29
+ const useMock = String(env.VICE_TEST_TARGET ?? "").trim().toLowerCase() === "mock";
30
+ const { configuredPort, hasExplicitPort } = parseVicePort(env.VICE_PORT);
31
+ const visible = visibleDemo || parseEnvBoolean(env.VICE_VISIBLE) === true;
32
+ const keepOpen = visibleDemo || parseEnvBoolean(env.VICE_KEEP_OPEN) === true;
33
+ const warpSetting = parseEnvBoolean(env.VICE_WARP);
34
+ return {
35
+ useMock,
36
+ configuredPort,
37
+ hasExplicitPort,
38
+ visible,
39
+ keepOpen,
40
+ warp: useMock ? true : visibleDemo ? false : warpSetting ?? true,
41
+ display: env.DISPLAY || ":99",
42
+ visibleDemo,
43
+ };
44
+ }
@@ -65,6 +65,8 @@ function which(binary) {
65
65
  return null;
66
66
  }
67
67
  function shouldUseXvfb() {
68
+ if (process.platform === "darwin" || process.platform === "win32")
69
+ return false;
68
70
  if (process.env.FORCE_XVFB === "1")
69
71
  return true;
70
72
  const ciValue = (process.env.CI || "").toLowerCase();
package/doc/developer.md CHANGED
@@ -119,8 +119,16 @@ Environment knobs: `RAG_EMBEDDINGS_DIR`, `RAG_BUILD_ON_START`, `RAG_REINDEX_INTE
119
119
 
120
120
  - `./build check` — build + test matrix in one pass
121
121
  - `./build changelog` — update CHANGELOG draft
122
- - `./build release --version <version>` — bump version, regenerate manifest, prepend changelog notes
123
- - Published package ships [`dist/`](../dist/), [`doc/`](../doc/), [`data/`](../data/), [`scripts/`](../scripts/), [`generated/`](../generated/), and [`mcp.json`](../mcp.json)
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
124
132
 
125
133
  ## 9. Troubleshooting Cheatsheet
126
134
 
@@ -27,13 +27,13 @@ suite can assert expectations in upcoming steps.
27
27
 
28
28
  | Family | Baseline Resources (URIs) | Rationale |
29
29
  | --- | --- | --- |
30
- | `basic-program` | `c64://specs/basic`, `c64://context/bootstrap`, `c64://docs/index` | Reinforce BASIC token rules, PETSCII gotchas, workflow expectations. |
31
- | `assembly-program` | `c64://specs/assembly`, `c64://context/bootstrap`, `c64://specs/vic`, `c64://specs/sid` | Provide opcode tables, IRQ setup, VIC/SID register references. |
32
- | `sid-music` | `c64://specs/sid`, `c64://specs/sidwave`, `c64://docs/sid/file-structure` | Supply register map, SIDWAVE format, SID file metadata. |
33
- | `graphics-demo` | `c64://specs/vic`, `c64://context/bootstrap` | Cover VIC mode bits, raster constraints, general workflow guidance. |
34
- | `printer-job` | `c64://specs/printer`, `c64://docs/printer/guide`, `c64://docs/printer/prompts` | Document PETSCII vs ESC/P workflows, sample templates, and safety checks. |
35
- | `memory-debug` | `c64://context/bootstrap`, `c64://specs/assembly`, `c64://docs/index` | Highlight addressing safeguards, workflow rules, and knowledge map summaries. |
36
- | `drive-manager` | `c64://context/bootstrap` | Ensure awareness of system-wide safety rules before altering drives. |
30
+ | `basic-program` | `c64://basic/spec`, `c64://guide/bootstrap`, `c64://guide/index` | Reinforce BASIC token rules, PETSCII gotchas, workflow expectations. |
31
+ | `assembly-program` | `c64://assembly/6510-spec`, `c64://guide/bootstrap`, `c64://graphics/vic/spec`, `c64://sound/sid/spec` | Provide opcode tables, IRQ setup, VIC/SID register references. |
32
+ | `sid-music` | `c64://sound/sid/spec`, `c64://sound/sidwave/spec`, `c64://sound/sid/file-format` | Supply register map, SIDWAVE format, SID file metadata. |
33
+ | `graphics-demo` | `c64://graphics/vic/spec`, `c64://guide/bootstrap` | Cover VIC mode bits, raster constraints, general workflow guidance. |
34
+ | `printer-job` | `c64://printer/spec`, `c64://printer/commodore/text`, `c64://printer/prompt-guide` | Document PETSCII vs ESC/P workflows, sample templates, and safety checks. |
35
+ | `memory-debug` | `c64://guide/bootstrap`, `c64://assembly/6510-spec`, `c64://guide/index` | Highlight addressing safeguards, workflow rules, and knowledge map summaries. |
36
+ | `drive-manager` | `c64://guide/bootstrap` | Ensure awareness of system-wide safety rules before altering drives. |
37
37
 
38
38
  Notes:
39
39