@bridge_gpt/mcp-server 0.2.30 → 0.2.32

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 CHANGED
@@ -31,18 +31,29 @@ npx -y @bridge_gpt/mcp-server@latest install-bridge
31
31
  ```
32
32
 
33
33
  Run bare like that in a terminal and it starts by asking
34
- **`Do you have a Bridge API key or invite? [Y/n]`**:
35
-
36
- - **Yes** (or just press Enter) — the existing-key flow. It asks for your **API key**
37
- (generate one on the Bridge API web UI **Security** page) and a **repo name**
38
- matching your server-side registration; everything else is derived. A
39
- `bapi_inv_…` credential entered here instead of a full API key is automatically
40
- detected and redeemed as a **bootstrap invite** — it creates a brand-new project
41
- and mints your admin API key rather than looking up an existing repository.
42
- - **No** — the **self-serve** flow. It asks for an **email**, then a name for your new
43
- Bridge project, and creates the workspace and your own admin API key for you. No
44
- account, no key, and no invite needed beforehand. Same as passing
45
- `--email you@example.com` (see below).
34
+ **`How would you like to connect to Bridge API?`** with three numbered choices:
35
+
36
+ ```
37
+ 1. I have a Bridge API key
38
+ 2. I have an invite token
39
+ 3. I'm new set me up with just my email
40
+ ```
41
+
42
+ - **1** — the existing-key flow. It asks for your **API key** (generate one on the
43
+ Bridge API web UI **Security** page) and a **repo name** matching your server-side
44
+ registration; everything else is derived. A `bapi_inv_…` credential entered here
45
+ instead of a full API key is automatically detected and redeemed as a **bootstrap
46
+ invite** — it creates a brand-new project and mints your admin API key rather than
47
+ looking up an existing repository.
48
+ - **2** — the **bootstrap-invite** flow. It asks for the invite token you were given,
49
+ with echo suppressed. Same as passing `--invite` (see below).
50
+ - **3** — the **self-serve** flow, and the right answer if you have nothing yet. It
51
+ asks for an **email**, then a name for your new Bridge project, and creates the
52
+ workspace and your own admin API key for you. No account, no key, and no invite
53
+ needed beforehand. Same as passing `--email you@example.com` (see below).
54
+
55
+ There is **no default**: pressing Enter selects nothing. A blank or invalid answer
56
+ re-prompts once with a hint, then exits with guidance naming all three routes.
46
57
 
47
58
  That question is asked only for a *bare interactive* run. Passing any flag, setting
48
59
  `BAPI_API_KEY`, or running without an interactive terminal skips it and keeps the
@@ -51,11 +62,12 @@ existing deterministic behavior.
51
62
  From there `install-bridge` scaffolds the project, writes your editor's MCP config
52
63
  with real values, verifies connectivity, persists your API key to the user-scoped
53
64
  credential store, and opens a fresh agent session that runs `/install-bridge` to
54
- derive and apply the remaining config, presents a **capability report** (what you can
55
- use now and what you'll unlock), and closes by asking whether to index the
56
- repository. It does **not** automatically run `/learn-repository` or index without
57
- your consent both remain available as separate steps. Add `--dry-run` to preview
58
- every step without writing, pinging, or spawning anything.
65
+ derive and apply the remaining config, presents a concise **capability report**
66
+ ("What Bridge can help with"), and recommends `/learn-repository` as your next step.
67
+ It does **not** run `/learn-repository` itself that stays your next explicit
68
+ invocation. There is **no indexing question**: indexing starts automatically
69
+ server-side once the repository reaches full parse readiness. Add `--dry-run` to
70
+ preview every step without writing, pinging, or spawning anything.
59
71
 
60
72
  **Were you sent a bootstrap invite?** Then you don't need an API key or the web UI
61
73
  at all — run the command your operator gave you:
@@ -148,12 +160,30 @@ shown**.
148
160
  The email may instead come from the `BAPI_SIGNUP_EMAIL` environment variable or a
149
161
  **visible** interactive prompt (email is not a secret, so it is echoed as you type —
150
162
  unlike the API key and the invite token, which use a hidden prompt). That prompt is
151
- what answering **no** to `Do you have a Bridge API key or invite? [Y/n]` on a bare run reaches,
152
- so `install-bridge --email you@example.com` and a bare `install-bridge` + "no" land
153
- in the same place. The email is still **never written to a log line**. No email
163
+ what **option 3** of the bare-run chooser reaches, so
164
+ `install-bridge --email you@example.com` and a bare `install-bridge` + `3` land in
165
+ the same place. The email is still **never written to a log line**. No email
154
166
  verification is performed and no message is sent to the address — it only labels your
155
167
  new workspace. `--email` is mutually exclusive with `--api-key` and `--invite`.
156
168
 
169
+ ##### Self-serve retries resume automatically
170
+
171
+ A self-serve run that fails part-way through — a network blip on the exchange, a
172
+ failed connectivity check, an interrupted credential write — saves its signup state
173
+ under `bootstrap-pending:<repo>` in the credential store (mode `0600`, fsync'd, the
174
+ same record that already holds your `key_secret`).
175
+
176
+ **Just re-run the self-serve flow.** It detects that saved attempt, prints
177
+ `resuming your previous signup attempt for <repo>`, and re-drives the *same*
178
+ exchange — it does **not** sign up again, so a retry never creates a second
179
+ workspace. This is why the self-serve record stores the minted invite token: you
180
+ were never shown that token, so nothing else could re-present it.
181
+
182
+ Do **not** copy, display, or hand-remove that record. If the saved invite has
183
+ genuinely expired, the CLI says so and **asks for confirmation** before discarding
184
+ it and starting fresh — it never discards it silently, because a record whose
185
+ exchange already succeeded is the only trace of a live admin key.
186
+
157
187
  Because this flow *creates* the project, it asks you to **name a new project**
158
188
  (`Name your new Bridge project [<inferred>]: `) rather than to match an existing
159
189
  server-side registration. The name must be globally unique; if it's taken, you're
@@ -689,12 +719,14 @@ Per-OS prerequisites: macOS `wt`, `git`, `osascript`; Windows `git-wt`, Git for
689
719
 
690
720
  ### `doctor`
691
721
 
692
- The package also ships a strictly **read-only** `doctor` subcommand that diagnoses the `start-tickets` prerequisites for the current OS without changing anything:
722
+ The package also ships a strictly **read-only** `doctor` subcommand titled **`bridge doctor — read-only diagnostics`** that diagnoses your whole Bridge install without changing anything:
693
723
 
694
724
  ```
