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,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.');
|