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,94 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ SID="${1:?Usage: sid2wav.sh <file.sid> [out.wav] [ntsc|pal] [limitCycles]}"
5
+ OUT="${2:-${SID%.sid}.wav}"
6
+ MODE="${3:-ntsc}"
7
+ CYCLES="${4:-120000000}"
8
+
9
+ case "${MODE}" in
10
+ pal|ntsc) ;;
11
+ *)
12
+ echo "Unsupported mode: ${MODE} (expected pal or ntsc)" >&2
13
+ exit 1
14
+ ;;
15
+ esac
16
+
17
+ PAL_CLOCK=985248
18
+ NTSC_CLOCK=1022727
19
+
20
+ if [[ "${CYCLES}" -le 0 ]]; then
21
+ echo "limitCycles must be positive" >&2
22
+ exit 1
23
+ fi
24
+
25
+ if [[ ! -f "${SID}" ]]; then
26
+ echo "SID file not found: ${SID}" >&2
27
+ exit 1
28
+ fi
29
+
30
+ if ! command -v sidplayfp >/dev/null 2>&1; then
31
+ echo "sidplayfp not found; attempting automatic installation" >&2
32
+ if command -v apt-get >/dev/null 2>&1; then
33
+ if command -v sudo >/dev/null 2>&1; then
34
+ sudo -n apt-get update -y || true
35
+ sudo -n apt-get install -y sidplayfp || true
36
+ else
37
+ apt-get update -y || true
38
+ apt-get install -y sidplayfp || true
39
+ fi
40
+ elif command -v brew >/dev/null 2>&1; then
41
+ brew install sidplayfp || true
42
+ elif command -v dnf >/dev/null 2>&1; then
43
+ if command -v sudo >/dev/null 2>&1; then
44
+ sudo -n dnf install -y sidplayfp || true
45
+ else
46
+ dnf install -y sidplayfp || true
47
+ fi
48
+ elif command -v pacman >/dev/null 2>&1; then
49
+ if command -v sudo >/dev/null 2>&1; then
50
+ sudo -n pacman -Sy --noconfirm sidplayfp || true
51
+ else
52
+ pacman -Sy --noconfirm sidplayfp || true
53
+ fi
54
+ elif command -v choco >/dev/null 2>&1; then
55
+ choco install sidplayfp -y || true
56
+ elif command -v winget >/dev/null 2>&1; then
57
+ winget install --id=SIDTools.sidplayfp -e --source winget || true
58
+ else
59
+ echo "Unable to auto-install sidplayfp; please install it manually." >&2
60
+ exit 127
61
+ fi
62
+
63
+ if ! command -v sidplayfp >/dev/null 2>&1; then
64
+ echo "sidplayfp is still missing after installation attempt." >&2
65
+ exit 127
66
+ fi
67
+ fi
68
+
69
+ clock=${PAL_CLOCK}
70
+ sidplay_opts=( )
71
+
72
+ if [[ "${MODE}" == "ntsc" ]]; then
73
+ clock=${NTSC_CLOCK}
74
+ sidplay_opts+=(-vn) # NTSC clock
75
+ else
76
+ sidplay_opts+=(-vp) # PAL clock
77
+ fi
78
+
79
+ # Convert cycle limit to playback duration in seconds (round up) using Node.js
80
+ duration_seconds=$(node -e 'const cycles=Number(process.argv[1]);const clock=Number(process.argv[2]);if(!Number.isFinite(cycles)||!Number.isFinite(clock)||cycles<=0||clock<=0){process.exit(1);}const seconds=Math.max(1, Math.ceil(cycles/clock));process.stdout.write(String(seconds));' "${CYCLES}" "${clock}")
81
+
82
+ if [[ -z "${duration_seconds}" ]]; then
83
+ echo "Failed to compute playback duration from cycle limit." >&2
84
+ exit 1
85
+ fi
86
+
87
+ sidplay_opts+=(
88
+ -f44100 # 44.1 kHz sample rate
89
+ -p16 # 16-bit signed PCM
90
+ -t"${duration_seconds}" # playback duration in seconds
91
+ -w"${OUT}" # output WAV path
92
+ )
93
+
94
+ exec sidplayfp "${sidplay_opts[@]}" "${SID}"
@@ -0,0 +1,65 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ MODE=${1:-local}
5
+ DURATION=${2:-5}
6
+ LOGFILE=${LOGFILE:-"mcp-server-$(date +%Y%m%d-%H%M%S).log"}
7
+ PACKAGE_DIR=${PACKAGE_DIR:-}
8
+
9
+ mkdir -p "$(dirname "$LOGFILE")"
10
+ : >"$LOGFILE"
11
+ LOGFILE=$(realpath "$LOGFILE")
12
+
13
+ echo "==> Starting MCP server in '$MODE' mode for ${DURATION}s"
14
+ echo "==> Logs: $LOGFILE"
15
+
16
+ STATUS=0
17
+
18
+ set +e
19
+ case "$MODE" in
20
+ local)
21
+ if [[ -z "$PACKAGE_DIR" ]]; then
22
+ echo "PACKAGE_DIR environment variable must be set for local mode" >&2
23
+ exit 1
24
+ fi
25
+ if [[ ! -d "$PACKAGE_DIR" ]]; then
26
+ echo "PACKAGE_DIR '$PACKAGE_DIR' does not exist" >&2
27
+ exit 1
28
+ fi
29
+ (
30
+ cd "$PACKAGE_DIR" || exit 1
31
+ timeout --signal=TERM "$DURATION" node dist/index.js
32
+ ) 2>&1 | tee "$LOGFILE"
33
+ STATUS=${PIPESTATUS[0]}
34
+ ;;
35
+ npm)
36
+ if [[ -z "$PACKAGE_DIR" ]]; then
37
+ echo "PACKAGE_DIR environment variable must be set for npm mode" >&2
38
+ exit 1
39
+ fi
40
+ if [[ ! -d "$PACKAGE_DIR" ]]; then
41
+ echo "PACKAGE_DIR '$PACKAGE_DIR' does not exist" >&2
42
+ exit 1
43
+ fi
44
+ (
45
+ cd "$PACKAGE_DIR" || exit 1
46
+ timeout --signal=TERM "$DURATION" node dist/index.js
47
+ ) 2>&1 | tee "$LOGFILE"
48
+ STATUS=${PIPESTATUS[0]}
49
+ ;;
50
+ *)
51
+ echo "Usage: $0 [local|npm] [duration_seconds]" >&2
52
+ exit 1
53
+ ;;
54
+ esac
55
+ set -e
56
+
57
+ if [[ $STATUS -eq 124 ]]; then
58
+ echo "==> Timeout reached after ${DURATION}s (expected)."
59
+ elif [[ $STATUS -ne 0 ]]; then
60
+ echo "==> MCP server exited with status $STATUS"
61
+ exit $STATUS
62
+ fi
63
+
64
+ echo "==> Run complete"
65
+ echo "==> Logs stored at: $LOGFILE"
@@ -0,0 +1,51 @@
1
+ #!/usr/bin/env node
2
+ // CI helper: writes the mock C64 base URL (plain text) for workflow callers and blocks until killed.
3
+ import { writeFileSync } from 'node:fs';
4
+ import process from 'node:process';
5
+ import path from 'node:path';
6
+
7
+ async function main() {
8
+ const outputArg = process.argv[2];
9
+ const resolvePath = (maybePath) => {
10
+ if (!maybePath) {
11
+ return null;
12
+ }
13
+ if (path.isAbsolute(maybePath)) {
14
+ return maybePath;
15
+ }
16
+ return path.resolve(process.cwd(), maybePath);
17
+ };
18
+ const outputFile = resolvePath(outputArg);
19
+
20
+ const moduleUrl = new URL('../test/mockC64Server.mjs', import.meta.url);
21
+ const { startMockC64Server } = await import(moduleUrl.href);
22
+ const server = await startMockC64Server();
23
+ const baseUrl = server.baseUrl;
24
+ console.log(`[mock-c64] listening at ${baseUrl}`);
25
+ if (outputFile) {
26
+ writeFileSync(outputFile, baseUrl, 'utf8');
27
+ }
28
+
29
+ const shutdown = async (signal) => {
30
+ console.log(`[mock-c64] received ${signal ?? 'signal'}, shutting down`);
31
+ try {
32
+ await server.close();
33
+ } finally {
34
+ process.exit(0);
35
+ }
36
+ };
37
+
38
+ process.on('SIGTERM', () => {
39
+ void shutdown('SIGTERM');
40
+ });
41
+ process.on('SIGINT', () => {
42
+ void shutdown('SIGINT');
43
+ });
44
+
45
+ await new Promise(() => {});
46
+ }
47
+
48
+ main().catch((error) => {
49
+ console.error('[mock-c64] failed to start mock server:', error);
50
+ process.exit(1);
51
+ });
@@ -0,0 +1,31 @@
1
+ #!/usr/bin/env node
2
+ // Local smoke-test helper: writes mock server info JSON for run-mcp-check.sh and keeps it alive.
3
+ import { writeFile } from 'node:fs/promises';
4
+ import path from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+
7
+ // Reuse the existing mock server implementation from tests
8
+ const here = path.dirname(fileURLToPath(import.meta.url));
9
+ const root = path.resolve(here, '..');
10
+
11
+ const { startMockC64Server } = await import(path.join(root, 'test', 'mockC64Server.mjs'));
12
+
13
+ const outPath = process.argv[2];
14
+ if (!outPath) {
15
+ console.error('Usage: start-mock.mjs <out-json-path>');
16
+ process.exit(1);
17
+ }
18
+
19
+ const mock = await startMockC64Server();
20
+ await writeFile(outPath, JSON.stringify({ baseUrl: mock.baseUrl }, null, 2) + '\n', 'utf8');
21
+
22
+ // Keep process alive until terminated; forward signals to close
23
+ const onExit = async () => {
24
+ try { await mock.close(); } catch {}
25
+ process.exit(0);
26
+ };
27
+ process.on('SIGTERM', onExit);
28
+ process.on('SIGINT', onExit);
29
+
30
+ // Idle forever
31
+ setInterval(() => {}, 1 << 30).unref();
@@ -0,0 +1,72 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Flexible launcher for the MCP server.
4
+ *
5
+ * - Prefer running the TypeScript sources via ts-node when they are available
6
+ * (local development workflow).
7
+ * - Fall back to the compiled JavaScript in dist/ when the package is
8
+ * consumed from npm, where the sources and dev dependencies are omitted.
9
+ */
10
+ import { fileURLToPath, pathToFileURL } from 'node:url';
11
+ import path from 'node:path';
12
+ import { stat } from 'node:fs/promises';
13
+
14
+ const here = path.dirname(fileURLToPath(import.meta.url));
15
+ const projectRoot = path.resolve(here, '..');
16
+
17
+ async function fileExists(filePath) {
18
+ try {
19
+ const stats = await stat(filePath);
20
+ return stats.isFile();
21
+ } catch (error) {
22
+ const err = error;
23
+ if (err && typeof err === 'object' && (err.code === 'ENOENT' || err.code === 'ENOTDIR')) {
24
+ return false;
25
+ }
26
+ throw error;
27
+ }
28
+ }
29
+
30
+ function isModuleNotFound(error) {
31
+ if (!error || typeof error !== 'object') {
32
+ return false;
33
+ }
34
+ const err = error;
35
+ if (err.code === 'ERR_MODULE_NOT_FOUND') {
36
+ return true;
37
+ }
38
+ const message = typeof err.message === 'string' ? err.message : '';
39
+ return message.includes('Cannot find module');
40
+ }
41
+
42
+ async function launch() {
43
+ const srcEntry = path.resolve(projectRoot, 'src/index.ts');
44
+ const distEntry = path.resolve(projectRoot, 'dist/index.js');
45
+
46
+ if (await fileExists(srcEntry)) {
47
+ try {
48
+ await import('./register-ts-node.mjs');
49
+ await import(pathToFileURL(srcEntry).href);
50
+ return;
51
+ } catch (error) {
52
+ if (isModuleNotFound(error)) {
53
+ console.warn('[start] ts-node not available; falling back to compiled output.');
54
+ } else {
55
+ console.error('[start] Failed to launch TypeScript sources:', error);
56
+ process.exitCode = 1;
57
+ return;
58
+ }
59
+ }
60
+ }
61
+
62
+ if (await fileExists(distEntry)) {
63
+ await import(pathToFileURL(distEntry).href);
64
+ return;
65
+ }
66
+
67
+ console.error('[start] Unable to locate server entry point: dist/index.js or src/index.ts.');
68
+ console.error('[start] Build the project with `npm run build` or install dev dependencies for ts-node.');
69
+ process.exitCode = 1;
70
+ }
71
+
72
+ await launch();
@@ -0,0 +1,164 @@
1
+ #!/usr/bin/env node
2
+ import { readFile, writeFile } from "node:fs/promises";
3
+ import { dirname, join } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { describeToolModules } from "../src/tools/registry.js";
6
+ import { listKnowledgeResources } from "../src/rag/knowledgeIndex.js";
7
+ import { createPromptRegistry } from "../src/prompts/registry.js";
8
+
9
+ const START_MARKER = "<!-- AUTO-GENERATED:MCP-DOCS-START -->";
10
+ const END_MARKER = "<!-- AUTO-GENERATED:MCP-DOCS-END -->";
11
+
12
+ const __filename = fileURLToPath(import.meta.url);
13
+ const __dirname = dirname(__filename);
14
+ const PROJECT_ROOT = join(__dirname, "..");
15
+ const README_PATH = join(PROJECT_ROOT, "README.md");
16
+
17
+ function titleCase(input: string): string {
18
+ return input
19
+ .split(/[\s_-]+/)
20
+ .filter(Boolean)
21
+ .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
22
+ .join(" ");
23
+ }
24
+
25
+ function escapeCell(value: string): string {
26
+ return value.replace(/\|/g, "\\|").replace(/\r?\n|\r/g, " ").trim();
27
+ }
28
+
29
+ function formatTags(tags: readonly string[]): string {
30
+ if (!tags.length) {
31
+ return "—";
32
+ }
33
+ return tags.map((tag) => `\`${tag}\``).join(", ");
34
+ }
35
+
36
+ function renderTable(headers: readonly string[], rows: readonly (readonly string[])[]): string {
37
+ const headerLine = `| ${headers.join(" | ")} |`;
38
+ const separator = `| ${headers.map(() => "---").join(" | ")} |`;
39
+ const body = rows.map((row) => `| ${row.join(" | ")} |`).join("\n");
40
+ return [headerLine, separator, body].filter(Boolean).join("\n");
41
+ }
42
+
43
+ function renderToolsSection(): string[] {
44
+ const modules = describeToolModules();
45
+ const lines: string[] = ["### Tools", ""];
46
+
47
+ for (const module of modules) {
48
+ lines.push(`#### ${titleCase(module.domain)}`);
49
+ if (module.summary) {
50
+ lines.push(`> ${module.summary}`);
51
+ }
52
+ if (module.workflowHints.length) {
53
+ lines.push("");
54
+ lines.push("**Workflow hints:**");
55
+ for (const hint of module.workflowHints) {
56
+ lines.push(`- ${hint}`);
57
+ }
58
+ }
59
+ if (module.defaultTags.length) {
60
+ lines.push("");
61
+ lines.push(`**Default tags:** ${module.defaultTags.map((tag) => `\`${tag}\``).join(", ")}`);
62
+ }
63
+
64
+ const rows = module.tools
65
+ .slice()
66
+ .sort((a, b) => a.name.localeCompare(b.name))
67
+ .map((tool) => [
68
+ `\`${tool.name}\``,
69
+ escapeCell(tool.description),
70
+ formatTags(tool.metadata.tags),
71
+ ]);
72
+
73
+ lines.push("");
74
+ if (rows.length) {
75
+ lines.push(renderTable(["Name", "Description", "Tags"], rows));
76
+ } else {
77
+ lines.push("_No tools registered._");
78
+ }
79
+ lines.push("");
80
+ }
81
+
82
+ return lines;
83
+ }
84
+
85
+ function renderResourcesSection(): string[] {
86
+ const resources = listKnowledgeResources()
87
+ .slice()
88
+ .sort((a, b) => {
89
+ const bundleOrder = a.metadata.bundle.order - b.metadata.bundle.order;
90
+ if (bundleOrder !== 0) {
91
+ return bundleOrder;
92
+ }
93
+ return a.metadata.order - b.metadata.order;
94
+ });
95
+
96
+ const rows = resources.map((resource) => [
97
+ `\`${resource.uri}\``,
98
+ escapeCell(resource.metadata.summary || resource.description),
99
+ ]);
100
+
101
+ return [
102
+ "### Resources",
103
+ "",
104
+ rows.length ? renderTable(["Name", "Summary"], rows) : "_No resources registered._",
105
+ "",
106
+ ];
107
+ }
108
+
109
+ function renderPromptsSection(): string[] {
110
+ const promptRegistry = createPromptRegistry();
111
+ const prompts = promptRegistry
112
+ .list()
113
+ .slice()
114
+ .sort((a, b) => a.descriptor.name.localeCompare(b.descriptor.name));
115
+
116
+ const rows = prompts.map((entry) => [
117
+ `\`${entry.descriptor.name}\``,
118
+ escapeCell(entry.descriptor.description),
119
+ ]);
120
+
121
+ return [
122
+ "### Prompts",
123
+ "",
124
+ rows.length ? renderTable(["Name", "Description"], rows) : "_No prompts registered._",
125
+ "",
126
+ ];
127
+ }
128
+
129
+ function buildDocumentation(): string {
130
+ const sections = [renderToolsSection(), renderResourcesSection(), renderPromptsSection()];
131
+ return sections.flat().join("\n").trim();
132
+ }
133
+
134
+ async function updateReadme(): Promise<boolean> {
135
+ const readme = await readFile(README_PATH, "utf8");
136
+ const pattern = new RegExp(
137
+ `${START_MARKER.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}([\\s\\S]*?)${END_MARKER.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}`,
138
+ );
139
+ if (!pattern.test(readme)) {
140
+ throw new Error(
141
+ `Could not find auto-generated section markers (${START_MARKER} / ${END_MARKER}) in README.md`,
142
+ );
143
+ }
144
+
145
+ const generated = `\n\n${buildDocumentation()}\n\n`;
146
+ const nextReadme = readme.replace(pattern, `${START_MARKER}${generated}${END_MARKER}`);
147
+
148
+ if (nextReadme === readme) {
149
+ return false;
150
+ }
151
+
152
+ await writeFile(README_PATH, nextReadme, "utf8");
153
+ return true;
154
+ }
155
+
156
+ try {
157
+ const updated = await updateReadme();
158
+ if (updated) {
159
+ console.error("README.md updated with MCP documentation.");
160
+ }
161
+ } catch (error) {
162
+ console.error("Failed to update README.md:", error);
163
+ process.exitCode = 1;
164
+ }
@@ -0,0 +1,147 @@
1
+ #!/usr/bin/env node
2
+ import { readdirSync, readFileSync, statSync } from 'node:fs';
3
+ import { resolve, relative } from 'node:path';
4
+ import crypto from 'node:crypto';
5
+
6
+ function usage() {
7
+ console.error('Usage: node scripts/verify-package.mjs <package-dir>');
8
+ process.exit(1);
9
+ }
10
+
11
+ const dirArg = process.argv[2];
12
+ if (!dirArg) {
13
+ usage();
14
+ }
15
+
16
+ const packageDir = resolve(process.cwd(), dirArg);
17
+
18
+ function ensureExists(relPath, type = 'file') {
19
+ const fullPath = resolve(packageDir, relPath);
20
+ try {
21
+ const stats = statSync(fullPath);
22
+ if (type === 'file' && !stats.isFile()) {
23
+ console.error(`Expected file at ${relPath}, found other type`);
24
+ process.exit(1);
25
+ }
26
+ if (type === 'dir' && !stats.isDirectory()) {
27
+ console.error(`Expected directory at ${relPath}, found other type`);
28
+ process.exit(1);
29
+ }
30
+ } catch (error) {
31
+ console.error(`Missing required ${type} at ${relPath}:`, error instanceof Error ? error.message : String(error));
32
+ process.exit(1);
33
+ }
34
+ }
35
+
36
+ function assertSpotChecks() {
37
+ const files = [
38
+ 'package.json',
39
+ 'mcp.json',
40
+ 'README.md',
41
+ 'LICENSE',
42
+ 'dist/index.js',
43
+ 'dist/c64Client.js',
44
+ 'dist/rag/sources.csv',
45
+ 'dist/rag/discover.config.json',
46
+ 'generated/c64/index.js',
47
+ 'scripts/cli.js',
48
+ 'scripts/start.mjs',
49
+ 'scripts/register-ts-node.mjs',
50
+ ];
51
+ const dirs = [
52
+ 'doc',
53
+ 'data',
54
+ 'dist/rag',
55
+ ];
56
+
57
+ files.forEach((file) => ensureExists(file, 'file'));
58
+ dirs.forEach((dir) => ensureExists(dir, 'dir'));
59
+
60
+ const embeddings = readdirSync(resolve(packageDir, 'data')).filter((name) => name.startsWith('embeddings_') && name.endsWith('.json'));
61
+ if (embeddings.length === 0) {
62
+ console.error('Expected embeddings JSON files under data/, none found');
63
+ process.exit(1);
64
+ }
65
+ }
66
+
67
+ function detectDuplicates() {
68
+ const hashes = new Map();
69
+ const duplicates = [];
70
+
71
+ const walk = (dir) => {
72
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
73
+ const fullPath = resolve(dir, entry.name);
74
+ if (entry.isDirectory()) {
75
+ walk(fullPath);
76
+ } else if (entry.isFile()) {
77
+ const rel = relative(packageDir, fullPath);
78
+ const data = readFileSync(fullPath);
79
+ const hash = crypto.createHash('sha256').update(data).digest('hex');
80
+ if (hashes.has(hash)) {
81
+ duplicates.push([hashes.get(hash), rel]);
82
+ } else {
83
+ hashes.set(hash, rel);
84
+ }
85
+ }
86
+ }
87
+ };
88
+
89
+ walk(packageDir);
90
+
91
+ if (duplicates.length > 0) {
92
+ console.error('Duplicate files detected in package (identical content):');
93
+ for (const [first, dup] of duplicates) {
94
+ console.error(` - ${first} == ${dup}`);
95
+ }
96
+ process.exit(1);
97
+ }
98
+ }
99
+
100
+ function showSourcesCsvPreview() {
101
+ const csvPath = resolve(packageDir, 'dist/rag/sources.csv');
102
+ try {
103
+ const preview = readFileSync(csvPath, 'utf8').split('\n').slice(0, 5).join('\n');
104
+ console.log('Preview dist/rag/sources.csv:');
105
+ console.log(preview);
106
+ } catch (error) {
107
+ console.error('Failed to read dist/rag/sources.csv:', error instanceof Error ? error.message : String(error));
108
+ process.exit(1);
109
+ }
110
+ }
111
+
112
+ function logPackageContents() {
113
+ const items = [];
114
+ const stack = ['.'];
115
+ while (stack.length > 0) {
116
+ const current = stack.pop();
117
+ const fullPath = resolve(packageDir, current);
118
+ const stats = statSync(fullPath);
119
+ if (stats.isDirectory()) {
120
+ if (current !== '.') {
121
+ items.push(`${current}/`);
122
+ }
123
+ const children = readdirSync(fullPath)
124
+ .map((name) => (current === '.' ? name : `${current}/${name}`))
125
+ .sort()
126
+ .reverse();
127
+ for (const child of children) {
128
+ stack.push(child);
129
+ }
130
+ } else if (stats.isFile()) {
131
+ items.push(current);
132
+ }
133
+ }
134
+
135
+ items.sort((a, b) => a.localeCompare(b));
136
+ console.log('Package file listing:');
137
+ for (const entry of items) {
138
+ console.log(` - ${entry}`);
139
+ }
140
+ }
141
+
142
+ assertSpotChecks();
143
+ detectDuplicates();
144
+ showSourcesCsvPreview();
145
+ logPackageContents();
146
+
147
+ console.log('Package verification succeeded.');