@bridge_gpt/mcp-server 0.2.55 → 0.2.57

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/build/init.js CHANGED
@@ -13,6 +13,7 @@ import { writeFile, mkdir, readFile, stat } from "fs/promises";
13
13
  import path from "path";
14
14
  import os from "os";
15
15
  import { COMMANDS } from "./commands.generated.js";
16
+ import { renderCommandForHost } from "./command-host.js";
16
17
  import { CODEX_SKILLS_DIR, codexSkillAssets } from "./codex-skill-adapter.js";
17
18
  import { AGENTS } from "./agents.generated.js";
18
19
  import { DOCS } from "./docs.generated.js";
@@ -770,7 +771,7 @@ export async function runInit(cwd, options = {}) {
770
771
  await mkdir(dir, { recursive: true });
771
772
  // Per-file read/compare/write runs concurrently; outcomes are applied to the
772
773
  // Sets afterward in entry order so the summary counts are unchanged.
773
- const outcomes = await Promise.all(commandEntries.map(([filename, content]) => scaffoldFile(path.join(dir, filename), content)));
774
+ const outcomes = await Promise.all(commandEntries.map(([filename, content]) => scaffoldFile(path.join(dir, filename), renderCommandForHost(filename, content, dir === path.join(cwd, ".cursor", "commands") ? "cursor" : "claude-code"))));
774
775
  commandEntries.forEach(([filename], i) => {
775
776
  const outcome = outcomes[i];
776
777
  if (outcome === "skipped") {
@@ -152,6 +152,7 @@ import { provisionHostTarget, createDefaultVendorProcessDeps, inspectBridgeApiPr
152
152
  import { adaptBridgeEntryForHostTarget, } from "./mcp-host-entry-adapter.js";
153
153
  import { MCP_SERVER_NAME, MCP_PACKAGE_NAME, resolveRegistrationKey, } from "./mcp-identity.js";
154
154
  import { DUPLICATE_REGISTRATION_GUIDANCE } from "./launcher-config-inspection.js";
155
+ import { codexHome, configureBridgeCodexProfile, inspectBridgeCodexProfile } from "./codex-bridge-profile.js";
155
156
  import { recordInstalledConductorCapability, recordInstalledExecutorServiceUnit, recordInstalledProjectArtifact, writeMcpInstallState, } from "./mcp-install-state.js";
156
157
  // BAPI-1207: opt-in cross-platform agent notes (AGENTS.md / CLAUDE.md).
157
158
  import { AGENT_GUIDANCE } from "./agent-guidance.generated.js";
@@ -517,6 +518,9 @@ export function getInstallBridgeUsage(baseUrl = DEFAULT_BAPI_BASE_URL) {
517
518
  " manual continuation instead). --tools= (empty) is",
518
519
  " an explicit empty selection: it writes nothing and",
519
520
  " launches nothing.",
521
+ " --codex-profile on|off Opt in to Bridge workflow MCP approvals for",
522
+ " Codex workers, or explicitly decline. With a",
523
+ " Codex TTY selection, the installer asks.",
520
524
  " --force Overwrite an existing real BAPI_API_KEY in a",
521
525
  " host config (or in the credential store) without",
522
526
  " prompting. It does NOT bypass the git-tracked",
@@ -530,7 +534,7 @@ export function getInstallBridgeUsage(baseUrl = DEFAULT_BAPI_BASE_URL) {
530
534
  " prompting, or spawning anything. With --invite it",
531
535
  " also never calls the exchange endpoint and never",
532
536
  " generates or stores a secret.",
533
- " --agent claude|cursor-agent Explicit launch override for the post-install",
537
+ " --agent claude|cursor-agent|codex Explicit agent choice for post-install",
534
538
  " session — it always wins, and there is NO Claude",
535
539
  " default. Without it the launch tool is derived",
536
540
  " from your selection: Claude Code opens `claude`.",
@@ -539,9 +543,11 @@ export function getInstallBridgeUsage(baseUrl = DEFAULT_BAPI_BASE_URL) {
539
543
  " session); selecting Cursor writes .cursor/mcp.json",
540
544
  " and prints how to finish by running /install-bridge",
541
545
  " in a new Cursor session. A selection whose tools have",
542
- " no agentic CLI (e.g. Copilot) likewise opens nothing",
546
+ " no supported auto-launch path (e.g. Copilot) opens nothing",
543
547
  " and prints how to finish configuring later. Passing",
544
- " --agent cursor-agent still force-spawns it.",
548
+ " --agent cursor-agent still force-spawns it. Codex",
549
+ " is accepted only for manual continuation: neither",
550
+ " this bootstrap nor start-tickets launches it yet.",
545
551
  " -h, --help Show this help.",
546
552
  "",
547
553
  "Subcommands:",
@@ -612,6 +618,7 @@ export function parseInstallBridgeArgs(argv) {
612
618
  let invite;
613
619
  let email;
614
620
  let tools;
621
+ let codexProfile;
615
622
  // Track SUPPLIED-ness separately from the values: `--invite` is legitimately
616
623
  // valueless (prompt path) and `--api-key ""` is still a contradiction with it.
617
624
  let inviteSupplied = false;
@@ -697,6 +704,16 @@ export function parseInstallBridgeArgs(argv) {
697
704
  i = r.nextIndex;
698
705
  continue;
699
706
  }
707
+ if (arg === "--codex-profile" || arg.startsWith("--codex-profile=")) {
708
+ const r = readValue(arg, "--codex-profile", i);
709
+ if ("error" in r)
710
+ return { status: "error", message: r.error };
711
+ if (r.value !== "on" && r.value !== "off")
712
+ return { status: "error", message: "--codex-profile must be on or off." };
713
+ codexProfile = r.value;
714
+ i = r.nextIndex;
715
+ continue;
716
+ }
700
717
  if (arg === "--agent" || arg.startsWith("--agent=")) {
701
718
  const r = readValue(arg, "--agent", i);
702
719
  if ("error" in r)
@@ -747,7 +764,7 @@ export function parseInstallBridgeArgs(argv) {
747
764
  }
748
765
  return {
749
766
  status: "ok",
750
- options: { apiKey, repo, force, dryRun, agentName, invite, inviteMode: inviteSupplied, email, tools },
767
+ options: { apiKey, repo, force, dryRun, agentName, invite, inviteMode: inviteSupplied, email, tools, codexProfile },
751
768
  };
752
769
  }
753
770
  /**
@@ -2057,7 +2074,7 @@ async function resolveDryRunHostPlatforms(deps, options) {
2057
2074
  * Launch agents install-bridge will NEVER auto-spawn from a host SELECTION, even
2058
2075
  * though their CLI exists and the host's MCP config is still written normally.
2059
2076
  *
2060
- * `cursor-agent` is excluded: its FIRST-RUN workspace-trust handshake collapses
2077
+ * `cursor-agent` and `codex` are excluded. Cursor's FIRST-RUN workspace-trust handshake collapses
2061
2078
  * under our osascript-spawned launch — the "Workspace Trust Required" dialog
2062
2079
  * renders, then the process exits before a selection registers, so configuration
2063
2080
  * never runs (reproduced on a pristine, never-trusted workspace; the identical
@@ -2065,16 +2082,20 @@ async function resolveDryRunHostPlatforms(deps, options) {
2065
2082
  * cursor-agent's own behavior on an untrusted workspace and is not something our
2066
2083
  * launch can drive, so we treat Cursor as a no-auto-launch host: selecting it
2067
2084
  * still writes `.cursor/mcp.json`, then prints the manual `/install-bridge`
2068
- * continuation (same path as Copilot/Codex/Windsurf, which have no agentic CLI).
2085
+ * continuation. Codex does have an agentic CLI, but this workflow is a Claude
2086
+ * `/install-bridge` command and its native skill is not available until after
2087
+ * the bootstrap it would be asked to run completes.
2069
2088
  * An explicit `--agent cursor-agent` override still spawns it — a deliberate,
2070
2089
  * documented power-user opt-in that bypasses this selection guard.
2071
2090
  */
2072
- const SELECTION_NON_LAUNCHABLE_AGENTS = new Set(["cursor-agent"]);
2091
+ const SELECTION_NON_LAUNCHABLE_AGENTS = new Set(["cursor-agent", "codex"]);
2092
+ /** Agents whose interactive command surface cannot run the post-install recipe yet. */
2093
+ const INSTALL_BRIDGE_UNSUPPORTED_AGENTS = new Set(["codex"]);
2073
2094
  /**
2074
2095
  * Resolve the post-install launch decision from the selected host platforms and an
2075
2096
  * OPTIONAL explicit `--agent` override (BAPI-657). Pure and total:
2076
2097
  *
2077
- * - An explicit `--agent` is an ABSOLUTE override (ratified AC-4): a valid explicit
2098
+ * - An explicit supported `--agent` is an ABSOLUTE override: a valid explicit
2078
2099
  * agent always returns `spawn(agent)`, regardless of which tools were selected —
2079
2100
  * the flag is honored even for a host outside the selection, and it bypasses the
2080
2101
  * {@link SELECTION_NON_LAUNCHABLE_AGENTS} guard below.
@@ -2089,6 +2110,9 @@ const SELECTION_NON_LAUNCHABLE_AGENTS = new Set(["cursor-agent"]);
2089
2110
  */
2090
2111
  export function resolveInstallBridgeLaunchDecision(selectedPlatforms, explicitAgent) {
2091
2112
  if (explicitAgent) {
2113
+ if (INSTALL_BRIDGE_UNSUPPORTED_AGENTS.has(explicitAgent)) {
2114
+ return { kind: "manual", reason: "unsupported-agent-workflow" };
2115
+ }
2092
2116
  return { kind: "spawn", agent: explicitAgent };
2093
2117
  }
2094
2118
  if (selectedPlatforms.length === 0) {
@@ -3871,7 +3895,9 @@ function describePlannedLaunchAgent(launch) {
3871
3895
  }
3872
3896
  return launch.reason === "empty-selection"
3873
3897
  ? "none — no tools selected"
3874
- : "none — no launchable tool selected; manual continuation is printed";
3898
+ : launch.reason === "unsupported-agent-workflow"
3899
+ ? "none — selected agent cannot run the install-bridge workflow; manual continuation is printed"
3900
+ : "none — no launchable tool selected; manual continuation is printed";
3875
3901
  }
3876
3902
  /**
3877
3903
  * The Step 5 preview lines, shared by both previews so the launch description
@@ -3923,7 +3949,9 @@ function buildLaunchStepPreview(launch) {
3923
3949
  ...githubLines,
3924
3950
  launch.reason === "empty-selection"
3925
3951
  ? "Step 5 — no agent session: no tools were selected, so nothing is configured or opened; re-run and select at least one tool."
3926
- : "Step 5 — no automatic launch: no selected tool has an agentic CLI. The deterministic setup completes and copy-pasteable /install-bridge continuation is printed (your Bridge MCP tools stay limited until configured).",
3952
+ : launch.reason === "unsupported-agent-workflow"
3953
+ ? "Step 5 — no automatic launch: the selected agent cannot run the install-bridge workflow. The deterministic setup completes and manual continuation is printed."
3954
+ : "Step 5 — no automatic launch: no selected tool can auto-launch this workflow. The deterministic setup completes and copy-pasteable /install-bridge continuation is printed (your Bridge MCP tools stay limited until configured).",
3927
3955
  ];
3928
3956
  }
3929
3957
  /**
@@ -4919,6 +4947,15 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
4919
4947
  // global (Codex) or manual (Windsurf) target is provisioned by the
4920
4948
  // registry-driven emitter after the connectivity check.
4921
4949
  const selectedPlatforms = await resolveSelectedHostPlatforms(deps, options);
4950
+ if (options.codexProfile && !selectedPlatforms.includes("codex")) {
4951
+ errorLog("Error: --codex-profile requires Codex in --tools or the interactive selection.");
4952
+ return 1;
4953
+ }
4954
+ let codexProfileChoice = options.codexProfile;
4955
+ if (selectedPlatforms.includes("codex") && !options.dryRun && !codexProfileChoice && deps.isTTY && deps.promptLine) {
4956
+ const answer = (await deps.promptLine("Install the opt-in Bridge Codex approval profile for spawned workers? (y/N) ")).trim().toLowerCase();
4957
+ codexProfileChoice = answer === "y" || answer === "yes" ? "on" : "off";
4958
+ }
4922
4959
  const targets = hostConfigTargetsForPlatforms(selectedPlatforms);
4923
4960
  // Read-only detection (safe in dry-run) of global-config editors we can't write.
4924
4961
  const manualEditors = await detectManualEditors(deps);
@@ -5699,6 +5736,28 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
5699
5736
  errorLog(" That host was left untouched. Re-run install-bridge once one entry is removed.");
5700
5737
  return 1;
5701
5738
  }
5739
+ if (selectedPlatforms.includes("codex")) {
5740
+ const home = codexHome(deps.env, deps.homedir());
5741
+ try {
5742
+ const profileDeps = {
5743
+ readFile: deps.readFile,
5744
+ writeFile: (p, data) => deps.writeFile(p, data, { encoding: "utf-8", mode: 0o600 }),
5745
+ mkdir: deps.mkdir,
5746
+ };
5747
+ const result = codexProfileChoice
5748
+ ? await configureBridgeCodexProfile(home, codexProfileChoice === "on" ? "enable" : "decline", profileDeps)
5749
+ : await inspectBridgeCodexProfile(home, deps.readFile);
5750
+ if (result.status === "active")
5751
+ log(" Codex Bridge approval profile installed; spawned workers use --profile bridge.");
5752
+ else if (result.status === "declined" || result.status === "missing")
5753
+ log(" Codex Bridge approval profile inactive; spawned workers use existing Codex approvals. Opt in with --codex-profile=on.");
5754
+ else
5755
+ log(` Codex Bridge approval profile ${result.status}; spawned workers will not use it. Inspect ${home}/bridge.config.toml and rerun with --codex-profile=on.`);
5756
+ }
5757
+ catch {
5758
+ log(" Codex Bridge approval profile could not be configured; spawned workers will not use it. Inspect CODEX_HOME and rerun with --codex-profile=on.");
5759
+ }
5760
+ }
5702
5761
  // Legacy manual-editor instructions cover editors that are DETECTED but were
5703
5762
  // NOT part of the selection (so the emitter above did not handle them). The two
5704
5763
  // editors are suppressed INDEPENDENTLY: Codex is dropped when it was selected
@@ -28,6 +28,7 @@
28
28
  * rather than reserialize unrelated content.
29
29
  */
30
30
  import { VERSION } from "./version.generated.js";
31
+ import { codexHome } from "./codex-home.js";
31
32
  import { refreshBridgeApiPackageSpec } from "./init.js";
32
33
  import { hostAdapterForTarget, } from "./mcp-host-targets.js";
33
34
  import { adaptBridgeEntryForHostTarget, } from "./mcp-host-entry-adapter.js";
@@ -56,6 +57,9 @@ function dirnameOf(p) {
56
57
  }
57
58
  /** Resolve a target's absolute path from injected cwd/homedir. */
58
59
  export function resolveTargetAbsPath(target, ctx) {
60
+ if (target.id === "codex" && ctx.env?.CODEX_HOME) {
61
+ return `${codexHome(ctx.env, ctx.homedir)}/config.toml`;
62
+ }
59
63
  if (target.scope === "project") {
60
64
  if (!target.relPath)
61
65
  throw new Error(`${target.id} missing relPath`);
@@ -15,6 +15,7 @@
15
15
  * platform is a matter of adding one entry to {@link MCP_HOST_TARGETS} plus its
16
16
  * `PlatformId` union member.
17
17
  */
18
+ import { codexHome } from "./codex-home.js";
18
19
  // ---------------------------------------------------------------------------
19
20
  // Path helpers (pure)
20
21
  // ---------------------------------------------------------------------------
@@ -73,7 +74,7 @@ export const MCP_HOST_TARGETS = {
73
74
  hostKind: "claude-code-json",
74
75
  vendorCli: { bin: "claude", kind: "claude-add-json" },
75
76
  launchAgent: "claude",
76
- worktreeSupported: true,
77
+ capabilities: { mcpConfiguration: true, customCommands: true, worktreeProvisioning: true, sessionSpawning: true },
77
78
  writeStrategy: "vendor-first",
78
79
  // Claude Code is always a candidate default.
79
80
  detect: () => true,
@@ -86,7 +87,7 @@ export const MCP_HOST_TARGETS = {
86
87
  displayPath: ".cursor/mcp.json",
87
88
  hostKind: "cursor-json",
88
89
  launchAgent: "cursor-agent",
89
- worktreeSupported: true,
90
+ capabilities: { mcpConfiguration: true, customCommands: true, worktreeProvisioning: true, sessionSpawning: true },
90
91
  writeStrategy: "direct",
91
92
  detect: (ctx) => ctx.exists(joinPath(ctx.cwd, ".cursor")) ||
92
93
  typeof ctx.env.CURSOR_TRACE_DIR === "string" &&
@@ -99,7 +100,7 @@ export const MCP_HOST_TARGETS = {
99
100
  relPath: ".vscode/mcp.json",
100
101
  displayPath: ".vscode/mcp.json",
101
102
  hostKind: "vscode-json",
102
- worktreeSupported: false,
103
+ capabilities: { mcpConfiguration: true, customCommands: true, worktreeProvisioning: false, sessionSpawning: false },
103
104
  writeStrategy: "direct",
104
105
  detect: (ctx) => ctx.exists(joinPath(ctx.cwd, ".vscode")),
105
106
  },
@@ -111,7 +112,10 @@ export const MCP_HOST_TARGETS = {
111
112
  displayPath: "~/.codex/config.toml",
112
113
  hostKind: "codex-toml",
113
114
  vendorCli: { bin: "codex", kind: "codex-add" },
114
- worktreeSupported: false,
115
+ launchAgent: "codex",
116
+ // The launcher injects a per-process, worktree-bound MCP table. The global
117
+ // registration remains the ordinary interactive default.
118
+ capabilities: { mcpConfiguration: true, customCommands: true, worktreeProvisioning: true, sessionSpawning: true },
115
119
  writeStrategy: "vendor-first",
116
120
  detect: (ctx) => ctx.exists(joinPath(ctx.homedir, ".codex")),
117
121
  },
@@ -122,7 +126,7 @@ export const MCP_HOST_TARGETS = {
122
126
  absPathResolver: (homedir) => joinPath(homedir, ".codeium/windsurf/mcp_config.json"),
123
127
  displayPath: "~/.codeium/windsurf/mcp_config.json",
124
128
  hostKind: "claude-code-json", // Windsurf uses the Claude-compatible JSON shape.
125
- worktreeSupported: false,
129
+ capabilities: { mcpConfiguration: true, customCommands: false, worktreeProvisioning: false, sessionSpawning: false },
126
130
  // Preserve current behavior: never auto-modify the global Windsurf file.
127
131
  writeStrategy: "manual-instructions",
128
132
  detect: (ctx) => ctx.exists(joinPath(ctx.cwd, ".windsurf")) ||
@@ -153,18 +157,22 @@ export function getHostTarget(id) {
153
157
  return isHostPlatformId(id) ? MCP_HOST_TARGETS[id] : undefined;
154
158
  }
155
159
  /**
156
- * The centrally-registered launchable agent for a host platform, or `undefined`
157
- * when the platform has no supported agentic CLI (Copilot / Codex / Windsurf).
160
+ * The centrally-registered agent identity for a host platform, or `undefined`
161
+ * when the platform has no registered agent (Copilot / Windsurf).
158
162
  * Pure projection of the registry's {@link HostTargetDefinition.launchAgent}
159
- * metadata — the single source of truth for which selected tools can open a
160
- * post-install configuration session (BAPI-657).
163
+ * metadata. Callers must check workflow-specific capabilities before launching.
161
164
  */
162
165
  export function agentForPlatform(id) {
163
166
  return MCP_HOST_TARGETS[id].launchAgent;
164
167
  }
165
- /** The two worktree-supported targets, in stable order (Claude, Cursor). */
168
+ /** Hosts whose declared workflow capability permits worktree provisioning. */
166
169
  export function getWorktreeHostTargets() {
167
- return allHostTargets().filter((t) => t.worktreeSupported);
170
+ return allHostTargets().filter((t) => t.capabilities.worktreeProvisioning);
171
+ }
172
+ /** True only when a host has every capability needed for a spawned workflow. */
173
+ export function isRunnableHostTarget(target) {
174
+ const c = target.capabilities;
175
+ return c.mcpConfiguration && c.customCommands && c.worktreeProvisioning && c.sessionSpawning && target.launchAgent !== undefined;
168
176
  }
169
177
  /** Project-scoped JSON targets (used by init, launcher-token merge, doctor). */
170
178
  export function getProjectJsonTargets() {
@@ -183,6 +191,8 @@ export function getManualHostTargets() {
183
191
  * Project targets resolve below `ctx.cwd`; global targets below `ctx.homedir`.
184
192
  */
185
193
  export function resolveHostTargetPath(target, ctx) {
194
+ if (target.id === "codex" && ctx.env?.CODEX_HOME)
195
+ return `${codexHome(ctx.env, ctx.homedir)}/config.toml`;
186
196
  if (target.scope === "project") {
187
197
  if (!target.relPath) {
188
198
  throw new Error(`project target ${target.id} is missing relPath`);
@@ -138,7 +138,7 @@ export function buildMcpServerEntriesForManifest(manifest, absoluteWorktreePath,
138
138
  /** Both registration files written for every provisioned worktree. */
139
139
  export function getWorktreeMcpRegistrationTargets(worktreePath, platform) {
140
140
  const api = pathApiForProvisioningPlatform(platform);
141
- return getWorktreeHostTargets().map((target) => ({
141
+ return getWorktreeHostTargets().filter((target) => target.scope === "project").map((target) => ({
142
142
  filePath: api.join(worktreePath, target.relPath),
143
143
  topLevelKey: hostAdapterForTarget(target).topLevelKey,
144
144
  }));
@@ -104,7 +104,7 @@ export function isBridgeApiShimEntry(entry, worktreeRoot) {
104
104
  * to validate.
105
105
  */
106
106
  export async function probeWorktreeMcpRegistration(worktreeRoot, deps) {
107
- const targets = getWorktreeHostTargets().map((target) => ({
107
+ const targets = getWorktreeHostTargets().filter((target) => target.scope === "project").map((target) => ({
108
108
  filePath: path.join(worktreeRoot, target.relPath),
109
109
  topLevelKey: hostAdapterForTarget(target).topLevelKey,
110
110
  }));