c64bridge 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (138) hide show
  1. package/.github/prompts/README.md +6 -0
  2. package/.github/prompts/assembly-routine-session.prompt.md +13 -0
  3. package/.github/prompts/basic-program-session.prompt.md +13 -0
  4. package/.github/prompts/drive-management-session.prompt.md +13 -0
  5. package/.github/prompts/petscii-art-session.prompt.md +13 -0
  6. package/.github/prompts/play-classic-game.prompt.md +13 -0
  7. package/.github/prompts/sid-music-session.prompt.md +13 -0
  8. package/AGENTS.md +173 -0
  9. package/LICENSE +339 -0
  10. package/README.md +582 -0
  11. package/data/README.md +16 -0
  12. package/data/api/basic-api-spec.md +154 -0
  13. package/data/api/kernal-api-spec.md +68 -0
  14. package/data/assembly/assembly-spec.md +196 -0
  15. package/data/assembly/examples/effects/sinewave.asm +5 -0
  16. package/data/assembly/examples/graphics/sprite/mover.asm +15 -0
  17. package/data/assembly/examples/sid-file-structure.md +8 -0
  18. package/data/assembly/examples/sid-overview.md +11 -0
  19. package/data/audio/examples/Biosphere.sid +0 -0
  20. package/data/audio/sid-file-structure.md +91 -0
  21. package/data/audio/sid-programming-best-practices.md +198 -0
  22. package/data/audio/sid-spec.md +166 -0
  23. package/data/audio/sidwave.md +131 -0
  24. package/data/audio/sidwave.schema.json +64 -0
  25. package/data/basic/basic-spec.md +230 -0
  26. package/data/basic/examples/audio/alle-meine-entchen-expressive.bas +41 -0
  27. package/data/basic/examples/audio/c-major-scale-pleasant.bas +29 -0
  28. package/data/basic/examples/io/joystick.bas +5 -0
  29. package/data/basic/examples/video/bounce.bas +8 -0
  30. package/data/basic/examples/video/entchen-petscii.bas +16 -0
  31. package/data/basic/examples/video/hello-world.bas +4 -0
  32. package/data/basic/examples/video/wave.bas +7 -0
  33. package/data/context/bootstrap.md +148 -0
  34. package/data/context/chat.md +36 -0
  35. package/data/drive/drive-spec.md +215 -0
  36. package/data/embeddings_asm.json +1 -0
  37. package/data/embeddings_basic.json +1 -0
  38. package/data/embeddings_hardware.json +1 -0
  39. package/data/embeddings_mixed.json +1 -0
  40. package/data/embeddings_other.json +1 -0
  41. package/data/io/cia-spec.md +115 -0
  42. package/data/io/io-spec.md +124 -0
  43. package/data/memory/kernal-memory-map.md +227 -0
  44. package/data/memory/low-memory-map.md +133 -0
  45. package/data/memory/memory-map.md +186 -0
  46. package/data/printer/printer-commodore-bitmap.md +31 -0
  47. package/data/printer/printer-commodore.md +28 -0
  48. package/data/printer/printer-epson-bitmap.md +29 -0
  49. package/data/printer/printer-epson.md +30 -0
  50. package/data/printer/printer-prompts.md +29 -0
  51. package/data/printer/printer-spec.md +180 -0
  52. package/data/video/character-set.csv +257 -0
  53. package/data/video/vic-spec.md +276 -0
  54. package/dist/assemblyConverter.js +1299 -0
  55. package/dist/audio/record_and_analyze_audio.js +292 -0
  56. package/dist/basicConverter.js +193 -0
  57. package/dist/bootstrap/stdio-logger.js +32 -0
  58. package/dist/c64Client.js +1052 -0
  59. package/dist/chargen.js +197 -0
  60. package/dist/config.js +142 -0
  61. package/dist/context.js +157 -0
  62. package/dist/device.js +413 -0
  63. package/dist/index.js +7 -0
  64. package/dist/knowledge.js +454 -0
  65. package/dist/logger.js +182 -0
  66. package/dist/loggingHttpClient.js +88 -0
  67. package/dist/mcp-server.js +426 -0
  68. package/dist/petscii.js +135 -0
  69. package/dist/petsciiArt.js +356 -0
  70. package/dist/platform.js +82 -0
  71. package/dist/prompts/registry.js +560 -0
  72. package/dist/rag/discover.config.json +41 -0
  73. package/dist/rag/discover.js +401 -0
  74. package/dist/rag/embeddings.js +60 -0
  75. package/dist/rag/externalFetcher.js +934 -0
  76. package/dist/rag/indexer.js +770 -0
  77. package/dist/rag/init.js +139 -0
  78. package/dist/rag/knowledgeIndex.js +361 -0
  79. package/dist/rag/loggingRetriever.js +46 -0
  80. package/dist/rag/rateLimiter.js +127 -0
  81. package/dist/rag/retriever.js +74 -0
  82. package/dist/rag/sources.csv +31 -0
  83. package/dist/rag/types.js +5 -0
  84. package/dist/rag/urlUtils.js +38 -0
  85. package/dist/sidplayRunner.js +240 -0
  86. package/dist/sidwave.js +101 -0
  87. package/dist/sidwaveCompiler.js +329 -0
  88. package/dist/tools/audio.js +944 -0
  89. package/dist/tools/developer.js +515 -0
  90. package/dist/tools/errors.js +59 -0
  91. package/dist/tools/graphics.js +469 -0
  92. package/dist/tools/machineControl.js +267 -0
  93. package/dist/tools/memory.js +224 -0
  94. package/dist/tools/printer.js +482 -0
  95. package/dist/tools/programRunners.js +315 -0
  96. package/dist/tools/rag.js +153 -0
  97. package/dist/tools/registry.js +58 -0
  98. package/dist/tools/responses.js +36 -0
  99. package/dist/tools/schema.js +304 -0
  100. package/dist/tools/storage.js +740 -0
  101. package/dist/tools/streaming.js +132 -0
  102. package/dist/tools/types.js +99 -0
  103. package/dist/viceRunner.js +239 -0
  104. package/doc/MCP_SETUP.md +153 -0
  105. package/doc/README.md +7 -0
  106. package/doc/developer.md +312 -0
  107. package/doc/img/logo.png +0 -0
  108. package/doc/img/prompts/duck_petscii.png +0 -0
  109. package/doc/img/prompts/duck_song.png +0 -0
  110. package/doc/img/vscode/vscode-copilot-c64-chat-mode.png +0 -0
  111. package/doc/img/vscode/vscode-copilot-hello-world.png +0 -0
  112. package/doc/prompts/README.md +96 -0
  113. package/doc/prompts/user-prompt-ideas.md +147 -0
  114. package/doc/rest/c64-openapi.yaml +979 -0
  115. package/doc/rest/c64-rest-api.md +106 -0
  116. package/doc/troubleshooting-mcp.md +177 -0
  117. package/generated/c64/index.js +857 -0
  118. package/mcp.json +106 -0
  119. package/package.json +79 -0
  120. package/scripts/c64-cli.mjs +142 -0
  121. package/scripts/c64-tool.sh +83 -0
  122. package/scripts/check-package.mjs +89 -0
  123. package/scripts/cli.js +61 -0
  124. package/scripts/fetch-external-sources.mjs +67 -0
  125. package/scripts/generate-changelog.mjs +131 -0
  126. package/scripts/postbuild.mjs +101 -0
  127. package/scripts/prepare-release.mjs +58 -0
  128. package/scripts/rag-rebuild.mjs +53 -0
  129. package/scripts/register-ts-node.mjs +6 -0
  130. package/scripts/run-mcp-check.sh +104 -0
  131. package/scripts/run-tests.mjs +195 -0
  132. package/scripts/sid2wav.sh +94 -0
  133. package/scripts/smoke-run.sh +65 -0
  134. package/scripts/start-mock-c64.mjs +51 -0
  135. package/scripts/start-mock.mjs +31 -0
  136. package/scripts/start.mjs +72 -0
  137. package/scripts/update-readme.ts +164 -0
  138. package/scripts/verify-package.mjs +147 -0
@@ -0,0 +1,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
+ }