@bridge_gpt/mcp-server 0.2.36 → 0.2.38

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 (92) hide show
  1. package/README.md +48 -8
  2. package/build/base-url.js +79 -0
  3. package/build/bridge-api-urls.js +9 -0
  4. package/build/chain-orchestrator.js +93 -15
  5. package/build/claude-user-config-doctor.js +317 -0
  6. package/build/commands.generated.js +2 -1
  7. package/build/conductor/bridge-api-client.js +178 -4
  8. package/build/conductor-bin.js +1 -1
  9. package/build/conductor-bundle-artifacts.js +7 -6
  10. package/build/credential-store.js +205 -4
  11. package/build/direct-ticket-tools.js +70 -0
  12. package/build/doctor.js +239 -80
  13. package/build/executor/cli.js +51 -1
  14. package/build/executor/credentials.js +1 -7
  15. package/build/executor/deps.js +18 -1
  16. package/build/executor/env.js +51 -25
  17. package/build/executor/heartbeat.js +138 -17
  18. package/build/executor/http-client.js +49 -8
  19. package/build/executor/job-errors.js +4 -0
  20. package/build/executor/job-runner.js +422 -22
  21. package/build/executor/observation.js +130 -0
  22. package/build/executor/permissions.js +104 -8
  23. package/build/executor/preflight.js +32 -0
  24. package/build/executor/runner.js +8 -0
  25. package/build/executor/test-clock.js +67 -3
  26. package/build/executor/types.js +4 -1
  27. package/build/executor/worker-command.js +11 -3
  28. package/build/executor/worker-config-isolation.js +287 -0
  29. package/build/executor/worker-finalization.js +68 -14
  30. package/build/executor/worktree.js +46 -4
  31. package/build/index.js +614 -244
  32. package/build/init.js +363 -73
  33. package/build/install-bridge.js +568 -80
  34. package/build/launcher-config-inspection.js +351 -0
  35. package/build/mcp-invoke.js +49 -6
  36. package/build/mcp-provisioning.js +30 -7
  37. package/build/mcp-registration-doctor.js +14 -5
  38. package/build/notifications.js +553 -0
  39. package/build/pipeline-orchestrator.js +146 -4
  40. package/build/pipeline-utils.js +3 -0
  41. package/build/pipelines.generated.js +22 -9
  42. package/build/plan-execution-ledger.js +550 -0
  43. package/build/plan-phase-routing.js +272 -0
  44. package/build/plane/alembic-head.js +110 -0
  45. package/build/plane/build-freshness.js +167 -0
  46. package/build/plane/cli.js +480 -0
  47. package/build/plane/defaults.js +266 -0
  48. package/build/plane/manifest.js +377 -0
  49. package/build/plane/member-logs.js +147 -0
  50. package/build/plane/member-roster.js +147 -0
  51. package/build/plane/preflight.js +289 -0
  52. package/build/plane/shutdown.js +195 -0
  53. package/build/plane/status.js +125 -0
  54. package/build/plane/supervisor.js +569 -0
  55. package/build/plane/test-fakes.js +156 -0
  56. package/build/plane/types.js +75 -0
  57. package/build/readme.generated.js +1 -1
  58. package/build/run-unit-tests-launcher.js +2 -0
  59. package/build/setup-epic.js +662 -27
  60. package/build/sfcc/log-gate.js +38 -11
  61. package/build/sfcc/log-query.js +55 -15
  62. package/build/sfcc/ocapi-shape.js +70 -14
  63. package/build/sfcc/output.js +41 -11
  64. package/build/sfcc/permissions.js +24 -2
  65. package/build/sfcc/read-body.js +92 -0
  66. package/build/sfcc/read-projection.js +185 -0
  67. package/build/sfcc/read-result.js +158 -0
  68. package/build/sfcc/reads-custom-object-def.js +57 -34
  69. package/build/sfcc/reads-site-preference.js +86 -33
  70. package/build/sfcc/reads-system-object.js +50 -38
  71. package/build/sfcc/sfcc-result.js +106 -0
  72. package/build/sfcc/tool-wrapper.js +56 -13
  73. package/build/sfcc/write-grants.js +45 -22
  74. package/build/sfcc/write-guard.js +21 -13
  75. package/build/sfcc/write-result.js +71 -15
  76. package/build/sfcc/write-tool-common.js +126 -32
  77. package/build/sfcc/writes-custom-object-def.js +6 -2
  78. package/build/sfcc/writes-system-object.js +11 -50
  79. package/build/start-tickets-prereqs.js +129 -0
  80. package/build/start-tickets.js +17 -13
  81. package/build/ticket-backend-metadata.js +59 -0
  82. package/build/ticket-key-utils.js +92 -0
  83. package/build/tool-error-envelope.js +71 -0
  84. package/build/tool-surface-gating.js +72 -0
  85. package/build/update-status.js +102 -0
  86. package/build/upgrade-advice.js +47 -0
  87. package/build/upgrade-cli.js +417 -101
  88. package/build/version.generated.js +1 -1
  89. package/build/worktree-core.js +73 -0
  90. package/docs/CONDUCTOR.md +23 -8
  91. package/package.json +3 -3
  92. package/pipelines/implement-ticket.json +15 -5
