@bridge_gpt/mcp-server 0.2.10 → 0.2.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/README.md +66 -4
  2. package/build/commands.generated.js +6 -6
  3. package/build/conductor/bridge-api-client.js +2 -1
  4. package/build/conductor/cli.js +16 -16
  5. package/build/conductor/doctor.js +79 -2
  6. package/build/conductor/epic-reconcile.js +213 -16
  7. package/build/conductor/epic-runtime.js +215 -60
  8. package/build/conductor/epic-state.js +105 -16
  9. package/build/conductor/errors.js +12 -0
  10. package/build/conductor/git-ci-types.js +10 -0
  11. package/build/conductor/git-producer.js +4 -4
  12. package/build/conductor/local-merge.js +212 -0
  13. package/build/conductor/merge-ledger.js +7 -7
  14. package/build/conductor/pr-ci-producer.js +18 -8
  15. package/build/conductor/pr-review-producer.js +2 -2
  16. package/build/conductor/producer-ledger.js +5 -5
  17. package/build/conductor/spec-review-producer.js +88 -0
  18. package/build/conductor/store.js +100 -25
  19. package/build/conductor/supervisor-ledger.js +2 -2
  20. package/build/conductor/supervisor-merge.js +5 -5
  21. package/build/conductor/supervisor-message-relay.js +1 -1
  22. package/build/conductor/supervisor-runtime.js +10 -10
  23. package/build/conductor/taxonomy.js +9 -0
  24. package/build/conductor/tools.js +5 -5
  25. package/build/conductor-bin.js +12689 -19
  26. package/build/conductor-claude-hook-bin.js +167 -17
  27. package/build/decision-page-schema.js +26 -0
  28. package/build/doctor.js +203 -0
  29. package/build/index.js +25084 -3632
  30. package/build/init.js +57 -0
  31. package/build/install-bridge.js +80 -0
  32. package/build/mcp-profile.js +33 -30
  33. package/build/pipelines.generated.js +70 -48
  34. package/build/readme.generated.js +1 -1
  35. package/build/sfcc/client.js +151 -0
  36. package/build/sfcc/config.js +39 -0
  37. package/build/sfcc/credentials.js +136 -0
  38. package/build/sfcc/ocapi-shape.js +77 -0
  39. package/build/sfcc/output.js +39 -0
  40. package/build/sfcc/permissions.js +136 -0
  41. package/build/sfcc/reads-custom-object-def.js +119 -0
  42. package/build/sfcc/reads-site-preference.js +158 -0
  43. package/build/sfcc/reads-system-object.js +162 -0
  44. package/build/sfcc/register.js +73 -0
  45. package/build/sfcc/setup-status.js +114 -0
  46. package/build/sfcc/tool-wrapper.js +70 -0
  47. package/build/start-tickets-conductor.js +9 -1
  48. package/build/start-tickets.js +47 -4
  49. package/build/version.generated.js +1 -1
  50. package/package.json +7 -4
  51. package/pipelines/check-ci-ticket.json +2 -2
  52. package/pipelines/implement-ticket.json +2 -2
  53. package/pipelines/learn-repository.json +84 -42
  54. package/smoke-test/SMOKE-TEST.md +11 -17
package/build/init.js CHANGED
@@ -78,6 +78,63 @@ export function buildBridgeConfigManifest(repoName) {
78
78
  "",
79
79
  ].join("\n");
80
80
  }
