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,401 @@
|
|
|
1
|
+
/*
|
|
2
|
+
MCP RAG - GitHub discovery for C64-related repositories
|
|
3
|
+
Uses GitHub REST API (Octokit) to search public repos with C64-related code
|
|
4
|
+
and appends them to src/rag/sources.csv while preserving existing data.
|
|
5
|
+
*/
|
|
6
|
+
import fs from 'node:fs/promises';
|
|
7
|
+
import fsSync from 'node:fs';
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
import crypto from 'node:crypto';
|
|
10
|
+
import process from 'node:process';
|
|
11
|
+
import { fileURLToPath } from 'node:url';
|
|
12
|
+
import { Octokit } from 'octokit';
|
|
13
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
14
|
+
const PROJECT_ROOT = path.resolve(path.join(__dirname, '..', '..'));
|
|
15
|
+
function resolveRagAsset(fileName) {
|
|
16
|
+
// Prefer dist/rag when running from a packaged install; fall back to src/rag in repo checkouts
|
|
17
|
+
const distPath = path.join(PROJECT_ROOT, 'dist', 'rag', fileName);
|
|
18
|
+
try {
|
|
19
|
+
if (fsSync.existsSync(distPath))
|
|
20
|
+
return distPath;
|
|
21
|
+
}
|
|
22
|
+
catch { }
|
|
23
|
+
return path.join(PROJECT_ROOT, 'src', 'rag', fileName);
|
|
24
|
+
}
|
|
25
|
+
const SOURCES_CSV = resolveRagAsset('sources.csv');
|
|
26
|
+
const CONFIG_JSON = resolveRagAsset('discover.config.json');
|
|
27
|
+
const CACHE_JSON = path.join(PROJECT_ROOT, 'data', 'discover-cache.json');
|
|
28
|
+
const ASSEMBLY_EXTS = new Set(['.asm', '.a', '.a65', '.acme', '.dasm', '.kick', '.s', '.x65']);
|
|
29
|
+
function stableJson(input) {
|
|
30
|
+
try {
|
|
31
|
+
return JSON.stringify(input, Object.keys(input).sort());
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return JSON.stringify(input);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function sha1(input) {
|
|
38
|
+
return crypto.createHash('sha1').update(input).digest('hex');
|
|
39
|
+
}
|
|
40
|
+
function csvEscape(value) {
|
|
41
|
+
const s = String(value ?? '');
|
|
42
|
+
if (/[",\n]/.test(s)) {
|
|
43
|
+
return '"' + s.replace(/"/g, '""') + '"';
|
|
44
|
+
}
|
|
45
|
+
return s;
|
|
46
|
+
}
|
|
47
|
+
function splitCsvLine(line) {
|
|
48
|
+
const result = [];
|
|
49
|
+
let current = '';
|
|
50
|
+
let inQuotes = false;
|
|
51
|
+
for (let i = 0; i < line.length; i++) {
|
|
52
|
+
const ch = line[i];
|
|
53
|
+
if (ch === '"') {
|
|
54
|
+
if (inQuotes && line[i + 1] === '"') {
|
|
55
|
+
current += '"';
|
|
56
|
+
i++;
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
inQuotes = !inQuotes;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
else if (ch === ',' && !inQuotes) {
|
|
63
|
+
result.push(current);
|
|
64
|
+
current = '';
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
current += ch;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
result.push(current);
|
|
71
|
+
return result.map((s) => s.trim());
|
|
72
|
+
}
|
|
73
|
+
async function readCsv(filePath) {
|
|
74
|
+
const raw = await fs.readFile(filePath, 'utf8');
|
|
75
|
+
const lines = raw
|
|
76
|
+
.split(/\r?\n/)
|
|
77
|
+
.map((l) => l.trim())
|
|
78
|
+
.filter((l) => l.length > 0 && !l.startsWith('#'));
|
|
79
|
+
if (lines.length === 0)
|
|
80
|
+
return [];
|
|
81
|
+
const header = lines.shift(); // header must exist
|
|
82
|
+
const cols = header.split(',').map((c) => c.trim());
|
|
83
|
+
const idx = {};
|
|
84
|
+
cols.forEach((c, i) => (idx[c] = i));
|
|
85
|
+
const rows = [];
|
|
86
|
+
for (const line of lines) {
|
|
87
|
+
const parts = splitCsvLine(line);
|
|
88
|
+
const type = parts[idx['type']] ?? '';
|
|
89
|
+
const description = parts[idx['description']] ?? '';
|
|
90
|
+
const link = parts[idx['link']];
|
|
91
|
+
const depthStr = parts[idx['depth']];
|
|
92
|
+
if (!link)
|
|
93
|
+
continue;
|
|
94
|
+
rows.push({ type, description, link, depth: depthStr ? Number(depthStr) : NaN });
|
|
95
|
+
}
|
|
96
|
+
return rows;
|
|
97
|
+
}
|
|
98
|
+
async function writeCsv(filePath, rows) {
|
|
99
|
+
const header = 'type,description,link,depth';
|
|
100
|
+
const lines = [header];
|
|
101
|
+
for (const row of rows) {
|
|
102
|
+
const line = [csvEscape(row.type), csvEscape(row.description), csvEscape(row.link), csvEscape(row.depth)].join(',');
|
|
103
|
+
lines.push(line);
|
|
104
|
+
}
|
|
105
|
+
const text = lines.join('\n') + '\n';
|
|
106
|
+
await fs.writeFile(filePath, text, 'utf8');
|
|
107
|
+
}
|
|
108
|
+
async function loadConfig() {
|
|
109
|
+
const raw = await fs.readFile(CONFIG_JSON, 'utf8');
|
|
110
|
+
const data = JSON.parse(raw);
|
|
111
|
+
return data;
|
|
112
|
+
}
|
|
113
|
+
function buildKeywordGroups(keywords) {
|
|
114
|
+
// Split into chunks so individual queries are not too long
|
|
115
|
+
const tokens = keywords.map((k) => (/\s/.test(k) ? `"${k}"` : k));
|
|
116
|
+
const groups = [];
|
|
117
|
+
const maxLen = 150; // keep well under GitHub query limits when combined with filters
|
|
118
|
+
let current = [];
|
|
119
|
+
let currentLen = 0;
|
|
120
|
+
for (const token of tokens) {
|
|
121
|
+
const addition = (current.length > 0 ? 4 : 0) + token.length; // account for " OR "
|
|
122
|
+
if (current.length > 0 && currentLen + addition > maxLen) {
|
|
123
|
+
groups.push(current);
|
|
124
|
+
current = [token];
|
|
125
|
+
currentLen = token.length;
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
current.push(token);
|
|
129
|
+
currentLen += addition;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
if (current.length > 0) {
|
|
133
|
+
groups.push(current);
|
|
134
|
+
}
|
|
135
|
+
return groups;
|
|
136
|
+
}
|
|
137
|
+
function normalizeRepoUrl(htmlUrl) {
|
|
138
|
+
try {
|
|
139
|
+
const u = new URL(htmlUrl);
|
|
140
|
+
if (u.hostname.toLowerCase() !== 'github.com')
|
|
141
|
+
return htmlUrl.replace(/\/$/, '');
|
|
142
|
+
const parts = u.pathname.split('/').filter(Boolean);
|
|
143
|
+
if (parts.length >= 2) {
|
|
144
|
+
const owner = parts[0].toLowerCase();
|
|
145
|
+
const repo = parts[1].replace(/\.git$/, '').toLowerCase();
|
|
146
|
+
return `https://github.com/${owner}/${repo}`;
|
|
147
|
+
}
|
|
148
|
+
return `https://github.com/${parts.join('/')}`.replace(/\/$/, '');
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
return htmlUrl.replace(/\/$/, '');
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
function classifyType(matchedExts) {
|
|
155
|
+
if (matchedExts.has('.bas'))
|
|
156
|
+
return 'basic';
|
|
157
|
+
for (const ext of matchedExts) {
|
|
158
|
+
if (ASSEMBLY_EXTS.has(ext))
|
|
159
|
+
return 'assembly';
|
|
160
|
+
}
|
|
161
|
+
return 'misc';
|
|
162
|
+
}
|
|
163
|
+
async function sleep(ms) {
|
|
164
|
+
await new Promise((r) => setTimeout(r, ms));
|
|
165
|
+
}
|
|
166
|
+
async function maybeWaitOnRateLimit(headers) {
|
|
167
|
+
const remainingRaw = headers['x-ratelimit-remaining'];
|
|
168
|
+
const resetRaw = headers['x-ratelimit-reset'];
|
|
169
|
+
const remaining = typeof remainingRaw === 'string' ? parseInt(remainingRaw, 10) : typeof remainingRaw === 'number' ? remainingRaw : NaN;
|
|
170
|
+
const reset = typeof resetRaw === 'string' ? parseInt(resetRaw, 10) : typeof resetRaw === 'number' ? resetRaw : NaN;
|
|
171
|
+
if (Number.isFinite(remaining) && remaining <= 1 && Number.isFinite(reset)) {
|
|
172
|
+
const nowSec = Math.floor(Date.now() / 1000);
|
|
173
|
+
const waitMs = Math.max(0, (reset - nowSec) * 1000) + 1000;
|
|
174
|
+
if (waitMs > 0) {
|
|
175
|
+
console.error(`Rate limit low (remaining=${remaining}). Waiting ${(waitMs / 1000).toFixed(1)}s until reset...`);
|
|
176
|
+
await sleep(waitMs);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
async function run() {
|
|
181
|
+
const cfg = await loadConfig();
|
|
182
|
+
const token = process.env.GITHUB_TOKEN;
|
|
183
|
+
if (!token) {
|
|
184
|
+
console.error('GITHUB_TOKEN is required to use the GitHub code search API. Please export GITHUB_TOKEN and rerun.');
|
|
185
|
+
process.exitCode = 1;
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
const octokit = new Octokit({ auth: token, userAgent: 'c64bridge-rag-discover/1.0' });
|
|
189
|
+
const configHash = sha1(stableJson(cfg));
|
|
190
|
+
// Load cache if present and not forced to refresh
|
|
191
|
+
let cache = null;
|
|
192
|
+
const forceRefresh = process.env.RAG_DISCOVER_FORCE_REFRESH === '1';
|
|
193
|
+
try {
|
|
194
|
+
const raw = await fs.readFile(CACHE_JSON, 'utf8');
|
|
195
|
+
const parsed = JSON.parse(raw);
|
|
196
|
+
if (!forceRefresh && parsed.configHash === configHash && Array.isArray(parsed.items)) {
|
|
197
|
+
cache = parsed;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
catch {
|
|
201
|
+
// ignore
|
|
202
|
+
}
|
|
203
|
+
const existingRows = await readCsv(SOURCES_CSV);
|
|
204
|
+
const existingLinks = new Set(existingRows.map((r) => normalizeRepoUrl(r.link)));
|
|
205
|
+
let repos = new Map(); // key: owner/repo lowercased
|
|
206
|
+
if (cache) {
|
|
207
|
+
for (const item of cache.items) {
|
|
208
|
+
const key = item.full_name.toLowerCase();
|
|
209
|
+
repos.set(key, {
|
|
210
|
+
full_name: item.full_name,
|
|
211
|
+
html_url: normalizeRepoUrl(item.html_url),
|
|
212
|
+
stargazers_count: item.stargazers_count,
|
|
213
|
+
description: item.description ?? null,
|
|
214
|
+
matchedExtensions: new Set(item.matchedExtensions.map((e) => e.toLowerCase())),
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
console.log(`Loaded ${repos.size} repositories from cache.`);
|
|
218
|
+
}
|
|
219
|
+
else {
|
|
220
|
+
const keywordGroups = buildKeywordGroups(cfg.keywords);
|
|
221
|
+
const targetExts = cfg.fileExtensions.map((e) => e.toLowerCase()).filter((e) => e.trim().length > 0);
|
|
222
|
+
outer: for (const extRaw of targetExts) {
|
|
223
|
+
const ext = extRaw.replace(/^\./, '');
|
|
224
|
+
if (!ext) {
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
const storageExt = `.${ext}`;
|
|
228
|
+
const queue = keywordGroups.map((tokens) => [...tokens]);
|
|
229
|
+
while (queue.length > 0) {
|
|
230
|
+
const tokens = queue.shift();
|
|
231
|
+
if (!tokens || tokens.length === 0) {
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
const clause = tokens.length === 1 ? tokens[0] : `(${tokens.join(' OR ')})`;
|
|
235
|
+
const q = `${clause} in:file extension:${ext}`;
|
|
236
|
+
let page = 1;
|
|
237
|
+
while (true) {
|
|
238
|
+
let res;
|
|
239
|
+
try {
|
|
240
|
+
res = await octokit.rest.search.code({ q, per_page: 100, page });
|
|
241
|
+
}
|
|
242
|
+
catch (err) {
|
|
243
|
+
const status = err?.status ?? err?.response?.status;
|
|
244
|
+
if (status === 422) {
|
|
245
|
+
if (tokens.length > 1) {
|
|
246
|
+
console.warn(`Query too complex for GitHub code search: "${q}". Falling back to individual keywords.`);
|
|
247
|
+
for (let i = tokens.length - 1; i >= 0; i--) {
|
|
248
|
+
queue.unshift([tokens[i]]);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
else {
|
|
252
|
+
console.warn(`Skipping keyword ${tokens[0]} with extension ${storageExt}: ${err?.message ?? 'query parsing error'}`);
|
|
253
|
+
}
|
|
254
|
+
break;
|
|
255
|
+
}
|
|
256
|
+
const headers = (err?.response?.headers ?? {});
|
|
257
|
+
const retryAfter = Number(headers['retry-after'] ?? 0);
|
|
258
|
+
if (status === 403 || status === 429) {
|
|
259
|
+
const reset = Number(headers['x-ratelimit-reset'] ?? 0);
|
|
260
|
+
if (retryAfter > 0) {
|
|
261
|
+
await sleep(retryAfter * 1000);
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
if (reset > 0) {
|
|
265
|
+
const nowSec = Math.floor(Date.now() / 1000);
|
|
266
|
+
const waitMs = Math.max(0, (reset - nowSec) * 1000) + 1000;
|
|
267
|
+
await sleep(waitMs);
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
throw err;
|
|
272
|
+
}
|
|
273
|
+
const { data, headers } = res;
|
|
274
|
+
await maybeWaitOnRateLimit(headers);
|
|
275
|
+
for (const item of data.items) {
|
|
276
|
+
// Each code result item includes a repository object
|
|
277
|
+
const repo = item.repository;
|
|
278
|
+
const key = repo.full_name.toLowerCase();
|
|
279
|
+
const existing = repos.get(key);
|
|
280
|
+
if (!existing) {
|
|
281
|
+
repos.set(key, {
|
|
282
|
+
full_name: repo.full_name,
|
|
283
|
+
html_url: normalizeRepoUrl(repo.html_url),
|
|
284
|
+
stargazers_count: 0,
|
|
285
|
+
description: null,
|
|
286
|
+
matchedExtensions: new Set([storageExt]),
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
else {
|
|
290
|
+
existing.matchedExtensions.add(storageExt);
|
|
291
|
+
}
|
|
292
|
+
if (repos.size >= cfg.maxRepos)
|
|
293
|
+
break outer;
|
|
294
|
+
}
|
|
295
|
+
const fetched = data.items.length;
|
|
296
|
+
if (fetched < 100 || (data.total_count && page * 100 >= Math.min(1000, data.total_count))) {
|
|
297
|
+
break; // no more pages for this query
|
|
298
|
+
}
|
|
299
|
+
page++;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
// Enrich with repo metadata and filter by stars
|
|
304
|
+
const keys = Array.from(repos.keys());
|
|
305
|
+
const concurrency = 6;
|
|
306
|
+
let i = 0;
|
|
307
|
+
const enriched = new Map();
|
|
308
|
+
async function worker() {
|
|
309
|
+
while (true) {
|
|
310
|
+
const idx = i++;
|
|
311
|
+
if (idx >= keys.length)
|
|
312
|
+
return;
|
|
313
|
+
const key = keys[idx];
|
|
314
|
+
const { full_name } = repos.get(key);
|
|
315
|
+
const [owner, repo] = full_name.split('/');
|
|
316
|
+
try {
|
|
317
|
+
const res = await octokit.rest.repos.get({ owner, repo });
|
|
318
|
+
await maybeWaitOnRateLimit(res.headers);
|
|
319
|
+
if (res.status === 200) {
|
|
320
|
+
const info = res.data;
|
|
321
|
+
const entry = repos.get(key);
|
|
322
|
+
entry.stargazers_count = info.stargazers_count ?? 0;
|
|
323
|
+
entry.description = info.description ?? null;
|
|
324
|
+
if (entry.stargazers_count >= cfg.minStars) {
|
|
325
|
+
enriched.set(key, entry);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
catch (err) {
|
|
330
|
+
const status = err?.status ?? err?.response?.status;
|
|
331
|
+
if (status === 404)
|
|
332
|
+
continue; // repo gone
|
|
333
|
+
const headers = (err?.response?.headers ?? {});
|
|
334
|
+
const retryAfter = Number(headers['retry-after'] ?? 0);
|
|
335
|
+
const reset = Number(headers['x-ratelimit-reset'] ?? 0);
|
|
336
|
+
if (status === 403 || status === 429) {
|
|
337
|
+
if (retryAfter > 0) {
|
|
338
|
+
await sleep(retryAfter * 1000);
|
|
339
|
+
i = Math.min(i, idx); // retry soon
|
|
340
|
+
continue;
|
|
341
|
+
}
|
|
342
|
+
if (reset > 0) {
|
|
343
|
+
const nowSec = Math.floor(Date.now() / 1000);
|
|
344
|
+
const waitMs = Math.max(0, (reset - nowSec) * 1000) + 1000;
|
|
345
|
+
await sleep(waitMs);
|
|
346
|
+
i = Math.min(i, idx);
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
// log and skip
|
|
351
|
+
console.error(`Repo fetch failed for ${full_name}: ${err?.message ?? err}`);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
await Promise.all(Array.from({ length: concurrency }, () => worker()));
|
|
356
|
+
repos = enriched;
|
|
357
|
+
// Save cache
|
|
358
|
+
const cacheDir = path.dirname(CACHE_JSON);
|
|
359
|
+
await fs.mkdir(cacheDir, { recursive: true });
|
|
360
|
+
const cacheData = {
|
|
361
|
+
version: 1,
|
|
362
|
+
configHash,
|
|
363
|
+
createdAt: new Date().toISOString(),
|
|
364
|
+
items: Array.from(repos.values()).map((r) => ({
|
|
365
|
+
full_name: r.full_name,
|
|
366
|
+
html_url: r.html_url,
|
|
367
|
+
stargazers_count: r.stargazers_count,
|
|
368
|
+
description: r.description ?? null,
|
|
369
|
+
matchedExtensions: Array.from(r.matchedExtensions),
|
|
370
|
+
})),
|
|
371
|
+
};
|
|
372
|
+
await fs.writeFile(CACHE_JSON, JSON.stringify(cacheData, null, 2) + '\n', 'utf8');
|
|
373
|
+
}
|
|
374
|
+
// Build new CSV rows
|
|
375
|
+
const newRows = [];
|
|
376
|
+
for (const r of repos.values()) {
|
|
377
|
+
const link = normalizeRepoUrl(r.html_url);
|
|
378
|
+
if (existingLinks.has(link))
|
|
379
|
+
continue;
|
|
380
|
+
const type = classifyType(r.matchedExtensions);
|
|
381
|
+
const description = (r.description?.trim() || 'C64 source repository').replace(/\r/g, ' ').replace(/\n/g, ' ');
|
|
382
|
+
newRows.push({ type, description, link, depth: cfg.defaultDepth });
|
|
383
|
+
}
|
|
384
|
+
// Merge and sort
|
|
385
|
+
const merged = existingRows.map((r) => ({
|
|
386
|
+
type: r.type ?? '',
|
|
387
|
+
description: (r.description ?? '').replace(/\r/g, ' ').replace(/\n/g, ' '),
|
|
388
|
+
link: normalizeRepoUrl(r.link),
|
|
389
|
+
depth: Number.isFinite(r.depth) ? r.depth : cfg.defaultDepth,
|
|
390
|
+
})).concat(newRows);
|
|
391
|
+
merged.sort((a, b) => a.description.toLowerCase().localeCompare(b.description.toLowerCase()));
|
|
392
|
+
await writeCsv(SOURCES_CSV, merged);
|
|
393
|
+
// Stats output
|
|
394
|
+
console.log(`Total found repositories: ${repos.size}`);
|
|
395
|
+
console.log(`New entries added: ${newRows.length}`);
|
|
396
|
+
console.log(`Total unique entries after merge: ${merged.length}`);
|
|
397
|
+
}
|
|
398
|
+
run().catch((err) => {
|
|
399
|
+
console.error(err);
|
|
400
|
+
process.exitCode = 1;
|
|
401
|
+
});
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/*
|
|
2
|
+
C64 Bridge - Local RAG Embeddings
|
|
3
|
+
GPL-2.0-only
|
|
4
|
+
*/
|
|
5
|
+
import crypto from "node:crypto";
|
|
6
|
+
/**
|
|
7
|
+
* Deterministic local embedding model using SHA-256 feature hashing.
|
|
8
|
+
* Provides stable, cosine-comparable vectors without external services.
|
|
9
|
+
*/
|
|
10
|
+
export class LocalMiniHashEmbedding {
|
|
11
|
+
constructor(dim = 384, numHashBuckets = 2048) {
|
|
12
|
+
this.tokenRegex = /[A-Za-z0-9_$#]+|[^\s]/g;
|
|
13
|
+
this.dim = dim;
|
|
14
|
+
this.numHashBuckets = numHashBuckets;
|
|
15
|
+
}
|
|
16
|
+
async embed(text) {
|
|
17
|
+
const vector = new Float32Array(this.dim);
|
|
18
|
+
if (!text)
|
|
19
|
+
return vector;
|
|
20
|
+
const tokens = text.match(this.tokenRegex) ?? [];
|
|
21
|
+
if (tokens.length === 0)
|
|
22
|
+
return vector;
|
|
23
|
+
for (const token of tokens) {
|
|
24
|
+
const h = this.hash(token);
|
|
25
|
+
const bucket = h % this.numHashBuckets;
|
|
26
|
+
const sign = (h & 1) === 0 ? 1 : -1;
|
|
27
|
+
const base = (bucket * 3) % this.dim;
|
|
28
|
+
vector[base] += sign * 1.0;
|
|
29
|
+
vector[(base + 97) % this.dim] += sign * 0.5;
|
|
30
|
+
vector[(base + 211) % this.dim] += sign * 0.25;
|
|
31
|
+
}
|
|
32
|
+
const norm = Math.hypot(...Array.from(vector));
|
|
33
|
+
if (norm > 0) {
|
|
34
|
+
for (let i = 0; i < vector.length; i++)
|
|
35
|
+
vector[i] /= norm;
|
|
36
|
+
}
|
|
37
|
+
return vector;
|
|
38
|
+
}
|
|
39
|
+
hash(input) {
|
|
40
|
+
const digest = crypto.createHash("sha256").update(input).digest();
|
|
41
|
+
return digest.readUInt32BE(0);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
export function cosineSimilarity(a, b) {
|
|
45
|
+
if (a.length !== b.length)
|
|
46
|
+
throw new Error("Vector size mismatch");
|
|
47
|
+
let dot = 0;
|
|
48
|
+
let na = 0;
|
|
49
|
+
let nb = 0;
|
|
50
|
+
for (let i = 0; i < a.length; i++) {
|
|
51
|
+
const va = a[i];
|
|
52
|
+
const vb = b[i];
|
|
53
|
+
dot += va * vb;
|
|
54
|
+
na += va * va;
|
|
55
|
+
nb += vb * vb;
|
|
56
|
+
}
|
|
57
|
+
if (na === 0 || nb === 0)
|
|
58
|
+
return 0;
|
|
59
|
+
return dot / (Math.sqrt(na) * Math.sqrt(nb));
|
|
60
|
+
}
|