@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 +53 -21
- package/build/command-assets-doctor.js +72 -0
- package/build/command-provisioning.js +157 -0
- package/build/credential-store.js +302 -18
- package/build/doctor.js +80 -35
- package/build/executor/test-clock.js +3 -1
- package/build/executor/worktree.js +31 -2
- package/build/git-ignore-utils.js +67 -14
- package/build/index.js +2874 -2873
- package/build/install-bridge.js +1513 -453
- package/build/install-doctor.js +37 -4
- package/build/readme.generated.js +1 -1
- package/build/start-tickets-prereqs.js +45 -0
- package/build/start-tickets.js +44 -3
- package/build/version.generated.js +1 -1
- package/package.json +4 -3
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
|
-
**`
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
- **
|
|
43
|
-
Bridge
|
|
44
|
-
|
|
45
|
-
|
|
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**
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
|
152
|
-
|
|
153
|
-
|
|
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
|
|
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
|
+
}
|