81
+ /**
82
+ * Merge a group token (e.g. "sfcc") into the BRIDGE_MCP_PROFILE env value of the
83
+ * `bridge-api` entry in each present MCP config file (.mcp.json, .vscode/mcp.json,
84
+ * .cursor/mcp.json). Existing tokens are preserved; the merge is idempotent.
85
+ *
86
+ * Only files where a `bridge-api` entry already exists are modified — new files
87
+ * are never created by this helper.
88
+ *
89
+ * Returns the final resolved profile string (comma-joined) after merging, or an
90
+ * empty string if no config file was found or updated.
91
+ */
92
+ export async function mergeBridgeApiProfileToken(cwd, token) {
93
+ const targets = [
94
+ { path: ".mcp.json", topLevelKey: "mcpServers" },
95
+ { path: ".vscode/mcp.json", topLevelKey: "servers" },
96
+ { path: ".cursor/mcp.json", topLevelKey: "mcpServers" },
97
+ ];
98
+ let lastProfile = "";
99
+ for (const target of targets) {
100
+ const fullPath = path.join(cwd, target.path);
101
+ let raw;
102
+ try {
103
+ raw = await readFile(fullPath, "utf-8");
104
+ }
105
+ catch {
106
+ continue; // file doesn't exist — skip
107
+ }
108
+ let parsed;
109
+ try {
110
+ parsed = JSON.parse(raw);
111
+ }
112
+ catch {
113
+ continue; // invalid JSON — skip
114
+ }
115
+ const topLevel = parsed[target.topLevelKey];
116
+ if (!topLevel || !topLevel["bridge-api"]) {
117
+ continue; // no bridge-api entry — skip
118
+ }
119
+ const entry = topLevel["bridge-api"];
120
+ if (!entry.env)
121
+ entry.env = {};
122
+ const existing = entry.env.BRIDGE_MCP_PROFILE;
123
+ // Use raw string splitting (not resolveProfiles) so we don't force-inject
124
+ // "core" into the stored value. The runtime always adds "core" implicitly
125
+ // when it reads the value, so storing it would cause duplication.
126
+ const existingTokens = existing
127
+ ? existing.split(",").map((t) => t.trim()).filter((t) => t.length > 0)
128
+ : [];
129
+ const tokenSet = new Set(existingTokens);
130
+ tokenSet.add(token);
131
+ const merged = Array.from(tokenSet).join(",");
132
+ entry.env.BRIDGE_MCP_PROFILE = merged;
133
+ await writeFile(fullPath, JSON.stringify(parsed, null, 2) + "\n", "utf-8");
134
+ lastProfile = merged;
135
+ }
136
+ return lastProfile;
137
+ }
81
138
  async function ensureGitignored(cwd, filePath) {
82
139
  await ensureGitignoredShared(cwd, filePath, {
83
140
  readFile: (p) => readFile(p, "utf-8"),
@@ -34,10 +34,12 @@
34
34
  * gitignored) and the user-scoped credential store (Step 4).
35
35
  */
36
36
  import { readFile, writeFile, mkdir, stat, rename, chmod, unlink } from "fs/promises";
37
+ import { spawn } from "child_process";
37
38
  import os from "os";
38
39
  import path from "path";
39
40
  import readline from "readline";
40
41
  import { runInit, buildBridgeApiEntry } from "./init.js";
42
+ import { VERSION } from "./version.generated.js";
41
43
  import { validateRepoName } from "./bridge-config.js";
42
44
  import { resolveStartTicketsRepoName } from "./start-tickets-repo.js";
43
45
  import { upsertBapiCredential, getPrimaryCredentialStorePath, } from "./credential-store.js";
@@ -45,6 +47,23 @@ import { DEFAULT_AGENT_NAME, resolveAgentSpec, isAgentName, formatValidAgentName
45
47
  import { buildGenericAgentShellCommand, getDefaultSpawnTerminalTabForPlatform, detectTerminal, createDefaultStartTicketsDeps, } from "./start-tickets.js";
46
48
  /** Redaction sentinel — the API-key value is NEVER printed; this stands in. */
47
49
  export const REDACTED_API_KEY = "<REDACTED>";
50
+ /**
51
+ * Always-surfaced success-path note (BAPI-451 W3, E-3). The pinned launcher's
52
+ * FIRST cold launch can resolve/download the package inline; if the MCP client's
53
+ * connect deadline is very short it may time out before the handshake. Pre-warming
54
+ * (below) makes this rare, but the note is surfaced unconditionally so the
55
+ * fail-soft backstop is always discoverable.
56
+ */
57
+ export const MCP_TIMEOUT_GUIDANCE = "Note: if your MCP client has a very short connect deadline, raise MCP_TIMEOUT for the first launch " +
58
+ "(the initial npx package resolution/download can exceed a short connect timeout).";
59
+ /** The exact, version-pinned launcher spec the pre-warm spawns (fast-exiting --version). */
60
+ export function buildPrewarmArgs() {
61
+ return ["-y", `@bridge_gpt/mcp-server@${VERSION}`, "--version"];
62
+ }
63
+ /** Secret-free preview of the pre-warm command (no env, no key). */
64
+ export function buildPrewarmCommandPreview() {
65
+ return `npx ${buildPrewarmArgs().join(" ")}`;
66
+ }
48
67
  /** The natural-language sequential prompt handed to the spawned agent session. */
49
68
  export const INSTALL_BRIDGE_AGENT_PROMPT = "Please execute the /install-bridge command. Once it completes successfully, execute the /learn-repository command.";
50
69
  /** Default base URL when `BAPI_BASE_URL` is unset (mirrors index.ts). */
@@ -198,6 +217,44 @@ function promptLineViaReadline(promptText) {
198
217
  });
199
218
  });
