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,131 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Generate or extend CHANGELOG.md from commit messages since the last tag,
|
|
4
|
+
* following a Conventional Commits-like format.
|
|
5
|
+
*
|
|
6
|
+
* Groups by:
|
|
7
|
+
* - feat → Features
|
|
8
|
+
* - fix → Bug Fixes
|
|
9
|
+
* - perf → Performance
|
|
10
|
+
* - refactor → Refactoring
|
|
11
|
+
* - docs → Documentation
|
|
12
|
+
* - test → Tests
|
|
13
|
+
* - build/ci/chore → Chores
|
|
14
|
+
* - other → Other
|
|
15
|
+
*
|
|
16
|
+
* Usage: node scripts/generate-changelog.mjs [version]
|
|
17
|
+
* - version is optional; when provided, it's used as the new release header.
|
|
18
|
+
*/
|
|
19
|
+
import { execSync } from 'node:child_process';
|
|
20
|
+
import { readFile, writeFile } from 'node:fs/promises';
|
|
21
|
+
import { existsSync } from 'node:fs';
|
|
22
|
+
import path from 'node:path';
|
|
23
|
+
|
|
24
|
+
const run = (cmd) => execSync(cmd, { encoding: 'utf8' }).trim();
|
|
25
|
+
|
|
26
|
+
const nowDate = new Date().toISOString().slice(0, 10);
|
|
27
|
+
const root = process.cwd();
|
|
28
|
+
|
|
29
|
+
function getLastTag() {
|
|
30
|
+
try {
|
|
31
|
+
return run('git describe --tags --abbrev=0');
|
|
32
|
+
} catch {
|
|
33
|
+
return '';
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function getCommits(range) {
|
|
38
|
+
// Subject and short SHA; filter out merge commits later
|
|
39
|
+
const format = `%s|%h`;
|
|
40
|
+
const cmd = range ? `git log --pretty=format:"${format}" ${range}` : `git log --pretty=format:"${format}"`;
|
|
41
|
+
const out = run(cmd);
|
|
42
|
+
if (!out) return [];
|
|
43
|
+
return out.split('\n').map((line) => {
|
|
44
|
+
const [subject, sha] = line.split('|');
|
|
45
|
+
return { subject, sha };
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function classify(subject) {
|
|
50
|
+
// Conventional commits: type(scope)!: desc
|
|
51
|
+
// Extract type and optional scope
|
|
52
|
+
const m = subject.match(/^([a-zA-Z]+)(\([^\)]+\))?(!)?:\s+(.*)$/);
|
|
53
|
+
if (!m) return { group: 'other', entry: subject };
|
|
54
|
+
const [, type,, bang, desc] = m; // scope ignored in output line beyond description
|
|
55
|
+
const map = {
|
|
56
|
+
feat: 'Features',
|
|
57
|
+
fix: 'Bug Fixes',
|
|
58
|
+
perf: 'Performance',
|
|
59
|
+
refactor: 'Refactoring',
|
|
60
|
+
docs: 'Documentation',
|
|
61
|
+
test: 'Tests',
|
|
62
|
+
build: 'Chores',
|
|
63
|
+
ci: 'Chores',
|
|
64
|
+
chore: 'Chores'
|
|
65
|
+
};
|
|
66
|
+
const group = map[type.toLowerCase()] || 'Other';
|
|
67
|
+
const breaking = Boolean(bang);
|
|
68
|
+
return { group, entry: desc + (breaking ? ' (BREAKING)' : '') };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function renderSection(version, date, groups) {
|
|
72
|
+
const order = ['Features', 'Bug Fixes', 'Performance', 'Refactoring', 'Documentation', 'Tests', 'Chores', 'Other'];
|
|
73
|
+
let out = `\n## ${version} - ${date}\n\n`;
|
|
74
|
+
let any = false;
|
|
75
|
+
for (const name of order) {
|
|
76
|
+
const items = groups[name] || [];
|
|
77
|
+
if (items.length === 0) continue;
|
|
78
|
+
any = true;
|
|
79
|
+
out += `### ${name}\n\n`;
|
|
80
|
+
for (const it of items) {
|
|
81
|
+
out += `- ${it.text}\n`;
|
|
82
|
+
}
|
|
83
|
+
out += '\n';
|
|
84
|
+
}
|
|
85
|
+
if (!any) {
|
|
86
|
+
out += '- No notable changes in this release.\n\n';
|
|
87
|
+
}
|
|
88
|
+
return out;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function main() {
|
|
92
|
+
const version = process.argv[2] || '';
|
|
93
|
+
const lastTag = getLastTag();
|
|
94
|
+
const range = lastTag ? `${lastTag}..HEAD` : '';
|
|
95
|
+
const rawCommits = getCommits(range)
|
|
96
|
+
.filter((c) => c.subject && !/^Merge\s/.test(c.subject));
|
|
97
|
+
|
|
98
|
+
const groups = {};
|
|
99
|
+
for (const c of rawCommits) {
|
|
100
|
+
const { group, entry } = classify(c.subject);
|
|
101
|
+
const line = `${entry} (${c.sha})`;
|
|
102
|
+
if (!groups[group]) groups[group] = [];
|
|
103
|
+
groups[group].push({ text: line });
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Load or create CHANGELOG.md
|
|
107
|
+
const changelogPath = path.join(root, 'CHANGELOG.md');
|
|
108
|
+
const exists = existsSync(changelogPath);
|
|
109
|
+
const existing = exists ? await readFile(changelogPath, 'utf8') : '# Changelog\n\nAll notable changes to this project will be documented in this file.\n\n';
|
|
110
|
+
|
|
111
|
+
const headerVersion = version || run('node -p "require(\'./package.json\').version"');
|
|
112
|
+
const section = renderSection(headerVersion, nowDate, groups);
|
|
113
|
+
|
|
114
|
+
// Prepend the new section after a top-level title if present
|
|
115
|
+
let updated;
|
|
116
|
+
const titleMatch = existing.match(/^#\s+Changelog\s*\n/);
|
|
117
|
+
if (titleMatch) {
|
|
118
|
+
const idx = titleMatch[0].length;
|
|
119
|
+
updated = existing.slice(0, idx) + section + existing.slice(idx);
|
|
120
|
+
} else {
|
|
121
|
+
updated = section + existing;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
await writeFile(changelogPath, updated);
|
|
125
|
+
console.log(`CHANGELOG.md updated for ${headerVersion} (since ${lastTag || 'repo start'}).`);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
main().catch((err) => {
|
|
129
|
+
console.error(err);
|
|
130
|
+
process.exit(1);
|
|
131
|
+
});
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Normalize the compiled output layout after TypeScript build.
|
|
4
|
+
*
|
|
5
|
+
* - Move everything from dist/src/* to dist/*
|
|
6
|
+
* - Remove the now-empty dist/src directory
|
|
7
|
+
*
|
|
8
|
+
* This keeps the published package tree leaner (single index.js location)
|
|
9
|
+
* while still allowing TypeScript to compile with include patterns.
|
|
10
|
+
*/
|
|
11
|
+
import { readdir, rename, rm, stat, mkdir, copyFile } from 'node:fs/promises';
|
|
12
|
+
import path from 'node:path';
|
|
13
|
+
import process from 'node:process';
|
|
14
|
+
|
|
15
|
+
const distRoot = path.resolve('dist');
|
|
16
|
+
const distSrc = path.join(distRoot, 'src');
|
|
17
|
+
|
|
18
|
+
async function main() {
|
|
19
|
+
try {
|
|
20
|
+
const stats = await stat(distSrc);
|
|
21
|
+
if (!stats.isDirectory()) {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
} catch (err) {
|
|
25
|
+
if (err && err.code === 'ENOENT') {
|
|
26
|
+
return; // nothing to do
|
|
27
|
+
}
|
|
28
|
+
throw err;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const entries = await readdir(distSrc, { withFileTypes: true });
|
|
32
|
+
for (const entry of entries) {
|
|
33
|
+
const from = path.join(distSrc, entry.name);
|
|
34
|
+
const to = path.join(distRoot, entry.name);
|
|
35
|
+
// Remove any existing destination to allow rename.
|
|
36
|
+
await rm(to, { recursive: true, force: true });
|
|
37
|
+
await rename(from, to);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
await rm(distSrc, { recursive: true, force: true });
|
|
41
|
+
|
|
42
|
+
// Ensure RAG static assets (CSV/JSON) are available under dist/rag for runtime tools
|
|
43
|
+
// Source-of-truth lives under src/rag/*. Copy selected assets if present.
|
|
44
|
+
const projectRoot = path.resolve('.');
|
|
45
|
+
const ragSrcDir = path.join(projectRoot, 'src', 'rag');
|
|
46
|
+
const ragDistDir = path.join(distRoot, 'rag');
|
|
47
|
+
try {
|
|
48
|
+
await mkdir(ragDistDir, { recursive: true });
|
|
49
|
+
const assets = ['sources.csv', 'discover.config.json'];
|
|
50
|
+
for (const file of assets) {
|
|
51
|
+
const srcPath = path.join(ragSrcDir, file);
|
|
52
|
+
const dstPath = path.join(ragDistDir, file);
|
|
53
|
+
try {
|
|
54
|
+
// Best-effort copy; skip if source asset does not exist.
|
|
55
|
+
await copyFile(srcPath, dstPath);
|
|
56
|
+
} catch {}
|
|
57
|
+
}
|
|
58
|
+
} catch {}
|
|
59
|
+
|
|
60
|
+
// Ensure compiled generated client is available at runtime import path (../generated/*)
|
|
61
|
+
await syncGeneratedRuntimeArtifacts();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
main().catch((error) => {
|
|
65
|
+
console.error('[postbuild] Failed to normalize dist layout:', error);
|
|
66
|
+
process.exitCode = 1;
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
async function syncGeneratedRuntimeArtifacts() {
|
|
70
|
+
const distGenerated = path.join(distRoot, 'generated');
|
|
71
|
+
try {
|
|
72
|
+
const stats = await stat(distGenerated);
|
|
73
|
+
if (!stats.isDirectory()) return;
|
|
74
|
+
} catch (err) {
|
|
75
|
+
if (err && err.code === 'ENOENT') return;
|
|
76
|
+
throw err;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const projectRoot = path.resolve('.');
|
|
80
|
+
const runtimeGenerated = path.join(projectRoot, 'generated');
|
|
81
|
+
|
|
82
|
+
await copyGenerated(distGenerated, runtimeGenerated);
|
|
83
|
+
// Remove the dist/generated copy to avoid duplicate payload in packages
|
|
84
|
+
await rm(distGenerated, { recursive: true, force: true });
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function copyGenerated(srcDir, dstDir) {
|
|
88
|
+
await mkdir(dstDir, { recursive: true });
|
|
89
|
+
const entries = await readdir(srcDir, { withFileTypes: true });
|
|
90
|
+
for (const entry of entries) {
|
|
91
|
+
const from = path.join(srcDir, entry.name);
|
|
92
|
+
const to = path.join(dstDir, entry.name);
|
|
93
|
+
if (entry.isDirectory()) {
|
|
94
|
+
await copyGenerated(from, to);
|
|
95
|
+
} else if (entry.isFile()) {
|
|
96
|
+
// Preserve timestamps to avoid unnecessary rebuild churn; overwrite existing files.
|
|
97
|
+
await mkdir(path.dirname(to), { recursive: true });
|
|
98
|
+
await copyFile(from, to);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { execSync } from 'node:child_process';
|
|
3
|
+
import { readFile, writeFile } from 'node:fs/promises';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import process from 'node:process';
|
|
6
|
+
|
|
7
|
+
const run = (command, options = {}) => {
|
|
8
|
+
execSync(command, { stdio: 'inherit', ...options });
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const updateJsonFile = async (relativePath, updater) => {
|
|
12
|
+
const filePath = path.resolve(relativePath);
|
|
13
|
+
const raw = await readFile(filePath, 'utf8');
|
|
14
|
+
const data = JSON.parse(raw);
|
|
15
|
+
const updated = await updater(data);
|
|
16
|
+
await writeFile(filePath, JSON.stringify(updated, null, 2) + '\n');
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const usage = `Usage: npm run release:prepare -- <new-version|major|minor|patch>
|
|
20
|
+
|
|
21
|
+
Examples:
|
|
22
|
+
npm run release:prepare -- 0.2.0
|
|
23
|
+
npm run release:prepare -- minor`;
|
|
24
|
+
|
|
25
|
+
const arg = process.argv[2];
|
|
26
|
+
if (!arg) {
|
|
27
|
+
console.error('Error: missing version argument.');
|
|
28
|
+
console.error(usage);
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
run(`npm version ${arg} --no-git-tag-version`);
|
|
34
|
+
} catch (error) {
|
|
35
|
+
console.error('npm version failed.');
|
|
36
|
+
process.exit(error.status || 1);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const pkgRaw = await readFile(path.resolve('package.json'), 'utf8');
|
|
40
|
+
const pkg = JSON.parse(pkgRaw);
|
|
41
|
+
const newVersion = pkg.version;
|
|
42
|
+
|
|
43
|
+
await updateJsonFile('mcp.json', async (data) => ({
|
|
44
|
+
...data,
|
|
45
|
+
version: newVersion,
|
|
46
|
+
}));
|
|
47
|
+
|
|
48
|
+
// Manifest generation removed (runtime discovery via MCP stdio)
|
|
49
|
+
|
|
50
|
+
// Update CHANGELOG.md from commits since last tag using Conventional Commits subjects.
|
|
51
|
+
try {
|
|
52
|
+
run(`node scripts/generate-changelog.mjs ${newVersion}`);
|
|
53
|
+
} catch (e) {
|
|
54
|
+
console.warn('WARN: Failed to generate CHANGELOG.md. You can run it manually: npm run changelog:generate');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
console.log(`Release metadata updated to ${newVersion}.`);
|
|
58
|
+
console.log('Next steps: commit the changes, open a PR, and tag the merged commit.');
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
4
|
+
import { stat } from 'node:fs/promises';
|
|
5
|
+
|
|
6
|
+
async function fileExists(p) {
|
|
7
|
+
try {
|
|
8
|
+
const s = await stat(p);
|
|
9
|
+
return s.isFile();
|
|
10
|
+
} catch {
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async function main() {
|
|
16
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
17
|
+
const projectRoot = path.resolve(here, '..');
|
|
18
|
+
const distEmbeddings = path.join(projectRoot, 'dist', 'rag', 'embeddings.js');
|
|
19
|
+
const distIndexer = path.join(projectRoot, 'dist', 'rag', 'indexer.js');
|
|
20
|
+
|
|
21
|
+
let LocalMiniHashEmbedding;
|
|
22
|
+
let buildAllIndexes;
|
|
23
|
+
|
|
24
|
+
if (await fileExists(distEmbeddings) && await fileExists(distIndexer)) {
|
|
25
|
+
const em = await import(pathToFileURL(distEmbeddings).href);
|
|
26
|
+
const ix = await import(pathToFileURL(distIndexer).href);
|
|
27
|
+
LocalMiniHashEmbedding = em.LocalMiniHashEmbedding;
|
|
28
|
+
buildAllIndexes = ix.buildAllIndexes;
|
|
29
|
+
} else {
|
|
30
|
+
// Fall back to TypeScript sources via ts-node when available
|
|
31
|
+
try {
|
|
32
|
+
await import('./register-ts-node.mjs');
|
|
33
|
+
const em = await import(pathToFileURL(path.join(projectRoot, 'src', 'rag', 'embeddings.ts')).href);
|
|
34
|
+
const ix = await import(pathToFileURL(path.join(projectRoot, 'src', 'rag', 'indexer.ts')).href);
|
|
35
|
+
LocalMiniHashEmbedding = em.LocalMiniHashEmbedding;
|
|
36
|
+
buildAllIndexes = ix.buildAllIndexes;
|
|
37
|
+
} catch (err) {
|
|
38
|
+
console.error('[rag-rebuild] Unable to load RAG modules from dist or src:', err);
|
|
39
|
+
process.exit(1);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const dim = Number(process.env.RAG_EMBED_DIM ?? 384);
|
|
45
|
+
const model = new LocalMiniHashEmbedding(dim);
|
|
46
|
+
await buildAllIndexes({ model });
|
|
47
|
+
process.stdout.write('Rebuilt RAG embeddings.\n');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
main().catch((error) => {
|
|
51
|
+
console.error('Failed to rebuild embeddings', error);
|
|
52
|
+
process.exit(1);
|
|
53
|
+
});
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { register } from "node:module";
|
|
2
|
+
|
|
3
|
+
// Register ts-node ESM loader using the recommended `register` API to avoid
|
|
4
|
+
// the experimental loader warning shown during CI runs.
|
|
5
|
+
const projectRootUrl = new URL("../", import.meta.url);
|
|
6
|
+
register("ts-node/esm", projectRootUrl);
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
# Start a mock C64U server, point MCP at it, then launch MCP for a short time.
|
|
5
|
+
# Usage: scripts/run-mcp-check.sh [local|npm] [duration_seconds]
|
|
6
|
+
# - local: runs the local build (dist/index.js)
|
|
7
|
+
# - npm: runs the published package via npx (c64bridge)
|
|
8
|
+
|
|
9
|
+
MODE="${1:-local}"
|
|
10
|
+
DURATION="${2:-5}"
|
|
11
|
+
LOGFILE="${LOGFILE:-mcp-server-$(date +%Y%m%d-%H%M%S).log}"
|
|
12
|
+
|
|
13
|
+
ROOT_DIR=$(cd "$(dirname "$0")/.." && pwd)
|
|
14
|
+
TMPDIR=$(mktemp -d)
|
|
15
|
+
MOCK_INFO="$TMPDIR/mock-info.json"
|
|
16
|
+
CFG_FILE="$TMPDIR/.c64bridge.json"
|
|
17
|
+
|
|
18
|
+
cleanup() {
|
|
19
|
+
set +e
|
|
20
|
+
[[ -n "${MCP_PID:-}" ]] && kill -TERM "$MCP_PID" 2>/dev/null || true
|
|
21
|
+
[[ -n "${MOCK_PID:-}" ]] && kill -TERM "$MOCK_PID" 2>/dev/null || true
|
|
22
|
+
wait "$MCP_PID" 2>/dev/null || true
|
|
23
|
+
wait "$MOCK_PID" 2>/dev/null || true
|
|
24
|
+
rm -rf "$TMPDIR" 2>/dev/null || true
|
|
25
|
+
}
|
|
26
|
+
trap cleanup EXIT
|
|
27
|
+
|
|
28
|
+
# 1) Start mock server and wait for baseUrl
|
|
29
|
+
node "$ROOT_DIR/scripts/start-mock.mjs" "$MOCK_INFO" &
|
|
30
|
+
MOCK_PID=$!
|
|
31
|
+
|
|
32
|
+
for i in {1..50}; do
|
|
33
|
+
[[ -f "$MOCK_INFO" ]] && break
|
|
34
|
+
sleep 0.1
|
|
35
|
+
done
|
|
36
|
+
|
|
37
|
+
if [[ ! -f "$MOCK_INFO" ]]; then
|
|
38
|
+
echo "ERROR: mock info file was not created." >&2
|
|
39
|
+
exit 1
|
|
40
|
+
fi
|
|
41
|
+
|
|
42
|
+
BASE_URL=$(node -e "const fs=require('fs'); const p=process.argv[1]; const j=JSON.parse(fs.readFileSync(p,'utf8')); console.log(j.baseUrl || '');" "$MOCK_INFO")
|
|
43
|
+
if [[ -z "$BASE_URL" ]]; then
|
|
44
|
+
echo "ERROR: mock baseUrl not found." >&2
|
|
45
|
+
exit 1
|
|
46
|
+
fi
|
|
47
|
+
|
|
48
|
+
HOST_VALUE=$(node -e "const { URL } = require('url'); const u = new URL(process.argv[1]); console.log(u.hostname);" "$BASE_URL")
|
|
49
|
+
PORT_VALUE=$(node -e "const { URL } = require('url'); const u = new URL(process.argv[1]); console.log(u.port ? Number(u.port) : 80);" "$BASE_URL")
|
|
50
|
+
|
|
51
|
+
# 2) Write MCP config pointing to mock
|
|
52
|
+
cat >"$CFG_FILE" <<EOF
|
|
53
|
+
{
|
|
54
|
+
"c64u": {
|
|
55
|
+
"host": "$HOST_VALUE",
|
|
56
|
+
"port": $PORT_VALUE
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
EOF
|
|
60
|
+
export C64BRIDGE_CONFIG="$CFG_FILE"
|
|
61
|
+
|
|
62
|
+
# 3) If local mode, verify package contents (spot checks, no duplicates)
|
|
63
|
+
if [[ "$MODE" == "local" ]]; then
|
|
64
|
+
(cd "$ROOT_DIR" && node scripts/check-package.mjs)
|
|
65
|
+
fi
|
|
66
|
+
|
|
67
|
+
echo "==> Starting MCP server in '$MODE' mode for $DURATION seconds..."
|
|
68
|
+
echo "==> Logs will be written to: $(realpath "$LOGFILE")"
|
|
69
|
+
|
|
70
|
+
if [[ "$MODE" == "local" ]]; then
|
|
71
|
+
(cd "$ROOT_DIR" && npm run build)
|
|
72
|
+
VERSION=$(node -p "require('$ROOT_DIR/package.json').version")
|
|
73
|
+
echo "==> Running local build version $VERSION"
|
|
74
|
+
(cd "$ROOT_DIR" && node dist/index.js) 2>&1 | tee "$LOGFILE" &
|
|
75
|
+
MCP_PID=$!
|
|
76
|
+
elif [[ "$MODE" == "npm" ]]; then
|
|
77
|
+
VERSION=$(npm view c64bridge version --silent || echo "unknown")
|
|
78
|
+
echo "==> Downloading c64bridge@$VERSION from npm registry..."
|
|
79
|
+
C64BRIDGE_CONFIG="$CFG_FILE" npx --yes c64bridge 2>&1 | tee "$LOGFILE" &
|
|
80
|
+
MCP_PID=$!
|
|
81
|
+
else
|
|
82
|
+
echo "Usage: $0 [local|npm] [duration_seconds]" >&2
|
|
83
|
+
exit 1
|
|
84
|
+
fi
|
|
85
|
+
|
|
86
|
+
sleep "$DURATION"
|
|
87
|
+
|
|
88
|
+
# Request graceful shutdown and collect exit
|
|
89
|
+
if kill -TERM "$MCP_PID" 2>/dev/null; then
|
|
90
|
+
echo "==> Graceful shutdown requested."
|
|
91
|
+
else
|
|
92
|
+
echo "==> Server already stopped."
|
|
93
|
+
fi
|
|
94
|
+
wait "$MCP_PID" 2>/dev/null || true
|
|
95
|
+
|
|
96
|
+
# 4) Verify connectivity log
|
|
97
|
+
if grep -q "Connectivity check succeeded" "$LOGFILE"; then
|
|
98
|
+
echo "==> Connectivity to mock confirmed: $BASE_URL"
|
|
99
|
+
else
|
|
100
|
+
echo "ERROR: Connectivity check did not succeed. See logs: $LOGFILE" >&2
|
|
101
|
+
exit 1
|
|
102
|
+
fi
|
|
103
|
+
|
|
104
|
+
echo "==> Run complete. Logs saved to: $(realpath "$LOGFILE")"
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
7
|
+
|
|
8
|
+
const DEFAULT_TARGET = "mock";
|
|
9
|
+
|
|
10
|
+
let target = DEFAULT_TARGET;
|
|
11
|
+
let explicitBaseUrl = null;
|
|
12
|
+
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
13
|
+
const registerSpec = pathToFileURL(path.join(repoRoot, "scripts", "register-ts-node.mjs")).href;
|
|
14
|
+
const nodeArgs = ["--import", registerSpec, "--test"];
|
|
15
|
+
|
|
16
|
+
for (const arg of process.argv.slice(2)) {
|
|
17
|
+
if (arg === "--mock") {
|
|
18
|
+
target = "mock";
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (arg === "--real") {
|
|
23
|
+
target = "real";
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (arg.startsWith("--target=")) {
|
|
28
|
+
target = arg.split("=", 2)[1] ?? DEFAULT_TARGET;
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (arg.startsWith("--base-url=")) {
|
|
33
|
+
explicitBaseUrl = arg.split("=", 2)[1] ?? null;
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
nodeArgs.push(arg);
|
|
38
|
+
}
|
|
39
|
+
const defaultEmbeddingsDir = path.join(repoRoot, "artifacts", "test-embeddings");
|
|
40
|
+
if (!fs.existsSync(defaultEmbeddingsDir)) {
|
|
41
|
+
fs.mkdirSync(defaultEmbeddingsDir, { recursive: true });
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const env = {
|
|
45
|
+
...process.env,
|
|
46
|
+
C64_TEST_TARGET: target,
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
if (!env.RAG_EMBEDDINGS_DIR) {
|
|
50
|
+
env.RAG_EMBEDDINGS_DIR = defaultEmbeddingsDir;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (!env.NODE_TEST_DISABLE_WORKER_THREADS) {
|
|
54
|
+
env.NODE_TEST_DISABLE_WORKER_THREADS = "1";
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (explicitBaseUrl) {
|
|
58
|
+
env.C64_TEST_BASE_URL = explicitBaseUrl;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (target === "real" && !env.C64_TEST_BASE_URL) {
|
|
62
|
+
env.C64_TEST_BASE_URL = resolveBaseUrlFromConfig() ?? "http://c64u";
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const child = spawn(process.execPath, nodeArgs, {
|
|
66
|
+
stdio: "inherit",
|
|
67
|
+
env,
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
child.on("close", (code, signal) => {
|
|
71
|
+
if (signal) {
|
|
72
|
+
process.kill(process.pid, signal);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
process.exit(code ?? 1);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
function resolveBaseUrlFromConfig() {
|
|
79
|
+
const configPathEnv = process.env.C64BRIDGE_CONFIG;
|
|
80
|
+
const homeConfig = os.homedir() ? path.join(os.homedir(), ".c64bridge.json") : null;
|
|
81
|
+
const repoConfig = path.join(repoRoot, ".c64bridge.json");
|
|
82
|
+
|
|
83
|
+
const candidates = [];
|
|
84
|
+
if (configPathEnv) candidates.push(configPathEnv);
|
|
85
|
+
if (homeConfig) candidates.push(homeConfig);
|
|
86
|
+
candidates.push(repoConfig);
|
|
87
|
+
|
|
88
|
+
for (const candidate of candidates) {
|
|
89
|
+
if (!candidate) continue;
|
|
90
|
+
try {
|
|
91
|
+
const raw = JSON.parse(fs.readFileSync(candidate, "utf8"));
|
|
92
|
+
const baseUrl = resolveBaseUrlFromJson(raw);
|
|
93
|
+
if (baseUrl) return baseUrl;
|
|
94
|
+
} catch (error) {
|
|
95
|
+
if (error && typeof error === "object" && "code" in error) {
|
|
96
|
+
if (error.code === "ENOENT") continue;
|
|
97
|
+
}
|
|
98
|
+
// surface malformed JSON to aid debugging
|
|
99
|
+
console.warn(`[run-tests] Failed to read config at ${candidate}:`, error);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function resolveBaseUrlFromJson(raw) {
|
|
106
|
+
if (!raw || typeof raw !== "object") return null;
|
|
107
|
+
|
|
108
|
+
const c64u = raw.c64u && typeof raw.c64u === "object" ? raw.c64u : null;
|
|
109
|
+
if (c64u) {
|
|
110
|
+
const base = normaliseBaseUrlFromString(c64u.baseUrl);
|
|
111
|
+
if (base) return base;
|
|
112
|
+
|
|
113
|
+
const hostEntry = firstDefined(
|
|
114
|
+
stringIfSet(c64u.host),
|
|
115
|
+
stringIfSet(c64u.hostname),
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
const parsed = parseEndpoint(hostEntry);
|
|
119
|
+
const port = firstDefined(numberIfPort(c64u.port), parsed.port, DEFAULT_PORT);
|
|
120
|
+
if (parsed.hostname) {
|
|
121
|
+
return buildBaseUrl(parsed.hostname, port);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const legacyBase = normaliseBaseUrlFromString(raw.baseUrl);
|
|
126
|
+
if (legacyBase) return legacyBase;
|
|
127
|
+
|
|
128
|
+
const legacyHost = stringIfSet(raw.c64_host) ?? stringIfSet(raw.c64_ip);
|
|
129
|
+
if (legacyHost) {
|
|
130
|
+
const parsed = parseEndpoint(legacyHost);
|
|
131
|
+
const port = firstDefined(parsed.port, DEFAULT_PORT);
|
|
132
|
+
const hostname = parsed.hostname ?? legacyHost;
|
|
133
|
+
return buildBaseUrl(hostname, port);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function stringIfSet(value) {
|
|
140
|
+
return typeof value === "string" && value.trim() ? value.trim() : null;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function numberIfPort(value) {
|
|
144
|
+
if (typeof value === "number" && Number.isInteger(value) && value > 0 && value <= 65535) {
|
|
145
|
+
return value;
|
|
146
|
+
}
|
|
147
|
+
if (typeof value === "string" && value.trim()) {
|
|
148
|
+
const parsed = Number(value.trim());
|
|
149
|
+
if (Number.isInteger(parsed) && parsed > 0 && parsed <= 65535) {
|
|
150
|
+
return parsed;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function normaliseBaseUrlFromString(value) {
|
|
157
|
+
const input = stringIfSet(value);
|
|
158
|
+
if (!input) return null;
|
|
159
|
+
if (!/^[a-z][a-z0-9+.-]*:\/\//i.test(input)) {
|
|
160
|
+
return `http://${input}`;
|
|
161
|
+
}
|
|
162
|
+
return input.replace(/\/+$/, "");
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function parseEndpoint(value) {
|
|
166
|
+
const input = stringIfSet(value);
|
|
167
|
+
if (!input) return {};
|
|
168
|
+
try {
|
|
169
|
+
const hasScheme = /^[a-z][a-z0-9+.-]*:\/\//i.test(input);
|
|
170
|
+
const url = new URL(hasScheme ? input : `http://${input}`);
|
|
171
|
+
const hostname = url.hostname || undefined;
|
|
172
|
+
const port = url.port ? numberIfPort(url.port) : undefined;
|
|
173
|
+
return { hostname, port };
|
|
174
|
+
} catch {
|
|
175
|
+
return {};
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const DEFAULT_PORT = 80;
|
|
180
|
+
|
|
181
|
+
function buildBaseUrl(host, port) {
|
|
182
|
+
const normalizedPort = Number.isInteger(port) && port > 0 ? port : DEFAULT_PORT;
|
|
183
|
+
const hostPart = host.includes(":") && !host.startsWith("[") ? `[${host}]` : host;
|
|
184
|
+
const suffix = normalizedPort === DEFAULT_PORT ? "" : `:${normalizedPort}`;
|
|
185
|
+
return `http://${hostPart}${suffix}`;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function firstDefined(...values) {
|
|
189
|
+
for (const value of values) {
|
|
190
|
+
if (value !== undefined && value !== null) {
|
|
191
|
+
return value;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return undefined;
|
|
195
|
+
}
|