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.
Files changed (138) hide show
  1. package/.github/prompts/README.md +6 -0
  2. package/.github/prompts/assembly-routine-session.prompt.md +13 -0
  3. package/.github/prompts/basic-program-session.prompt.md +13 -0
  4. package/.github/prompts/drive-management-session.prompt.md +13 -0
  5. package/.github/prompts/petscii-art-session.prompt.md +13 -0
  6. package/.github/prompts/play-classic-game.prompt.md +13 -0
  7. package/.github/prompts/sid-music-session.prompt.md +13 -0
  8. package/AGENTS.md +173 -0
  9. package/LICENSE +339 -0
  10. package/README.md +582 -0
  11. package/data/README.md +16 -0
  12. package/data/api/basic-api-spec.md +154 -0
  13. package/data/api/kernal-api-spec.md +68 -0
  14. package/data/assembly/assembly-spec.md +196 -0
  15. package/data/assembly/examples/effects/sinewave.asm +5 -0
  16. package/data/assembly/examples/graphics/sprite/mover.asm +15 -0
  17. package/data/assembly/examples/sid-file-structure.md +8 -0
  18. package/data/assembly/examples/sid-overview.md +11 -0
  19. package/data/audio/examples/Biosphere.sid +0 -0
  20. package/data/audio/sid-file-structure.md +91 -0
  21. package/data/audio/sid-programming-best-practices.md +198 -0
  22. package/data/audio/sid-spec.md +166 -0
  23. package/data/audio/sidwave.md +131 -0
  24. package/data/audio/sidwave.schema.json +64 -0
  25. package/data/basic/basic-spec.md +230 -0
  26. package/data/basic/examples/audio/alle-meine-entchen-expressive.bas +41 -0
  27. package/data/basic/examples/audio/c-major-scale-pleasant.bas +29 -0
  28. package/data/basic/examples/io/joystick.bas +5 -0
  29. package/data/basic/examples/video/bounce.bas +8 -0
  30. package/data/basic/examples/video/entchen-petscii.bas +16 -0
  31. package/data/basic/examples/video/hello-world.bas +4 -0
  32. package/data/basic/examples/video/wave.bas +7 -0
  33. package/data/context/bootstrap.md +148 -0
  34. package/data/context/chat.md +36 -0
  35. package/data/drive/drive-spec.md +215 -0
  36. package/data/embeddings_asm.json +1 -0
  37. package/data/embeddings_basic.json +1 -0
  38. package/data/embeddings_hardware.json +1 -0
  39. package/data/embeddings_mixed.json +1 -0
  40. package/data/embeddings_other.json +1 -0
  41. package/data/io/cia-spec.md +115 -0
  42. package/data/io/io-spec.md +124 -0
  43. package/data/memory/kernal-memory-map.md +227 -0
  44. package/data/memory/low-memory-map.md +133 -0
  45. package/data/memory/memory-map.md +186 -0
  46. package/data/printer/printer-commodore-bitmap.md +31 -0
  47. package/data/printer/printer-commodore.md +28 -0
  48. package/data/printer/printer-epson-bitmap.md +29 -0
  49. package/data/printer/printer-epson.md +30 -0
  50. package/data/printer/printer-prompts.md +29 -0
  51. package/data/printer/printer-spec.md +180 -0
  52. package/data/video/character-set.csv +257 -0
  53. package/data/video/vic-spec.md +276 -0
  54. package/dist/assemblyConverter.js +1299 -0
  55. package/dist/audio/record_and_analyze_audio.js +292 -0
  56. package/dist/basicConverter.js +193 -0
  57. package/dist/bootstrap/stdio-logger.js +32 -0
  58. package/dist/c64Client.js +1052 -0
  59. package/dist/chargen.js +197 -0
  60. package/dist/config.js +142 -0
  61. package/dist/context.js +157 -0
  62. package/dist/device.js +413 -0
  63. package/dist/index.js +7 -0
  64. package/dist/knowledge.js +454 -0
  65. package/dist/logger.js +182 -0
  66. package/dist/loggingHttpClient.js +88 -0
  67. package/dist/mcp-server.js +426 -0
  68. package/dist/petscii.js +135 -0
  69. package/dist/petsciiArt.js +356 -0
  70. package/dist/platform.js +82 -0
  71. package/dist/prompts/registry.js +560 -0
  72. package/dist/rag/discover.config.json +41 -0
  73. package/dist/rag/discover.js +401 -0
  74. package/dist/rag/embeddings.js +60 -0
  75. package/dist/rag/externalFetcher.js +934 -0
  76. package/dist/rag/indexer.js +770 -0
  77. package/dist/rag/init.js +139 -0
  78. package/dist/rag/knowledgeIndex.js +361 -0
  79. package/dist/rag/loggingRetriever.js +46 -0
  80. package/dist/rag/rateLimiter.js +127 -0
  81. package/dist/rag/retriever.js +74 -0
  82. package/dist/rag/sources.csv +31 -0
  83. package/dist/rag/types.js +5 -0
  84. package/dist/rag/urlUtils.js +38 -0
  85. package/dist/sidplayRunner.js +240 -0
  86. package/dist/sidwave.js +101 -0
  87. package/dist/sidwaveCompiler.js +329 -0
  88. package/dist/tools/audio.js +944 -0
  89. package/dist/tools/developer.js +515 -0
  90. package/dist/tools/errors.js +59 -0
  91. package/dist/tools/graphics.js +469 -0
  92. package/dist/tools/machineControl.js +267 -0
  93. package/dist/tools/memory.js +224 -0
  94. package/dist/tools/printer.js +482 -0
  95. package/dist/tools/programRunners.js +315 -0
  96. package/dist/tools/rag.js +153 -0
  97. package/dist/tools/registry.js +58 -0
  98. package/dist/tools/responses.js +36 -0
  99. package/dist/tools/schema.js +304 -0
  100. package/dist/tools/storage.js +740 -0
  101. package/dist/tools/streaming.js +132 -0
  102. package/dist/tools/types.js +99 -0
  103. package/dist/viceRunner.js +239 -0
  104. package/doc/MCP_SETUP.md +153 -0
  105. package/doc/README.md +7 -0
  106. package/doc/developer.md +312 -0
  107. package/doc/img/logo.png +0 -0
  108. package/doc/img/prompts/duck_petscii.png +0 -0
  109. package/doc/img/prompts/duck_song.png +0 -0
  110. package/doc/img/vscode/vscode-copilot-c64-chat-mode.png +0 -0
  111. package/doc/img/vscode/vscode-copilot-hello-world.png +0 -0
  112. package/doc/prompts/README.md +96 -0
  113. package/doc/prompts/user-prompt-ideas.md +147 -0
  114. package/doc/rest/c64-openapi.yaml +979 -0
  115. package/doc/rest/c64-rest-api.md +106 -0
  116. package/doc/troubleshooting-mcp.md +177 -0
  117. package/generated/c64/index.js +857 -0
  118. package/mcp.json +106 -0
  119. package/package.json +79 -0
  120. package/scripts/c64-cli.mjs +142 -0
  121. package/scripts/c64-tool.sh +83 -0
  122. package/scripts/check-package.mjs +89 -0
  123. package/scripts/cli.js +61 -0
  124. package/scripts/fetch-external-sources.mjs +67 -0
  125. package/scripts/generate-changelog.mjs +131 -0
  126. package/scripts/postbuild.mjs +101 -0
  127. package/scripts/prepare-release.mjs +58 -0
  128. package/scripts/rag-rebuild.mjs +53 -0
  129. package/scripts/register-ts-node.mjs +6 -0
  130. package/scripts/run-mcp-check.sh +104 -0
  131. package/scripts/run-tests.mjs +195 -0
  132. package/scripts/sid2wav.sh +94 -0
  133. package/scripts/smoke-run.sh +65 -0
  134. package/scripts/start-mock-c64.mjs +51 -0
  135. package/scripts/start-mock.mjs +31 -0
  136. package/scripts/start.mjs +72 -0
  137. package/scripts/update-readme.ts +164 -0
  138. package/scripts/verify-package.mjs +147 -0
