@bridge_gpt/mcp-server 0.2.25 → 0.2.27

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 (73) hide show
  1. package/README.md +59 -9
  2. package/build/agents.generated.js +1 -1
  3. package/build/bridge-api-urls.js +31 -0
  4. package/build/commands.generated.js +4 -4
  5. package/build/conductor-bin.js +68 -9267
  6. package/build/conductor-bundle-artifacts.js +802 -0
  7. package/build/conductor-bundle-cli.js +256 -0
  8. package/build/conductor-claude-hook-bin.js +2 -172
  9. package/build/decision-page-template.js +2 -31
  10. package/build/docs.generated.js +2 -1
  11. package/build/doctor.js +148 -1
  12. package/build/env-flags.js +31 -0
  13. package/build/index.js +4659 -34836
  14. package/build/init.js +87 -59
  15. package/build/install-bridge.js +380 -22
  16. package/build/mcp-host-config.js +521 -0
  17. package/build/mcp-host-targets.js +194 -0
  18. package/build/mcp-install-state.js +175 -0
  19. package/build/pipelines.generated.js +5 -4
  20. package/build/readme.generated.js +1 -1
  21. package/build/start-tickets.js +118 -5
  22. package/build/tool-surface-gating.js +396 -0
  23. package/build/version.generated.js +1 -1
  24. package/docs/install/mcp-tool-integrations.md +2 -2
  25. package/package.json +5 -7
  26. package/design-assets/favicon/android-chrome-192x192.png +0 -0
  27. package/design-assets/favicon/android-chrome-512x512.png +0 -0
  28. package/design-assets/favicon/apple-touch-icon.png +0 -0
  29. package/design-assets/favicon/favicon-16x16.png +0 -0
  30. package/design-assets/favicon/favicon.ico +0 -0
  31. package/design-assets/favicon/site.webmanifest +0 -1
  32. package/public/css/main.min.css +0 -4971
  33. package/public/css/main.min.css.map +0 -1
  34. package/public/fonts/OFL.txt +0 -93
  35. package/public/fonts/SourceSansPro-Black.ttf +0 -0
  36. package/public/fonts/SourceSansPro-BlackItalic.ttf +0 -0
  37. package/public/fonts/SourceSansPro-Bold.ttf +0 -0
  38. package/public/fonts/SourceSansPro-BoldItalic.ttf +0 -0
  39. package/public/fonts/SourceSansPro-ExtraLight.ttf +0 -0
  40. package/public/fonts/SourceSansPro-ExtraLightItalic.ttf +0 -0
  41. package/public/fonts/SourceSansPro-Italic.ttf +0 -0
  42. package/public/fonts/SourceSansPro-Light.ttf +0 -0
  43. package/public/fonts/SourceSansPro-LightItalic.ttf +0 -0
  44. package/public/fonts/SourceSansPro-Regular.ttf +0 -0
  45. package/public/fonts/SourceSansPro-SemiBold.ttf +0 -0
  46. package/public/fonts/SourceSansPro-SemiBoldItalic.ttf +0 -0
  47. package/public/img/bridge-logo-160x51.webp +0 -0
  48. package/public/img/bridge-logo-300x92.webp +0 -0
  49. package/public/img/favicon/android-chrome-192x192.png +0 -0
  50. package/public/img/favicon/android-chrome-512x512.png +0 -0
  51. package/public/img/favicon/apple-touch-icon.png +0 -0
  52. package/public/img/favicon/favicon-16x16.png +0 -0
  53. package/public/img/favicon/favicon-32x32.png +0 -0
  54. package/public/img/favicon/favicon.ico +0 -0
  55. package/public/img/favicon/site.webmanifest +0 -1
  56. package/public/img/installation/bitbucket/app-password-1.png +0 -0
  57. package/public/img/installation/bitbucket/app-password-2.png +0 -0
  58. package/public/img/installation/bitbucket/create-token-1.png +0 -0
  59. package/public/img/installation/bitbucket/create-token-2.png +0 -0
  60. package/public/img/installation/bitbucket/webhook-1.png +0 -0
  61. package/public/img/installation/github/github-review-webhook.png +0 -0
  62. package/public/img/installation/jira/credentials/api-key.png +0 -0
  63. package/public/img/installation/jira/webhook/create-rule.png +0 -0
  64. package/public/img/installation/jira/webhook/project-settings.png +0 -0
  65. package/public/img/installation/jira/webhook/rule-create-1.png +0 -0
  66. package/public/img/installation/jira/webhook/rule-create-2.png +0 -0
  67. package/public/img/installation/jira/webhook/rule-create-3.png +0 -0
  68. package/public/img/installation/pinecone/pinecone-api-key.png +0 -0
  69. package/public/img/installation/pinecone/pinecone-index.png +0 -0
  70. package/public/js/main.min.js +0 -10427
  71. package/public/js/main.min.js.map +0 -1
  72. package/smoke-test/SMOKE-TEST.md +0 -560
  73. package/smoke-test/smoke-test-mcp.md +0 -23
