@bridge_gpt/mcp-server 0.2.32 → 0.2.33

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.
@@ -22,6 +22,7 @@ const TERMINAL_STATUSES = new Set([
22
22
  "connected",
23
23
  "expired",
24
24
  "invalid",
25
+ "revoked",
25
26
  "verification-failed",
26
27
  "no-repositories",
27
28
  "conflict",
@@ -228,6 +229,108 @@ export async function confirmGithubConnection(deps, repoName, state, githubRepos
228
229
  },
229
230
  };
230
231
  }
232
+ const ALL_HANDOFF_STATUSES = new Set([
233
+ "pending",
234
+ "staged",
235
+ "awaiting-organization-approval",
236
+ "connected",
237
+ "expired",
238
+ "revoked",
239
+ "verification-failed",
240
+ "no-repositories",
241
+ "conflict",
242
+ "failed",
243
+ ]);
244
+ /**
245
+ * Mint a delegated handoff link.
246
+ *
247
+ * Deliberately parses only the URL and expiry: the backend does not return the state as
248
+ * a separate field, and nothing here should reconstruct one.
249
+ */
250
+ export async function mintGithubHandoff(deps, repoName) {
251
+ const res = await postJson(deps, "/setup/github/cli/handoff", {
252
+ repo_name: repoName,
253
+ });
254
+ if (!res.ok)
255
+ return res;
256
+ if (res.value.status !== 200) {
257
+ return { ok: false, kind: classifyStatus(res.value.status) };
258
+ }
259
+ const body = asRecord(res.value.body);
260
+ const installUrl = asString(body?.install_url);
261
+ const expiresAt = asString(body?.expires_at);
262
+ const ttlSeconds = body?.ttl_seconds;
263
+ if (!body || !installUrl || !expiresAt || typeof ttlSeconds !== "number") {
264
+ return { ok: false, kind: "malformed" };
265
+ }
266
+ return { ok: true, value: { installUrl, expiresAt, ttlSeconds } };
267
+ }
268
+ /** Strictly parse one handoff list item. Returns null for any malformed shape. */
269
+ function parseHandoffSnapshot(raw) {
270
+ const rec = asRecord(raw);
271
+ if (!rec)
272
+ return null;
273
+ const state = asString(rec.state);
274
+ const status = asString(rec.status);
275
+ if (!state || !status || !ALL_HANDOFF_STATUSES.has(status))
276
+ return null;
277
+ const candidates = parseCandidates(rec.candidates ?? []);
278
+ if (candidates === null)
279
+ return null;
280
+ return {
281
+ state,
282
+ status: status,
283
+ createdAt: asNullableString(rec.created_at),
284
+ expiresAt: asNullableString(rec.expires_at),
285
+ revokedAt: asNullableString(rec.revoked_at),
286
+ candidates,
287
+ githubRepoName: asNullableString(rec.github_repo_name),
288
+ };
289
+ }
290
+ /** Discover outstanding delegated handoffs for a project (the resume/revoke lookup). */
291
+ export async function listGithubHandoffs(deps, repoName) {
292
+ const res = await postJson(deps, "/setup/github/cli/handoffs", {
293
+ repo_name: repoName,
294
+ });
295
+ if (!res.ok)
296
+ return res;
297
+ if (res.value.status !== 200) {
298
+ return { ok: false, kind: classifyStatus(res.value.status) };
299
+ }
300
+ const body = asRecord(res.value.body);
301
+ const rawHandoffs = body?.handoffs;
302
+ if (!body || !Array.isArray(rawHandoffs)) {
303
+ return { ok: false, kind: "malformed" };
304
+ }
305
+ const out = [];
306
+ for (const raw of rawHandoffs) {
307
+ const parsed = parseHandoffSnapshot(raw);
308
+ // A malformed entry is a malformed RESPONSE, not an item to quietly drop: dropping
309
+ // it would silently shrink the admin's picker and could hide a live handoff.
310
+ if (!parsed)
311
+ return { ok: false, kind: "malformed" };
312
+ out.push(parsed);
313
+ }
314
+ return { ok: true, value: out };
315
+ }
316
+ /** Revoke one outstanding delegated handoff. The state travels in the body. */
317
+ export async function revokeGithubHandoff(deps, repoName, state) {
318
+ const res = await postJson(deps, "/setup/github/cli/revoke", {
319
+ repo_name: repoName,
320
+ state,
321
+ });
322
+ if (!res.ok)
323
+ return res;
324
+ if (res.value.status !== 200) {
325
+ return { ok: false, kind: classifyStatus(res.value.status) };
326
+ }
327
+ const body = asRecord(res.value.body);
328
+ const status = asString(body?.status);
329
+ if (!body || (status !== "revoked" && status !== "already-revoked")) {
330
+ return { ok: false, kind: "malformed" };
331
+ }
332
+ return { ok: true, value: status };
333
+ }
231
334
  /**
232
335
  * Read whether GitHub is already configured for a project.
233
336
  *
@@ -28,8 +28,8 @@ import { DEFAULT_BAPI_BASE_URL } from "./install-bridge.js";
28
28
  import { validateRepoName } from "./bridge-config.js";
29
29
  import { resolveStartTicketsRepoName } from "./start-tickets-repo.js";
30
30
  import { resolveBapiCredentials, } from "./credential-store.js";
31
- import { POLL_DEADLINE_MS, confirmGithubConnection, mintGithubConnection, pollGithubConnection, } from "./connect-github-api.js";
32
- const USAGE = `Usage: connect-github [--repo <repo_name>]
31
+ import { POLL_DEADLINE_MS, confirmGithubConnection, listGithubHandoffs, mintGithubConnection, mintGithubHandoff, pollGithubConnection, revokeGithubHandoff, } from "./connect-github-api.js";
32
+ const USAGE = `Usage: connect-github [--repo <repo_name>] [--handoff | --resume | --revoke]
33
33
 
34
34
  Connect a GitHub repository to a Bridge project from your terminal.
35
35
 
@@ -37,10 +37,24 @@ Opens the GitHub App install page in your browser, waits for you to install it,
37
37
  then asks which repository to connect. You are never asked for a GitHub token or
38
38
  password — you authenticate to GitHub in the browser.
39
39
 
40
+ If you do not administer the GitHub organization yourself, use the delegated
41
+ handoff: mint a link with --handoff, send it to whoever does, and finish with
42
+ --resume once they have completed their part.
43
+
40
44
  Options:
41
45
  --repo <repo_name> Bridge project to connect (inferred from this directory
42
46
  when omitted; you will be asked to confirm).
43
- --help Show this message.`;
47
+ --handoff Print a shareable install link (valid 72 hours) instead of
48
+ connecting here. Does not open a browser or wait.
49
+ --resume Look up an outstanding handoff and finish connecting it.
50
+ Works from any machine holding this project's API key.
51
+ --revoke Invalidate an outstanding handoff link.
52
+ --help Show this message.
53
+
54
+ --handoff, --resume, and --revoke are mutually exclusive. Minting a new link does
55
+ not invalidate an existing one — use --revoke for that.`;
56
+ /** The three delegated modes, for the mutual-exclusion check and its message. */
57
+ const DELEGATED_MODE_FLAGS = ["--handoff", "--resume", "--revoke"];
44
58
  /**
45
59
  * Parse argv. Deliberately strict — an unknown flag is an error, not something to
46
60
  * ignore. In particular there is NO `--yes` (every bind is confirmed by a human) and no
@@ -55,6 +69,18 @@ export function parseConnectGithubArgs(argv) {
55
69
  out.help = true;
56
70
  continue;
57
71
  }
72
+ if (arg === "--handoff") {
73
+ out.handoff = true;
74
+ continue;
75
+ }
76
+ if (arg === "--resume") {
77
+ out.resume = true;
78
+ continue;
79
+ }
80
+ if (arg === "--revoke") {
81
+ out.revoke = true;
82
+ continue;
83
+ }
58
84
  if (arg === "--repo") {
59
85
  const value = argv[i + 1];
60
86
  if (!value || value.startsWith("-")) {
@@ -91,6 +117,18 @@ export function parseConnectGithubArgs(argv) {
91
117
  }
92
118
  return { ok: false, error: `Unexpected argument: ${arg}` };
93
119
  }
120
+ // Mutually exclusive: each mode is a different operation on a different object, and
121
+ // guessing at a precedence order would silently do something the caller did not ask
122
+ // for. Refuse rather than pick.
123
+ const selected = DELEGATED_MODE_FLAGS.filter((flag) => (flag === "--handoff" && out.handoff) ||
124
+ (flag === "--resume" && out.resume) ||
125
+ (flag === "--revoke" && out.revoke));
126
+ if (selected.length > 1) {
127
+ return {
128
+ ok: false,
129
+ error: `Choose only one of ${DELEGATED_MODE_FLAGS.join(", ")} (got ${selected.join(", ")}).`,
130
+ };
131
+ }
94
132
  return { ok: true, value: out };
95
133
  }
96
134
  /** Echoed single-line prompt on stderr. */
@@ -195,6 +233,28 @@ export async function resolveConnectGithubRepoName(args, deps) {
195
233
  const validated = validateRepoName(chosen);
196
234
  return validated.ok ? { ok: true, value: validated.value } : { ok: false, error: validated.error };
197
235
  }
236
+ /**
237
+ * Resolve the project for non-interactive `--handoff` (BAPI-686).
238
+ *
239
+ * Synchronous and prompt-free, because mint mode may legitimately run without a TTY.
240
+ * That removes the interactive confirmation that makes inference safe in the default
241
+ * flow, so this deliberately does NOT fall back to guessing from the directory name:
242
+ * either `--repo` was given, or the caller is told to give it. Silently minting a live
243
+ * bearer link against the wrong project is a worse outcome than an error message.
244
+ */
245
+ export function resolveHandoffRepoName(args, _deps) {
246
+ if (!args.repo) {
247
+ return {
248
+ ok: false,
249
+ error: "connect-github --handoff needs an explicit project: pass --repo <repo_name>. " +
250
+ "It cannot ask you to confirm an inferred name when run non-interactively.",
251
+ };
252
+ }
253
+ const validated = validateRepoName(args.repo);
254
+ return validated.ok
255
+ ? { ok: true, value: validated.value }
256
+ : { ok: false, error: validated.error };
257
+ }
198
258
  // ---------------------------------------------------------------------------
199
259
  // Browser handoff
200
260
  // ---------------------------------------------------------------------------
@@ -262,6 +322,47 @@ function reportNoConnection(deps, detail) {
262
322
  deps.stderr(detail);
263
323
  return 1;
264
324
  }