@@ -0,0 +1,197 @@
1
+ /*
2
+ C64 Bridge - An MCP Server for the Commodore 64 Ultimate
3
+ Copyright (C) 2025 Christian Gleissner
4
+
5
+ Licensed under the GNU General Public License v2.0 or later.
6
+ See <https://www.gnu.org/licenses/> for details.
7
+ */
8
+ import { readFileSync } from "node:fs";
9
+ import { dirname, join } from "node:path";
10
+ import { fileURLToPath } from "node:url";
11
+ const glyphCache = {
12
+ loaded: false,
13
+ glyphs: [],
14
+ byPetscii: new Map(),
15
+ byBasic: new Map(),
16
+ };
17
+ function parseCsvLine(line) {
18
+ const cells = [];
19
+ let current = "";
20
+ let inQuotes = false;
21
+ for (let i = 0; i < line.length; i += 1) {
22
+ const ch = line[i];
23
+ if (inQuotes) {
24
+ if (ch === "\"") {
25
+ if (line[i + 1] === "\"") {
26
+ current += "\"";
27
+ i += 1;
28
+ }
29
+ else {
30
+ inQuotes = false;
31
+ }
32
+ }
33
+ else {
34
+ current += ch;
35
+ }
36
+ }
37
+ else if (ch === "\"") {
38
+ inQuotes = true;
39
+ }
40
+ else if (ch === ",") {
41
+ cells.push(current);
42
+ current = "";
43
+ }
44
+ else {
45
+ current += ch;
46
+ }
47
+ }
48
+ cells.push(current);
49
+ return cells;
50
+ }
51
+ function parseCsv(content) {
52
+ const lines = content.split(/\r?\n/).filter((line) => line.trim().length > 0);
53
+ if (lines.length === 0) {
54
+ return [];
55
+ }
56
+ const headerCells = parseCsvLine(lines[0].trim());
57
+ const rows = [];
58
+ for (let i = 1; i < lines.length; i += 1) {
59
+ const raw = lines[i];
60
+ const cells = parseCsvLine(raw.trim());
61
+ const row = {};
62
+ for (let j = 0; j < headerCells.length; j += 1) {
63
+ const key = headerCells[j];
64
+ const value = cells[j] ?? "";
65
+ row[key] = value;
66
+ }
67
+ rows.push(row);
68
+ }
69
+ return rows;
70
+ }
71
+ function parseDecimal(value) {
72
+ if (!value) {
73
+ return undefined;
74
+ }
75
+ const trimmed = value.trim();
76
+ if (!trimmed) {
77
+ return undefined;
78
+ }
79
+ const parsed = Number.parseInt(trimmed, 10);
80
+ return Number.isFinite(parsed) ? parsed : undefined;
81
+ }
82
+ function parseHex(value) {
83
+ if (!value) {
84
+ return undefined;
85
+ }
86
+ const trimmed = value.trim();
87
+ if (!trimmed) {
88
+ return undefined;
89
+ }
90
+ let normalized = trimmed;
91
+ if (normalized.startsWith("$")) {
92
+ normalized = normalized.slice(1);
93
+ }
94
+ else if (normalized.toLowerCase().startsWith("0x")) {
95
+ normalized = normalized.slice(2);
96
+ }
97
+ const parsed = Number.parseInt(normalized, 16);
98
+ if (Number.isNaN(parsed)) {
99
+ return undefined;
100
+ }
101
+ return parsed;
102
+ }
103
+ function parseHexByte(value) {
104
+ const parsed = parseHex(value);
105
+ if (parsed === undefined || parsed < 0 || parsed > 0xff) {
106
+ return undefined;
107
+ }
108
+ return parsed;
109
+ }
110
+ function parsePetsciiCode(row) {
111
+ return (parseDecimal(row.petscii_code_dec)
112
+ ?? parseHex(row.petscii_code)
113
+ ?? parseDecimal(row.petscii_code)
114
+ ?? parseDecimal(row.petcsii_code) // legacy typo fallback
115
+ );
116
+ }
117
+ function parseScreenCode(row) {
118
+ return (parseDecimal(row.screen_code_dec)
119
+ ?? parseHex(row.screen_code)
120
+ ?? parseDecimal(row.screen_code));
121
+ }
122
+ function readChargenCsv(baseDir) {
123
+ const csvPath = join(baseDir, "..", "data", "video", "character-set.csv");
124
+ const csv = readFileSync(csvPath, "utf8");
125
+ return parseCsv(csv);
126
+ }
127
+ function loadGlyphs() {
128
+ if (glyphCache.loaded) {
129
+ return;
130
+ }
131
+ const baseDir = dirname(fileURLToPath(import.meta.url));
132
+ const rows = readChargenCsv(baseDir);
133
+ for (const row of rows) {
134
+ const petsciiCode = parsePetsciiCode(row);
135
+ const screenCode = parseScreenCode(row);
136
+ if (petsciiCode === undefined || petsciiCode < 0) {
137
+ continue;
138
+ }
139
+ if (screenCode === undefined || screenCode < 0) {
140
+ continue;
141
+ }
142
+ const bytes = [];
143
+ for (let i = 0; i < 8; i += 1) {
144
+ const key = `byte${i + 1}`;
145
+ const legacyKey = `byte${i}`;
146
+ const value = row[key] ?? row[legacyKey];
147
+ const parsed = parseHexByte(value);
148
+ bytes.push(parsed ?? 0);
149
+ }
150
+ const name = (row.name ?? row.char_name)?.trim() || undefined;
151
+ const basicRaw = [row.basic_repr, row.char, row.keyboard, row.control_code]
152
+ .map((value) => value?.trim())
153
+ .find((value) => value && value.length > 0);
154
+ const basic = basicRaw && basicRaw.length > 0 ? basicRaw : undefined;
155
+ const glyph = {
156
+ petsciiCode,
157
+ screenCode,
158
+ basic,
159
+ name,
160
+ bitmap: Uint8Array.from(bytes),
161
+ };
162
+ glyphCache.glyphs.push(glyph);
163
+ glyphCache.byPetscii.set(petsciiCode, glyph);
164
+ if (basic && basic.length === 1) {
165
+ const setIfMissing = (key) => {
166
+ if (!glyphCache.byBasic.has(key)) {
167
+ glyphCache.byBasic.set(key, glyph);
168
+ }
169
+ };
170
+ setIfMissing(basic);
171
+ const upper = basic.toUpperCase();
172
+ if (upper !== basic) {
173
+ setIfMissing(upper);
174
+ }
175
+ const lower = basic.toLowerCase();
176
+ if (lower !== basic) {
177
+ setIfMissing(lower);
178
+ }
179
+ }
180
+ }
181
+ glyphCache.loaded = true;
182
+ }
183
+ export function getChargenGlyphs() {
184
+ loadGlyphs();
185
+ return glyphCache.glyphs;
186
+ }
187
+ export function findGlyphByPetscii(code) {
188
+ loadGlyphs();
189
+ return glyphCache.byPetscii.get(code);
190
+ }
191
+ export function findGlyphByBasicChar(char) {
192
+ if (!char) {
193
+ return undefined;
194
+ }
195
+ loadGlyphs();
196
+ return glyphCache.byBasic.get(char);
197
+ }
package/dist/config.js ADDED
@@ -0,0 +1,142 @@
1
+ import { readFileSync } from "fs";
2
+ import { dirname, join } from "path";
3
+ import { fileURLToPath } from "url";
4
+ const DEFAULT_HOST = "c64u";
5
+ const DEFAULT_PORT = 80;
6
+ const DEFAULT_CONFIG = {
7
+ c64_host: DEFAULT_HOST,
8
+ baseUrl: buildBaseUrl(DEFAULT_HOST, DEFAULT_PORT),
9
+ c64_port: DEFAULT_PORT,
10
+ };
11
+ let cachedConfig = null;
12
+ export function loadConfig() {
13
+ if (cachedConfig) {
14
+ return cachedConfig;
15
+ }
16
+ const configPath = process.env.C64BRIDGE_CONFIG ?? `${process.env.HOME}/.c64bridge.json`;
17
+ const repoConfigPath = join(dirname(fileURLToPath(import.meta.url)), "..", ".c64bridge.json");
18
+ let rawConfig;
19
+ try {
20
+ rawConfig = JSON.parse(readFileSync(configPath, "utf-8"));
21
+ }
22
+ catch (error) {
23
+ if (error.code === "ENOENT") {
24
+ try {
25
+ rawConfig = JSON.parse(readFileSync(repoConfigPath, "utf-8"));
26
+ }
27
+ catch (fallbackError) {
28
+ if (fallbackError.code === "ENOENT") {
29
+ rawConfig = {};
30
+ }
31
+ else
32
+ throw fallbackError;
33
+ }
34
+ }
35
+ else {
36
+ throw error;
37
+ }
38
+ }
39
+ // New schema: prefer c64u.{host,port,baseUrl}; keep legacy fields as fallback
40
+ const c64u = rawConfig?.c64u;
41
+ const parsedC64uHost = parseEndpoint(configuredString(c64u?.host));
42
+ const parsedC64uHostname = parseEndpoint(configuredString(c64u?.hostname));
43
+ const parsedLegacyHost = parseEndpoint(configuredString(rawConfig?.c64_host));
44
+ const parsedLegacyIp = parseEndpoint(configuredString(rawConfig?.c64_ip));
45
+ const parsedBaseOverrides = [
46
+ parseEndpoint(normaliseBaseUrl(c64u?.baseUrl)),
47
+ parseEndpoint(normaliseBaseUrl(rawConfig?.baseUrl)),
48
+ ];
49
+ const hostCandidates = [
50
+ parsedC64uHost.hostname,
51
+ parsedC64uHostname.hostname,
52
+ parsedLegacyHost.hostname,
53
+ parsedLegacyIp.hostname,
54
+ ...parsedBaseOverrides.map((entry) => entry.hostname),
55
+ ];
56
+ const portCandidates = [
57
+ configuredPort(c64u?.port),
58
+ parsedC64uHost.port,
59
+ parsedC64uHostname.port,
60
+ configuredPort(rawConfig?.c64_port),
61
+ parsedLegacyHost.port,
62
+ parsedLegacyIp.port,
63
+ ...parsedBaseOverrides.map((entry) => entry.port),
64
+ ];
65
+ const host = firstDefined(...hostCandidates) ?? DEFAULT_HOST;
66
+ const port = firstDefined(...portCandidates) ?? DEFAULT_PORT;
67
+ const baseUrl = buildBaseUrl(host, port);
68
+ const hostLabel = formatHost(host);
69
+ const hostWithPort = port === DEFAULT_PORT ? hostLabel : `${hostLabel}:${port}`;
70
+ const config = {
71
+ c64_host: hostWithPort,
72
+ baseUrl,
73
+ c64_port: port,
74
+ };
75
+ cachedConfig = config;
76
+ return config;
77
+ }
78
+ function configuredString(value) {
79
+ return typeof value === "string" ? value.trim() : undefined;
80
+ }
81
+ function configuredPort(value) {
82
+ if (typeof value === "number" && Number.isInteger(value) && value > 0 && value <= 65535) {
83
+ return value;
84
+ }
85
+ if (typeof value === "string" && value.trim()) {
86
+ const parsed = Number(value.trim());
87
+ if (Number.isInteger(parsed) && parsed > 0 && parsed <= 65535) {
88
+ return parsed;
89
+ }
90
+ }
91
+ return undefined;
92
+ }
93
+ function normaliseBaseUrl(value) {
94
+ const input = configuredString(value);
95
+ if (!input)
96
+ return undefined;
97
+ if (!/^[a-z][a-z0-9+.-]*:\/\//i.test(input)) {
98
+ return `http://${input}`;
99
+ }
100
+ return stripTrailingSlash(input);
101
+ }
102
+ function parseEndpoint(value) {
103
+ const input = configuredString(value);
104
+ if (!input)
105
+ return {};
106
+ try {
107
+ const hasScheme = /^[a-z][a-z0-9+.-]*:\/\//i.test(input);
108
+ const url = new URL(hasScheme ? input : `http://${input}`);
109
+ const hostname = url.hostname || undefined;
110
+ const port = url.port ? configuredPort(url.port) : undefined;
111
+ const baseUrl = stripTrailingSlash(`${url.protocol}//${url.host}`);
112
+ return { hostname, port, baseUrl };
113
+ }
114
+ catch {
115
+ return {};
116
+ }
117
+ }
118
+ function stripTrailingSlash(input) {
119
+ return input.replace(/\/+$/, "");
120
+ }
121
+ function firstDefined(...values) {
122
+ for (const value of values) {
123
+ if (value !== undefined && value !== null)
124
+ return value;
125
+ }
126
+ return undefined;
127
+ }
128
+ function buildBaseUrl(host, port) {
129
+ const normalizedPort = Number.isInteger(port) && port > 0 ? port : DEFAULT_PORT;
130
+ const hostPart = formatHost(host);
131
+ const suffix = normalizedPort === DEFAULT_PORT ? "" : `:${normalizedPort}`;
132
+ return `http://${hostPart}${suffix}`;
133
+ }
134
+ function formatHost(host) {
135
+ if (host.includes(":") && !host.startsWith("[")) {
136
+ return `[${host}]`;
137
+ }
138
+ return host;
139
+ }
140
+ export function __resetConfigCacheForTests() {
141
+ cachedConfig = null;
142
+ }
@@ -0,0 +1,157 @@
1
+ /*
2
+ C64 Bridge - Context Loader and Utilities
3
+ GPL-2.0-only
4
+ */
5
+ import fs from "node:fs/promises";
6
+ import path from "node:path";
7
+ const ROOT = process.cwd();
8
+ const BOOTSTRAP_PATH = path.resolve(ROOT, "data/context/bootstrap.md");
9
+ const AGENTS_PATH = path.resolve(ROOT, "AGENTS.md");
10
+ const PROMPTS_DIR = path.resolve(ROOT, ".github/prompts");
11
+ const CHAT_PATH = path.resolve(ROOT, "data/context/chat.md");
12
+ export async function loadBootstrapContext() {
13
+ const [primer, agents, prompts, chat] = await Promise.all([
14
+ readIfExists(BOOTSTRAP_PATH),
15
+ readIfExists(AGENTS_PATH),
16
+ loadPromptDefinitions(),
17
+ readIfExists(CHAT_PATH),
18
+ ]);
19
+ return {
20
+ primer: primer ?? "",
21
+ agents: agents ?? "",
22
+ prompts: prompts ?? [],
23
+ chat: chat ?? "",
24
+ };
25
+ }
26
+ async function readIfExists(file) {
27
+ try {
28
+ return await fs.readFile(file, "utf8");
29
+ }
30
+ catch (err) {
31
+ if (err && typeof err === "object" && "code" in err && err.code === "ENOENT") {
32
+ return undefined;
33
+ }
34
+ throw err;
35
+ }
36
+ }
37
+ async function loadPromptDefinitions() {
38
+ try {
39
+ await fs.access(PROMPTS_DIR);
40
+ }
41
+ catch {
42
+ return [];
43
+ }
44
+ const dirEntries = await fs.readdir(PROMPTS_DIR, { withFileTypes: true });
45
+ const files = dirEntries.filter((entry) => entry.isFile() && entry.name.endsWith(".prompt.md"));
46
+ files.sort((a, b) => a.name.localeCompare(b.name));
47
+ const defs = [];
48
+ for (const entry of files) {
49
+ const filePath = path.join(PROMPTS_DIR, entry.name);
50
+ try {
51
+ const raw = await fs.readFile(filePath, "utf8");
52
+ const parsed = parsePromptFile(raw, entry.name, filePath);
53
+ defs.push(parsed);
54
+ }
55
+ catch (err) {
56
+ // eslint-disable-next-line no-console
57
+ console.warn(`Failed to load prompt ${entry.name}:`, err);
58
+ }
59
+ }
60
+ return defs;
61
+ }
62
+ function parsePromptFile(raw, fileName, filePath) {
63
+ const { body, metadata } = stripFrontmatterAndMetadata(raw);
64
+ const slug = fileName.replace(/\.prompt\.md$/, "");
65
+ const id = metadata.id ?? slug.replace(/[-\s]+/g, "_");
66
+ const title = metadata.title ?? slug
67
+ .split(/[-_]/)
68
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
69
+ .join(" ");
70
+ const keywords = metadata.keywords ?? slug.split(/[-_]/).map((part) => part.toLowerCase());
71
+ const source = path.relative(ROOT, filePath).split(path.sep).join("/");
72
+ return {
73
+ id,
74
+ title,
75
+ keywords,
76
+ body: body.trim(),
77
+ source,
78
+ };
79
+ }
80
+ function stripFrontmatterAndMetadata(raw) {
81
+ let text = raw.trim();
82
+ const metadata = {};
83
+ const frontmatterMatch = /^---\s*\n([\s\S]*?)\n---\s*/.exec(text);
84
+ if (frontmatterMatch) {
85
+ text = text.slice(frontmatterMatch[0].length).trimStart();
86
+ }
87
+ const idMatch = text.match(/<!--\s*id:\s*([^>]+?)\s*-->/i);
88
+ if (idMatch) {
89
+ metadata.id = idMatch[1].trim();
90
+ text = text.replace(idMatch[0], "").trimStart();
91
+ }
92
+ const keywordMatch = text.match(/<!--\s*keywords:\s*([^>]+?)\s*-->/i);
93
+ if (keywordMatch) {
94
+ metadata.keywords = keywordMatch[1]
95
+ .split(/[,\s]+/)
96
+ .map((word) => word.trim().toLowerCase())
97
+ .filter(Boolean);
98
+ text = text.replace(keywordMatch[0], "").trimStart();
99
+ }
100
+ const titleMatch = text.match(/<!--\s*title:\s*([^>]+?)\s*-->/i);
101
+ if (titleMatch) {
102
+ metadata.title = titleMatch[1].trim();
103
+ text = text.replace(titleMatch[0], "").trimStart();
104
+ }
105
+ return { body: text, metadata };
106
+ }
107
+ export function extractSections(markdown, level = 2) {
108
+ const lines = markdown.split(/\r?\n/);
109
+ const re = new RegExp(`^${"#".repeat(level)}\\s+(.*)$`);
110
+ const results = [];
111
+ let current = null;
112
+ for (const line of lines) {
113
+ const m = re.exec(line);
114
+ if (m) {
115
+ if (current)
116
+ results.push(current);
117
+ current = { header: m[1].trim(), content: "" };
118
+ }
119
+ else if (current) {
120
+ current.content += (current.content ? "\n" : "") + line;
121
+ }
122
+ }
123
+ if (current)
124
+ results.push(current);
125
+ return results;
126
+ }
127
+ export function matchAgent(userText, agentsMd) {
128
+ const sections = extractSections(agentsMd, 2);
129
+ const lowered = userText.toLowerCase();
130
+ for (const sec of sections) {
131
+ const name = sec.header.toLowerCase();
132
+ if (lowered.includes("basic") && name.includes("basic"))
133
+ return sec.content.trim();
134
+ if ((/asm|assembly|machine\s*code/).test(lowered) && name.includes("asm"))
135
+ return sec.content.trim();
136
+ if ((/sid|music|song|compose/).test(lowered) && name.includes("sid"))
137
+ return sec.content.trim();
138
+ if ((/memory|peek|poke|disassemble|screen|color|colour/).test(lowered) && name.includes("memory"))
139
+ return sec.content.trim();
140
+ if ((/drive|d64|d71|d81|mount|disk/).test(lowered) && name.includes("drive"))
141
+ return sec.content.trim();
142
+ }
143
+ return undefined;
144
+ }
145
+ export function matchPrompt(userText, prompts) {
146
+ const lowered = userText.toLowerCase();
147
+ for (const prompt of prompts) {
148
+ const keywords = prompt.keywords.length > 0 ? prompt.keywords : [prompt.title.toLowerCase()];
149
+ if (keywords.some((keyword) => keyword && lowered.includes(keyword))) {
150
+ return annotated(prompt.body, prompt.source, prompt.title);
151
+ }
152
+ }
153
+ return undefined;
154
+ }
155
+ function annotated(content, source, section) {
156
+ return `<!-- Source: ${source} | Section: ${section} -->\n${content.trim()}`;
157
+ }