package/build/init.js CHANGED
@@ -11,17 +11,24 @@
11
11
  */
12
12
  import { writeFile, mkdir, readFile, stat } from "fs/promises";
13
13
  import path from "path";
14
+ import os from "os";
14
15
  import { COMMANDS } from "./commands.generated.js";
15
16
  import { AGENTS } from "./agents.generated.js";
16
17
  import { DOCS } from "./docs.generated.js";
17
18
  import { VERSION } from "./version.generated.js";
18
19
  import { reconstructAgentMarkdown, translateAgentToCopilot } from "./agent-utils.js";
19
- import { validateRepoName } from "./bridge-config.js";
20
+ import { validateRepoName, resolveRepoNameForProjectRoot } from "./bridge-config.js";
20
21
  import { ensureGitignored as ensureGitignoredShared } from "./git-ignore-utils.js";
22
+ import { resolveBapiCredentials } from "./credential-store.js";
23
+ import { BRIDGE_ENTRY_KEY, describeLauncherReason, inspectLauncherConfigs, isPrerequisiteFailure, launcherTargetFor, } from "./launcher-config-inspection.js";
21
24
  // ---------------------------------------------------------------------------
22
25
  // CLI: --init scaffolds slash commands and MCP configs into the current project
23
26
  // ---------------------------------------------------------------------------