200
219
  }
220
+ /**
221
+ * Default pre-warm spawner (BAPI-451 W3). Spawns array-based (`shell: false`) so
222
+ * no argument is ever shell-interpreted, ignores all child stdio (node-gyp /
223
+ * download noise stays out of the CLI UX), bounds the run with a 60s timeout, and
224
+ * passes a SANITIZED env clone with `BAPI_API_KEY` deleted — the `--version`
225
+ * pre-warm never needs the key, and it must never reach an npm lifecycle script.
226
+ * Always resolves (never rejects): a spawn error / non-zero exit / timeout becomes
227
+ * `{ ok: false, warning }` with secret-free warning text.
228
+ */
229
+ function spawnPrewarmDefault(command, args, env) {
230
+ return new Promise((resolve) => {
231
+ const sanitizedEnv = { ...env };
232
+ delete sanitizedEnv.BAPI_API_KEY;
233
+ try {
234
+ const child = spawn(command, args, {
235
+ shell: false,
236
+ stdio: "ignore",
237
+ timeout: 60_000,
238
+ env: sanitizedEnv,
239
+ });
240
+ child.on("error", () => resolve({ ok: false, warning: "the pre-warm process could not be started" }));
241
+ child.on("close", (code, signal) => {
242
+ if (signal) {
243
+ resolve({ ok: false, warning: `the pre-warm process timed out or was terminated (${signal})` });
244
+ }
245
+ else if (code === 0) {
246
+ resolve({ ok: true });
247
+ }
248
+ else {
249
+ resolve({ ok: false, warning: `the pre-warm process exited with code ${code}` });
250
+ }
251
+ });
252
+ }
253
+ catch {
254
+ resolve({ ok: false, warning: "the pre-warm process could not be started" });
255
+ }
256
+ });
257
+ }
201
258
  /** Build default deps from the live process. */
