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
package/dist/rag/init.js
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/*
|
|
2
|
+
C64 Bridge - Local RAG bootstrap
|
|
3
|
+
GPL-2.0-only
|
|
4
|
+
*/
|
|
5
|
+
import fs from "node:fs/promises";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import { LocalMiniHashEmbedding } from "./embeddings.js";
|
|
8
|
+
import { buildAllIndexes, loadIndexes } from "./indexer.js";
|
|
9
|
+
import { LocalRagRetriever } from "./retriever.js";
|
|
10
|
+
import { LoggingRagRetriever } from "./loggingRetriever.js";
|
|
11
|
+
const EXTERNAL_DIR = path.resolve("external");
|
|
12
|
+
const BASIC_DATA_DIR = path.resolve("data/basic/examples");
|
|
13
|
+
const ASM_DATA_DIR = path.resolve("data/assembly/examples");
|
|
14
|
+
const DOC_DIR = path.resolve("doc");
|
|
15
|
+
const CONTEXT_DIR = path.resolve("doc/context");
|
|
16
|
+
const BOOTSTRAP_PATH = path.join(CONTEXT_DIR, "bootstrap.md");
|
|
17
|
+
const AGENTS_PATH = path.resolve("AGENTS.md");
|
|
18
|
+
const PROMPTS_DIR = path.resolve(".github/prompts");
|
|
19
|
+
const CHAT_PATH = path.join(CONTEXT_DIR, "chat.md");
|
|
20
|
+
function resolveEmbeddingsDir() {
|
|
21
|
+
return path.resolve(process.env.RAG_EMBEDDINGS_DIR ?? "data");
|
|
22
|
+
}
|
|
23
|
+
function embeddingIndexPaths() {
|
|
24
|
+
const dir = resolveEmbeddingsDir();
|
|
25
|
+
return {
|
|
26
|
+
dir,
|
|
27
|
+
basic: path.join(dir, "embeddings_basic.json"),
|
|
28
|
+
asm: path.join(dir, "embeddings_asm.json"),
|
|
29
|
+
mixed: path.join(dir, "embeddings_mixed.json"),
|
|
30
|
+
hardware: path.join(dir, "embeddings_hardware.json"),
|
|
31
|
+
other: path.join(dir, "embeddings_other.json"),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
export async function initRag() {
|
|
35
|
+
const model = new LocalMiniHashEmbedding(384);
|
|
36
|
+
// Build on start only when explicitly requested
|
|
37
|
+
const buildOnStart = String(process.env.RAG_BUILD_ON_START ?? "").trim().toLowerCase();
|
|
38
|
+
if (buildOnStart === "1" || buildOnStart === "true" || buildOnStart === "yes") {
|
|
39
|
+
const needBuild = await needsRebuild();
|
|
40
|
+
if (needBuild) {
|
|
41
|
+
await buildAllIndexes({ model });
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
const embeddingsDir = resolveEmbeddingsDir();
|
|
45
|
+
const { basic, asm, mixed, hardware, other } = await loadIndexes({ embeddingsDir });
|
|
46
|
+
const baseRetriever = new LocalRagRetriever(model, { basic, asm, mixed, hardware, other });
|
|
47
|
+
const loggingRetriever = new LoggingRagRetriever(baseRetriever);
|
|
48
|
+
// Background watcher: reindex if source files change (checks mtimes periodically)
|
|
49
|
+
// Default disabled to avoid churn/conflicts unless explicitly enabled
|
|
50
|
+
const intervalMs = Number(process.env.RAG_REINDEX_INTERVAL_MS ?? 0);
|
|
51
|
+
if (intervalMs > 0) {
|
|
52
|
+
setInterval(async () => {
|
|
53
|
+
try {
|
|
54
|
+
if (await needsRebuild()) {
|
|
55
|
+
await buildAllIndexes({ model });
|
|
56
|
+
const updated = await loadIndexes({ embeddingsDir: resolveEmbeddingsDir() });
|
|
57
|
+
baseRetriever.updateIndexes(updated);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
catch (err) {
|
|
61
|
+
// swallow in background to avoid crashing server
|
|
62
|
+
// eslint-disable-next-line no-console
|
|
63
|
+
console.warn("RAG reindex error", err);
|
|
64
|
+
}
|
|
65
|
+
}, intervalMs).unref();
|
|
66
|
+
}
|
|
67
|
+
return loggingRetriever;
|
|
68
|
+
}
|
|
69
|
+
async function fileMtime(file) {
|
|
70
|
+
try {
|
|
71
|
+
const stat = await fs.stat(file);
|
|
72
|
+
return stat.mtimeMs;
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
async function dirMtimeRecursive(root) {
|
|
79
|
+
let newest = 0;
|
|
80
|
+
async function walk(dir) {
|
|
81
|
+
let entries = [];
|
|
82
|
+
try {
|
|
83
|
+
entries = await fs.readdir(dir, { withFileTypes: true });
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
for (const entry of entries) {
|
|
89
|
+
const full = path.join(dir, entry.name);
|
|
90
|
+
if (entry.isDirectory())
|
|
91
|
+
await walk(full);
|
|
92
|
+
else if (entry.isFile()) {
|
|
93
|
+
const stat = await fs.stat(full);
|
|
94
|
+
if (stat.mtimeMs > newest)
|
|
95
|
+
newest = stat.mtimeMs;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
await walk(root);
|
|
100
|
+
return newest;
|
|
101
|
+
}
|
|
102
|
+
async function needsRebuild() {
|
|
103
|
+
const paths = embeddingIndexPaths();
|
|
104
|
+
const indexFiles = [paths.basic, paths.asm, paths.mixed, paths.hardware, paths.other];
|
|
105
|
+
const indexTimes = await Promise.all(indexFiles.map((file) => fileMtime(file)));
|
|
106
|
+
if (indexTimes.some((time) => time === null)) {
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
const oldestIndex = Math.min(...indexTimes);
|
|
110
|
+
const [basicDataM, asmDataM, externalM, docsM, bootstrapM, agentsM, promptsM, chatM] = await Promise.all([
|
|
111
|
+
dirMtimeRecursive(BASIC_DATA_DIR),
|
|
112
|
+
dirMtimeRecursive(ASM_DATA_DIR),
|
|
113
|
+
dirMtimeRecursive(EXTERNAL_DIR),
|
|
114
|
+
dirMtimeRecursive(DOC_DIR).catch(() => 0),
|
|
115
|
+
fileMtime(BOOTSTRAP_PATH).then((v) => v ?? 0),
|
|
116
|
+
fileMtime(AGENTS_PATH).then((v) => v ?? 0),
|
|
117
|
+
promptFilesMtime().then((v) => v ?? 0),
|
|
118
|
+
fileMtime(CHAT_PATH).then((v) => v ?? 0),
|
|
119
|
+
]);
|
|
120
|
+
const newestSource = Math.max(basicDataM, asmDataM, externalM, docsM, bootstrapM, agentsM, promptsM, chatM);
|
|
121
|
+
return newestSource > oldestIndex;
|
|
122
|
+
}
|
|
123
|
+
async function promptFilesMtime() {
|
|
124
|
+
let newest = 0;
|
|
125
|
+
try {
|
|
126
|
+
const entries = await fs.readdir(PROMPTS_DIR, { withFileTypes: true });
|
|
127
|
+
for (const entry of entries) {
|
|
128
|
+
if (!entry.isFile() || !entry.name.endsWith(".prompt.md"))
|
|
129
|
+
continue;
|
|
130
|
+
const stat = await fs.stat(path.join(PROMPTS_DIR, entry.name)).catch(() => null);
|
|
131
|
+
if (stat && stat.mtimeMs > newest)
|
|
132
|
+
newest = stat.mtimeMs;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
return 0;
|
|
137
|
+
}
|
|
138
|
+
return newest;
|
|
139
|
+
}
|
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
const KNOWLEDGE_BUNDLES = [
|
|
4
|
+
{
|
|
5
|
+
id: "orientation",
|
|
6
|
+
title: "Workflow & Orientation",
|
|
7
|
+
summary: "Mandatory workflow and safety guidance before issuing any C64 commands.",
|
|
8
|
+
prompts: ["basic-program", "assembly-program", "memory-debug"],
|
|
9
|
+
tools: ["upload_and_run_basic", "upload_and_run_asm", "read_screen", "read_memory"],
|
|
10
|
+
resources: [
|
|
11
|
+
{
|
|
12
|
+
uri: "c64://context/bootstrap",
|
|
13
|
+
name: "Workflow Rules & Best Practices",
|
|
14
|
+
description: "CRITICAL: Mandatory workflow rules for all C64 programming",
|
|
15
|
+
relativePath: "data/context/bootstrap.md",
|
|
16
|
+
priority: "critical",
|
|
17
|
+
summary: "Step-by-step rules for safe automation, verification, and rollback on the C64.",
|
|
18
|
+
prompts: ["basic-program", "assembly-program", "memory-debug"],
|
|
19
|
+
tools: ["upload_and_run_basic", "upload_and_run_asm", "read_screen", "read_memory"],
|
|
20
|
+
tags: ["workflow", "safety"],
|
|
21
|
+
},
|
|
22
|
+
],
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
id: "languages",
|
|
26
|
+
title: "Programming Languages",
|
|
27
|
+
summary: "Language references required before generating BASIC or 6502 assembly programs.",
|
|
28
|
+
prompts: ["basic-program", "assembly-program"],
|
|
29
|
+
tools: ["upload_and_run_basic", "upload_and_run_asm", "read_screen", "read_memory"],
|
|
30
|
+
resources: [
|
|
31
|
+
{
|
|
32
|
+
uri: "c64://specs/basic",
|
|
33
|
+
name: "Commodore BASIC v2 Specification",
|
|
34
|
+
description: "Complete BASIC v2 reference. READ THIS BEFORE generating any BASIC code!",
|
|
35
|
+
relativePath: "data/basic/basic-spec.md",
|
|
36
|
+
priority: "critical",
|
|
37
|
+
summary: "Token definitions, syntax rules, and device I/O guidance for BASIC v2.",
|
|
38
|
+
prompts: ["basic-program"],
|
|
39
|
+
tools: ["upload_and_run_basic", "read_screen"],
|
|
40
|
+
relatedResources: ["c64://context/bootstrap"],
|
|
41
|
+
tags: ["basic", "language"],
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
uri: "c64://specs/assembly",
|
|
45
|
+
name: "6502/6510 Assembly Reference",
|
|
46
|
+
description: "Full instruction set and addressing modes. READ THIS BEFORE generating assembly!",
|
|
47
|
+
relativePath: "data/assembly/assembly-spec.md",
|
|
48
|
+
priority: "critical",
|
|
49
|
+
summary: "Official opcode matrix, addressing modes, and zero-page strategy for the 6510 CPU.",
|
|
50
|
+
prompts: ["assembly-program"],
|
|
51
|
+
tools: ["upload_and_run_asm", "read_memory"],
|
|
52
|
+
relatedResources: ["c64://context/bootstrap"],
|
|
53
|
+
tags: ["assembly", "language"],
|
|
54
|
+
},
|
|
55
|
+
],
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
id: "audio",
|
|
59
|
+
title: "SID & Audio",
|
|
60
|
+
summary: "All references needed to compose, play, and verify SID music.",
|
|
61
|
+
prompts: ["sid-music"],
|
|
62
|
+
tools: ["music_generate", "sid_note_on", "sid_note_off", "sid_volume", "sidplay_file", "analyze_audio"],
|
|
63
|
+
resources: [
|
|
64
|
+
{
|
|
65
|
+
uri: "c64://specs/sid",
|
|
66
|
+
name: "SID Chip Programming Guide",
|
|
67
|
+
description: "Sound Interface Device registers and music programming",
|
|
68
|
+
relativePath: "data/audio/sid-spec.md",
|
|
69
|
+
priority: "critical",
|
|
70
|
+
summary: "Register map, waveform behaviour, and ADSR envelopes for expressive SID playback.",
|
|
71
|
+
prompts: ["sid-music"],
|
|
72
|
+
tools: ["sid_note_on", "sid_note_off", "sid_volume", "music_generate", "analyze_audio"],
|
|
73
|
+
relatedResources: ["c64://specs/sidwave", "c64://docs/sid/file-structure"],
|
|
74
|
+
tags: ["sid", "audio"],
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
uri: "c64://specs/sidwave",
|
|
78
|
+
name: "SIDWAVE Music Format Specification",
|
|
79
|
+
description: "YAML/JSON music composition format for SID chip",
|
|
80
|
+
relativePath: "data/audio/sidwave.md",
|
|
81
|
+
priority: "reference",
|
|
82
|
+
summary: "Defines the SIDWAVE interchange format used by the SID composer workflow.",
|
|
83
|
+
prompts: ["sid-music"],
|
|
84
|
+
tools: ["music_generate", "sidplay_file"],
|
|
85
|
+
relatedResources: ["c64://specs/sid", "c64://docs/sid/file-structure"],
|
|
86
|
+
tags: ["sid", "format"],
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
uri: "c64://docs/sid/file-structure",
|
|
90
|
+
name: "SID File Structure Reference",
|
|
91
|
+
description: "Breakdown of the SID file format layout and metadata",
|
|
92
|
+
relativePath: "data/audio/sid-file-structure.md",
|
|
93
|
+
priority: "reference",
|
|
94
|
+
summary: "Explains PSID/RSID headers, metadata blocks, and compatibility notes for imported music.",
|
|
95
|
+
prompts: ["sid-music"],
|
|
96
|
+
tools: ["sidplay_file", "music_generate"],
|
|
97
|
+
relatedResources: ["c64://specs/sid", "c64://specs/sidwave"],
|
|
98
|
+
tags: ["sid", "format"],
|
|
99
|
+
},
|
|
100
|
+
],
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
id: "graphics",
|
|
104
|
+
title: "Graphics & VIC-II",
|
|
105
|
+
summary: "Key material for PETSCII art, sprites, and raster control on the VIC-II.",
|
|
106
|
+
prompts: ["graphics-demo"],
|
|
107
|
+
tools: ["render_petscii_screen", "generate_sprite_prg", "write_memory"],
|
|
108
|
+
resources: [
|
|
109
|
+
{
|
|
110
|
+
uri: "c64://specs/vic",
|
|
111
|
+
name: "VIC-II Graphics Specification",
|
|
112
|
+
description: "Video chip, sprites, raster programming, and timing",
|
|
113
|
+
relativePath: "data/video/vic-spec.md",
|
|
114
|
+
priority: "critical",
|
|
115
|
+
summary: "Covers raster timing, sprite control, colour RAM, and bitmap modes on the VIC-II.",
|
|
116
|
+
prompts: ["graphics-demo"],
|
|
117
|
+
tools: ["render_petscii_screen", "generate_sprite_prg", "write_memory"],
|
|
118
|
+
relatedResources: ["c64://specs/assembly"],
|
|
119
|
+
tags: ["vic", "graphics"],
|
|
120
|
+
},
|
|
121
|
+
],
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
id: "printer",
|
|
125
|
+
title: "Printers & Hardcopy",
|
|
126
|
+
summary: "Guides for Commodore MPS and Epson FX printing workflows, including prompts.",
|
|
127
|
+
prompts: ["printer-job"],
|
|
128
|
+
tools: [
|
|
129
|
+
"printer_guide",
|
|
130
|
+
"printer_commodore_text",
|
|
131
|
+
"printer_commodore_bitmap",
|
|
132
|
+
"printer_epson_text",
|
|
133
|
+
"printer_epson_bitmap",
|
|
134
|
+
"printer_prompts",
|
|
135
|
+
],
|
|
136
|
+
resources: [
|
|
137
|
+
{
|
|
138
|
+
uri: "c64://specs/printer",
|
|
139
|
+
name: "Printer Programming Guide",
|
|
140
|
+
description: "Commodore MPS and Epson FX printer control",
|
|
141
|
+
relativePath: "data/printer/printer-spec.md",
|
|
142
|
+
priority: "critical",
|
|
143
|
+
summary: "Covers device setup, control codes, and Ultimate 64 integration for printers.",
|
|
144
|
+
prompts: ["printer-job"],
|
|
145
|
+
tools: ["printer_guide", "printer_commodore_text", "printer_epson_text"],
|
|
146
|
+
relatedResources: [
|
|
147
|
+
"c64://docs/printer/guide",
|
|
148
|
+
"c64://docs/printer/commodore-text",
|
|
149
|
+
"c64://docs/printer/epson-text",
|
|
150
|
+
],
|
|
151
|
+
tags: ["printer", "spec"],
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
uri: "c64://docs/printer/guide",
|
|
155
|
+
name: "Printer Workflow Guide",
|
|
156
|
+
description: "Unified quick reference for Commodore and Epson printers",
|
|
157
|
+
relativePath: "data/printer/printer-spec.md",
|
|
158
|
+
priority: "reference",
|
|
159
|
+
summary: "Quick-look workflow covering setup, troubleshooting, and sample jobs for both printer families.",
|
|
160
|
+
prompts: ["printer-job"],
|
|
161
|
+
tools: ["printer_guide"],
|
|
162
|
+
relatedResources: [
|
|
163
|
+
"c64://specs/printer",
|
|
164
|
+
"c64://docs/printer/commodore-text",
|
|
165
|
+
"c64://docs/printer/epson-text",
|
|
166
|
+
],
|
|
167
|
+
tags: ["printer", "workflow"],
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
uri: "c64://docs/printer/commodore-text",
|
|
171
|
+
name: "Commodore Printer Text Guide",
|
|
172
|
+
description: "Device 4 character printing reference for Commodore MPS printers",
|
|
173
|
+
relativePath: "data/printer/printer-commodore.md",
|
|
174
|
+
priority: "reference",
|
|
175
|
+
summary: "Character sets, control codes, and formatting for Commodore MPS text output.",
|
|
176
|
+
prompts: ["printer-job"],
|
|
177
|
+
tools: ["printer_commodore_text"],
|
|
178
|
+
relatedResources: [
|
|
179
|
+
"c64://specs/printer",
|
|
180
|
+
"c64://docs/printer/commodore-bitmap",
|
|
181
|
+
],
|
|
182
|
+
tags: ["printer", "commodore"],
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
uri: "c64://docs/printer/commodore-bitmap",
|
|
186
|
+
name: "Commodore Printer Bitmap Guide",
|
|
187
|
+
description: "Bitmap and custom character printing workflow for Commodore printers",
|
|
188
|
+
relativePath: "data/printer/printer-commodore-bitmap.md",
|
|
189
|
+
priority: "reference",
|
|
190
|
+
summary: "Details bitmap modes, graphics commands, and data layout for MPS bitmap printing.",
|
|
191
|
+
prompts: ["printer-job"],
|
|
192
|
+
tools: ["printer_commodore_bitmap"],
|
|
193
|
+
relatedResources: [
|
|
194
|
+
"c64://docs/printer/commodore-text",
|
|
195
|
+
"c64://specs/printer",
|
|
196
|
+
],
|
|
197
|
+
tags: ["printer", "commodore", "graphics"],
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
uri: "c64://docs/printer/epson-text",
|
|
201
|
+
name: "Epson Printer Text Guide",
|
|
202
|
+
description: "Text control sequences for Epson FX-compatible printers",
|
|
203
|
+
relativePath: "data/printer/printer-epson.md",
|
|
204
|
+
priority: "reference",
|
|
205
|
+
summary: "Lists ESC/P control codes and formatting advice for Epson FX text output.",
|
|
206
|
+
prompts: ["printer-job"],
|
|
207
|
+
tools: ["printer_epson_text"],
|
|
208
|
+
relatedResources: [
|
|
209
|
+
"c64://docs/printer/epson-bitmap",
|
|
210
|
+
"c64://specs/printer",
|
|
211
|
+
],
|
|
212
|
+
tags: ["printer", "epson"],
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
uri: "c64://docs/printer/epson-bitmap",
|
|
216
|
+
name: "Epson Printer Bitmap Guide",
|
|
217
|
+
description: "Bitmap printing and graphics control for Epson FX printers",
|
|
218
|
+
relativePath: "data/printer/printer-epson-bitmap.md",
|
|
219
|
+
priority: "reference",
|
|
220
|
+
summary: "Explains bit-image modes, density options, and data packing for Epson bitmap jobs.",
|
|
221
|
+
prompts: ["printer-job"],
|
|
222
|
+
tools: ["printer_epson_bitmap"],
|
|
223
|
+
relatedResources: [
|
|
224
|
+
"c64://docs/printer/epson-text",
|
|
225
|
+
"c64://specs/printer",
|
|
226
|
+
],
|
|
227
|
+
tags: ["printer", "epson", "graphics"],
|
|
228
|
+
},
|
|
229
|
+
{
|
|
230
|
+
uri: "c64://docs/printer/prompts",
|
|
231
|
+
name: "Printer Prompt Templates",
|
|
232
|
+
description: "Template prompts and workflow guidance for printer jobs",
|
|
233
|
+
relativePath: "data/printer/printer-prompts.md",
|
|
234
|
+
priority: "supplemental",
|
|
235
|
+
summary: "Reusable prompt templates that drive complex printer jobs through the MCP server.",
|
|
236
|
+
prompts: ["printer-job"],
|
|
237
|
+
tools: ["printer_prompts"],
|
|
238
|
+
relatedResources: [
|
|
239
|
+
"c64://docs/printer/guide",
|
|
240
|
+
"c64://docs/printer/commodore-text",
|
|
241
|
+
"c64://docs/printer/epson-text",
|
|
242
|
+
],
|
|
243
|
+
tags: ["printer", "prompts"],
|
|
244
|
+
},
|
|
245
|
+
],
|
|
246
|
+
},
|
|
247
|
+
];
|
|
248
|
+
const BASE_RESOURCES = KNOWLEDGE_BUNDLES.flatMap((bundle, bundleIndex) => bundle.resources.map((resource, resourceIndex) => ({
|
|
249
|
+
uri: resource.uri,
|
|
250
|
+
name: resource.name,
|
|
251
|
+
description: resource.description,
|
|
252
|
+
mimeType: "text/markdown",
|
|
253
|
+
relativePath: resource.relativePath,
|
|
254
|
+
metadata: {
|
|
255
|
+
domain: bundle.id,
|
|
256
|
+
priority: resource.priority,
|
|
257
|
+
summary: resource.summary,
|
|
258
|
+
prompts: resource.prompts,
|
|
259
|
+
tools: resource.tools,
|
|
260
|
+
tags: resource.tags ?? [],
|
|
261
|
+
bundle: {
|
|
262
|
+
id: bundle.id,
|
|
263
|
+
title: bundle.title,
|
|
264
|
+
summary: bundle.summary,
|
|
265
|
+
order: bundleIndex,
|
|
266
|
+
},
|
|
267
|
+
order: resourceIndex,
|
|
268
|
+
relatedResources: resource.relatedResources ?? [],
|
|
269
|
+
},
|
|
270
|
+
})));
|
|
271
|
+
const INDEX_RESOURCE = {
|
|
272
|
+
uri: "c64://docs/index",
|
|
273
|
+
name: "C64 Knowledge Map",
|
|
274
|
+
description: "Start here for a guided tour of all Commodore 64 knowledge resources",
|
|
275
|
+
mimeType: "text/markdown",
|
|
276
|
+
metadata: {
|
|
277
|
+
domain: "overview",
|
|
278
|
+
priority: "critical",
|
|
279
|
+
summary: "Explains how to approach each knowledge bundle and when to consult it.",
|
|
280
|
+
prompts: ["basic-program", "assembly-program", "sid-music", "graphics-demo", "printer-job", "memory-debug"],
|
|
281
|
+
tools: [
|
|
282
|
+
"upload_and_run_basic",
|
|
283
|
+
"upload_and_run_asm",
|
|
284
|
+
"music_generate",
|
|
285
|
+
"render_petscii_screen",
|
|
286
|
+
"printer_guide",
|
|
287
|
+
"read_memory",
|
|
288
|
+
],
|
|
289
|
+
tags: ["overview"],
|
|
290
|
+
bundle: {
|
|
291
|
+
id: "overview",
|
|
292
|
+
title: "Knowledge Overview",
|
|
293
|
+
summary: "Read this first to understand how the MCP server organizes Commodore 64 expertise.",
|
|
294
|
+
order: -1,
|
|
295
|
+
},
|
|
296
|
+
order: 0,
|
|
297
|
+
relatedResources: BASE_RESOURCES.map((resource) => resource.uri),
|
|
298
|
+
},
|
|
299
|
+
buildContent: (resources) => renderKnowledgeIndex(resources),
|
|
300
|
+
};
|
|
301
|
+
const ALL_RESOURCES = [...BASE_RESOURCES, INDEX_RESOURCE];
|
|
302
|
+
function renderKnowledgeIndex(resources) {
|
|
303
|
+
const sections = KNOWLEDGE_BUNDLES.map((bundle) => {
|
|
304
|
+
const bundleResources = resources.filter((resource) => resource.metadata.bundle.id === bundle.id);
|
|
305
|
+
const entries = bundleResources
|
|
306
|
+
.map((resource) => {
|
|
307
|
+
const star = resource.metadata.priority === "critical" ? "★ " : "";
|
|
308
|
+
const prompts = resource.metadata.prompts.length
|
|
309
|
+
? ` Prompts: ${resource.metadata.prompts
|
|
310
|
+
.map((prompt) => `\`${prompt}\``)
|
|
311
|
+
.join(", ")}.`
|
|
312
|
+
: "";
|
|
313
|
+
const tools = resource.metadata.tools.length
|
|
314
|
+
? ` Tools: ${resource.metadata.tools
|
|
315
|
+
.map((tool) => `\`${tool}\``)
|
|
316
|
+
.join(", ")}.`
|
|
317
|
+
: "";
|
|
318
|
+
const tags = resource.metadata.tags.length
|
|
319
|
+
? ` Tags: ${resource.metadata.tags.join(", ")}.`
|
|
320
|
+
: "";
|
|
321
|
+
return `- ${star}**${resource.name}** (\`${resource.uri}\`) — ${resource.metadata.summary}.${prompts}${tools}${tags}`;
|
|
322
|
+
})
|
|
323
|
+
.join("\n");
|
|
324
|
+
return `## ${bundle.title}\n\n${bundle.summary}\n\n${entries}`;
|
|
325
|
+
});
|
|
326
|
+
return [
|
|
327
|
+
"# C64 Knowledge Map",
|
|
328
|
+
"Start with critical (★) entries before invoking tools or generating code.",
|
|
329
|
+
...sections,
|
|
330
|
+
].join("\n\n");
|
|
331
|
+
}
|
|
332
|
+
export function listKnowledgeResources() {
|
|
333
|
+
return ALL_RESOURCES;
|
|
334
|
+
}
|
|
335
|
+
export function getKnowledgeResource(uri) {
|
|
336
|
+
return ALL_RESOURCES.find((resource) => resource.uri === uri);
|
|
337
|
+
}
|
|
338
|
+
export function readKnowledgeResource(uri, projectRoot) {
|
|
339
|
+
const resource = getKnowledgeResource(uri);
|
|
340
|
+
if (!resource) {
|
|
341
|
+
return undefined;
|
|
342
|
+
}
|
|
343
|
+
if (resource.buildContent) {
|
|
344
|
+
const baseResources = ALL_RESOURCES.filter((item) => item.uri !== resource.uri);
|
|
345
|
+
return {
|
|
346
|
+
uri: resource.uri,
|
|
347
|
+
mimeType: resource.mimeType,
|
|
348
|
+
text: resource.buildContent(baseResources),
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
if (!resource.relativePath) {
|
|
352
|
+
return undefined;
|
|
353
|
+
}
|
|
354
|
+
const fullPath = join(projectRoot, resource.relativePath);
|
|
355
|
+
const text = readFileSync(fullPath, "utf-8");
|
|
356
|
+
return {
|
|
357
|
+
uri: resource.uri,
|
|
358
|
+
mimeType: resource.mimeType,
|
|
359
|
+
text,
|
|
360
|
+
};
|
|
361
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { formatErrorMessage, formatPayloadForDebug, loggerFor, payloadByteLength } from "../logger.js";
|
|
2
|
+
export class LoggingRagRetriever {
|
|
3
|
+
constructor(inner, logger = loggerFor("rag")) {
|
|
4
|
+
this.inner = inner;
|
|
5
|
+
this.log = logger;
|
|
6
|
+
}
|
|
7
|
+
async retrieve(query, topK, filterLanguage) {
|
|
8
|
+
const startedAt = Date.now();
|
|
9
|
+
if (this.log.isDebugEnabled()) {
|
|
10
|
+
this.log.debug("rag request", {
|
|
11
|
+
query,
|
|
12
|
+
topK,
|
|
13
|
+
language: filterLanguage,
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
try {
|
|
17
|
+
const results = await this.inner.retrieve(query, topK, filterLanguage);
|
|
18
|
+
const latency = Date.now() - startedAt;
|
|
19
|
+
const bytes = payloadByteLength(results);
|
|
20
|
+
const keywords = summarizeQuery(query);
|
|
21
|
+
const language = filterLanguage ?? "any";
|
|
22
|
+
this.log.info(`retrieve language=${language} keywords="${keywords}" hits=${results.length} bytes=${bytes} latencyMs=${latency}`);
|
|
23
|
+
if (this.log.isDebugEnabled()) {
|
|
24
|
+
this.log.debug("rag response", {
|
|
25
|
+
results: formatPayloadForDebug(results),
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
return results;
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
const latency = Date.now() - startedAt;
|
|
32
|
+
const keywords = summarizeQuery(query);
|
|
33
|
+
const language = filterLanguage ?? "any";
|
|
34
|
+
this.log.error(`retrieve language=${language} keywords="${keywords}" bytes=0 latencyMs=${latency} error=${formatErrorMessage(error)}`);
|
|
35
|
+
throw error;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
function summarizeQuery(query) {
|
|
40
|
+
const cleaned = query.trim().replace(/\s+/g, " ");
|
|
41
|
+
if (!cleaned)
|
|
42
|
+
return "";
|
|
43
|
+
const words = cleaned.split(" ").slice(0, 6);
|
|
44
|
+
const summary = words.join(" ");
|
|
45
|
+
return summary.length > 64 ? `${summary.slice(0, 61)}...` : summary;
|
|
46
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/*
|
|
2
|
+
C64 Bridge - Simple per-domain rate limiter
|
|
3
|
+
GPL-2.0-only
|
|
4
|
+
*/
|
|
5
|
+
export class RealTimeSource {
|
|
6
|
+
nowMs() {
|
|
7
|
+
return Date.now();
|
|
8
|
+
}
|
|
9
|
+
async sleepMs(ms) {
|
|
10
|
+
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Sliding-window rate limiter enforcing at most `maxPerSecond` events per key.
|
|
15
|
+
*/
|
|
16
|
+
export class SlidingWindowRateLimiter {
|
|
17
|
+
constructor(maxPerSecond, timeSource = new RealTimeSource()) {
|
|
18
|
+
this.keyToTimestamps = new Map();
|
|
19
|
+
this.maxPerSecond = maxPerSecond;
|
|
20
|
+
this.timeSource = timeSource;
|
|
21
|
+
}
|
|
22
|
+
pruneOld(key, now) {
|
|
23
|
+
const windowStart = now - 1000;
|
|
24
|
+
const arr = this.keyToTimestamps.get(key);
|
|
25
|
+
if (!arr)
|
|
26
|
+
return;
|
|
27
|
+
let startIdx = 0;
|
|
28
|
+
while (startIdx < arr.length && arr[startIdx] < windowStart)
|
|
29
|
+
startIdx++;
|
|
30
|
+
if (startIdx > 0)
|
|
31
|
+
arr.splice(0, startIdx);
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Wait until a token is available for the given key, then consume it.
|
|
35
|
+
*/
|
|
36
|
+
async consume(key) {
|
|
37
|
+
if (this.maxPerSecond === Infinity)
|
|
38
|
+
return; // disabled
|
|
39
|
+
if (this.maxPerSecond <= 0)
|
|
40
|
+
throw new Error("maxPerSecond must be > 0 or Infinity");
|
|
41
|
+
// eslint-disable-next-line no-constant-condition
|
|
42
|
+
while (true) {
|
|
43
|
+
const now = this.timeSource.nowMs();
|
|
44
|
+
const arr = this.keyToTimestamps.get(key) ?? [];
|
|
45
|
+
this.pruneOld(key, now);
|
|
46
|
+
const inWindow = arr.length;
|
|
47
|
+
if (inWindow < this.maxPerSecond) {
|
|
48
|
+
arr.push(now);
|
|
49
|
+
this.keyToTimestamps.set(key, arr);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
const oldest = arr[0];
|
|
53
|
+
const waitMs = Math.max(0, 1000 - (now - oldest));
|
|
54
|
+
await this.timeSource.sleepMs(Math.min(waitMs, 50));
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
export class AdaptiveRateLimiter {
|
|
59
|
+
constructor(defaultRps, timeSource = new RealTimeSource(), opts = {}) {
|
|
60
|
+
this.keyToTimestamps = new Map();
|
|
61
|
+
this.keyToRps = new Map(); // [currentRps]
|
|
62
|
+
this.keyToLastAdjust = new Map();
|
|
63
|
+
if (!(defaultRps > 0))
|
|
64
|
+
throw new Error("defaultRps must be > 0");
|
|
65
|
+
this.defaultRps = defaultRps;
|
|
66
|
+
this.timeSource = timeSource;
|
|
67
|
+
this.minRps = opts.minRps ?? 1;
|
|
68
|
+
this.increaseIntervalMs = opts.increaseIntervalMs ?? 15000;
|
|
69
|
+
this.increaseStep = opts.increaseStep ?? 1;
|
|
70
|
+
}
|
|
71
|
+
pruneOld(key, now) {
|
|
72
|
+
const arr = this.keyToTimestamps.get(key);
|
|
73
|
+
if (!arr || arr.length === 0)
|
|
74
|
+
return;
|
|
75
|
+
const cutoff = now - 1000;
|
|
76
|
+
let i = 0;
|
|
77
|
+
while (i < arr.length && arr[i] < cutoff)
|
|
78
|
+
i++;
|
|
79
|
+
if (i > 0)
|
|
80
|
+
arr.splice(0, i);
|
|
81
|
+
}
|
|
82
|
+
currentRps(key) {
|
|
83
|
+
const cur = this.keyToRps.get(key)?.[0];
|
|
84
|
+
if (cur && cur > 0)
|
|
85
|
+
return cur;
|
|
86
|
+
this.keyToRps.set(key, [this.defaultRps]);
|
|
87
|
+
return this.defaultRps;
|
|
88
|
+
}
|
|
89
|
+
/** Notify the limiter that throttling occurred, reducing RPS for the key. */
|
|
90
|
+
notifyThrottle(key, factor = 0.5) {
|
|
91
|
+
const now = this.timeSource.nowMs();
|
|
92
|
+
const cur = this.currentRps(key);
|
|
93
|
+
const next = Math.max(this.minRps, Math.floor(cur * factor));
|
|
94
|
+
this.keyToRps.set(key, [next]);
|
|
95
|
+
this.keyToLastAdjust.set(key, now);
|
|
96
|
+
}
|
|
97
|
+
maybeRecover(key, now) {
|
|
98
|
+
const cur = this.currentRps(key);
|
|
99
|
+
if (cur >= this.defaultRps)
|
|
100
|
+
return;
|
|
101
|
+
const last = this.keyToLastAdjust.get(key) ?? 0;
|
|
102
|
+
if (now - last >= this.increaseIntervalMs) {
|
|
103
|
+
const next = Math.min(this.defaultRps, cur + this.increaseStep);
|
|
104
|
+
this.keyToRps.set(key, [next]);
|
|
105
|
+
this.keyToLastAdjust.set(key, now);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
/** Wait until a token is available for the given key, then consume it. */
|
|
109
|
+
async consume(key) {
|
|
110
|
+
// eslint-disable-next-line no-constant-condition
|
|
111
|
+
while (true) {
|
|
112
|
+
const now = this.timeSource.nowMs();
|
|
113
|
+
const arr = this.keyToTimestamps.get(key) ?? [];
|
|
114
|
+
this.pruneOld(key, now);
|
|
115
|
+
this.maybeRecover(key, now);
|
|
116
|
+
const maxPerSecond = this.currentRps(key);
|
|
117
|
+
if (arr.length < maxPerSecond) {
|
|
118
|
+
arr.push(now);
|
|
119
|
+
this.keyToTimestamps.set(key, arr);
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
const oldest = arr[0];
|
|
123
|
+
const waitMs = Math.max(0, 1000 - (now - oldest));
|
|
124
|
+
await this.timeSource.sleepMs(Math.min(waitMs, 10));
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|