@bridge_gpt/mcp-server 0.2.24 → 0.2.25

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.
@@ -36,8 +36,26 @@
36
36
  * reload the just-written `.mcp.json`, and field derivation needs an agent
37
37
  * runtime the shell does not have.
38
38
  *
39
- * BOOTSTRAP-INVITE MODE (BAPI-606) the one exception to "this command consumes
40
- * a key, it does not create one". With `--invite` (or `BAPI_INVITE`) there is no
39
+ * That spawn command embeds the entire agent prompt and runs to multiple KB, which
40
+ * no macOS terminal will accept as one typed line. So it is never typed: the full
41
+ * command is written to a restricted (0600) launch script and only a short
42
+ * `. '<path>'` runner is spawned (BAPI-626, via `materializeWorkerLaunchCommand`).
43
+ * Materialization happens BEFORE any install side effect, because a command that
44
+ * cannot be launched is a whole-run failure, not a Step 5 warning.
45
+ *
46
+ * ONBOARDING BRANCHES — `have-key` (consume an existing key) vs. `need-key` (this
47
+ * command creates the project AND its first admin key). There are two need-key
48
+ * methods, differing only in how the first token is obtained: `bootstrap-invite`
49
+ * (BAPI-606) redeems a pre-issued invite; `self-serve` (BAPI-618) mints one from an
50
+ * email. Both then feed the SAME redemption protocol. Selection is pure and
51
+ * deterministic from flags/env, except that a BARE interactive run is asked which
52
+ * branch it wants (BAPI-626) — without that question a first-time user cannot
53
+ * discover self-serve at all. Either need-key method NAMES a new project, so the
54
+ * repository prompt asks for a new project name rather than an existing
55
+ * registration (see `RepoNamePromptMode`).
56
+ *
57
+ * BOOTSTRAP-INVITE MODE (BAPI-606) — one of the two exceptions to "this command
58
+ * consumes a key, it does not create one". With `--invite` (or `BAPI_INVITE`) there is no
41
59
  * API key yet, so the pre-flight ping of Step 2 CANNOT be made: the exchange is
42
60
  * what mints the key, and it REPLACES that ping. The order becomes:
43
61
  *
@@ -71,9 +89,13 @@ import { runInit, buildBridgeApiEntry } from "./init.js";
71
89
  import { VERSION } from "./version.generated.js";
72
90
  import { validateRepoName } from "./bridge-config.js";
73
91
  import { resolveStartTicketsRepoName } from "./start-tickets-repo.js";
74
- import { upsertBapiCredential, getPrimaryCredentialStorePath, prepareBootstrapPendingCredential, repointBootstrapPendingCredential, promoteBootstrapPendingCredential, } from "./credential-store.js";
92
+ import { upsertBapiCredential, getPrimaryCredentialStorePath, prepareBootstrapPendingCredential, repointBootstrapPendingCredential, promoteBootstrapPendingCredential, resolveBapiCredentials, } from "./credential-store.js";
93
+ // BAPI-631: the optional GitHub connect offer reuses the standalone command's flow and
94
+ // API primitives verbatim — no duplicated polling, browser, or picker logic here.
95
+ import { fetchGithubConfigurationState } from "./connect-github-api.js";
96
+ import { createDefaultConnectGithubDeps, runGithubConnectionFlow } from "./connect-github.js";
75
97
  import { DEFAULT_AGENT_NAME, resolveAgentSpec, isAgentName, formatValidAgentNames, } from "./agent-registry.js";
76
- import { buildGenericAgentShellCommand, getDefaultSpawnTerminalTabForPlatform, detectTerminal, createDefaultStartTicketsDeps, } from "./start-tickets.js";
98
+ import { buildGenericAgentShellCommand, getDefaultSpawnTerminalTabForPlatform, detectTerminal, createDefaultStartTicketsDeps, materializeWorkerLaunchCommand, MAX_TERMINAL_COMMAND_BYTES, } from "./start-tickets.js";
77
99
  /** Redaction sentinel — the API-key value is NEVER printed; this stands in. */
78
100
  export const REDACTED_API_KEY = "<REDACTED>";