202
259
  export function createDefaultInstallBridgeDeps() {
203
260
  const isTTY = Boolean(process.stdin.isTTY);
@@ -217,6 +274,7 @@ export function createDefaultInstallBridgeDeps() {
217
274
  promptSecret: isTTY ? promptSecretViaReadline : undefined,
218
275
  promptLine: isTTY ? promptLineViaReadline : undefined,
219
276
  fetch: (...args) => fetch(...args),
277
+ spawnPrewarm: spawnPrewarmDefault,
220
278
  runInit,
221
279
  upsertCredential: upsertBapiCredential,
222
280
  buildShellCommand: buildGenericAgentShellCommand,
@@ -472,6 +530,8 @@ export function buildDryRunPreview(plan) {
472
530
  ...(plan.manualEditors.length > 0
473
531
  ? [` ${plan.manualEditors.join(" + ")}: detected (global config) — manual setup instructions would be printed.`]
474
532
  : []),
533
+ `Step 3b — pre-warm the version-pinned launcher bucket (fail-open, env sanitized — BAPI_API_KEY removed): ${plan.prewarmCommand}`,
534
+ MCP_TIMEOUT_GUIDANCE,
475
535
  `Step 4 — persist routing credential: target ${plan.credentialTarget} at ${plan.credentialStorePath}`,
476
536
  `Step 5 — spawn agent session: ${plan.spawnCommand}`,
477
537
  ];
@@ -590,6 +650,7 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
590
650
  credentialTarget: `bapi:${repoName}`,
591
651
  credentialStorePath,
592
652
  pingUrl: buildPingUrl(baseUrl, repoName),
653
+ prewarmCommand: buildPrewarmCommandPreview(),
593
654
  spawnCommand,
594
655
  };
595
656
  // ---- --dry-run: preview every step, strictly no side effects ----
@@ -639,6 +700,25 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
639
700
  const manualInstructions = buildManualHostInstructions(entry, manualEditors);
640
701
  if (manualInstructions)
641
702
  log(manualInstructions);
703
+ // ---- Step 3b — pre-warm the @${VERSION}-pinned _npx bucket (BAPI-451 W3) ----
704
+ // The launcher just written is pinned to @${VERSION}, a DIFFERENT _npx bucket
705
+ // than the @latest bucket this `npx … install-bridge` invocation warmed. Spawn
706
+ // the exact pinned launcher once with the fast-exiting `--version` flag so the
707
+ // first real MCP launch resolves from a warm bucket instead of paying the cold
708
+ // install inline (which can exceed the client's connect deadline). Strictly
709
+ // fail-open: a failure warns (secret-free) but never fails the install.
710
+ log(" pre-warming the version-pinned launcher bucket…");
711
+ const prewarm = await deps.spawnPrewarm("npx", buildPrewarmArgs(), deps.env);
712
+ if (prewarm.ok) {
713
+ log(" launcher bucket warmed (the first MCP launch will not pay a cold install).");
714
+ log(` ${MCP_TIMEOUT_GUIDANCE}`);
715
+ }
716
+ else {
717
+ // Escalate to a stronger, still secret-free warning when pre-warm didn't
718
+ // complete cleanly — but the install itself still succeeds (exit unaffected).
719
+ errorLog(`Warning: could not pre-warm the version-pinned launcher bucket${prewarm.warning ? ` (${prewarm.warning})` : ""}. ` +
720
+ `The first MCP launch may pay a one-time cold install and could be slow. ${MCP_TIMEOUT_GUIDANCE}`);
721
+ }
642
722
  // ---- Step 4 — persist routing credential (non-blocking / fail-open) ----
643
723
  log("Step 4/5 — persisting routing credential…");
644
724
  try {
@@ -1,43 +1,46 @@
1
1
  /**
2
- * MCP profile resolver (BAPI-434 Phase 2a).
2
+ * MCP profile resolver (BAPI-453).
3
3
  *
4
- * Defines the four startup-time tool registration profiles and provides pure,
5
- * unit-testable helpers for resolving the active profile from an env-var string
6
- * and for checking group membership.
4
+ * Generalizes the startup-time tool registration model from a single-string
5
+ * profile to a multi-valued group set. BRIDGE_MCP_PROFILE accepts a
6
+ * comma-separated list of group tokens; unknown tokens are silently dropped
7
+ * (security sentinel — raw input is never echoed). "core" is always implicitly
8
+ * included. "full" is an input shortcut that expands to every known group.
7
9
  *
8
- * Profiles are static they gate which tools are registered at process startup
9
- * and cannot be changed mid-session via tools/list_changed.
10
+ * Groups are resolved once at process startup and cannot change mid-session.
10
11
  */
11
- const VALID_PROFILES = new Set([
12
+ const VALID_GROUPS = new Set([
12
13
  "core",
13
14
  "conductor",
14
15
  "pipeline-authoring",
15
- "full",
16
+ "sfcc",
16
17
  ]);
17
18
  /**
18
- * Resolve the active MCP profile from a raw env-var string.
19
+ * Resolve the active MCP groups from a raw env-var string.
19
20
  *
20
- * - `undefined` or blank → `"core"` (fail-safe to smallest surface).
21
- * - Known values (case-insensitive, whitespace-trimmed) → normalized profile.
22
- * - Unknown / malformed values `"core"` (fail-safe).
21
+ * - `undefined` or blank → `{"core"}` (fail-safe to smallest surface).
22
+ * - `"full"` (any case) → all four groups.
23
+ * - Comma-separated list: each token is trimmed, lowercased, and checked
24
+ * against VALID_GROUPS. Unknown tokens are dropped. Known tokens are unioned
25
+ * with the implicit `"core"` baseline.
26
+ * - All-unknown tokens → `{"core"}`.
27
+ * - Raw input is never echoed in the output (security NFR).
23
28
  */
24
- export function resolveProfile(raw) {
29
+ export function resolveProfiles(raw) {
30
+ const baseline = new Set(["core"]);
25
31
  if (raw === undefined)
26
- return "core";
27
- const normalized = raw.trim().toLowerCase();
28
- if (normalized === "")
29
- return "core";
30
- if (VALID_PROFILES.has(normalized))
31
- return normalized;
32
- return "core";
33
- }
34
- /**
35
- * Return true when `profile` should include tools from the given gated group.
36
- *
37
- * - `"full"` includes every group.
38
- * - Any other profile includes only its own matching group.
39
- * - `"core"` includes neither gated group.
40
- */
41
- export function profileIncludes(profile, group) {
42
- return profile === "full" || profile === group;
32
+ return baseline;
33
+ const trimmed = raw.trim();
34
+ if (trimmed === "")
35
+ return baseline;
36
+ const tokens = trimmed.split(",").map((t) => t.trim().toLowerCase());
37
+ if (tokens.some((t) => t === "full")) {
38
+ return new Set(["core", "conductor", "pipeline-authoring", "sfcc"]);
39
+ }
40
+ for (const token of tokens) {
41
+ if (VALID_GROUPS.has(token)) {
42
+ baseline.add(token);
43
+ }
44
+ }
45
+ return baseline;
43
46
  }