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,770 @@
|
|
|
1
|
+
/*
|
|
2
|
+
C64 Bridge - Local RAG Indexer
|
|
3
|
+
GPL-2.0-only
|
|
4
|
+
*/
|
|
5
|
+
import fs from "node:fs/promises";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import fsSync from "node:fs";
|
|
8
|
+
const BASIC_DIR = path.resolve("data/basic/examples");
|
|
9
|
+
const ASM_DIR = path.resolve("data/assembly/examples");
|
|
10
|
+
const EXTERNAL_DIR = path.resolve("external");
|
|
11
|
+
const DOC_ROOT = path.resolve("doc");
|
|
12
|
+
const CONTEXT_DIR = path.resolve("data/context");
|
|
13
|
+
const BOOTSTRAP_PATH = path.join(CONTEXT_DIR, "bootstrap.md");
|
|
14
|
+
const AGENTS_PATH = path.resolve("AGENTS.md");
|
|
15
|
+
const PROMPTS_DIR = path.resolve(".github/prompts");
|
|
16
|
+
const CHAT_PATH = path.join(CONTEXT_DIR, "chat.md");
|
|
17
|
+
// RAG_DOC_FILES env var can add extra specific files, comma-separated
|
|
18
|
+
const ENV_DOC_FILES = (process.env.RAG_DOC_FILES ?? "")
|
|
19
|
+
.split(",")
|
|
20
|
+
.map((entry) => entry.trim())
|
|
21
|
+
.filter(Boolean)
|
|
22
|
+
.map((entry) => path.resolve(entry));
|
|
23
|
+
function resolveEmbeddingsDir(override) {
|
|
24
|
+
return path.resolve(override ?? process.env.RAG_EMBEDDINGS_DIR ?? "data");
|
|
25
|
+
}
|
|
26
|
+
function embeddingIndexPaths(dir) {
|
|
27
|
+
return {
|
|
28
|
+
basic: path.join(dir, "embeddings_basic.json"),
|
|
29
|
+
asm: path.join(dir, "embeddings_asm.json"),
|
|
30
|
+
mixed: path.join(dir, "embeddings_mixed.json"),
|
|
31
|
+
hardware: path.join(dir, "embeddings_hardware.json"),
|
|
32
|
+
other: path.join(dir, "embeddings_other.json"),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
export async function ensureSeedDirs() {
|
|
36
|
+
await fs.mkdir(BASIC_DIR, { recursive: true });
|
|
37
|
+
await fs.mkdir(ASM_DIR, { recursive: true });
|
|
38
|
+
// External dir is created by the fetch CLI; ensure it exists for indexing if present
|
|
39
|
+
if (!fsSync.existsSync(EXTERNAL_DIR)) {
|
|
40
|
+
try {
|
|
41
|
+
await fs.mkdir(EXTERNAL_DIR, { recursive: true });
|
|
42
|
+
}
|
|
43
|
+
catch { }
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
async function collectFiles(root, exts) {
|
|
47
|
+
const results = [];
|
|
48
|
+
async function walk(dir) {
|
|
49
|
+
let entries = [];
|
|
50
|
+
try {
|
|
51
|
+
entries = await fs.readdir(dir, { withFileTypes: true });
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
for (const entry of entries) {
|
|
57
|
+
const full = path.join(dir, entry.name);
|
|
58
|
+
if (entry.isDirectory()) {
|
|
59
|
+
await walk(full);
|
|
60
|
+
}
|
|
61
|
+
else if (entry.isFile()) {
|
|
62
|
+
const ext = path.extname(entry.name).toLowerCase();
|
|
63
|
+
if (exts.includes(ext))
|
|
64
|
+
results.push(full);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
await walk(root);
|
|
69
|
+
return results.sort();
|
|
70
|
+
}
|
|
71
|
+
async function loadText(file) {
|
|
72
|
+
return fs.readFile(file, "utf8");
|
|
73
|
+
}
|
|
74
|
+
function isWithinExternal(dir) {
|
|
75
|
+
const rel = path.relative(EXTERNAL_DIR, path.resolve(dir));
|
|
76
|
+
return rel === "" || (!rel.startsWith("..") && !path.isAbsolute(rel));
|
|
77
|
+
}
|
|
78
|
+
async function resolveRepoMetadata(file) {
|
|
79
|
+
let current = path.dirname(file);
|
|
80
|
+
const visited = [];
|
|
81
|
+
while (isWithinExternal(current)) {
|
|
82
|
+
const resolved = path.resolve(current);
|
|
83
|
+
if (repoMetadataCache.has(resolved)) {
|
|
84
|
+
const cached = repoMetadataCache.get(resolved);
|
|
85
|
+
for (const visit of visited) {
|
|
86
|
+
repoMetadataCache.set(visit, cached);
|
|
87
|
+
}
|
|
88
|
+
return { metadata: cached, repoRoot: cached ? resolved : undefined };
|
|
89
|
+
}
|
|
90
|
+
const metadataPath = path.join(resolved, "_metadata.json");
|
|
91
|
+
try {
|
|
92
|
+
const raw = await fs.readFile(metadataPath, "utf8");
|
|
93
|
+
const parsed = JSON.parse(raw);
|
|
94
|
+
if (parsed && parsed.type === "github" && typeof parsed.owner === "string" && typeof parsed.repo === "string") {
|
|
95
|
+
const metadata = {
|
|
96
|
+
type: "github",
|
|
97
|
+
owner: parsed.owner,
|
|
98
|
+
repo: parsed.repo,
|
|
99
|
+
branch: typeof parsed.branch === "string" ? parsed.branch : undefined,
|
|
100
|
+
repoUrl: typeof parsed.repoUrl === "string" ? parsed.repoUrl : `https://github.com/${parsed.owner}/${parsed.repo}`,
|
|
101
|
+
license: parsed.license
|
|
102
|
+
? {
|
|
103
|
+
spdxId: parsed.license.spdxId ?? null,
|
|
104
|
+
name: parsed.license.name ?? null,
|
|
105
|
+
url: parsed.license.url ?? null,
|
|
106
|
+
attribution: parsed.license.attribution ?? null,
|
|
107
|
+
}
|
|
108
|
+
: null,
|
|
109
|
+
};
|
|
110
|
+
repoMetadataCache.set(resolved, metadata);
|
|
111
|
+
for (const visit of visited) {
|
|
112
|
+
repoMetadataCache.set(visit, metadata);
|
|
113
|
+
}
|
|
114
|
+
return { metadata, repoRoot: resolved };
|
|
115
|
+
}
|
|
116
|
+
repoMetadataCache.set(resolved, null);
|
|
117
|
+
}
|
|
118
|
+
catch (err) {
|
|
119
|
+
if (err && typeof err === "object" && "code" in err && err.code === "ENOENT") {
|
|
120
|
+
// ignore missing metadata file
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
repoMetadataCache.set(resolved, null);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
visited.push(resolved);
|
|
127
|
+
if (resolved === EXTERNAL_DIR)
|
|
128
|
+
break;
|
|
129
|
+
const parent = path.dirname(resolved);
|
|
130
|
+
if (parent === resolved)
|
|
131
|
+
break;
|
|
132
|
+
current = parent;
|
|
133
|
+
}
|
|
134
|
+
for (const visit of visited) {
|
|
135
|
+
if (!repoMetadataCache.has(visit)) {
|
|
136
|
+
repoMetadataCache.set(visit, null);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return { metadata: null };
|
|
140
|
+
}
|
|
141
|
+
const BASIC_KEYWORDS = [
|
|
142
|
+
"PRINT",
|
|
143
|
+
"GOTO",
|
|
144
|
+
"GOSUB",
|
|
145
|
+
"FOR",
|
|
146
|
+
"NEXT",
|
|
147
|
+
"IF",
|
|
148
|
+
"THEN",
|
|
149
|
+
"REM",
|
|
150
|
+
"POKE",
|
|
151
|
+
"DATA",
|
|
152
|
+
"READ",
|
|
153
|
+
"INPUT",
|
|
154
|
+
"RUN",
|
|
155
|
+
"SYS",
|
|
156
|
+
"RESTORE",
|
|
157
|
+
"RETURN",
|
|
158
|
+
"END",
|
|
159
|
+
"TAB(",
|
|
160
|
+
"TI$",
|
|
161
|
+
"PEEK",
|
|
162
|
+
"USR",
|
|
163
|
+
"CHR$",
|
|
164
|
+
"LEFT$",
|
|
165
|
+
"RIGHT$",
|
|
166
|
+
];
|
|
167
|
+
const BASIC_KEYWORD_REGEX = BASIC_KEYWORDS.map((keyword) => {
|
|
168
|
+
const escaped = escapeRegExp(keyword);
|
|
169
|
+
if (/\w$/.test(keyword)) {
|
|
170
|
+
return new RegExp(`\\b${escaped}\\b`, "i");
|
|
171
|
+
}
|
|
172
|
+
return new RegExp(`\\b${escaped}`, "i");
|
|
173
|
+
});
|
|
174
|
+
const ASM_OPCODES = new Set([
|
|
175
|
+
"ADC", "AND", "ASL", "BCC", "BCS", "BEQ", "BIT", "BMI", "BNE", "BPL", "BRK", "BVC", "BVS",
|
|
176
|
+
"CLC", "CLD", "CLI", "CLV", "CMP", "CPX", "CPY", "DEC", "DEX", "DEY", "EOR", "INC", "INX",
|
|
177
|
+
"INY", "JMP", "JSR", "LDA", "LDX", "LDY", "LSR", "NOP", "ORA", "PHA", "PHP", "PLA", "PLP",
|
|
178
|
+
"ROL", "ROR", "RTI", "RTS", "SBC", "SEC", "SED", "SEI", "STA", "STX", "STY", "TAX", "TAY",
|
|
179
|
+
"TSX", "TXA", "TXS", "TYA",
|
|
180
|
+
]);
|
|
181
|
+
const ASM_DIRECTIVE_RE = /^\s*(?:!|\.)(?:byte|word|text|ascii|pet|fill|scr|org|addr|set|equ|macro|endm|include|ifdef|ifndef|endif)/i;
|
|
182
|
+
const ASM_OPCODE_RE = /^\s*(?:[A-Z_][\w]*\s*:\s*)?(?:\.\w+\s+)?([A-Z]{2,4})\b/;
|
|
183
|
+
const HARDWARE_KEYWORDS = [
|
|
184
|
+
"SID", "VIC", "VIC-II", "VICII", "CIA", "CIA1", "CIA2", "6510", "6502", "6526", "COLOR RAM",
|
|
185
|
+
"SPRITE", "RASTER", "KERNAL", "SCREEN RAM", "BORDER", "JOYSTICK", "SERIAL BUS", "IEC",
|
|
186
|
+
"LORAM", "HIRAM", "CHAREN", "BASIC ROM", "KERNAL ROM", "ULTIMATE 64", "DMA", "NMI", "IRQ",
|
|
187
|
+
"AUDIO FILTER", "VOICE 1", "VOICE 2", "VOICE 3",
|
|
188
|
+
];
|
|
189
|
+
const HARDWARE_DECIMAL_ADDRS = new Set([
|
|
190
|
+
53248, 53249, 53250, 53251, 53252, 53253, 53254, 53255, 53256, 53257, 53258, 53259, 53260, 53261, 53262, 53263,
|
|
191
|
+
53264, 53265, 53266, 53267, 53268, 53269, 53270, 53271, 53272, 53273, 53274, 53275, 53276, 53277, 53278, 53279,
|
|
192
|
+
53280, 53281, 53282, 53283, 53284, 53285, 53286, 53287, 53288, 53289, 53290, 53291, 53292, 53293, 53294, 53295,
|
|
193
|
+
54272, 54273, 54274, 54275, 54276, 54277, 54278, 54279, 54280, 54281, 54282, 54283, 54284, 54285, 54286, 54287,
|
|
194
|
+
56320, 56321, 56322, 56323, 56324, 56325,
|
|
195
|
+
]);
|
|
196
|
+
const HARDWARE_HEX_ADDR_RE = /\$[D4F][0-9A-F]{3}\b|\$040[0-9A-F]\b/i;
|
|
197
|
+
const BASIC_DECIMAL_ADDR_RE = /\b53(26[0-9]|27[0-9]|28[0-9]|29[0-9])\b/;
|
|
198
|
+
const BASIC_EXTS = [".bas", ".txt"];
|
|
199
|
+
const ASM_EXTS = [".asm", ".s", ".a65", ".inc", ".mac", ".lst", ".pal", ".src", ".txt", ".md"];
|
|
200
|
+
const EXTERNAL_EXTS = Array.from(new Set([...BASIC_EXTS, ...ASM_EXTS, ".seq"]));
|
|
201
|
+
const CATEGORY_LIST = ["basic", "asm", "mixed", "hardware", "other"];
|
|
202
|
+
const BASIC_SCORE_THRESHOLD = 4;
|
|
203
|
+
const ASM_SCORE_THRESHOLD = 4;
|
|
204
|
+
const HARDWARE_SCORE_THRESHOLD = 2;
|
|
205
|
+
const repoMetadataCache = new Map();
|
|
206
|
+
function readPromptFiles() {
|
|
207
|
+
try {
|
|
208
|
+
return fsSync
|
|
209
|
+
.readdirSync(PROMPTS_DIR, { withFileTypes: true })
|
|
210
|
+
.filter((entry) => entry.isFile() && entry.name.endsWith(".prompt.md"))
|
|
211
|
+
.map((entry) => path.join(PROMPTS_DIR, entry.name))
|
|
212
|
+
.sort();
|
|
213
|
+
}
|
|
214
|
+
catch {
|
|
215
|
+
return [];
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
function analyzeText(text) {
|
|
219
|
+
const lines = text.split(/\r?\n/);
|
|
220
|
+
let basicScore = 0;
|
|
221
|
+
let asmScore = 0;
|
|
222
|
+
let hardwareScore = 0;
|
|
223
|
+
let basicLines = 0;
|
|
224
|
+
let asmLines = 0;
|
|
225
|
+
const lowered = text.toLowerCase();
|
|
226
|
+
if (/```(?:basic|commodore|cbm-basic)/.test(lowered)) {
|
|
227
|
+
basicScore += 4;
|
|
228
|
+
}
|
|
229
|
+
if (/```(?:asm|assembly|ca65|acme|kickass)/.test(lowered)) {
|
|
230
|
+
asmScore += 4;
|
|
231
|
+
}
|
|
232
|
+
for (const line of lines) {
|
|
233
|
+
const trimmed = line.trim();
|
|
234
|
+
if (!trimmed) {
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
const upper = trimmed.toUpperCase();
|
|
238
|
+
if (/^\d{1,5}\s/.test(trimmed)) {
|
|
239
|
+
basicScore += 3;
|
|
240
|
+
basicLines++;
|
|
241
|
+
}
|
|
242
|
+
if (BASIC_DECIMAL_ADDR_RE.test(trimmed)) {
|
|
243
|
+
hardwareScore += 1;
|
|
244
|
+
}
|
|
245
|
+
if (BASIC_KEYWORD_REGEX.some((regex) => regex.test(trimmed))) {
|
|
246
|
+
basicScore += 1;
|
|
247
|
+
}
|
|
248
|
+
if (/\bREM\b/i.test(trimmed) || /\bPRINT\b/i.test(trimmed) || /\bINPUT\b/i.test(trimmed)) {
|
|
249
|
+
basicScore += 1;
|
|
250
|
+
}
|
|
251
|
+
if (/^\s*\*\s*=\s*\$?[0-9A-F]{3,4}/i.test(trimmed)) {
|
|
252
|
+
asmScore += 3;
|
|
253
|
+
asmLines++;
|
|
254
|
+
}
|
|
255
|
+
if (/^\s*;/.test(trimmed)) {
|
|
256
|
+
asmScore += 1;
|
|
257
|
+
}
|
|
258
|
+
if (ASM_DIRECTIVE_RE.test(trimmed)) {
|
|
259
|
+
asmScore += 2;
|
|
260
|
+
asmLines++;
|
|
261
|
+
}
|
|
262
|
+
else {
|
|
263
|
+
const match = ASM_OPCODE_RE.exec(upper);
|
|
264
|
+
if (match) {
|
|
265
|
+
const opcode = match[1];
|
|
266
|
+
if (opcode && ASM_OPCODES.has(opcode)) {
|
|
267
|
+
asmScore += 2;
|
|
268
|
+
asmLines++;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
if (/\#?\$[0-9A-F]{2,4}/i.test(trimmed)) {
|
|
273
|
+
asmScore += 1;
|
|
274
|
+
hardwareScore += 0.5;
|
|
275
|
+
}
|
|
276
|
+
if (HARDWARE_HEX_ADDR_RE.test(trimmed)) {
|
|
277
|
+
hardwareScore += 1;
|
|
278
|
+
}
|
|
279
|
+
const decimalMatches = trimmed.match(/\b\d{4,5}\b/g);
|
|
280
|
+
if (decimalMatches) {
|
|
281
|
+
for (const token of decimalMatches) {
|
|
282
|
+
const value = Number(token);
|
|
283
|
+
if (HARDWARE_DECIMAL_ADDRS.has(value)) {
|
|
284
|
+
hardwareScore += 1;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
for (const keyword of HARDWARE_KEYWORDS) {
|
|
289
|
+
if (upper.includes(keyword)) {
|
|
290
|
+
hardwareScore += 1;
|
|
291
|
+
break;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
return { basicScore, asmScore, hardwareScore, basicLines, asmLines };
|
|
296
|
+
}
|
|
297
|
+
function escapeRegExp(input) {
|
|
298
|
+
return input.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
299
|
+
}
|
|
300
|
+
function decideCategory(analysis, ext, root) {
|
|
301
|
+
const lowerExt = ext.toLowerCase();
|
|
302
|
+
const hasBasic = analysis.basicScore >= BASIC_SCORE_THRESHOLD || analysis.basicLines >= 3;
|
|
303
|
+
const hasAsm = analysis.asmScore >= ASM_SCORE_THRESHOLD || analysis.asmLines >= 3;
|
|
304
|
+
const hasHardware = analysis.hardwareScore >= HARDWARE_SCORE_THRESHOLD;
|
|
305
|
+
if (hasBasic && hasAsm)
|
|
306
|
+
return "mixed";
|
|
307
|
+
if (hasBasic)
|
|
308
|
+
return "basic";
|
|
309
|
+
if (hasAsm)
|
|
310
|
+
return "asm";
|
|
311
|
+
if (hasHardware)
|
|
312
|
+
return "hardware";
|
|
313
|
+
if (lowerExt === ".bas" || root === BASIC_DIR)
|
|
314
|
+
return "basic";
|
|
315
|
+
if ([".asm", ".s", ".a65", ".inc", ".mac", ".lst", ".pal", ".src"].includes(lowerExt) || root === ASM_DIR) {
|
|
316
|
+
return "asm";
|
|
317
|
+
}
|
|
318
|
+
return "other";
|
|
319
|
+
}
|
|
320
|
+
function toPosixRelative(from, to) {
|
|
321
|
+
return path.relative(from, to).split("\\").join("/");
|
|
322
|
+
}
|
|
323
|
+
function nameRelativeTo(root, file) {
|
|
324
|
+
return toPosixRelative(root, file);
|
|
325
|
+
}
|
|
326
|
+
function sanitizeSectionTitle(input) {
|
|
327
|
+
const trimmed = input.trim().replace(/\s+/g, " ");
|
|
328
|
+
if (!trimmed)
|
|
329
|
+
return "section";
|
|
330
|
+
const collapsed = trimmed.replace(/[^A-Za-z0-9 -]/g, "").trim();
|
|
331
|
+
const normalized = collapsed || "section";
|
|
332
|
+
return normalized
|
|
333
|
+
.replace(/\s+/g, "-")
|
|
334
|
+
.replace(/-+/g, "-")
|
|
335
|
+
.slice(0, 60);
|
|
336
|
+
}
|
|
337
|
+
function detectHeadingCandidate(paragraph) {
|
|
338
|
+
const singleLine = !/\n/.test(paragraph);
|
|
339
|
+
if (!singleLine)
|
|
340
|
+
return null;
|
|
341
|
+
const trimmed = paragraph.trim();
|
|
342
|
+
if (!trimmed)
|
|
343
|
+
return null;
|
|
344
|
+
if (trimmed.length > 80)
|
|
345
|
+
return null;
|
|
346
|
+
if (/^chapter\s+\d+/i.test(trimmed))
|
|
347
|
+
return trimmed;
|
|
348
|
+
if (/^(section|appendix|part)\b/i.test(trimmed))
|
|
349
|
+
return trimmed;
|
|
350
|
+
if (/^\d+(?:\.\d+)*\s+[A-Za-z]/.test(trimmed))
|
|
351
|
+
return trimmed;
|
|
352
|
+
const letters = trimmed.replace(/[^A-Za-z]/g, "");
|
|
353
|
+
if (letters.length < 4)
|
|
354
|
+
return null;
|
|
355
|
+
const uppercase = letters.replace(/[^A-Z]/g, "");
|
|
356
|
+
const lowercase = letters.replace(/[^a-z]/g, "");
|
|
357
|
+
const uppercaseRatio = uppercase.length / letters.length;
|
|
358
|
+
if (uppercaseRatio >= 0.7 && lowercase.length <= 2) {
|
|
359
|
+
return trimmed;
|
|
360
|
+
}
|
|
361
|
+
return null;
|
|
362
|
+
}
|
|
363
|
+
function chunkPlainTextSections(text, filePath) {
|
|
364
|
+
const normalized = text.replace(/\r\n?/g, "\n");
|
|
365
|
+
const pages = normalized.split(/\f+/);
|
|
366
|
+
const relativePath = toPosixRelative(process.cwd(), filePath);
|
|
367
|
+
const results = [];
|
|
368
|
+
const pushChunk = (payload, sectionTitle, sequence) => {
|
|
369
|
+
if (!payload.length)
|
|
370
|
+
return;
|
|
371
|
+
const body = payload.join("\n\n").trim();
|
|
372
|
+
if (!body)
|
|
373
|
+
return;
|
|
374
|
+
const suffix = sequence > 1 ? `-${sequence}` : "";
|
|
375
|
+
const origin = `${relativePath}#${sanitizeSectionTitle(sectionTitle)}${suffix}`;
|
|
376
|
+
results.push({ text: body, origin });
|
|
377
|
+
};
|
|
378
|
+
pages.forEach((pageText, pageIndex) => {
|
|
379
|
+
const pageTag = `Page-${pageIndex + 1}`;
|
|
380
|
+
const paragraphs = pageText.split(/\n{2,}/);
|
|
381
|
+
let sectionTitle = pageTag;
|
|
382
|
+
let payload = [];
|
|
383
|
+
let charCount = 0;
|
|
384
|
+
let chunkSequence = 1;
|
|
385
|
+
const flush = () => {
|
|
386
|
+
pushChunk(payload, sectionTitle, chunkSequence);
|
|
387
|
+
if (payload.length) {
|
|
388
|
+
chunkSequence += 1;
|
|
389
|
+
}
|
|
390
|
+
payload = [];
|
|
391
|
+
charCount = 0;
|
|
392
|
+
};
|
|
393
|
+
for (const rawParagraph of paragraphs) {
|
|
394
|
+
const paragraph = rawParagraph.trim();
|
|
395
|
+
if (!paragraph)
|
|
396
|
+
continue;
|
|
397
|
+
const heading = detectHeadingCandidate(paragraph);
|
|
398
|
+
if (heading) {
|
|
399
|
+
flush();
|
|
400
|
+
sectionTitle = heading;
|
|
401
|
+
chunkSequence = 1;
|
|
402
|
+
payload.push(paragraph);
|
|
403
|
+
charCount = paragraph.length;
|
|
404
|
+
continue;
|
|
405
|
+
}
|
|
406
|
+
const additionLength = payload.length ? paragraph.length + 2 : paragraph.length;
|
|
407
|
+
if (charCount + additionLength > 1600) {
|
|
408
|
+
flush();
|
|
409
|
+
}
|
|
410
|
+
payload.push(paragraph);
|
|
411
|
+
charCount += additionLength;
|
|
412
|
+
}
|
|
413
|
+
flush();
|
|
414
|
+
});
|
|
415
|
+
if (results.length === 0) {
|
|
416
|
+
const fallbackOrigin = `${relativePath}#Full`;
|
|
417
|
+
return [{ text: normalized, origin: fallbackOrigin }];
|
|
418
|
+
}
|
|
419
|
+
return results;
|
|
420
|
+
}
|
|
421
|
+
async function buildIndexForCategory(category, files, model) {
|
|
422
|
+
const records = [];
|
|
423
|
+
const sorted = [...files].sort((a, b) => nameRelativeTo(a.root, a.file).localeCompare(nameRelativeTo(b.root, b.file)));
|
|
424
|
+
for (const entry of sorted) {
|
|
425
|
+
const vector = Array.from(await model.embed(entry.text));
|
|
426
|
+
records.push({
|
|
427
|
+
name: nameRelativeTo(entry.root, entry.file),
|
|
428
|
+
language: category,
|
|
429
|
+
vector,
|
|
430
|
+
text: entry.text,
|
|
431
|
+
sourcePath: toPosixRelative(process.cwd(), entry.file),
|
|
432
|
+
sourceMtimeMs: entry.mtimeMs,
|
|
433
|
+
sourceUrl: entry.sourceUrl,
|
|
434
|
+
sourceRepoUrl: entry.sourceRepoUrl,
|
|
435
|
+
license: entry.license,
|
|
436
|
+
licenseSpdxId: entry.licenseSpdxId,
|
|
437
|
+
licenseName: entry.licenseName,
|
|
438
|
+
licenseUrl: entry.licenseUrl,
|
|
439
|
+
attribution: entry.attribution,
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
return { dim: model.dim, model: model.constructor.name, records };
|
|
443
|
+
}
|
|
444
|
+
async function writeIndex(filePath, index) {
|
|
445
|
+
const json = JSON.stringify(index);
|
|
446
|
+
try {
|
|
447
|
+
const existing = await fs.readFile(filePath, "utf8");
|
|
448
|
+
if (existing === json) {
|
|
449
|
+
return; // no-op if unchanged
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
catch {
|
|
453
|
+
// file does not exist or unreadable — proceed to write
|
|
454
|
+
}
|
|
455
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
456
|
+
await fs.writeFile(filePath, json, "utf8");
|
|
457
|
+
}
|
|
458
|
+
export async function buildAllIndexes({ model, embeddingsDir: overrideDir, basicDirs, asmDirs, externalDirs, docFiles }) {
|
|
459
|
+
await ensureSeedDirs();
|
|
460
|
+
repoMetadataCache.clear();
|
|
461
|
+
const embeddingsDir = resolveEmbeddingsDir(overrideDir);
|
|
462
|
+
const paths = embeddingIndexPaths(embeddingsDir);
|
|
463
|
+
// Collect all markdown files under doc/ recursively, plus any explicitly provided RAG_DOC_FILES
|
|
464
|
+
const docCandidates = [];
|
|
465
|
+
try {
|
|
466
|
+
const mdFiles = await collectFiles(DOC_ROOT, [".md"]);
|
|
467
|
+
for (const f of mdFiles)
|
|
468
|
+
docCandidates.push(f);
|
|
469
|
+
}
|
|
470
|
+
catch { }
|
|
471
|
+
for (const f of ENV_DOC_FILES)
|
|
472
|
+
docCandidates.push(f);
|
|
473
|
+
// De-duplicate
|
|
474
|
+
const seenDocs = new Set();
|
|
475
|
+
const docIncluded = docCandidates.filter((f) => {
|
|
476
|
+
const exists = fsSync.existsSync(f);
|
|
477
|
+
const norm = path.resolve(f);
|
|
478
|
+
if (!exists || seenDocs.has(norm))
|
|
479
|
+
return false;
|
|
480
|
+
seenDocs.add(norm);
|
|
481
|
+
return true;
|
|
482
|
+
});
|
|
483
|
+
const resolvedBasicDirs = (basicDirs ?? [BASIC_DIR]).map((dir) => path.resolve(dir));
|
|
484
|
+
const resolvedAsmDirs = (asmDirs ?? [ASM_DIR]).map((dir) => path.resolve(dir));
|
|
485
|
+
const resolvedExternalDirs = (externalDirs ?? [EXTERNAL_DIR]).map((dir) => path.resolve(dir));
|
|
486
|
+
const externalRoots = resolvedExternalDirs.map((dir) => path.resolve(dir));
|
|
487
|
+
const sourcesConfig = [
|
|
488
|
+
...resolvedBasicDirs.map((root) => ({ root, exts: BASIC_EXTS })),
|
|
489
|
+
...resolvedAsmDirs.map((root) => ({ root, exts: ASM_EXTS })),
|
|
490
|
+
...resolvedExternalDirs.map((root) => ({ root, exts: EXTERNAL_EXTS })),
|
|
491
|
+
];
|
|
492
|
+
const fileSources = [];
|
|
493
|
+
for (const cfg of sourcesConfig) {
|
|
494
|
+
const files = await collectFiles(cfg.root, cfg.exts);
|
|
495
|
+
for (const file of files) {
|
|
496
|
+
fileSources.push({ file, root: cfg.root });
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
for (const docFile of docIncluded) {
|
|
500
|
+
fileSources.push({ file: docFile, root: process.cwd() });
|
|
501
|
+
}
|
|
502
|
+
// Include context files as single-chunk documents if present
|
|
503
|
+
const promptFiles = readPromptFiles();
|
|
504
|
+
const contextFiles = [BOOTSTRAP_PATH, AGENTS_PATH, CHAT_PATH, ...promptFiles];
|
|
505
|
+
const contextSet = new Set(contextFiles.map((file) => path.resolve(file)));
|
|
506
|
+
for (const cf of contextFiles) {
|
|
507
|
+
if (fsSync.existsSync(cf))
|
|
508
|
+
fileSources.push({ file: cf, root: process.cwd() });
|
|
509
|
+
}
|
|
510
|
+
const seen = new Set();
|
|
511
|
+
const preparedByCategory = {
|
|
512
|
+
basic: [],
|
|
513
|
+
asm: [],
|
|
514
|
+
mixed: [],
|
|
515
|
+
hardware: [],
|
|
516
|
+
other: [],
|
|
517
|
+
};
|
|
518
|
+
for (const source of fileSources) {
|
|
519
|
+
if (seen.has(source.file))
|
|
520
|
+
continue;
|
|
521
|
+
seen.add(source.file);
|
|
522
|
+
let text;
|
|
523
|
+
try {
|
|
524
|
+
text = await loadText(source.file);
|
|
525
|
+
}
|
|
526
|
+
catch {
|
|
527
|
+
continue;
|
|
528
|
+
}
|
|
529
|
+
const stat = await fs.stat(source.file).catch(() => null);
|
|
530
|
+
if (!stat)
|
|
531
|
+
continue;
|
|
532
|
+
// Resolve provenance/metadata (GitHub origin and license) once per file
|
|
533
|
+
const provenance = await resolveProvenance(source.file);
|
|
534
|
+
// For markdown docs under doc/, chunk by sections; for context files inject as a single small chunk
|
|
535
|
+
const absoluteFile = path.resolve(source.file);
|
|
536
|
+
const ext = path.extname(absoluteFile).toLowerCase();
|
|
537
|
+
const isMarkdown = ext === ".md";
|
|
538
|
+
const isContext = contextSet.has(absoluteFile);
|
|
539
|
+
const isDocFile = isMarkdown && absoluteFile.startsWith(path.resolve(DOC_ROOT) + path.sep);
|
|
540
|
+
const isExternalFile = externalRoots.some((root) => absoluteFile === root || absoluteFile.startsWith(root + path.sep));
|
|
541
|
+
const isPlainText = ext === ".txt" || ext === ".text";
|
|
542
|
+
if (isDocFile) {
|
|
543
|
+
const chunks = chunkMarkdownSections(text, source.file);
|
|
544
|
+
for (const chunk of chunks) {
|
|
545
|
+
const analysis = analyzeText(chunk.text);
|
|
546
|
+
const category = decideCategory(analysis, path.extname(source.file), source.root);
|
|
547
|
+
preparedByCategory[category].push({
|
|
548
|
+
file: source.file,
|
|
549
|
+
root: source.root,
|
|
550
|
+
text: addProvenanceComment(chunk.text, chunk.origin),
|
|
551
|
+
mtimeMs: stat.mtimeMs,
|
|
552
|
+
sourceUrl: provenance.sourceUrl,
|
|
553
|
+
sourceRepoUrl: provenance.sourceRepoUrl,
|
|
554
|
+
license: provenance.licenseLabel,
|
|
555
|
+
licenseSpdxId: provenance.licenseSpdxId,
|
|
556
|
+
licenseName: provenance.licenseName,
|
|
557
|
+
licenseUrl: provenance.licenseUrl,
|
|
558
|
+
attribution: provenance.attribution,
|
|
559
|
+
origin: chunk.origin,
|
|
560
|
+
});
|
|
561
|
+
}
|
|
562
|
+
continue;
|
|
563
|
+
}
|
|
564
|
+
if (isContext) {
|
|
565
|
+
const origin = path.relative(process.cwd(), source.file).split(path.sep).join("/");
|
|
566
|
+
const analysis = analyzeText(text);
|
|
567
|
+
const category = decideCategory(analysis, path.extname(source.file), source.root);
|
|
568
|
+
preparedByCategory[category].push({
|
|
569
|
+
file: source.file,
|
|
570
|
+
root: source.root,
|
|
571
|
+
text: addProvenanceComment(text, origin),
|
|
572
|
+
mtimeMs: stat.mtimeMs,
|
|
573
|
+
sourceUrl: provenance.sourceUrl,
|
|
574
|
+
sourceRepoUrl: provenance.sourceRepoUrl,
|
|
575
|
+
license: provenance.licenseLabel,
|
|
576
|
+
licenseSpdxId: provenance.licenseSpdxId,
|
|
577
|
+
licenseName: provenance.licenseName,
|
|
578
|
+
licenseUrl: provenance.licenseUrl,
|
|
579
|
+
attribution: provenance.attribution,
|
|
580
|
+
origin,
|
|
581
|
+
});
|
|
582
|
+
continue;
|
|
583
|
+
}
|
|
584
|
+
if (isExternalFile && isPlainText) {
|
|
585
|
+
const chunks = chunkPlainTextSections(text, source.file);
|
|
586
|
+
for (const chunk of chunks) {
|
|
587
|
+
const analysis = analyzeText(chunk.text);
|
|
588
|
+
const category = decideCategory(analysis, ext, source.root);
|
|
589
|
+
preparedByCategory[category].push({
|
|
590
|
+
file: source.file,
|
|
591
|
+
root: source.root,
|
|
592
|
+
text: addProvenanceComment(chunk.text, chunk.origin),
|
|
593
|
+
mtimeMs: stat.mtimeMs,
|
|
594
|
+
sourceUrl: provenance.sourceUrl,
|
|
595
|
+
sourceRepoUrl: provenance.sourceRepoUrl,
|
|
596
|
+
license: provenance.licenseLabel,
|
|
597
|
+
licenseSpdxId: provenance.licenseSpdxId,
|
|
598
|
+
licenseName: provenance.licenseName,
|
|
599
|
+
licenseUrl: provenance.licenseUrl,
|
|
600
|
+
attribution: provenance.attribution,
|
|
601
|
+
origin: chunk.origin,
|
|
602
|
+
});
|
|
603
|
+
}
|
|
604
|
+
continue;
|
|
605
|
+
}
|
|
606
|
+
// Default: treat as a single record (BASIC/ASM/external)
|
|
607
|
+
{
|
|
608
|
+
const analysis = analyzeText(text);
|
|
609
|
+
const category = decideCategory(analysis, ext, source.root);
|
|
610
|
+
preparedByCategory[category].push({
|
|
611
|
+
file: source.file,
|
|
612
|
+
root: source.root,
|
|
613
|
+
text,
|
|
614
|
+
mtimeMs: stat.mtimeMs,
|
|
615
|
+
sourceUrl: provenance.sourceUrl,
|
|
616
|
+
sourceRepoUrl: provenance.sourceRepoUrl,
|
|
617
|
+
license: provenance.licenseLabel,
|
|
618
|
+
licenseSpdxId: provenance.licenseSpdxId,
|
|
619
|
+
licenseName: provenance.licenseName,
|
|
620
|
+
licenseUrl: provenance.licenseUrl,
|
|
621
|
+
attribution: provenance.attribution,
|
|
622
|
+
});
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
const indexesByCategory = new Map();
|
|
626
|
+
for (const category of CATEGORY_LIST) {
|
|
627
|
+
const index = await buildIndexForCategory(category, preparedByCategory[category], model);
|
|
628
|
+
indexesByCategory.set(category, index);
|
|
629
|
+
}
|
|
630
|
+
await Promise.all([
|
|
631
|
+
writeIndex(paths.basic, indexesByCategory.get("basic")),
|
|
632
|
+
writeIndex(paths.asm, indexesByCategory.get("asm")),
|
|
633
|
+
writeIndex(paths.mixed, indexesByCategory.get("mixed")),
|
|
634
|
+
writeIndex(paths.hardware, indexesByCategory.get("hardware")),
|
|
635
|
+
writeIndex(paths.other, indexesByCategory.get("other")),
|
|
636
|
+
]);
|
|
637
|
+
}
|
|
638
|
+
// --- Helpers ---
|
|
639
|
+
function chunkMarkdownSections(text, filePath) {
|
|
640
|
+
// Split on H2/H3 headers; keep headers with content; bound chunk size to ~1500-2500 chars
|
|
641
|
+
const lines = text.split(/\r?\n/);
|
|
642
|
+
const headerRe = /^(#{1,3})\s+(.*)$/; // matches '#', '##', or '###'
|
|
643
|
+
const chunks = [];
|
|
644
|
+
let current = null;
|
|
645
|
+
for (const line of lines) {
|
|
646
|
+
const m = headerRe.exec(line);
|
|
647
|
+
if (m) {
|
|
648
|
+
if (current)
|
|
649
|
+
chunks.push(current);
|
|
650
|
+
current = { title: m[2].trim(), lines: [line] };
|
|
651
|
+
}
|
|
652
|
+
else if (current) {
|
|
653
|
+
current.lines.push(line);
|
|
654
|
+
}
|
|
655
|
+
else {
|
|
656
|
+
// preamble before first header
|
|
657
|
+
current = { title: "Preamble", lines: [line] };
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
if (current)
|
|
661
|
+
chunks.push(current);
|
|
662
|
+
const results = [];
|
|
663
|
+
for (const c of chunks) {
|
|
664
|
+
const origin = `${toPosixRelative(process.cwd(), filePath)}#${c.title}`;
|
|
665
|
+
const content = c.lines.join("\n").trim();
|
|
666
|
+
if (content.length === 0)
|
|
667
|
+
continue;
|
|
668
|
+
// If too large, further split by paragraphs
|
|
669
|
+
if (content.length > 2500) {
|
|
670
|
+
const paragraphs = content.split(/\n\n+/);
|
|
671
|
+
let buf = [];
|
|
672
|
+
let acc = 0;
|
|
673
|
+
for (const p of paragraphs) {
|
|
674
|
+
const toAdd = (buf.length ? "\n\n" : "") + p;
|
|
675
|
+
if (acc + toAdd.length > 1800) {
|
|
676
|
+
results.push({ text: buf.join("\n\n"), origin });
|
|
677
|
+
buf = [p];
|
|
678
|
+
acc = p.length;
|
|
679
|
+
}
|
|
680
|
+
else {
|
|
681
|
+
buf.push(p);
|
|
682
|
+
acc += toAdd.length;
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
if (buf.length)
|
|
686
|
+
results.push({ text: buf.join("\n\n"), origin });
|
|
687
|
+
}
|
|
688
|
+
else {
|
|
689
|
+
results.push({ text: content, origin });
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
return results;
|
|
693
|
+
}
|
|
694
|
+
function addProvenanceComment(content, origin) {
|
|
695
|
+
if (!origin)
|
|
696
|
+
return content;
|
|
697
|
+
return `<!-- Source: ${origin} -->\n${content}`;
|
|
698
|
+
}
|
|
699
|
+
async function resolveProvenance(filePath) {
|
|
700
|
+
let sourceUrl;
|
|
701
|
+
let sourceRepoUrl;
|
|
702
|
+
let licenseSpdxId;
|
|
703
|
+
let licenseName;
|
|
704
|
+
let licenseUrl;
|
|
705
|
+
let attribution;
|
|
706
|
+
const { metadata, repoRoot } = await resolveRepoMetadata(filePath);
|
|
707
|
+
if (metadata?.type === "github" && repoRoot) {
|
|
708
|
+
sourceRepoUrl = metadata.repoUrl;
|
|
709
|
+
const branch = metadata.branch ?? "main";
|
|
710
|
+
const repoRelative = path.relative(repoRoot, filePath);
|
|
711
|
+
if (!repoRelative.startsWith("..")) {
|
|
712
|
+
const encodedPath = repoRelative
|
|
713
|
+
.split(path.sep)
|
|
714
|
+
.map((segment) => encodeURIComponent(segment))
|
|
715
|
+
.join("/");
|
|
716
|
+
sourceUrl = `${metadata.repoUrl}/blob/${branch}/${encodedPath}`;
|
|
717
|
+
}
|
|
718
|
+
if (metadata.license) {
|
|
719
|
+
const spdx = metadata.license.spdxId ?? undefined;
|
|
720
|
+
if (spdx && spdx !== "NOASSERTION") {
|
|
721
|
+
licenseSpdxId = spdx;
|
|
722
|
+
}
|
|
723
|
+
licenseName = metadata.license.name ?? undefined;
|
|
724
|
+
licenseUrl = metadata.license.url ?? undefined;
|
|
725
|
+
attribution = metadata.license.attribution ?? undefined;
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
let licenseLabel;
|
|
729
|
+
if (metadata?.type === "github") {
|
|
730
|
+
if (licenseSpdxId)
|
|
731
|
+
licenseLabel = licenseSpdxId;
|
|
732
|
+
else if (licenseName)
|
|
733
|
+
licenseLabel = licenseName;
|
|
734
|
+
else
|
|
735
|
+
licenseLabel = "UNKNOWN";
|
|
736
|
+
}
|
|
737
|
+
return { sourceUrl, sourceRepoUrl, licenseLabel, licenseSpdxId, licenseName, licenseUrl, attribution };
|
|
738
|
+
}
|
|
739
|
+
export async function loadIndexes(opts = {}) {
|
|
740
|
+
// Best-effort loading
|
|
741
|
+
const result = {};
|
|
742
|
+
const embeddingsDir = resolveEmbeddingsDir(opts.embeddingsDir);
|
|
743
|
+
const paths = embeddingIndexPaths(embeddingsDir);
|
|
744
|
+
try {
|
|
745
|
+
const basic = JSON.parse(await fs.readFile(paths.basic, "utf8"));
|
|
746
|
+
result.basic = basic;
|
|
747
|
+
}
|
|
748
|
+
catch { }
|
|
749
|
+
try {
|
|
750
|
+
const asm = JSON.parse(await fs.readFile(paths.asm, "utf8"));
|
|
751
|
+
result.asm = asm;
|
|
752
|
+
}
|
|
753
|
+
catch { }
|
|
754
|
+
try {
|
|
755
|
+
const mixed = JSON.parse(await fs.readFile(paths.mixed, "utf8"));
|
|
756
|
+
result.mixed = mixed;
|
|
757
|
+
}
|
|
758
|
+
catch { }
|
|
759
|
+
try {
|
|
760
|
+
const hardware = JSON.parse(await fs.readFile(paths.hardware, "utf8"));
|
|
761
|
+
result.hardware = hardware;
|
|
762
|
+
}
|
|
763
|
+
catch { }
|
|
764
|
+
try {
|
|
765
|
+
const other = JSON.parse(await fs.readFile(paths.other, "utf8"));
|
|
766
|
+
result.other = other;
|
|
767
|
+
}
|
|
768
|
+
catch { }
|
|
769
|
+
return result;
|
|
770
|
+
}
|