79
101
  /**
@@ -126,8 +148,10 @@ export const INSTALL_BRIDGE_AGENT_PROMPT = "Execute the /install-bridge command
126
148
  "claim the job was queued — report the sanitized result and leave indexing pending. " +
127
149
  "On NO (or any unavailable/non-interactive resolution): do not index; print the exact copy-paste " +
128
150
  "continuation command '/parse-repository' on its own line and state that indexing remains pending. " +
129
- "Never request, echo, or transport any credential — only ever direct the human to the setup UI via " +
130
- "the command's configure_in pointer. " +
151
+ "Never request, echo, or transport any credential — only ever direct the human to that " +
152
+ "integration's own configure_in pointer, verbatim. The pointer is per-integration and is NOT " +
153
+ "always the setup UI: GitHub's is a terminal command (connect-github), while Jira, SFCC, and " +
154
+ "Bitbucket point at the setup UI. Follow whatever the report says rather than assuming. " +
131
155
  "End with an explicit summary line stating how many config fields the apply_install_manifest call " +
132
156
  "applied (e.g. 'Applied 8 of 9 derived fields') and whether indexing was queued or left pending — " +
133
157
  "if 0 fields were applied, say so loudly and explain what is still pending.";
@@ -146,13 +170,20 @@ export function getInstallBridgeUsage() {
146
170
  "routing credential, then opens a fresh agent session to derive the remaining",
147
171
  "config, present a capability report, and offer optional repository indexing.",
148
172
  "",
173
+ "Run it bare — `install-bridge` with no flags — in a terminal and it asks",
174
+ `\`${INSTALL_BRIDGE_KEY_SELECTOR_PROMPT.trim()}\` first. Answer yes (or press Enter) for the`,
175
+ "existing-key flow below; answer no and it asks for an email and creates a new",
176
+ "Bridge workspace for you (the self-serve flow). That question is asked ONLY for a",
177
+ "bare interactive run: passing ANY flag, setting BAPI_API_KEY, or running without",
178
+ "an interactive terminal keeps the existing deterministic behavior and no prompt.",
179
+ "",
149
180
  "Inputs (the only two irreducible ones):",
150
181
  " --api-key <key> Bridge API key. Falls back to the BAPI_API_KEY env var,",
151
182
  " then an interactive (no-echo) prompt. Generate one in the",
152
183
  " Bridge API web UI Security page — this command consumes a",
153
- " key, it does not create one (--invite is the one exception:",
154
- " it CREATES the project and its first admin key). NEVER",
155
- " printed or logged.",
184
+ " key, it does not create one (--email and --invite are the",
185
+ " exceptions: they CREATE the project and its first admin key).",
186
+ " NEVER printed or logged.",
156
187
  " --repo <name> Repository name. --repo and BAPI_REPO_NAME still take",
157
188
  " priority and short-circuit before any network call. When",
158
189
  " neither is set, a compatible server resolves the unique",
@@ -160,10 +191,13 @@ export function getInstallBridgeUsage() {
160
191
  " server is older, the key is unresolvable, or resolution",
161
192
  " fails, it falls back to an inferred default you confirm",
162
193
  " interactively (and to a required --repo when stdin is",
163
- " non-interactive). MUST match the server-side repo",
164
- " registration (it keys the credential store as bapi:<repo>).",
165
- " With --invite it is the name your NEW project is created",
166
- " under (globally unique).",
194
+ " non-interactive). In the existing-key flow it MUST match the",
195
+ " server-side repo registration (it keys the credential store",
196
+ " as bapi:<repo>). In either new-project flow (--email,",
197
+ " --invite, or a negative answer to the key question above) it",
198
+ " instead NAMES the project this run creates, so you are asked",
199
+ " to name a new project rather than match an existing one; the",
200
+ " name must be globally unique.",
167
201
  "",
168
202
  "Self-serve onboarding (no account, no API key, no pre-issued invite):",
169
203
  " --email <addr> Create a brand-new Bridge workspace from just an email —",
@@ -171,8 +205,10 @@ export function getInstallBridgeUsage() {
171
205
  " It requests a fresh workspace for that email, then creates",
172
206
  " the project and mints your own admin API key in one command.",
173
207
  " Falls back to the BAPI_SIGNUP_EMAIL env var, then a visible",
174
- " interactive prompt. The email is NOT a secret (it is shown",
175
- " as you type), but it is never printed to a log. Mutually",
208
+ " interactive prompt which is also what a negative answer to",
209
+ " the bare-run key question above reaches. The email is NOT a",
210
+ " secret (it is shown as you type), but it is never printed to",
211
+ " a log. Mutually",
176
212
  " exclusive with --api-key and --invite. No email verification",
177
213
  " is performed and no message is sent to the address — it only",
178
214
  " labels the new workspace.",
@@ -420,6 +456,66 @@ export function promptSecretViaReadline(promptText, input = process.stdin, outpu
420
456
  muted = true;
421
457
  });
422
458
  }
459
+ /**
460
+ * Offer to connect GitHub, if it is not already connected (BAPI-631).
461
+ *
462
+ * Entirely best-effort and non-destructive: by the time this runs the install itself is
463
+ * already complete and durable, so nothing here may fail the run. Every branch that is
464
+ * not "the user said yes and it worked" simply proceeds to the agent session.
465
+ *
466
+ * Reuses the shared connect-github flow rather than duplicating the API, polling,
467
+ * browser, or picker logic — there is exactly one implementation of that handshake.
468
+ */
469
+ async function offerGithubConnection(repoName, deps, log) {
470
+ // No prompt surface → no offer. Never assume consent on a non-interactive run.
471
+ if (!deps.isTTY || !deps.promptLine)
472
+ return;
473
+ try {
474
+ const credDeps = {
475
+ env: deps.env,
476
+ homedir: deps.homedir,
477
+ platform: deps.platform,
478
+ readFile: deps.readFile,
479
+ stat: deps.stat,
480
+ stderr: () => { },
481
+ };
482
+ // Resolve through the shared resolver rather than reusing an in-memory key from
483
+ // this run: the offered flow is the same shell-spawned surface the standalone
484
+ // command uses, and it must resolve credentials the same way (project MCP config
485
+ // env is NOT visible to a spawned shell).
486
+ const cred = await resolveBapiCredentials(repoName, credDeps);
487
+ if (!cred.ok)
488
+ return;
489
+ const api = {
490
+ fetch: deps.fetch,
491
+ baseUrl: deps.env.BAPI_BASE_URL?.trim() || DEFAULT_BAPI_BASE_URL,
492
+ apiKey: cred.credentials.apiKey,
493
+ };
494
+ const state = await fetchGithubConfigurationState(api, repoName);
495
+ if (state === "configured")
496
+ return;
497
+ if (state === "unavailable") {
498
+ // Do NOT fabricate "unconfigured" from a probe that simply failed — offering to
499
+ // connect an already-connected repo is worse than staying quiet.
500
+ log(" note: could not read GitHub configuration status; skipping the GitHub offer.");
501
+ return;
502
+ }
503
+ const answer = (await deps.promptLine("Connect GitHub? (Y/n): ")).trim().toLowerCase();
504
+ if (answer === "n" || answer === "no")
505
+ return; // declining is a normal outcome
506
+ const connectDeps = createDefaultConnectGithubDeps();
507
+ const code = await runGithubConnectionFlow(connectDeps, api, repoName);
508
+ if (code !== 0) {
509
+ log(" note: GitHub was not connected. Your install is complete — connect GitHub later with " +
510
+ `'npx -y @bridge_gpt/mcp-server@latest connect-github --repo ${repoName}'.`);
511
+ }
512
+ }
513
+ catch {
514
+ // The install is already durable; a failure here is never fatal to it.
515
+ log(" note: the GitHub connection offer could not run. Your install is complete — connect " +
516
+ `GitHub later with 'npx -y @bridge_gpt/mcp-server@latest connect-github --repo ${repoName}'.`);
517
+ }
518
+ }
423
519
  /** Echoed single-line prompt on stderr (used for repo confirmation / value). */