24
- export function buildBridgeApiEntry(cwd) {
27
+ /** The production Bridge API endpoint a placeholder entry falls back to. */
28
+ export const DEFAULT_BRIDGE_BASE_URL = "https://bridgegpt-api.com";
29
+ /** The repo-name placeholder that marks an entry as never-configured. */
30
+ export const PLACEHOLDER_REPO_NAME = "YOUR_REPO_NAME";
31
+ export function buildBridgeApiEntry(cwd, metadata) {
25
32
  // Secret-free by design: BAPI_API_KEY is NEVER scaffolded into generated MCP
26
33
  // config. The server self-resolves the key from the environment or the
27
34
  // home-dir credential store (~/.config/bridge/credentials.json) at runtime.
@@ -47,12 +54,15 @@ export function buildBridgeApiEntry(cwd) {
47
54
  // an installer invocation. Bare invocation remains a permanent server-start
48
55
  // contract; see `refreshBridgeApiPackageSpec` for why EXISTING configs are never
49
56
  // migrated to this shape.
57
+ // BAPI-728: `repoName`/`baseUrl` are non-secret and always supplied by the
58
+ // caller when it has already resolved them. `BAPI_API_KEY` remains absent in
59
+ // EVERY branch — the server self-resolves it at runtime.
50
60
  return {
51
61
  command: "npx",
52
62
  args: ["-y", "--prefer-offline", `@bridge_gpt/mcp-server@${VERSION}`, "serve"],
53
63
  env: {
54
- BAPI_BASE_URL: "https://bridgegpt-api.com",
55
- BAPI_REPO_NAME: "YOUR_REPO_NAME",
64
+ BAPI_BASE_URL: metadata?.baseUrl ?? DEFAULT_BRIDGE_BASE_URL,
65
+ BAPI_REPO_NAME: metadata?.repoName ?? PLACEHOLDER_REPO_NAME,
56
66
  BAPI_DOCS_DIR: "docs/tmp",
57
67
  BAPI_PROJECT_ROOT: cwd,
58
68
  },
@@ -271,11 +281,11 @@ export async function mergeBridgeApiProfileToken(cwd, token) {
271
281
  }
272
282
  return lastProfile;
273
283
  }
274
- async function ensureGitignored(cwd, filePath) {
284
+ async function ensureGitignored(cwd, filePath, io) {
275
285
  await ensureGitignoredShared(cwd, filePath, {
276
- readFile: (p) => readFile(p, "utf-8"),
277
- writeFile: (p, data) => writeFile(p, data, "utf-8"),
278
- mkdir: (p, options) => mkdir(p, options),
286
+ readFile: (p) => (io?.readFile ?? readFile)(p, "utf-8"),
287
+ writeFile: (p, data) => (io?.writeFile ?? writeFile)(p, data, "utf-8"),
288
+ mkdir: (p, options) => (io?.mkdir ?? mkdir)(p, options),
279
289
  });
280
290
  }
281
291
  /**
@@ -304,11 +314,149 @@ async function scaffoldFile(target, content, mkdirDir) {
304
314
  await writeFile(target, content, "utf-8");
305
315
  return existed ? "overwritten" : "written";
306
316
  }
317
+ /**
318
+ * A preflight failure: an existing config we must not write over. Thrown BEFORE
319
+ * any mutation so a run either reconciles everything or changes nothing.
320
+ */
321
+ export class InitPreflightError extends Error {
322
+ failures;
323
+ constructor(failures) {
324
+ super(`Cannot reconcile MCP launcher configs:\n${failures
325
+ .map((f) => ` ${f.relPath}: ${f.reason}`)
326
+ .join("\n")}`);
327
+ this.name = "InitPreflightError";
328
+ this.failures = failures;
329
+ }
330
+ }
331
+ /**
332
+ * Resolve the non-secret identity a NEW `bridge-api` entry should carry.
333
+ *
334
+ * Precedence (plan Step 3.9): an explicit caller context wins; then an explicit
335
+ * `BAPI_REPO_NAME`; then the committed `.bridge/config` / git-derived identity.
336
+ * A cwd basename is only ever an unconfirmed candidate — it is promoted to the
337
+ * real identity ONLY when a `bapi:<repo>` credential actually resolves for it,
338
+ * so a directory name can never masquerade as a configured repository.
339
+ *
340
+ * The resolved API key is reduced immediately to `credentialAvailable`. It is
341
+ * never returned, logged, persisted, or written into a generated MCP entry.
342
+ */
343
+ async function resolveNewEntryIdentity(cwd, options, io) {
344
+ const env = options.env ?? process.env;
345
+ const explicitBaseUrl = typeof env.BAPI_BASE_URL === "string" ? env.BAPI_BASE_URL.trim() : "";
346
+ const baseUrl = explicitBaseUrl.length > 0 ? explicitBaseUrl : undefined;
347
+ // A caller that already did verified resolution is authoritative.
348
+ if (options.creationContext?.repoName) {
349
+ return {
350
+ repoName: options.creationContext.repoName,
351
+ baseUrl: options.creationContext.baseUrl ?? baseUrl,
352
+ credentialAvailable: options.creationContext.credentialAvailable ?? true,
353
+ baseUrlDefaulted: !(options.creationContext.baseUrl ?? baseUrl),
354
+ };
355
+ }
356
+ const checkCredential = async (repoName) => {
357
+ try {
358
+ const result = options.resolveCredentials
359
+ ? await options.resolveCredentials(repoName)
360
+ : await resolveBapiCredentials(repoName, {
361
+ env,
362
+ homedir: options.homedir ?? os.homedir,
363
+ platform: options.platform ?? process.platform,
364
+ readFile: (p) => io.readFile(p, "utf-8"),
365
+ stat: async (p) => {
366
+ const s = (await io.stat(p));
367
+ return { mode: typeof s?.mode === "number" ? s.mode : 0o600 };
368
+ },
369
+ // Silence the resolver's own stderr: init owns its reporting.
370
+ stderr: () => { },
371
+ });
372
+ return result.ok;
373
+ }
374
+ catch {
375
+ return false;
376
+ }
377
+ };
378
+ /** Candidate identities in precedence order; the first that authenticates wins. */
379
+ const confirmed = [];
380
+ const explicitRepo = typeof env.BAPI_REPO_NAME === "string" ? env.BAPI_REPO_NAME.trim() : "";
381
+ if (explicitRepo.length > 0 && explicitRepo !== PLACEHOLDER_REPO_NAME) {
382
+ const validated = validateRepoName(explicitRepo);
383
+ if (validated.ok) {
384
+ // An explicit environment identity is a deliberate operator statement, so
385
+ // it is used even when no credential is present locally; readiness still
386
+ // reflects the missing credential.
387
+ return {
388
+ repoName: validated.value,
389
+ baseUrl,
390
+ credentialAvailable: await checkCredential(validated.value),
391
+ baseUrlDefaulted: baseUrl === undefined,
392
+ };
393
+ }
394
+ }
395
+ const manifestDeps = {
396
+ readFile: (p) => io.readFile(p, "utf-8"),
397
+ runCommand: undefined,
398
+ };
399
+ try {
400
+ const resolved = options.resolveRepoName
401
+ ? await options.resolveRepoName(cwd)
402
+ : await resolveRepoNameForProjectRoot(cwd, manifestDeps);
403
+ if (resolved.ok && resolved.value !== PLACEHOLDER_REPO_NAME) {
404
+ confirmed.push(resolved.value);
405
+ }
406
+ }
407
+ catch {
408
+ // A malformed or unreadable manifest is not fatal for identity resolution;
409
+ // it just means this source contributes no candidate.
410
+ }
411
+ for (const candidate of confirmed) {
412
+ if (await checkCredential(candidate)) {
413
+ return {
414
+ repoName: candidate,
415
+ baseUrl,
416
+ credentialAvailable: true,
417
+ baseUrlDefaulted: baseUrl === undefined,
418
+ };
419
+ }
420
+ }
421
+ // The cwd basename is the weakest signal: accepted ONLY when a credential
422
+ // exists for it. Otherwise it stays an unconfirmed candidate and the entry is
423
+ // written explicitly unconfigured rather than looking configured.
424
+ const basename = validateRepoName(path.basename(cwd));
425
+ if (basename.ok && (await checkCredential(basename.value))) {
426
+ return {
427
+ repoName: basename.value,
428
+ baseUrl,
429
+ credentialAvailable: true,
430
+ baseUrlDefaulted: baseUrl === undefined,
431
+ };
432
+ }
433
+ // Nothing resolvable. Fall back to a manifest/git identity WITHOUT claiming it
434
+ // is configured, so the entry at least names the right repo for a human to fix.
435
+ return {
436
+ repoName: confirmed[0],
437
+ baseUrl,
438
+ credentialAvailable: false,
439
+ baseUrlDefaulted: baseUrl === undefined,
440
+ };
441
+ }
307
442
  /**
308
443
  * Core initialization logic shared by --init and --upgrade.
309
444
  * Runs all scaffolding phases without calling process.exit().
445
+ *
446
+ * BAPI-728: performs a complete read-only launcher preflight BEFORE its first
447
+ * write, and returns structured, secret-free results instead of `void`.
310
448
  */
311
- export async function runInit(cwd) {
449
+ export async function runInit(cwd, options = {}) {
450
+ const io = {
451
+ readFile: options.io?.readFile ?? readFile,
452
+ writeFile: options.io?.writeFile ?? writeFile,
453
+ mkdir: options.io?.mkdir ?? mkdir,
454
+ stat: options.io?.stat ?? stat,
455
+ };
456
+ const targetVersion = options.targetVersion ?? VERSION;
457
+ const targetSpec = `@bridge_gpt/mcp-server@${targetVersion}`;
458
+ const say = options.stdout ?? ((m) => console.log(m));
459
+ const warn = options.stderr ?? ((m) => console.error(m));
312
460
  // ---- Phase 1: IDE Detection ----
313
461
  // BAPI-708: detection lives in `detectInitIdes` so the commit-safe asset
314
462
  // manifest resolves the SAME conditional targets this scaffolder writes.
@@ -325,82 +473,215 @@ export async function runInit(cwd) {
325
473
  windsurfSnippet + "\n");
326
474
  }
327
475
  // ---- Phase 3: Config file handling ----
476
+ // Order is preserved from the original implementation so existing console
477
+ // narration and its tests are unchanged; the per-path metadata (which root
478
+ // key each config uses) now comes from the shared inspection module.
328
479
  const configTargets = [
329
- { path: ".mcp.json", topLevelKey: "mcpServers", shouldCreate: true },
330
- { path: ".vscode/mcp.json", topLevelKey: "servers", shouldCreate: ideDetection.vscode },
331
- { path: ".cursor/mcp.json", topLevelKey: "mcpServers", shouldCreate: ideDetection.cursor },
332
- ];
480
+ { path: ".mcp.json", shouldCreate: true },
481
+ { path: ".vscode/mcp.json", shouldCreate: ideDetection.vscode },
482
+ { path: ".cursor/mcp.json", shouldCreate: ideDetection.cursor },
483
+ ].map((t) => ({
484
+ ...t,
485
+ topLevelKey: launcherTargetFor(t.path).topLevelKey,
486
+ }));
487
+ // ---- Phase 3a: read-only preflight, BEFORE the first write ----
488
+ // A run must either reconcile every config or change nothing. Discovering a
489
+ // malformed second config only after the first was rewritten would leave the
490
+ // project half-migrated with no record of what moved.
491
+ const inspections = await inspectLauncherConfigs({
492
+ cwd,
493
+ targetVersion,
494
+ readFile: (p) => io.readFile(p, "utf-8"),
495
+ isCreatable: (relPath) => configTargets.find((t) => t.path === relPath)?.shouldCreate ?? false,
496
+ targets: configTargets.map((t) => ({
497
+ relPath: t.path,
498
+ topLevelKey: t.topLevelKey,
499
+ })),
500
+ });
501
+ const blocking = inspections.filter(isPrerequisiteFailure);
502
+ if (blocking.length > 0) {
503
+ throw new InitPreflightError(blocking.map((f) => ({
504
+ relPath: f.relPath,
505
+ reason: describeLauncherReason(f.reason),
506
+ })));
507
+ }
508
+ const byPath = new Map(inspections.map((i) => [i.relPath, i]));
509
+ // Resolve the identity a NEW entry would carry — only when one is actually
510
+ // needed, so an ordinary repin run never touches the credential store.
511
+ const needsNewEntry = inspections.some((i) => i.action === "create" || i.action === "add");
512
+ const identity = needsNewEntry
513
+ ? await resolveNewEntryIdentity(cwd, options, io)
514
+ : { credentialAvailable: true, baseUrlDefaulted: false };
515
+ const entryReady = Boolean(identity.repoName) && identity.credentialAvailable;
516
+ const newEntryMetadata = {
517
+ repoName: entryReady ? identity.repoName : undefined,
518
+ baseUrl: identity.baseUrl,
519
+ };
333
520
  const configActions = [];
521
+ const structuredActions = [];
522
+ const readinessWarnings = [];
334
523
  for (const target of configTargets) {
524
+ const found = byPath.get(target.path);
335
525
  const fullPath = path.join(cwd, target.path);
336
- let fileExists = false;
337
- try {
338
- await stat(fullPath);
339
- fileExists = true;
340
- }
341
- catch { }
342
- if (!target.shouldCreate && !fileExists) {
526
+ if (found.action === "skip-inactive") {
343
527
  configActions.push({ path: target.path, action: "skipped — IDE not detected" });
528
+ structuredActions.push({ relPath: target.path, kind: "skipped-inactive" });
344
529
  continue;
345
530
  }
346
- if (fileExists) {
347
- // Read and parse existing file
348
- const raw = await readFile(fullPath, "utf-8");
349
- let parsed;
350
- try {
351
- parsed = JSON.parse(raw);
352
- }
353
- catch {
354
- console.warn(` ${target.path} skipped — invalid JSON format`);
355
- configActions.push({ path: target.path, action: "skipped — invalid JSON" });
356
- continue;
357
- }
358
- const topLevel = parsed[target.topLevelKey];
359
- if (topLevel && topLevel["bridge-api"]) {
360
- // Entry exists update BAPI_PROJECT_ROOT and refresh the version pin.
361
- // Note: command is always rewritten to "npx". A custom launcher (e.g. "bun x", wrapper
362
- // script) will be replaced. This is intentional for the standard npx-based setup.
363
- const entryTemplate = buildBridgeApiEntry(cwd);
364
- const prevCommand = topLevel["bridge-api"].command;
365
- if (prevCommand && prevCommand !== entryTemplate.command) {
366
- console.warn(`Warning: replacing launcher "${prevCommand}" with "${entryTemplate.command}" in ${target.path}. If you use a custom launcher, re-add it after this upgrade.`);
367
- }
368
- topLevel["bridge-api"].command = entryTemplate.command;
369
- // BAPI-714: refresh ONLY the package-spec token. Assigning the whole
370
- // template array here would migrate every legacy bare launcher to
371
- // `serve` on the next --init/install/upgrade, which requirement C-3
372
- // forbids. An args array we cannot recognize falls back to the template
373
- // (that is the pre-existing behavior for a shape we cannot preserve).
374
- topLevel["bridge-api"].args =
375
- refreshBridgeApiPackageSpec(topLevel["bridge-api"].args, currentBridgePackageSpec()) ??
376
- entryTemplate.args;
377
- if (!topLevel["bridge-api"].env)
378
- topLevel["bridge-api"].env = {};
379
- topLevel["bridge-api"].env.BAPI_PROJECT_ROOT = cwd;
380
- await writeFile(fullPath, JSON.stringify(parsed, null, 2) + "\n", "utf-8");
381
- configActions.push({ path: target.path, action: "updated entry and pinned version" });
531
+ if (found.action === "skip-worktree-shim") {
532
+ // BAPI-714 / worktree provisioning: this registration points at a sibling
533
+ // checkout's build. Gate BEFORE touching `command`, `args`, or `env` —
534
+ // rewriting any of them would break the worktree's server.
535
+ configActions.push({
536
+ path: target.path,
537
+ action: "skipped — worktree mcp-invoke shim preserved",
538
+ });
539
+ structuredActions.push({
540
+ relPath: target.path,
541
+ kind: "skipped-worktree-shim",
542
+ });
543
+ continue;
544
+ }
545
+ if (found.action === "ahead") {
546
+ // Never walk a project backwards. `upgrade` blocks on this before it
547
+ // ever calls init; a direct `--init` from an older CLI reaches here, and
548
+ // must leave the newer pin exactly as it found it.
549
+ configActions.push({
550
+ path: target.path,
551
+ action: `skipped pinned ahead at ${found.currentVersion}`,
552
+ });
553
+ structuredActions.push({
554
+ relPath: target.path,
555
+ kind: "skipped-ahead",
556
+ fromVersion: found.currentVersion ?? undefined,
557
+ fromSpec: found.spec ?? undefined,
558
+ });
559
+ continue;
560
+ }
561
+ if (found.action === "create") {
562
+ await io.mkdir(path.dirname(fullPath), { recursive: true });
563
+ const content = {
564
+ [target.topLevelKey]: {
565
+ [BRIDGE_ENTRY_KEY]: buildBridgeApiEntry(cwd, newEntryMetadata),
566
+ },
567
+ };
568
+ await io.writeFile(fullPath, JSON.stringify(content, null, 2) + "\n", "utf-8");
569
+ await ensureGitignored(cwd, target.path, io);
570
+ configActions.push({ path: target.path, action: "created" });
571
+ structuredActions.push({
572
+ relPath: target.path,
573
+ kind: "created",
574
+ toVersion: targetVersion,
575
+ unconfigured: !entryReady,
576
+ });
577
+ if (!entryReady) {
578
+ readinessWarnings.push({
579
+ relPath: target.path,
580
+ kind: "unconfigured-entry-created",
581
+ baseUrlDefaulted: identity.baseUrlDefaulted,
582
+ });
382
583
  }
383
- else {
384
- // Entry missing — add it, preserving existing content
385
- if (!parsed[target.topLevelKey])
386
- parsed[target.topLevelKey] = {};
387
- parsed[target.topLevelKey]["bridge-api"] = buildBridgeApiEntry(cwd);
388
- await writeFile(fullPath, JSON.stringify(parsed, null, 2) + "\n", "utf-8");
389
- configActions.push({ path: target.path, action: "added entry" });
584
+ continue;
585
+ }
586
+ // Every remaining action reads and rewrites an existing, already-validated
587
+ // file. The preflight above proved it parses and carries a shape we can
588
+ // safely reconcile, so no defensive re-parse branch is needed here.
589
+ const raw = await io.readFile(fullPath, "utf-8");
590
+ const parsed = JSON.parse(raw);
591
+ if (found.action === "add") {
592
+ if (!parsed[target.topLevelKey])
593
+ parsed[target.topLevelKey] = {};
594
+ parsed[target.topLevelKey][BRIDGE_ENTRY_KEY] = buildBridgeApiEntry(cwd, newEntryMetadata);
595
+ await io.writeFile(fullPath, JSON.stringify(parsed, null, 2) + "\n", "utf-8");
596
+ configActions.push({ path: target.path, action: "added entry" });
597
+ structuredActions.push({
598
+ relPath: target.path,
599
+ kind: "added",
600
+ toVersion: targetVersion,
601
+ unconfigured: !entryReady,
602
+ });
603
+ if (!entryReady) {
604
+ readinessWarnings.push({
605
+ relPath: target.path,
606
+ kind: "unconfigured-entry-created",
607
+ baseUrlDefaulted: identity.baseUrlDefaulted,
608
+ });
390
609
  }
610
+ continue;
391
611
  }
392
- else {
393
- // Create new file
394
- await mkdir(path.dirname(fullPath), { recursive: true });
395
- const content = { [target.topLevelKey]: { "bridge-api": buildBridgeApiEntry(cwd) } };
396
- await writeFile(fullPath, JSON.stringify(content, null, 2) + "\n", "utf-8");
397
- await ensureGitignored(cwd, target.path);
398
- configActions.push({ path: target.path, action: "created" });
612
+ // Existing standard launcher: change ONLY the package-spec token and
613
+ // BAPI_PROJECT_ROOT. `command` is preserved (BAPI-728 — the old code
614
+ // rewrote it to "npx" unconditionally, silently replacing a custom
615
+ // launcher), and `refreshBridgeApiPackageSpec` preserves the bare-versus-
616
+ // `serve` composition plus every surrounding argument (BAPI-714 C-3).
617
+ const entry = parsed[target.topLevelKey][BRIDGE_ENTRY_KEY];
618
+ const refreshed = refreshBridgeApiPackageSpec(entry.args, targetSpec);
619
+ if (refreshed === null) {
620
+ // Unreachable: preflight classifies an unrecognizable args array as
621
+ // `unsupported` and throws above. Fail closed rather than fall back to
622
+ // the template, which is what used to clobber custom launchers.
623
+ throw new InitPreflightError([
624
+ { relPath: target.path, reason: describeLauncherReason("no-package-token") },
625
+ ]);
626
+ }
627
+ entry.args = refreshed;
628
+ if (!entry.env)
629
+ entry.env = {};
630
+ entry.env.BAPI_PROJECT_ROOT = cwd;
631
+ await io.writeFile(fullPath, JSON.stringify(parsed, null, 2) + "\n", "utf-8");
632
+ const kind = found.action === "already-current"
633
+ ? "already-current"
634
+ : found.action === "normalize-unpinned"
635
+ ? "normalized"
636
+ : "repinned";
637
+ configActions.push({ path: target.path, action: "updated entry and pinned version" });
638
+ structuredActions.push({
639
+ relPath: target.path,
640
+ kind,
641
+ fromVersion: found.currentVersion ?? undefined,
642
+ fromSpec: found.spec ?? undefined,
643
+ toVersion: targetVersion,
644
+ });
645
+ // An existing entry that still says YOUR_REPO_NAME loads without error and
646
+ // authenticates as nobody. Its env is left exactly as-is — but the run is
647
+ // no longer allowed to report itself as a working configuration.
648
+ if (entry.env?.BAPI_REPO_NAME === PLACEHOLDER_REPO_NAME) {
649
+ readinessWarnings.push({
650
+ relPath: target.path,
651
+ kind: "unconfigured-entry-existing",
652
+ });
399
653
  }
400
654
  }
401
- console.log("\nMCP config files:");
402
- for (const entry of configActions) {
403
- console.log(` ${entry.path}: ${entry.action}`);
655
+ if (!options.suppressConfigSummary) {
656
+ say("\nMCP config files:");
657
+ for (const entry of configActions) {
658
+ say(` ${entry.path}: ${entry.action}`);
659
+ }
660
+ }
661
+ // Loud, secret-free readiness reporting. A config that looks configured but
662
+ // silently 401s is the failure mode BAPI-728 exists to end, so this warning
663
+ // names every affected path and what to do about it.
664
+ if (readinessWarnings.length > 0) {
665
+ const created = readinessWarnings.filter((w) => w.kind === "unconfigured-entry-created");
666
+ const existing = readinessWarnings.filter((w) => w.kind === "unconfigured-entry-existing");
667
+ warn("\n⚠ Bridge API MCP configuration is NOT ready to authenticate.");
668
+ if (created.length > 0) {
669
+ warn(` Wrote an UNCONFIGURED bridge-api entry (BAPI_REPO_NAME=${PLACEHOLDER_REPO_NAME}) to: ${created
670
+ .map((w) => w.relPath)
671
+ .join(", ")}`);
672
+ warn(identity.repoName
673
+ ? ` Reason: no API key could be resolved for repository "${identity.repoName}".`
674
+ : " Reason: no repository identity could be resolved from the environment, .bridge/config, or the credential store.");
675
+ if (identity.baseUrlDefaulted) {
676
+ warn(` BAPI_BASE_URL was defaulted to the production endpoint ${DEFAULT_BRIDGE_BASE_URL}.`);
677
+ }
678
+ }
679
+ if (existing.length > 0) {
680
+ warn(` Existing bridge-api entry still uses the ${PLACEHOLDER_REPO_NAME} placeholder in: ${existing
681
+ .map((w) => w.relPath)
682
+ .join(", ")}`);
683
+ }
684
+ warn(" Fix with: npx -y @bridge_gpt/mcp-server install (or set BAPI_REPO_NAME and store a key with the install flow)");
404
685
  }
405
686
  // ---- Phase 4: Scaffold slash command directories ----
406
687
  const commandDirs = [path.join(cwd, ".claude", "commands")];
@@ -694,4 +975,13 @@ body explicitly says to serialize structured output.
694
975
  await mkdir(path.dirname(bridgeConfigPath), { recursive: true });
695
976
  await writeFile(bridgeConfigPath, buildBridgeConfigManifest(chooseScaffoldRepoName(cwd)), "utf-8");
696
977
  }
978
+ // BAPI-728: authoritative, secret-free outcome. `ok` is false whenever any
979
+ // entry authenticates as nobody, so no caller can report full success over a
980
+ // configuration that will 401 on its first request.
981
+ return {
982
+ ok: readinessWarnings.length === 0,
983
+ configActions: structuredActions,
984
+ readinessWarnings,
985
+ targetVersion,
986
+ };
697
987
  }