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.
- package/.github/prompts/README.md +14 -4
- package/.github/prompts/assembly-routine-session.prompt.md +9 -9
- package/.github/prompts/basic-program-session.prompt.md +11 -9
- package/.github/prompts/drive-management-session.prompt.md +9 -9
- package/.github/prompts/hello-world-session.prompt.md +17 -0
- package/.github/prompts/petscii-art-session.prompt.md +9 -9
- package/.github/prompts/play-classic-game.prompt.md +9 -9
- package/.github/prompts/pr-converge.prompt.md +13 -0
- package/.github/prompts/sid-music-session.prompt.md +9 -9
- package/AGENTS.md +59 -60
- package/README.md +550 -191
- package/data/assembly/assembly-spec.md +290 -1
- package/data/context/bootstrap.md +26 -2
- package/data/context/fast-paths.md +44 -0
- package/data/graphics/petscii-style-guide.md +6 -2
- package/data/graphics/sprite-charset-best-practices.md +5 -5
- package/dist/.tsbuildinfo +1 -1
- package/dist/c64Client.js +990 -26
- package/dist/config.js +5 -1
- package/dist/device.js +680 -57
- package/dist/diagnostics.js +371 -0
- package/dist/mcp/metadata.js +39 -0
- package/dist/mcp-server.js +170 -20
- package/dist/platform.js +10 -2
- package/dist/prompts/registry.js +166 -309
- package/dist/rag/init.js +37 -1
- package/dist/rag/knowledgeIndex.js +18 -6
- package/dist/rag/rateLimiter.js +2 -2
- package/dist/streamCapture.js +135 -0
- package/dist/tools/audio.js +3 -1
- package/dist/tools/debug.js +470 -0
- package/dist/tools/developer.js +10 -4
- package/dist/tools/graphics.js +183 -17
- package/dist/tools/machineControl.js +11 -5
- package/dist/tools/memory.js +19 -10
- package/dist/tools/meta/audio.js +240 -1
- package/dist/tools/meta/config.js +20 -28
- package/dist/tools/meta/configInventory.js +104 -0
- package/dist/tools/meta/diagnostics.js +45 -2
- package/dist/tools/meta/graphics.js +14 -6
- package/dist/tools/meta/index.js +1 -0
- package/dist/tools/meta/memory.js +20 -6
- package/dist/tools/meta/program.js +473 -0
- package/dist/tools/pollValidator.js +4 -2
- package/dist/tools/programRunners.js +8 -4
- package/dist/tools/registry/config.js +17 -0
- package/dist/tools/registry/disk.js +7 -0
- package/dist/tools/registry/drive.js +3 -0
- package/dist/tools/registry/graphics.js +111 -27
- package/dist/tools/registry/index.js +6 -0
- package/dist/tools/registry/memory.js +1 -0
- package/dist/tools/registry/platform.js +121 -0
- package/dist/tools/registry/program.js +26 -8
- package/dist/tools/registry/sound.js +96 -0
- package/dist/tools/registry/system.js +15 -2
- package/dist/tools/storage.js +1 -0
- package/dist/tools/streaming.js +1 -0
- package/dist/tools/types.js +38 -4
- package/dist/tools/vice.js +160 -0
- package/dist/vicBitmap.js +317 -0
- package/dist/vice/mockServer.js +439 -0
- package/dist/vice/process.js +310 -0
- package/dist/vice/readiness.js +18 -2
- package/dist/vice/viceClient.js +382 -22
- package/doc/developer.md +71 -25
- package/doc/img/backend-switch/hello-c64u.png +0 -0
- package/doc/img/backend-switch/hello-vice-and-c64u.png +0 -0
- package/doc/img/backend-switch/hello-vice.png +0 -0
- package/doc/img/backend-switch/metadata.json +50 -0
- package/doc/plans/vice-backend-switch/plan.md +486 -0
- package/doc/plans/vice-backend-switch-2/plan.md +48 -0
- package/doc/plans/vice-support/notes.md +1 -0
- package/doc/plans/vice-support/plan.md +5 -0
- package/doc/plans/vice-support/rollout.md +23 -17
- package/doc/reviews/vice-support-review.md +185 -0
- package/doc/troubleshooting-mcp.md +35 -6
- package/doc/vice/support-matrix.md +54 -0
- package/mcp/prompts.json +292 -0
- package/mcp/protocol-examples.json +46 -0
- package/mcp/resources.json +940 -0
- package/mcp/schemas/c64_config.schema.json +350 -0
- package/mcp/schemas/c64_debug.schema.json +380 -0
- package/mcp/schemas/c64_disk.schema.json +278 -0
- package/mcp/schemas/c64_drive.schema.json +120 -0
- package/mcp/schemas/c64_extract.schema.json +302 -0
- package/mcp/schemas/c64_graphics.schema.json +333 -0
- package/mcp/schemas/c64_memory.schema.json +155 -0
- package/mcp/schemas/c64_printer.schema.json +218 -0
- package/mcp/schemas/c64_program.schema.json +391 -0
- package/mcp/schemas/c64_rag.schema.json +67 -0
- package/mcp/schemas/c64_select_backend.schema.json +32 -0
- package/mcp/schemas/c64_sound.schema.json +644 -0
- package/mcp/schemas/c64_stream.schema.json +61 -0
- package/mcp/schemas/c64_system.schema.json +227 -0
- package/mcp/schemas/c64_vice.schema.json +63 -0
- package/mcp/server.json +15 -0
- package/mcp/tools.json +4517 -0
- package/mcp.json +90 -15
- package/package.json +11 -4
- package/scripts/capture-backend-switch-screenshots.mjs +276 -0
- package/scripts/generate-mcp-interface.ts +289 -0
- package/scripts/invoke-bun.mjs +67 -21
- package/scripts/mockC64Server.mjs +162 -6
- package/scripts/prepare-release.mjs +59 -2
- package/scripts/run-coverage.mjs +284 -0
- package/scripts/run-tests.mjs +95 -39
- package/scripts/run-tests.ts +378 -73
- package/scripts/update-readme.ts +147 -13
- package/server.json +94 -0
package/mcp.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "c64bridge",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.1",
|
|
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",
|
|
@@ -9,11 +9,14 @@
|
|
|
9
9
|
"mcp-server",
|
|
10
10
|
"model context protocol",
|
|
11
11
|
"openai",
|
|
12
|
-
"
|
|
13
|
-
"commodore64",
|
|
14
|
-
"commodore 64",
|
|
12
|
+
"commodore",
|
|
15
13
|
"c64",
|
|
16
14
|
"c64u",
|
|
15
|
+
"commodore64",
|
|
16
|
+
"commodore 64",
|
|
17
|
+
"commodore 64 ultimate",
|
|
18
|
+
"ultimate64",
|
|
19
|
+
"ultimate 64",
|
|
17
20
|
"sid",
|
|
18
21
|
"basic",
|
|
19
22
|
"assembly",
|
|
@@ -22,9 +25,7 @@
|
|
|
22
25
|
"retrogaming",
|
|
23
26
|
"vintage computing",
|
|
24
27
|
"hardware integration",
|
|
25
|
-
"c64 development"
|
|
26
|
-
"commodore 64 ultimate",
|
|
27
|
-
"ultimate 64"
|
|
28
|
+
"c64 development"
|
|
28
29
|
],
|
|
29
30
|
"env": {
|
|
30
31
|
"LOG_LEVEL": {
|
|
@@ -39,6 +40,42 @@
|
|
|
39
40
|
"description": "Select active backend (c64u for Ultimate hardware, vice for emulator)",
|
|
40
41
|
"default": "c64u"
|
|
41
42
|
},
|
|
43
|
+
"C64U_HOST": {
|
|
44
|
+
"description": "Override the C64 Ultimate host name or IP address",
|
|
45
|
+
"default": "c64u"
|
|
46
|
+
},
|
|
47
|
+
"C64U_PORT": {
|
|
48
|
+
"description": "Override the C64 Ultimate REST port",
|
|
49
|
+
"default": "80"
|
|
50
|
+
},
|
|
51
|
+
"C64U_PASSWORD": {
|
|
52
|
+
"description": "Override the C64 Ultimate network password sent as X-Password",
|
|
53
|
+
"default": ""
|
|
54
|
+
},
|
|
55
|
+
"C64BRIDGE_DIAGNOSTICS_DIR": {
|
|
56
|
+
"description": "Override the directory where persistent MCP diagnostics files are written",
|
|
57
|
+
"default": "~/.c64bridge/diagnostics"
|
|
58
|
+
},
|
|
59
|
+
"C64BRIDGE_DISABLE_DIAGNOSTICS": {
|
|
60
|
+
"description": "Set to 1 to disable persistent diagnostics logging",
|
|
61
|
+
"default": "0"
|
|
62
|
+
},
|
|
63
|
+
"C64BRIDGE_POLL_MAX_MS": {
|
|
64
|
+
"description": "Maximum time to poll for program-output validation before timing out in normal runtime mode",
|
|
65
|
+
"default": "2000"
|
|
66
|
+
},
|
|
67
|
+
"C64BRIDGE_POLL_INTERVAL_MS": {
|
|
68
|
+
"description": "Interval between screen polls during program-output validation in normal runtime mode",
|
|
69
|
+
"default": "200"
|
|
70
|
+
},
|
|
71
|
+
"C64BRIDGE_POLL_STABILIZE_MS": {
|
|
72
|
+
"description": "Extra settle time after a successful poll match before considering output stable",
|
|
73
|
+
"default": "100"
|
|
74
|
+
},
|
|
75
|
+
"C64_TASK_STATE_FILE": {
|
|
76
|
+
"description": "Override the path used to persist MCP background-task state",
|
|
77
|
+
"default": "auto"
|
|
78
|
+
},
|
|
42
79
|
"RAG_EMBEDDINGS_DIR": {
|
|
43
80
|
"description": "Directory containing RAG embedding JSON files",
|
|
44
81
|
"default": "data"
|
|
@@ -48,19 +85,24 @@
|
|
|
48
85
|
"default": "0"
|
|
49
86
|
},
|
|
50
87
|
"RAG_BUILD_ON_START": {
|
|
51
|
-
"description": "Set to 1 to rebuild embeddings on server start"
|
|
88
|
+
"description": "Set to 1 to rebuild embeddings on server start",
|
|
89
|
+
"default": "0"
|
|
52
90
|
},
|
|
53
91
|
"RAG_DOC_FILES": {
|
|
54
|
-
"description": "Comma-separated extra docs to include in RAG"
|
|
92
|
+
"description": "Comma-separated extra docs to include in RAG",
|
|
93
|
+
"default": ""
|
|
55
94
|
},
|
|
56
95
|
"RAG_DISCOVER_FORCE_REFRESH": {
|
|
57
|
-
"description": "Set to 1 to ignore cached discovery results when fetching external docs"
|
|
96
|
+
"description": "Set to 1 to ignore cached discovery results when fetching external docs",
|
|
97
|
+
"default": "0"
|
|
58
98
|
},
|
|
59
99
|
"GITHUB_TOKEN": {
|
|
60
|
-
"description": "Personal access token used for optional RAG discovery against GitHub"
|
|
100
|
+
"description": "Personal access token used for optional RAG discovery against GitHub",
|
|
101
|
+
"default": ""
|
|
61
102
|
},
|
|
62
103
|
"C64_TEST_TARGET": {
|
|
63
|
-
"description": "Overrides integration tests to hit mock or real hardware (mock|real)"
|
|
104
|
+
"description": "Overrides integration tests to hit mock or real hardware (mock|real)",
|
|
105
|
+
"default": ""
|
|
64
106
|
},
|
|
65
107
|
"VICE_MODE": {
|
|
66
108
|
"description": "Default video standard for VICE audio capture (ntsc|pal)",
|
|
@@ -71,17 +113,49 @@
|
|
|
71
113
|
"default": "120000000"
|
|
72
114
|
},
|
|
73
115
|
"VICE_BINARY": {
|
|
74
|
-
"description": "VICE binary to launch for audio capture",
|
|
116
|
+
"description": "VICE binary to launch for managed emulator sessions and audio capture",
|
|
75
117
|
"default": "x64sc"
|
|
76
118
|
},
|
|
119
|
+
"VICE_DIRECTORY": {
|
|
120
|
+
"description": "Override the VICE resource directory used for ROM and UI asset discovery",
|
|
121
|
+
"default": "auto-detect"
|
|
122
|
+
},
|
|
123
|
+
"VICE_HOST": {
|
|
124
|
+
"description": "Override the VICE Binary Monitor host",
|
|
125
|
+
"default": "127.0.0.1"
|
|
126
|
+
},
|
|
127
|
+
"VICE_PORT": {
|
|
128
|
+
"description": "Override the VICE Binary Monitor port",
|
|
129
|
+
"default": "6502"
|
|
130
|
+
},
|
|
131
|
+
"VICE_VISIBLE": {
|
|
132
|
+
"description": "Launch VICE visibly on the desktop instead of headless/Xvfb when possible",
|
|
133
|
+
"default": "true"
|
|
134
|
+
},
|
|
135
|
+
"VICE_WARP": {
|
|
136
|
+
"description": "Enable warp mode for managed VICE sessions",
|
|
137
|
+
"default": "false when visible, true when headless"
|
|
138
|
+
},
|
|
139
|
+
"VICE_ARGS": {
|
|
140
|
+
"description": "Extra command-line arguments forwarded to managed VICE launches",
|
|
141
|
+
"default": ""
|
|
142
|
+
},
|
|
77
143
|
"VICE_RUN_TIMEOUT_MS": {
|
|
78
144
|
"description": "Timeout for headless VICE runs in milliseconds",
|
|
79
145
|
"default": "10000"
|
|
80
146
|
},
|
|
81
147
|
"FORCE_XVFB": {
|
|
82
|
-
"description": "Set to 1 to force
|
|
148
|
+
"description": "Set to 1 to force managed VICE launches to run under Xvfb",
|
|
83
149
|
"default": "0"
|
|
84
150
|
},
|
|
151
|
+
"DISABLE_XVFB": {
|
|
152
|
+
"description": "Set to 1 to disable Xvfb fallback and use the current display only",
|
|
153
|
+
"default": "0"
|
|
154
|
+
},
|
|
155
|
+
"VICE_XVFB_DISPLAY": {
|
|
156
|
+
"description": "Display number to use when managed VICE launches under Xvfb",
|
|
157
|
+
"default": ":99"
|
|
158
|
+
},
|
|
85
159
|
"SIDPLAY_MODE": {
|
|
86
160
|
"description": "Default SID playback mode (ntsc|pal)",
|
|
87
161
|
"default": "ntsc"
|
|
@@ -95,7 +169,8 @@
|
|
|
95
169
|
"default": "sidplayfp"
|
|
96
170
|
},
|
|
97
171
|
"SIDPLAYFP_BINARY": {
|
|
98
|
-
"description": "Legacy alias for SIDPLAY_BINARY (sidplayfp executable name)"
|
|
172
|
+
"description": "Legacy alias for SIDPLAY_BINARY (sidplayfp executable name)",
|
|
173
|
+
"default": ""
|
|
99
174
|
}
|
|
100
175
|
},
|
|
101
176
|
"repository": {
|
package/package.json
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "c64bridge",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
3
|
+
"version": "0.8.1",
|
|
4
|
+
"mcpName": "io.github.chrisgleissner/c64bridge",
|
|
5
|
+
"description": "C64 Bridge MCP server for Commodore 64 Ultimate hardware and VICE emulator control",
|
|
5
6
|
"type": "module",
|
|
6
7
|
"main": "dist/index.js",
|
|
7
8
|
"packageManager": "bun@1.3.1",
|
|
@@ -17,11 +18,14 @@
|
|
|
17
18
|
"mcp": "node scripts/invoke-bun.mjs src/mcp-server.ts",
|
|
18
19
|
"mcp:node": "node dist/mcp-server.js",
|
|
19
20
|
"mcp:build": "npm run build && node dist/mcp-server.js",
|
|
20
|
-
"build": "node scripts/prebuild.mjs && tsc -p tsconfig.json && node scripts/postbuild.mjs && node scripts/invoke-bun.mjs scripts/update-readme.ts",
|
|
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
22
|
"lint": "npm run check",
|
|
22
23
|
"test": "node scripts/invoke-bun.mjs scripts/run-tests.ts",
|
|
23
24
|
"test:node": "node scripts/run-tests.mjs",
|
|
24
|
-
"coverage": "node scripts/
|
|
25
|
+
"coverage": "node scripts/run-coverage.mjs",
|
|
26
|
+
"coverage:single": "node scripts/invoke-bun.mjs scripts/run-tests.ts --coverage",
|
|
27
|
+
"test:matrix": "npm run test -- --platform=c64u --target=mock && npm run test -- --platform=vice --target=mock && npm run test -- --platform=vice --target=device",
|
|
28
|
+
"mcp:generate": "node scripts/invoke-bun.mjs scripts/generate-mcp-interface.ts",
|
|
25
29
|
"check:package": "node scripts/check-package.mjs",
|
|
26
30
|
"check:run-local": "bash scripts/run-mcp-check.sh local 5",
|
|
27
31
|
"check:run-npm": "bash scripts/run-mcp-check.sh npm 5",
|
|
@@ -59,6 +63,7 @@
|
|
|
59
63
|
"axios": "^1.7.7",
|
|
60
64
|
"date-fns": "^4.1.0",
|
|
61
65
|
"fastify": "^5.6.1",
|
|
66
|
+
"jimp": "^1.6.0",
|
|
62
67
|
"meyda": "^5.6.2",
|
|
63
68
|
"octokit": "^5.0.4",
|
|
64
69
|
"pitchfinder": "^2.2.0",
|
|
@@ -75,9 +80,11 @@
|
|
|
75
80
|
},
|
|
76
81
|
"files": [
|
|
77
82
|
"mcp.json",
|
|
83
|
+
"server.json",
|
|
78
84
|
"AGENTS.md",
|
|
79
85
|
"LICENSE",
|
|
80
86
|
"README.md",
|
|
87
|
+
"mcp/**",
|
|
81
88
|
"dist/**",
|
|
82
89
|
"data/**",
|
|
83
90
|
"doc/**",
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
6
|
+
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
7
|
+
import { CallToolResultSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
8
|
+
import { Jimp, rgbaToInt } from "jimp";
|
|
9
|
+
import { findGlyphByBasicChar } from "../dist/chargen.js";
|
|
10
|
+
|
|
11
|
+
const OUTPUT_DIR = path.resolve("doc/img/backend-switch");
|
|
12
|
+
|
|
13
|
+
const VICE_BINARY = process.env.VICE_BINARY || "/usr/local/bin/x64sc";
|
|
14
|
+
const VICE_DIRECTORY = process.env.VICE_DIRECTORY || "/usr/local/share/vice";
|
|
15
|
+
|
|
16
|
+
const PROMPTS = {
|
|
17
|
+
c64u: "c64u: write a small BASIC program that clears the screen and prints HELLO C64U",
|
|
18
|
+
vice: "vice: write a small BASIC program that clears the screen and prints HELLO VICE",
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const PROGRAMS = {
|
|
22
|
+
c64u: "10 PRINT CHR$(147);\"HELLO C64U\"\n20 END",
|
|
23
|
+
vice: "10 PRINT CHR$(147);\"HELLO VICE\"\n20 END",
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const VIC_PALETTE = [
|
|
27
|
+
[0x00, 0x00, 0x00],
|
|
28
|
+
[0xff, 0xff, 0xff],
|
|
29
|
+
[0x81, 0x33, 0x38],
|
|
30
|
+
[0x75, 0xce, 0xc8],
|
|
31
|
+
[0x8e, 0x3c, 0x97],
|
|
32
|
+
[0x56, 0xac, 0x4d],
|
|
33
|
+
[0x2e, 0x2c, 0x9b],
|
|
34
|
+
[0xed, 0xf1, 0x71],
|
|
35
|
+
[0x8e, 0x50, 0x29],
|
|
36
|
+
[0x55, 0x38, 0x00],
|
|
37
|
+
[0xc4, 0x6c, 0x71],
|
|
38
|
+
[0x4a, 0x4a, 0x4a],
|
|
39
|
+
[0x7b, 0x7b, 0x7b],
|
|
40
|
+
[0xa9, 0xff, 0x9f],
|
|
41
|
+
[0x70, 0x6d, 0xeb],
|
|
42
|
+
[0xb2, 0xb2, 0xb2],
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
const OCR_CANDIDATES = " ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789.".split("");
|
|
46
|
+
const BLANK_GLYPH = { bitmap: Uint8Array.from([0, 0, 0, 0, 0, 0, 0, 0]) };
|
|
47
|
+
|
|
48
|
+
function ensureOutputDir() {
|
|
49
|
+
fs.mkdirSync(OUTPUT_DIR, { recursive: true });
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function textContent(result) {
|
|
53
|
+
return (result.content ?? [])
|
|
54
|
+
.filter((item) => item.type === "text")
|
|
55
|
+
.map((item) => item.text)
|
|
56
|
+
.join("\n");
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function parseJsonResult(result) {
|
|
60
|
+
return JSON.parse(textContent(result));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function callTool(client, name, args) {
|
|
64
|
+
const result = await client.request(
|
|
65
|
+
{ method: "tools/call", params: { name, arguments: args } },
|
|
66
|
+
CallToolResultSchema,
|
|
67
|
+
);
|
|
68
|
+
if (result.isError) {
|
|
69
|
+
throw new Error(`${name} failed: ${textContent(result)}`);
|
|
70
|
+
}
|
|
71
|
+
return result;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function rgba(image, x, y) {
|
|
75
|
+
const value = image.getPixelColor(x, y) >>> 0;
|
|
76
|
+
return {
|
|
77
|
+
r: (value >> 24) & 0xff,
|
|
78
|
+
g: (value >> 16) & 0xff,
|
|
79
|
+
b: (value >> 8) & 0xff,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function colorDistance(left, right) {
|
|
84
|
+
const dr = left.r - right.r;
|
|
85
|
+
const dg = left.g - right.g;
|
|
86
|
+
const db = left.b - right.b;
|
|
87
|
+
return dr * dr + dg * dg + db * db;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function dominantColors(image, startX, startY, width, height) {
|
|
91
|
+
const counts = new Map();
|
|
92
|
+
for (let y = startY; y < startY + height; y += 1) {
|
|
93
|
+
for (let x = startX; x < startX + width; x += 1) {
|
|
94
|
+
const key = image.getPixelColor(x, y) >>> 0;
|
|
95
|
+
counts.set(key, (counts.get(key) ?? 0) + 1);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return [...counts.entries()]
|
|
99
|
+
.sort((left, right) => right[1] - left[1])
|
|
100
|
+
.slice(0, 2)
|
|
101
|
+
.map(([value]) => ({
|
|
102
|
+
r: (value >> 24) & 0xff,
|
|
103
|
+
g: (value >> 16) & 0xff,
|
|
104
|
+
b: (value >> 8) & 0xff,
|
|
105
|
+
}));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function scoreGlyph(image, glyph, originX, originY, background, foreground) {
|
|
109
|
+
let score = 0;
|
|
110
|
+
for (let row = 0; row < 8; row += 1) {
|
|
111
|
+
const bits = glyph.bitmap[row] ?? 0;
|
|
112
|
+
for (let col = 0; col < 8; col += 1) {
|
|
113
|
+
const expectedOn = ((bits >> (7 - col)) & 1) === 1;
|
|
114
|
+
const actual = rgba(image, originX + col, originY + row);
|
|
115
|
+
const actualOn = colorDistance(actual, foreground) < colorDistance(actual, background);
|
|
116
|
+
if (expectedOn !== actualOn) {
|
|
117
|
+
score += 1;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return score;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function scoreString(image, expected, originX, originY) {
|
|
125
|
+
const colors = dominantColors(image, originX, originY, expected.length * 8, 8);
|
|
126
|
+
const background = colors[0] ?? { r: 0, g: 0, b: 0 };
|
|
127
|
+
const foreground = colors[1] ?? { r: 255, g: 255, b: 255 };
|
|
128
|
+
let score = 0;
|
|
129
|
+
for (let index = 0; index < expected.length; index += 1) {
|
|
130
|
+
const glyph = expected[index] === " " ? BLANK_GLYPH : findGlyphByBasicChar(expected[index]);
|
|
131
|
+
if (!glyph) {
|
|
132
|
+
score += 1000;
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
score += scoreGlyph(image, glyph, originX + (index * 8), originY, background, foreground);
|
|
136
|
+
}
|
|
137
|
+
return score;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function decodeStringAt(image, length, originX, originY) {
|
|
141
|
+
const colors = dominantColors(image, originX, originY, length * 8, 8);
|
|
142
|
+
const background = colors[0] ?? { r: 0, g: 0, b: 0 };
|
|
143
|
+
const foreground = colors[1] ?? { r: 255, g: 255, b: 255 };
|
|
144
|
+
let text = "";
|
|
145
|
+
const scores = [];
|
|
146
|
+
|
|
147
|
+
for (let index = 0; index < length; index += 1) {
|
|
148
|
+
let best = { char: "?", score: Number.POSITIVE_INFINITY };
|
|
149
|
+
for (const candidate of OCR_CANDIDATES) {
|
|
150
|
+
const glyph = candidate === " " ? BLANK_GLYPH : findGlyphByBasicChar(candidate);
|
|
151
|
+
if (!glyph) {
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
const score = scoreGlyph(image, glyph, originX + (index * 8), originY, background, foreground);
|
|
155
|
+
if (score < best.score) {
|
|
156
|
+
best = { char: candidate, score };
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
text += best.char;
|
|
160
|
+
scores.push(best.score);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return { text, scores };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async function verifyScreenshot(filePath, expected, searchWindow) {
|
|
167
|
+
const image = await Jimp.read(filePath);
|
|
168
|
+
let best = { score: Number.POSITIVE_INFINITY, x: searchWindow.x0, y: searchWindow.y0 };
|
|
169
|
+
|
|
170
|
+
for (let y = searchWindow.y0; y <= searchWindow.y1; y += 1) {
|
|
171
|
+
for (let x = searchWindow.x0; x <= searchWindow.x1; x += 1) {
|
|
172
|
+
const score = scoreString(image, expected, x, y);
|
|
173
|
+
if (score < best.score) {
|
|
174
|
+
best = { score, x, y };
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const decoded = decodeStringAt(image, expected.length, best.x, best.y);
|
|
180
|
+
if (decoded.text !== expected) {
|
|
181
|
+
throw new Error(`OCR mismatch for ${path.basename(filePath)}: expected '${expected}' but decoded '${decoded.text}' at (${best.x}, ${best.y})`);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return {
|
|
185
|
+
expected,
|
|
186
|
+
recognized: decoded.text,
|
|
187
|
+
origin: { x: best.x, y: best.y },
|
|
188
|
+
score: best.score,
|
|
189
|
+
characterScores: decoded.scores,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
async function renderIndexedFrame(frame, outputPath) {
|
|
194
|
+
const image = new Jimp({ width: frame.width, height: frame.height, color: 0x000000ff });
|
|
195
|
+
const pixels = Buffer.from(frame.pixels.data, frame.pixels.encoding);
|
|
196
|
+
|
|
197
|
+
for (let y = 0; y < frame.height; y += 1) {
|
|
198
|
+
for (let x = 0; x < frame.width; x += 1) {
|
|
199
|
+
const colorIndex = pixels[(y * frame.width) + x] ?? 0;
|
|
200
|
+
const [r, g, b] = VIC_PALETTE[colorIndex & 0x0f] ?? VIC_PALETTE[0];
|
|
201
|
+
image.setPixelColor(rgbaToInt(r, g, b, 255), x, y);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
await image.write(outputPath);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
async function main() {
|
|
209
|
+
ensureOutputDir();
|
|
210
|
+
|
|
211
|
+
const transport = new StdioClientTransport({
|
|
212
|
+
command: "node",
|
|
213
|
+
args: ["scripts/start.mjs"],
|
|
214
|
+
cwd: process.cwd(),
|
|
215
|
+
env: {
|
|
216
|
+
...process.env,
|
|
217
|
+
C64_MODE: "vice",
|
|
218
|
+
C64U_HOST: process.env.C64U_HOST || "c64u",
|
|
219
|
+
C64U_PORT: process.env.C64U_PORT || "80",
|
|
220
|
+
VICE_BINARY,
|
|
221
|
+
VICE_DIRECTORY,
|
|
222
|
+
VICE_VISIBLE: process.env.VICE_VISIBLE || "true",
|
|
223
|
+
VICE_WARP: process.env.VICE_WARP || "false",
|
|
224
|
+
},
|
|
225
|
+
stderr: "pipe",
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
const client = new Client(
|
|
229
|
+
{ name: "backend-switch-artifacts", version: "1.0.0" },
|
|
230
|
+
{ capabilities: { tools: {}, resources: {}, prompts: {} } },
|
|
231
|
+
);
|
|
232
|
+
|
|
233
|
+
try {
|
|
234
|
+
await client.connect(transport);
|
|
235
|
+
await new Promise((resolve) => setTimeout(resolve, 3500));
|
|
236
|
+
|
|
237
|
+
await callTool(client, "c64_select_backend", { op: "select", backend: "c64u" });
|
|
238
|
+
await callTool(client, "c64_program", { op: "upload_run_basic", program: PROGRAMS.c64u });
|
|
239
|
+
await callTool(client, "c64_memory", { op: "wait_for_text", pattern: "HELLO C64U" });
|
|
240
|
+
const c64uCapture = parseJsonResult(await callTool(client, "c64_graphics", {
|
|
241
|
+
op: "capture_frame",
|
|
242
|
+
count: 1,
|
|
243
|
+
includePixels: true,
|
|
244
|
+
encoding: "base64",
|
|
245
|
+
}));
|
|
246
|
+
await renderIndexedFrame(c64uCapture.frames[0], path.join(OUTPUT_DIR, "hello-c64u.png"));
|
|
247
|
+
|
|
248
|
+
await callTool(client, "c64_select_backend", { op: "select", backend: "vice" });
|
|
249
|
+
await callTool(client, "c64_program", { op: "upload_run_basic", program: PROGRAMS.vice });
|
|
250
|
+
await callTool(client, "c64_memory", { op: "wait_for_text", pattern: "HELLO VICE" });
|
|
251
|
+
const viceCapture = parseJsonResult(await callTool(client, "c64_graphics", {
|
|
252
|
+
op: "capture_frame",
|
|
253
|
+
count: 1,
|
|
254
|
+
includePixels: true,
|
|
255
|
+
encoding: "base64",
|
|
256
|
+
}));
|
|
257
|
+
await renderIndexedFrame(viceCapture.frames[0], path.join(OUTPUT_DIR, "hello-vice.png"));
|
|
258
|
+
|
|
259
|
+
const ocr = {
|
|
260
|
+
c64u: await verifyScreenshot(path.join(OUTPUT_DIR, "hello-c64u.png"), "HELLO C64U", { x0: 20, x1: 90, y0: 20, y1: 60 }),
|
|
261
|
+
vice: await verifyScreenshot(path.join(OUTPUT_DIR, "hello-vice.png"), "HELLO VICE", { x0: 20, x1: 140, y0: 20, y1: 80 }),
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
fs.writeFileSync(
|
|
265
|
+
path.join(OUTPUT_DIR, "metadata.json"),
|
|
266
|
+
JSON.stringify({ prompts: PROMPTS, ocr }, null, 2),
|
|
267
|
+
"utf8",
|
|
268
|
+
);
|
|
269
|
+
|
|
270
|
+
console.log(JSON.stringify({ ok: true, prompts: PROMPTS, ocr }, null, 2));
|
|
271
|
+
} finally {
|
|
272
|
+
await client.close().catch(() => undefined);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
await main();
|