@bridge_gpt/mcp-server 0.2.53 → 0.2.55
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.
- package/README.md +116 -56
- package/build/agent-guidance.generated.js +3 -0
- package/build/agent-launchers/claude-executor-adapter.js +3 -0
- package/build/agent-launchers/claude.js +3 -3
- package/build/agent-launchers/prompt.js +8 -11
- package/build/agent-notes.js +178 -0
- package/build/agent-registry.js +5 -2
- package/build/agent-utils.js +58 -0
- package/build/agents.generated.js +1 -1
- package/build/base-ref.js +33 -9
- package/build/bounded-wait.js +174 -0
- package/build/codex-skill-adapter.js +55 -0
- package/build/commands.generated.js +6 -5
- package/build/conductor/bridge-api-client.js +235 -14
- package/build/conductor/bring-up-facts.js +187 -0
- package/build/conductor/claude-hook.js +7 -5
- package/build/conductor/cli.js +28 -0
- package/build/conductor/doctor.js +80 -9
- package/build/conductor/epic-implementer-cli.js +1298 -0
- package/build/conductor/epic-runtime.js +134 -98
- package/build/conductor/errors.js +2 -2
- package/build/conductor/git-ci-types.js +1 -1
- package/build/conductor/git-hooks.js +28 -14
- package/build/conductor/install-doctor.js +11 -5
- package/build/conductor/readiness-cli.js +10 -10
- package/build/conductor/readiness-sections.js +58 -9
- package/build/conductor/readiness.js +120 -4
- package/build/conductor/recovery-cli.js +289 -10
- package/build/conductor/recovery-operations.js +125 -2
- package/build/conductor/repair-contract.js +58 -0
- package/build/conductor/run-branch.js +137 -0
- package/build/conductor/store.js +2 -2
- package/build/conductor/supervisor-runtime.js +1 -1
- package/build/conductor/test-run-branch-vectors.js +165 -0
- package/build/conductor-bin.js +2 -139
- package/build/conductor-claude-hook-bin.js +2 -2
- package/build/conductor-claude-hook-removed-stub-bin.js +31 -0
- package/build/conductor-removed-stub-bin.js +30 -0
- package/build/docs.generated.js +1 -1
- package/build/doctor.js +145 -18
- package/build/drive-epic.js +752 -90
- package/build/epic-implementer-bin.js +145 -0
- package/build/epic-implementer-bundle-cli.js +264 -0
- package/build/epic-implementer-claude-hook-bin.js +3 -0
- package/build/epic-integration-pr.js +5 -3
- package/build/executor/claim-scope.js +104 -0
- package/build/executor/cli.js +14 -25
- package/build/executor/env-file-guard.js +82 -3
- package/build/executor/env.js +6 -0
- package/build/executor/interrupted-worktree.js +60 -0
- package/build/executor/job-errors.js +45 -0
- package/build/executor/job-runner.js +334 -9
- package/build/executor/job-types.js +25 -9
- package/build/executor/merge-tree-classifier.js +171 -0
- package/build/executor/reconcile-overlap-governance.js +129 -0
- package/build/executor/reconcile-overlap-job.js +989 -0
- package/build/executor/reconcile-overlap-types.js +14 -0
- package/build/executor/spawn-job-driver.js +1 -0
- package/build/executor/types.js +2 -0
- package/build/executor/worker-finalization.js +25 -2
- package/build/executor/worker-guard-hook.js +15 -7
- package/build/implement-epic/bridge-client.js +773 -0
- package/build/implement-epic/checkpoint-store.js +542 -0
- package/build/implement-epic/cli.js +3158 -0
- package/build/implement-epic/cut-protocol.js +392 -0
- package/build/implement-epic/lock.js +302 -0
- package/build/implement-epic/pr-state.js +286 -0
- package/build/implement-epic/spawn.js +113 -0
- package/build/index.js +657 -481
- package/build/init.js +72 -8
- package/build/install-bridge-conductor.js +5 -5
- package/build/install-bridge.js +403 -70
- package/build/local-artifact-storage.js +130 -0
- package/build/mcp-host-config.js +22 -60
- package/build/mcp-host-entry-adapter.js +18 -0
- package/build/mcp-host-targets.js +1 -21
- package/build/merge-pull-request.js +1 -1
- package/build/pipelines.generated.js +23 -16
- package/build/plan-epic-conductor-eligibility.js +1 -1
- package/build/plane/cli.js +321 -41
- package/build/plane/manifest.js +209 -1
- package/build/plane/member-roster.js +70 -0
- package/build/plane/preflight.js +128 -12
- package/build/plane/shutdown.js +18 -5
- package/build/plane/status.js +35 -1
- package/build/plane/supervisor.js +546 -164
- package/build/plane/types.js +25 -2
- package/build/polling-policy.js +72 -0
- package/build/readiness-check.js +3 -3
- package/build/readme.generated.js +1 -1
- package/build/review-generation.js +219 -0
- package/build/run-unit-tests-launcher.js +6 -1
- package/build/setup-epic.js +567 -38
- package/build/start-tickets-conductor.js +8 -7
- package/build/ticket-key-utils.js +4 -3
- package/build/ticket-review-artifact-gate.js +461 -0
- package/build/upgrade-cli.js +5 -26
- package/build/version.generated.js +3 -3
- package/build/worker-guard-hook-bin.js +1 -1
- package/docs/CONDUCTOR.md +8 -6
- package/docs/install/mcp-tool-integrations.md +23 -1
- package/package.json +5 -3
- package/pipelines/review-ticket.json +17 -4
package/build/install-bridge.js
CHANGED
|
@@ -149,9 +149,13 @@ import { VERSION } from "./version.generated.js";
|
|
|
149
149
|
import { validateRepoName } from "./bridge-config.js";
|
|
150
150
|
import { MCP_HOST_TARGETS, HOST_PLATFORM_ORDER, allHostTargets, agentForPlatform, isHostPlatformId, detectDefaultPlatforms, hostAdapterForTarget, } from "./mcp-host-targets.js";
|
|
151
151
|
import { provisionHostTarget, createDefaultVendorProcessDeps, inspectBridgeApiProfileToken, } from "./mcp-host-config.js";
|
|
152
|
+
import { adaptBridgeEntryForHostTarget, } from "./mcp-host-entry-adapter.js";
|
|
152
153
|
import { MCP_SERVER_NAME, MCP_PACKAGE_NAME, resolveRegistrationKey, } from "./mcp-identity.js";
|
|
153
154
|
import { DUPLICATE_REGISTRATION_GUIDANCE } from "./launcher-config-inspection.js";
|
|
154
155
|
import { recordInstalledConductorCapability, recordInstalledExecutorServiceUnit, recordInstalledProjectArtifact, writeMcpInstallState, } from "./mcp-install-state.js";
|
|
156
|
+
// BAPI-1207: opt-in cross-platform agent notes (AGENTS.md / CLAUDE.md).
|
|
157
|
+
import { AGENT_GUIDANCE } from "./agent-guidance.generated.js";
|
|
158
|
+
import { resolveAgentNotesTargets, writeAgentNotes, } from "./agent-notes.js";
|
|
155
159
|
// Executor provisioning (BAPI-779). The generator and the lifecycle module are
|
|
156
160
|
// imported DIRECTLY and driven programmatically — never by spawning
|
|
157
161
|
// `executor install-service` as a subprocess, which would lose the typed plan,
|
|
@@ -178,8 +182,8 @@ import { fetchGithubConfigurationState } from "./connect-github-api.js";
|
|
|
178
182
|
import { createDefaultConnectGithubDeps, runGithubConnectionFlow } from "./connect-github.js";
|
|
179
183
|
import { fetchBitbucketConfigurationState } from "./connect-bitbucket-api.js";
|
|
180
184
|
import { createDefaultConnectBitbucketDeps, runBitbucketConnectionFlow } from "./connect-bitbucket.js";
|
|
181
|
-
import { resolveAgentSpec, isAgentName, formatValidAgentNames, } from "./agent-registry.js";
|
|
182
|
-
import { buildGenericAgentShellCommand, getDefaultSpawnTerminalTabForPlatform, detectTerminal, createDefaultStartTicketsDeps, materializeWorkerLaunchCommand, MAX_TERMINAL_COMMAND_BYTES, } from "./start-tickets.js";
|
|
185
|
+
import { resolveAgentSpec, isAgentName, formatValidAgentNames, resolveModelAlias, } from "./agent-registry.js";
|
|
186
|
+
import { buildGenericAgentShellCommand, getDefaultSpawnTerminalTabForPlatform, detectTerminal, createDefaultStartTicketsDeps, materializeWorkerLaunchCommand, MAX_TERMINAL_COMMAND_BYTES, validateResolvedModelAliasForAgent, } from "./start-tickets.js";
|
|
183
187
|
/** Redaction sentinel — the API-key value is NEVER printed; this stands in. */
|
|
184
188
|
export const REDACTED_API_KEY = "<REDACTED>";
|
|
185
189
|
/**
|
|
@@ -226,8 +230,16 @@ export function buildPrewarmCommandPreview() {
|
|
|
226
230
|
* automatic, gated by the existing readiness funnel
|
|
227
231
|
* (`maybe_auto_parse_if_ready`), with no prompt on any path.
|
|
228
232
|
*/
|
|
229
|
-
export const INSTALL_BRIDGE_AGENT_PROMPT = "
|
|
230
|
-
"
|
|
233
|
+
export const INSTALL_BRIDGE_AGENT_PROMPT = "Run /install-bridge now, with no arguments — that is the entire invocation. You are in the " +
|
|
234
|
+
"install-spawn context: that is a mode this prompt establishes in plain language, not a " +
|
|
235
|
+
"command-line option, so never convert it into --context install-spawn or any other flag. The " +
|
|
236
|
+
"Bridge MCP tools named throughout this command are available only inside this currently running " +
|
|
237
|
+
"agent session — never invoke them through Bash or by constructing a direct HTTP request. " +
|
|
238
|
+
"/install-bridge owns and automatically executes its own internal stages; do not reproduce, " +
|
|
239
|
+
"narrate as separate manual steps, or hand-execute those stages yourself. Never place the " +
|
|
240
|
+
"API-key value in a Bash command, its arguments, printed output, a log line, an error message, " +
|
|
241
|
+
"or a file you write; verify connectivity with the in-session ping MCP tool only — never through " +
|
|
242
|
+
"an environment export, echo, or any other shell probe. " +
|
|
231
243
|
"The command owns its own Stage 2 decision and evaluates it in a fixed order from the install " +
|
|
232
244
|
"manifest — do NOT force an outcome. `configured == true` is JOIN MODE; otherwise a manifest whose " +
|
|
233
245
|
"`greenfield_setup_path` field is already set is GREENFIELD REPORT-ONLY; otherwise the command's " +
|
|
@@ -255,6 +267,31 @@ export const INSTALL_BRIDGE_AGENT_PROMPT = "Execute the /install-bridge command
|
|
|
255
267
|
"back to the complete tool_capabilities catalog or the workflows collection. If " +
|
|
256
268
|
"concise_tool_capabilities is missing or malformed, take the command's single fallback line and " +
|
|
257
269
|
"render nothing else in its place. " +
|
|
270
|
+
"The same manifest response also carries a SEPARATE top-level human_setup field (contract v9) — an " +
|
|
271
|
+
"ordered list naming each remaining unconfigured integration and what finishing it unlocks, " +
|
|
272
|
+
"additive over the same checklist integrations already carries. It is never a third section of " +
|
|
273
|
+
"concise_tool_capabilities; render it as its own separate block, after the two-section report above. " +
|
|
274
|
+
"Absent (an older compatible manifest), malformed (not a list, or an entry missing display_text), or " +
|
|
275
|
+
"a valid empty list all render NOTHING for this block — no heading, no fabricated completion line, " +
|
|
276
|
+
"no invented setup advice; the two-section report above still renders unaffected either way. A " +
|
|
277
|
+
"valid non-empty list renders under the heading 'What finishing setup unlocks', one compact item per " +
|
|
278
|
+
"entry in the server's given order, using human_setup[].display_text as that item's entire " +
|
|
279
|
+
"substantive content, byte-for-byte — never summarized, reworded, sorted, filtered, merged, or " +
|
|
280
|
+
"recomposed from configure_in, setup_instructions, required_for, integrations, or any tool/workflow " +
|
|
281
|
+
"catalog. The same server-authority rule the concise capability report already follows extends to " +
|
|
282
|
+
"this field. " +
|
|
283
|
+
"Plain-language narration (BAPI-1174): explain each major action you take — reading the manifest, " +
|
|
284
|
+
"applying configuration, persisting the routing credential, rendering the capability report — in a " +
|
|
285
|
+
"short, plain-language sentence covering what is happening and why it matters to the person running " +
|
|
286
|
+
"this install, rather than in internal shorthand. Narration you compose yourself must never name an " +
|
|
287
|
+
"internal stage number, this command's contract version, a raw internal MCP tool identifier (like " +
|
|
288
|
+
"get_install_manifest, apply_install_manifest, or invite_member), or a manifest field key (like " +
|
|
289
|
+
"command_contract_version, next_step, or configured) — describe what is actually happening instead. " +
|
|
290
|
+
"This restriction is scoped to narration you author: it does not apply to a user-invocable command " +
|
|
291
|
+
"name you must give the human verbatim (/install-bridge, /learn-repository, connect-github, " +
|
|
292
|
+
"start-tickets, and the like) or to any value a stage marks as server-authored and rendered " +
|
|
293
|
+
"verbatim (the concise capability report, human_setup display text, a next_step.command string) — " +
|
|
294
|
+
"those are never reworded, curated, or filtered by you either way. " +
|
|
258
295
|
"In the install-spawn context Stage 6 must STILL call persist_routing_credential, but must NOT " +
|
|
259
296
|
"print or summarize its SUCCESSFUL result anywhere — no 'Stored routing credential for …' line, " +
|
|
260
297
|
"and no restatement of that target or path in any later summary or Return section — because the " +
|
|
@@ -352,13 +389,13 @@ export function getInstallBridgeUsage(baseUrl = DEFAULT_BAPI_BASE_URL) {
|
|
|
352
389
|
...INSTALL_BRIDGE_ONBOARDING_CHOICES.map((choice) => ` ${choice}`),
|
|
353
390
|
"There is NO default — pressing Enter selects nothing; you must type 1 or 2 (one",
|
|
354
391
|
"blank or invalid answer re-prompts once, then exits with guidance). Answering 1",
|
|
355
|
-
"
|
|
356
|
-
"
|
|
357
|
-
"
|
|
358
|
-
"
|
|
359
|
-
"
|
|
360
|
-
"
|
|
361
|
-
"run: passing ANY flag, setting BAPI_API_KEY, or running without an interactive",
|
|
392
|
+
"prompts for an email and creates a brand-new Bridge workspace for you (the",
|
|
393
|
+
"self-serve flow). Answering 2 opens a single hidden token prompt: paste whatever",
|
|
394
|
+
"you were given and its SHAPE picks the route — a bootstrap invite (bapi_inv_…)",
|
|
395
|
+
"is redeemed to CREATE a new project and mint your first admin key, while a",
|
|
396
|
+
"Bridge API key connects to the project that already exists. You are",
|
|
397
|
+
"never asked which kind of token you hold. That question is asked ONLY for a",
|
|
398
|
+
"bare interactive run: passing ANY flag, setting BAPI_API_KEY, or running without an interactive",
|
|
362
399
|
"terminal keeps the existing deterministic behavior and no prompt.",
|
|
363
400
|
"",
|
|
364
401
|
"On that bare run the route is INFERRED, so it is only announced — the detected",
|
|
@@ -426,7 +463,7 @@ export function getInstallBridgeUsage(baseUrl = DEFAULT_BAPI_BASE_URL) {
|
|
|
426
463
|
" the code is entered — no workspace, no project, no API key.",
|
|
427
464
|
" Delivery is asynchronous, so the message arrives shortly",
|
|
428
465
|
" after you are prompted rather than instantly. The code",
|
|
429
|
-
" expires
|
|
466
|
+
" expires 4 hours after it is requested, and you can ask",
|
|
430
467
|
" for a new one if it does not arrive; earlier codes keep",
|
|
431
468
|
" working until one is accepted.",
|
|
432
469
|
"",
|
|
@@ -827,7 +864,7 @@ export const INSTALL_BRIDGE_GITHUB_CONNECT_PROMPT = "Connect GitHub? [y/N]: ";
|
|
|
827
864
|
* Reuses the shared connect-github flow rather than duplicating the API, polling,
|
|
828
865
|
* browser, or picker logic — there is exactly one implementation of that handshake.
|
|
829
866
|
*/
|
|
830
|
-
export async function offerGithubConnection(repoName, baseUrl, deps, log) {
|
|
867
|
+
export async function offerGithubConnection(repoName, baseUrl, deps, log, session) {
|
|
831
868
|
// No prompt surface → no offer. Never assume consent on a non-interactive run.
|
|
832
869
|
if (!deps.isTTY || !deps.promptLine)
|
|
833
870
|
return;
|
|
@@ -875,7 +912,12 @@ export async function offerGithubConnection(repoName, baseUrl, deps, log) {
|
|
|
875
912
|
return; // declining is a normal outcome
|
|
876
913
|
const connectDeps = createDefaultConnectGithubDeps();
|
|
877
914
|
const code = await runGithubConnectionFlow(connectDeps, api, repoName);
|
|
878
|
-
if (code
|
|
915
|
+
if (code === 0) {
|
|
916
|
+
// BAPI-1171: only a CONFIRMED success suppresses the Bitbucket offer below —
|
|
917
|
+
// a decline, failure, or thrown error must never look like a connection.
|
|
918
|
+
session.vcsConnected = true;
|
|
919
|
+
}
|
|
920
|
+
else {
|
|
879
921
|
log(" note: GitHub was not connected. Your install is complete — connect GitHub later with " +
|
|
880
922
|
`'npx -y ${MCP_PACKAGE_NAME}@latest connect-github --repo ${repoName}'.`);
|
|
881
923
|
}
|
|
@@ -915,10 +957,15 @@ export const INSTALL_BRIDGE_BITBUCKET_CONNECT_PROMPT = "Connect Bitbucket? [y/N]
|
|
|
915
957
|
* Reuses the shared connect-bitbucket flow rather than duplicating the API, polling,
|
|
916
958
|
* browser, or picker logic — there is exactly one implementation of that handshake.
|
|
917
959
|
*/
|
|
918
|
-
export async function offerBitbucketConnection(repoName, baseUrl, deps, log) {
|
|
960
|
+
export async function offerBitbucketConnection(repoName, baseUrl, deps, log, session) {
|
|
919
961
|
// No prompt surface → no offer. Never assume consent on a non-interactive run.
|
|
920
962
|
if (!deps.isTTY || !deps.promptLine)
|
|
921
963
|
return;
|
|
964
|
+
// BAPI-1171: a GitHub connection made earlier in THIS run already leaves the
|
|
965
|
+
// project with a connected VCS provider — skip provider lookup, prompting, the
|
|
966
|
+
// browser flow, and the connection handshake entirely rather than asking again.
|
|
967
|
+
if (session.vcsConnected)
|
|
968
|
+
return;
|
|
922
969
|
try {
|
|
923
970
|
const credDeps = {
|
|
924
971
|
env: deps.env,
|
|
@@ -971,6 +1018,88 @@ export async function offerBitbucketConnection(repoName, baseUrl, deps, log) {
|
|
|
971
1018
|
`Bitbucket later with 'npx -y ${MCP_PACKAGE_NAME}@latest connect-bitbucket --repo ${repoName}'.`);
|
|
972
1019
|
}
|
|
973
1020
|
}
|
|
1021
|
+
/** Default-No agent-notes offer prompt. Mirrors {@link INSTALL_BRIDGE_GITHUB_CONNECT_PROMPT}. */
|
|
1022
|
+
export const INSTALL_BRIDGE_AGENT_NOTES_PROMPT = "Add cross-platform agent notes? [y/N]: ";
|
|
1023
|
+
/**
|
|
1024
|
+
* The offer-context lines printed immediately before the agent-notes prompt
|
|
1025
|
+
* (BAPI-1207). Names the applicable file(s) and states the marker-scoped,
|
|
1026
|
+
* non-destructive contract up front — mirrors the GitHub/Bitbucket offers'
|
|
1027
|
+
* "explain before asking" convention.
|
|
1028
|
+
*/
|
|
1029
|
+
export function buildAgentNotesOfferContext(targets) {
|
|
1030
|
+
return [
|
|
1031
|
+
"",
|
|
1032
|
+
"Step 4c — optional: cross-platform agent notes.",
|
|
1033
|
+
` This creates or refreshes a Bridge-owned region in ${targets.join(" and ")} with situation-`,
|
|
1034
|
+
" to-tool guidance for a few Bridge MCP tools. Only that marked region is created or",
|
|
1035
|
+
" refreshed — any existing content in the file, inside or outside it, is otherwise preserved.",
|
|
1036
|
+
];
|
|
1037
|
+
}
|
|
1038
|
+
/**
|
|
1039
|
+
* Offer to create or refresh the cross-platform agent notes, if the selected
|
|
1040
|
+
* platforms resolve to at least one writable target (BAPI-1207).
|
|
1041
|
+
*
|
|
1042
|
+
* Entirely best-effort and non-destructive, mirroring
|
|
1043
|
+
* {@link offerGithubConnection}: by the time this runs the install itself is
|
|
1044
|
+
* already complete and durable, so nothing here may fail the run. This
|
|
1045
|
+
* function only resolves the CONSENT decision — the caller performs the
|
|
1046
|
+
* actual write via {@link writeAgentNotes}, so a `--dry-run` (which never
|
|
1047
|
+
* reaches this call) and the live path share exactly one write path.
|
|
1048
|
+
*/
|
|
1049
|
+
export async function offerAgentGuidanceNotes(deps, log, targets) {
|
|
1050
|
+
if (targets.length === 0)
|
|
1051
|
+
return { kind: "not-applicable" };
|
|
1052
|
+
// No prompt surface → no offer. Never assume consent on a non-interactive run.
|
|
1053
|
+
if (!deps.isTTY || !deps.promptLine)
|
|
1054
|
+
return { kind: "headless-skip" };
|
|
1055
|
+
try {
|
|
1056
|
+
for (const line of buildAgentNotesOfferContext(targets))
|
|
1057
|
+
log(line);
|
|
1058
|
+
const answer = (await deps.promptLine(INSTALL_BRIDGE_AGENT_NOTES_PROMPT)).trim().toLowerCase();
|
|
1059
|
+
// Only an explicit yes proceeds — blank input, EOF, "n", and anything malformed
|
|
1060
|
+
// all decline without writing.
|
|
1061
|
+
return answer === "y" || answer === "yes" ? { kind: "consented" } : { kind: "declined" };
|
|
1062
|
+
}
|
|
1063
|
+
catch {
|
|
1064
|
+
// The install is already durable; a failure here is never fatal to it.
|
|
1065
|
+
log(" note: the agent-notes offer could not run. No notes files were written.");
|
|
1066
|
+
return { kind: "declined" };
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
/**
|
|
1070
|
+
* One secret-free summary line for the final install output (BAPI-1207). Never
|
|
1071
|
+
* reveals guidance content, file bytes, or a credential — only which files
|
|
1072
|
+
* were touched, skipped, or failed, and which selected platforms have no
|
|
1073
|
+
* supported notes convention (currently only Windsurf).
|
|
1074
|
+
*/
|
|
1075
|
+
export function buildAgentNotesSummaryLine(offer, result, writeFailed, unsupportedPlatforms) {
|
|
1076
|
+
const unsupportedNote = unsupportedPlatforms.length > 0
|
|
1077
|
+
? ` (${unsupportedPlatforms.join(", ")}: no supported agent-notes convention)`
|
|
1078
|
+
: "";
|
|
1079
|
+
if (offer.kind === "not-applicable") {
|
|
1080
|
+
return unsupportedPlatforms.length > 0 ? `Agent notes: not written${unsupportedNote}.` : undefined;
|
|
1081
|
+
}
|
|
1082
|
+
if (offer.kind === "headless-skip") {
|
|
1083
|
+
return `Agent notes: skipped (non-interactive session)${unsupportedNote}.`;
|
|
1084
|
+
}
|
|
1085
|
+
if (offer.kind === "declined") {
|
|
1086
|
+
return `Agent notes: skipped (declined)${unsupportedNote}.`;
|
|
1087
|
+
}
|
|
1088
|
+
if (writeFailed || !result) {
|
|
1089
|
+
return `Agent notes: could not be written (unexpected error)${unsupportedNote}.`;
|
|
1090
|
+
}
|
|
1091
|
+
const written = result.files.filter((f) => f.status === "written").map((f) => f.target);
|
|
1092
|
+
const unchanged = result.files.filter((f) => f.status === "skipped").map((f) => f.target);
|
|
1093
|
+
const failed = result.files.filter((f) => f.status === "failed").map((f) => f.target);
|
|
1094
|
+
const parts = [];
|
|
1095
|
+
if (written.length > 0)
|
|
1096
|
+
parts.push(`written to ${written.join(", ")}`);
|
|
1097
|
+
if (unchanged.length > 0)
|
|
1098
|
+
parts.push(`already up to date in ${unchanged.join(", ")}`);
|
|
1099
|
+
if (failed.length > 0)
|
|
1100
|
+
parts.push(`failed for ${failed.join(", ")}`);
|
|
1101
|
+
return `Agent notes: ${parts.join("; ")}${unsupportedNote}.`;
|
|
1102
|
+
}
|
|
974
1103
|
/**
|
|
975
1104
|
* Echoed single-line prompt on stderr (used for repo confirmation / value).
|
|
976
1105
|
* Exported so `index.ts`'s `--init` can share the exact same prompt mechanics
|
|
@@ -1484,11 +1613,18 @@ export async function resolveInviteToken(options, deps) {
|
|
|
1484
1613
|
* is never written to a log line; it is only ever echoed as the user's own keystrokes
|
|
1485
1614
|
* and placed in the mint request body.)
|
|
1486
1615
|
*
|
|
1487
|
-
*
|
|
1488
|
-
*
|
|
1489
|
-
* permissive "stored unvalidated" contract (D-2). The
|
|
1616
|
+
* Explicit sources (`--email`, `BAPI_SIGNUP_EMAIL`) still take only trimmed
|
|
1617
|
+
* whitespace — matching the other CLI input seams and the server's deliberately
|
|
1618
|
+
* permissive "stored unvalidated" contract (D-2). The interactive prompt, added by
|
|
1619
|
+
* BAPI-1171, is bounded and additionally requires the trimmed value to match a
|
|
1620
|
+
* minimal `local@domain` shape before it is accepted — mirroring the one-hint/
|
|
1621
|
+
* one-re-prompt contract {@link resolveInstallBridgeOnboardingBranchForRun} uses,
|
|
1622
|
+
* so an unanswerable prompt cannot loop forever. The entered value is never echoed
|
|
1490
1623
|
* back in an error message.
|
|
1491
1624
|
*/
|
|
1625
|
+
const INSTALL_BRIDGE_SIGNUP_EMAIL_SHAPE = /^[^@\s]+@[^@\s]+$/;
|
|
1626
|
+
/** The one hint line printed after a blank/malformed email, before the single re-prompt. */
|
|
1627
|
+
export const INSTALL_BRIDGE_SIGNUP_EMAIL_HINT = "Enter a valid email address (e.g. you@example.com).";
|
|
1492
1628
|
export async function resolveSignupEmail(options, deps) {
|
|
1493
1629
|
// BAPI-757 (R-3 / AC-9): self-serve signup now REQUIRES an interactive terminal,
|
|
1494
1630
|
// and the check runs FIRST — before `--email` and before `BAPI_SIGNUP_EMAIL` —
|
|
@@ -1507,9 +1643,19 @@ export async function resolveSignupEmail(options, deps) {
|
|
|
1507
1643
|
if (typeof fromEnv === "string" && fromEnv.trim().length > 0) {
|
|
1508
1644
|
return { ok: true, value: fromEnv.trim() };
|
|
1509
1645
|
}
|
|
1510
|
-
const
|
|
1511
|
-
|
|
1512
|
-
|
|
1646
|
+
const promptLine = deps.promptLine;
|
|
1647
|
+
// BAPI-1171: bounded to two reads total — the first answer, then ONE re-prompt
|
|
1648
|
+
// after a single hint line. EOF resolves as an empty value (promptLineViaReadline
|
|
1649
|
+
// resolves "" on close), which fails the shape check like any other blank
|
|
1650
|
+
// answer, so it costs one re-prompt and then fails cleanly rather than hanging.
|
|
1651
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
1652
|
+
const raw = await promptLine("Email for Bridge workspace setup (required): ");
|
|
1653
|
+
const entered = String(raw ?? "").trim();
|
|
1654
|
+
if (INSTALL_BRIDGE_SIGNUP_EMAIL_SHAPE.test(entered)) {
|
|
1655
|
+
return { ok: true, value: entered };
|
|
1656
|
+
}
|
|
1657
|
+
if (attempt === 0)
|
|
1658
|
+
deps.log(INSTALL_BRIDGE_SIGNUP_EMAIL_HINT);
|
|
1513
1659
|
}
|
|
1514
1660
|
return { ok: false, error: "No email entered." };
|
|
1515
1661
|
}
|
|
@@ -1557,15 +1703,16 @@ export function resolveInstallBridgeOnboardingBranch(options, env) {
|
|
|
1557
1703
|
export const INSTALL_BRIDGE_KEY_SELECTOR_PROMPT = "Welcome to Bridge - do you have a token yet?";
|
|
1558
1704
|
/**
|
|
1559
1705
|
* The question's numbered options, in render order. Index + 1 is the accepted
|
|
1560
|
-
* token
|
|
1706
|
+
* token. BAPI-1171 swapped the order so the self-serve, need-a-token path is
|
|
1707
|
+
* option 1 — first-time signup is the common case and now leads.
|
|
1561
1708
|
*
|
|
1562
1709
|
* "Token" is deliberately credential-agnostic: an invite and an API key are both
|
|
1563
|
-
* tokens as far as this question is concerned, and answering "
|
|
1710
|
+
* tokens as far as this question is concerned, and answering "2" leads to ONE
|
|
1564
1711
|
* hidden prompt whose value selects the route by shape.
|
|
1565
1712
|
*/
|
|
1566
1713
|
export const INSTALL_BRIDGE_ONBOARDING_CHOICES = [
|
|
1567
|
-
"1.
|
|
1568
|
-
"2.
|
|
1714
|
+
"1. I need a token",
|
|
1715
|
+
"2. I already have a token",
|
|
1569
1716
|
];
|
|
1570
1717
|
/** The single-line answer prompt drawn after the options (no bracketed default). */
|
|
1571
1718
|
export const INSTALL_BRIDGE_ONBOARDING_CHOICE_PROMPT = "Enter 1 or 2: ";
|
|
@@ -1579,9 +1726,9 @@ export const INSTALL_BRIDGE_ONBOARDING_CHOICE_HINT = "Enter 1 or 2.";
|
|
|
1579
1726
|
* advised "try the hidden prompt again" — false, because the process has already
|
|
1580
1727
|
* exited by the time the user reads it. All three now name the SAME three routes.
|
|
1581
1728
|
*/
|
|
1582
|
-
export const INSTALL_BRIDGE_ENTRY_ROUTES_ADVICE = "Re-run install: on a bare interactive run answer 1
|
|
1583
|
-
"(an invite or an API key — either one is accepted at the hidden prompt)
|
|
1584
|
-
"
|
|
1729
|
+
export const INSTALL_BRIDGE_ENTRY_ROUTES_ADVICE = "Re-run install: on a bare interactive run answer 1 to request a new token or 2 if you " +
|
|
1730
|
+
"already have one (an invite or an API key — either one is accepted at the hidden prompt). " +
|
|
1731
|
+
"Non-interactively, pass --api-key <key> (or set BAPI_API_KEY) for an " +
|
|
1585
1732
|
"existing project, --invite if you were sent an invite token, or --email <addr> to sign up.";
|
|
1586
1733
|
/**
|
|
1587
1734
|
* Interactive wrapper around {@link resolveInstallBridgeOnboardingBranch}.
|
|
@@ -1651,20 +1798,21 @@ export async function resolveInstallBridgeOnboardingBranchForRun(options, deps,
|
|
|
1651
1798
|
// it can never hang or silently select a branch.
|
|
1652
1799
|
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
1653
1800
|
const answer = (await promptLine(INSTALL_BRIDGE_ONBOARDING_CHOICE_PROMPT)).trim();
|
|
1654
|
-
// BAPI-708 (A-1): the accepted tokens are
|
|
1655
|
-
// aliases), preserving the one-strict-line contract.
|
|
1801
|
+
// BAPI-708 (A-1): the accepted tokens are exactly 1/2 (no letters, no
|
|
1802
|
+
// aliases), preserving the one-strict-line contract. BAPI-1171 swapped the
|
|
1803
|
+
// order so "1" is the self-serve need-key branch and "2" routes to the
|
|
1656
1804
|
// have-key branch's credential-agnostic prompt, whose value is classified by
|
|
1657
1805
|
// SHAPE — so a user holding an invite still lands in the redemption protocol
|
|
1658
1806
|
// without ever having been asked which kind of token they hold.
|
|
1659
|
-
if (answer === "1")
|
|
1660
|
-
return { ok: true, branch: { kind: "have-key" }, credentialIntent: "inferred" };
|
|
1661
|
-
if (answer === "2") {
|
|
1807
|
+
if (answer === "1") {
|
|
1662
1808
|
return {
|
|
1663
1809
|
ok: true,
|
|
1664
1810
|
branch: { kind: "need-key", method: "self-serve" },
|
|
1665
1811
|
credentialIntent: "inferred",
|
|
1666
1812
|
};
|
|
1667
1813
|
}
|
|
1814
|
+
if (answer === "2")
|
|
1815
|
+
return { ok: true, branch: { kind: "have-key" }, credentialIntent: "inferred" };
|
|
1668
1816
|
if (attempt === 0)
|
|
1669
1817
|
deps.log(INSTALL_BRIDGE_ONBOARDING_CHOICE_HINT);
|
|
1670
1818
|
}
|
|
@@ -1854,8 +2002,8 @@ export function promptMultiSelectViaReadline(promptText, options, input = proces
|
|
|
1854
2002
|
* 2. interactive multi-select on a TTY (AC-2: no row pre-selected — the picker
|
|
1855
2003
|
* starts empty and requires an explicit ≥1 selection),
|
|
1856
2004
|
* 3. the legacy non-TTY automatic set: Claude Code plus only the currently
|
|
1857
|
-
* detected Copilot VS Code and Cursor automatic targets (never Codex
|
|
1858
|
-
*
|
|
2005
|
+
* detected Copilot VS Code and Cursor automatic targets (never Codex just
|
|
2006
|
+
* because a global directory exists).
|
|
1859
2007
|
*/
|
|
1860
2008
|
export async function resolveSelectedHostPlatforms(deps, options) {
|
|
1861
2009
|
// 1. Explicit --tools (empty array is an explicit empty selection).
|
|
@@ -2137,8 +2285,8 @@ async function buildDetectionContext(deps) {
|
|
|
2137
2285
|
/**
|
|
2138
2286
|
* Resolve which project-local JSON host configs to write for the selected
|
|
2139
2287
|
* platforms. Only the project-scoped JSON targets (Claude Code, Cursor, Copilot
|
|
2140
|
-
* VS Code) are returned here; global
|
|
2141
|
-
*
|
|
2288
|
+
* VS Code) are returned here; the global target (Codex) and manual targets
|
|
2289
|
+
* (Windsurf) are handled by the registry-driven emitter in
|
|
2142
2290
|
* `runInstallBridgeCli`. The returned shape is unchanged so the existing
|
|
2143
2291
|
* read-merge-write path and overwrite-consent detection are preserved.
|
|
2144
2292
|
*/
|
|
@@ -2147,7 +2295,7 @@ function hostConfigTargetsForPlatforms(platforms) {
|
|
|
2147
2295
|
return HOST_PLATFORM_ORDER.filter((id) => set.has(id))
|
|
2148
2296
|
.map((id) => MCP_HOST_TARGETS[id])
|
|
2149
2297
|
.filter((t) => t.scope === "project" && hostAdapterForTarget(t).format === "json")
|
|
2150
|
-
.map(
|
|
2298
|
+
.map(toHostConfigTarget);
|
|
2151
2299
|
}
|
|
2152
2300
|
/**
|
|
2153
2301
|
* Derive the human-facing labels for a set of selected platforms, in registry
|
|
@@ -2162,7 +2310,7 @@ export function labelsForPlatforms(platforms) {
|
|
|
2162
2310
|
}
|
|
2163
2311
|
/** Project-local host-config metadata sourced from the shared registry/adapter. */
|
|
2164
2312
|
function toHostConfigTarget(target) {
|
|
2165
|
-
return { relPath: target.relPath, topLevelKey: hostAdapterForTarget(target).topLevelKey };
|
|
2313
|
+
return { target, relPath: target.relPath, topLevelKey: hostAdapterForTarget(target).topLevelKey };
|
|
2166
2314
|
}
|
|
2167
2315
|
/** Resolve which project-local host configs to write, mirroring runInit detection. */
|
|
2168
2316
|
async function resolveHostConfigTargets(deps) {
|
|
@@ -2400,9 +2548,10 @@ async function requestTrackedConfigConsent(deps, relPath) {
|
|
|
2400
2548
|
* helper governs launcher-token migration only, never credentials.
|
|
2401
2549
|
*/
|
|
2402
2550
|
export function preserveExistingLauncherArgs(existing, entry) {
|
|
2551
|
+
const existingEntry = asRecord(existing) ?? {};
|
|
2403
2552
|
const existingArgs = asRecord(existing)?.args;
|
|
2404
2553
|
const preserved = refreshBridgeApiPackageSpec(existingArgs, currentBridgePackageSpec());
|
|
2405
|
-
return preserved ? {
|
|
2554
|
+
return { ...existingEntry, ...entry, ...(preserved ? { args: preserved } : {}) };
|
|
2406
2555
|
}
|
|
2407
2556
|
/**
|
|
2408
2557
|
* Read-only pre-scan for both-key configs across EVERY project target (BAPI-807).
|
|
@@ -2457,7 +2606,8 @@ async function writeHostConfigs(deps, targets, entries, trackedState, ctx) {
|
|
|
2457
2606
|
// file that will be skipped never triggers a misleading write-consent prompt.
|
|
2458
2607
|
if (read.state === "invalid") {
|
|
2459
2608
|
deps.errorLog(formatInvalidConfigNotice(target.relPath));
|
|
2460
|
-
|
|
2609
|
+
const secretFreeEntry = adaptBridgeEntryForHostTarget(entries.secretFree, target.target);
|
|
2610
|
+
for (const line of formatSecretFreeManualMerge(target.relPath, target.topLevelKey, secretFreeEntry)) {
|
|
2461
2611
|
deps.errorLog(line);
|
|
2462
2612
|
}
|
|
2463
2613
|
skipped.push({ relPath: target.relPath, reason: "invalid" });
|
|
@@ -2511,7 +2661,8 @@ async function writeHostConfigs(deps, targets, entries, trackedState, ctx) {
|
|
|
2511
2661
|
// the full template args, `serve` included. The rule is identical for the
|
|
2512
2662
|
// real-key and secret-free variants: credential safety must not decide whether
|
|
2513
2663
|
// a launcher is migrated.
|
|
2514
|
-
|
|
2664
|
+
const physicalEntry = adaptBridgeEntryForHostTarget(entry, target.target);
|
|
2665
|
+
topLevel[registrationKey] = preserveExistingLauncherArgs(topLevel[registrationKey], physicalEntry);
|
|
2515
2666
|
config[target.topLevelKey] = topLevel;
|
|
2516
2667
|
await deps.mkdir(path.dirname(fullPath), { recursive: true });
|
|
2517
2668
|
await deps.writeFile(fullPath, JSON.stringify(config, null, 2) + "\n", {
|
|
@@ -2522,7 +2673,7 @@ async function writeHostConfigs(deps, targets, entries, trackedState, ctx) {
|
|
|
2522
2673
|
return { written, skipped, duplicateRegistrations: [] };
|
|
2523
2674
|
}
|
|
2524
2675
|
/**
|
|
2525
|
-
* Provision the selected GLOBAL (Codex
|
|
2676
|
+
* Provision the selected GLOBAL (Codex) and MANUAL (Windsurf)
|
|
2526
2677
|
* targets through the registry-driven emitter (BAPI-635). Project JSON targets
|
|
2527
2678
|
* are handled by {@link writeHostConfigs}; this covers everything else. Global
|
|
2528
2679
|
* config paths are never added to the repository .gitignore. Returns secret-free
|
|
@@ -3084,6 +3235,15 @@ export const INSTALL_BRIDGE_VERIFY_THROTTLED = "Bridge is at capacity and could
|
|
|
3084
3235
|
* hide a real outage behind a version-skew story.
|
|
3085
3236
|
*/
|
|
3086
3237
|
export const INSTALL_BRIDGE_VERIFY_SERVER_ERROR = "Error: Bridge could not complete verification right now. Nothing was created — wait a few minutes and re-run this command; the code already emailed to you stays valid until it expires.";
|
|
3238
|
+
/**
|
|
3239
|
+
* BAPI-1173: printed after a successful verification whose response says, with an
|
|
3240
|
+
* explicit `trial_granted: false`, that this email already used its one free trial.
|
|
3241
|
+
*
|
|
3242
|
+
* Text only — deliberately no URL. The canonical purchase link is authored once,
|
|
3243
|
+
* server-side, and the first metered-work denial already delivers it. Non-metered
|
|
3244
|
+
* work still runs, so this informs rather than blocks.
|
|
3245
|
+
*/
|
|
3246
|
+
export const INSTALL_BRIDGE_TRIAL_ALREADY_USED = "Your free trial was already used - this project is on the metered plan; add payment to run metered work.";
|
|
3087
3247
|
/**
|
|
3088
3248
|
* Local cap on rejected verification attempts before the CLI stops re-prompting.
|
|
3089
3249
|
*
|
|
@@ -3122,6 +3282,25 @@ export const INSTALL_BRIDGE_SIGNUP_UPGRADE_REQUIRED = [
|
|
|
3122
3282
|
"",
|
|
3123
3283
|
` npx -y ${MCP_PACKAGE_NAME}@latest install --email <addr>`,
|
|
3124
3284
|
].join("\n");
|
|
3285
|
+
/**
|
|
3286
|
+
* HTTP 403 on the self-serve request or verify call (BAPI-1171).
|
|
3287
|
+
*
|
|
3288
|
+
* Deliberately distinct from {@link INSTALL_BRIDGE_SIGNUP_UPGRADE_REQUIRED}: a 403
|
|
3289
|
+
* means the request was refused before signup logic ran at all — an upgrade would
|
|
3290
|
+
* not change the outcome, so the message names the actual cause instead.
|
|
3291
|
+
*/
|
|
3292
|
+
export const INSTALL_BRIDGE_SIGNUP_BLOCKED = "Error: Bridge signup is not available from your current network — your network/IP address " +
|
|
3293
|
+
"appears to be blocked. Try again from a different network, or contact Bridge if you believe " +
|
|
3294
|
+
"this is a mistake.";
|
|
3295
|
+
/**
|
|
3296
|
+
* A self-serve HTTP status this client has no specific handling for (BAPI-1171).
|
|
3297
|
+
*
|
|
3298
|
+
* Carries only the numeric status — never response text or a body, which could
|
|
3299
|
+
* echo back the email address or another value this client must not print.
|
|
3300
|
+
*/
|
|
3301
|
+
export function formatSelfServeUnexpectedStatus(status) {
|
|
3302
|
+
return `Error: Bridge signup returned an unexpected response (status ${status}). Try again in a few minutes.`;
|
|
3303
|
+
}
|
|
3125
3304
|
/**
|
|
3126
3305
|
* Mask an address for display: first character, then the domain.
|
|
3127
3306
|
*
|
|
@@ -3156,7 +3335,12 @@ export function formatChallengeRemaining(expiresAtIso, nowMs) {
|
|
|
3156
3335
|
return "This code has expired.";
|
|
3157
3336
|
if (minutes === 1)
|
|
3158
3337
|
return "This code is valid for about another minute.";
|
|
3159
|
-
|
|
3338
|
+
if (minutes < 60)
|
|
3339
|
+
return `This code is valid for about another ${minutes} minutes.`;
|
|
3340
|
+
if (minutes < 120)
|
|
3341
|
+
return "This code is valid for about another hour.";
|
|
3342
|
+
const hours = Math.floor(minutes / 60);
|
|
3343
|
+
return `This code is valid for about another ${hours} hours.`;
|
|
3160
3344
|
}
|
|
3161
3345
|
/**
|
|
3162
3346
|
* Prompt for the verification code, validating its shape LOCALLY before any
|
|
@@ -3246,8 +3430,11 @@ export async function requestSelfServeChallenge(deps, baseUrl, email) {
|
|
|
3246
3430
|
});
|
|
3247
3431
|
}
|
|
3248
3432
|
catch (err) {
|
|
3433
|
+
// BAPI-1171: the fetch itself failed (DNS, TLS, connection refused, timeout) —
|
|
3434
|
+
// distinct from the server responding with an error status. The caught value
|
|
3435
|
+
// is never inspected; only its category is reported.
|
|
3249
3436
|
void err;
|
|
3250
|
-
return { ok: false, category: "
|
|
3437
|
+
return { ok: false, category: "network-error" };
|
|
3251
3438
|
}
|
|
3252
3439
|
if (resp.ok) {
|
|
3253
3440
|
let handle;
|
|
@@ -3278,7 +3465,12 @@ export async function requestSelfServeChallenge(deps, baseUrl, email) {
|
|
|
3278
3465
|
return { ok: false, category: "unavailable" };
|
|
3279
3466
|
if (resp.status === 400 || resp.status === 422)
|
|
3280
3467
|
return { ok: false, category: "invalid" };
|
|
3281
|
-
|
|
3468
|
+
// BAPI-1171: 403 means the caller's network/IP was refused outright — distinct
|
|
3469
|
+
// from every status above, and from a genuinely unhandled status below. Neither
|
|
3470
|
+
// branch reads the response body.
|
|
3471
|
+
if (resp.status === 403)
|
|
3472
|
+
return { ok: false, category: "blocked" };
|
|
3473
|
+
return { ok: false, category: "unexpected", status: resp.status };
|
|
3282
3474
|
}
|
|
3283
3475
|
/**
|
|
3284
3476
|
* POST the VERIFY phase: a handle and a code in, the invite token out.
|
|
@@ -3297,14 +3489,18 @@ export async function verifySelfServeChallenge(deps, baseUrl, handle, code) {
|
|
|
3297
3489
|
});
|
|
3298
3490
|
}
|
|
3299
3491
|
catch (err) {
|
|
3492
|
+
// BAPI-1171: the fetch itself failed — distinct from the server responding
|
|
3493
|
+
// with an error status. The caught value is never inspected.
|
|
3300
3494
|
void err;
|
|
3301
|
-
return { ok: false, category: "
|
|
3495
|
+
return { ok: false, category: "network-error" };
|
|
3302
3496
|
}
|
|
3303
3497
|
if (resp.ok) {
|
|
3304
3498
|
let token;
|
|
3499
|
+
let trialGranted;
|
|
3305
3500
|
try {
|
|
3306
3501
|
const body = (await resp.json());
|
|
3307
3502
|
token = body?.token;
|
|
3503
|
+
trialGranted = body?.trial_granted;
|
|
3308
3504
|
}
|
|
3309
3505
|
catch {
|
|
3310
3506
|
return { ok: false, category: "failed" };
|
|
@@ -3314,6 +3510,12 @@ export async function verifySelfServeChallenge(deps, baseUrl, handle, code) {
|
|
|
3314
3510
|
!token.startsWith(BOOTSTRAP_INVITE_TOKEN_PREFIX)) {
|
|
3315
3511
|
return { ok: false, category: "failed" };
|
|
3316
3512
|
}
|
|
3513
|
+
// BAPI-1173: forward-compatible. Only a real boolean is carried; an absent
|
|
3514
|
+
// field (an older server) or a malformed one leaves the result exactly the
|
|
3515
|
+
// legacy `{ ok: true, token }` shape rather than being coerced to false.
|
|
3516
|
+
if (typeof trialGranted === "boolean") {
|
|
3517
|
+
return { ok: true, token, trialGranted };
|
|
3518
|
+
}
|
|
3317
3519
|
return { ok: true, token };
|
|
3318
3520
|
}
|
|
3319
3521
|
// 401 is the server's uniform invalid-challenge outcome. It covers a wrong code,
|
|
@@ -3328,7 +3530,12 @@ export async function verifySelfServeChallenge(deps, baseUrl, handle, code) {
|
|
|
3328
3530
|
// caller can say "try again" instead of "upgrade the CLI".
|
|
3329
3531
|
if (resp.status >= 500)
|
|
3330
3532
|
return { ok: false, category: "server-error" };
|
|
3331
|
-
|
|
3533
|
+
// BAPI-1171: 403 means the caller's network/IP was refused outright — distinct
|
|
3534
|
+
// from every status above, and from a genuinely unhandled status below. Neither
|
|
3535
|
+
// branch reads the response body.
|
|
3536
|
+
if (resp.status === 403)
|
|
3537
|
+
return { ok: false, category: "blocked" };
|
|
3538
|
+
return { ok: false, category: "unexpected", status: resp.status };
|
|
3332
3539
|
}
|
|
3333
3540
|
/**
|
|
3334
3541
|
* POST the RESEND phase: the handle alone, timing metadata out.
|
|
@@ -3630,6 +3837,7 @@ export function buildDryRunPreview(ctx) {
|
|
|
3630
3837
|
...credentialStep,
|
|
3631
3838
|
...buildCommitAssetsPreviewLines(ctx.commitSafeAssets, ctx.host.kind === "explicit" ? ctx.host.configTargets : []),
|
|
3632
3839
|
...(ctx.host.kind === "explicit" ? buildLaunchStepPreview(ctx.host.launch) : buildDeferredLaunchStepPreview()),
|
|
3840
|
+
...(ctx.host.kind === "explicit" ? buildAgentNotesPreviewLines(ctx.host.notesTargets ?? []) : []),
|
|
3633
3841
|
];
|
|
3634
3842
|
}
|
|
3635
3843
|
/**
|
|
@@ -3729,6 +3937,25 @@ function buildDeferredLaunchStepPreview() {
|
|
|
3729
3937
|
" not yet known — a real run resolves it first, then follows from there).",
|
|
3730
3938
|
];
|
|
3731
3939
|
}
|
|
3940
|
+
/**
|
|
3941
|
+
* The optional agent-notes offer preview line (BAPI-1207). A --dry-run never
|
|
3942
|
+
* prompts or writes; when the selection resolves to at least one writable
|
|
3943
|
+
* notes target this states plainly that a real interactive run would ask
|
|
3944
|
+
* default-No consent before touching it. Empty when the selection resolves to
|
|
3945
|
+
* no writable target (e.g. Windsurf-only), mirroring how the GitHub offer
|
|
3946
|
+
* lines are unconditional but this offer's applicability is not.
|
|
3947
|
+
*/
|
|
3948
|
+
function buildAgentNotesPreviewLines(notesTargets) {
|
|
3949
|
+
if (notesTargets.length === 0)
|
|
3950
|
+
return [];
|
|
3951
|
+
return [
|
|
3952
|
+
"Step 4c — optional: cross-platform agent notes (SKIPPED in --dry-run): if the terminal is",
|
|
3953
|
+
` interactive, offer to create or refresh a Bridge-owned marker region in ${notesTargets.join(" and ")}`,
|
|
3954
|
+
" with situation-to-tool guidance for a few Bridge MCP tools; defaults to No, and",
|
|
3955
|
+
" existing content outside that region is always preserved. No notes files are",
|
|
3956
|
+
" written in --dry-run.",
|
|
3957
|
+
];
|
|
3958
|
+
}
|
|
3732
3959
|
/**
|
|
3733
3960
|
* Bootstrap-invite dry-run preview. A --dry-run must not consume the invite OR
|
|
3734
3961
|
* LEAVE STATE BEHIND: skipping the HTTP call is not enough — a preview that wrote
|
|
@@ -3809,6 +4036,7 @@ function buildBootstrapDryRunPreview(ctx) {
|
|
|
3809
4036
|
...promoteStep,
|
|
3810
4037
|
...buildCommitAssetsPreviewLines(ctx.commitSafeAssets, ctx.host.kind === "explicit" ? ctx.host.configTargets : []),
|
|
3811
4038
|
...(ctx.host.kind === "explicit" ? buildLaunchStepPreview(ctx.host.launch) : buildDeferredLaunchStepPreview()),
|
|
4039
|
+
...(ctx.host.kind === "explicit" ? buildAgentNotesPreviewLines(ctx.host.notesTargets ?? []) : []),
|
|
3812
4040
|
];
|
|
3813
4041
|
}
|
|
3814
4042
|
/**
|
|
@@ -4150,6 +4378,30 @@ export const INSTALL_BRIDGE_NO_GIT_NONINTERACTIVE_WARNING = `Warning: ${INSTALL_
|
|
|
4150
4378
|
async function hasProjectRootMarker(deps) {
|
|
4151
4379
|
return sharedHasProjectRootMarker(deps.cwd, deps.stat);
|
|
4152
4380
|
}
|
|
4381
|
+
/**
|
|
4382
|
+
* Resolve, then live-validate, the premium model alias to pin the spawned
|
|
4383
|
+
* install-spawn session to (BAPI-1206). Fail-open at every step: an agent with
|
|
4384
|
+
* no model override support, a validation failure, or an unavailable live
|
|
4385
|
+
* probe all resolve to `null` (omit `--model`, use the agent default) rather
|
|
4386
|
+
* than aborting installation or spawning. For a statically allowlisted agent
|
|
4387
|
+
* (e.g. claude), {@link validateResolvedModelAliasForAgent} returns the
|
|
4388
|
+
* candidate unchanged without probing anything — only an agent like
|
|
4389
|
+
* `cursor-agent`, which has no `staticModelAliasAllowlist`, triggers the live
|
|
4390
|
+
* advertised-model check. A non-null candidate that fails validation logs a
|
|
4391
|
+
* secret-free diagnostic through `deps.debugLog` (silent unless
|
|
4392
|
+
* `BAPI_INSTALL_DEBUG` is set) and is never surfaced as a user-facing warning.
|
|
4393
|
+
*/
|
|
4394
|
+
export async function resolveValidatedPremiumModelAlias(deps, agent) {
|
|
4395
|
+
const candidate = resolveModelAlias(agent, "premium");
|
|
4396
|
+
if (!candidate)
|
|
4397
|
+
return null;
|
|
4398
|
+
const validation = await validateResolvedModelAliasForAgent(deps.startTicketsDeps, agent, candidate);
|
|
4399
|
+
if (!validation.ok) {
|
|
4400
|
+
deps.debugLog(`install-bridge: ${validation.warning}`);
|
|
4401
|
+
return null;
|
|
4402
|
+
}
|
|
4403
|
+
return validation.alias;
|
|
4404
|
+
}
|
|
4153
4405
|
// ---------------------------------------------------------------------------
|
|
4154
4406
|
// CLI entry
|
|
4155
4407
|
// ---------------------------------------------------------------------------
|
|
@@ -4203,9 +4455,12 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
|
|
|
4203
4455
|
// be an argument error. Deliberately after the deps merge and the base-URL
|
|
4204
4456
|
// resolution above, so the nested command inherits the same final merged deps
|
|
4205
4457
|
// (including a test's `fetch` override) and the same normalized base URL.
|
|
4206
|
-
// Note this is `install-bridge conductor`; the
|
|
4207
|
-
//
|
|
4208
|
-
//
|
|
4458
|
+
// Note this is `install-bridge conductor`; it is UNRELATED to the top-level
|
|
4459
|
+
// `conductor`/`epic-implementer` CLI split (S4/BAPI-1080) — the bare
|
|
4460
|
+
// top-level `conductor` token is now a fixed migration stub and the real
|
|
4461
|
+
// event-ledger CLI routes through `runEpicImplementerCli` under the
|
|
4462
|
+
// `epic-implementer` name. This nested `install-bridge conductor` surface is
|
|
4463
|
+
// untouched by that rename.
|
|
4209
4464
|
if (argv[0] === "conductor") {
|
|
4210
4465
|
if (!baseUrlResult.ok) {
|
|
4211
4466
|
errorLog(baseUrlResult.error);
|
|
@@ -4339,10 +4594,11 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
|
|
|
4339
4594
|
errorLog(`Error: no launch agent is registered for '${launchDecision.agent}'.`);
|
|
4340
4595
|
return 1;
|
|
4341
4596
|
}
|
|
4597
|
+
const modelAlias = await resolveValidatedPremiumModelAlias(deps, spec);
|
|
4342
4598
|
launch = {
|
|
4343
4599
|
kind: "spawn",
|
|
4344
4600
|
agent: launchDecision.agent,
|
|
4345
|
-
spawnCommand: deps.buildShellCommand(spec, INSTALL_BRIDGE_AGENT_PROMPT, deps.cwd, deps.platform),
|
|
4601
|
+
spawnCommand: deps.buildShellCommand(spec, INSTALL_BRIDGE_AGENT_PROMPT, deps.cwd, deps.platform, modelAlias),
|
|
4346
4602
|
};
|
|
4347
4603
|
}
|
|
4348
4604
|
else if (launchDecision.kind === "choose-one") {
|
|
@@ -4356,6 +4612,7 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
|
|
|
4356
4612
|
configTargets: dryRunTargets.map((t) => t.relPath),
|
|
4357
4613
|
manualEditors: manualEditorNames(await detectManualEditors(deps)),
|
|
4358
4614
|
launch,
|
|
4615
|
+
notesTargets: resolveAgentNotesTargets(dryRunPlatforms).targets,
|
|
4359
4616
|
};
|
|
4360
4617
|
}
|
|
4361
4618
|
else {
|
|
@@ -4658,8 +4915,8 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
|
|
|
4658
4915
|
// BAPI-635: resolve the AI-coding-tool selection FIRST — BAPI-657 makes this
|
|
4659
4916
|
// selection (not a hidden --agent default) drive which post-install session opens.
|
|
4660
4917
|
// Explicit --tools, then the TTY multi-select, then the legacy non-TTY automatic
|
|
4661
|
-
// set. The selected project JSON targets drive the read-merge-write path; selected
|
|
4662
|
-
// global (Codex
|
|
4918
|
+
// set. The selected project JSON targets drive the read-merge-write path; a selected
|
|
4919
|
+
// global (Codex) or manual (Windsurf) target is provisioned by the
|
|
4663
4920
|
// registry-driven emitter after the connectivity check.
|
|
4664
4921
|
const selectedPlatforms = await resolveSelectedHostPlatforms(deps, options);
|
|
4665
4922
|
const targets = hostConfigTargetsForPlatforms(selectedPlatforms);
|
|
@@ -4680,10 +4937,11 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
|
|
|
4680
4937
|
errorLog(`Error: no launch agent is registered for '${launchDecision.agent}'.`);
|
|
4681
4938
|
return 1;
|
|
4682
4939
|
}
|
|
4940
|
+
const modelAlias = await resolveValidatedPremiumModelAlias(deps, spec);
|
|
4683
4941
|
planLaunch = {
|
|
4684
4942
|
kind: "spawn",
|
|
4685
4943
|
agent: launchDecision.agent,
|
|
4686
|
-
spawnCommand: deps.buildShellCommand(spec, INSTALL_BRIDGE_AGENT_PROMPT, deps.cwd, deps.platform),
|
|
4944
|
+
spawnCommand: deps.buildShellCommand(spec, INSTALL_BRIDGE_AGENT_PROMPT, deps.cwd, deps.platform, modelAlias),
|
|
4687
4945
|
};
|
|
4688
4946
|
}
|
|
4689
4947
|
else if (launchDecision.kind === "choose-one") {
|
|
@@ -4738,7 +4996,8 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
|
|
|
4738
4996
|
return 1;
|
|
4739
4997
|
}
|
|
4740
4998
|
finalAgentName = chosen;
|
|
4741
|
-
|
|
4999
|
+
const modelAlias = await resolveValidatedPremiumModelAlias(deps, spec);
|
|
5000
|
+
finalSpawnCommand = deps.buildShellCommand(spec, INSTALL_BRIDGE_AGENT_PROMPT, deps.cwd, deps.platform, modelAlias);
|
|
4742
5001
|
}
|
|
4743
5002
|
// chosen === null → non-TTY / unavailable / invalid → manual continuation below.
|
|
4744
5003
|
}
|
|
@@ -4936,8 +5195,24 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
|
|
|
4936
5195
|
else if (requested.category === "invalid") {
|
|
4937
5196
|
reportFatal("Error: Self-serve setup could not be requested. Check the email value and try again.");
|
|
4938
5197
|
}
|
|
5198
|
+
else if (requested.category === "blocked") {
|
|
5199
|
+
// BAPI-1171: the request was refused before signup logic ran — an
|
|
5200
|
+
// upgrade would not change the outcome, so this is NOT protocol
|
|
5201
|
+
// guidance.
|
|
5202
|
+
reportFatal(INSTALL_BRIDGE_SIGNUP_BLOCKED);
|
|
5203
|
+
}
|
|
5204
|
+
else if (requested.category === "unexpected") {
|
|
5205
|
+
// BAPI-1171: an HTTP status this client has no specific handling
|
|
5206
|
+
// for. Reports only the numeric status — never response text.
|
|
5207
|
+
reportFatal(formatSelfServeUnexpectedStatus(requested.status ?? 0));
|
|
5208
|
+
}
|
|
5209
|
+
else if (requested.category === "network-error") {
|
|
5210
|
+
// BAPI-1171: the fetch itself failed — a transport problem, not a
|
|
5211
|
+
// protocol mismatch, so this is NOT upgrade guidance either.
|
|
5212
|
+
reportFatal(INSTALL_BRIDGE_SIGNUP_UNAVAILABLE);
|
|
5213
|
+
}
|
|
4939
5214
|
else {
|
|
4940
|
-
// R-2: a malformed or tokenless
|
|
5215
|
+
// R-2: a malformed or tokenless success body is a PROTOCOL
|
|
4941
5216
|
// mismatch, not a network problem. Saying "check connectivity" here
|
|
4942
5217
|
// was the old behavior and is exactly what this ticket replaces.
|
|
4943
5218
|
reportFatal(INSTALL_BRIDGE_SIGNUP_UPGRADE_REQUIRED);
|
|
@@ -4986,6 +5261,11 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
|
|
|
4986
5261
|
const verified = await verifySelfServeChallenge(deps, baseUrl, challengeHandle, code);
|
|
4987
5262
|
if (verified.ok) {
|
|
4988
5263
|
inviteToken = verified.token;
|
|
5264
|
+
// BAPI-1173: only an EXPLICIT false prints. `true`, and an absent field
|
|
5265
|
+
// from an older server, add nothing.
|
|
5266
|
+
if (verified.trialGranted === false) {
|
|
5267
|
+
log(` ${INSTALL_BRIDGE_TRIAL_ALREADY_USED}`);
|
|
5268
|
+
}
|
|
4989
5269
|
break;
|
|
4990
5270
|
}
|
|
4991
5271
|
if (verified.category === "rate-limited") {
|
|
@@ -4999,10 +5279,28 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
|
|
|
4999
5279
|
reportFatal(INSTALL_BRIDGE_VERIFY_SERVER_ERROR);
|
|
5000
5280
|
return false;
|
|
5001
5281
|
}
|
|
5282
|
+
if (verified.category === "blocked") {
|
|
5283
|
+
// BAPI-1171: the request was refused before verification logic ran —
|
|
5284
|
+
// an upgrade would not change the outcome.
|
|
5285
|
+
reportFatal(INSTALL_BRIDGE_SIGNUP_BLOCKED);
|
|
5286
|
+
return false;
|
|
5287
|
+
}
|
|
5288
|
+
if (verified.category === "unexpected") {
|
|
5289
|
+
// BAPI-1171: an HTTP status this client has no specific handling for.
|
|
5290
|
+
// Reports only the numeric status — never response text.
|
|
5291
|
+
reportFatal(formatSelfServeUnexpectedStatus(verified.status ?? 0));
|
|
5292
|
+
return false;
|
|
5293
|
+
}
|
|
5294
|
+
if (verified.category === "network-error") {
|
|
5295
|
+
// BAPI-1171: the fetch itself failed — a transport problem, not a
|
|
5296
|
+
// protocol mismatch.
|
|
5297
|
+
reportFatal(INSTALL_BRIDGE_VERIFY_SERVER_ERROR);
|
|
5298
|
+
return false;
|
|
5299
|
+
}
|
|
5002
5300
|
if (verified.category === "failed") {
|
|
5003
|
-
// Only a genuine protocol mismatch reaches here now: a malformed body
|
|
5004
|
-
// a missing token
|
|
5005
|
-
//
|
|
5301
|
+
// Only a genuine protocol mismatch reaches here now: a malformed body
|
|
5302
|
+
// or a missing token in an otherwise successful response. R-2's
|
|
5303
|
+
// upgrade instruction is the right answer for that.
|
|
5006
5304
|
reportFatal(INSTALL_BRIDGE_SIGNUP_UPGRADE_REQUIRED);
|
|
5007
5305
|
return false;
|
|
5008
5306
|
}
|
|
@@ -5377,7 +5675,7 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
|
|
|
5377
5675
|
for (const { relPath } of writeResult.skipped) {
|
|
5378
5676
|
log(` skipped ${relPath} — existing config could not be parsed safely; left untouched`);
|
|
5379
5677
|
}
|
|
5380
|
-
// BAPI-635: provision selected GLOBAL
|
|
5678
|
+
// BAPI-635: provision a selected GLOBAL target (Codex) and MANUAL
|
|
5381
5679
|
// targets (Windsurf) via the registry-driven emitter. Global paths are never
|
|
5382
5680
|
// added to the repository .gitignore.
|
|
5383
5681
|
// BAPI-669 (R8): only a need-key run has a key the user has never seen, so only it
|
|
@@ -5413,12 +5711,6 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
|
|
|
5413
5711
|
const manualInstructions = buildManualHostInstructions(entry, legacyManualEditors, needKeyManual);
|
|
5414
5712
|
if (manualInstructions)
|
|
5415
5713
|
log(manualInstructions);
|
|
5416
|
-
// BAPI-635 (Step 7): when both Claude Code and Copilot CLI are selected, warn
|
|
5417
|
-
// that they use different, non-shared config surfaces.
|
|
5418
|
-
if (selectedPlatforms.includes("claude-code") && selectedPlatforms.includes("copilot-cli")) {
|
|
5419
|
-
log(" Note: Claude Code uses the project .mcp.json while GitHub Copilot CLI uses only its " +
|
|
5420
|
-
"global ~/.copilot/mcp-config.json — the two are configured separately.");
|
|
5421
|
-
}
|
|
5422
5714
|
// BAPI-708 (B-c): the Claude trust reminder that used to print here was
|
|
5423
5715
|
// RELOCATED, not dropped — the reload requirement is genuinely actionable (the
|
|
5424
5716
|
// server does not work until the host reloads), but at Step 3 it duplicated the
|
|
@@ -5523,15 +5815,56 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
|
|
|
5523
5815
|
// the agent spawn, for two reasons: the spawned session's capability report should
|
|
5524
5816
|
// observe GitHub as configured if the user connects it here, and running it after the
|
|
5525
5817
|
// spawn would put two prompts on the same terminal at once.
|
|
5526
|
-
|
|
5818
|
+
//
|
|
5819
|
+
// BAPI-1171: one shared session-state object, constructed fresh for this run, is
|
|
5820
|
+
// passed to both offers so a GitHub connection made here suppresses the Bitbucket
|
|
5821
|
+
// offer that follows — see `InstallBridgeVcsSessionState`.
|
|
5822
|
+
const vcsSessionState = { vcsConnected: false };
|
|
5823
|
+
await offerGithubConnection(repoName, baseUrl, deps, log, vcsSessionState);
|
|
5527
5824
|
// ---- optional Bitbucket connect offer (BAPI-966) ----
|
|
5528
5825
|
// Same placement reasoning as the GitHub offer above. Both offers are called
|
|
5529
5826
|
// unconditionally here — each self-gates on its OWN provider-scoped configuration
|
|
5530
5827
|
// state (`fetchGithubConfigurationState`/`fetchBitbucketConfigurationState` return
|
|
5531
5828
|
// "unavailable" when their integration id is absent from the manifest, which is the
|
|
5532
5829
|
// normal outcome for the non-matching provider), so only the project's actual VCS
|
|
5533
|
-
// provider ever reaches its prompt
|
|
5534
|
-
|
|
5830
|
+
// provider ever reaches its prompt — and the GitHub offer's own success, tracked in
|
|
5831
|
+
// `vcsSessionState`, can additionally short-circuit this one before either check runs.
|
|
5832
|
+
await offerBitbucketConnection(repoName, baseUrl, deps, log, vcsSessionState);
|
|
5833
|
+
// ---- optional cross-platform agent notes offer (BAPI-1207) ----
|
|
5834
|
+
// Placed alongside the GitHub/Bitbucket offers, after the durable install work —
|
|
5835
|
+
// never re-detects platforms; the plan is computed from THIS run's already
|
|
5836
|
+
// selected set. `writeAgentNotes` never touches `writeMcpInstallState` or any
|
|
5837
|
+
// other persisted install-state contract — consent is re-evaluated every run.
|
|
5838
|
+
const notesPlan = resolveAgentNotesTargets(selectedPlatforms);
|
|
5839
|
+
const notesOffer = await offerAgentGuidanceNotes(deps, log, notesPlan.targets);
|
|
5840
|
+
let notesResult;
|
|
5841
|
+
let notesWriteFailed = false;
|
|
5842
|
+
if (notesOffer.kind === "consented") {
|
|
5843
|
+
try {
|
|
5844
|
+
notesResult = await writeAgentNotes({
|
|
5845
|
+
cwd: deps.cwd,
|
|
5846
|
+
selectedPlatforms,
|
|
5847
|
+
guidance: AGENT_GUIDANCE,
|
|
5848
|
+
readFile: deps.readFile,
|
|
5849
|
+
writeFile: deps.writeFile,
|
|
5850
|
+
mkdir: deps.mkdir,
|
|
5851
|
+
});
|
|
5852
|
+
for (const file of notesResult.files) {
|
|
5853
|
+
if (file.status === "failed") {
|
|
5854
|
+
errorLog(` warning: could not update ${file.target} with agent notes (${file.reason}).`);
|
|
5855
|
+
}
|
|
5856
|
+
}
|
|
5857
|
+
}
|
|
5858
|
+
catch {
|
|
5859
|
+
// Fail-open, mirroring the routing-credential persistence above: the
|
|
5860
|
+
// install itself is already complete and durable.
|
|
5861
|
+
notesWriteFailed = true;
|
|
5862
|
+
errorLog(" warning: could not write agent notes (unexpected error).");
|
|
5863
|
+
}
|
|
5864
|
+
}
|
|
5865
|
+
const notesSummaryLine = buildAgentNotesSummaryLine(notesOffer, notesResult, notesWriteFailed, notesPlan.unsupportedPlatforms);
|
|
5866
|
+
if (notesSummaryLine)
|
|
5867
|
+
log(notesSummaryLine);
|
|
5535
5868
|
// ---- Step 3b (await) — settle the background pre-warm before the session opens ----
|
|
5536
5869
|
// The warm was kicked off (only on a spawn-capable path) right after the
|
|
5537
5870
|
// launch-command guards; await it HERE so the bucket is settled before the spawned
|