424
520
  function promptLineViaReadline(promptText) {
425
521
  return new Promise((resolve) => {
@@ -666,6 +762,68 @@ export function resolveInstallBridgeOnboardingBranch(options, env) {
666
762
  return { kind: "need-key", method: "self-serve" };
667
763
  return { kind: "have-key" };
668
764
  }
765
+ /** The exact visible text of the bare-TTY onboarding selector (BAPI-626). */
766
+ export const INSTALL_BRIDGE_KEY_SELECTOR_PROMPT = "Do you have a Bridge API key? [Y/n] ";
767
+ /**
768
+ * Interactive wrapper around {@link resolveInstallBridgeOnboardingBranch}.
769
+ *
770
+ * The pure resolver defaults every un-signalled invocation to `have-key`, which is
771
+ * correct for a script but wrong for a human: a first-time user with nothing yet
772
+ * runs a bare `install-bridge`, gets the hidden API-key prompt, and has no way to
773
+ * discover that the self-serve email path exists. So a BARE INTERACTIVE run — and
774
+ * only that — is asked which branch it wants.
775
+ *
776
+ * "Bare" is deliberately strict. Any of these keeps the existing deterministic
777
+ * behaviour with NO prompt:
778
+ *
779
+ * - stdin is not a TTY, or no `promptLine` seam is available (scripts, CI);
780
+ * - any CLI argument was supplied (the user already stated an intent);
781
+ * - a non-blank `BAPI_API_KEY` is present (that IS the existing-key intent);
782
+ * - the pure resolver already chose a need-key branch explicitly.
783
+ *
784
+ * A prompt failure becomes a typed, secret-free failure rather than an exception:
785
+ * the caller turns it into an exit code, and terminal/internal error text never
786
+ * reaches the user.
787
+ */
788
+ export async function resolveInstallBridgeOnboardingBranchForRun(options, deps, argv) {
789
+ const branch = resolveInstallBridgeOnboardingBranch(options, deps.env);
790
+ // An explicit need-key intent is already unambiguous — never re-ask it.
791
+ if (branch.kind === "need-key")
792
+ return { ok: true, branch };
793
+ const hasEnvApiKey = (deps.env.BAPI_API_KEY ?? "").trim().length > 0;
794
+ const isBareInvocation = argv.length === 0;
795
+ if (!deps.isTTY || !deps.promptLine || !isBareInvocation || hasEnvApiKey) {
796
+ return { ok: true, branch };
797
+ }
798
+ const promptLine = deps.promptLine;
799
+ try {
800
+ // Bounded so a prompt seam that returns the same invalid value forever (a
801
+ // misbehaving pipe that passes the TTY check) cannot spin indefinitely.
802
+ for (let attempt = 0; attempt < 5; attempt += 1) {
803
+ const answer = (await promptLine(INSTALL_BRIDGE_KEY_SELECTOR_PROMPT)).trim().toLowerCase();
804
+ // Blank = accept the bracketed default (Y), matching the prompt's own contract.
805
+ if (answer.length === 0 || answer === "y" || answer === "yes") {
806
+ return { ok: true, branch: { kind: "have-key" } };
807
+ }
808
+ if (answer === "n" || answer === "no") {
809
+ return { ok: true, branch: { kind: "need-key", method: "self-serve" } };
810
+ }
811
+ deps.log("Please answer y or n (press Enter for yes).");
812
+ }
813
+ return {
814
+ ok: false,
815
+ error: "No valid answer to the Bridge API key question. Re-run and answer y or n.",
816
+ };
817
+ }
818
+ catch {
819
+ // Secret-free by construction: the caught value is never surfaced.
820
+ return {
821
+ ok: false,
822
+ error: "Could not read your answer from the terminal. Re-run with --api-key <key> if you have a " +
823
+ "Bridge API key, or --email <addr> to create a new Bridge workspace.",
824
+ };
825
+ }
826
+ }
669
827
  /**
670
828
  * Return the explicitly configured repository name (`--repo`, then
671
829
  * `BAPI_REPO_NAME`), trimmed, or `undefined` when neither is supplied. Pure: no
@@ -682,7 +840,18 @@ export function resolveConfiguredRepoName(options, env) {
682
840
  }
683
841
  return undefined;
684
842
  }
685
- export async function resolveRepoName(options, deps) {
843
+ /**
844
+ * Resolve the repo name: `--repo` → `BAPI_REPO_NAME` env → inferred default
845
+ * (from .bridge/config, else the cwd basename) confirmed interactively. Fails
846
+ * fast (no inference) when neither is supplied and stdin is non-interactive —
847
+ * the repo identity keys the credential store, so it is never silently inferred
848
+ * non-interactively.
849
+ *
850
+ * `mode` selects only the WORDING (prompt and non-interactive error); resolution
851
+ * order, inference, and validation are identical in both. It defaults to
852
+ * `existing-registration` to preserve the behaviour of pre-BAPI-626 callers.
853
+ */
854
+ export async function resolveRepoName(options, deps, mode = "existing-registration") {
686
855
  const configured = resolveConfiguredRepoName(options, deps.env);
687
856
  if (configured !== undefined) {
688
857
  return { ok: true, value: configured };
@@ -692,9 +861,13 @@ export async function resolveRepoName(options, deps) {
692
861
  if (!deps.isTTY || !deps.promptLine) {
693
862
  return {
694
863
  ok: false,
695
- error: "A repo name is required. Pass --repo or set the BAPI_REPO_NAME environment variable " +
696
- "(no interactive terminal is available to confirm an inferred name). It must match the " +
697
- "server-side repository registration.",
864
+ error: mode === "new-project"
865
+ ? "A project name is required. Pass --repo or set the BAPI_REPO_NAME environment " +
866
+ "variable (no interactive terminal is available to confirm an inferred name). It " +
867
+ "names the new Bridge project this run creates and must be globally unique."
868
+ : "A repo name is required. Pass --repo or set the BAPI_REPO_NAME environment variable " +
869
+ "(no interactive terminal is available to confirm an inferred name). It must match the " +
870
+ "server-side repository registration.",
698
871
  };
699
872
  }
700
873
  // Infer a sensible default: existing .bridge/config, else the cwd basename.
@@ -709,13 +882,19 @@ export async function resolveRepoName(options, deps) {
709
882
  inferred = validated.value;
710
883
  }
711
884
  if (inferred) {
712
- const answer = (await deps.promptLine(`Repo name [${inferred}] (must match server-side registration): `)).trim();
885
+ const promptText = mode === "new-project"
886
+ ? `Name your new Bridge project [${inferred}]: `
887
+ : `Repo name [${inferred}] (must match server-side registration): `;
888
+ const answer = (await deps.promptLine(promptText)).trim();
713
889
  const chosen = answer.length > 0 ? answer : inferred;
714
890
  if (chosen.length > 0)
715
891
  return { ok: true, value: chosen };
716
892
  }
717
893
  else {
718
- const answer = (await deps.promptLine("Repo name (must match server-side registration): ")).trim();
894
+ const promptText = mode === "new-project"
895
+ ? "Name your new Bridge project: "
896
+ : "Repo name (must match server-side registration): ";
897
+ const answer = (await deps.promptLine(promptText)).trim();
719
898
  if (answer.length > 0)
720
899
  return { ok: true, value: answer };
721
900
  }
@@ -1159,7 +1338,29 @@ export function buildDryRunPreview(plan) {
1159
1338
  `Step 3b — pre-warm the version-pinned launcher bucket (fail-open, env sanitized — BAPI_API_KEY removed): ${plan.prewarmCommand}`,
1160
1339
  MCP_TIMEOUT_GUIDANCE,
1161
1340
  `Step 4 — persist routing credential: target ${plan.credentialTarget} at ${plan.credentialStorePath}`,
1162
- `Step 5 — spawn agent session: ${plan.spawnCommand}`,
1341
+ ...buildLaunchStepPreview(plan),
1342
+ ];
1343
+ }
1344
+ /**
1345
+ * The Step 5 preview lines, shared by both previews so the launch description
1346
+ * cannot drift between the have-key and need-key flows.
1347
+ *
1348
+ * A --dry-run never writes the launch script, so the preview describes the
1349
+ * materialization rather than performing it — but it still shows the full command,
1350
+ * because the command is what the user is previewing and it is secret-free.
1351
+ */
1352
+ function buildLaunchStepPreview(plan) {
1353
+ return [
1354
+ // BAPI-631: described, never performed in --dry-run — a preview must not open a
1355
+ // browser or reach the network. It is also strictly optional, so it carries no step
1356
+ // number of its own and never changes the 5-step count.
1357
+ "Step 4b — optional GitHub connect (SKIPPED in --dry-run): read GitHub's configured state",
1358
+ " via the install manifest and, only when it is unconfigured and the terminal is",
1359
+ " interactive, offer 'Connect GitHub? (Y/n)' before the agent session starts.",
1360
+ "Step 5 — spawn agent session: the full command below is stored in a restricted launch script",
1361
+ " (mode 0600, under the system temp dir) and only a short sourced runner is spawned",
1362
+ " (the script itself is NOT written in --dry-run):",
1363
+ ` ${plan.spawnCommand}`,
1163
1364
  ];
1164
1365
  }
1165
1366
  /**
@@ -1214,7 +1415,7 @@ function buildBootstrapDryRunPreview(plan) {
1214
1415
  `Step 3b — pre-warm the version-pinned launcher bucket (fail-open, env sanitized — BAPI_API_KEY / BAPI_INVITE removed): ${plan.prewarmCommand}`,
1215
1416
  MCP_TIMEOUT_GUIDANCE,
1216
1417
  `Step 4 — promote ${pendingTarget} → ${plan.credentialTarget} at ${plan.credentialStorePath} (only after the exchange succeeds)`,
1217
- `Step 5 — spawn agent session: ${plan.spawnCommand}`,
1418
+ ...buildLaunchStepPreview(plan),
1218
1419
  ];
1219
1420
  }
1220
1421
  /**
@@ -1301,12 +1502,21 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
1301
1502
  return 1;
1302
1503
  }
1303
1504
  const options = parsed.options;
1304
- // Onboarding branch (pure, deterministic): have-key vs. a need-key method
1305
- // (`bootstrap-invite` = redeem a pre-issued invite, `self-serve` = mint one from
1306
- // an email, BAPI-618). BOTH need-key methods share the downstream redemption
1307
- // protocol, so `bootstrapInviteMode` is true for both; `selfServeSignupMode`
1308
- // discriminates the one extra step (mint-from-email) the self-serve path adds.
1309
- const branch = resolveInstallBridgeOnboardingBranch(options, deps.env);
1505
+ // Onboarding branch: have-key vs. a need-key method (`bootstrap-invite` = redeem
1506
+ // a pre-issued invite, `self-serve` = mint one from an email, BAPI-618). BOTH
1507
+ // need-key methods share the downstream redemption protocol, so
1508
+ // `bootstrapInviteMode` is true for both; `selfServeSignupMode` discriminates the
1509
+ // one extra step (mint-from-email) the self-serve path adds.
1510
+ //
1511
+ // The selection is pure and deterministic for every explicit, env-driven, and
1512
+ // non-TTY invocation; only a BARE interactive run is asked which branch it wants
1513
+ // (BAPI-626 — otherwise a first-time user can never reach self-serve).
1514
+ const branchResult = await resolveInstallBridgeOnboardingBranchForRun(options, deps, argv);
1515
+ if (!branchResult.ok) {
1516
+ errorLog(`Error: ${branchResult.error}`);
1517
+ return 1;
1518
+ }
1519
+ const branch = branchResult.branch;
1310
1520
  const bootstrapInviteMode = branch.kind === "need-key";
1311
1521
  const selfServeSignupMode = branch.kind === "need-key" && branch.method === "self-serve";
1312
1522
  // ---- Resolve inputs (may prompt when interactive) ----
@@ -1348,15 +1558,17 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
1348
1558
  const baseUrl = deps.env.BAPI_BASE_URL ?? DEFAULT_BAPI_BASE_URL;
1349
1559
  const docsDir = deps.env.BAPI_DOCS_DIR ?? DEFAULT_BAPI_DOCS_DIR;
1350
1560
  // ---- Resolve the repository name ----
1351
- // Bootstrap-invite: unchanged (choose-a-name for the new project). Have-key:
1561
+ // Need-key (invite or self-serve): choose-a-name for the project this run is
1562
+ // about to create, so the prompt says exactly that. Have-key:
1352
1563
  // `--repo`/`BAPI_REPO_NAME` short-circuit deterministically; otherwise resolve
1353
1564
  // it server-side from the API key (BAPI-616), and on ANY non-resolution outcome
1354
1565
  // (unresolved / not-deployed / error) fall back to the existing local
1355
- // prompt/inference — never a hard failure.
1566
+ // prompt/inference — never a hard failure. That fallback keeps the
1567
+ // existing-registration wording: there the name must match a real project.
1356
1568
  let repoName;
1357
1569
  let attemptedServerResolution = false;
1358
1570
  if (bootstrapInviteMode) {
1359
- const repoResult = await resolveRepoName(options, deps);
1571
+ const repoResult = await resolveRepoName(options, deps, "new-project");
1360
1572
  if (!repoResult.ok) {
1361
1573
  errorLog(`Error: ${repoResult.error}`);
1362
1574
  return 1;
@@ -1387,7 +1599,7 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
1387
1599
  // Feature-detect + degrade: 404 (old server), 409 (unresolved/ambiguous/
1388
1600
  // client-scoped), and network/other errors all fall back to the existing
1389
1601
  // local resolution WITHOUT a cause-specific message or leaked detail.
1390
- const repoResult = await resolveRepoName(options, deps);
1602
+ const repoResult = await resolveRepoName(options, deps, "existing-registration");
1391
1603
  if (!repoResult.ok) {
1392
1604
  errorLog(`Error: ${repoResult.error}`);
1393
1605
  return 1;
@@ -1432,6 +1644,31 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
1432
1644
  log(line);
1433
1645
  return 0;
1434
1646
  }
1647
+ // ---- Materialize the Step 5 launch command BEFORE any install side effect ----
1648
+ // `spawnCommand` embeds the whole INSTALL_BRIDGE_AGENT_PROMPT and is multiple KB
1649
+ // — far past what osascript can type into a Terminal/iTerm tab, which is why the
1650
+ // spawn silently delivered a truncated line. It must travel via a launch script.
1651
+ //
1652
+ // This runs HERE, before the scaffold/mint/credential/config writes, because a
1653
+ // command that cannot be launched safely is a whole-run failure, not a Step 5
1654
+ // warning: the deterministic setup would otherwise complete and leave the user
1655
+ // in the "configured but never configured by the agent" state the warning below
1656
+ // explicitly calls out. Failing first means nothing is half-done.
1657
+ const materialized = await materializeWorkerLaunchCommand(deps.startTicketsDeps, "install", spawnCommand);
1658
+ if (!materialized.ok) {
1659
+ errorLog(`Error: ${materialized.error}`);
1660
+ return 1;
1661
+ }
1662
+ const launchCommand = materialized.command;
1663
+ // Independent final guard on the line ACTUALLY handed to the terminal — not on
1664
+ // the original command. It catches the two ways a "successful" materialization
1665
+ // can still be unlaunchable: no writer seam (inline command preserved verbatim),
1666
+ // and a runner whose script path is unexpectedly long.
1667
+ if (Buffer.byteLength(launchCommand, "utf8") >= MAX_TERMINAL_COMMAND_BYTES) {
1668
+ errorLog("Error: the agent session command is too long to send to the terminal safely. " +
1669
+ "Check that the system temporary directory is writable so the launch script can be used.");
1670
+ return 1;
1671
+ }
1435
1672
  const credentialWriteDeps = {
1436
1673
  env: deps.env,
1437
1674
  homedir: deps.homedir,
@@ -1721,10 +1958,17 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
1721
1958
  "'npx -y @bridge_gpt/mcp-server doctor'.");
1722
1959
  }
1723
1960
  }
1961
+ // ---- optional GitHub connect offer (BAPI-631) ----
1962
+ // Placed AFTER the credential is durable (the flow needs a resolvable key) and BEFORE
1963
+ // the agent spawn, for two reasons: the spawned session's capability report should
1964
+ // observe GitHub as configured if the user connects it here, and running it after the
1965
+ // spawn would put two prompts on the same terminal at once.
1966
+ await offerGithubConnection(repoName, deps, log);
1724
1967
  // ---- Step 5 — spawn a fresh agent session for the agentic remainder ----
1725
1968
  log(`Step 5/5 — opening a ${agent.name} session for /install-bridge configuration + capability report…`);
1726
1969
  const terminal = detectTerminal(undefined, deps.env);
1727
- const spawnResult = await deps.spawnTerminalTab(deps.startTicketsDeps, terminal, spawnCommand, {
1970
+ // Only the validated short runner reaches the terminal never the inline prompt.
1971
+ const spawnResult = await deps.spawnTerminalTab(deps.startTicketsDeps, terminal, launchCommand, {
1728
1972
  key: "install",
1729
1973
  worktreePath: deps.cwd,
1730
1974
  });
@@ -141,6 +141,27 @@ function summarizeIntegrations(body) {
141
141
  }
142
142
  return { total, unconfigured };
143
143
  }
144
+ /**
145
+ * Read `github_app.is_configured` from a manifest response (BAPI-631).
146
+ *
147
+ * Returns null when the entry is absent or is not a boolean — the caller must render
148
+ * that as SKIP rather than inventing a configured/unconfigured verdict. GitHub is
149
+ * legitimately absent from the checklist on a Bitbucket project.
150
+ */
151
+ function readGithubConfiguredFlag(body) {
152
+ if (!body || typeof body !== "object")
153
+ return null;
154
+ const integrations = body.integrations;
155
+ if (!Array.isArray(integrations))
156
+ return null;
157
+ for (const item of integrations) {
158
+ if (item?.id !== "github_app")
159
+ continue;
160
+ const configured = item.is_configured;
161
+ return typeof configured === "boolean" ? configured : null;
162
+ }
163
+ return null;
164
+ }
144
165
  /**
145
166
  * Collect the install-status checklist. Never throws; every failure path becomes
146
167
  * a WARN or SKIP check line. Performs at most three read-only GETs, and only
@@ -333,6 +354,49 @@ export async function collectInstallStatusChecks(deps) {
333
354
  detail: "manifest unavailable",
334
355
  });
335
356
  }
357
+ // --- GitHub connection (BAPI-631) ---
358
+ // Read-only, and derived from the SAME manifest response above — never from the
359
+ // nonce-scoped CLI status endpoint, which describes one transient connection attempt
360
+ // rather than the durable project binding doctor reports on. Doctor diagnoses; it
361
+ // never mints, syncs, confirms, opens a browser, writes a credential, or repairs.
362
+ if (manifest.ok && manifest.status === 200) {
363
+ const github = readGithubConfiguredFlag(manifest.body);
364
+ if (github === null) {
365
+ checks.push({
366
+ id: "github",
367
+ label: "GitHub connection",
368
+ status: "SKIP",
369
+ detail: "the manifest response carried no GitHub integration entry",
370
+ });
371
+ }
372
+ else if (github) {
373
+ checks.push({
374
+ id: "github",
375
+ label: "GitHub connection",
376
+ status: "PASS",
377
+ detail: "a GitHub repository is connected to this project",
378
+ });
379
+ }
380
+ else {
381
+ checks.push({
382
+ id: "github",
383
+ label: "GitHub connection",
384
+ status: "WARN",
385
+ detail: "no GitHub repository is connected to this project",
386
+ remediation: `run 'npx -y @bridge_gpt/mcp-server@latest connect-github --repo ${target.repoName}'.`,
387
+ });
388
+ }
389
+ }
390
+ else {
391
+ // Unavailable is NOT unconfigured: reporting either concrete state from a failed
392
+ // probe would be a fabricated finding.
393
+ checks.push({
394
+ id: "github",
395
+ label: "GitHub connection",
396
+ status: "SKIP",
397
+ detail: "manifest unavailable",
398
+ });
399
+ }
336
400
  // --- indexing ---
337
401
  const parse = await probeGet(deps, `${target.baseUrl}/jira/parse-status?${repoQuery}`, apiKey);
338
402
  if (!parse.ok || parse.status !== 200) {