325
+ /**
326
+ * Ask which repository to bind. Returns null when the user declined or chose nothing.
327
+ *
328
+ * Extracted from `runGithubConnectionFlow` (BAPI-686) so the delegated `--resume` path
329
+ * reuses this exact interaction rather than growing a parallel copy that could drift
330
+ * from it. Behavior — the single-candidate `[y/N]`, the numbered picker, the absence of
331
+ * a default, and every message — is unchanged from BAPI-631.
332
+ */
333
+ async function chooseCandidate(deps, candidates) {
334
+ if (candidates.length === 1) {
335
+ const only = candidates[0];
336
+ // Even with one option: show the full identity and require a yes. The user is
337
+ // authorizing a binding, not acknowledging a notice.
338
+ const answer = (await deps.promptLine(`Connect ${candidateLabel(only)}? [y/N]: `))
339
+ .trim()
340
+ .toLowerCase();
341
+ if (answer !== "y" && answer !== "yes") {
342
+ deps.stderr("");
343
+ deps.stderr("No GitHub connection was made. Nothing was changed.");
344
+ return null;
345
+ }
346
+ return only;
347
+ }
348
+ deps.stderr("");
349
+ deps.stderr("Your GitHub installation includes multiple repositories:");
350
+ candidates.forEach((c, i) => {
351
+ deps.stderr(` ${String(i + 1).padStart(2, " ")}. ${candidateLabel(c)}`);
352
+ });
353
+ deps.stderr("");
354
+ // No default: a stray Enter must not bind anything.
355
+ const answer = (await deps.promptLine(`Choose a repository [1-${candidates.length}]: `)).trim();
356
+ const index = Number(answer);
357
+ if (!/^\d+$/.test(answer) || !Number.isInteger(index) || index < 1 || index > candidates.length) {
358
+ deps.stderr("");
359
+ deps.stderr("No GitHub connection was made. No repository was selected.");
360
+ return null;
361
+ }
362
+ const selected = candidates[index - 1];
363
+ deps.stderr(`Selected ${candidateLabel(selected)}.`);
364
+ return selected;
365
+ }
265
366
  export async function runGithubConnectionFlow(deps, api, repoName) {
266
367
  renderStep(deps, 0);
267
368
  const minted = await mintGithubConnection(api, repoName);
@@ -313,39 +414,9 @@ export async function runGithubConnectionFlow(deps, api, repoName) {
313
414
  return reportNoConnection(deps, OUTCOME_MESSAGES["no-repositories"]);
314
415
  }
315
416
  renderStep(deps, 3);
316
- let selected = null;
317
- if (candidates.length === 1) {
318
- const only = candidates[0];
319
- // Even with one option: show the full identity and require a yes. The user is
320
- // authorizing a binding, not acknowledging a notice.
321
- const answer = (await deps.promptLine(`Connect ${candidateLabel(only)}? [y/N]: `))
322
- .trim()
323
- .toLowerCase();
324
- if (answer !== "y" && answer !== "yes") {
325
- deps.stderr("");
326
- deps.stderr("No GitHub connection was made. Nothing was changed.");
327
- return 1;
328
- }
329
- selected = only;
330
- }
331
- else {
332
- deps.stderr("");
333
- deps.stderr("Your GitHub installation includes multiple repositories:");
334
- candidates.forEach((c, i) => {
335
- deps.stderr(` ${String(i + 1).padStart(2, " ")}. ${candidateLabel(c)}`);
336
- });
337
- deps.stderr("");
338
- // No default: a stray Enter must not bind anything.
339
- const answer = (await deps.promptLine(`Choose a repository [1-${candidates.length}]: `)).trim();
340
- const index = Number(answer);
341
- if (!/^\d+$/.test(answer) || !Number.isInteger(index) || index < 1 || index > candidates.length) {
342
- deps.stderr("");
343
- deps.stderr("No GitHub connection was made. No repository was selected.");
344
- return 1;
345
- }
346
- selected = candidates[index - 1];
347
- deps.stderr(`Selected ${candidateLabel(selected)}.`);
348
- }
417
+ const selected = await chooseCandidate(deps, candidates);
418
+ if (!selected)
419
+ return 1;
349
420
  renderStep(deps, 4);
350
421
  const confirmed = await confirmGithubConnection(api, repoName, minted.value.state, selected.github_repository_id);
351
422
  if (!confirmed.ok) {
@@ -365,6 +436,207 @@ export async function runGithubConnectionFlow(deps, api, repoName) {
365
436
  return 0;
366
437
  }
367
438
  // ---------------------------------------------------------------------------
439
+ // BAPI-686: delegated handoff modes
440
+ //
441
+ // Three self-contained flows that share nothing with `runGithubConnectionFlow` except
442
+ // the API transport and the candidate picker. The default flow above is deliberately
443
+ // untouched (AC-7): it still opens the browser itself and still never emits a URL.
444
+ // ---------------------------------------------------------------------------
445
+ /**
446
+ * The one place in this file permitted to write a bearer URL to stdout.
447
+ *
448
+ * `openGithubInstallPage`'s no-print contract is what keeps the *default* flow's nonce
449
+ * out of the user's scrollback, and it stays intact — mint mode never calls it. Here the
450
+ * URL is the entire deliverable: the admin has to be able to copy it and send it on. It
451
+ * goes to stdout only (never stderr, never a log, never an error path), so it can be
452
+ * piped without dragging the surrounding narration along with it.
453
+ */
454
+ export async function runGithubHandoffMintFlow(deps, api, repoName) {
455
+ const minted = await mintGithubHandoff(api, repoName);
456
+ if (!minted.ok) {
457
+ return reportNoHandoff(deps, FAILURE_MESSAGES[minted.kind]);
458
+ }
459
+ deps.stdout(minted.value.installUrl);
460
+ deps.stderr("");
461
+ deps.stderr(`Send this link to whoever administers your GitHub organization.`);
462
+ deps.stderr(`It works once and expires ${formatExpiry(minted.value.expiresAt)}.`);
463
+ deps.stderr("They'll finish in their browser — ask them to ping you, then run " +
464
+ "`connect-github --resume`.");
465
+ return 0;
466
+ }
467
+ /** Render an expiry for humans without pretending to know the reader's locale. */
468
+ function formatExpiry(iso) {
469
+ const parsed = Date.parse(iso);
470
+ if (Number.isNaN(parsed))
471
+ return "in 72 hours";
472
+ return `on ${new Date(parsed).toISOString().replace("T", " ").slice(0, 16)} UTC`;
473
+ }
474
+ /** The no-handoff framing, mirroring `reportNoConnection` for the delegated modes. */
475
+ function reportNoHandoff(deps, detail) {
476
+ deps.stderr("");
477
+ deps.stderr("No handoff link was created.");
478
+ deps.stderr(detail);
479
+ return 1;
480
+ }
481
+ /** Statuses a handoff can still be revoked from. Anything else has nothing to withdraw. */
482
+ const REVOCABLE_STATUSES = new Set([
483
+ "pending",
484
+ "awaiting-organization-approval",
485
+ "staged",
486
+ ]);
487
+ /** One fixed, actionable line per non-staged handoff status. Never any upstream detail. */
488
+ const HANDOFF_STATUS_MESSAGES = {
489
+ pending: "Nobody has completed the link yet. Ask them to open it, then run connect-github --resume again.",
490
+ expired: "The link expired before it was used. Run connect-github --handoff to create a new one.",
491
+ revoked: "This link was revoked. Run connect-github --handoff to create a new one.",
492
+ "verification-failed": "Bridge could not verify the GitHub installation. Run connect-github --handoff to create a new link.",
493
+ "no-repositories": "The GitHub App installation did not include any repositories Bridge can access. " +
494
+ "Create a new link with connect-github --handoff and ask them to grant access to at least one repository.",
495
+ conflict: "This GitHub installation or project is already connected to a different Bridge " +
496
+ "account or repository. Contact support if that is unexpected.",
497
+ failed: "The GitHub install did not complete. Run connect-github --handoff to create a new link.",
498
+ };
499
+ /**
500
+ * Label one handoff for the selection prompt.
501
+ *
502
+ * Uses ONLY status and timestamps. The state never appears — it is a bearer credential,
503
+ * and printing it into a picker would paste it into the user's scrollback for no reason.
504
+ */
505
+ function handoffLabel(h) {
506
+ const created = h.createdAt ? ` · created ${h.createdAt.slice(0, 16).replace("T", " ")}` : "";
507
+ return `${h.status}${created}`;
508
+ }
509
+ /**
510
+ * Choose among discovered handoffs.
511
+ *
512
+ * A single candidate is returned without a prompt: unlike a repository *binding*, which
513
+ * always requires an explicit yes, selecting which handoff to inspect changes nothing on
514
+ * its own — the binding confirmation still happens downstream.
515
+ */
516
+ export async function selectDelegatedHandoff(deps, handoffs) {
517
+ if (handoffs.length === 0)
518
+ return null;
519
+ if (handoffs.length === 1)
520
+ return handoffs[0];
521
+ deps.stderr("");
522
+ deps.stderr("Outstanding handoff links:");
523
+ handoffs.forEach((h, i) => {
524
+ deps.stderr(` ${String(i + 1).padStart(2, " ")}. ${handoffLabel(h)}`);
525
+ });
526
+ deps.stderr("");
527
+ // No default: a stray Enter must not pick one.
528
+ const answer = (await deps.promptLine(`Choose a handoff [1-${handoffs.length}]: `)).trim();
529
+ const index = Number(answer);
530
+ if (!/^\d+$/.test(answer) || !Number.isInteger(index) || index < 1 || index > handoffs.length) {
531
+ return null;
532
+ }
533
+ return handoffs[index - 1];
534
+ }
535
+ /**
536
+ * Look up an outstanding handoff and continue it (AC-4, AC-6).
537
+ *
538
+ * Status-only, never polling: a delegated handoff waits on a person who may take days,
539
+ * so blocking a terminal on it would be useless. The admin re-runs this when they are
540
+ * told the other half is done.
541
+ */
542
+ export async function runGithubHandoffResumeFlow(deps, api, repoName) {
543
+ const listed = await listGithubHandoffs(api, repoName);
544
+ if (!listed.ok) {
545
+ return reportNoConnection(deps, FAILURE_MESSAGES[listed.kind]);
546
+ }
547
+ if (listed.value.length === 0) {
548
+ return reportNoConnection(deps, "No handoff links exist for this project. Run connect-github --handoff to create one.");
549
+ }
550
+ const selected = await selectDelegatedHandoff(deps, listed.value);
551
+ if (!selected) {
552
+ return reportNoConnection(deps, "No handoff was selected.");
553
+ }
554
+ if (selected.status === "connected") {
555
+ deps.stdout(`Connected ${selected.githubRepoName ?? repoName}.`);
556
+ return 0;
557
+ }
558
+ if (selected.status === "awaiting-organization-approval") {
559
+ // R-6: this is genuinely in-flight, not a failure — the link stays live until its
560
+ // deadline, and approval may still land. Reported as a live state (exit 0) so a
561
+ // scripted caller does not treat "waiting on a human" as an error.
562
+ deps.stderr("");
563
+ deps.stderr("The install was sent to a GitHub organization owner for approval, and that " +
564
+ "approval has not happened yet.");
565
+ deps.stderr("GitHub does not notify Bridge when it lands, so re-run connect-github --resume " +
566
+ "once the owner has approved it.");
567
+ if (selected.expiresAt) {
568
+ deps.stderr(`The link stays usable until ${formatExpiry(selected.expiresAt)}.`);
569
+ }
570
+ return 0;
571
+ }
572
+ if (selected.status !== "staged") {
573
+ const message = HANDOFF_STATUS_MESSAGES[selected.status] ?? HANDOFF_STATUS_MESSAGES.failed;
574
+ if (selected.status === "pending" || selected.status === "expired") {
575
+ // AC-8 fallback. Bridge cannot safely discover an existing installation on an org
576
+ // it was never called back about, so this deliberately does NOT claim to have
577
+ // detected one — it points at the documented manual path instead.
578
+ deps.stderr("");
579
+ deps.stderr(message);
580
+ deps.stderr("If the GitHub App is already installed on that organization, GitHub may not " +
581
+ "send Bridge a callback at all. In that case follow the manual installation-ID " +
582
+ "steps in the GitHub App setup guide (docs/install/github-app.md).");
583
+ // `pending` is a live state, not a failure; `expired` is terminal.
584
+ return selected.status === "pending" ? 0 : 1;
585
+ }
586
+ return reportNoConnection(deps, message);
587
+ }
588
+ // Staged: reuse the existing candidate flow verbatim — same labels, same picker, same
589
+ // explicit [y/N] on a single candidate. A delegated handoff gets no auto-bind shortcut.
590
+ const candidates = selected.candidates;
591
+ if (candidates.length === 0) {
592
+ return reportNoConnection(deps, OUTCOME_MESSAGES["no-repositories"]);
593
+ }
594
+ const chosen = await chooseCandidate(deps, candidates);
595
+ if (!chosen)
596
+ return 1;
597
+ const confirmed = await confirmGithubConnection(api, repoName, selected.state, chosen.github_repository_id);
598
+ if (!confirmed.ok) {
599
+ return reportNoConnection(deps, FAILURE_MESSAGES[confirmed.kind]);
600
+ }
601
+ deps.stdout(`Connected ${confirmed.value.githubRepoFullName ?? confirmed.value.githubRepoName}.`);
602
+ if (confirmed.value.indexingStatus === "started") {
603
+ deps.stdout("Indexing started automatically.");
604
+ }
605
+ else if (confirmed.value.indexingStatus === "waiting-for-setup") {
606
+ deps.stdout("Indexing will start automatically once setup completes.");
607
+ }
608
+ return 0;
609
+ }
610
+ /** Invalidate an outstanding handoff (R-5). */
611
+ export async function runGithubHandoffRevokeFlow(deps, api, repoName) {
612
+ const listed = await listGithubHandoffs(api, repoName);
613
+ if (!listed.ok) {
614
+ return reportNoRevocation(deps, FAILURE_MESSAGES[listed.kind]);
615
+ }
616
+ const revocable = listed.value.filter((h) => REVOCABLE_STATUSES.has(h.status));
617
+ if (revocable.length === 0) {
618
+ return reportNoRevocation(deps, "There are no handoff links left to revoke.");
619
+ }
620
+ const selected = await selectDelegatedHandoff(deps, revocable);
621
+ if (!selected) {
622
+ return reportNoRevocation(deps, "No handoff was selected.");
623
+ }
624
+ const revoked = await revokeGithubHandoff(api, repoName, selected.state);
625
+ if (!revoked.ok) {
626
+ return reportNoRevocation(deps, FAILURE_MESSAGES[revoked.kind]);
627
+ }
628
+ deps.stdout(revoked.value === "already-revoked"
629
+ ? "That handoff link was already revoked."
630
+ : "Handoff link revoked. It can no longer be used.");
631
+ return 0;
632
+ }
633
+ function reportNoRevocation(deps, detail) {
634
+ deps.stderr("");
635
+ deps.stderr("Nothing was revoked.");
636
+ deps.stderr(detail);
637
+ return 1;
638
+ }
639
+ // ---------------------------------------------------------------------------
368
640
  // Process boundary
369
641
  // ---------------------------------------------------------------------------
370
642
  /**
@@ -386,14 +658,22 @@ export async function runConnectGithubCli(argv, injected) {
386
658
  deps.stdout(USAGE);
387
659
  return 0;
388
660
  }
389
- // Checked BEFORE minting: this flow always ends in a human choice, so a
661
+ const mintOnly = parsed.value.handoff;
662
+ // Checked BEFORE minting: these flows always end in a human choice, so a
390
663
  // non-interactive run can only ever strand a code it can never confirm.
391
- if (!deps.isTTY) {
664
+ //
665
+ // `--handoff` is the one exception (BAPI-686), and only because the premise no
666
+ // longer holds for it: it ends at a printed URL, not at a prompt, and it strands
667
+ // nothing because handoffs are discoverable server-side afterwards via --resume.
668
+ // --resume and --revoke both still prompt, so they keep the gate.
669
+ if (!deps.isTTY && !mintOnly) {
392
670
  deps.stderr("connect-github needs an interactive terminal: it asks you to confirm which " +
393
671
  "repository to connect. Run it directly in your terminal.");
394
672
  return 1;
395
673
  }
396
- const repo = await resolveConnectGithubRepoName(parsed.value, deps);
674
+ const repo = mintOnly
675
+ ? resolveHandoffRepoName(parsed.value, deps)
676
+ : await resolveConnectGithubRepoName(parsed.value, deps);
397
677
  if (!repo.ok) {
398
678
  deps.stderr(repo.error);
399
679
  return 1;
@@ -415,6 +695,12 @@ export async function runConnectGithubCli(argv, injected) {
415
695
  baseUrl: deps.env.BAPI_BASE_URL?.trim() || DEFAULT_BAPI_BASE_URL,
416
696
  apiKey: cred.credentials.apiKey,
417
697
  };
698
+ if (parsed.value.handoff)
699
+ return await runGithubHandoffMintFlow(deps, api, repo.value);
700
+ if (parsed.value.resume)
701
+ return await runGithubHandoffResumeFlow(deps, api, repo.value);
702
+ if (parsed.value.revoke)
703
+ return await runGithubHandoffRevokeFlow(deps, api, repo.value);
418
704
  return await runGithubConnectionFlow(deps, api, repo.value);
419
705
  }
420
706
  catch {
package/build/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- var __defProp=Object.defineProperty;var __getOwnPropNames=Object.getOwnPropertyNames;var __esm=(fn,res,err)=>function(){if(err)throw err[0];try{return fn&&(res=(0,fn[__getOwnPropNames(fn)[0]])(fn=0)),res}catch(e){throw err=[e],e}};var __export=(target,all)=>{for(var name in all)__defProp(target,name,{get:all[name],enumerable:!0})};var VERSION,init_version_generated=__esm({"src/version.generated.ts"(){"use strict";VERSION="0.2.32"}});var COMMANDS,init_commands_generated=__esm({"src/commands.generated.ts"(){"use strict";COMMANDS={"bridge-research.md":`Run multi-source, fact-checked web research via Bridge API and save a cited report locally.
2
+ var __defProp=Object.defineProperty;var __getOwnPropNames=Object.getOwnPropertyNames;var __esm=(fn,res,err)=>function(){if(err)throw err[0];try{return fn&&(res=(0,fn[__getOwnPropNames(fn)[0]])(fn=0)),res}catch(e){throw err=[e],e}};var __export=(target,all)=>{for(var name in all)__defProp(target,name,{get:all[name],enumerable:!0})};var VERSION,init_version_generated=__esm({"src/version.generated.ts"(){"use strict";VERSION="0.2.33"}});var COMMANDS,init_commands_generated=__esm({"src/commands.generated.ts"(){"use strict";COMMANDS={"bridge-research.md":`Run multi-source, fact-checked web research via Bridge API and save a cited report locally.
3
3
 
4
4
  $ARGUMENTS
5
5
 
@@ -4765,7 +4765,7 @@ Detail: ${errorDetail(err)}`):deps.errorLog(`Failed to approve the plan: ${error
4765
4765
  `:`
4766
4766
 
4767
4767
  `;return{action:"append",content:content+boundary+renderCodexBridgeToml(entry)}}async function readTomlHostConfig(path39,deps){try{return{state:"text",content:await deps.readFile(path39)}}catch(err){return isEnoent(err)?{state:"missing"}:{state:"read-error",message:"config could not be read"}}}async function writeCodexHostConfig(path39,entry,deps){let read=await readTomlHostConfig(path39,deps),merge=mergeCodexHostConfig(read,entry);return merge.action==="manual-required"?{status:"manual-required"}:(await deps.mkdir(dirnameOf(path39),{recursive:!0}),await deps.writeFile(path39,merge.content),{status:merge.action==="create"?"created":"direct-written",path:path39})}async function inspectJsonHostEntry(path39,target,deps){let read=await readJsonHostConfig(path39,deps);if(read.state!=="valid")return{present:!1};let root=read.value[target.topLevelKey];if(!root||typeof root!="object"||Array.isArray(root))return{present:!1};let entry=root["bridge-api"];if(!entry||typeof entry!="object")return{present:!1};let args=entry.args;return{present:!0,args:Array.isArray(args)?args.filter(a=>typeof a=="string"):void 0}}async function inspectTomlHostEntry(path39,deps){let read=await readTomlHostConfig(path39,deps);if(read.state!=="text")return{present:!1};if(!codexHasBridgeTable(read.content))return{present:!1};let m=read.content.match(/^\s*args\s*=\s*\[(.*?)\]/m),args;return m&&(args=m[1].split(",").map(s=>s.trim().replace(/^["']|["']$/g,"")).filter(s=>s.length>0)),{present:!0,args}}async function inspectHostEntry(target,ctx,deps){let path39=resolveTargetAbsPath(target,ctx);return target.format==="toml"?inspectTomlHostEntry(path39,deps):inspectJsonHostEntry(path39,target,deps)}function isVendorContractVerified(kind){return kind==="claude-add-json"||kind==="copilot-add"}function buildVendorInvocation(target,entry){let vendor=target.vendorCli;if(!vendor||!isVendorContractVerified(vendor.kind))return null;if(vendor.kind==="claude-add-json"){let json=JSON.stringify({command:entry.command,args:entry.args,env:entry.env});return{bin:vendor.bin,args:["mcp","add-json","bridge-api",json,"--scope","project"]}}if(vendor.kind==="copilot-add"){let envArgs=[];for(let[k,v]of Object.entries(entry.env))envArgs.push("--env",`${k}=${v}`);return{bin:vendor.bin,args:["mcp","add","bridge-api","--tools","*",...envArgs,"--",entry.command,...entry.args]}}return null}function outcome(target,status,detail){return{platform:target.id,status,displayPath:target.displayPath,detail}}async function directWriteTarget(target,entry,deps){let path39=resolveTargetAbsPath(target,deps);if(target.format==="toml")return(await writeCodexHostConfig(path39,entry,deps.fs)).status==="manual-required"?outcome(target,"manual-required","existing config could not be safely updated"):outcome(target,"direct-written");let adapted=adaptBridgeEntryForHostTarget(entry,target),res=await writeJsonHostConfig(path39,target,adapted,deps.fs);return res.status==="skipped-invalid"?outcome(target,"skipped-invalid",res.message):outcome(target,"direct-written")}async function provisionHostTarget(target,entry,deps){if(target.writeStrategy==="manual-instructions")return outcome(target,"manual-required","global config \u2014 manual setup");if(target.writeStrategy==="direct")return directWriteTarget(target,entry,deps);let invocation=buildVendorInvocation(target,entry);return invocation&&await deps.vendor.probeBinary(invocation.bin,deps.env)&&(await deps.vendor.invokeVendorAdd(invocation,deps.env)).ok&&(await inspectHostEntry(target,deps,deps.fs)).present?outcome(target,"vendor-written"):directWriteTarget(target,entry,deps)}var PROBE_SECRET_KEYS=["BAPI_API_KEY","BAPI_INVITE","BAPI_SIGNUP_EMAIL"];function sanitizeProbeEnv(env){let clone={...env};for(let key of PROBE_SECRET_KEYS)delete clone[key];return clone}var VENDOR_PROCESS_TIMEOUT_MS=8e3;function createDefaultVendorProcessDeps(spawnFn){return{probeBinary:(bin,env)=>new Promise(resolve2=>{let settled=!1,done=ok=>{settled||(settled=!0,resolve2(ok))};try{let child=spawnFn(bin,["--version"],{stdio:"ignore",shell:!1,env:sanitizeProbeEnv(env),timeout:VENDOR_PROCESS_TIMEOUT_MS});child.on("error",()=>done(!1)),child.on("close",code=>done(code===0))}catch{done(!1)}}),invokeVendorAdd:(invocation,env)=>new Promise(resolve2=>{let settled=!1,done=ok=>{settled||(settled=!0,resolve2({ok}))};try{let child=spawnFn(invocation.bin,invocation.args,{stdio:"ignore",shell:!1,env:sanitizeProbeEnv(env),timeout:VENDOR_PROCESS_TIMEOUT_MS});child.on("error",()=>done(!1)),child.on("close",code=>done(code===0))}catch{done(!1)}})}}var MCP_INSTALL_STATE_VERSION=1,MCP_INSTALL_STATE_RELPATH=".bridge/install-state.json";function joinCwd(cwd,rel){return`${cwd.endsWith("/")?cwd.slice(0,-1):cwd}/${rel}`}function installStatePath(cwd){return joinCwd(cwd,MCP_INSTALL_STATE_RELPATH)}function installStateTempPath(cwd){return joinCwd(cwd,`${MCP_INSTALL_STATE_RELPATH}.tmp`)}function bridgeDirPath(cwd){return joinCwd(cwd,".bridge")}function normalizePlatforms(ids){let wanted=new Set(ids);return HOST_PLATFORM_ORDER.filter(id=>wanted.has(id))}function normalizeProjectPaths(paths){let seen=new Set,out=[];for(let p of paths)typeof p=="string"&&p.length>0&&!seen.has(p)&&(seen.add(p),out.push(p));return out.sort(),out}function serializeMcpInstallState(state){let ordered={version:state.version,selectedPlatforms:state.selectedPlatforms,projectConfigPaths:state.projectConfigPaths};return JSON.stringify(ordered,null,2)+`
4768
- `}async function writeMcpInstallState(cwd,input,deps){let state={version:MCP_INSTALL_STATE_VERSION,selectedPlatforms:normalizePlatforms(input.selectedPlatforms),projectConfigPaths:normalizeProjectPaths(input.projectConfigPaths)},finalPath=installStatePath(cwd),tempPath=installStateTempPath(cwd),serialized=serializeMcpInstallState(state);try{await deps.mkdir(bridgeDirPath(cwd),{recursive:!0}),await deps.writeFile(tempPath,serialized),await deps.rename(tempPath,finalPath)}catch(err){if(deps.unlink)try{await deps.unlink(tempPath)}catch{}return{ok:!1,error:`failed to persist install state: ${err instanceof Error?err.message:String(err)}`}}return{ok:!0,path:finalPath,state}}init_git_ignore_utils();init_start_tickets_repo();init_credential_store();var TERMINAL_STATUSES=new Set(["staged","awaiting-organization-approval","connected","expired","invalid","verification-failed","no-repositories","conflict","failed"]),ALL_STATUSES=new Set(["waiting",...TERMINAL_STATUSES]),KNOWN_INDEXING_STATUSES=new Set(["started","waiting-for-setup"]),REQUEST_TIMEOUT_MS=15e3;async function postJson(deps,path39,payload){let resp;try{resp=await deps.fetch(`${deps.baseUrl}${path39}`,{method:"POST",headers:{"Content-Type":"application/json","X-API-Key":deps.apiKey},body:JSON.stringify(payload),signal:AbortSignal.timeout(REQUEST_TIMEOUT_MS)})}catch(e){return{ok:!1,kind:e instanceof Error&&(e.name==="TimeoutError"||e.name==="AbortError")?"timeout":"network"}}let body=null;try{body=await resp.json()}catch{}return{ok:!0,value:{status:resp.status,body,retryAfter:resp.headers.get("Retry-After")}}}function classifyStatus(status){return status===401||status===403?"unauthorized":status===404?"not-found":"server"}function asRecord(value){return value&&typeof value=="object"&&!Array.isArray(value)?value:null}function asString3(value){return typeof value=="string"&&value.length>0?value:null}function asNullableString(value){return typeof value=="string"&&value.length>0?value:null}function parseRetryAfterMs(header,remainingMs,nowMs){if(!header)return null;let trimmed=header.trim();if(!trimmed)return null;let clamp=ms=>!Number.isFinite(ms)||ms<0?null:Math.min(ms,Math.max(0,remainingMs));if(/^\d+$/.test(trimmed))return clamp(Number(trimmed)*1e3);let parsed=Date.parse(trimmed);return Number.isNaN(parsed)?null:clamp(parsed-nowMs)}async function mintGithubConnection(deps,repoName){let res=await postJson(deps,"/setup/github/cli/connection-code",{repo_name:repoName});if(!res.ok)return res;if(res.value.status!==200)return{ok:!1,kind:classifyStatus(res.value.status)};let body=asRecord(res.value.body),state=asString3(body?.state),installUrl=asString3(body?.install_url),ttlSeconds=body?.ttl_seconds;return!body||!state||!installUrl||typeof ttlSeconds!="number"?{ok:!1,kind:"malformed"}:{ok:!0,value:{state,installUrl,ttlSeconds}}}function parseCandidates(value){if(!Array.isArray(value))return null;let out=[];for(let raw of value){let rec=asRecord(raw),id=asString3(rec?.github_repository_id),name=asString3(rec?.github_repo_name);if(!rec||!id||!name)return null;out.push({github_repository_id:id,github_repo_name:name,github_repo_full_name:asNullableString(rec.github_repo_full_name),owner:asNullableString(rec.owner)})}return out}async function confirmGithubConnection(deps,repoName,state,githubRepositoryId){let res=await postJson(deps,"/setup/github/cli/confirm",{repo_name:repoName,state,github_repository_id:githubRepositoryId});if(!res.ok)return res;if(res.value.status!==200)return{ok:!1,kind:classifyStatus(res.value.status)};let body=asRecord(res.value.body),name=asString3(body?.github_repo_name);if(!body||body.status!=="connected"||!name)return{ok:!1,kind:"malformed"};let rawIndexingStatus=body.indexing_status,indexingStatus=typeof rawIndexingStatus=="string"&&KNOWN_INDEXING_STATUSES.has(rawIndexingStatus)?rawIndexingStatus:null;return{ok:!0,value:{githubRepoName:name,githubRepoFullName:asNullableString(body.github_repo_full_name),indexingStatus}}}async function fetchGithubConfigurationState(deps,repoName){let resp;try{resp=await deps.fetch(`${deps.baseUrl}/jira/config/install-manifest?repo_name=${encodeURIComponent(repoName)}`,{headers:{"X-API-Key":deps.apiKey},signal:AbortSignal.timeout(REQUEST_TIMEOUT_MS)})}catch{return"unavailable"}if(resp.status!==200)return"unavailable";let body;try{body=await resp.json()}catch{return"unavailable"}let integrations=asRecord(body)?.integrations;if(!Array.isArray(integrations))return"unavailable";for(let raw of integrations){let rec=asRecord(raw);if(rec?.id==="github_app")return typeof rec.is_configured!="boolean"?"unavailable":rec.is_configured?"configured":"unconfigured"}return"unavailable"}var POLL_DEADLINE_MS=15.5*60*1e3,POLL_DELAYS_MS=[2e3,3e3,5e3],MAX_JITTER_MS=400,RETRYABLE_TRANSPORT=new Set(["network","timeout"]);function isRetryableStatus(status){return status===429||status>=500}async function pollGithubConnection(deps,poll,repoName,state){let started=poll.now(),attempt=0;for(;;){let elapsed=poll.now()-started,remaining=POLL_DEADLINE_MS-elapsed;if(remaining<=0)return{ok:!1,kind:"deadline"};let res=await postJson(deps,"/setup/github/cli/status",{repo_name:repoName,state}),waitMs=null;if(res.ok)if(res.value.status===200){let body=asRecord(res.value.body),status=asString3(body?.status);if(!body||!status||!ALL_STATUSES.has(status))return{ok:!1,kind:"malformed"};let candidates=parseCandidates(body.candidates??[]);if(candidates===null)return{ok:!1,kind:"malformed"};let typed=status;if(TERMINAL_STATUSES.has(typed))return{ok:!0,value:{status:typed,candidates,githubRepoName:asNullableString(body.github_repo_name),retryAfterMs:null}};waitMs=parseRetryAfterMs(res.value.retryAfter,remaining,poll.now())}else if(isRetryableStatus(res.value.status))waitMs=parseRetryAfterMs(res.value.retryAfter,remaining,poll.now());else return{ok:!1,kind:classifyStatus(res.value.status)};else if(!RETRYABLE_TRANSPORT.has(res.kind))return{ok:!1,kind:res.kind};waitMs===null&&(waitMs=POLL_DELAYS_MS[Math.min(attempt,POLL_DELAYS_MS.length-1)]+Math.floor(poll.jitter()*MAX_JITTER_MS)),attempt+=1;let capped=Math.min(waitMs,Math.max(0,POLL_DEADLINE_MS-(poll.now()-started)));if(capped<=0)return{ok:!1,kind:"deadline"};await poll.sleep(capped)}}import{readFile as readFile10,stat as stat7}from"fs/promises";import{spawn as spawn6}from"child_process";import os13 from"os";import path27 from"path";import readline2 from"readline";init_bridge_config();init_start_tickets_repo();init_credential_store();var USAGE2=`Usage: connect-github [--repo <repo_name>]
4768
+ `}async function writeMcpInstallState(cwd,input,deps){let state={version:MCP_INSTALL_STATE_VERSION,selectedPlatforms:normalizePlatforms(input.selectedPlatforms),projectConfigPaths:normalizeProjectPaths(input.projectConfigPaths)},finalPath=installStatePath(cwd),tempPath=installStateTempPath(cwd),serialized=serializeMcpInstallState(state);try{await deps.mkdir(bridgeDirPath(cwd),{recursive:!0}),await deps.writeFile(tempPath,serialized),await deps.rename(tempPath,finalPath)}catch(err){if(deps.unlink)try{await deps.unlink(tempPath)}catch{}return{ok:!1,error:`failed to persist install state: ${err instanceof Error?err.message:String(err)}`}}return{ok:!0,path:finalPath,state}}init_git_ignore_utils();init_start_tickets_repo();init_credential_store();var TERMINAL_STATUSES=new Set(["staged","awaiting-organization-approval","connected","expired","invalid","revoked","verification-failed","no-repositories","conflict","failed"]),ALL_STATUSES=new Set(["waiting",...TERMINAL_STATUSES]),KNOWN_INDEXING_STATUSES=new Set(["started","waiting-for-setup"]),REQUEST_TIMEOUT_MS=15e3;async function postJson(deps,path39,payload){let resp;try{resp=await deps.fetch(`${deps.baseUrl}${path39}`,{method:"POST",headers:{"Content-Type":"application/json","X-API-Key":deps.apiKey},body:JSON.stringify(payload),signal:AbortSignal.timeout(REQUEST_TIMEOUT_MS)})}catch(e){return{ok:!1,kind:e instanceof Error&&(e.name==="TimeoutError"||e.name==="AbortError")?"timeout":"network"}}let body=null;try{body=await resp.json()}catch{}return{ok:!0,value:{status:resp.status,body,retryAfter:resp.headers.get("Retry-After")}}}function classifyStatus(status){return status===401||status===403?"unauthorized":status===404?"not-found":"server"}function asRecord(value){return value&&typeof value=="object"&&!Array.isArray(value)?value:null}function asString3(value){return typeof value=="string"&&value.length>0?value:null}function asNullableString(value){return typeof value=="string"&&value.length>0?value:null}function parseRetryAfterMs(header,remainingMs,nowMs){if(!header)return null;let trimmed=header.trim();if(!trimmed)return null;let clamp=ms=>!Number.isFinite(ms)||ms<0?null:Math.min(ms,Math.max(0,remainingMs));if(/^\d+$/.test(trimmed))return clamp(Number(trimmed)*1e3);let parsed=Date.parse(trimmed);return Number.isNaN(parsed)?null:clamp(parsed-nowMs)}async function mintGithubConnection(deps,repoName){let res=await postJson(deps,"/setup/github/cli/connection-code",{repo_name:repoName});if(!res.ok)return res;if(res.value.status!==200)return{ok:!1,kind:classifyStatus(res.value.status)};let body=asRecord(res.value.body),state=asString3(body?.state),installUrl=asString3(body?.install_url),ttlSeconds=body?.ttl_seconds;return!body||!state||!installUrl||typeof ttlSeconds!="number"?{ok:!1,kind:"malformed"}:{ok:!0,value:{state,installUrl,ttlSeconds}}}function parseCandidates(value){if(!Array.isArray(value))return null;let out=[];for(let raw of value){let rec=asRecord(raw),id=asString3(rec?.github_repository_id),name=asString3(rec?.github_repo_name);if(!rec||!id||!name)return null;out.push({github_repository_id:id,github_repo_name:name,github_repo_full_name:asNullableString(rec.github_repo_full_name),owner:asNullableString(rec.owner)})}return out}async function confirmGithubConnection(deps,repoName,state,githubRepositoryId){let res=await postJson(deps,"/setup/github/cli/confirm",{repo_name:repoName,state,github_repository_id:githubRepositoryId});if(!res.ok)return res;if(res.value.status!==200)return{ok:!1,kind:classifyStatus(res.value.status)};let body=asRecord(res.value.body),name=asString3(body?.github_repo_name);if(!body||body.status!=="connected"||!name)return{ok:!1,kind:"malformed"};let rawIndexingStatus=body.indexing_status,indexingStatus=typeof rawIndexingStatus=="string"&&KNOWN_INDEXING_STATUSES.has(rawIndexingStatus)?rawIndexingStatus:null;return{ok:!0,value:{githubRepoName:name,githubRepoFullName:asNullableString(body.github_repo_full_name),indexingStatus}}}var ALL_HANDOFF_STATUSES=new Set(["pending","staged","awaiting-organization-approval","connected","expired","revoked","verification-failed","no-repositories","conflict","failed"]);async function mintGithubHandoff(deps,repoName){let res=await postJson(deps,"/setup/github/cli/handoff",{repo_name:repoName});if(!res.ok)return res;if(res.value.status!==200)return{ok:!1,kind:classifyStatus(res.value.status)};let body=asRecord(res.value.body),installUrl=asString3(body?.install_url),expiresAt=asString3(body?.expires_at),ttlSeconds=body?.ttl_seconds;return!body||!installUrl||!expiresAt||typeof ttlSeconds!="number"?{ok:!1,kind:"malformed"}:{ok:!0,value:{installUrl,expiresAt,ttlSeconds}}}function parseHandoffSnapshot(raw){let rec=asRecord(raw);if(!rec)return null;let state=asString3(rec.state),status=asString3(rec.status);if(!state||!status||!ALL_HANDOFF_STATUSES.has(status))return null;let candidates=parseCandidates(rec.candidates??[]);return candidates===null?null:{state,status,createdAt:asNullableString(rec.created_at),expiresAt:asNullableString(rec.expires_at),revokedAt:asNullableString(rec.revoked_at),candidates,githubRepoName:asNullableString(rec.github_repo_name)}}async function listGithubHandoffs(deps,repoName){let res=await postJson(deps,"/setup/github/cli/handoffs",{repo_name:repoName});if(!res.ok)return res;if(res.value.status!==200)return{ok:!1,kind:classifyStatus(res.value.status)};let body=asRecord(res.value.body),rawHandoffs=body?.handoffs;if(!body||!Array.isArray(rawHandoffs))return{ok:!1,kind:"malformed"};let out=[];for(let raw of rawHandoffs){let parsed=parseHandoffSnapshot(raw);if(!parsed)return{ok:!1,kind:"malformed"};out.push(parsed)}return{ok:!0,value:out}}async function revokeGithubHandoff(deps,repoName,state){let res=await postJson(deps,"/setup/github/cli/revoke",{repo_name:repoName,state});if(!res.ok)return res;if(res.value.status!==200)return{ok:!1,kind:classifyStatus(res.value.status)};let body=asRecord(res.value.body),status=asString3(body?.status);return!body||status!=="revoked"&&status!=="already-revoked"?{ok:!1,kind:"malformed"}:{ok:!0,value:status}}async function fetchGithubConfigurationState(deps,repoName){let resp;try{resp=await deps.fetch(`${deps.baseUrl}/jira/config/install-manifest?repo_name=${encodeURIComponent(repoName)}`,{headers:{"X-API-Key":deps.apiKey},signal:AbortSignal.timeout(REQUEST_TIMEOUT_MS)})}catch{return"unavailable"}if(resp.status!==200)return"unavailable";let body;try{body=await resp.json()}catch{return"unavailable"}let integrations=asRecord(body)?.integrations;if(!Array.isArray(integrations))return"unavailable";for(let raw of integrations){let rec=asRecord(raw);if(rec?.id==="github_app")return typeof rec.is_configured!="boolean"?"unavailable":rec.is_configured?"configured":"unconfigured"}return"unavailable"}var POLL_DEADLINE_MS=15.5*60*1e3,POLL_DELAYS_MS=[2e3,3e3,5e3],MAX_JITTER_MS=400,RETRYABLE_TRANSPORT=new Set(["network","timeout"]);function isRetryableStatus(status){return status===429||status>=500}async function pollGithubConnection(deps,poll,repoName,state){let started=poll.now(),attempt=0;for(;;){let elapsed=poll.now()-started,remaining=POLL_DEADLINE_MS-elapsed;if(remaining<=0)return{ok:!1,kind:"deadline"};let res=await postJson(deps,"/setup/github/cli/status",{repo_name:repoName,state}),waitMs=null;if(res.ok)if(res.value.status===200){let body=asRecord(res.value.body),status=asString3(body?.status);if(!body||!status||!ALL_STATUSES.has(status))return{ok:!1,kind:"malformed"};let candidates=parseCandidates(body.candidates??[]);if(candidates===null)return{ok:!1,kind:"malformed"};let typed=status;if(TERMINAL_STATUSES.has(typed))return{ok:!0,value:{status:typed,candidates,githubRepoName:asNullableString(body.github_repo_name),retryAfterMs:null}};waitMs=parseRetryAfterMs(res.value.retryAfter,remaining,poll.now())}else if(isRetryableStatus(res.value.status))waitMs=parseRetryAfterMs(res.value.retryAfter,remaining,poll.now());else return{ok:!1,kind:classifyStatus(res.value.status)};else if(!RETRYABLE_TRANSPORT.has(res.kind))return{ok:!1,kind:res.kind};waitMs===null&&(waitMs=POLL_DELAYS_MS[Math.min(attempt,POLL_DELAYS_MS.length-1)]+Math.floor(poll.jitter()*MAX_JITTER_MS)),attempt+=1;let capped=Math.min(waitMs,Math.max(0,POLL_DEADLINE_MS-(poll.now()-started)));if(capped<=0)return{ok:!1,kind:"deadline"};await poll.sleep(capped)}}import{readFile as readFile10,stat as stat7}from"fs/promises";import{spawn as spawn6}from"child_process";import os13 from"os";import path27 from"path";import readline2 from"readline";init_bridge_config();init_start_tickets_repo();init_credential_store();var USAGE2=`Usage: connect-github [--repo <repo_name>] [--handoff | --resume | --revoke]
4769
4769
 
4770
4770
  Connect a GitHub repository to a Bridge project from your terminal.
4771
4771
 
@@ -4773,12 +4773,24 @@ Opens the GitHub App install page in your browser, waits for you to install it,
4773
4773
  then asks which repository to connect. You are never asked for a GitHub token or
4774
4774
  password \u2014 you authenticate to GitHub in the browser.
4775
4775
 
4776
+ If you do not administer the GitHub organization yourself, use the delegated
4777
+ handoff: mint a link with --handoff, send it to whoever does, and finish with
4778
+ --resume once they have completed their part.
4779
+
4776
4780
  Options:
4777
4781
  --repo <repo_name> Bridge project to connect (inferred from this directory
4778
4782
  when omitted; you will be asked to confirm).
4779
- --help Show this message.`;function parseConnectGithubArgs(argv){let out={help:!1};for(let i=0;i<argv.length;i+=1){let arg=argv[i];if(arg==="--help"||arg==="-h"){out.help=!0;continue}if(arg==="--repo"){let value=argv[i+1];if(!value||value.startsWith("-"))return{ok:!1,error:"--repo requires a value (e.g. --repo my-project)."};out.repo=value,i+=1;continue}if(arg.startsWith("--repo=")){let value=arg.slice(7);if(!value)return{ok:!1,error:"--repo requires a value (e.g. --repo my-project)."};out.repo=value;continue}return arg==="--yes"||arg==="-y"?{ok:!1,error:"connect-github does not support --yes: connecting a repository always requires an explicit confirmation."}:arg==="--installation-id"||arg.startsWith("--installation-id=")?{ok:!1,error:"connect-github does not accept --installation-id: the installation is verified by Bridge from your browser install, not supplied by the caller."}:arg.startsWith("-")?{ok:!1,error:`Unknown option: ${arg}`}:{ok:!1,error:`Unexpected argument: ${arg}`}}return{ok:!0,value:out}}function defaultPromptLine2(promptText){return new Promise(resolve2=>{let rl=readline2.createInterface({input:process.stdin,output:process.stderr}),answered=!1;rl.on("close",()=>{answered||resolve2("")}),rl.question(promptText,answer=>{answered=!0,rl.close(),resolve2(answer.trim())})})}function defaultOpenBrowser(platform,url){let[command,args]=platform==="darwin"?["open",[url]]:platform==="win32"?["cmd",["/c","start","",url]]:["xdg-open",[url]];return new Promise(resolve2=>{try{let child=spawn6(command,args,{stdio:"ignore",detached:!1,shell:!1});child.on("error",()=>resolve2(!1)),child.on("spawn",()=>resolve2(!0))}catch{resolve2(!1)}})}function createDefaultConnectGithubDeps(){return{env:process.env,cwd:process.cwd(),platform:process.platform,homedir:os13.homedir,isTTY:!!process.stdin.isTTY,readFile:filePath=>readFile10(filePath,"utf-8"),stat:async filePath=>({mode:(await stat7(filePath)).mode}),fetch:globalThis.fetch,sleep:ms=>new Promise(resolve2=>setTimeout(resolve2,ms)),now:()=>Date.now(),jitter:()=>Math.random(),promptLine:defaultPromptLine2,openBrowser:url=>defaultOpenBrowser(process.platform,url),stdout:message=>process.stdout.write(`${message}
4783
+ --handoff Print a shareable install link (valid 72 hours) instead of
4784
+ connecting here. Does not open a browser or wait.
4785
+ --resume Look up an outstanding handoff and finish connecting it.
4786
+ Works from any machine holding this project's API key.
4787
+ --revoke Invalidate an outstanding handoff link.
4788
+ --help Show this message.
4789
+
4790
+ --handoff, --resume, and --revoke are mutually exclusive. Minting a new link does
4791
+ not invalidate an existing one \u2014 use --revoke for that.`,DELEGATED_MODE_FLAGS=["--handoff","--resume","--revoke"];function parseConnectGithubArgs(argv){let out={help:!1};for(let i=0;i<argv.length;i+=1){let arg=argv[i];if(arg==="--help"||arg==="-h"){out.help=!0;continue}if(arg==="--handoff"){out.handoff=!0;continue}if(arg==="--resume"){out.resume=!0;continue}if(arg==="--revoke"){out.revoke=!0;continue}if(arg==="--repo"){let value=argv[i+1];if(!value||value.startsWith("-"))return{ok:!1,error:"--repo requires a value (e.g. --repo my-project)."};out.repo=value,i+=1;continue}if(arg.startsWith("--repo=")){let value=arg.slice(7);if(!value)return{ok:!1,error:"--repo requires a value (e.g. --repo my-project)."};out.repo=value;continue}return arg==="--yes"||arg==="-y"?{ok:!1,error:"connect-github does not support --yes: connecting a repository always requires an explicit confirmation."}:arg==="--installation-id"||arg.startsWith("--installation-id=")?{ok:!1,error:"connect-github does not accept --installation-id: the installation is verified by Bridge from your browser install, not supplied by the caller."}:arg.startsWith("-")?{ok:!1,error:`Unknown option: ${arg}`}:{ok:!1,error:`Unexpected argument: ${arg}`}}let selected=DELEGATED_MODE_FLAGS.filter(flag=>flag==="--handoff"&&out.handoff||flag==="--resume"&&out.resume||flag==="--revoke"&&out.revoke);return selected.length>1?{ok:!1,error:`Choose only one of ${DELEGATED_MODE_FLAGS.join(", ")} (got ${selected.join(", ")}).`}:{ok:!0,value:out}}function defaultPromptLine2(promptText){return new Promise(resolve2=>{let rl=readline2.createInterface({input:process.stdin,output:process.stderr}),answered=!1;rl.on("close",()=>{answered||resolve2("")}),rl.question(promptText,answer=>{answered=!0,rl.close(),resolve2(answer.trim())})})}function defaultOpenBrowser(platform,url){let[command,args]=platform==="darwin"?["open",[url]]:platform==="win32"?["cmd",["/c","start","",url]]:["xdg-open",[url]];return new Promise(resolve2=>{try{let child=spawn6(command,args,{stdio:"ignore",detached:!1,shell:!1});child.on("error",()=>resolve2(!1)),child.on("spawn",()=>resolve2(!0))}catch{resolve2(!1)}})}function createDefaultConnectGithubDeps(){return{env:process.env,cwd:process.cwd(),platform:process.platform,homedir:os13.homedir,isTTY:!!process.stdin.isTTY,readFile:filePath=>readFile10(filePath,"utf-8"),stat:async filePath=>({mode:(await stat7(filePath)).mode}),fetch:globalThis.fetch,sleep:ms=>new Promise(resolve2=>setTimeout(resolve2,ms)),now:()=>Date.now(),jitter:()=>Math.random(),promptLine:defaultPromptLine2,openBrowser:url=>defaultOpenBrowser(process.platform,url),stdout:message=>process.stdout.write(`${message}
4780
4792
  `),stderr:message=>process.stderr.write(`${message}
4781
- `)}}async function resolveConnectGithubRepoName(args,deps){if(args.repo){let validated2=validateRepoName(args.repo);return validated2.ok?{ok:!0,value:validated2.value}:{ok:!1,error:validated2.error}}let inferred=await resolveStartTicketsRepoName({env:deps.env,cwd:deps.cwd,readFile:deps.readFile});if(!inferred){let validated2=validateRepoName(path27.basename(deps.cwd));validated2.ok&&(inferred=validated2.value)}if(!inferred)return{ok:!1,error:"Could not determine the Bridge project. Pass --repo <repo_name>."};let answer=(await deps.promptLine(`Bridge project [${inferred}]: `)).trim(),chosen=answer.length>0?answer:inferred,validated=validateRepoName(chosen);return validated.ok?{ok:!0,value:validated.value}:{ok:!1,error:validated.error}}async function openGithubInstallPage(deps,installUrl){return await deps.openBrowser(installUrl)?{ok:!0}:{ok:!1,error:"Could not open your browser automatically. Re-run this command from a desktop session with a browser available."}}var STEPS=["Connect GitHub","Complete GitHub in browser","Verify connection","Choose repository","Confirm connection"];function renderStep(deps,index){deps.stderr(`[${index+1}/${STEPS.length}] ${STEPS[index]}`)}function candidateLabel(c){return c.github_repo_full_name?c.github_repo_full_name:c.owner?`${c.owner}/${c.github_repo_name}`:c.github_repo_name}var OUTCOME_MESSAGES={expired:"The connection request expired before GitHub reported back. Run connect-github again.",invalid:"This connection request is no longer valid. Run connect-github again.","verification-failed":"Bridge could not verify the GitHub installation. Run connect-github again.","no-repositories":"The GitHub App installation did not include any repositories Bridge can access. Re-run connect-github and grant access to at least one repository.",conflict:"This GitHub installation or project is already connected to a different Bridge account or repository. Contact support if that is unexpected.",failed:"The GitHub connection did not complete. Run connect-github again."},FAILURE_MESSAGES={network:"Could not reach Bridge API. Check your network, then run connect-github again.",timeout:"Bridge API did not respond in time. Run connect-github again.",unauthorized:"Bridge rejected your API key for this project. Re-run install-bridge with a current key.","not-found":"Bridge does not recognize this project. Check --repo matches your Bridge project name.",server:"Bridge API returned an error. Run connect-github again shortly.",malformed:"Bridge API returned an unexpected response. Run connect-github again shortly.",deadline:"Timed out waiting for GitHub. If you completed the install, run connect-github again to pick up the connection."};function reportNoConnection(deps,detail){return deps.stderr(""),deps.stderr("No GitHub connection was made."),deps.stderr(detail),1}async function runGithubConnectionFlow(deps,api,repoName){renderStep(deps,0);let minted=await mintGithubConnection(api,repoName);if(!minted.ok)return reportNoConnection(deps,FAILURE_MESSAGES[minted.kind]);renderStep(deps,1),deps.stderr("Opening GitHub\u2026");let opened=await openGithubInstallPage(deps,minted.value.installUrl);if(!opened.ok)return reportNoConnection(deps,opened.error);renderStep(deps,2);let minutes=Math.floor(POLL_DEADLINE_MS/6e4);deps.stderr(`Waiting for GitHub installation\u2026 (up to ~${minutes} minutes)`);let pollDeps={sleep:deps.sleep,now:deps.now,jitter:deps.jitter},started=deps.now(),polled=await pollGithubConnection(api,pollDeps,repoName,minted.value.state);if(!polled.ok)return reportNoConnection(deps,FAILURE_MESSAGES[polled.kind]);let elapsedSec=Math.max(0,Math.round((deps.now()-started)/1e3));deps.stderr(`Waited ${elapsedSec}s.`);let result=polled.value;if(result.status==="awaiting-organization-approval")return deps.stderr(""),deps.stderr("No GitHub connection was made."),deps.stderr("Your request to install the GitHub App was sent to an organization owner for approval."),deps.stderr("That approval happens on GitHub and does not return here, so this command cannot wait for it."),deps.stderr("Once an owner approves the install, finish the connection with the manual steps in the GitHub App setup guide (docs/install/github-app.md)."),1;if(result.status==="connected")return deps.stdout(`Connected ${result.githubRepoName??repoName}.`),0;if(result.status!=="staged")return reportNoConnection(deps,OUTCOME_MESSAGES[result.status]??OUTCOME_MESSAGES.failed);let candidates=result.candidates;if(candidates.length===0)return reportNoConnection(deps,OUTCOME_MESSAGES["no-repositories"]);renderStep(deps,3);let selected=null;if(candidates.length===1){let only=candidates[0],answer=(await deps.promptLine(`Connect ${candidateLabel(only)}? [y/N]: `)).trim().toLowerCase();if(answer!=="y"&&answer!=="yes")return deps.stderr(""),deps.stderr("No GitHub connection was made. Nothing was changed."),1;selected=only}else{deps.stderr(""),deps.stderr("Your GitHub installation includes multiple repositories:"),candidates.forEach((c,i)=>{deps.stderr(` ${String(i+1).padStart(2," ")}. ${candidateLabel(c)}`)}),deps.stderr("");let answer=(await deps.promptLine(`Choose a repository [1-${candidates.length}]: `)).trim(),index=Number(answer);if(!/^\d+$/.test(answer)||!Number.isInteger(index)||index<1||index>candidates.length)return deps.stderr(""),deps.stderr("No GitHub connection was made. No repository was selected."),1;selected=candidates[index-1],deps.stderr(`Selected ${candidateLabel(selected)}.`)}renderStep(deps,4);let confirmed=await confirmGithubConnection(api,repoName,minted.value.state,selected.github_repository_id);return confirmed.ok?(deps.stdout(`Connected ${confirmed.value.githubRepoFullName??confirmed.value.githubRepoName}.`),confirmed.value.indexingStatus==="started"?deps.stdout("Indexing started automatically."):confirmed.value.indexingStatus==="waiting-for-setup"&&deps.stdout("Indexing will start automatically once setup completes."),0):reportNoConnection(deps,FAILURE_MESSAGES[confirmed.kind])}async function runConnectGithubCli(argv,injected){let deps=injected??createDefaultConnectGithubDeps();try{let parsed=parseConnectGithubArgs(argv);if(!parsed.ok)return deps.stderr(parsed.error),deps.stderr(""),deps.stderr(USAGE2),1;if(parsed.value.help)return deps.stdout(USAGE2),0;if(!deps.isTTY)return deps.stderr("connect-github needs an interactive terminal: it asks you to confirm which repository to connect. Run it directly in your terminal."),1;let repo=await resolveConnectGithubRepoName(parsed.value,deps);if(!repo.ok)return deps.stderr(repo.error),1;let cred=await resolveBapiCredentials(repo.value,{env:deps.env,homedir:deps.homedir,platform:deps.platform,readFile:deps.readFile,stat:deps.stat,stderr:()=>{}});if(!cred.ok)return deps.stderr(cred.error),1;let api={fetch:deps.fetch,baseUrl:deps.env.BAPI_BASE_URL?.trim()||DEFAULT_BAPI_BASE_URL2,apiKey:cred.credentials.apiKey};return await runGithubConnectionFlow(deps,api,repo.value)}catch{return deps.stderr("No GitHub connection was made. An unexpected error occurred."),1}}init_agent_registry();init_start_tickets();var REDACTED_API_KEY="<REDACTED>",MCP_TIMEOUT_GUIDANCE="Note: if your MCP client has a very short connect deadline, raise MCP_TIMEOUT for the first launch (the initial npx package resolution/download can exceed a short connect timeout).";function buildPrewarmArgs(){return["-y","--prefer-offline",`@bridge_gpt/mcp-server@${VERSION}`,"--version"]}function buildPrewarmCommandPreview(){return`npx ${buildPrewarmArgs().join(" ")}`}var INSTALL_BRIDGE_AGENT_PROMPT="Execute the /install-bridge command in the install-spawn context (tell the command it is running in the install-spawn context so it SKIPS its Stage 8, Stage 9, and Stage 10 offers). The command chooses its own mode from the install manifest's `configured` flag \u2014 do NOT force a mode: when `configured` is false it runs its fresh-configuration flow, and when `configured` is true it runs JOIN MODE. In JOIN MODE the command must NOT derive, approve, apply, or offer any configuration and applies zero fields for any caller; a joining member's only closing interaction is the welcome plus the concise capability report (no /learn-repository prompt for a member). Complete the command's concise capability report: the single 'What Bridge can help with' section, rendered exactly as the server's concise_tool_capabilities field gives it (Regularly useful, then Occasionally useful, each tool's display_name only, plus each tier's server-computed '+N more' where non-zero). Do not render the obsolete five-section report, and do not locally filter, count, or fall back to the complete tool_capabilities catalog. The install-spawn skip set is exactly Stage 8, Stage 9, and Stage 10. The command-owned Stage 11 (invite teammates) is NOT in that skip set: it is independently gated and may run only when the caller's role is admin AND customer_type is b2b AND an interactive response is available; otherwise it is skipped silently. If Stage 11 mints a teammate key, show that plaintext key exactly once and never repeat it in any summary, retry, or diagnostic. Do NOT ask any indexing question, call parse_repository, mention /parse-repository, or claim indexing has started or is pending \u2014 indexing is decided entirely by the server-side readiness funnel with no visibility from this session, so say nothing about it at all. After the capability report on the fresh-configuration path, recommend /learn-repository as the next step: explain briefly that it learns repository-specific architecture, review, testing, correctness, and validation-manual configuration by researching the actual codebase. Do NOT run /learn-repository yourself \u2014 only recommend it; running it is the human's next explicit invocation. Never request, echo, or transport any credential \u2014 only ever direct the human to that integration's own configure_in pointer, verbatim. The pointer is per-integration and is NOT always the setup UI: GitHub's is a terminal command (connect-github), while Jira, SFCC, and Bitbucket point at the setup UI. Follow whatever the report says rather than assuming. On the fresh-configuration path, end with an explicit summary line stating how many config fields the apply_install_manifest call applied (e.g. 'Applied 8 of 9 derived fields') \u2014 if 0 fields were applied, say so loudly and explain what is still pending. In JOIN MODE there is no apply \u2014 state instead that the project was already configured and zero changes were made, without a fabricated applied count.",DEFAULT_BAPI_BASE_URL2="https://bridgegpt-api.com";function buildInstallBridgeSetupUrl(baseUrl){return`${baseUrl.replace(/\/+$/,"")}/setup`}var DEFAULT_BAPI_DOCS_DIR="docs/tmp";function getInstallBridgeUsage(baseUrl=DEFAULT_BAPI_BASE_URL2){let setupUrl=buildInstallBridgeSetupUrl(baseUrl);return["Usage:"," npx -y @bridge_gpt/mcp-server@latest install-bridge [flags]","","One-command Bridge API project bootstrap. Scaffolds the project, writes the","per-host MCP config with your credentials, verifies connectivity, persists the","routing credential, then \u2014 on a TTY, only after a Y/N consent prompt \u2014 opens a","fresh session in your selected tool. If the project is not yet configured it","derives the remaining config, presents a concise capability report, and","recommends /learn-repository. If the project is already configured it instead","joins you to it without proposing or applying any changes and just shows the","concise capability report (a b2b admin is additionally offered a teammate-invite","step). So not every run applies config fields. Indexing is never asked about \u2014","it starts automatically once the repository reaches full parse readiness.","","Your API key is written to a project MCP config only when that file is safe: a","valid, git-ignored config gets the real key, but a config already TRACKED by git","gets it only after an explicit default-No confirmation (and never at all in a","non-interactive run) \u2014 otherwise a secret-free entry is written and the server","resolves your key from the credential store at runtime. A config file that","cannot be parsed is left untouched, with manual-merge instructions printed.","","Run it bare \u2014 `install-bridge` with no flags \u2014 in a terminal and it asks",`\`${INSTALL_BRIDGE_KEY_SELECTOR_PROMPT}\` first, with three numbered choices:`,...INSTALL_BRIDGE_ONBOARDING_CHOICES.map(choice=>` ${choice}`),"There is NO default \u2014 pressing Enter selects nothing; you must type 1, 2, or 3","(one blank or invalid answer re-prompts once, then exits with guidance). Option 1","is the existing-key flow below, option 2 is the bootstrap-invite flow, and option 3","prompts for an email and creates a brand-new Bridge workspace for you (the","self-serve flow). That question is asked ONLY for a bare interactive run: passing","ANY flag, setting BAPI_API_KEY, or running without an interactive terminal keeps","the existing deterministic behavior and no prompt.","","Inputs (the only two irreducible ones):"," --api-key <key> Bridge API key OR bootstrap invite. Falls back to the"," BAPI_API_KEY env var, then an interactive (no-echo) prompt.",` Generate a key at ${setupUrl} (Security page) \u2014 this`," command consumes a key, it does not create one. A value"," detected as a bootstrap invite (bapi_inv_\u2026), from any of the"," three sources above, is instead redeemed to CREATE a new"," project and its first admin key \u2014 the same as --invite \u2014"," so it skips repository lookup entirely (--email and --invite"," remain the explicit, preferred entry points for a new project)."," NEVER printed or logged."," --repo <name> Repository name. --repo and BAPI_REPO_NAME still take"," priority and short-circuit before any network call. When"," neither is set, a compatible server resolves the unique"," repository from your existing API key automatically; if the"," server is older, the key is unresolvable, or resolution"," fails, it falls back to an inferred default you confirm"," interactively (and to a required --repo when stdin is"," non-interactive). In the existing-key flow it MUST match the"," server-side repo registration (it keys the credential store"," as bapi:<repo>). In either new-project flow (--email,"," --invite, or chooser option 2 or 3 above) it"," instead NAMES the project this run creates, so you are asked"," to name a new project rather than match an existing one; the"," name must be globally unique.","","Self-serve onboarding (no account, no API key, no pre-issued invite):"," --email <addr> Create a brand-new Bridge workspace from just an email \u2014"," the primary path for a first-time user with nothing yet."," It requests a fresh workspace for that email, then creates"," the project and mints your own admin API key in one command."," Falls back to the BAPI_SIGNUP_EMAIL env var, then a visible"," interactive prompt \u2014 which is also what chooser option 3 on a"," bare run reaches. The email is NOT a"," secret (it is shown as you type), but it is never printed to"," a log. Mutually"," exclusive with --api-key and --invite. No email verification"," is performed and no message is sent to the address \u2014 it only"," labels the new workspace.",""," RESUMABLE: a self-serve run that fails mid-protocol saves its"," signup state under bootstrap-pending:<repo> in the credential"," store. Re-running the self-serve flow RESUMES that attempt \u2014"," it does not sign up again \u2014 so a retry never creates a second"," workspace. Never copy, display, or hand-remove that record; if"," the saved invite has genuinely expired the CLI asks before"," discarding it.","","Bootstrap-invite onboarding (no web UI, no pre-existing key):"," --invite [token] Redeem a bootstrap invite you were already given: creates"," the project and mints your own admin API key in one command."," Mutually exclusive with --api-key and --email (in this mode"," the key is created, not consumed).",""," Run it WITHOUT a value \u2014 `install-bridge --invite` \u2014 and the"," token is read from an interactive prompt with echo"," suppressed, then sent only in the request body. This is the"," default and the recommended path: 'a copy/paste one-liner'"," and 'the token never touches shell history' are"," contradictory, so the one-liner your operator sends you is"," SECRET-FREE and the CLI asks for the token.",""," Passing the token inline (--invite <token>, --invite=<token>)"," or via BAPI_INVITE is for SCRIPTING ONLY: both forms EXPOSE"," THE TOKEN to your shell history and to the process list.","","Flags:"," --tools <ids> Comma-separated AI-coding tools to configure,"," bypassing the interactive picker. Accepted ids:",` ${HOST_PLATFORM_ORDER.join(", ")}.`," Both --tools=claude-code,codex and"," --tools claude-code,codex are accepted. On an"," interactive terminal WITHOUT this flag you get a"," numbered picker: enter one or more tool numbers,"," comma-separated (e.g. 1,3), and press Enter once \u2014"," no tool is pre-selected (not even Claude Code) and"," you must select at least one. A non-interactive"," (non-TTY) run without"," --tools writes the legacy automatic set (Claude"," Code plus any detected Cursor / Copilot VS Code)"," and never opens an agent terminal (it prints"," manual continuation instead). --tools= (empty) is"," an explicit empty selection: it writes nothing and"," launches nothing."," --force Overwrite an existing real BAPI_API_KEY in a"," host config (or in the credential store) without"," prompting. It does NOT bypass the git-tracked"," config safeguard below: --force authorizes"," replacing a credential, not disclosing your key"," into version control."," --dry-run Preview every step (scaffold targets, config"," files + keys with the key REDACTED, ping"," target, credential target, and the consent-gated"," launch outcome) without writing, pinging,"," prompting, or spawning anything. With --invite it"," also never calls the exchange endpoint and never"," generates or stores a secret."," --agent claude|cursor-agent Explicit launch override for the post-install"," session \u2014 it always wins, and there is NO Claude"," default. Without it the launch tool is derived"," from your selection: Claude Code opens `claude`."," Cursor is NOT auto-opened (its cursor-agent CLI"," first-run workspace-trust prompt collapses a spawned"," session); selecting Cursor writes .cursor/mcp.json"," and prints how to finish by running /install-bridge"," in a new Cursor session. A selection whose tools have"," no agentic CLI (e.g. Copilot) likewise opens nothing"," and prints how to finish configuring later. Passing"," --agent cursor-agent still force-spawns it."," -h, --help Show this help.","","Environment: BAPI_BASE_URL (default https://bridgegpt-api.com) and BAPI_DOCS_DIR","(default docs/tmp) are read from the environment with the shown fallbacks.","BAPI_SIGNUP_EMAIL supplies the self-serve signup email non-interactively (it is","visible input, not a secret). BAPI_INVITE supplies the bootstrap-invite token","non-interactively (scripting only \u2014 it is exposed to shell history; prefer the","prompt)."].join(`
4793
+ `)}}async function resolveConnectGithubRepoName(args,deps){if(args.repo){let validated2=validateRepoName(args.repo);return validated2.ok?{ok:!0,value:validated2.value}:{ok:!1,error:validated2.error}}let inferred=await resolveStartTicketsRepoName({env:deps.env,cwd:deps.cwd,readFile:deps.readFile});if(!inferred){let validated2=validateRepoName(path27.basename(deps.cwd));validated2.ok&&(inferred=validated2.value)}if(!inferred)return{ok:!1,error:"Could not determine the Bridge project. Pass --repo <repo_name>."};let answer=(await deps.promptLine(`Bridge project [${inferred}]: `)).trim(),chosen=answer.length>0?answer:inferred,validated=validateRepoName(chosen);return validated.ok?{ok:!0,value:validated.value}:{ok:!1,error:validated.error}}function resolveHandoffRepoName(args,_deps){if(!args.repo)return{ok:!1,error:"connect-github --handoff needs an explicit project: pass --repo <repo_name>. It cannot ask you to confirm an inferred name when run non-interactively."};let validated=validateRepoName(args.repo);return validated.ok?{ok:!0,value:validated.value}:{ok:!1,error:validated.error}}async function openGithubInstallPage(deps,installUrl){return await deps.openBrowser(installUrl)?{ok:!0}:{ok:!1,error:"Could not open your browser automatically. Re-run this command from a desktop session with a browser available."}}var STEPS=["Connect GitHub","Complete GitHub in browser","Verify connection","Choose repository","Confirm connection"];function renderStep(deps,index){deps.stderr(`[${index+1}/${STEPS.length}] ${STEPS[index]}`)}function candidateLabel(c){return c.github_repo_full_name?c.github_repo_full_name:c.owner?`${c.owner}/${c.github_repo_name}`:c.github_repo_name}var OUTCOME_MESSAGES={expired:"The connection request expired before GitHub reported back. Run connect-github again.",invalid:"This connection request is no longer valid. Run connect-github again.","verification-failed":"Bridge could not verify the GitHub installation. Run connect-github again.","no-repositories":"The GitHub App installation did not include any repositories Bridge can access. Re-run connect-github and grant access to at least one repository.",conflict:"This GitHub installation or project is already connected to a different Bridge account or repository. Contact support if that is unexpected.",failed:"The GitHub connection did not complete. Run connect-github again."},FAILURE_MESSAGES={network:"Could not reach Bridge API. Check your network, then run connect-github again.",timeout:"Bridge API did not respond in time. Run connect-github again.",unauthorized:"Bridge rejected your API key for this project. Re-run install-bridge with a current key.","not-found":"Bridge does not recognize this project. Check --repo matches your Bridge project name.",server:"Bridge API returned an error. Run connect-github again shortly.",malformed:"Bridge API returned an unexpected response. Run connect-github again shortly.",deadline:"Timed out waiting for GitHub. If you completed the install, run connect-github again to pick up the connection."};function reportNoConnection(deps,detail){return deps.stderr(""),deps.stderr("No GitHub connection was made."),deps.stderr(detail),1}async function chooseCandidate(deps,candidates){if(candidates.length===1){let only=candidates[0],answer2=(await deps.promptLine(`Connect ${candidateLabel(only)}? [y/N]: `)).trim().toLowerCase();return answer2!=="y"&&answer2!=="yes"?(deps.stderr(""),deps.stderr("No GitHub connection was made. Nothing was changed."),null):only}deps.stderr(""),deps.stderr("Your GitHub installation includes multiple repositories:"),candidates.forEach((c,i)=>{deps.stderr(` ${String(i+1).padStart(2," ")}. ${candidateLabel(c)}`)}),deps.stderr("");let answer=(await deps.promptLine(`Choose a repository [1-${candidates.length}]: `)).trim(),index=Number(answer);if(!/^\d+$/.test(answer)||!Number.isInteger(index)||index<1||index>candidates.length)return deps.stderr(""),deps.stderr("No GitHub connection was made. No repository was selected."),null;let selected=candidates[index-1];return deps.stderr(`Selected ${candidateLabel(selected)}.`),selected}async function runGithubConnectionFlow(deps,api,repoName){renderStep(deps,0);let minted=await mintGithubConnection(api,repoName);if(!minted.ok)return reportNoConnection(deps,FAILURE_MESSAGES[minted.kind]);renderStep(deps,1),deps.stderr("Opening GitHub\u2026");let opened=await openGithubInstallPage(deps,minted.value.installUrl);if(!opened.ok)return reportNoConnection(deps,opened.error);renderStep(deps,2);let minutes=Math.floor(POLL_DEADLINE_MS/6e4);deps.stderr(`Waiting for GitHub installation\u2026 (up to ~${minutes} minutes)`);let pollDeps={sleep:deps.sleep,now:deps.now,jitter:deps.jitter},started=deps.now(),polled=await pollGithubConnection(api,pollDeps,repoName,minted.value.state);if(!polled.ok)return reportNoConnection(deps,FAILURE_MESSAGES[polled.kind]);let elapsedSec=Math.max(0,Math.round((deps.now()-started)/1e3));deps.stderr(`Waited ${elapsedSec}s.`);let result=polled.value;if(result.status==="awaiting-organization-approval")return deps.stderr(""),deps.stderr("No GitHub connection was made."),deps.stderr("Your request to install the GitHub App was sent to an organization owner for approval."),deps.stderr("That approval happens on GitHub and does not return here, so this command cannot wait for it."),deps.stderr("Once an owner approves the install, finish the connection with the manual steps in the GitHub App setup guide (docs/install/github-app.md)."),1;if(result.status==="connected")return deps.stdout(`Connected ${result.githubRepoName??repoName}.`),0;if(result.status!=="staged")return reportNoConnection(deps,OUTCOME_MESSAGES[result.status]??OUTCOME_MESSAGES.failed);let candidates=result.candidates;if(candidates.length===0)return reportNoConnection(deps,OUTCOME_MESSAGES["no-repositories"]);renderStep(deps,3);let selected=await chooseCandidate(deps,candidates);if(!selected)return 1;renderStep(deps,4);let confirmed=await confirmGithubConnection(api,repoName,minted.value.state,selected.github_repository_id);return confirmed.ok?(deps.stdout(`Connected ${confirmed.value.githubRepoFullName??confirmed.value.githubRepoName}.`),confirmed.value.indexingStatus==="started"?deps.stdout("Indexing started automatically."):confirmed.value.indexingStatus==="waiting-for-setup"&&deps.stdout("Indexing will start automatically once setup completes."),0):reportNoConnection(deps,FAILURE_MESSAGES[confirmed.kind])}async function runGithubHandoffMintFlow(deps,api,repoName){let minted=await mintGithubHandoff(api,repoName);return minted.ok?(deps.stdout(minted.value.installUrl),deps.stderr(""),deps.stderr("Send this link to whoever administers your GitHub organization."),deps.stderr(`It works once and expires ${formatExpiry(minted.value.expiresAt)}.`),deps.stderr("They'll finish in their browser \u2014 ask them to ping you, then run `connect-github --resume`."),0):reportNoHandoff(deps,FAILURE_MESSAGES[minted.kind])}function formatExpiry(iso){let parsed=Date.parse(iso);return Number.isNaN(parsed)?"in 72 hours":`on ${new Date(parsed).toISOString().replace("T"," ").slice(0,16)} UTC`}function reportNoHandoff(deps,detail){return deps.stderr(""),deps.stderr("No handoff link was created."),deps.stderr(detail),1}var REVOCABLE_STATUSES=new Set(["pending","awaiting-organization-approval","staged"]),HANDOFF_STATUS_MESSAGES={pending:"Nobody has completed the link yet. Ask them to open it, then run connect-github --resume again.",expired:"The link expired before it was used. Run connect-github --handoff to create a new one.",revoked:"This link was revoked. Run connect-github --handoff to create a new one.","verification-failed":"Bridge could not verify the GitHub installation. Run connect-github --handoff to create a new link.","no-repositories":"The GitHub App installation did not include any repositories Bridge can access. Create a new link with connect-github --handoff and ask them to grant access to at least one repository.",conflict:"This GitHub installation or project is already connected to a different Bridge account or repository. Contact support if that is unexpected.",failed:"The GitHub install did not complete. Run connect-github --handoff to create a new link."};function handoffLabel(h){let created=h.createdAt?` \xB7 created ${h.createdAt.slice(0,16).replace("T"," ")}`:"";return`${h.status}${created}`}async function selectDelegatedHandoff(deps,handoffs){if(handoffs.length===0)return null;if(handoffs.length===1)return handoffs[0];deps.stderr(""),deps.stderr("Outstanding handoff links:"),handoffs.forEach((h,i)=>{deps.stderr(` ${String(i+1).padStart(2," ")}. ${handoffLabel(h)}`)}),deps.stderr("");let answer=(await deps.promptLine(`Choose a handoff [1-${handoffs.length}]: `)).trim(),index=Number(answer);return!/^\d+$/.test(answer)||!Number.isInteger(index)||index<1||index>handoffs.length?null:handoffs[index-1]}async function runGithubHandoffResumeFlow(deps,api,repoName){let listed=await listGithubHandoffs(api,repoName);if(!listed.ok)return reportNoConnection(deps,FAILURE_MESSAGES[listed.kind]);if(listed.value.length===0)return reportNoConnection(deps,"No handoff links exist for this project. Run connect-github --handoff to create one.");let selected=await selectDelegatedHandoff(deps,listed.value);if(!selected)return reportNoConnection(deps,"No handoff was selected.");if(selected.status==="connected")return deps.stdout(`Connected ${selected.githubRepoName??repoName}.`),0;if(selected.status==="awaiting-organization-approval")return deps.stderr(""),deps.stderr("The install was sent to a GitHub organization owner for approval, and that approval has not happened yet."),deps.stderr("GitHub does not notify Bridge when it lands, so re-run connect-github --resume once the owner has approved it."),selected.expiresAt&&deps.stderr(`The link stays usable until ${formatExpiry(selected.expiresAt)}.`),0;if(selected.status!=="staged"){let message=HANDOFF_STATUS_MESSAGES[selected.status]??HANDOFF_STATUS_MESSAGES.failed;return selected.status==="pending"||selected.status==="expired"?(deps.stderr(""),deps.stderr(message),deps.stderr("If the GitHub App is already installed on that organization, GitHub may not send Bridge a callback at all. In that case follow the manual installation-ID steps in the GitHub App setup guide (docs/install/github-app.md)."),selected.status==="pending"?0:1):reportNoConnection(deps,message)}let candidates=selected.candidates;if(candidates.length===0)return reportNoConnection(deps,OUTCOME_MESSAGES["no-repositories"]);let chosen=await chooseCandidate(deps,candidates);if(!chosen)return 1;let confirmed=await confirmGithubConnection(api,repoName,selected.state,chosen.github_repository_id);return confirmed.ok?(deps.stdout(`Connected ${confirmed.value.githubRepoFullName??confirmed.value.githubRepoName}.`),confirmed.value.indexingStatus==="started"?deps.stdout("Indexing started automatically."):confirmed.value.indexingStatus==="waiting-for-setup"&&deps.stdout("Indexing will start automatically once setup completes."),0):reportNoConnection(deps,FAILURE_MESSAGES[confirmed.kind])}async function runGithubHandoffRevokeFlow(deps,api,repoName){let listed=await listGithubHandoffs(api,repoName);if(!listed.ok)return reportNoRevocation(deps,FAILURE_MESSAGES[listed.kind]);let revocable=listed.value.filter(h=>REVOCABLE_STATUSES.has(h.status));if(revocable.length===0)return reportNoRevocation(deps,"There are no handoff links left to revoke.");let selected=await selectDelegatedHandoff(deps,revocable);if(!selected)return reportNoRevocation(deps,"No handoff was selected.");let revoked=await revokeGithubHandoff(api,repoName,selected.state);return revoked.ok?(deps.stdout(revoked.value==="already-revoked"?"That handoff link was already revoked.":"Handoff link revoked. It can no longer be used."),0):reportNoRevocation(deps,FAILURE_MESSAGES[revoked.kind])}function reportNoRevocation(deps,detail){return deps.stderr(""),deps.stderr("Nothing was revoked."),deps.stderr(detail),1}async function runConnectGithubCli(argv,injected){let deps=injected??createDefaultConnectGithubDeps();try{let parsed=parseConnectGithubArgs(argv);if(!parsed.ok)return deps.stderr(parsed.error),deps.stderr(""),deps.stderr(USAGE2),1;if(parsed.value.help)return deps.stdout(USAGE2),0;let mintOnly=parsed.value.handoff;if(!deps.isTTY&&!mintOnly)return deps.stderr("connect-github needs an interactive terminal: it asks you to confirm which repository to connect. Run it directly in your terminal."),1;let repo=mintOnly?resolveHandoffRepoName(parsed.value,deps):await resolveConnectGithubRepoName(parsed.value,deps);if(!repo.ok)return deps.stderr(repo.error),1;let cred=await resolveBapiCredentials(repo.value,{env:deps.env,homedir:deps.homedir,platform:deps.platform,readFile:deps.readFile,stat:deps.stat,stderr:()=>{}});if(!cred.ok)return deps.stderr(cred.error),1;let api={fetch:deps.fetch,baseUrl:deps.env.BAPI_BASE_URL?.trim()||DEFAULT_BAPI_BASE_URL2,apiKey:cred.credentials.apiKey};return parsed.value.handoff?await runGithubHandoffMintFlow(deps,api,repo.value):parsed.value.resume?await runGithubHandoffResumeFlow(deps,api,repo.value):parsed.value.revoke?await runGithubHandoffRevokeFlow(deps,api,repo.value):await runGithubConnectionFlow(deps,api,repo.value)}catch{return deps.stderr("No GitHub connection was made. An unexpected error occurred."),1}}init_agent_registry();init_start_tickets();var REDACTED_API_KEY="<REDACTED>",MCP_TIMEOUT_GUIDANCE="Note: if your MCP client has a very short connect deadline, raise MCP_TIMEOUT for the first launch (the initial npx package resolution/download can exceed a short connect timeout).";function buildPrewarmArgs(){return["-y","--prefer-offline",`@bridge_gpt/mcp-server@${VERSION}`,"--version"]}function buildPrewarmCommandPreview(){return`npx ${buildPrewarmArgs().join(" ")}`}var INSTALL_BRIDGE_AGENT_PROMPT="Execute the /install-bridge command in the install-spawn context (tell the command it is running in the install-spawn context so it SKIPS its Stage 8, Stage 9, and Stage 10 offers). The command chooses its own mode from the install manifest's `configured` flag \u2014 do NOT force a mode: when `configured` is false it runs its fresh-configuration flow, and when `configured` is true it runs JOIN MODE. In JOIN MODE the command must NOT derive, approve, apply, or offer any configuration and applies zero fields for any caller; a joining member's only closing interaction is the welcome plus the concise capability report (no /learn-repository prompt for a member). Complete the command's concise capability report: the single 'What Bridge can help with' section, rendered exactly as the server's concise_tool_capabilities field gives it (Regularly useful, then Occasionally useful, each tool's display_name only, plus each tier's server-computed '+N more' where non-zero). Do not render the obsolete five-section report, and do not locally filter, count, or fall back to the complete tool_capabilities catalog. The install-spawn skip set is exactly Stage 8, Stage 9, and Stage 10. The command-owned Stage 11 (invite teammates) is NOT in that skip set: it is independently gated and may run only when the caller's role is admin AND customer_type is b2b AND an interactive response is available; otherwise it is skipped silently. If Stage 11 mints a teammate key, show that plaintext key exactly once and never repeat it in any summary, retry, or diagnostic. Do NOT ask any indexing question, call parse_repository, mention /parse-repository, or claim indexing has started or is pending \u2014 indexing is decided entirely by the server-side readiness funnel with no visibility from this session, so say nothing about it at all. After the capability report on the fresh-configuration path, recommend /learn-repository as the next step: explain briefly that it learns repository-specific architecture, review, testing, correctness, and validation-manual configuration by researching the actual codebase. Do NOT run /learn-repository yourself \u2014 only recommend it; running it is the human's next explicit invocation. Never request, echo, or transport any credential \u2014 only ever direct the human to that integration's own configure_in pointer, verbatim. The pointer is per-integration and is NOT always the setup UI: GitHub's is a terminal command (connect-github), while Jira, SFCC, and Bitbucket point at the setup UI. Follow whatever the report says rather than assuming. On the fresh-configuration path, end with an explicit summary line stating how many config fields the apply_install_manifest call applied (e.g. 'Applied 8 of 9 derived fields') \u2014 if 0 fields were applied, say so loudly and explain what is still pending. In JOIN MODE there is no apply \u2014 state instead that the project was already configured and zero changes were made, without a fabricated applied count.",DEFAULT_BAPI_BASE_URL2="https://bridgegpt-api.com";function buildInstallBridgeSetupUrl(baseUrl){return`${baseUrl.replace(/\/+$/,"")}/setup`}var DEFAULT_BAPI_DOCS_DIR="docs/tmp";function getInstallBridgeUsage(baseUrl=DEFAULT_BAPI_BASE_URL2){let setupUrl=buildInstallBridgeSetupUrl(baseUrl);return["Usage:"," npx -y @bridge_gpt/mcp-server@latest install-bridge [flags]","","One-command Bridge API project bootstrap. Scaffolds the project, writes the","per-host MCP config with your credentials, verifies connectivity, persists the","routing credential, then \u2014 on a TTY, only after a Y/N consent prompt \u2014 opens a","fresh session in your selected tool. If the project is not yet configured it","derives the remaining config, presents a concise capability report, and","recommends /learn-repository. If the project is already configured it instead","joins you to it without proposing or applying any changes and just shows the","concise capability report (a b2b admin is additionally offered a teammate-invite","step). So not every run applies config fields. Indexing is never asked about \u2014","it starts automatically once the repository reaches full parse readiness.","","Your API key is written to a project MCP config only when that file is safe: a","valid, git-ignored config gets the real key, but a config already TRACKED by git","gets it only after an explicit default-No confirmation (and never at all in a","non-interactive run) \u2014 otherwise a secret-free entry is written and the server","resolves your key from the credential store at runtime. A config file that","cannot be parsed is left untouched, with manual-merge instructions printed.","","Run it bare \u2014 `install-bridge` with no flags \u2014 in a terminal and it asks",`\`${INSTALL_BRIDGE_KEY_SELECTOR_PROMPT}\` first, with three numbered choices:`,...INSTALL_BRIDGE_ONBOARDING_CHOICES.map(choice=>` ${choice}`),"There is NO default \u2014 pressing Enter selects nothing; you must type 1, 2, or 3","(one blank or invalid answer re-prompts once, then exits with guidance). Option 1","is the existing-key flow below, option 2 is the bootstrap-invite flow, and option 3","prompts for an email and creates a brand-new Bridge workspace for you (the","self-serve flow). That question is asked ONLY for a bare interactive run: passing","ANY flag, setting BAPI_API_KEY, or running without an interactive terminal keeps","the existing deterministic behavior and no prompt.","","Inputs (the only two irreducible ones):"," --api-key <key> Bridge API key OR bootstrap invite. Falls back to the"," BAPI_API_KEY env var, then an interactive (no-echo) prompt.",` Generate a key at ${setupUrl} (Security page) \u2014 this`," command consumes a key, it does not create one. A value"," detected as a bootstrap invite (bapi_inv_\u2026), from any of the"," three sources above, is instead redeemed to CREATE a new"," project and its first admin key \u2014 the same as --invite \u2014"," so it skips repository lookup entirely (--email and --invite"," remain the explicit, preferred entry points for a new project)."," NEVER printed or logged."," --repo <name> Repository name. --repo and BAPI_REPO_NAME still take"," priority and short-circuit before any network call. When"," neither is set, a compatible server resolves the unique"," repository from your existing API key automatically; if the"," server is older, the key is unresolvable, or resolution"," fails, it falls back to an inferred default you confirm"," interactively (and to a required --repo when stdin is"," non-interactive). In the existing-key flow it MUST match the"," server-side repo registration (it keys the credential store"," as bapi:<repo>). In either new-project flow (--email,"," --invite, or chooser option 2 or 3 above) it"," instead NAMES the project this run creates, so you are asked"," to name a new project rather than match an existing one; the"," name must be globally unique.","","Self-serve onboarding (no account, no API key, no pre-issued invite):"," --email <addr> Create a brand-new Bridge workspace from just an email \u2014"," the primary path for a first-time user with nothing yet."," It requests a fresh workspace for that email, then creates"," the project and mints your own admin API key in one command."," Falls back to the BAPI_SIGNUP_EMAIL env var, then a visible"," interactive prompt \u2014 which is also what chooser option 3 on a"," bare run reaches. The email is NOT a"," secret (it is shown as you type), but it is never printed to"," a log. Mutually"," exclusive with --api-key and --invite. No email verification"," is performed and no message is sent to the address \u2014 it only"," labels the new workspace.",""," RESUMABLE: a self-serve run that fails mid-protocol saves its"," signup state under bootstrap-pending:<repo> in the credential"," store. Re-running the self-serve flow RESUMES that attempt \u2014"," it does not sign up again \u2014 so a retry never creates a second"," workspace. Never copy, display, or hand-remove that record; if"," the saved invite has genuinely expired the CLI asks before"," discarding it.","","Bootstrap-invite onboarding (no web UI, no pre-existing key):"," --invite [token] Redeem a bootstrap invite you were already given: creates"," the project and mints your own admin API key in one command."," Mutually exclusive with --api-key and --email (in this mode"," the key is created, not consumed).",""," Run it WITHOUT a value \u2014 `install-bridge --invite` \u2014 and the"," token is read from an interactive prompt with echo"," suppressed, then sent only in the request body. This is the"," default and the recommended path: 'a copy/paste one-liner'"," and 'the token never touches shell history' are"," contradictory, so the one-liner your operator sends you is"," SECRET-FREE and the CLI asks for the token.",""," Passing the token inline (--invite <token>, --invite=<token>)"," or via BAPI_INVITE is for SCRIPTING ONLY: both forms EXPOSE"," THE TOKEN to your shell history and to the process list.","","Flags:"," --tools <ids> Comma-separated AI-coding tools to configure,"," bypassing the interactive picker. Accepted ids:",` ${HOST_PLATFORM_ORDER.join(", ")}.`," Both --tools=claude-code,codex and"," --tools claude-code,codex are accepted. On an"," interactive terminal WITHOUT this flag you get a"," numbered picker: enter one or more tool numbers,"," comma-separated (e.g. 1,3), and press Enter once \u2014"," no tool is pre-selected (not even Claude Code) and"," you must select at least one. A non-interactive"," (non-TTY) run without"," --tools writes the legacy automatic set (Claude"," Code plus any detected Cursor / Copilot VS Code)"," and never opens an agent terminal (it prints"," manual continuation instead). --tools= (empty) is"," an explicit empty selection: it writes nothing and"," launches nothing."," --force Overwrite an existing real BAPI_API_KEY in a"," host config (or in the credential store) without"," prompting. It does NOT bypass the git-tracked"," config safeguard below: --force authorizes"," replacing a credential, not disclosing your key"," into version control."," --dry-run Preview every step (scaffold targets, config"," files + keys with the key REDACTED, ping"," target, credential target, and the consent-gated"," launch outcome) without writing, pinging,"," prompting, or spawning anything. With --invite it"," also never calls the exchange endpoint and never"," generates or stores a secret."," --agent claude|cursor-agent Explicit launch override for the post-install"," session \u2014 it always wins, and there is NO Claude"," default. Without it the launch tool is derived"," from your selection: Claude Code opens `claude`."," Cursor is NOT auto-opened (its cursor-agent CLI"," first-run workspace-trust prompt collapses a spawned"," session); selecting Cursor writes .cursor/mcp.json"," and prints how to finish by running /install-bridge"," in a new Cursor session. A selection whose tools have"," no agentic CLI (e.g. Copilot) likewise opens nothing"," and prints how to finish configuring later. Passing"," --agent cursor-agent still force-spawns it."," -h, --help Show this help.","","Environment: BAPI_BASE_URL (default https://bridgegpt-api.com) and BAPI_DOCS_DIR","(default docs/tmp) are read from the environment with the shown fallbacks.","BAPI_SIGNUP_EMAIL supplies the self-serve signup email non-interactively (it is","visible input, not a secret). BAPI_INVITE supplies the bootstrap-invite token","non-interactively (scripting only \u2014 it is exposed to shell history; prefer the","prompt)."].join(`
4782
4794
  `)}function parseInstallBridgeArgs(argv){if(argv.includes("-h")||argv.includes("--help"))return{status:"help",usage:getInstallBridgeUsage()};let apiKey,repo,force=!1,dryRun=!1,agentName,invite,email,tools,inviteSupplied=!1,apiKeySupplied=!1,emailSupplied=!1,readValue=(arg,flag,i)=>arg.startsWith(`${flag}=`)?{value:arg.slice(flag.length+1),nextIndex:i}:i+1>=argv.length?{error:`${flag} requires a value.`}:{value:argv[i+1],nextIndex:i+1};for(let i=0;i<argv.length;i++){let arg=argv[i];if(arg==="--force"){force=!0;continue}if(arg==="--dry-run"){dryRun=!0;continue}if(arg==="--api-key"||arg.startsWith("--api-key=")){let r=readValue(arg,"--api-key",i);if("error"in r)return{status:"error",message:r.error};apiKey=r.value,apiKeySupplied=!0,i=r.nextIndex;continue}if(arg==="--invite"||arg.startsWith("--invite=")){if(inviteSupplied=!0,arg.startsWith("--invite="))invite=arg.slice(9);else{let next=argv[i+1];typeof next=="string"&&!next.startsWith("-")&&(invite=next,i+=1)}continue}if(arg==="--email"||arg.startsWith("--email=")){let r=readValue(arg,"--email",i);if("error"in r)return{status:"error",message:r.error};if(r.value.trim().length===0)return{status:"error",message:"--email requires a non-empty value."};email=r.value.trim(),emailSupplied=!0,i=r.nextIndex;continue}if(arg==="--repo"||arg.startsWith("--repo=")){let r=readValue(arg,"--repo",i);if("error"in r)return{status:"error",message:r.error};repo=r.value,i=r.nextIndex;continue}if(arg==="--tools"||arg.startsWith("--tools=")){let r=readValue(arg,"--tools",i);if("error"in r)return{status:"error",message:r.error};let parsed=parseToolsSelection(r.value);if("error"in parsed)return{status:"error",message:parsed.error};tools=parsed.tools,i=r.nextIndex;continue}if(arg==="--agent"||arg.startsWith("--agent=")){let r=readValue(arg,"--agent",i);if("error"in r)return{status:"error",message:r.error};if(!isAgentName(r.value))return{status:"error",message:`Invalid --agent value: '${r.value}' (allowed agents: ${formatValidAgentNames()}).`};agentName=r.value,i=r.nextIndex;continue}return arg.startsWith("-")?{status:"error",message:`Unknown flag: ${arg}`}:{status:"error",message:`Unexpected positional argument: '${arg}'. install-bridge does not accept positional arguments.`}}return inviteSupplied&&apiKeySupplied?{status:"error",message:"--invite and --api-key are mutually exclusive: a bootstrap invite creates your API key, it does not consume an existing one."}:emailSupplied&&apiKeySupplied?{status:"error",message:"--email and --api-key are mutually exclusive: self-serve signup creates your API key, it does not consume an existing one."}:emailSupplied&&inviteSupplied?{status:"error",message:"--email and --invite are mutually exclusive: use --email for self-serve signup (no pre-issued invite), or --invite to redeem an invite you already have."}:{status:"ok",options:{apiKey,repo,force,dryRun,agentName,invite,inviteMode:inviteSupplied,email,tools}}}function parseToolsSelection(value){let raw=value.split(",").map(s=>s.trim()).filter(s=>s.length>0),seen=new Set;for(let id of raw){if(!isHostPlatformId(id)){let allowed=HOST_PLATFORM_ORDER.join(", ");return{error:`Invalid --tools value: '${id}' (allowed tools: ${allowed}).`}}seen.add(id)}return{tools:HOST_PLATFORM_ORDER.filter(id=>seen.has(id))}}function promptSecretViaReadline(promptText,input=process.stdin,output=process.stderr){return new Promise(resolve2=>{let rl=readline3.createInterface({input,output,terminal:!0}),mutable=rl,muted=!1;mutable._writeToOutput=s=>{muted?s.includes(promptText)&&output.write(promptText):output.write(s)};let answered=!1;rl.on("close",()=>{answered||resolve2("")}),rl.question(promptText,answer=>{answered=!0,rl.close(),output.write(`
4783
4795
  `),resolve2(answer.trim())}),muted=!0})}var INSTALL_BRIDGE_CONNECT_GITHUB_COMMAND="npx -y @bridge_gpt/mcp-server connect-github",INSTALL_BRIDGE_GITHUB_OFFER_CONTEXT=["","Step 4b \u2014 optional: connect GitHub."," This installs the Bridge GitHub App so pull requests and code review work. It opens"," github.com in your browser; no GitHub credential is shared with Bridge.",` You can do this later instead: ${INSTALL_BRIDGE_CONNECT_GITHUB_COMMAND}`],INSTALL_BRIDGE_GITHUB_CONNECT_PROMPT="Connect GitHub? [y/N]: ";async function offerGithubConnection(repoName,baseUrl,deps,log){if(!(!deps.isTTY||!deps.promptLine))try{let credDeps={env:deps.env,homedir:deps.homedir,platform:deps.platform,readFile:deps.readFile,stat:deps.stat,stderr:()=>{}},cred=await resolveBapiCredentials(repoName,credDeps);if(!cred.ok)return;let api={fetch:deps.fetch,baseUrl,apiKey:cred.credentials.apiKey},state=await fetchGithubConfigurationState(api,repoName);if(state==="configured")return;if(state==="unavailable"){log(" note: could not read GitHub configuration status; skipping the GitHub offer.");return}for(let line of INSTALL_BRIDGE_GITHUB_OFFER_CONTEXT)log(line);let answer=(await deps.promptLine(INSTALL_BRIDGE_GITHUB_CONNECT_PROMPT)).trim().toLowerCase();if(answer!=="y"&&answer!=="yes")return;let connectDeps=createDefaultConnectGithubDeps();await runGithubConnectionFlow(connectDeps,api,repoName)!==0&&log(` note: GitHub was not connected. Your install is complete \u2014 connect GitHub later with 'npx -y @bridge_gpt/mcp-server@latest connect-github --repo ${repoName}'.`)}catch{log(` note: the GitHub connection offer could not run. Your install is complete \u2014 connect GitHub later with 'npx -y @bridge_gpt/mcp-server@latest connect-github --repo ${repoName}'.`)}}function promptLineViaReadline(promptText){return new Promise(resolve2=>{let rl=readline3.createInterface({input:process.stdin,output:process.stderr}),answered=!1;rl.on("close",()=>{answered||resolve2("")}),rl.question(promptText,answer=>{answered=!0,rl.close(),resolve2(answer.trim())})})}function sanitizePrewarmEnv(env){let sanitized={...env};return delete sanitized.BAPI_API_KEY,delete sanitized.BAPI_INVITE,delete sanitized.BAPI_SIGNUP_EMAIL,sanitized}function spawnPrewarmDefault(command,args,env){return new Promise(resolve2=>{let sanitizedEnv=sanitizePrewarmEnv(env);try{let child=spawn7(command,args,{shell:!1,stdio:"ignore",timeout:6e4,env:sanitizedEnv});child.on("error",()=>resolve2({ok:!1,warning:"the pre-warm process could not be started"})),child.on("close",(code,signal)=>{resolve2(signal?{ok:!1,warning:`the pre-warm process timed out or was terminated (${signal})`}:code===0?{ok:!0}:{ok:!1,warning:`the pre-warm process exited with code ${code}`})})}catch{resolve2({ok:!1,warning:"the pre-warm process could not be started"})}})}function createDefaultInstallBridgeDeps(){let isTTY=!!process.stdin.isTTY,productionFetch=(...args)=>fetch(...args);return{env:process.env,cwd:process.cwd(),platform:process.platform,homedir:os14.homedir,isTTY,readFile:p=>readFile11(p,"utf-8"),writeFile:(p,data,options)=>writeFile7(p,data,options),mkdir:(p,options)=>mkdir7(p,options),stat:p=>stat8(p),rename:(a,b)=>rename(a,b),chmod:(p,m)=>chmod(p,m),unlink:p=>unlink(p),open:async(p,flags,mode)=>{let handle=await open(p,flags,mode);return{writeFile:data=>handle.writeFile(data,{encoding:"utf-8"}),sync:()=>handle.sync(),close:()=>handle.close()}},randomBytes:size=>cryptoRandomBytes(size),promptSecret:isTTY?promptSecretViaReadline:void 0,promptLine:isTTY?promptLineViaReadline:void 0,promptMultiSelect:isTTY?promptMultiSelectViaReadline:void 0,vendor:createDefaultVendorProcessDeps(spawn7),fetch:productionFetch,resolveRepoViaServer:(baseUrl,apiKey)=>resolveRepoViaServer(productionFetch,baseUrl,apiKey),spawnPrewarm:spawnPrewarmDefault,runInit,upsertCredential:upsertBapiCredential,prepareBootstrapPending:prepareBootstrapPendingCredential,repointBootstrapPending:repointBootstrapPendingCredential,promoteBootstrapPending:promoteBootstrapPendingCredential,lookupSelfServeBootstrapPending:lookupSelfServeBootstrapPendingCredential,discardBootstrapPending:discardBootstrapPendingCredential,buildShellCommand:buildGenericAgentShellCommand,spawnTerminalTab:getDefaultSpawnTerminalTabForPlatform(process.platform),startTicketsDeps:createDefaultStartTicketsDeps(),log:m=>console.log(m),errorLog:m=>console.error(m),debugLog:m=>{process.env.BAPI_INSTALL_DEBUG&&console.error(m)}}}async function resolveApiKey(options,deps){if(typeof options.apiKey=="string"&&options.apiKey.trim().length>0)return{ok:!0,value:options.apiKey.trim(),source:"flag"};let fromEnv=deps.env.BAPI_API_KEY;if(typeof fromEnv=="string"&&fromEnv.trim().length>0)return{ok:!0,value:fromEnv.trim(),source:"env"};if(deps.isTTY&&deps.promptSecret){let entered=(await deps.promptSecret("Bridge API key or invite (input hidden): ")).trim();return entered.length>0?{ok:!0,value:entered,source:"prompt"}:{ok:!1,error:`No Bridge API key or invite entered. ${INSTALL_BRIDGE_ENTRY_ROUTES_ADVICE}`}}return{ok:!1,error:`A Bridge API key or invite is required (no interactive terminal is available to prompt for it). ${INSTALL_BRIDGE_ENTRY_ROUTES_ADVICE}`}}async function resolveInviteToken(options,deps){if(typeof options.invite=="string"&&options.invite.trim().length>0)return{ok:!0,value:options.invite.trim()};let fromEnv=deps.env.BAPI_INVITE;if(typeof fromEnv=="string"&&fromEnv.trim().length>0)return{ok:!0,value:fromEnv.trim()};if(deps.isTTY&&deps.promptSecret){let entered=(await deps.promptSecret("Bootstrap invite token (input hidden): ")).trim();return entered.length>0?{ok:!0,value:entered}:{ok:!1,error:"No bootstrap invite token entered."}}return{ok:!1,error:"A bootstrap invite token is required. Pass --invite <token> or set the BAPI_INVITE environment variable (no interactive terminal is available to prompt for it). Note that both forms expose the token to your shell history and process list \u2014 prefer running 'install-bridge --invite' interactively."}}async function resolveSignupEmail(options,deps){if(typeof options.email=="string"&&options.email.trim().length>0)return{ok:!0,value:options.email.trim()};let fromEnv=deps.env.BAPI_SIGNUP_EMAIL;if(typeof fromEnv=="string"&&fromEnv.trim().length>0)return{ok:!0,value:fromEnv.trim()};if(deps.isTTY&&deps.promptLine){let entered=(await deps.promptLine("Email for Bridge workspace setup: ")).trim();return entered.length>0?{ok:!0,value:entered}:{ok:!1,error:"No email entered."}}return{ok:!1,error:"An email is required to create a Bridge workspace. Pass --email <addr> or set the BAPI_SIGNUP_EMAIL environment variable (no interactive terminal is available to prompt for it)."}}function resolveInstallBridgeOnboardingBranch(options,env){return options.inviteMode===!0||(env.BAPI_INVITE??"").trim().length>0?{kind:"need-key",method:"bootstrap-invite"}:(options.email??"").trim().length>0||(env.BAPI_SIGNUP_EMAIL??"").trim().length>0?{kind:"need-key",method:"self-serve"}:{kind:"have-key"}}var INSTALL_BRIDGE_KEY_SELECTOR_PROMPT="How would you like to connect to Bridge API?",INSTALL_BRIDGE_ONBOARDING_CHOICES=["1. I have a Bridge API key","2. I have an invite token","3. I'm new \u2014 set me up with just my email"],INSTALL_BRIDGE_ONBOARDING_CHOICE_PROMPT="Enter 1, 2, or 3: ",INSTALL_BRIDGE_ONBOARDING_CHOICE_HINT="Enter 1, 2, or 3.",INSTALL_BRIDGE_ENTRY_ROUTES_ADVICE="Re-run install-bridge and choose an option: pass --api-key <key> if you have a Bridge API key (or set BAPI_API_KEY), --invite if you were sent an invite token, or --email <addr> to sign up with just an email \u2014 on a bare interactive run, re-run and choose option 3 to sign up with just an email.";async function resolveInstallBridgeOnboardingBranchForRun(options,deps,argv){let branch=resolveInstallBridgeOnboardingBranch(options,deps.env);if(branch.kind==="need-key")return{ok:!0,branch};let hasEnvApiKey=(deps.env.BAPI_API_KEY??"").trim().length>0,isBareInvocation=argv.length===0;if(!deps.isTTY||!deps.promptLine||!isBareInvocation||hasEnvApiKey)return{ok:!0,branch};let promptLine=deps.promptLine;try{deps.log(INSTALL_BRIDGE_KEY_SELECTOR_PROMPT);for(let choice of INSTALL_BRIDGE_ONBOARDING_CHOICES)deps.log(choice);for(let attempt=0;attempt<2;attempt+=1){let answer=(await promptLine(INSTALL_BRIDGE_ONBOARDING_CHOICE_PROMPT)).trim();if(answer==="1")return{ok:!0,branch:{kind:"have-key"}};if(answer==="2")return{ok:!0,branch:{kind:"need-key",method:"bootstrap-invite"}};if(answer==="3")return{ok:!0,branch:{kind:"need-key",method:"self-serve"}};attempt===0&&deps.log(INSTALL_BRIDGE_ONBOARDING_CHOICE_HINT)}return{ok:!1,error:`No option was selected. ${INSTALL_BRIDGE_ENTRY_ROUTES_ADVICE}`}}catch{return{ok:!1,error:`Could not read your answer from the terminal. ${INSTALL_BRIDGE_ENTRY_ROUTES_ADVICE}`}}}function resolveConfiguredRepoName(options,env){if(typeof options.repo=="string"&&options.repo.trim().length>0)return options.repo.trim();let fromEnv=env.BAPI_REPO_NAME;if(typeof fromEnv=="string"&&fromEnv.trim().length>0)return fromEnv.trim()}async function resolveRepoName(options,deps,mode="existing-registration"){let configured=resolveConfiguredRepoName(options,deps.env);if(configured!==void 0)return{ok:!0,value:configured};if(!deps.isTTY||!deps.promptLine)return{ok:!1,error:mode==="new-project"?"A project name is required. Pass --repo or set the BAPI_REPO_NAME environment variable (no interactive terminal is available to confirm an inferred name). It names the new Bridge project this run creates and must be globally unique.":"A repo name is required. Pass --repo or set the BAPI_REPO_NAME environment variable (no interactive terminal is available to confirm an inferred name). It must match the server-side repository registration."};let inferred=await resolveStartTicketsRepoName({env:deps.env,cwd:deps.cwd,readFile:deps.readFile});if(!inferred){let validated=validateRepoName(path28.basename(deps.cwd));validated.ok&&(inferred=validated.value)}if(inferred){let promptText=mode==="new-project"?`Name your new Bridge project [${inferred}]: `:`Repo name [${inferred}] (must match server-side registration): `,answer=(await deps.promptLine(promptText)).trim(),chosen=answer.length>0?answer:inferred;if(chosen.length>0)return{ok:!0,value:chosen}}else{let promptText=mode==="new-project"?"Name your new Bridge project: ":"Repo name (must match server-side registration): ",answer=(await deps.promptLine(promptText)).trim();if(answer.length>0)return{ok:!0,value:answer}}return{ok:!1,error:"No repo name provided."}}var INSTALL_BRIDGE_TOOL_SELECTOR_PROMPT="Which AI coding tools do you use on this project?",SELECTION_TOKEN_PATTERN=/^[1-9][0-9]*$/;function promptMultiSelectViaReadline(promptText,options,input=process.stdin,output=process.stderr){return options.length===0?Promise.resolve([]):new Promise(resolve2=>{let rl=readline3.createInterface({input,output}),settled=!1,finish=result=>{settled||(settled=!0,rl.close(),resolve2(result))};rl.on("close",()=>finish([])),output.write(`
4784
4796
  ${promptText}
@@ -1,2 +1,2 @@
1
1
  // AUTO-GENERATED — do not edit manually. Regenerate with: npm run build
2
- export const VERSION = "0.2.32";
2
+ export const VERSION = "0.2.33";
@@ -76,10 +76,96 @@ expires after about 15 minutes, so a stale attempt is never left half-applied.
76
76
  If an organization owns the repository and you are not an owner, GitHub sends an
77
77
  **approval request** to an owner instead of installing the app. That approval happens
78
78
  entirely on GitHub's side and **does not return to your terminal**, so `connect-github`
79
- cannot wait for it — the original request expires. This is a real limitation, not a bug.
79
+ cannot wait for it — the command exits, and the 15-minute request expires.
80
80
 
81
- Once an owner has approved the install, finish the connection with
82
- [Option C](#option-c--manual-install--installation-id-fallback) below.
81
+ You do not have to fall back to a manual Installation ID for this. Use
82
+ [Option A-2 — delegated handoff](#option-a-2--delegated-handoff-someone-else-installs-it)
83
+ instead: its link stays valid for 72 hours, `--resume` reports
84
+ `awaiting organization approval` while you wait, and once an owner approves you finish
85
+ the connection normally.
86
+
87
+ ---
88
+
89
+ ## Option A-2 — Delegated handoff (someone else installs it)
90
+
91
+ Use this when **you hold the Bridge API key but someone else administers the GitHub
92
+ organization**. Options A and B both assume one person has both; in most organizations
93
+ they are two people.
94
+
95
+ You mint a shareable link, send it to whoever can install the app, and finish the
96
+ connection yourself afterwards. The other person needs **only the link and a browser** —
97
+ no Bridge account, no API key, no CLI, no MCP server, and no access to your project.
98
+
99
+ ### 1. Mint the link
100
+
101
+ ```bash
102
+ npx -y @bridge_gpt/mcp-server@latest connect-github --handoff --repo <repo_name>
103
+ ```
104
+
105
+ `--repo` is **required** here: unlike the default flow, minting does not prompt, so it
106
+ will not guess a project name for a link it is about to hand out. The command prints the
107
+ link on stdout, does not open a browser, and does not wait. Only an **admin** on the
108
+ project may mint one.
109
+
110
+ The link is valid for **72 hours** and is **single-use**.
111
+
112
+ ### 2. Send it to your GitHub org admin
113
+
114
+ They open it, choose the repositories under **Repository access**, and click **Install**.
115
+ They then land on a Bridge page that tells them whether their part succeeded — and
116
+ nothing else. It names no project, repository, owner, organization, or person.
117
+
118
+ Bridge never learns who clicked the link. The audit record names the GitHub installation
119
+ and the admin who minted the handoff.
120
+
121
+ ### 3. Finish the connection
122
+
123
+ There is deliberately **no notification** when they are done — ask them to ping you. Then,
124
+ from any machine holding the project's API key:
125
+
126
+ ```bash
127
+ npx -y @bridge_gpt/mcp-server@latest connect-github --resume --repo <repo_name>
128
+ ```
129
+
130
+ `--resume` looks the handoff up **server-side**, so you do not need to have kept anything
131
+ from step 1 — not the link, not the terminal session, not even the same machine. It
132
+ reports the current state and, when the install has completed, shows the repositories and
133
+ asks which one to connect, exactly like the default flow. Every binding is still confirmed
134
+ by hand.
135
+
136
+ Possible states `--resume` will report:
137
+
138
+ | State | Meaning |
139
+ |---|---|
140
+ | `pending` | Nobody has opened the link yet. |
141
+ | `awaiting organization approval` | They requested the install; a GitHub org owner must approve it. The link stays usable until it expires. |
142
+ | `staged` | Ready — pick a repository and connect. |
143
+ | `expired` / `revoked` | Mint a new link with `--handoff`. |
144
+ | `connected` | Already finished. |
145
+
146
+ **A staged handoff stays confirmable after the link's 72-hour deadline.** The deadline
147
+ bounds how long the other person has to *click*, not how long you have to *confirm* —
148
+ confirmation re-verifies your access with GitHub at that moment, so a multi-day gap is
149
+ safe. Only the click window expires.
150
+
151
+ ### Revoking a link
152
+
153
+ ```bash
154
+ npx -y @bridge_gpt/mcp-server@latest connect-github --revoke --repo <repo_name>
155
+ ```
156
+
157
+ Minting a new link **does not** invalidate an outstanding one — revoke it explicitly if
158
+ it went to the wrong person or is no longer wanted. Revocation takes effect immediately:
159
+ a revoked link can no longer install, and a revoked handoff can no longer be confirmed
160
+ even if it had already staged.
161
+
162
+ ### If the app is already installed on that organization
163
+
164
+ If the GitHub App is *already* installed on the target organization, GitHub may not send
165
+ Bridge a callback at all when your link is opened. `--resume` will keep reporting
166
+ `pending` until the link expires, and will point you at the fallback below. Bridge does
167
+ not claim to have detected an installation it cannot safely see. In that case use
168
+ [Option C](#option-c--manual-install--installation-id-fallback).
83
169
 
84
170
  ---
85
171
 
@@ -118,11 +204,13 @@ if you want it.
118
204
 
119
205
  ## Option C — Manual install + Installation ID (fallback)
120
206
 
121
- This is the **advanced fallback**, not the normal path — use it only if neither the
122
- `connect-github` command (Option A) nor the one-click **Connect GitHub** button (Option B)
123
- is available to you (for example an org admin-approval or GitHub Marketplace install), or
124
- if automatic linking failed. Get Started links to it from the manual-fallback note under
125
- the GitHub panel.
207
+ This is the **advanced fallback**, not the normal path — use it only if none of the
208
+ `connect-github` command (Option A), the
209
+ [delegated handoff](#option-a-2--delegated-handoff-someone-else-installs-it) (Option A-2),
210
+ or the one-click **Connect GitHub** button (Option B) is available to you, or if automatic
211
+ linking failed. The main case that still lands here is a GitHub App **already installed**
212
+ on the owning organization, where GitHub may send no callback for Bridge to act on. Get
213
+ Started links to it from the manual-fallback note under the GitHub panel.
126
214
 
127
215
  ### 1. Install the app
128
216
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bridge_gpt/mcp-server",
3
- "version": "0.2.32",
3
+ "version": "0.2.33",
4
4
  "description": "Bridge API MCP server — exposes Jira endpoints as MCP tools for Claude Code agents",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -25,7 +25,7 @@
25
25
  "check:version-generated": "node scripts/bundle-version.js && node scripts/check-version-generated.js",
26
26
  "postbuild": "node scripts/prepend-shebang.cjs",
27
27
  "start": "node build/index.js",
28
- "test": "node --test --test-force-exit build/pipeline-utils.test.js build/backend-warnings.test.js build/update-check.test.js build/cli-upgrade.test.js build/decision-page-schema.test.js build/decision-page-template.test.js build/bundle-pipelines.test.js build/instructions-contract.test.js build/pipeline-orchestrator-persistence.test.js build/pipeline-orchestrator-execution.test.js build/pipeline-orchestrator-integration.test.js build/index-static.test.js build/config-fields.static.test.js build/request-brainstorm.static.test.js build/execute-plan-instructions.static.test.js build/index-resolvers.test.js build/index-project-root.test.js build/index-pipelines.test.js build/index.test.js build/bridge-config.test.js build/credential-store.test.js build/agent-config-credential-migration.test.js build/mcp-invoke.test.js build/mcp-provisioning.test.js build/mcp-server-invocation.test.js build/third-party-mcp-targets.test.js build/git-ignore-utils.test.js build/command-provisioning.test.js build/command-assets-doctor.test.js build/credential-materialization.test.js build/mcp-registration-doctor.test.js build/mcp-host-targets.test.js build/mcp-install-state.test.js build/mcp-host-config.test.js build/secret-safety.test.js build/base-ref.test.js build/pr-base-contract.test.js build/start-tickets.test.js build/review-tickets.test.js build/start-tickets-base-branch.test.js build/agent-registry.test.js build/agent-registry.model-routing.test.js build/start-tickets.shell-model-routing.test.js build/start-tickets.bridge-api-model-routing.test.js build/start-tickets.tier-fetch-model-routing.test.js build/start-tickets.resolve-model-routing.test.js build/start-tickets.orchestrate-model-routing.test.js build/start-tickets.routing-diagnostics.test.js build/start-tickets-repo.test.js build/start-tickets-credential-invariants.static.test.js build/credentials-cli.test.js build/start-tickets-prereqs.test.js build/regression-check.test.js build/doctor.test.js build/install-doctor.test.js build/install-bridge.test.js build/install-bridge-invite.test.js build/install-bridge-prompt.test.js build/install-bridge-tools.test.js build/install-bridge-join-static.test.js build/init.test.js build/init-docs.test.js build/resolveUploadAttachment.test.js build/package-static.test.js build/chain-utils.test.js build/chain-orchestrator.test.js build/scheduler-backends/types.test.js build/scheduler-backends/escaping.test.js build/scheduler-backends/launchd.test.js build/scheduler-backends/task-scheduler.test.js build/scheduler-backends/systemd-user.test.js build/scheduler-backends/at-fallback.test.js build/scheduler-backends/index.test.js build/command-catalog.test.js build/scheduled-prompt.test.js build/agent-launchers/claude.test.js build/agent-launchers/cursor.test.js build/agent-launchers/index.test.js build/schedule-store.test.js build/schedule-run.test.js build/setup-epic.test.js build/conductor-bundle-artifacts.test.js build/conductor-bundle-cli.test.js build/agent-capabilities/cli.test.js build/agent-capabilities/runner.test.js build/agent-capabilities/probes.test.js build/agent-capabilities/probe-context.test.js build/agent-capabilities/reporter.test.js build/conductor/taxonomy-and-errors.test.js build/conductor/github-mergeability.test.js build/conductor/merge-conflict-routing.test.js build/conductor/redaction-normalization.test.js build/conductor/claude-hook.test.js build/conductor/git-ci-types.test.js build/conductor/done-gate.test.js build/conductor/git-ci-taxonomy-payload.test.js build/conductor/bridge-api-client.test.js build/conductor/plan.test.js build/conductor/producer-ledger.test.js build/conductor/spec-review-producer.test.js build/conductor/git-producer.test.js build/conductor/git-hooks.test.js build/conductor/store-migration.test.js build/conductor/pr-discovery.test.js build/conductor/pr-ci-producer.test.js build/conductor/pr-review-producer.test.js build/conductor/doctor.test.js build/conductor/deny-enforcement-preflight.test.js build/conductor/errors.test.js build/conductor/store.test.js build/conductor/index-poll-ci-producer.test.js build/start-tickets-conductor.test.js build/start-tickets-conductor.spawn.test.js build/start-tickets-conductor-node-propagation.test.js build/start-tickets.non-mutating-base.test.js build/start-tickets-live-source-guard.test.js build/conductor/worker-ledger-cli.test.js build/conductor/supervisor-config.test.js build/conductor/supervisor-ledger.test.js build/conductor/supervisor-state-reducer.test.js build/conductor/supervisor-housekeeping-projection.test.js build/conductor/supervisor-escalation.test.js build/conductor/supervisor-retired-judgment.static.test.js build/conductor/supervisor-runtime.test.js build/conductor/supervisor-store-projection.test.js build/conductor/supervisor-cli.test.js build/conductor/supervisor-start-tickets.test.js build/conductor/supervisor-message-relay.test.js build/conductor/supervisor-state-message-events.test.js build/conductor/store-message-relay.test.js build/start-tickets-message-relay.test.js build/conductor/event-accessors.test.js build/conductor/merge-ledger.test.js build/conductor/deterministic-completion.static.test.js build/conductor/deterministic-completion.integration.test.js build/conductor/local-merge.test.js build/conductor/local-merge.static.test.js build/conductor/supervisor-merge.test.js build/conductor/bridge-api-merge-client.test.js build/conductor/bridge-api-epic-client.test.js build/conductor/supervisor-merge-runtime-state.test.js build/conductor/epic-state.test.js build/executor/claude-settings.test.js build/executor/cli.test.js build/executor/credentials.test.js build/executor/echo-acceptance.test.js build/executor/env.test.js build/executor/base-branch.test.js build/executor/heartbeat.test.js build/executor/http-client.test.js build/executor/job-runner.test.js build/executor/job-runner.payload-timeout.test.js build/executor/worker-finalization.test.js build/executor/job-runner.static.test.js build/executor/prompt-spec.test.js build/executor/job-types.test.js build/executor/recovery-job.static.test.js build/executor/observation.test.js build/executor/permissions.test.js build/executor/preflight.test.js build/executor/process.test.js build/executor/results.test.js build/executor/runner.test.js build/executor/terminal-mutation.test.js build/executor/worker-command.test.js build/executor/worktree-core.test.js build/executor/worktree.test.js build/executor/job-errors.test.js build/executor/worktree-inspection.test.js build/executor/resume-pre-spawn.test.js build/executor/verdict-artifact.test.js build/executor/worker-log.test.js build/executor/job-log-registry.test.js build/executor/viewer-tabs.test.js build/executor/watch-cli.test.js build/executor/merge-job.test.js build/executor/job-runner.merge.test.js build/executor/worktree-gc.test.js build/executor/runner.job-behaviors.test.js build/conductor/cli-freeze.test.js build/conductor/epic-reconcile.test.js build/conductor/epic-runtime.test.js build/conductor/epic-tick-sequence.test.js build/conductor/epic-runtime-post-action.test.js build/conductor/file-scope-guard.test.js build/conductor/file-scope-guard.integration.test.js build/mcp-profile.test.js build/mcp-profile-registration.test.js build/env-flags.test.js build/bridge-api-urls.test.js build/tool-surface-gating.test.js build/tools-budget.test.js build/visual-diff-worker.test.js build/visual-diff.test.js build/estimate-epic.test.js build/integration/measure-tools.test.js build/sfcc/config.test.js build/sfcc/ocapi-shape.test.js build/sfcc/output.test.js build/sfcc/credentials.test.js build/sfcc/ocapi-write-faults.test.js build/sfcc/write-guard.test.js build/sfcc/write-grants.test.js build/sfcc/write-result.test.js build/sfcc/writes.test.js build/sfcc/writes-system-object-payloads.test.js build/sfcc/writes-payloads.test.js build/sfcc/log-gate.test.js build/sfcc/log-query.test.js && node --experimental-test-module-mocks --test --test-force-exit build/index-heavy-read-truncation.test.js build/index-artifacts.test.js build/automation-progress.test.js build/recovery-formatting.test.js build/wait-for-result.test.js build/ticket-wait-recovery.test.js build/council-wait-recovery.test.js build/index.review-rounds.test.js build/index-brainstorm-filenames.test.js build/index-output-path.test.js build/index-generate-decision-page.test.js build/index-generate-decision-page.integration.test.js build/visual-diff.registration.test.js build/visual-diff.attachment-adapter.test.js build/attachment-download.test.js build/attachment-upload.test.js build/conductor/paths.test.js build/conductor/store-lifecycle.test.js build/conductor/store-queries.test.js build/conductor/tools.test.js build/conductor/cli.test.js build/conductor/security-regressions.test.js build/conductor/git-inspection.test.js build/conductor/tools-done-gate.test.js build/conductor/pr-ci-producer-emit-seam.test.js build/conductor/cli-git-hooks.test.js build/sfcc/client.test.js build/sfcc/tool-wrapper.test.js build/sfcc/setup-status.test.js build/sfcc/permissions.test.js build/sfcc/register.test.js build/sfcc/reads-system-object.test.js build/sfcc/reads-custom-object-def.test.js build/sfcc/reads-site-preference.test.js build/sfcc/writes-system-object.test.js build/sfcc/writes-custom-object-def.test.js build/sfcc/writes-site-preference.test.js build/connect-github.test.js build/connect-github-api.test.js",
28
+ "test": "node --test --test-force-exit build/pipeline-utils.test.js build/backend-warnings.test.js build/update-check.test.js build/cli-upgrade.test.js build/decision-page-schema.test.js build/decision-page-template.test.js build/bundle-pipelines.test.js build/instructions-contract.test.js build/pipeline-orchestrator-persistence.test.js build/pipeline-orchestrator-execution.test.js build/pipeline-orchestrator-integration.test.js build/index-static.test.js build/config-fields.static.test.js build/request-brainstorm.static.test.js build/execute-plan-instructions.static.test.js build/index-resolvers.test.js build/index-project-root.test.js build/index-pipelines.test.js build/index.test.js build/bridge-config.test.js build/credential-store.test.js build/agent-config-credential-migration.test.js build/mcp-invoke.test.js build/mcp-provisioning.test.js build/mcp-server-invocation.test.js build/third-party-mcp-targets.test.js build/git-ignore-utils.test.js build/command-provisioning.test.js build/command-assets-doctor.test.js build/credential-materialization.test.js build/mcp-registration-doctor.test.js build/mcp-host-targets.test.js build/mcp-install-state.test.js build/mcp-host-config.test.js build/secret-safety.test.js build/base-ref.test.js build/pr-base-contract.test.js build/start-tickets.test.js build/review-tickets.test.js build/start-tickets-base-branch.test.js build/agent-registry.test.js build/agent-registry.model-routing.test.js build/start-tickets.shell-model-routing.test.js build/start-tickets.bridge-api-model-routing.test.js build/start-tickets.tier-fetch-model-routing.test.js build/start-tickets.resolve-model-routing.test.js build/start-tickets.orchestrate-model-routing.test.js build/start-tickets.routing-diagnostics.test.js build/start-tickets-repo.test.js build/start-tickets-credential-invariants.static.test.js build/credentials-cli.test.js build/start-tickets-prereqs.test.js build/regression-check.test.js build/doctor.test.js build/install-doctor.test.js build/install-bridge.test.js build/install-bridge-invite.test.js build/install-bridge-prompt.test.js build/install-bridge-tools.test.js build/install-bridge-join-static.test.js build/init.test.js build/init-docs.test.js build/resolveUploadAttachment.test.js build/package-static.test.js build/chain-utils.test.js build/chain-orchestrator.test.js build/scheduler-backends/types.test.js build/scheduler-backends/escaping.test.js build/scheduler-backends/launchd.test.js build/scheduler-backends/task-scheduler.test.js build/scheduler-backends/systemd-user.test.js build/scheduler-backends/at-fallback.test.js build/scheduler-backends/index.test.js build/command-catalog.test.js build/scheduled-prompt.test.js build/agent-launchers/claude.test.js build/agent-launchers/cursor.test.js build/agent-launchers/index.test.js build/schedule-store.test.js build/schedule-run.test.js build/setup-epic.test.js build/conductor-bundle-artifacts.test.js build/conductor-bundle-cli.test.js build/agent-capabilities/cli.test.js build/agent-capabilities/runner.test.js build/agent-capabilities/probes.test.js build/agent-capabilities/probe-context.test.js build/agent-capabilities/reporter.test.js build/conductor/taxonomy-and-errors.test.js build/conductor/github-mergeability.test.js build/conductor/merge-conflict-routing.test.js build/conductor/redaction-normalization.test.js build/conductor/claude-hook.test.js build/conductor/git-ci-types.test.js build/conductor/done-gate.test.js build/conductor/git-ci-taxonomy-payload.test.js build/conductor/bridge-api-client.test.js build/conductor/plan.test.js build/conductor/producer-ledger.test.js build/conductor/spec-review-producer.test.js build/conductor/git-producer.test.js build/conductor/git-hooks.test.js build/conductor/store-migration.test.js build/conductor/pr-discovery.test.js build/conductor/pr-ci-producer.test.js build/conductor/pr-review-producer.test.js build/conductor/doctor.test.js build/conductor/deny-enforcement-preflight.test.js build/conductor/errors.test.js build/conductor/store.test.js build/conductor/index-poll-ci-producer.test.js build/start-tickets-conductor.test.js build/start-tickets-conductor.spawn.test.js build/start-tickets-conductor-node-propagation.test.js build/start-tickets.non-mutating-base.test.js build/start-tickets-live-source-guard.test.js build/conductor/worker-ledger-cli.test.js build/conductor/supervisor-config.test.js build/conductor/supervisor-ledger.test.js build/conductor/supervisor-state-reducer.test.js build/conductor/supervisor-housekeeping-projection.test.js build/conductor/supervisor-escalation.test.js build/conductor/supervisor-retired-judgment.static.test.js build/conductor/supervisor-runtime.test.js build/conductor/supervisor-store-projection.test.js build/conductor/supervisor-cli.test.js build/conductor/supervisor-start-tickets.test.js build/conductor/supervisor-message-relay.test.js build/conductor/supervisor-state-message-events.test.js build/conductor/store-message-relay.test.js build/start-tickets-message-relay.test.js build/conductor/event-accessors.test.js build/conductor/merge-ledger.test.js build/conductor/deterministic-completion.static.test.js build/conductor/deterministic-completion.integration.test.js build/conductor/local-merge.test.js build/conductor/local-merge.static.test.js build/conductor/supervisor-merge.test.js build/conductor/bridge-api-merge-client.test.js build/conductor/bridge-api-epic-client.test.js build/conductor/supervisor-merge-runtime-state.test.js build/conductor/epic-state.test.js build/executor/claude-settings.test.js build/executor/cli.test.js build/executor/credentials.test.js build/executor/echo-acceptance.test.js build/executor/env.test.js build/executor/base-branch.test.js build/executor/heartbeat.test.js build/executor/http-client.test.js build/executor/job-runner.test.js build/executor/job-runner.payload-timeout.test.js build/executor/worker-finalization.test.js build/executor/job-runner.static.test.js build/executor/prompt-spec.test.js build/executor/job-types.test.js build/executor/recovery-job.static.test.js build/executor/observation.test.js build/executor/permissions.test.js build/executor/preflight.test.js build/executor/process.test.js build/executor/results.test.js build/executor/runner.test.js build/executor/terminal-mutation.test.js build/executor/worker-command.test.js build/executor/worktree-core.test.js build/executor/worktree.test.js build/executor/job-errors.test.js build/executor/worktree-inspection.test.js build/executor/resume-pre-spawn.test.js build/executor/verdict-artifact.test.js build/executor/worker-log.test.js build/executor/job-log-registry.test.js build/executor/viewer-tabs.test.js build/executor/watch-cli.test.js build/executor/merge-job.test.js build/executor/job-runner.merge.test.js build/executor/worktree-gc.test.js build/executor/runner.job-behaviors.test.js build/conductor/cli-freeze.test.js build/conductor/epic-reconcile.test.js build/conductor/epic-runtime.test.js build/conductor/epic-tick-sequence.test.js build/conductor/epic-runtime-post-action.test.js build/conductor/file-scope-guard.test.js build/conductor/file-scope-guard.integration.test.js build/mcp-profile.test.js build/mcp-profile-registration.test.js build/env-flags.test.js build/bridge-api-urls.test.js build/tool-surface-gating.test.js build/tools-budget.test.js build/visual-diff-worker.test.js build/visual-diff.test.js build/estimate-epic.test.js build/integration/measure-tools.test.js build/sfcc/config.test.js build/sfcc/ocapi-shape.test.js build/sfcc/output.test.js build/sfcc/credentials.test.js build/sfcc/ocapi-write-faults.test.js build/sfcc/write-guard.test.js build/sfcc/write-grants.test.js build/sfcc/write-result.test.js build/sfcc/writes.test.js build/sfcc/writes-system-object-payloads.test.js build/sfcc/writes-payloads.test.js build/sfcc/log-gate.test.js build/sfcc/log-query.test.js && node --experimental-test-module-mocks --test --test-force-exit build/index-heavy-read-truncation.test.js build/index-artifacts.test.js build/automation-progress.test.js build/recovery-formatting.test.js build/wait-for-result.test.js build/ticket-wait-recovery.test.js build/council-wait-recovery.test.js build/index.review-rounds.test.js build/index-brainstorm-filenames.test.js build/index-output-path.test.js build/index-generate-decision-page.test.js build/index-generate-decision-page.integration.test.js build/visual-diff.registration.test.js build/visual-diff.attachment-adapter.test.js build/attachment-download.test.js build/attachment-upload.test.js build/conductor/paths.test.js build/conductor/store-lifecycle.test.js build/conductor/store-queries.test.js build/conductor/tools.test.js build/conductor/cli.test.js build/conductor/security-regressions.test.js build/conductor/git-inspection.test.js build/conductor/tools-done-gate.test.js build/conductor/pr-ci-producer-emit-seam.test.js build/conductor/cli-git-hooks.test.js build/sfcc/client.test.js build/sfcc/tool-wrapper.test.js build/sfcc/setup-status.test.js build/sfcc/permissions.test.js build/sfcc/register.test.js build/sfcc/reads-system-object.test.js build/sfcc/reads-custom-object-def.test.js build/sfcc/reads-site-preference.test.js build/sfcc/writes-system-object.test.js build/sfcc/writes-custom-object-def.test.js build/sfcc/writes-site-preference.test.js build/connect-github.test.js build/connect-github-api.test.js build/connect-github-handoff.test.js build/connect-github-dispatch.static.test.js",
29
29
  "test:integration": "node --test build/integration/refresh-main.integration.test.js build/integration/command-provisioning.integration.test.js build/integration/start-tickets.integration.test.js build/integration/start-tickets-tier-handoff.integration.test.js build/integration/doctor.integration.test.js build/integration/agent-capabilities.integration.test.js build/integration/conductor-producer.integration.test.js build/integration/conductor-message-relay.integration.test.js build/integration/executor-http-runner.integration.test.js build/integration/executor-job-behaviors.integration.test.js build/integration/executor-recovery-jobs.integration.test.js build/integration/executor-spec-review-prompt.integration.test.js build/integration/resume-pre-spawn.git.integration.test.js build/integration/worker-finalization-origin.integration.test.js build/integration/post-remediation-merge-ci-wait.integration.test.js build/integration/executor-merge-supervision.integration.test.js build/integration/attachment-binary-roundtrip.integration.test.js build/integration/dependent-ticket-fresh-base.integration.test.js build/integration/execute-plan-instructions.integration.test.js build/integration/conductor-bundle-artifacts.integration.test.js build/integration/install-bridge-repo-resolution.integration.test.js build/integration/capability-report-contract.integration.test.js build/integration/request-brainstorm-general.integration.test.js build/integration/request-council-trigger-drop.integration.test.js build/integration/install-bridge-onboarding-launch.integration.test.js build/integration/install-bridge-failure-guards.integration.test.js build/integration/learn-repository-pipeline.integration.test.js build/integration/visual-diff-mcp.integration.test.js",
30
30
  "test:smoke": "node --test build/integration/packaged-cli-smoke.test.js",
31
31
  "canary:agent-capabilities": "npm run build && node scripts/agent-capabilities-canary.mjs",