@bitflower/va-mcp 1.0.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 (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +90 -0
  3. package/dist/index.js +584 -0
  4. package/package.json +45 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 bitflower
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,90 @@
1
+ # @bitflower/va-mcp
2
+
3
+ Install the **Virtual Architect** MCP server and its SessionStart playbook
4
+ into a Claude Code project, with a single command. The installer registers
5
+ the `virtual-architect` MCP server with your local `claude` CLI and drops a
6
+ SessionStart hook into the project so every Claude Code session in that
7
+ directory can call Virtual Architect tools and starts with a short usage
8
+ playbook in context. It runs entirely on your machine and makes no network
9
+ call to discover or download anything at install time.
10
+
11
+ ## Install
12
+
13
+ ```
14
+ npx @bitflower/va-mcp install \
15
+ --url https://va.example.com/mcp \
16
+ --api-key va_live_<your-key>
17
+ ```
18
+
19
+ Targets the current directory by default; pass `--project-dir <path>` to
20
+ target another project. Run `install` with no flags to be prompted
21
+ interactively (the API key is masked as you type).
22
+
23
+ For reproducible installs (CI, shared setup scripts), pin a version:
24
+
25
+ ```
26
+ npx @bitflower/va-mcp@0.1.0 install --url <url> --api-key <key>
27
+ ```
28
+
29
+ ## Uninstall
30
+
31
+ ```
32
+ npx @bitflower/va-mcp uninstall
33
+ ```
34
+
35
+ Deregisters the MCP server and removes the SessionStart hook entry. The
36
+ hook script file is left on disk for you to delete manually.
37
+
38
+ ## Help and version
39
+
40
+ ```
41
+ npx @bitflower/va-mcp --help
42
+ npx @bitflower/va-mcp --version
43
+ ```
44
+
45
+ ## Requirements
46
+
47
+ - The `claude` CLI on your `$PATH` — <https://docs.claude.com/en/docs/claude-code>
48
+ - Node.js 18 or newer
49
+ - A Virtual Architect deployment URL and API key
50
+
51
+ ## gitignore precondition
52
+
53
+ If the target directory is a git repository, add
54
+ `.claude/settings.local.json` to your `.gitignore` **before** running the
55
+ installer — it refuses otherwise, so per-machine hook configuration never
56
+ gets committed:
57
+
58
+ ```
59
+ grep -qxF '.claude/settings.local.json' .gitignore || echo '.claude/settings.local.json' >> .gitignore
60
+ ```
61
+
62
+ The check is deliberately conservative: it recognizes only an exact-path or
63
+ directory-prefix entry — glob patterns (e.g. `.claude/*`), `!`-negation lines,
64
+ and nested `.gitignore` files are **not** evaluated. See the
65
+ [full guide](../../documentation/mcp-installer-guide.md) for the exact matcher
66
+ semantics. The check is skipped for non-git directories. In a monorepo, put
67
+ the entry in the **project-local** `.gitignore` (the one in the target
68
+ directory), not the repo-root `.gitignore` — the installer reads only the
69
+ target directory's file.
70
+
71
+ ## No catalog lookup or remote calls at install time
72
+
73
+ The installer performs no catalog lookup or remote download. It talks only
74
+ to your local `claude` CLI and writes into the target project, so it works
75
+ without outbound network access (beyond `npx` fetching the package itself).
76
+
77
+ ## Notes
78
+
79
+ - **Idempotent:** re-running `install` with the same arguments produces the
80
+ same on-disk state and does not duplicate the hook entry.
81
+ - **Windows:** use WSL. A native Windows hook format is not part of this
82
+ release.
83
+ - **Bearer token:** your API key is passed only to the `claude` CLI and is
84
+ never written to any file the installer creates.
85
+
86
+ ## Full guide
87
+
88
+ See **[documentation/mcp-installer-guide.md](../../documentation/mcp-installer-guide.md)**
89
+ for prerequisites in depth, the data-flow and bearer-exposure warnings,
90
+ verification steps, key rotation, and LAN access.
package/dist/index.js ADDED
@@ -0,0 +1,584 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { realpathSync as realpathSync2 } from "fs";
5
+ import { pathToFileURL } from "url";
6
+ import { Command, CommanderError } from "commander";
7
+
8
+ // package.json
9
+ var package_default = {
10
+ name: "@bitflower/va-mcp",
11
+ version: "1.0.0",
12
+ description: "Install the Virtual Architect MCP server and SessionStart playbook into a Claude Code project.",
13
+ repository: {
14
+ type: "git",
15
+ url: "git+https://github.com/bitflower/pathrag-ui-virtual-architect.git",
16
+ directory: "clients/va-mcp"
17
+ },
18
+ type: "module",
19
+ bin: {
20
+ "va-mcp": "./dist/index.js"
21
+ },
22
+ files: [
23
+ "dist"
24
+ ],
25
+ engines: {
26
+ node: ">=18"
27
+ },
28
+ scripts: {
29
+ build: "tsup",
30
+ test: "vitest run",
31
+ "test:watch": "vitest",
32
+ typecheck: "tsc --noEmit",
33
+ prepublishOnly: "npm run build"
34
+ },
35
+ keywords: [
36
+ "mcp",
37
+ "claude-code",
38
+ "virtual-architect"
39
+ ],
40
+ license: "MIT",
41
+ publishConfig: {
42
+ access: "public"
43
+ },
44
+ dependencies: {
45
+ commander: "^12.1.0"
46
+ },
47
+ devDependencies: {
48
+ "@types/node": "^20.14.0",
49
+ tsup: "^8.3.0",
50
+ typescript: "^5.6.0",
51
+ vitest: "^2.1.0"
52
+ }
53
+ };
54
+
55
+ // src/errors.ts
56
+ var InstallerError = class extends Error {
57
+ constructor(message) {
58
+ super(message);
59
+ this.name = "InstallerError";
60
+ }
61
+ };
62
+
63
+ // src/install.ts
64
+ import * as fs5 from "fs";
65
+ import * as path5 from "path";
66
+
67
+ // src/claude-binary.ts
68
+ import { spawnSync } from "child_process";
69
+ import * as fs from "fs";
70
+ import * as path from "path";
71
+
72
+ // src/constants.ts
73
+ var SERVER_NAME = "virtual-architect";
74
+ var CLAUDE_INSTALL_HINT = "Install Claude Code: https://docs.claude.com/en/docs/claude-code";
75
+ var ANTHROPIC_DATA_FLOW_WARNING = "WARNING: MCP responses flow through Claude \u2192 Anthropic. Review the data-flow implications before deploying for regulated workloads.";
76
+ var BEARER_EXPOSURE_WARNING = "WARNING: 'claude mcp add' accepts the bearer token only via argv, so during this install the API key is briefly visible to other local processes (ps / /proc/<pid>/cmdline). Acceptable on a single-user workstation; treat the key as exposed if other users share this host.";
77
+
78
+ // src/claude-binary.ts
79
+ var CLAUDE_BIN = "claude";
80
+ function claudeCandidateNames() {
81
+ if (process.platform === "win32") {
82
+ return [`${CLAUDE_BIN}.exe`, `${CLAUDE_BIN}.cmd`, `${CLAUDE_BIN}.bat`, CLAUDE_BIN];
83
+ }
84
+ return [CLAUDE_BIN];
85
+ }
86
+ function isExecutableFile(filePath) {
87
+ let stat;
88
+ try {
89
+ stat = fs.statSync(filePath);
90
+ } catch {
91
+ return false;
92
+ }
93
+ if (!stat.isFile()) return false;
94
+ if (process.platform === "win32") return true;
95
+ return (stat.mode & 73) !== 0;
96
+ }
97
+ function findClaudeBinary() {
98
+ const rawPath = process.env.PATH ?? "";
99
+ if (rawPath === "") return null;
100
+ const names = claudeCandidateNames();
101
+ for (const dir of rawPath.split(path.delimiter)) {
102
+ if (dir === "") continue;
103
+ for (const name of names) {
104
+ const candidate = path.join(dir, name);
105
+ if (isExecutableFile(candidate)) return candidate;
106
+ }
107
+ }
108
+ return null;
109
+ }
110
+ function defaultSpawn(command, args, cwd) {
111
+ const result = spawnSync(command, args, {
112
+ cwd,
113
+ // Capture (do not inherit) so the token-bearing stderr never reaches our
114
+ // console; we deliberately drop it.
115
+ stdio: ["ignore", "pipe", "pipe"]
116
+ });
117
+ return { status: result.status, error: result.error };
118
+ }
119
+ function runClaudeMcpAdd(url, apiKey, cwd, spawn = defaultSpawn) {
120
+ const args = [
121
+ "mcp",
122
+ "add",
123
+ "-t",
124
+ "http",
125
+ SERVER_NAME,
126
+ url,
127
+ "--header",
128
+ `Authorization: Bearer ${apiKey}`
129
+ ];
130
+ const { status, error } = spawn(CLAUDE_BIN, args, cwd);
131
+ if (error) {
132
+ throw new InstallerError(
133
+ `could not run 'claude mcp add' (${error.message}). Re-run the command manually with the CLI's --debug flag to diagnose.`
134
+ );
135
+ }
136
+ if (status !== 0) {
137
+ throw new InstallerError(
138
+ `'claude mcp add' exited ${status}. Re-run the command manually with the CLI's --debug flag to see details.`
139
+ );
140
+ }
141
+ }
142
+ function runClaudeMcpRemove(cwd, spawn = defaultSpawn) {
143
+ const { status, error } = spawn(CLAUDE_BIN, ["mcp", "remove", SERVER_NAME], cwd);
144
+ if (error) {
145
+ throw new InstallerError(
146
+ `could not run 'claude mcp remove' (${error.message}). Re-run the command manually with the CLI's --debug flag to diagnose.`
147
+ );
148
+ }
149
+ return status === 0;
150
+ }
151
+
152
+ // src/claude-config.ts
153
+ import * as fs2 from "fs";
154
+ import * as path2 from "path";
155
+
156
+ // src/hook-template.ts
157
+ var HOOK_SCRIPT_NAME = "mcp_sessionstart_hook.sh";
158
+ var HOOK_COMMAND_RELPATH = `.claude/hooks/${HOOK_SCRIPT_NAME}`;
159
+ var HOOK_SCRIPT_CONTENT = `#!/usr/bin/env bash
160
+ # Virtual Architect \u2014 MCP SessionStart hook.
161
+ #
162
+ # Emits the static tool-usage playbook to stdout. Claude Code injects
163
+ # whatever this script writes to stdout into the agent's initial
164
+ # context for the session.
165
+ #
166
+ # No HTTP calls, no MCP fetches, no curl. Hardcoded text only \u2014 keep
167
+ # session start instantaneous and offline-safe.
168
+ set -euo pipefail
169
+
170
+ cat <<'PLAYBOOK'
171
+ How to use VA's MCP tools:
172
+ - For any natural-language question, START with \`query(question)\` \u2014
173
+ it runs Virtual Architect's curated retrieval and returns the most
174
+ relevant grounded paths. Prefer this over search_entities unless you
175
+ already have a specific entity name in hand.
176
+ - For "what is X connected to?" questions, call search_entities(X) to
177
+ find the canonical ID, then get_relationships(id).
178
+ - To orient before drilling in, \`list_documents()\` shows what's indexed;
179
+ follow up with \`query(topic)\` or \`get_document_entities(document_id)\`.
180
+ - Never speculate about entities or relationships not returned by a tool.
181
+ If a tool returns empty, say so \u2014 do not fabricate.
182
+ PLAYBOOK
183
+ `;
184
+
185
+ // src/claude-config.ts
186
+ function settingsLocalPath(projectDir) {
187
+ return path2.join(projectDir, ".claude", "settings.local.json");
188
+ }
189
+ function isPlainObject(value) {
190
+ return typeof value === "object" && value !== null && !Array.isArray(value);
191
+ }
192
+ function loadSettings(settingsPath) {
193
+ let raw;
194
+ try {
195
+ raw = fs2.readFileSync(settingsPath, "utf8");
196
+ } catch (err) {
197
+ if (err.code === "ENOENT") return {};
198
+ throw new InstallerError(`cannot read ${settingsPath}: ${err.message}`);
199
+ }
200
+ let parsed;
201
+ try {
202
+ parsed = JSON.parse(raw || "{}");
203
+ } catch (err) {
204
+ throw new InstallerError(
205
+ `existing ${settingsPath} is not valid JSON: ${err.message}`
206
+ );
207
+ }
208
+ if (!isPlainObject(parsed)) {
209
+ throw new InstallerError(`existing ${settingsPath} is not a JSON object.`);
210
+ }
211
+ return parsed;
212
+ }
213
+ function writeSettings(settingsPath, settings) {
214
+ fs2.mkdirSync(path2.dirname(settingsPath), { recursive: true });
215
+ fs2.writeFileSync(settingsPath, `${JSON.stringify(settings, null, 2)}
216
+ `);
217
+ }
218
+ function ensureObject(parent, key) {
219
+ if (!(key in parent)) parent[key] = {};
220
+ const value = parent[key];
221
+ if (!isPlainObject(value)) {
222
+ throw new InstallerError(`settings "${key}" is not an object.`);
223
+ }
224
+ return value;
225
+ }
226
+ function ensureArray(parent, key) {
227
+ if (!(key in parent)) parent[key] = [];
228
+ const value = parent[key];
229
+ if (!Array.isArray(value)) {
230
+ throw new InstallerError(`settings "${key}" is not an array.`);
231
+ }
232
+ return value;
233
+ }
234
+ function entryReferences(entry, commandNeedle) {
235
+ if (!isPlainObject(entry)) return false;
236
+ const hooks = entry.hooks;
237
+ if (!Array.isArray(hooks)) return false;
238
+ return hooks.some(
239
+ (hook) => isPlainObject(hook) && typeof hook.command === "string" && hook.command.includes(commandNeedle)
240
+ );
241
+ }
242
+ function addSessionStartHook(settings, hookRelpath) {
243
+ const hooks = ensureObject(settings, "hooks");
244
+ const sessionStart = ensureArray(hooks, "SessionStart");
245
+ if (sessionStart.some((entry2) => entryReferences(entry2, hookRelpath))) return;
246
+ const entry = {
247
+ hooks: [{ type: "command", command: `./${hookRelpath}` }]
248
+ };
249
+ sessionStart.push(entry);
250
+ }
251
+ function removeSessionStartHook(settings, hookRelpath) {
252
+ const hooks = settings.hooks;
253
+ if (!isPlainObject(hooks)) return false;
254
+ const sessionStart = hooks.SessionStart;
255
+ if (!Array.isArray(sessionStart)) return false;
256
+ const kept = sessionStart.filter((entry) => !entryReferences(entry, hookRelpath));
257
+ if (kept.length === sessionStart.length) return false;
258
+ if (kept.length > 0) {
259
+ hooks.SessionStart = kept;
260
+ } else {
261
+ delete hooks.SessionStart;
262
+ }
263
+ if (Object.keys(hooks).length === 0) delete settings.hooks;
264
+ return true;
265
+ }
266
+ function mergeSettingsLocal(projectDir) {
267
+ const settingsPath = settingsLocalPath(projectDir);
268
+ const settings = loadSettings(settingsPath);
269
+ addSessionStartHook(settings, HOOK_COMMAND_RELPATH);
270
+ writeSettings(settingsPath, settings);
271
+ }
272
+ function pruneSettingsLocal(projectDir) {
273
+ const settingsPath = settingsLocalPath(projectDir);
274
+ if (!fs2.existsSync(settingsPath)) return false;
275
+ const settings = loadSettings(settingsPath);
276
+ if (!removeSessionStartHook(settings, HOOK_COMMAND_RELPATH)) return false;
277
+ writeSettings(settingsPath, settings);
278
+ return true;
279
+ }
280
+
281
+ // src/gitignore-check.ts
282
+ import * as fs3 from "fs";
283
+ import * as path3 from "path";
284
+ var SETTINGS_RELPATH = ".claude/settings.local.json";
285
+ function stripSlashes(value) {
286
+ return value.replace(/^\/+/, "").replace(/\/+$/, "");
287
+ }
288
+ function isInsideGitWorkTree(startDir) {
289
+ let dir = path3.resolve(startDir);
290
+ for (; ; ) {
291
+ if (fs3.existsSync(path3.join(dir, ".git"))) return true;
292
+ const parent = path3.dirname(dir);
293
+ if (parent === dir) return false;
294
+ dir = parent;
295
+ }
296
+ }
297
+ function isPathIgnored(projectDir, relpath) {
298
+ let content;
299
+ try {
300
+ content = fs3.readFileSync(path3.join(projectDir, ".gitignore"), "utf8");
301
+ } catch {
302
+ return false;
303
+ }
304
+ const rel = stripSlashes(relpath);
305
+ const slash = rel.lastIndexOf("/");
306
+ const parent = slash >= 0 ? rel.slice(0, slash) : null;
307
+ const matchesTarget = (candidate) => candidate === rel || parent !== null && candidate === parent;
308
+ let ignored = false;
309
+ for (const raw of content.split(/\r?\n/)) {
310
+ const line = raw.trim();
311
+ if (!line || line.startsWith("#")) continue;
312
+ if (line.startsWith("!")) {
313
+ if (matchesTarget(stripSlashes(line.slice(1)))) return false;
314
+ continue;
315
+ }
316
+ if (matchesTarget(stripSlashes(line))) ignored = true;
317
+ }
318
+ return ignored;
319
+ }
320
+ function checkGitignorePrecondition(projectDir) {
321
+ if (!isInsideGitWorkTree(projectDir)) return;
322
+ if (isPathIgnored(projectDir, SETTINGS_RELPATH)) return;
323
+ throw new InstallerError(
324
+ `refusing to write ${SETTINGS_RELPATH} into a git work tree. The installer's plain-text gitignore scan found no exact-path or directory-prefix entry that ignores it (globs like '.claude/*' and '!' negation lines are NOT evaluated \u2014 we err on the safe side and refuse if there is any chance the file ends up tracked). To proceed, add an exact entry to ${path3.join(projectDir, ".gitignore")}:
325
+
326
+ ${SETTINGS_RELPATH}
327
+ `
328
+ );
329
+ }
330
+
331
+ // src/output.ts
332
+ function emit(sink, message) {
333
+ sink.write(`${message}
334
+ `);
335
+ }
336
+
337
+ // src/project.ts
338
+ import * as fs4 from "fs";
339
+ import * as path4 from "path";
340
+ function resolveProjectDir(projectDir) {
341
+ let resolved;
342
+ try {
343
+ resolved = fs4.realpathSync(path4.resolve(projectDir));
344
+ } catch {
345
+ throw new InstallerError(`project directory does not exist: ${projectDir}`);
346
+ }
347
+ if (!fs4.statSync(resolved).isDirectory()) {
348
+ throw new InstallerError(`project directory is not a directory: ${resolved}`);
349
+ }
350
+ return resolved;
351
+ }
352
+
353
+ // src/install.ts
354
+ function writeHookScript(projectDir) {
355
+ const hooksDir = path5.join(projectDir, ".claude", "hooks");
356
+ fs5.mkdirSync(hooksDir, { recursive: true });
357
+ const target = path5.join(hooksDir, HOOK_SCRIPT_NAME);
358
+ fs5.writeFileSync(target, HOOK_SCRIPT_CONTENT);
359
+ fs5.chmodSync(target, 493);
360
+ }
361
+ function install(opts, deps = {}) {
362
+ const findClaude = deps.findClaude ?? findClaudeBinary;
363
+ const stderr = deps.stderr ?? process.stderr;
364
+ const projectDir = resolveProjectDir(opts.projectDir);
365
+ checkGitignorePrecondition(projectDir);
366
+ if (findClaude() === null) {
367
+ throw new InstallerError(
368
+ `the 'claude' CLI was not found on PATH. ${CLAUDE_INSTALL_HINT}`
369
+ );
370
+ }
371
+ emit(stderr, ANTHROPIC_DATA_FLOW_WARNING);
372
+ emit(stderr, BEARER_EXPOSURE_WARNING);
373
+ runClaudeMcpAdd(opts.url, opts.apiKey, projectDir, deps.spawn);
374
+ writeHookScript(projectDir);
375
+ mergeSettingsLocal(projectDir);
376
+ emit(
377
+ stderr,
378
+ `Installed the Virtual Architect MCP server '${SERVER_NAME}' for the project at ${projectDir}.`
379
+ );
380
+ emit(stderr, `Next: cd '${projectDir}' && claude`);
381
+ }
382
+
383
+ // src/interactive.ts
384
+ import * as readline from "readline";
385
+ var VA_KEY_PREFIX = "va_live_";
386
+ var MAX_ATTEMPTS = 3;
387
+ function isHttpUrl(value) {
388
+ let parsed;
389
+ try {
390
+ parsed = new URL(value);
391
+ } catch {
392
+ return false;
393
+ }
394
+ return parsed.protocol === "http:" || parsed.protocol === "https:";
395
+ }
396
+ function readLine(question, io) {
397
+ const rl = readline.createInterface({ input: io.input, output: io.output });
398
+ return new Promise((resolve3, reject) => {
399
+ let answered = false;
400
+ rl.on("close", () => {
401
+ if (!answered) {
402
+ reject(
403
+ new InstallerError(
404
+ "input stream closed before a value was provided. Re-run with --url/--api-key."
405
+ )
406
+ );
407
+ }
408
+ });
409
+ rl.question(question, (answer) => {
410
+ answered = true;
411
+ rl.close();
412
+ resolve3(answer.trim());
413
+ });
414
+ });
415
+ }
416
+ function readSecret(question, io) {
417
+ const rl = readline.createInterface({ input: io.input, output: io.output, terminal: true });
418
+ let muted = false;
419
+ const mutableRl = rl;
420
+ mutableRl._writeToOutput = (chunk) => {
421
+ if (!muted || chunk.includes("\n") || chunk.includes("\r")) {
422
+ io.output.write(chunk);
423
+ }
424
+ };
425
+ return new Promise((resolve3, reject) => {
426
+ let answered = false;
427
+ rl.on("close", () => {
428
+ if (!answered) {
429
+ reject(
430
+ new InstallerError(
431
+ "input stream closed before a value was provided. Re-run with --url/--api-key."
432
+ )
433
+ );
434
+ }
435
+ });
436
+ rl.question(question, (answer) => {
437
+ answered = true;
438
+ rl.close();
439
+ io.output.write("\n");
440
+ resolve3(answer.trim());
441
+ });
442
+ muted = true;
443
+ });
444
+ }
445
+ async function promptUrl(io) {
446
+ for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
447
+ const value = await readLine("Virtual Architect deployment URL: ", io);
448
+ if (isHttpUrl(value)) return value;
449
+ io.output.write(" Please enter a valid http(s) URL.\n");
450
+ }
451
+ throw new InstallerError(
452
+ "no valid deployment URL provided after several attempts. Re-run with --url <https://\u2026>."
453
+ );
454
+ }
455
+ async function promptApiKey(io) {
456
+ for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
457
+ const value = await readSecret("Virtual Architect API key (input hidden): ", io);
458
+ if (value.startsWith(VA_KEY_PREFIX)) return value;
459
+ io.output.write(` The API key should start with "${VA_KEY_PREFIX}".
460
+ `);
461
+ }
462
+ throw new InstallerError(
463
+ "no valid API key provided after several attempts. Re-run with --api-key <key>."
464
+ );
465
+ }
466
+ async function promptForMissing(partial, io = {}) {
467
+ const resolvedIo = {
468
+ input: io.input ?? process.stdin,
469
+ output: io.output ?? process.stderr
470
+ };
471
+ let url = partial.url;
472
+ if (url === void 0) {
473
+ url = await promptUrl(resolvedIo);
474
+ } else if (!isHttpUrl(url)) {
475
+ throw new InstallerError(`--url must be a valid http(s) URL, got: ${url}`);
476
+ }
477
+ let apiKey = partial.apiKey;
478
+ if (apiKey === void 0) {
479
+ apiKey = await promptApiKey(resolvedIo);
480
+ } else if (!apiKey.startsWith(VA_KEY_PREFIX)) {
481
+ throw new InstallerError(`--api-key must start with "${VA_KEY_PREFIX}".`);
482
+ }
483
+ return { url, apiKey, projectDir: partial.projectDir };
484
+ }
485
+
486
+ // src/uninstall.ts
487
+ function uninstall(opts, deps = {}) {
488
+ const stderr = deps.stderr ?? process.stderr;
489
+ const projectDir = resolveProjectDir(opts.projectDir);
490
+ const removed = runClaudeMcpRemove(projectDir, deps.spawn);
491
+ if (removed) {
492
+ emit(stderr, `Deregistered the Virtual Architect MCP server '${SERVER_NAME}'.`);
493
+ } else {
494
+ emit(
495
+ stderr,
496
+ `The Virtual Architect MCP server '${SERVER_NAME}' was not registered for this project \u2014 nothing to deregister.`
497
+ );
498
+ }
499
+ const pruned = pruneSettingsLocal(projectDir);
500
+ if (pruned) {
501
+ emit(stderr, "Removed the SessionStart hook from .claude/settings.local.json.");
502
+ } else {
503
+ emit(stderr, "No SessionStart hook entry found in .claude/settings.local.json.");
504
+ }
505
+ emit(stderr, "The hook script under .claude/hooks/ was left in place.");
506
+ }
507
+
508
+ // src/index.ts
509
+ var PROGRAM_NAME = "va-mcp";
510
+ function buildProgram(deps) {
511
+ const run = { fn: null };
512
+ const program = new Command();
513
+ program.name(PROGRAM_NAME).description(
514
+ "Install the Virtual Architect MCP server and SessionStart playbook into a Claude Code project."
515
+ ).version(package_default.version, "-v, --version", "output the version number").configureOutput({
516
+ writeOut: (str) => deps.stdout.write(str),
517
+ writeErr: (str) => deps.stderr.write(str)
518
+ }).exitOverride();
519
+ program.command("install").description("Register the server with Claude Code and install the SessionStart hook.").option("--url <url>", "Virtual Architect deployment URL").option("--api-key <key>", "Virtual Architect API key (prompted if omitted)").option("--project-dir <dir>", "target project directory", process.cwd()).action((flags) => {
520
+ run.fn = async () => {
521
+ const resolved = await promptForMissing(
522
+ { url: flags.url, apiKey: flags.apiKey, projectDir: flags.projectDir },
523
+ { output: deps.stderr }
524
+ );
525
+ install(resolved, { stderr: deps.stderr });
526
+ };
527
+ });
528
+ program.command("uninstall").description("Deregister the server and remove the SessionStart hook entry.").option("--project-dir <dir>", "target project directory", process.cwd()).action(({ projectDir }) => {
529
+ run.fn = () => {
530
+ uninstall({ projectDir }, { stderr: deps.stderr });
531
+ return Promise.resolve();
532
+ };
533
+ });
534
+ return { program, run };
535
+ }
536
+ async function main(argv, deps = {}) {
537
+ const resolved = {
538
+ stdout: deps.stdout ?? process.stdout,
539
+ stderr: deps.stderr ?? process.stderr
540
+ };
541
+ if (argv.length === 0) {
542
+ const { program: program2 } = buildProgram(resolved);
543
+ resolved.stdout.write(program2.helpInformation());
544
+ return 0;
545
+ }
546
+ const { program, run } = buildProgram(resolved);
547
+ try {
548
+ await program.parseAsync(argv, { from: "user" });
549
+ } catch (err) {
550
+ if (err instanceof CommanderError) {
551
+ return err.exitCode;
552
+ }
553
+ throw err;
554
+ }
555
+ try {
556
+ if (run.fn) await run.fn();
557
+ } catch (err) {
558
+ if (err instanceof InstallerError) {
559
+ emit(resolved.stderr, `${PROGRAM_NAME}: ${err.message}`);
560
+ return 1;
561
+ }
562
+ throw err;
563
+ }
564
+ return 0;
565
+ }
566
+ async function cli() {
567
+ const code = await main(process.argv.slice(2));
568
+ process.exit(code);
569
+ }
570
+ function isMainModule() {
571
+ const entry = process.argv[1];
572
+ if (!entry) return false;
573
+ try {
574
+ return import.meta.url === pathToFileURL(realpathSync2(entry)).href;
575
+ } catch {
576
+ return false;
577
+ }
578
+ }
579
+ if (isMainModule()) {
580
+ void cli();
581
+ }
582
+ export {
583
+ main
584
+ };
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "@bitflower/va-mcp",
3
+ "version": "1.0.0",
4
+ "description": "Install the Virtual Architect MCP server and SessionStart playbook into a Claude Code project.",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/bitflower/pathrag-ui-virtual-architect.git",
8
+ "directory": "clients/va-mcp"
9
+ },
10
+ "type": "module",
11
+ "bin": {
12
+ "va-mcp": "./dist/index.js"
13
+ },
14
+ "files": [
15
+ "dist"
16
+ ],
17
+ "engines": {
18
+ "node": ">=18"
19
+ },
20
+ "scripts": {
21
+ "build": "tsup",
22
+ "test": "vitest run",
23
+ "test:watch": "vitest",
24
+ "typecheck": "tsc --noEmit",
25
+ "prepublishOnly": "npm run build"
26
+ },
27
+ "keywords": [
28
+ "mcp",
29
+ "claude-code",
30
+ "virtual-architect"
31
+ ],
32
+ "license": "MIT",
33
+ "publishConfig": {
34
+ "access": "public"
35
+ },
36
+ "dependencies": {
37
+ "commander": "^12.1.0"
38
+ },
39
+ "devDependencies": {
40
+ "@types/node": "^20.14.0",
41
+ "tsup": "^8.3.0",
42
+ "typescript": "^5.6.0",
43
+ "vitest": "^2.1.0"
44
+ }
45
+ }