c64bridge 0.5.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.
- package/.github/prompts/README.md +6 -0
- package/.github/prompts/assembly-routine-session.prompt.md +13 -0
- package/.github/prompts/basic-program-session.prompt.md +13 -0
- package/.github/prompts/drive-management-session.prompt.md +13 -0
- package/.github/prompts/petscii-art-session.prompt.md +13 -0
- package/.github/prompts/play-classic-game.prompt.md +13 -0
- package/.github/prompts/sid-music-session.prompt.md +13 -0
- package/AGENTS.md +173 -0
- package/LICENSE +339 -0
- package/README.md +582 -0
- package/data/README.md +16 -0
- package/data/api/basic-api-spec.md +154 -0
- package/data/api/kernal-api-spec.md +68 -0
- package/data/assembly/assembly-spec.md +196 -0
- package/data/assembly/examples/effects/sinewave.asm +5 -0
- package/data/assembly/examples/graphics/sprite/mover.asm +15 -0
- package/data/assembly/examples/sid-file-structure.md +8 -0
- package/data/assembly/examples/sid-overview.md +11 -0
- package/data/audio/examples/Biosphere.sid +0 -0
- package/data/audio/sid-file-structure.md +91 -0
- package/data/audio/sid-programming-best-practices.md +198 -0
- package/data/audio/sid-spec.md +166 -0
- package/data/audio/sidwave.md +131 -0
- package/data/audio/sidwave.schema.json +64 -0
- package/data/basic/basic-spec.md +230 -0
- package/data/basic/examples/audio/alle-meine-entchen-expressive.bas +41 -0
- package/data/basic/examples/audio/c-major-scale-pleasant.bas +29 -0
- package/data/basic/examples/io/joystick.bas +5 -0
- package/data/basic/examples/video/bounce.bas +8 -0
- package/data/basic/examples/video/entchen-petscii.bas +16 -0
- package/data/basic/examples/video/hello-world.bas +4 -0
- package/data/basic/examples/video/wave.bas +7 -0
- package/data/context/bootstrap.md +148 -0
- package/data/context/chat.md +36 -0
- package/data/drive/drive-spec.md +215 -0
- package/data/embeddings_asm.json +1 -0
- package/data/embeddings_basic.json +1 -0
- package/data/embeddings_hardware.json +1 -0
- package/data/embeddings_mixed.json +1 -0
- package/data/embeddings_other.json +1 -0
- package/data/io/cia-spec.md +115 -0
- package/data/io/io-spec.md +124 -0
- package/data/memory/kernal-memory-map.md +227 -0
- package/data/memory/low-memory-map.md +133 -0
- package/data/memory/memory-map.md +186 -0
- package/data/printer/printer-commodore-bitmap.md +31 -0
- package/data/printer/printer-commodore.md +28 -0
- package/data/printer/printer-epson-bitmap.md +29 -0
- package/data/printer/printer-epson.md +30 -0
- package/data/printer/printer-prompts.md +29 -0
- package/data/printer/printer-spec.md +180 -0
- package/data/video/character-set.csv +257 -0
- package/data/video/vic-spec.md +276 -0
- package/dist/assemblyConverter.js +1299 -0
- package/dist/audio/record_and_analyze_audio.js +292 -0
- package/dist/basicConverter.js +193 -0
- package/dist/bootstrap/stdio-logger.js +32 -0
- package/dist/c64Client.js +1052 -0
- package/dist/chargen.js +197 -0
- package/dist/config.js +142 -0
- package/dist/context.js +157 -0
- package/dist/device.js +413 -0
- package/dist/index.js +7 -0
- package/dist/knowledge.js +454 -0
- package/dist/logger.js +182 -0
- package/dist/loggingHttpClient.js +88 -0
- package/dist/mcp-server.js +426 -0
- package/dist/petscii.js +135 -0
- package/dist/petsciiArt.js +356 -0
- package/dist/platform.js +82 -0
- package/dist/prompts/registry.js +560 -0
- package/dist/rag/discover.config.json +41 -0
- package/dist/rag/discover.js +401 -0
- package/dist/rag/embeddings.js +60 -0
- package/dist/rag/externalFetcher.js +934 -0
- package/dist/rag/indexer.js +770 -0
- package/dist/rag/init.js +139 -0
- package/dist/rag/knowledgeIndex.js +361 -0
- package/dist/rag/loggingRetriever.js +46 -0
- package/dist/rag/rateLimiter.js +127 -0
- package/dist/rag/retriever.js +74 -0
- package/dist/rag/sources.csv +31 -0
- package/dist/rag/types.js +5 -0
- package/dist/rag/urlUtils.js +38 -0
- package/dist/sidplayRunner.js +240 -0
- package/dist/sidwave.js +101 -0
- package/dist/sidwaveCompiler.js +329 -0
- package/dist/tools/audio.js +944 -0
- package/dist/tools/developer.js +515 -0
- package/dist/tools/errors.js +59 -0
- package/dist/tools/graphics.js +469 -0
- package/dist/tools/machineControl.js +267 -0
- package/dist/tools/memory.js +224 -0
- package/dist/tools/printer.js +482 -0
- package/dist/tools/programRunners.js +315 -0
- package/dist/tools/rag.js +153 -0
- package/dist/tools/registry.js +58 -0
- package/dist/tools/responses.js +36 -0
- package/dist/tools/schema.js +304 -0
- package/dist/tools/storage.js +740 -0
- package/dist/tools/streaming.js +132 -0
- package/dist/tools/types.js +99 -0
- package/dist/viceRunner.js +239 -0
- package/doc/MCP_SETUP.md +153 -0
- package/doc/README.md +7 -0
- package/doc/developer.md +312 -0
- package/doc/img/logo.png +0 -0
- package/doc/img/prompts/duck_petscii.png +0 -0
- package/doc/img/prompts/duck_song.png +0 -0
- package/doc/img/vscode/vscode-copilot-c64-chat-mode.png +0 -0
- package/doc/img/vscode/vscode-copilot-hello-world.png +0 -0
- package/doc/prompts/README.md +96 -0
- package/doc/prompts/user-prompt-ideas.md +147 -0
- package/doc/rest/c64-openapi.yaml +979 -0
- package/doc/rest/c64-rest-api.md +106 -0
- package/doc/troubleshooting-mcp.md +177 -0
- package/generated/c64/index.js +857 -0
- package/mcp.json +106 -0
- package/package.json +79 -0
- package/scripts/c64-cli.mjs +142 -0
- package/scripts/c64-tool.sh +83 -0
- package/scripts/check-package.mjs +89 -0
- package/scripts/cli.js +61 -0
- package/scripts/fetch-external-sources.mjs +67 -0
- package/scripts/generate-changelog.mjs +131 -0
- package/scripts/postbuild.mjs +101 -0
- package/scripts/prepare-release.mjs +58 -0
- package/scripts/rag-rebuild.mjs +53 -0
- package/scripts/register-ts-node.mjs +6 -0
- package/scripts/run-mcp-check.sh +104 -0
- package/scripts/run-tests.mjs +195 -0
- package/scripts/sid2wav.sh +94 -0
- package/scripts/smoke-run.sh +65 -0
- package/scripts/start-mock-c64.mjs +51 -0
- package/scripts/start-mock.mjs +31 -0
- package/scripts/start.mjs +72 -0
- package/scripts/update-readme.ts +164 -0
- package/scripts/verify-package.mjs +147 -0
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { defineToolModule } from "./types.js";
|
|
2
|
+
import { objectSchema, stringSchema } from "./schema.js";
|
|
3
|
+
import { textResult } from "./responses.js";
|
|
4
|
+
import { ToolError, ToolExecutionError, toolErrorResult, unknownErrorResult, } from "./errors.js";
|
|
5
|
+
const streamIdentifierSchema = stringSchema({
|
|
6
|
+
description: "Stream type to control (video/audio/debug).",
|
|
7
|
+
enum: ["video", "audio", "debug"],
|
|
8
|
+
});
|
|
9
|
+
const streamStartArgsSchema = objectSchema({
|
|
10
|
+
description: "Arguments for starting an Ultimate streaming session.",
|
|
11
|
+
properties: {
|
|
12
|
+
stream: streamIdentifierSchema,
|
|
13
|
+
target: stringSchema({
|
|
14
|
+
description: "Destination host:port or UDP target understood by the firmware.",
|
|
15
|
+
minLength: 3,
|
|
16
|
+
}),
|
|
17
|
+
},
|
|
18
|
+
required: ["stream", "target"],
|
|
19
|
+
additionalProperties: false,
|
|
20
|
+
});
|
|
21
|
+
const streamStopArgsSchema = objectSchema({
|
|
22
|
+
description: "Arguments for stopping an Ultimate streaming session.",
|
|
23
|
+
properties: {
|
|
24
|
+
stream: streamIdentifierSchema,
|
|
25
|
+
},
|
|
26
|
+
required: ["stream"],
|
|
27
|
+
additionalProperties: false,
|
|
28
|
+
});
|
|
29
|
+
function toRecord(details) {
|
|
30
|
+
if (details && typeof details === "object") {
|
|
31
|
+
return details;
|
|
32
|
+
}
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
export const streamingModule = defineToolModule({
|
|
36
|
+
domain: "streaming",
|
|
37
|
+
summary: "Long-running or streaming workflows such as audio capture or SID playback monitoring.",
|
|
38
|
+
resources: [
|
|
39
|
+
"c64://specs/sid",
|
|
40
|
+
"c64://docs/index",
|
|
41
|
+
],
|
|
42
|
+
prompts: ["sid-music"],
|
|
43
|
+
defaultLifecycle: "stream",
|
|
44
|
+
defaultTags: ["stream", "monitoring"],
|
|
45
|
+
workflowHints: [
|
|
46
|
+
"Use streaming tools for long-running capture or monitoring workflows such as audio verification.",
|
|
47
|
+
"Clarify that streams keep running until stopped so the user can manage resources.",
|
|
48
|
+
],
|
|
49
|
+
tools: [
|
|
50
|
+
{
|
|
51
|
+
name: "stream_start",
|
|
52
|
+
description: "Start an Ultimate streaming session (video/audio/debug) targeting a host:port destination. See c64://docs/index for usage notes.",
|
|
53
|
+
summary: "Validates stream arguments and forwards the request to firmware, returning status metadata.",
|
|
54
|
+
inputSchema: streamStartArgsSchema.jsonSchema,
|
|
55
|
+
tags: ["stream", "start"],
|
|
56
|
+
prerequisites: [],
|
|
57
|
+
examples: [
|
|
58
|
+
{ name: "Start audio", description: "Send audio to localhost:9000", arguments: { stream: "audio", target: "127.0.0.1:9000" } },
|
|
59
|
+
],
|
|
60
|
+
workflowHints: [
|
|
61
|
+
"Use when the user wants to begin audio/video capture; restate the destination target so they know where to listen.",
|
|
62
|
+
"Remind them to stop the stream after gathering enough data.",
|
|
63
|
+
],
|
|
64
|
+
async execute(args, ctx) {
|
|
65
|
+
try {
|
|
66
|
+
const parsed = streamStartArgsSchema.parse(args ?? {});
|
|
67
|
+
ctx.logger.info("Starting Ultimate stream", {
|
|
68
|
+
stream: parsed.stream,
|
|
69
|
+
target: parsed.target,
|
|
70
|
+
});
|
|
71
|
+
const result = await ctx.client.streamStart(parsed.stream, parsed.target);
|
|
72
|
+
if (!result.success) {
|
|
73
|
+
throw new ToolExecutionError("C64 firmware reported failure while starting stream", {
|
|
74
|
+
details: toRecord(result.details),
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
return textResult(`Stream ${parsed.stream} started toward ${parsed.target}.`, {
|
|
78
|
+
success: true,
|
|
79
|
+
stream: parsed.stream,
|
|
80
|
+
target: parsed.target,
|
|
81
|
+
details: toRecord(result.details) ?? null,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
catch (error) {
|
|
85
|
+
if (error instanceof ToolError) {
|
|
86
|
+
return toolErrorResult(error);
|
|
87
|
+
}
|
|
88
|
+
return unknownErrorResult(error);
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
name: "stream_stop",
|
|
94
|
+
description: "Stop an Ultimate streaming session (video/audio/debug).",
|
|
95
|
+
summary: "Requests the firmware to stop the specified stream and returns completion metadata.",
|
|
96
|
+
inputSchema: streamStopArgsSchema.jsonSchema,
|
|
97
|
+
tags: ["stream", "stop"],
|
|
98
|
+
prerequisites: ["stream_start"],
|
|
99
|
+
examples: [
|
|
100
|
+
{ name: "Stop audio", description: "Stop audio stream", arguments: { stream: "audio" } },
|
|
101
|
+
],
|
|
102
|
+
workflowHints: [
|
|
103
|
+
"Call after a monitoring session to release resources; confirm whether the firmware acknowledged the stop.",
|
|
104
|
+
],
|
|
105
|
+
async execute(args, ctx) {
|
|
106
|
+
try {
|
|
107
|
+
const parsed = streamStopArgsSchema.parse(args ?? {});
|
|
108
|
+
ctx.logger.info("Stopping Ultimate stream", {
|
|
109
|
+
stream: parsed.stream,
|
|
110
|
+
});
|
|
111
|
+
const result = await ctx.client.streamStop(parsed.stream);
|
|
112
|
+
if (!result.success) {
|
|
113
|
+
throw new ToolExecutionError("C64 firmware reported failure while stopping stream", {
|
|
114
|
+
details: toRecord(result.details),
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
return textResult(`Stream ${parsed.stream} stop requested.`, {
|
|
118
|
+
success: true,
|
|
119
|
+
stream: parsed.stream,
|
|
120
|
+
details: toRecord(result.details) ?? null,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
catch (error) {
|
|
124
|
+
if (error instanceof ToolError) {
|
|
125
|
+
return toolErrorResult(error);
|
|
126
|
+
}
|
|
127
|
+
return unknownErrorResult(error);
|
|
128
|
+
}
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
],
|
|
132
|
+
});
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { getPlatformStatus, isPlatformSupported, setPlatform, } from "../platform.js";
|
|
2
|
+
import { ToolUnsupportedPlatformError } from "./errors.js";
|
|
3
|
+
export function defineToolModule(config) {
|
|
4
|
+
const defaultLifecycle = config.defaultLifecycle ?? "request-response";
|
|
5
|
+
const defaultTags = Object.freeze([...(config.defaultTags ?? [])]);
|
|
6
|
+
const defaultResources = Object.freeze([...(config.resources ?? [])]);
|
|
7
|
+
const defaultPrompts = Object.freeze([...(config.prompts ?? [])]);
|
|
8
|
+
const defaultWorkflowHints = Object.freeze([...(config.workflowHints ?? [])]);
|
|
9
|
+
const defaultPrerequisites = Object.freeze([...(config.prerequisites ?? [])]);
|
|
10
|
+
const defaultPlatforms = config.supportedPlatforms
|
|
11
|
+
? Object.freeze([...(config.supportedPlatforms)])
|
|
12
|
+
: Object.freeze(["c64u"]);
|
|
13
|
+
const toolMap = new Map(config.tools.map((tool) => [tool.name, tool]));
|
|
14
|
+
return {
|
|
15
|
+
domain: config.domain,
|
|
16
|
+
summary: config.summary,
|
|
17
|
+
defaultTags,
|
|
18
|
+
workflowHints: defaultWorkflowHints.length > 0 ? defaultWorkflowHints : undefined,
|
|
19
|
+
describeTools() {
|
|
20
|
+
return config.tools.map((tool) => {
|
|
21
|
+
const workflowHints = mergeOptionalStrings(defaultWorkflowHints, tool.workflowHints);
|
|
22
|
+
const prerequisites = mergeOptionalStrings(defaultPrerequisites, tool.prerequisites);
|
|
23
|
+
const platforms = mergePlatforms(defaultPlatforms, tool.supportedPlatforms);
|
|
24
|
+
const metadata = {
|
|
25
|
+
domain: config.domain,
|
|
26
|
+
summary: tool.summary ?? tool.description,
|
|
27
|
+
lifecycle: tool.lifecycle ?? defaultLifecycle,
|
|
28
|
+
resources: mergeUnique(defaultResources, tool.relatedResources),
|
|
29
|
+
prompts: mergeUnique(defaultPrompts, tool.relatedPrompts),
|
|
30
|
+
examples: tool.examples,
|
|
31
|
+
tags: mergeUnique(defaultTags, tool.tags),
|
|
32
|
+
...(workflowHints ? { workflowHints } : {}),
|
|
33
|
+
...(prerequisites ? { prerequisites } : {}),
|
|
34
|
+
...(platforms ? { platforms } : {}),
|
|
35
|
+
};
|
|
36
|
+
return {
|
|
37
|
+
name: tool.name,
|
|
38
|
+
description: tool.description,
|
|
39
|
+
inputSchema: tool.inputSchema,
|
|
40
|
+
metadata,
|
|
41
|
+
};
|
|
42
|
+
});
|
|
43
|
+
},
|
|
44
|
+
async invoke(name, args, ctx) {
|
|
45
|
+
const tool = toolMap.get(name);
|
|
46
|
+
if (!tool) {
|
|
47
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
48
|
+
}
|
|
49
|
+
const platforms = mergePlatforms(defaultPlatforms, tool.supportedPlatforms) ?? defaultPlatforms;
|
|
50
|
+
const status = ctx.platform ?? getPlatformStatus();
|
|
51
|
+
const setter = ctx.setPlatform ?? setPlatform;
|
|
52
|
+
if (!isPlatformSupported(status.id, platforms)) {
|
|
53
|
+
throw new ToolUnsupportedPlatformError(name, status.id, platforms);
|
|
54
|
+
}
|
|
55
|
+
const enrichedCtx = {
|
|
56
|
+
...ctx,
|
|
57
|
+
platform: status,
|
|
58
|
+
setPlatform: setter,
|
|
59
|
+
};
|
|
60
|
+
return tool.execute(args, enrichedCtx);
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
function mergeUnique(base, extra) {
|
|
65
|
+
if (!extra || extra.length === 0) {
|
|
66
|
+
return base;
|
|
67
|
+
}
|
|
68
|
+
const set = new Set(base);
|
|
69
|
+
for (const item of extra) {
|
|
70
|
+
set.add(item);
|
|
71
|
+
}
|
|
72
|
+
return Array.from(set);
|
|
73
|
+
}
|
|
74
|
+
function mergeOptionalStrings(base, extra) {
|
|
75
|
+
if ((!base || base.length === 0) && (!extra || extra.length === 0)) {
|
|
76
|
+
return base && base.length > 0 ? base : undefined;
|
|
77
|
+
}
|
|
78
|
+
const set = new Set(base ?? []);
|
|
79
|
+
if (extra) {
|
|
80
|
+
for (const item of extra) {
|
|
81
|
+
set.add(item);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
const merged = Array.from(set);
|
|
85
|
+
return merged.length > 0 ? merged : undefined;
|
|
86
|
+
}
|
|
87
|
+
function mergePlatforms(base, extra) {
|
|
88
|
+
if ((!base || base.length === 0) && (!extra || extra.length === 0)) {
|
|
89
|
+
return base && base.length > 0 ? base : undefined;
|
|
90
|
+
}
|
|
91
|
+
const set = new Set(base ?? []);
|
|
92
|
+
if (extra) {
|
|
93
|
+
for (const item of extra) {
|
|
94
|
+
set.add(item);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
const merged = Array.from(set);
|
|
98
|
+
return merged.length > 0 ? merged : undefined;
|
|
99
|
+
}
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* VICE Runner Utility (x64sc) with optional xvfb-run wrapper for headless CI
|
|
3
|
+
*/
|
|
4
|
+
import { spawn } from "node:child_process";
|
|
5
|
+
import fs from "node:fs";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
export class ViceExecutionError extends Error {
|
|
8
|
+
constructor(options) {
|
|
9
|
+
const first = options.stderr.subarray(0, 1000).toString("utf8");
|
|
10
|
+
const last = options.stderr.length > 1000
|
|
11
|
+
? options.stderr.subarray(options.stderr.length - 1000).toString("utf8")
|
|
12
|
+
: "";
|
|
13
|
+
super(`${options.message}\n` +
|
|
14
|
+
`command: ${options.command}\n` +
|
|
15
|
+
`exitCode: ${String(options.exitCode)}\n` +
|
|
16
|
+
`stderr(first 1000):\n${first}\n` +
|
|
17
|
+
(last ? `stderr(last 1000):\n${last}\n` : "") +
|
|
18
|
+
`wav: exists=${options.wavExists}, size=${options.wavSize}`);
|
|
19
|
+
this.name = "ViceExecutionError";
|
|
20
|
+
this.command = options.command;
|
|
21
|
+
this.exitCode = options.exitCode;
|
|
22
|
+
this.stderrFirst = first;
|
|
23
|
+
this.stderrLast = last;
|
|
24
|
+
this.wavExists = options.wavExists;
|
|
25
|
+
this.wavSize = options.wavSize;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function resolveMode(input) {
|
|
29
|
+
const envMode = (process.env.VICE_MODE || "").toLowerCase();
|
|
30
|
+
const raw = (input || envMode || "ntsc").toLowerCase();
|
|
31
|
+
return raw === "pal" ? "pal" : "ntsc";
|
|
32
|
+
}
|
|
33
|
+
function resolveLimitCycles(input) {
|
|
34
|
+
const fromEnv = Number(process.env.VICE_LIMIT_CYCLES);
|
|
35
|
+
if (Number.isFinite(fromEnv) && fromEnv > 0)
|
|
36
|
+
return Math.floor(fromEnv);
|
|
37
|
+
if (typeof input === "number" && input > 0)
|
|
38
|
+
return Math.floor(input);
|
|
39
|
+
return 120000000;
|
|
40
|
+
}
|
|
41
|
+
function resolveBinary(input) {
|
|
42
|
+
return process.env.VICE_BINARY || input || "x64sc";
|
|
43
|
+
}
|
|
44
|
+
function which(binary) {
|
|
45
|
+
const hasSep = binary.includes("/") || binary.includes("\\");
|
|
46
|
+
if (hasSep) {
|
|
47
|
+
try {
|
|
48
|
+
if (fs.existsSync(binary))
|
|
49
|
+
return binary;
|
|
50
|
+
}
|
|
51
|
+
catch { }
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
const envPath = process.env.PATH || "";
|
|
55
|
+
for (const dir of envPath.split(path.delimiter)) {
|
|
56
|
+
if (!dir)
|
|
57
|
+
continue;
|
|
58
|
+
const candidate = path.join(dir, binary);
|
|
59
|
+
try {
|
|
60
|
+
if (fs.existsSync(candidate))
|
|
61
|
+
return candidate;
|
|
62
|
+
}
|
|
63
|
+
catch { }
|
|
64
|
+
}
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
function shouldUseXvfb() {
|
|
68
|
+
if (process.env.FORCE_XVFB === "1")
|
|
69
|
+
return true;
|
|
70
|
+
const ciValue = (process.env.CI || "").toLowerCase();
|
|
71
|
+
return ciValue === "true" || ciValue === "1" || ciValue === "yes";
|
|
72
|
+
}
|
|
73
|
+
function ensureParentDir(filePath) {
|
|
74
|
+
const dir = path.dirname(filePath);
|
|
75
|
+
if (!fs.existsSync(dir))
|
|
76
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
77
|
+
}
|
|
78
|
+
function buildViceArgs(params) {
|
|
79
|
+
const args = [
|
|
80
|
+
`-${params.mode}`,
|
|
81
|
+
"-sounddev", "wav",
|
|
82
|
+
"-soundarg", `output=${params.wavPath}`,
|
|
83
|
+
"-soundarg", "bits=16",
|
|
84
|
+
"-soundrate", "44100",
|
|
85
|
+
"-soundvol", "100",
|
|
86
|
+
"-limitcycles", String(params.limitCycles),
|
|
87
|
+
];
|
|
88
|
+
if (typeof params.tune === "number" && Number.isFinite(params.tune)) {
|
|
89
|
+
args.push("-tune", String(Math.floor(params.tune)));
|
|
90
|
+
}
|
|
91
|
+
args.push(params.sidPath);
|
|
92
|
+
return args;
|
|
93
|
+
}
|
|
94
|
+
function buildCommand(binary, args) {
|
|
95
|
+
const useXvfb = shouldUseXvfb();
|
|
96
|
+
if (useXvfb) {
|
|
97
|
+
const file = "xvfb-run";
|
|
98
|
+
const wrappedArgs = ["-a", binary, ...args];
|
|
99
|
+
const display = [file, ...wrappedArgs.map(quoteArg)].join(" ");
|
|
100
|
+
return { file, args: wrappedArgs, display };
|
|
101
|
+
}
|
|
102
|
+
const display = [binary, ...args.map(quoteArg)].join(" ");
|
|
103
|
+
return { file: binary, args, display };
|
|
104
|
+
}
|
|
105
|
+
function quoteArg(a) {
|
|
106
|
+
if (/^[A-Za-z0-9_\-\.\/]+$/.test(a))
|
|
107
|
+
return a;
|
|
108
|
+
return `'${a.replace(/'/g, "'\\''")}'`;
|
|
109
|
+
}
|
|
110
|
+
function parseWavHeader(buffer) {
|
|
111
|
+
if (buffer.length < 44)
|
|
112
|
+
throw new Error("WAV too small");
|
|
113
|
+
if (buffer.toString("ascii", 0, 4) !== "RIFF")
|
|
114
|
+
throw new Error("WAV missing RIFF header");
|
|
115
|
+
if (buffer.toString("ascii", 8, 12) !== "WAVE")
|
|
116
|
+
throw new Error("WAV missing WAVE header");
|
|
117
|
+
let offset = 12;
|
|
118
|
+
let fmt = null;
|
|
119
|
+
let dataBytes = 0;
|
|
120
|
+
while (offset + 8 <= buffer.length) {
|
|
121
|
+
const id = buffer.toString("ascii", offset, offset + 4);
|
|
122
|
+
const size = buffer.readUInt32LE(offset + 4);
|
|
123
|
+
offset += 8;
|
|
124
|
+
if (id === "fmt ") {
|
|
125
|
+
const audioFormat = buffer.readUInt16LE(offset + 0);
|
|
126
|
+
const numChannels = buffer.readUInt16LE(offset + 2);
|
|
127
|
+
const sampleRate = buffer.readUInt32LE(offset + 4);
|
|
128
|
+
const bitsPerSample = buffer.readUInt16LE(offset + 14);
|
|
129
|
+
fmt = { audioFormat, numChannels, sampleRate, bitsPerSample, dataBytes: 0 };
|
|
130
|
+
}
|
|
131
|
+
else if (id === "data") {
|
|
132
|
+
dataBytes = size;
|
|
133
|
+
}
|
|
134
|
+
offset += size;
|
|
135
|
+
}
|
|
136
|
+
if (!fmt)
|
|
137
|
+
throw new Error("WAV missing fmt chunk");
|
|
138
|
+
if (dataBytes <= 0)
|
|
139
|
+
throw new Error("WAV missing data chunk");
|
|
140
|
+
fmt.dataBytes = dataBytes;
|
|
141
|
+
return fmt;
|
|
142
|
+
}
|
|
143
|
+
export async function runSidToWav(params) {
|
|
144
|
+
const mode = resolveMode(params.mode);
|
|
145
|
+
const limitCycles = resolveLimitCycles(params.limitCycles);
|
|
146
|
+
const binary = resolveBinary(params.binary);
|
|
147
|
+
if (!params.sidPath)
|
|
148
|
+
throw new Error("sidPath is required");
|
|
149
|
+
if (!params.wavPath)
|
|
150
|
+
throw new Error("wavPath is required");
|
|
151
|
+
if (!fs.existsSync(params.sidPath))
|
|
152
|
+
throw new Error(`SID not found: ${params.sidPath}`);
|
|
153
|
+
ensureParentDir(params.wavPath);
|
|
154
|
+
const full = {
|
|
155
|
+
sidPath: params.sidPath,
|
|
156
|
+
wavPath: params.wavPath,
|
|
157
|
+
mode,
|
|
158
|
+
limitCycles,
|
|
159
|
+
tune: typeof params.tune === "number" ? params.tune : undefined,
|
|
160
|
+
binary,
|
|
161
|
+
};
|
|
162
|
+
// Validate binary availability to produce clearer 127 semantics even when wrapped by xvfb-run
|
|
163
|
+
const binaryPath = which(full.binary);
|
|
164
|
+
if (!binaryPath) {
|
|
165
|
+
const msg = Buffer.from(`${full.binary} not found in PATH`);
|
|
166
|
+
throw new ViceExecutionError({
|
|
167
|
+
message: "VICE binary is not installed",
|
|
168
|
+
command: full.binary,
|
|
169
|
+
exitCode: 127,
|
|
170
|
+
stderr: msg,
|
|
171
|
+
wavExists: false,
|
|
172
|
+
wavSize: 0,
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
const args = buildViceArgs({ ...full });
|
|
176
|
+
const cmd = buildCommand(binaryPath, args);
|
|
177
|
+
const stdoutChunks = [];
|
|
178
|
+
const stderrChunks = [];
|
|
179
|
+
const exitCode = await new Promise((resolve) => {
|
|
180
|
+
const child = spawn(cmd.file, cmd.args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
181
|
+
child.stdout?.on("data", (c) => stdoutChunks.push(Buffer.from(c)));
|
|
182
|
+
child.stderr?.on("data", (c) => stderrChunks.push(Buffer.from(c)));
|
|
183
|
+
child.on("error", (err) => {
|
|
184
|
+
// Map ENOENT to 127-like semantics
|
|
185
|
+
const enoent = (err && typeof err === "object" && "code" in err && err.code === "ENOENT");
|
|
186
|
+
// Push a hint into stderr buffer
|
|
187
|
+
const hint = Buffer.from(String(err?.message || err || "spawn error"));
|
|
188
|
+
stderrChunks.push(hint);
|
|
189
|
+
resolve(enoent ? 127 : 1);
|
|
190
|
+
});
|
|
191
|
+
child.on("close", (code) => resolve(code ?? 1));
|
|
192
|
+
});
|
|
193
|
+
const stdout = Buffer.concat(stdoutChunks).toString("utf8");
|
|
194
|
+
const stderrBuf = Buffer.concat(stderrChunks);
|
|
195
|
+
const stderr = stderrBuf.toString("utf8");
|
|
196
|
+
const wavExists = fs.existsSync(full.wavPath);
|
|
197
|
+
const wavStat = wavExists ? fs.statSync(full.wavPath) : null;
|
|
198
|
+
const wavSize = wavStat?.size ?? 0;
|
|
199
|
+
if (exitCode !== 0) {
|
|
200
|
+
throw new ViceExecutionError({
|
|
201
|
+
message: "VICE failed (non-zero exit code)",
|
|
202
|
+
command: cmd.display,
|
|
203
|
+
exitCode,
|
|
204
|
+
stderr: stderrBuf,
|
|
205
|
+
wavExists,
|
|
206
|
+
wavSize,
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
if (!wavExists || wavSize === 0) {
|
|
210
|
+
throw new ViceExecutionError({
|
|
211
|
+
message: "VICE did not produce a WAV file",
|
|
212
|
+
command: cmd.display,
|
|
213
|
+
exitCode,
|
|
214
|
+
stderr: stderrBuf,
|
|
215
|
+
wavExists,
|
|
216
|
+
wavSize,
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
const header = fs.readFileSync(full.wavPath);
|
|
220
|
+
const info = parseWavHeader(header);
|
|
221
|
+
if (info.audioFormat !== 1 || info.sampleRate !== 44100 || info.bitsPerSample !== 16) {
|
|
222
|
+
throw new ViceExecutionError({
|
|
223
|
+
message: `Unexpected WAV format (expected PCM 16-bit @ 44100 Hz); got format=${info.audioFormat}, bits=${info.bitsPerSample}, rate=${info.sampleRate}`,
|
|
224
|
+
command: cmd.display,
|
|
225
|
+
exitCode,
|
|
226
|
+
stderr: stderrBuf,
|
|
227
|
+
wavExists,
|
|
228
|
+
wavSize,
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
return {
|
|
232
|
+
command: cmd.display,
|
|
233
|
+
exitCode: exitCode ?? 0,
|
|
234
|
+
stdout,
|
|
235
|
+
stderr,
|
|
236
|
+
wavPath: full.wavPath,
|
|
237
|
+
wavInfo: info,
|
|
238
|
+
};
|
|
239
|
+
}
|
package/doc/MCP_SETUP.md
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
# MCP Setup Guide
|
|
2
|
+
|
|
3
|
+
Step-by-step instructions for installing, configuring, and running the Commodore 64 MCP server with the official Model Context Protocol SDK.
|
|
4
|
+
|
|
5
|
+
## Audience
|
|
6
|
+
|
|
7
|
+
This guide targets engineers and advanced users who need to bring the C64 Bridge server online, wire it into an MCP-capable client (such as GitHub Copilot Chat), and verify connectivity against either real hardware or the bundled mock server.
|
|
8
|
+
|
|
9
|
+
## Prerequisites
|
|
10
|
+
|
|
11
|
+
- Node.js 18 or newer (Node 20 recommended)
|
|
12
|
+
- npm (ships with Node)
|
|
13
|
+
- Optional: Ultimate 64 or Commodore 64 Ultimate hardware reachable over HTTP
|
|
14
|
+
- macOS, Linux, or WSL are fully supported. Windows works when the shell supports stdio transports.
|
|
15
|
+
|
|
16
|
+
## 1. Install Dependencies
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm install
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
The install step pulls the MCP SDK, test tooling, and the generated REST client stubs.
|
|
23
|
+
|
|
24
|
+
## 2. Configure the C64 Endpoint
|
|
25
|
+
|
|
26
|
+
The server looks for configuration in the following order (first match wins):
|
|
27
|
+
|
|
28
|
+
1. JSON file referenced by the `C64BRIDGE_CONFIG` environment variable (if the variable is unset, the default path is `~/.c64bridge.json`)
|
|
29
|
+
2. `.c64bridge.json` in the repository root
|
|
30
|
+
3. Built-in defaults (`host=c64u`, `port=80`)
|
|
31
|
+
|
|
32
|
+
Recommended configuration file:
|
|
33
|
+
|
|
34
|
+
```json
|
|
35
|
+
{
|
|
36
|
+
"c64u": {
|
|
37
|
+
"host": "c64u",
|
|
38
|
+
"port": 80
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## 3. Run the MCP Server (source tree)
|
|
44
|
+
|
|
45
|
+
For active development against TypeScript sources:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
npm start
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
`npm start` locates `src/mcp-server.ts`, loads the config, and launches the stdio transport. Successful startup prints:
|
|
52
|
+
|
|
53
|
+
```text
|
|
54
|
+
> c64bridge@0.4.0 start
|
|
55
|
+
> node scripts/start.mjs
|
|
56
|
+
|
|
57
|
+
Starting c64bridge MCP server...
|
|
58
|
+
[c64u] GET http://192.168.1.64 status=200 bytes=41608 latencyMs=177
|
|
59
|
+
Connectivity check succeeded for c64 device at http://192.168.1.64
|
|
60
|
+
[c64u] GET /v1/machine:readmem status=200 bytes=1 latencyMs=31
|
|
61
|
+
Zero-page probe @ $0000: $00
|
|
62
|
+
c64bridge MCP server running on stdio
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
When TypeScript is unavailable (for example inside a packaged release tarball) use the compiled entry point:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
npm run build
|
|
69
|
+
node dist/index.js
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
The npm package also exposes a CLI named `c64bridge`; `npx c64bridge` or an installed binary launches the same stdio server.
|
|
73
|
+
|
|
74
|
+
## 4. Connect an MCP Client
|
|
75
|
+
|
|
76
|
+
### GitHub Copilot Chat (VS Code)
|
|
77
|
+
|
|
78
|
+
1. Enable MCP support: Settings > Extensions > GitHub Copilot > Chat > Experimental: MCP.
|
|
79
|
+
2. Add the server entry (Command Palette > Preferences: Open Settings (JSON)).
|
|
80
|
+
|
|
81
|
+
When running from the cloned repository after `npm run build`:
|
|
82
|
+
|
|
83
|
+
```json
|
|
84
|
+
{
|
|
85
|
+
"github.copilot.chat.experimental.mcp": {
|
|
86
|
+
"servers": [
|
|
87
|
+
{
|
|
88
|
+
"name": "c64bridge",
|
|
89
|
+
"command": "node",
|
|
90
|
+
"args": ["./dist/index.js"],
|
|
91
|
+
"type": "stdio"
|
|
92
|
+
}
|
|
93
|
+
]
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
When using the published npm package instead of the local build:
|
|
99
|
+
|
|
100
|
+
```json
|
|
101
|
+
{
|
|
102
|
+
"github.copilot.chat.experimental.mcp": {
|
|
103
|
+
"servers": [
|
|
104
|
+
{
|
|
105
|
+
"name": "c64bridge",
|
|
106
|
+
"command": "node",
|
|
107
|
+
"args": ["./node_modules/c64bridge/dist/index.js"],
|
|
108
|
+
"type": "stdio"
|
|
109
|
+
}
|
|
110
|
+
]
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
3. Keep `npm start` or the CLI running in the background.
|
|
116
|
+
4. In Copilot Chat, reference tools naturally (for example, "Read the current screen" or "Upload and run this BASIC program").
|
|
117
|
+
|
|
118
|
+
## 5. Verify Operation
|
|
119
|
+
|
|
120
|
+
Run the automated test suites against the mock hardware:
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
npm test
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
To exercise a real device, ensure your config points at the desired host/port and append `-- --real`:
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
npm test -- --real
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
`npm run coverage` captures c8 coverage for CI parity.
|
|
133
|
+
|
|
134
|
+
## 6. Common Tasks
|
|
135
|
+
|
|
136
|
+
- Rebuild TypeScript output: `npm run build`
|
|
137
|
+
- Regenerate REST client from OpenAPI: `npm run api:generate`
|
|
138
|
+
- Refresh RAG embeddings after updating `data/`: `npm run rag:rebuild`
|
|
139
|
+
- Smoke test the packaged tarball (used in CI): `npm run verify-package`
|
|
140
|
+
|
|
141
|
+
## 7. Troubleshooting
|
|
142
|
+
|
|
143
|
+
- Confirm connectivity logs appear on startup; missing logs indicate the REST endpoint is unreachable.
|
|
144
|
+
- If `npm start` exits immediately, ensure Node can locate `ts-node/register` (installed via dev dependencies).
|
|
145
|
+
- When GitHub Copilot cannot see tools, restart VS Code after editing MCP settings.
|
|
146
|
+
- Additional guidance lives in `doc/troubleshooting-mcp.md`.
|
|
147
|
+
|
|
148
|
+
## 8. Where to Go Next
|
|
149
|
+
|
|
150
|
+
- Architecture and contribution details: `doc/developer.md`
|
|
151
|
+
- MCP tooling deep dive: `doc/tasks/use-mcp-sdk.md`
|
|
152
|
+
- Agent personas and context layering: `AGENTS.md`
|
|
153
|
+
- Knowledge base surfaced to LLMs: `data/` markdown files and specs
|
package/doc/README.md
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# Documentation Folder
|
|
2
|
+
|
|
3
|
+
This directory contains **developer and extended user documentation** for this Model Context Protocol (MCP) server.
|
|
4
|
+
|
|
5
|
+
Unlike the [`data`](../data/README.md) folder, which provides domain-specific C64 knowledge, this directory focuses on **implementation details** and **usage guides** relevant to developers of this project, and end users.
|
|
6
|
+
|
|
7
|
+
All documents in this folder are included in the **Retrieval-Augmented Generation (RAG)** index to allow the MCP’s AI components to retrieve and reason about internal design and operational aspects.
|