@@ -0,0 +1,521 @@
1
+ /**
2
+ * Per-platform MCP host-config emission + provisioning (BAPI-635).
3
+ *
4
+ * Converts the single logical Bridge entry (from
5
+ * {@link file:./install-bridge.ts} `buildInstallBridgeServerEntry`) into each
6
+ * platform's physical JSON or TOML representation, and writes it without
7
+ * clobbering user-managed configuration.
8
+ *
9
+ * Write strategy per target comes from the data-only registry
10
+ * {@link file:./mcp-host-targets.ts}:
11
+ * - vendor-first — prefer the official vendor CLI, fall back to a safe direct
12
+ * write (Claude Code, Copilot CLI, Codex).
13
+ * - direct — merge-write the JSON file directly (Cursor, Copilot VS Code).
14
+ * - manual-instructions — never auto-write; emit redacted instructions
15
+ * (Windsurf).
16
+ *
17
+ * SECRET DISCIPLINE: the API key lives only inside the logical entry's `env`.
18
+ * When a verified vendor contract delivers it via argv (all three do — none
19
+ * expose a stdin channel), the argv is constructed and spawned INSIDE the vendor
20
+ * adapter and is never stringified, logged, previewed, returned, or interpolated
21
+ * into a warning. Structured outcomes and inspectors carry presence + launcher
22
+ * args only, never env values.
23
+ *
24
+ * TOML NOTE: there is no comment-preserving TOML library available, so Codex
25
+ * direct writes render a canonical document for a missing file, APPEND canonical
26
+ * tables to an existing file that has no `bridge-api` table (preserving every
27
+ * existing byte), and DECLINE (`manual-required`) an existing `bridge-api` table
28
+ * rather than reserialize unrelated content.
29
+ */
30
+ import { VERSION } from "./version.generated.js";
31
+ // ---------------------------------------------------------------------------
32
+ // Path helpers (POSIX-normalized; module stays free of node:path).
33
+ // ---------------------------------------------------------------------------
34
+ function joinPath(base, rel) {
35
+ const b = base.endsWith("/") ? base.slice(0, -1) : base;
36
+ const r = rel.startsWith("/") ? rel.slice(1) : rel;
37
+ return `${b}/${r}`;
38
+ }
39
+ function dirnameOf(p) {
40
+ const i = p.lastIndexOf("/");
41
+ if (i <= 0)
42
+ return "/";
43
+ return p.slice(0, i);
44
+ }
45
+ /** Resolve a target's absolute path from injected cwd/homedir. */
46
+ export function resolveTargetAbsPath(target, ctx) {
47
+ if (target.scope === "project") {
48
+ if (!target.relPath)
49
+ throw new Error(`${target.id} missing relPath`);
50
+ return joinPath(ctx.cwd, target.relPath);
51
+ }
52
+ if (!target.absPathResolver)
53
+ throw new Error(`${target.id} missing absPathResolver`);
54
+ return target.absPathResolver(ctx.homedir);
55
+ }
56
+ // ---------------------------------------------------------------------------
57
+ // Entry adaptation
58
+ // ---------------------------------------------------------------------------
59
+ /**
60
+ * Adapt the shared logical entry into a target's physical JSON entry: copy
61
+ * command/args/env, conditionally add the registry `transportType` as `type`,
62
+ * and merge `extraEntryKeys`. Claude Code omits `type`; Cursor/Copilot VS Code
63
+ * use `stdio`; Copilot CLI uses `local` + `tools: ["*"]`.
64
+ */
65
+ export function adaptBridgeEntryForHostTarget(entry, target) {
66
+ const physical = {};
67
+ if (target.transportType !== undefined) {
68
+ physical.type = target.transportType;
69
+ }
70
+ physical.command = entry.command;
71
+ physical.args = [...entry.args];
72
+ physical.env = { ...entry.env };
73
+ if (target.extraEntryKeys) {
74
+ for (const [k, v] of Object.entries(target.extraEntryKeys)) {
75
+ physical[k] = v;
76
+ }
77
+ }
78
+ return physical;
79
+ }
80
+ // ---------------------------------------------------------------------------
81
+ // JSON read / merge / write
82
+ // ---------------------------------------------------------------------------
83
+ function isEnoent(err) {
84
+ return (typeof err === "object" &&
85
+ err !== null &&
86
+ err.code === "ENOENT");
87
+ }
88
+ /** Read a JSON host config into distinct outcomes; malformed is never `{}`. */
89
+ export async function readJsonHostConfig(path, deps) {
90
+ let raw;
91
+ try {
92
+ raw = await deps.readFile(path);
93
+ }
94
+ catch (err) {
95
+ if (isEnoent(err))
96
+ return { state: "missing" };
97
+ return { state: "read-error", message: "config could not be read" };
98
+ }
99
+ let parsed;
100
+ try {
101
+ parsed = JSON.parse(raw);
102
+ }
103
+ catch {
104
+ return { state: "invalid" };
105
+ }
106
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
107
+ return { state: "invalid" };
108
+ }
109
+ return { state: "valid", value: parsed };
110
+ }
111
+ /**
112
+ * Merge the adapted `bridge-api` entry into an existing JSON document under the
113
+ * registry-selected root key. Unrelated top-level keys and unrelated servers are
114
+ * preserved; only the `bridge-api` entry is replaced.
115
+ */
116
+ export function mergeJsonHostConfig(existing, target, adaptedEntry) {
117
+ const merged = { ...existing };
118
+ const rootRaw = merged[target.topLevelKey];
119
+ const root = rootRaw && typeof rootRaw === "object" && !Array.isArray(rootRaw)
120
+ ? { ...rootRaw }
121
+ : {};
122
+ root["bridge-api"] = adaptedEntry;
123
+ merged[target.topLevelKey] = root;
124
+ return merged;
125
+ }
126
+ /** Build a fresh JSON document containing only the Bridge entry. */
127
+ function freshJsonDocument(target, adaptedEntry) {
128
+ return { [target.topLevelKey]: { "bridge-api": adaptedEntry } };
129
+ }
130
+ /**
131
+ * Direct-write a JSON host config. Malformed existing JSON is skipped with an
132
+ * explicit `skipped — invalid JSON` result and is never overwritten.
133
+ */
134
+ export async function writeJsonHostConfig(path, target, adaptedEntry, deps) {
135
+ const read = await readJsonHostConfig(path, deps);
136
+ if (read.state === "invalid" || read.state === "read-error") {
137
+ return { status: "skipped-invalid", message: "skipped — invalid JSON" };
138
+ }
139
+ const isCreate = read.state === "missing";
140
+ const doc = isCreate
141
+ ? freshJsonDocument(target, adaptedEntry)
142
+ : mergeJsonHostConfig(read.value, target, adaptedEntry);
143
+ await deps.mkdir(dirnameOf(path), { recursive: true });
144
+ await deps.writeFile(path, JSON.stringify(doc, null, 2) + "\n");
145
+ return { status: isCreate ? "created" : "direct-written", path };
146
+ }
147
+ // ---------------------------------------------------------------------------
148
+ // Codex TOML rendering + safe merge
149
+ // ---------------------------------------------------------------------------
150
+ /** Escape a string for a TOML basic string literal. */
151
+ export function tomlEscapeString(value) {
152
+ let out = "";
153
+ for (const ch of value) {
154
+ switch (ch) {
155
+ case "\\":
156
+ out += "\\\\";
157
+ break;
158
+ case '"':
159
+ out += '\\"';
160
+ break;
161
+ case "\n":
162
+ out += "\\n";
163
+ break;
164
+ case "\r":
165
+ out += "\\r";
166
+ break;
167
+ case "\t":
168
+ out += "\\t";
169
+ break;
170
+ case "\b":
171
+ out += "\\b";
172
+ break;
173
+ case "\f":
174
+ out += "\\f";
175
+ break;
176
+ default: {
177
+ const code = ch.codePointAt(0);
178
+ if (code < 0x20) {
179
+ out += "\\u" + code.toString(16).padStart(4, "0");
180
+ }
181
+ else {
182
+ out += ch;
183
+ }
184
+ }
185
+ }
186
+ }
187
+ return out;
188
+ }
189
+ function tomlBasicString(value) {
190
+ return `"${tomlEscapeString(value)}"`;
191
+ }
192
+ function tomlStringArray(values) {
193
+ return `[${values.map(tomlBasicString).join(", ")}]`;
194
+ }
195
+ /** Deterministic env-key order for the Codex table (matches the web snippet). */
196
+ const CODEX_ENV_ORDER = [
197
+ "BAPI_BASE_URL",
198
+ "BAPI_REPO_NAME",
199
+ "BAPI_API_KEY",
200
+ "BAPI_DOCS_DIR",
201
+ "BAPI_PROJECT_ROOT",
202
+ ];
203
+ function orderedEnvKeys(env) {
204
+ const known = CODEX_ENV_ORDER.filter((k) => k in env);
205
+ const extra = Object.keys(env)
206
+ .filter((k) => !CODEX_ENV_ORDER.includes(k))
207
+ .sort();
208
+ return [...known, ...extra];
209
+ }
210
+ /**
211
+ * Render the canonical Codex TOML for the Bridge entry. Every string, path, URL,
212
+ * and launcher arg is TOML-escaped so it cannot produce malformed TOML.
213
+ */
214
+ export function renderCodexBridgeToml(entry) {
215
+ const lines = [
216
+ "[mcp_servers.bridge-api]",
217
+ `command = ${tomlBasicString(entry.command)}`,
218
+ `args = ${tomlStringArray(entry.args)}`,
219
+ "",
220
+ "[mcp_servers.bridge-api.env]",
221
+ ];
222
+ for (const key of orderedEnvKeys(entry.env)) {
223
+ lines.push(`${key} = ${tomlBasicString(entry.env[key])}`);
224
+ }
225
+ return lines.join("\n") + "\n";
226
+ }
227
+ /** Whether a TOML document already declares a `[mcp_servers.bridge-api]` table. */
228
+ export function codexHasBridgeTable(content) {
229
+ // Match the bare table header or any dotted subtable header, at line start
230
+ // (allowing leading whitespace). Never reserialize — presence only.
231
+ return /^[ \t]*\[mcp_servers\.bridge-api(\.[^\]]*)?\][ \t]*$/m.test(content);
232
+ }
233
+ /**
234
+ * Compute the safe Codex merge action. A missing document → canonical create; an
235
+ * existing document without a Bridge table → append (existing bytes preserved,
236
+ * minimal newline boundary); an existing Bridge table → decline (manual-required)
237
+ * rather than reserialize.
238
+ */
239
+ export function mergeCodexHostConfig(existing, entry) {
240
+ if (existing.state === "missing") {
241
+ return { action: "create", content: renderCodexBridgeToml(entry) };
242
+ }
243
+ if (existing.state === "read-error" || existing.state === "invalid") {
244
+ // Cannot safely reason about the file → decline.
245
+ return { action: "manual-required" };
246
+ }
247
+ const content = existing.state === "text"
248
+ ? existing.content
249
+ : // "valid" JsonReadResult is not used for TOML; guard anyway.
250
+ "";
251
+ if (codexHasBridgeTable(content)) {
252
+ return { action: "manual-required" };
253
+ }
254
+ const boundary = content.length === 0 || content.endsWith("\n") ? "\n" : "\n\n";
255
+ return { action: "append", content: content + boundary + renderCodexBridgeToml(entry) };
256
+ }
257
+ /** Read a TOML file into a missing / text / read-error result. */
258
+ async function readTomlHostConfig(path, deps) {
259
+ try {
260
+ const content = await deps.readFile(path);
261
+ return { state: "text", content };
262
+ }
263
+ catch (err) {
264
+ if (isEnoent(err))
265
+ return { state: "missing" };
266
+ return { state: "read-error", message: "config could not be read" };
267
+ }
268
+ }
269
+ /** Direct-write the Codex TOML config using the safe merge strategy. */
270
+ export async function writeCodexHostConfig(path, entry, deps) {
271
+ const read = await readTomlHostConfig(path, deps);
272
+ const merge = mergeCodexHostConfig(read, entry);
273
+ if (merge.action === "manual-required") {
274
+ return { status: "manual-required" };
275
+ }
276
+ await deps.mkdir(dirnameOf(path), { recursive: true });
277
+ await deps.writeFile(path, merge.content);
278
+ return { status: merge.action === "create" ? "created" : "direct-written", path };
279
+ }
280
+ /** Inspect a JSON host config for a `bridge-api` entry (no env values read). */
281
+ export async function inspectJsonHostEntry(path, target, deps) {
282
+ const read = await readJsonHostConfig(path, deps);
283
+ if (read.state !== "valid")
284
+ return { present: false };
285
+ const root = read.value[target.topLevelKey];
286
+ if (!root || typeof root !== "object" || Array.isArray(root))
287
+ return { present: false };
288
+ const entry = root["bridge-api"];
289
+ if (!entry || typeof entry !== "object")
290
+ return { present: false };
291
+ const args = entry.args;
292
+ return {
293
+ present: true,
294
+ args: Array.isArray(args) ? args.filter((a) => typeof a === "string") : undefined,
295
+ };
296
+ }
297
+ /** Inspect a Codex TOML config for a `bridge-api` table (no env values read). */
298
+ export async function inspectTomlHostEntry(path, deps) {
299
+ const read = await readTomlHostConfig(path, deps);
300
+ if (read.state !== "text")
301
+ return { present: false };
302
+ if (!codexHasBridgeTable(read.content))
303
+ return { present: false };
304
+ // Extract the args = [...] line from the main table for pin classification.
305
+ const m = read.content.match(/^\s*args\s*=\s*\[(.*?)\]/m);
306
+ let args;
307
+ if (m) {
308
+ args = m[1]
309
+ .split(",")
310
+ .map((s) => s.trim().replace(/^["']|["']$/g, ""))
311
+ .filter((s) => s.length > 0);
312
+ }
313
+ return { present: true, args };
314
+ }
315
+ /** Inspect any target (format-aware) for a `bridge-api` entry. */
316
+ export async function inspectHostEntry(target, ctx, deps) {
317
+ const path = resolveTargetAbsPath(target, ctx);
318
+ return target.format === "toml"
319
+ ? inspectTomlHostEntry(path, deps)
320
+ : inspectJsonHostEntry(path, target, deps);
321
+ }
322
+ // ---------------------------------------------------------------------------
323
+ // Vendor CLI invocation builders (argv-isolated; secret-bearing for some kinds)
324
+ // ---------------------------------------------------------------------------
325
+ /**
326
+ * Whether a vendor kind's secret-delivery contract is VERIFIED. Only verified
327
+ * contracts are ever invoked; an uncertain contract falls back to a direct
328
+ * write rather than guessing argv. Verified from empirical probes (BAPI-635):
329
+ * claude `mcp add-json <name> <json>` and copilot `mcp add --env` both deliver
330
+ * the secret through argv. Codex's contract is unverified (binary absent at
331
+ * authoring time) → never invoked; always direct-written.
332
+ */
333
+ function isVendorContractVerified(kind) {
334
+ return kind === "claude-add-json" || kind === "copilot-add";
335
+ }
336
+ /**
337
+ * Build the vendor add invocation for a target. Returns `null` when the
338
+ * contract is unverified (caller must fall back to a direct write). The returned
339
+ * `args` may be secret-bearing and MUST be handed straight to the process
340
+ * boundary without logging.
341
+ */
342
+ function buildVendorInvocation(target, entry) {
343
+ const vendor = target.vendorCli;
344
+ if (!vendor || !isVendorContractVerified(vendor.kind))
345
+ return null;
346
+ if (vendor.kind === "claude-add-json") {
347
+ // claude mcp add-json <name> <json> --scope project
348
+ // The JSON payload carries env.BAPI_API_KEY → secret-bearing argv.
349
+ const json = JSON.stringify({
350
+ command: entry.command,
351
+ args: entry.args,
352
+ env: entry.env,
353
+ });
354
+ return {
355
+ bin: vendor.bin,
356
+ args: ["mcp", "add-json", "bridge-api", json, "--scope", "project"],
357
+ };
358
+ }
359
+ if (vendor.kind === "copilot-add") {
360
+ // copilot mcp add bridge-api --tools "*" --env K=V ... -- <command> <args...>
361
+ // Env pairs (incl BAPI_API_KEY) are secret-bearing argv.
362
+ const envArgs = [];
363
+ for (const [k, v] of Object.entries(entry.env)) {
364
+ envArgs.push("--env", `${k}=${v}`);
365
+ }
366
+ return {
367
+ bin: vendor.bin,
368
+ args: [
369
+ "mcp",
370
+ "add",
371
+ "bridge-api",
372
+ "--tools",
373
+ "*",
374
+ ...envArgs,
375
+ "--",
376
+ entry.command,
377
+ ...entry.args,
378
+ ],
379
+ };
380
+ }
381
+ return null;
382
+ }
383
+ // ---------------------------------------------------------------------------
384
+ // Provisioning orchestration
385
+ // ---------------------------------------------------------------------------
386
+ function outcome(target, status, detail) {
387
+ return {
388
+ platform: target.id,
389
+ status,
390
+ displayPath: target.displayPath,
391
+ detail,
392
+ };
393
+ }
394
+ /** Direct-write a single target (JSON or Codex TOML) and map to an outcome. */
395
+ async function directWriteTarget(target, entry, deps) {
396
+ const path = resolveTargetAbsPath(target, deps);
397
+ if (target.format === "toml") {
398
+ const res = await writeCodexHostConfig(path, entry, deps.fs);
399
+ if (res.status === "manual-required") {
400
+ return outcome(target, "manual-required", "existing config could not be safely updated");
401
+ }
402
+ return outcome(target, "direct-written");
403
+ }
404
+ const adapted = adaptBridgeEntryForHostTarget(entry, target);
405
+ const res = await writeJsonHostConfig(path, target, adapted, deps.fs);
406
+ if (res.status === "skipped-invalid") {
407
+ return outcome(target, "skipped-invalid", res.message);
408
+ }
409
+ return outcome(target, "direct-written");
410
+ }
411
+ /**
412
+ * Provision a single host target, honoring its write strategy:
413
+ * - manual-instructions → `manual-required`
414
+ * - direct → JSON merge-write (or Codex safe write)
415
+ * - vendor-first → probe + invoke the verified vendor CLI, verify the entry
416
+ * landed, else fall back to a safe direct write. Any missing binary, spawn
417
+ * error, timeout, non-zero exit, or missing post-write entry triggers the
418
+ * fallback rather than a fatal error.
419
+ */
420
+ export async function provisionHostTarget(target, entry, deps) {
421
+ if (target.writeStrategy === "manual-instructions") {
422
+ return outcome(target, "manual-required", "global config — manual setup");
423
+ }
424
+ if (target.writeStrategy === "direct") {
425
+ return directWriteTarget(target, entry, deps);
426
+ }
427
+ // vendor-first
428
+ const invocation = buildVendorInvocation(target, entry);
429
+ if (invocation) {
430
+ const available = await deps.vendor.probeBinary(invocation.bin, deps.env);
431
+ if (available) {
432
+ const result = await deps.vendor.invokeVendorAdd(invocation, deps.env);
433
+ if (result.ok) {
434
+ // Verify the entry actually landed before trusting the vendor CLI.
435
+ const inspected = await inspectHostEntry(target, deps, deps.fs);
436
+ if (inspected.present) {
437
+ return outcome(target, "vendor-written");
438
+ }
439
+ }
440
+ }
441
+ }
442
+ // Fallback: safe direct write (covers unverified contracts like Codex, a
443
+ // missing/failed vendor binary, or a nominally-successful write that left no
444
+ // entry).
445
+ return directWriteTarget(target, entry, deps);
446
+ }
447
+ // ---------------------------------------------------------------------------
448
+ // Default vendor process boundary (spawns real vendor CLIs, secret-safe)
449
+ // ---------------------------------------------------------------------------
450
+ /** Env keys stripped from a read-only probe environment. */
451
+ const PROBE_SECRET_KEYS = ["BAPI_API_KEY", "BAPI_INVITE", "BAPI_SIGNUP_EMAIL"];
452
+ /** Return a shallow env clone with credential sentinels removed. */
453
+ export function sanitizeProbeEnv(env) {
454
+ const clone = { ...env };
455
+ for (const key of PROBE_SECRET_KEYS)
456
+ delete clone[key];
457
+ return clone;
458
+ }
459
+ /** The launcher spec for the current version (pins are version-locked). */
460
+ export function currentLauncherSpec() {
461
+ return `@bridge_gpt/mcp-server@${VERSION}`;
462
+ }
463
+ /** Bounded probe/invoke timeout (ms). */
464
+ const VENDOR_PROCESS_TIMEOUT_MS = 8000;
465
+ /**
466
+ * Build the default vendor process boundary. Both operations use array-based
467
+ * spawning with `shell:false`, ignored stdio, a bounded timeout, and a sanitized
468
+ * env for the read-only probe. Every path resolves (never rejects) so a vendor
469
+ * failure is a fallback trigger, not a crash. Secret-bearing argv is passed
470
+ * straight to `spawn` and never logged.
471
+ */
472
+ export function createDefaultVendorProcessDeps(spawnFn) {
473
+ return {
474
+ probeBinary: (bin, env) => new Promise((resolve) => {
475
+ let settled = false;
476
+ const done = (ok) => {
477
+ if (settled)
478
+ return;
479
+ settled = true;
480
+ resolve(ok);
481
+ };
482
+ try {
483
+ const child = spawnFn(bin, ["--version"], {
484
+ stdio: "ignore",
485
+ shell: false,
486
+ env: sanitizeProbeEnv(env),
487
+ timeout: VENDOR_PROCESS_TIMEOUT_MS,
488
+ });
489
+ child.on("error", () => done(false));
490
+ child.on("close", (code) => done(code === 0));
491
+ }
492
+ catch {
493
+ done(false);
494
+ }
495
+ }),
496
+ invokeVendorAdd: (invocation, env) => new Promise((resolve) => {
497
+ let settled = false;
498
+ const done = (ok) => {
499
+ if (settled)
500
+ return;
501
+ settled = true;
502
+ resolve({ ok });
503
+ };
504
+ try {
505
+ const child = spawnFn(invocation.bin, invocation.args, {
506
+ stdio: "ignore",
507
+ shell: false,
508
+ // The add command needs the credential sentinels in argv, not env;
509
+ // env stays sanitized so nothing leaks through a second channel.
510
+ env: sanitizeProbeEnv(env),
511
+ timeout: VENDOR_PROCESS_TIMEOUT_MS,
512
+ });
513
+ child.on("error", () => done(false));
514
+ child.on("close", (code) => done(code === 0));
515
+ }
516
+ catch {
517
+ done(false);
518
+ }
519
+ }),
520
+ };
521
+ }