695
725
  npx -y @bridge_gpt/mcp-server doctor [--agent <name>]
696
726
  ```
697
727
 
728
+ The report always leads with the advisory **`Install status`** section (repo identity, credential resolution, server connectivity, bootstrap-field completeness, integration credentials, indexing state) **before** the `start-tickets` prerequisite diagnostics; the launcher-cache and MCP tool-surface sections follow. `Install status` is read-only GETs only and never affects the exit code.
729
+
698
730
  It is **read-only**: it never installs anything, modifies your system, adds an npm `postinstall`, spawns a terminal, or starts the MCP server, and there is no `--fix`. For each prerequisite it prints found/missing and, when missing, the exact per-OS install command **as a manual instruction you run yourself**. The checked set is the `start-tickets` preflight prerequisites **plus `uv`** **plus the selected agent's command** (`claude` by default, or `cursor-agent` with `--agent cursor-agent`). The Worktrunk binary is probed via the resolved name (honoring `BAPI_WORKTRUNK_BIN`), not a hard-coded one. **Exit code:** `0` when all required prerequisites are present, non-zero when any is missing or the platform is unsupported. A failing `start-tickets` preflight now hints you to run `doctor` for an actionable diagnostics report.
699
731
 
700
732
  ### `setup-epic`
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Read-only worktree Claude command-asset diagnostic (doctor-only, BAPI-664).
3
+ *
4
+ * Reports whether the current worktree contains every packaged Claude slash
5
+ * command under `.claude/commands/`. This probe ONLY reads command paths — it
6
+ * never writes, creates directories, invokes Git, spawns a process, compares or
7
+ * reports command file CONTENTS, or leaks raw exceptions. A committed customer
8
+ * version of a command counts as present regardless of its contents (the command
9
+ * bundle is fill-only and never refreshed), so the probe checks presence /
10
+ * readability, not freshness. The sole injected dependencies are `readFile` and
11
+ * `platform`.
12
+ */
13
+ import path from "path";
14
+ import { COMMANDS } from "./commands.generated.js";
15
+ /** Stable user-facing relative location shown in every detail string. */
16
+ const COMMAND_DIR_LABEL = ".claude/commands/";
17
+ /** How many missing filenames to list before summarizing the remainder. */
18
+ const MAX_LISTED_MISSING = 5;
19
+ /** The `path` API for the target platform (win32 vs posix). */
20
+ function pathApiForPlatform(platform) {
21
+ return platform === "win32" ? path.win32 : path.posix;
22
+ }
23
+ /** Render a bounded, deterministic list of missing filenames (generated order). */
24
+ function formatMissing(missing) {
25
+ if (missing.length <= MAX_LISTED_MISSING) {
26
+ return missing.join(", ");
27
+ }
28
+ const shown = missing.slice(0, MAX_LISTED_MISSING).join(", ");
29
+ return `${shown} (+${missing.length - MAX_LISTED_MISSING} more)`;
30
+ }
31
+ /**
32
+ * Probe whether `<worktreeRoot>/.claude/commands/` contains every packaged
33
+ * command asset. A `readFile` that resolves counts the asset as present (any
34
+ * contents); a rejection (missing OR unreadable) counts it as absent. Returns
35
+ * `found: true` only when every packaged command filename is present, otherwise a
36
+ * `found: false` result with a concise, actionable, secret-free detail that
37
+ * points the operator back to `start-tickets` (never to manually copying or
38
+ * committing generated files).
39
+ */
40
+ export async function probeWorktreeCommandAssets(worktreeRoot, deps) {
41
+ const filenames = Object.keys(COMMANDS);
42
+ if (filenames.length === 0) {
43
+ return {
44
+ found: false,
45
+ detail: "Packaged command bundle is empty — reinstall or rebuild the MCP server package.",
46
+ };
47
+ }
48
+ const api = pathApiForPlatform(deps.platform);
49
+ const commandsDir = api.join(worktreeRoot, ".claude", "commands");
50
+ const missing = [];
51
+ for (const filename of filenames) {
52
+ try {
53
+ await deps.readFile(api.join(commandsDir, filename));
54
+ }
55
+ catch {
56
+ // Missing OR unreadable — either way the asset is not usable. We never
57
+ // surface the underlying error (it can carry paths / exception text).
58
+ missing.push(filename);
59
+ }
60
+ }
61
+ if (missing.length === 0) {
62
+ return {
63
+ found: true,
64
+ detail: `${filenames.length} packaged command assets present under ${COMMAND_DIR_LABEL}`,
65
+ };
66
+ }
67
+ return {
68
+ found: false,
69
+ detail: `${missing.length} of ${filenames.length} packaged command assets missing or unreadable under ` +
70
+ `${COMMAND_DIR_LABEL} (${formatMissing(missing)}). Re-run start-tickets to provision them.`,
71
+ };
72
+ }
@@ -0,0 +1,157 @@
1
+ /**
2
+ * Shared Claude command-asset provisioning (BAPI-664).
3
+ *
4
+ * Materializes the FULL packaged command bundle (`COMMANDS` from
5
+ * `commands.generated.ts`, the single source of truth) into a worktree's
6
+ * `.claude/commands/` directory so every packaged slash command is runnable from
7
+ * a freshly created worktree — whether spawned by the interactive `start-tickets`
8
+ * orchestration or by the headless conductor executor.
9
+ *
10
+ * Semantics (locked ticket decisions):
11
+ * - FULL bundle: every entry in `COMMANDS` is provisioned.
12
+ * - FILL-ONLY: a command file that already exists is treated as a customer asset
13
+ * and left byte-for-byte unchanged — its contents are never read-compared or
14
+ * refreshed. Only a missing (`ENOENT`) file is created. There is no refresh
15
+ * flag.
16
+ * - Repository-common exclude: after materialization, `.claude/commands/` is
17
+ * added to the worktree's Git `info/exclude` (resolved via Git for linked
18
+ * worktrees). `info/exclude` lives in the common Git directory shared by
19
+ * sibling worktrees, and an ignore rule never hides an already-tracked file,
20
+ * so this common scope is safe.
21
+ *
22
+ * All filesystem + command access is dependency-injected so this is unit-testable
23
+ * with no real I/O. This module NEVER imports `node:fs` / `child_process`, writes
24
+ * to stdout/stderr, or registers an MCP tool — failures are surfaced structurally
25
+ * and rendered by the caller's existing orchestration boundary.
26
+ */
27
+ import path from "path";
28
+ import { COMMANDS } from "./commands.generated.js";
29
+ import { ensureGitInfoExcluded } from "./git-ignore-utils.js";
30
+ /** The exact exclude entry appended for the command directory (POSIX-relative). */
31
+ const COMMAND_DIR_EXCLUDE_ENTRY = ".claude/commands/";
32
+ /** Bounded, secret-free error surfaced when the packaged bundle is empty. */
33
+ const EMPTY_BUNDLE_ERROR = "Command provisioning failed: the packaged command bundle is empty — reinstall or rebuild the MCP server package.";
34
+ /**
35
+ * Resolve the path API for the target platform. Local (not imported from
36
+ * `start-tickets.ts`) to avoid a runtime import cycle.
37
+ */
38
+ export function pathApiForCommandProvisioningPlatform(platform) {
39
+ return platform === "win32" ? path.win32 : path.posix;
40
+ }
41
+ /** True only for a Node `ENOENT` (missing-file) error. */
42
+ function isEnoentError(err) {
43
+ return (typeof err === "object" &&
44
+ err !== null &&
45
+ err.code === "ENOENT");
46
+ }
47
+ /**
48
+ * Fill every MISSING packaged command asset into `<worktreeRoot>/.claude/commands/`
49
+ * without touching any existing customer file, then ensure `.claude/commands/` is
50
+ * added to the worktree's Git exclude. Returns a structured result; never throws
51
+ * for an expected filesystem/Git failure, and never leaks raw exception text,
52
+ * command output, or file contents.
53
+ */
54
+ export async function provisionCommandsForWorktree(worktreeRoot, deps) {
55
+ const api = pathApiForCommandProvisioningPlatform(deps.platform);
56
+ const normalizedRoot = api.isAbsolute(worktreeRoot)
57
+ ? api.normalize(worktreeRoot)
58
+ : api.resolve(deps.cwd, worktreeRoot);
59
+ const commandsDir = api.join(normalizedRoot, ".claude", "commands");
60
+ const entries = Object.entries(COMMANDS);
61
+ if (entries.length === 0) {
62
+ // No runnable slash commands could ever be provisioned — fail loudly rather
63
+ // than reporting a hollow success.
64
+ return { ok: false, error: EMPTY_BUNDLE_ERROR };
65
+ }
66
+ // Phase 1 — discover which packaged files are absent. A successful read means
67
+ // the customer already owns that asset; leave it untouched. Only ENOENT counts
68
+ // as "missing"; any other read failure is a bounded, secret-free error.
69
+ let fillError = null;
70
+ const missing = [];
71
+ for (const [filename, content] of entries) {
72
+ const target = api.join(commandsDir, filename);
73
+ try {
74
+ await deps.readFile(target);
75
+ }
76
+ catch (err) {
77
+ if (isEnoentError(err)) {
78
+ missing.push([filename, content]);
79
+ }
80
+ else {
81
+ fillError = `Command provisioning failed: could not read existing command asset '${filename}'.`;
82
+ break;
83
+ }
84
+ }
85
+ }
86
+ // Phase 2 — create the directory (only when something is missing) and write the
87
+ // absent files in bundle order using the packaged string unchanged.
88
+ if (!fillError && missing.length > 0) {
89
+ try {
90
+ await deps.mkdir(commandsDir, { recursive: true });
91
+ for (const [filename, content] of missing) {
92
+ await deps.writeFile(api.join(commandsDir, filename), content);
93
+ }
94
+ }
95
+ catch {
96
+ fillError =
97
+ "Command provisioning failed: could not write one or more packaged command assets.";
98
+ }
99
+ }
100
+ // Phase 3 — ALWAYS attempt exclusion, even after a partial or failed fill, so
101
+ // partially bootstrapped files are never left visible to Git. The primary fill
102
+ // failure is preserved if both the fill and the exclusion fail.
103
+ let excludeError = null;
104
+ try {
105
+ await ensureGitInfoExcluded(normalizedRoot, COMMAND_DIR_EXCLUDE_ENTRY, {
106
+ readFile: deps.readFile,
107
+ writeFile: deps.writeFile,
108
+ mkdir: deps.mkdir,
109
+ runCommand: deps.runCommand,
110
+ platform: deps.platform,
111
+ });
112
+ }
113
+ catch {
114
+ excludeError =
115
+ "Command provisioning failed: could not add '.claude/commands/' to the worktree Git exclude file.";
116
+ }
117
+ if (fillError)
118
+ return { ok: false, error: fillError };
119
+ if (excludeError)
120
+ return { ok: false, error: excludeError };
121
+ return { ok: true };
122
+ }
123
+ /**
124
+ * Provision command assets for every eligible (`created`, path-bearing) row, in
125
+ * input order, serially. A non-`created` row or a `created` row without a usable
126
+ * path is returned unchanged. A per-worktree bootstrap failure marks ONLY that
127
+ * row `spawn-failed` (with a secret-free `Command provisioning failed: …` error)
128
+ * so the affected worker is skipped by all later spawn logic while its siblings
129
+ * proceed. A defensive per-row catch guarantees one unexpected failure cannot
130
+ * abort later rows or reject the overall call.
131
+ */
132
+ export async function provisionCommandsForCreatedWorktrees(rows, deps) {
133
+ const out = [];
134
+ for (const row of rows) {
135
+ if (row.status !== "created" || !row.path) {
136
+ out.push(row);
137
+ continue;
138
+ }
139
+ try {
140
+ const result = await provisionCommandsForWorktree(row.path, deps);
141
+ if (result.ok) {
142
+ out.push(row);
143
+ }
144
+ else {
145
+ out.push({ ...row, status: "spawn-failed", error: result.error });
146
+ }
147
+ }
148
+ catch {
149
+ out.push({
150
+ ...row,
151
+ status: "spawn-failed",
152
+ error: "Command provisioning failed: an unexpected error occurred while bootstrapping worktree command assets.",
153
+ });
154
+ }
155
+ }
156
+ return out;
157
+ }