@bridge_gpt/mcp-server 0.2.67 → 0.2.69
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 +272 -51
- package/build/agent-launchers/claude-executor-adapter.js +27 -0
- package/build/agents.generated.js +22 -2
- package/build/bitbucket-workspace-remote.js +136 -0
- package/build/bootstrap-project-identity.js +24 -0
- package/build/bridge-permissions.js +1362 -0
- package/build/check-update-cli.js +74 -0
- package/build/claude-settings.js +102 -2
- package/build/cli-help.js +27 -0
- package/build/codex-home.js +9 -1
- package/build/codex-launcher-pin.js +598 -0
- package/build/command-assets-doctor.js +24 -70
- package/build/command-provisioning.js +29 -5
- package/build/commands.generated.js +9 -9
- package/build/conductor/bridge-api-client.js +714 -0
- package/build/conductor/command-drift-production.js +92 -0
- package/build/conductor/command-drift.js +89 -0
- package/build/conductor/doctor.js +69 -26
- package/build/conductor/epic-branch-ci-inspection.js +1375 -0
- package/build/conductor/epic-implementer-cli.js +76 -22
- package/build/conductor/gate-discovery.js +890 -0
- package/build/conductor/install-doctor.js +15 -3
- package/build/conductor/json-output.js +63 -0
- package/build/conductor/readiness-cli.js +95 -10
- package/build/conductor/readiness-sections.js +369 -3
- package/build/conductor/readiness.js +211 -8
- package/build/conductor/recovery-cli.js +110 -4
- package/build/conductor/recovery-operations.js +99 -1
- package/build/connect-bitbucket-api.js +41 -6
- package/build/connect-bitbucket.js +106 -33
- package/build/connect-github-api.js +33 -14
- package/build/credential-store.js +207 -47
- package/build/docs.generated.js +57 -3
- package/build/doctor.js +198 -4
- package/build/drive-epic.js +45 -8
- package/build/epic-coherence-input.test-fixtures.js +4 -3
- package/build/epic-implementer-bin.js +1369 -25
- package/build/epic-workarounds-path.js +39 -0
- package/build/executor/cli.js +30 -0
- package/build/executor/completion-contract.js +20 -5
- package/build/executor/deps.js +4 -1
- package/build/executor/gate-qualification-job.js +499 -0
- package/build/executor/guarded-fast-forward-push.js +129 -0
- package/build/executor/http-client.js +43 -4
- package/build/executor/integration-job.js +122 -8
- package/build/executor/job-errors.js +8 -1
- package/build/executor/job-runner.js +589 -47
- package/build/executor/likely-tests.js +290 -0
- package/build/executor/live-worker-registry.js +1 -1
- package/build/executor/observation.js +31 -2
- package/build/executor/packaged-recipe.js +106 -0
- package/build/executor/permissions.js +115 -28
- package/build/executor/platform.js +1 -1
- package/build/executor/preflight.js +61 -12
- package/build/executor/process-containment.js +36 -12
- package/build/executor/process.js +15 -7
- package/build/executor/prompt-spec.js +33 -1
- package/build/executor/recently-failed-tests-file.js +71 -0
- package/build/executor/reconcile-overlap-job.js +41 -23
- package/build/executor/required-verification.js +1 -1
- package/build/executor/required-work-ledger.js +105 -17
- package/build/executor/retirement.js +69 -0
- package/build/executor/runner.js +114 -11
- package/build/executor/test-clock.js +15 -0
- package/build/executor/types.js +32 -0
- package/build/executor/viewer-tabs.js +58 -8
- package/build/executor/worker-command.js +23 -7
- package/build/executor/worker-diagnostics.js +37 -0
- package/build/executor/worker-finalization.js +31 -5
- package/build/executor/worker-guard-hook.js +179 -25
- package/build/executor/worker-log.js +4 -3
- package/build/executor/worker-verification-guidance.js +37 -0
- package/build/executor/worktree.js +88 -11
- package/build/first-build-advisory.js +97 -0
- package/build/generated-assets-advisory.js +14 -0
- package/build/generated-assets-catalog.js +66 -0
- package/build/generated-assets-context.js +46 -0
- package/build/generated-assets-dependencies.js +26 -0
- package/build/generated-assets-doctor.js +50 -0
- package/build/generated-assets-health.js +65 -0
- package/build/generated-assets-host.js +17 -0
- package/build/generated-assets-inventory.js +17 -0
- package/build/generated-assets-paths.js +60 -0
- package/build/generated-assets-publisher.js +133 -0
- package/build/generated-assets-remedy.js +25 -0
- package/build/generated-assets-repair.js +72 -0
- package/build/generated-assets-session.js +71 -0
- package/build/generated-assets-test-support.js +35 -0
- package/build/index.js +1109 -695
- package/build/init.js +54 -177
- package/build/install-bridge-conductor-gate.js +1372 -0
- package/build/install-bridge-conductor.js +289 -36
- package/build/install-bridge.js +722 -252
- package/build/launcher-config-inspection.js +184 -19
- package/build/mcp-host-targets.js +4 -0
- package/build/mcp-identity.js +7 -0
- package/build/mcp-install-state.js +50 -4
- package/build/merge-workflow-install.js +3 -4
- package/build/pipeline-utils.js +6 -2
- package/build/pipelines.generated.js +22 -142
- package/build/plan-execution-ledger.js +2 -32
- package/build/plane/manifest.js +16 -1
- package/build/plane/preflight.js +54 -1
- package/build/printed-remedy-commands.js +16 -1
- package/build/readme.generated.js +1 -1
- package/build/repair-assets.js +93 -0
- package/build/run-unit-tests-launcher.js +130 -49
- package/build/secret-redaction.js +33 -0
- package/build/session-upgrade-notice.js +86 -0
- package/build/setup-epic.js +108 -3
- package/build/start-tickets-prereqs.js +70 -16
- package/build/start-tickets.js +44 -5
- package/build/tool-call-advisory.js +162 -0
- package/build/tool-surface-gating.js +32 -57
- package/build/update-check.js +97 -16
- package/build/update-status.js +61 -71
- package/build/upgrade-advice.js +15 -11
- package/build/upgrade-cli.js +398 -17
- package/build/version.generated.js +3 -3
- package/build/worker-guard-hook-bin.js +3 -2
- package/build/worker-guard-hook-main.js +1 -1
- package/build/worktree-core.js +15 -2
- package/docs/EPIC-IMPLEMENTER-INSTALL.md +30 -12
- package/docs/EPIC-IMPLEMENTER.md +68 -2
- package/package.json +3 -3
- package/pipelines/greenfield-setup.json +4 -128
- package/pipelines/implement-ticket.json +3 -2
|
@@ -0,0 +1,598 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure, conservative reader and surgical splicer for the Bridge launcher in
|
|
3
|
+
* Codex's global `config.toml` (BAPI-1445).
|
|
4
|
+
*
|
|
5
|
+
* Codex keeps its MCP registrations in one per-user TOML file that also holds
|
|
6
|
+
* unrelated settings, comments, and a Bridge `.env` table full of credentials.
|
|
7
|
+
* `upgrade` must move the package pin inside that file without ever
|
|
8
|
+
* re-serializing it. So this module does not parse TOML into a document model.
|
|
9
|
+
* It runs a small lexical scanner that finds exactly one supported
|
|
10
|
+
* `[mcp_servers.<name>]` base table, where `<name>` is any recognized key,
|
|
11
|
+
* reads its `args` array, and reports the character offsets of the one package
|
|
12
|
+
* specifier worth changing. {@link applyCodexRepin} then splices only those
|
|
13
|
+
* characters; every other byte of the file is carried through unchanged.
|
|
14
|
+
*
|
|
15
|
+
* The contract is "refuse when unsure". Any structure the scanner cannot read
|
|
16
|
+
* with confidence — inline tables, dotted keys, repeated tables, escapes inside
|
|
17
|
+
* the package token, multiline strings in the Bridge table, malformed arrays —
|
|
18
|
+
* comes back as `unlocatable`, never as a guess.
|
|
19
|
+
*
|
|
20
|
+
* Purity and secret safety:
|
|
21
|
+
* - No file I/O, network, process spawning, or logging. Imports are limited to
|
|
22
|
+
* identity-level constants and types from `mcp-identity.ts`; this module must
|
|
23
|
+
* never import `launcher-config-inspection.ts` or `mcp-host-config.ts`.
|
|
24
|
+
* - A result carries only the registration name, the parsed launcher
|
|
25
|
+
* arguments, and splice coordinates. It never carries the file text, the
|
|
26
|
+
* command, any environment value, a parse excerpt, or a raw error. The
|
|
27
|
+
* `.env` table is scanned only to keep lexical state and table boundaries.
|
|
28
|
+
* - Offsets are half-open JavaScript string offsets in UTF-16 code units, not
|
|
29
|
+
* UTF-8 byte positions. Callers must read and write the file as UTF-8 text so
|
|
30
|
+
* string offsets and file content stay in step.
|
|
31
|
+
*/
|
|
32
|
+
import { MCP_PACKAGE_NAME, RECOGNIZED_SERVER_NAMES, WORKTREE_SHIM_TOKEN, } from "./mcp-identity.js";
|
|
33
|
+
/** The only error {@link applyCodexRepin} throws. Carries no source text. */
|
|
34
|
+
export const CODEX_SPLICE_ERROR_MESSAGE = "the Codex launcher splice no longer matches the inspected configuration";
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
// Lexical scanner
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
/** Internal: the scanner met a construct it will not interpret. Never escapes. */
|
|
39
|
+
class ScanFailure extends Error {
|
|
40
|
+
}
|
|
41
|
+
const BARE_KEY_CHAR = /[A-Za-z0-9_-]/;
|
|
42
|
+
/** Scan the whole document into headers, key/values, and multiline-string starts. */
|
|
43
|
+
function scanDocument(content) {
|
|
44
|
+
const items = [];
|
|
45
|
+
const multilineStarts = [];
|
|
46
|
+
const n = content.length;
|
|
47
|
+
const skipHorizontal = (from) => {
|
|
48
|
+
let i = from;
|
|
49
|
+
while (i < n && (content[i] === " " || content[i] === "\t"))
|
|
50
|
+
i++;
|
|
51
|
+
return i;
|
|
52
|
+
};
|
|
53
|
+
/** Skip whitespace, newlines, and comments. */
|
|
54
|
+
const skipBlank = (from) => {
|
|
55
|
+
let i = from;
|
|
56
|
+
while (i < n) {
|
|
57
|
+
const c = content[i];
|
|
58
|
+
if (c === " " || c === "\t" || c === "\r" || c === "\n") {
|
|
59
|
+
i++;
|
|
60
|
+
}
|
|
61
|
+
else if (c === "#") {
|
|
62
|
+
while (i < n && content[i] !== "\n")
|
|
63
|
+
i++;
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return i;
|
|
70
|
+
};
|
|
71
|
+
const readString = (from) => {
|
|
72
|
+
const quote = content[from];
|
|
73
|
+
const triple = content.startsWith(quote.repeat(3), from);
|
|
74
|
+
const kind = quote === '"' ? "basic" : "literal";
|
|
75
|
+
if (triple) {
|
|
76
|
+
multilineStarts.push(from);
|
|
77
|
+
let j = from + 3;
|
|
78
|
+
while (j < n) {
|
|
79
|
+
if (kind === "basic" && content[j] === "\\") {
|
|
80
|
+
j += 2;
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
if (content.startsWith(quote.repeat(3), j)) {
|
|
84
|
+
let end = j + 3;
|
|
85
|
+
let extra = 0;
|
|
86
|
+
while (extra < 2 && content[end] === quote) {
|
|
87
|
+
end++;
|
|
88
|
+
extra++;
|
|
89
|
+
}
|
|
90
|
+
return {
|
|
91
|
+
start: from,
|
|
92
|
+
end,
|
|
93
|
+
innerStart: from + 3,
|
|
94
|
+
innerEnd: end - 3,
|
|
95
|
+
kind,
|
|
96
|
+
multiline: true,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
j++;
|
|
100
|
+
}
|
|
101
|
+
throw new ScanFailure();
|
|
102
|
+
}
|
|
103
|
+
let j = from + 1;
|
|
104
|
+
while (j < n) {
|
|
105
|
+
const c = content[j];
|
|
106
|
+
if (c === "\n" || c === "\r")
|
|
107
|
+
throw new ScanFailure();
|
|
108
|
+
if (kind === "basic" && c === "\\") {
|
|
109
|
+
j += 2;
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
if (c === quote) {
|
|
113
|
+
return {
|
|
114
|
+
start: from,
|
|
115
|
+
end: j + 1,
|
|
116
|
+
innerStart: from + 1,
|
|
117
|
+
innerEnd: j,
|
|
118
|
+
kind,
|
|
119
|
+
multiline: false,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
j++;
|
|
123
|
+
}
|
|
124
|
+
throw new ScanFailure();
|
|
125
|
+
};
|
|
126
|
+
const parseKeyPath = (from) => {
|
|
127
|
+
const segments = [];
|
|
128
|
+
let i = from;
|
|
129
|
+
for (;;) {
|
|
130
|
+
i = skipHorizontal(i);
|
|
131
|
+
const c = content[i];
|
|
132
|
+
if (c === '"' || c === "'") {
|
|
133
|
+
const token = readString(i);
|
|
134
|
+
if (token.multiline)
|
|
135
|
+
throw new ScanFailure();
|
|
136
|
+
const raw = content.slice(token.innerStart, token.innerEnd);
|
|
137
|
+
segments.push(token.kind === "basic" ? decodeBasicString(raw) : raw);
|
|
138
|
+
i = token.end;
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
let j = i;
|
|
142
|
+
while (j < n && BARE_KEY_CHAR.test(content[j]))
|
|
143
|
+
j++;
|
|
144
|
+
if (j === i)
|
|
145
|
+
throw new ScanFailure();
|
|
146
|
+
segments.push(content.slice(i, j));
|
|
147
|
+
i = j;
|
|
148
|
+
}
|
|
149
|
+
i = skipHorizontal(i);
|
|
150
|
+
if (content[i] === ".") {
|
|
151
|
+
i++;
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
return { segments, next: i };
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
/** Skip one value of any TOML type, returning the offset just past it. */
|
|
158
|
+
const skipValue = (from) => {
|
|
159
|
+
const c = content[from];
|
|
160
|
+
if (c === '"' || c === "'")
|
|
161
|
+
return readString(from).end;
|
|
162
|
+
if (c === "[") {
|
|
163
|
+
let i = from + 1;
|
|
164
|
+
for (;;) {
|
|
165
|
+
i = skipBlank(i);
|
|
166
|
+
if (i >= n)
|
|
167
|
+
throw new ScanFailure();
|
|
168
|
+
if (content[i] === "]")
|
|
169
|
+
return i + 1;
|
|
170
|
+
i = skipBlank(skipValue(i));
|
|
171
|
+
if (content[i] === ",") {
|
|
172
|
+
i++;
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
if (content[i] === "]")
|
|
176
|
+
return i + 1;
|
|
177
|
+
throw new ScanFailure();
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
if (c === "{") {
|
|
181
|
+
let i = from + 1;
|
|
182
|
+
for (;;) {
|
|
183
|
+
i = skipBlank(i);
|
|
184
|
+
if (i >= n)
|
|
185
|
+
throw new ScanFailure();
|
|
186
|
+
if (content[i] === "}")
|
|
187
|
+
return i + 1;
|
|
188
|
+
const { next } = parseKeyPath(i);
|
|
189
|
+
i = skipHorizontal(next);
|
|
190
|
+
if (content[i] !== "=")
|
|
191
|
+
throw new ScanFailure();
|
|
192
|
+
i = skipBlank(i + 1);
|
|
193
|
+
i = skipBlank(skipValue(i));
|
|
194
|
+
if (content[i] === ",") {
|
|
195
|
+
i++;
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
if (content[i] === "}")
|
|
199
|
+
return i + 1;
|
|
200
|
+
throw new ScanFailure();
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
// A bare scalar: number, boolean, or date-time, ending at a delimiter.
|
|
204
|
+
let j = from;
|
|
205
|
+
while (j < n && !/[\s,\]}#]/.test(content[j]))
|
|
206
|
+
j++;
|
|
207
|
+
if (j === from)
|
|
208
|
+
throw new ScanFailure();
|
|
209
|
+
return j;
|
|
210
|
+
};
|
|
211
|
+
/** After a statement, only horizontal space and a comment may precede the newline. */
|
|
212
|
+
const expectLineEnd = (from) => {
|
|
213
|
+
let i = skipHorizontal(from);
|
|
214
|
+
if (content[i] === "#") {
|
|
215
|
+
while (i < n && content[i] !== "\n")
|
|
216
|
+
i++;
|
|
217
|
+
}
|
|
218
|
+
if (i < n && content[i] === "\r")
|
|
219
|
+
i++;
|
|
220
|
+
if (i < n && content[i] !== "\n")
|
|
221
|
+
throw new ScanFailure();
|
|
222
|
+
return i;
|
|
223
|
+
};
|
|
224
|
+
let i = 0;
|
|
225
|
+
while (i < n) {
|
|
226
|
+
i = skipBlank(i);
|
|
227
|
+
if (i >= n)
|
|
228
|
+
break;
|
|
229
|
+
const start = i;
|
|
230
|
+
if (content[i] === "[") {
|
|
231
|
+
const isArrayTable = content[i + 1] === "[";
|
|
232
|
+
const { segments, next } = parseKeyPath(i + (isArrayTable ? 2 : 1));
|
|
233
|
+
let j = skipHorizontal(next);
|
|
234
|
+
const closer = isArrayTable ? "]]" : "]";
|
|
235
|
+
if (!content.startsWith(closer, j))
|
|
236
|
+
throw new ScanFailure();
|
|
237
|
+
j += closer.length;
|
|
238
|
+
items.push({ type: "header", start, end: j, segments, isArrayTable });
|
|
239
|
+
i = expectLineEnd(j);
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
const { segments, next } = parseKeyPath(i);
|
|
243
|
+
let j = skipHorizontal(next);
|
|
244
|
+
if (content[j] !== "=")
|
|
245
|
+
throw new ScanFailure();
|
|
246
|
+
j = skipHorizontal(j + 1);
|
|
247
|
+
const valueStart = j;
|
|
248
|
+
const valueEnd = skipValue(valueStart);
|
|
249
|
+
items.push({ type: "kv", start, keySegments: segments, valueStart, valueEnd });
|
|
250
|
+
i = expectLineEnd(valueEnd);
|
|
251
|
+
}
|
|
252
|
+
return { items, multilineStarts };
|
|
253
|
+
}
|
|
254
|
+
/** Decode a TOML basic-string body. Throws {@link ScanFailure} on a bad escape. */
|
|
255
|
+
function decodeBasicString(raw) {
|
|
256
|
+
let out = "";
|
|
257
|
+
for (let i = 0; i < raw.length; i++) {
|
|
258
|
+
const c = raw[i];
|
|
259
|
+
if (c !== "\\") {
|
|
260
|
+
out += c;
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
const e = raw[++i];
|
|
264
|
+
switch (e) {
|
|
265
|
+
case "b":
|
|
266
|
+
out += "\b";
|
|
267
|
+
break;
|
|
268
|
+
case "t":
|
|
269
|
+
out += "\t";
|
|
270
|
+
break;
|
|
271
|
+
case "n":
|
|
272
|
+
out += "\n";
|
|
273
|
+
break;
|
|
274
|
+
case "f":
|
|
275
|
+
out += "\f";
|
|
276
|
+
break;
|
|
277
|
+
case "r":
|
|
278
|
+
out += "\r";
|
|
279
|
+
break;
|
|
280
|
+
case '"':
|
|
281
|
+
out += '"';
|
|
282
|
+
break;
|
|
283
|
+
case "\\":
|
|
284
|
+
out += "\\";
|
|
285
|
+
break;
|
|
286
|
+
case "u":
|
|
287
|
+
case "U": {
|
|
288
|
+
const width = e === "u" ? 4 : 8;
|
|
289
|
+
const hex = raw.slice(i + 1, i + 1 + width);
|
|
290
|
+
if (hex.length !== width || !/^[0-9A-Fa-f]+$/.test(hex))
|
|
291
|
+
throw new ScanFailure();
|
|
292
|
+
try {
|
|
293
|
+
out += String.fromCodePoint(parseInt(hex, 16));
|
|
294
|
+
}
|
|
295
|
+
catch {
|
|
296
|
+
throw new ScanFailure();
|
|
297
|
+
}
|
|
298
|
+
i += width;
|
|
299
|
+
break;
|
|
300
|
+
}
|
|
301
|
+
default:
|
|
302
|
+
throw new ScanFailure();
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
return out;
|
|
306
|
+
}
|
|
307
|
+
/** Read a single- or multi-line array of single-line strings, strictly. */
|
|
308
|
+
function readArgsArray(content, valueStart, valueEnd) {
|
|
309
|
+
const n = content.length;
|
|
310
|
+
if (content[valueStart] !== "[")
|
|
311
|
+
throw new ScanFailure();
|
|
312
|
+
const elements = [];
|
|
313
|
+
let i = valueStart + 1;
|
|
314
|
+
const skipBlank = (from) => {
|
|
315
|
+
let j = from;
|
|
316
|
+
while (j < n) {
|
|
317
|
+
const c = content[j];
|
|
318
|
+
if (c === " " || c === "\t" || c === "\r" || c === "\n") {
|
|
319
|
+
j++;
|
|
320
|
+
}
|
|
321
|
+
else if (c === "#") {
|
|
322
|
+
while (j < n && content[j] !== "\n")
|
|
323
|
+
j++;
|
|
324
|
+
}
|
|
325
|
+
else {
|
|
326
|
+
break;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
return j;
|
|
330
|
+
};
|
|
331
|
+
for (;;) {
|
|
332
|
+
i = skipBlank(i);
|
|
333
|
+
if (i >= valueEnd)
|
|
334
|
+
throw new ScanFailure();
|
|
335
|
+
if (content[i] === "]") {
|
|
336
|
+
if (i + 1 !== valueEnd)
|
|
337
|
+
throw new ScanFailure();
|
|
338
|
+
return elements;
|
|
339
|
+
}
|
|
340
|
+
const quote = content[i];
|
|
341
|
+
if (quote !== '"' && quote !== "'")
|
|
342
|
+
throw new ScanFailure();
|
|
343
|
+
if (content.startsWith(quote.repeat(3), i))
|
|
344
|
+
throw new ScanFailure();
|
|
345
|
+
let j = i + 1;
|
|
346
|
+
let closed = false;
|
|
347
|
+
while (j < valueEnd) {
|
|
348
|
+
const c = content[j];
|
|
349
|
+
if (c === "\n" || c === "\r")
|
|
350
|
+
throw new ScanFailure();
|
|
351
|
+
if (quote === '"' && c === "\\") {
|
|
352
|
+
j += 2;
|
|
353
|
+
continue;
|
|
354
|
+
}
|
|
355
|
+
if (c === quote) {
|
|
356
|
+
closed = true;
|
|
357
|
+
break;
|
|
358
|
+
}
|
|
359
|
+
j++;
|
|
360
|
+
}
|
|
361
|
+
if (!closed)
|
|
362
|
+
throw new ScanFailure();
|
|
363
|
+
const raw = content.slice(i + 1, j);
|
|
364
|
+
elements.push({
|
|
365
|
+
value: quote === '"' ? decodeBasicString(raw) : raw,
|
|
366
|
+
innerStart: i + 1,
|
|
367
|
+
innerEnd: j,
|
|
368
|
+
hasBackslash: quote === '"' && raw.includes("\\"),
|
|
369
|
+
});
|
|
370
|
+
i = skipBlank(j + 1);
|
|
371
|
+
if (content[i] === ",") {
|
|
372
|
+
i++;
|
|
373
|
+
continue;
|
|
374
|
+
}
|
|
375
|
+
if (content[i] === "]") {
|
|
376
|
+
if (i + 1 !== valueEnd)
|
|
377
|
+
throw new ScanFailure();
|
|
378
|
+
return elements;
|
|
379
|
+
}
|
|
380
|
+
throw new ScanFailure();
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
// ---------------------------------------------------------------------------
|
|
384
|
+
// Inspection
|
|
385
|
+
// ---------------------------------------------------------------------------
|
|
386
|
+
const SERVERS_ROOT = "mcp_servers";
|
|
387
|
+
const NONE = { kind: "none" };
|
|
388
|
+
const UNLOCATABLE = { kind: "unlocatable" };
|
|
389
|
+
function recognizedName(name) {
|
|
390
|
+
return RECOGNIZED_SERVER_NAMES.includes(name ?? "")
|
|
391
|
+
? name
|
|
392
|
+
: null;
|
|
393
|
+
}
|
|
394
|
+
/** True when a package token names the Bridge package, bare or with an `@` suffix. */
|
|
395
|
+
function isBridgePackageToken(arg) {
|
|
396
|
+
return arg === MCP_PACKAGE_NAME || arg.startsWith(`${MCP_PACKAGE_NAME}@`);
|
|
397
|
+
}
|
|
398
|
+
/**
|
|
399
|
+
* Locate the one supported Bridge launcher in a Codex `config.toml`.
|
|
400
|
+
*
|
|
401
|
+
* Returns `none` only when the file holds no Bridge declaration of any kind.
|
|
402
|
+
* A file the scanner cannot read at all is `none` when it never mentions Bridge
|
|
403
|
+
* and `unlocatable` when it does — a broken file that might hide a registration
|
|
404
|
+
* is never reported as "nothing to do".
|
|
405
|
+
*/
|
|
406
|
+
export function inspectCodexLauncher(content) {
|
|
407
|
+
let doc;
|
|
408
|
+
try {
|
|
409
|
+
doc = scanDocument(content);
|
|
410
|
+
}
|
|
411
|
+
catch (err) {
|
|
412
|
+
if (!(err instanceof ScanFailure))
|
|
413
|
+
throw err;
|
|
414
|
+
return /bridge/i.test(content) ? UNLOCATABLE : NONE;
|
|
415
|
+
}
|
|
416
|
+
try {
|
|
417
|
+
return classifyDocument(content, doc);
|
|
418
|
+
}
|
|
419
|
+
catch (err) {
|
|
420
|
+
if (!(err instanceof ScanFailure))
|
|
421
|
+
throw err;
|
|
422
|
+
return UNLOCATABLE;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
function classifyDocument(content, doc) {
|
|
426
|
+
const { items, multilineStarts } = doc;
|
|
427
|
+
const baseTables = [];
|
|
428
|
+
const subtableNames = new Set();
|
|
429
|
+
let sawUnsupportedDeclaration = false;
|
|
430
|
+
let current = [];
|
|
431
|
+
for (let index = 0; index < items.length; index++) {
|
|
432
|
+
const item = items[index];
|
|
433
|
+
if (item.type === "header") {
|
|
434
|
+
current = item.segments;
|
|
435
|
+
if (item.segments[0] !== SERVERS_ROOT || item.segments.length < 2)
|
|
436
|
+
continue;
|
|
437
|
+
const name = recognizedName(item.segments[1]);
|
|
438
|
+
if (!name)
|
|
439
|
+
continue;
|
|
440
|
+
if (item.isArrayTable) {
|
|
441
|
+
sawUnsupportedDeclaration = true;
|
|
442
|
+
}
|
|
443
|
+
else if (item.segments.length === 2) {
|
|
444
|
+
baseTables.push({ name, headerIndex: index });
|
|
445
|
+
}
|
|
446
|
+
else {
|
|
447
|
+
subtableNames.add(name);
|
|
448
|
+
}
|
|
449
|
+
continue;
|
|
450
|
+
}
|
|
451
|
+
// A key/value. Its full path is the enclosing table path plus its own key.
|
|
452
|
+
const full = [...current, ...item.keySegments];
|
|
453
|
+
if (full[0] !== SERVERS_ROOT)
|
|
454
|
+
continue;
|
|
455
|
+
if (full.length === 1) {
|
|
456
|
+
// `mcp_servers = { ... }`: an inline servers table. Refuse it if it could
|
|
457
|
+
// be declaring Bridge.
|
|
458
|
+
if (/bridge/i.test(content.slice(item.valueStart, item.valueEnd))) {
|
|
459
|
+
sawUnsupportedDeclaration = true;
|
|
460
|
+
}
|
|
461
|
+
continue;
|
|
462
|
+
}
|
|
463
|
+
const name = recognizedName(full[1]);
|
|
464
|
+
if (!name)
|
|
465
|
+
continue;
|
|
466
|
+
const insideOwnTable = current[0] === SERVERS_ROOT && current[1] === full[1];
|
|
467
|
+
if (!insideOwnTable)
|
|
468
|
+
sawUnsupportedDeclaration = true;
|
|
469
|
+
}
|
|
470
|
+
const names = new Set(baseTables.map((t) => t.name));
|
|
471
|
+
for (const name of subtableNames) {
|
|
472
|
+
if (!names.has(name))
|
|
473
|
+
return UNLOCATABLE; // an orphan `.env` or other subtable
|
|
474
|
+
}
|
|
475
|
+
if (sawUnsupportedDeclaration)
|
|
476
|
+
return UNLOCATABLE;
|
|
477
|
+
if (names.size > 1)
|
|
478
|
+
return { kind: "duplicate" };
|
|
479
|
+
if (baseTables.length === 0)
|
|
480
|
+
return NONE;
|
|
481
|
+
if (baseTables.length > 1)
|
|
482
|
+
return UNLOCATABLE; // the same base table repeated
|
|
483
|
+
const [base] = baseTables;
|
|
484
|
+
const bodyStart = items[base.headerIndex].end;
|
|
485
|
+
const nextHeader = items.slice(base.headerIndex + 1).find((i) => i.type === "header");
|
|
486
|
+
const bodyEnd = nextHeader ? nextHeader.start : content.length;
|
|
487
|
+
if (multilineStarts.some((s) => s >= bodyStart && s < bodyEnd))
|
|
488
|
+
return UNLOCATABLE;
|
|
489
|
+
const argsItems = [];
|
|
490
|
+
for (let k = base.headerIndex + 1; k < items.length; k++) {
|
|
491
|
+
const item = items[k];
|
|
492
|
+
if (item.type === "header")
|
|
493
|
+
break;
|
|
494
|
+
if (item.keySegments[0] !== "args")
|
|
495
|
+
continue;
|
|
496
|
+
if (item.keySegments.length > 1)
|
|
497
|
+
return UNLOCATABLE;
|
|
498
|
+
argsItems.push(item);
|
|
499
|
+
}
|
|
500
|
+
if (argsItems.length !== 1)
|
|
501
|
+
return UNLOCATABLE;
|
|
502
|
+
const elements = readArgsArray(content, argsItems[0].valueStart, argsItems[0].valueEnd);
|
|
503
|
+
const args = elements.map((e) => e.value);
|
|
504
|
+
const tokenElements = elements.filter((e) => isBridgePackageToken(e.value));
|
|
505
|
+
if (tokenElements.some((e) => e.hasBackslash))
|
|
506
|
+
return UNLOCATABLE;
|
|
507
|
+
if (tokenElements.length > 1)
|
|
508
|
+
return UNLOCATABLE;
|
|
509
|
+
if (args.includes(WORKTREE_SHIM_TOKEN)) {
|
|
510
|
+
return { kind: "worktree-shim", registrationName: base.name, args };
|
|
511
|
+
}
|
|
512
|
+
if (tokenElements.length === 0)
|
|
513
|
+
return UNLOCATABLE;
|
|
514
|
+
const [token] = tokenElements;
|
|
515
|
+
return {
|
|
516
|
+
kind: "located",
|
|
517
|
+
registrationName: base.name,
|
|
518
|
+
args,
|
|
519
|
+
splice: spliceFor(token),
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
function spliceFor(token) {
|
|
523
|
+
const tokenText = token.value;
|
|
524
|
+
const common = {
|
|
525
|
+
tokenStart: token.innerStart,
|
|
526
|
+
tokenEnd: token.innerEnd,
|
|
527
|
+
tokenText,
|
|
528
|
+
};
|
|
529
|
+
if (tokenText === MCP_PACKAGE_NAME) {
|
|
530
|
+
return {
|
|
531
|
+
...common,
|
|
532
|
+
replaceStart: token.innerEnd,
|
|
533
|
+
replaceEnd: token.innerEnd,
|
|
534
|
+
replacedText: "",
|
|
535
|
+
needsAtSign: true,
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
const specStart = token.innerStart + MCP_PACKAGE_NAME.length + 1;
|
|
539
|
+
const rest = tokenText.slice(MCP_PACKAGE_NAME.length + 1);
|
|
540
|
+
const trimmed = rest.trim();
|
|
541
|
+
if (trimmed.length === 0) {
|
|
542
|
+
return {
|
|
543
|
+
...common,
|
|
544
|
+
replaceStart: specStart,
|
|
545
|
+
replaceEnd: specStart,
|
|
546
|
+
replacedText: "",
|
|
547
|
+
needsAtSign: false,
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
const lead = rest.length - rest.trimStart().length;
|
|
551
|
+
return {
|
|
552
|
+
...common,
|
|
553
|
+
replaceStart: specStart + lead,
|
|
554
|
+
replaceEnd: specStart + lead + trimmed.length,
|
|
555
|
+
replacedText: trimmed,
|
|
556
|
+
needsAtSign: false,
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
// ---------------------------------------------------------------------------
|
|
560
|
+
// Surgical splice
|
|
561
|
+
// ---------------------------------------------------------------------------
|
|
562
|
+
/** A target version safe to write inside a TOML string without escaping. */
|
|
563
|
+
const SAFE_VERSION = /^[0-9A-Za-z][0-9A-Za-z.+-]*$/;
|
|
564
|
+
/**
|
|
565
|
+
* Splice `version` into `content` at the coordinates `inspection` located.
|
|
566
|
+
*
|
|
567
|
+
* Everything outside the replaced range — comments, CRLF, table names, the whole
|
|
568
|
+
* `.env` table, non-ASCII text — is copied through unchanged. Throws the one
|
|
569
|
+
* fixed {@link CODEX_SPLICE_ERROR_MESSAGE} when the inspection is not a located
|
|
570
|
+
* launcher, the coordinates fall outside `content`, or the text at them no longer
|
|
571
|
+
* matches what was inspected. The error never contains source text.
|
|
572
|
+
*/
|
|
573
|
+
export function applyCodexRepin(content, inspection, version) {
|
|
574
|
+
if (inspection.kind !== "located" || !SAFE_VERSION.test(version)) {
|
|
575
|
+
throw new Error(CODEX_SPLICE_ERROR_MESSAGE);
|
|
576
|
+
}
|
|
577
|
+
const s = inspection.splice;
|
|
578
|
+
const offsets = [s.tokenStart, s.tokenEnd, s.replaceStart, s.replaceEnd];
|
|
579
|
+
const inRange = offsets.every((o) => Number.isInteger(o) && o >= 0 && o <= content.length) &&
|
|
580
|
+
s.tokenStart <= s.replaceStart &&
|
|
581
|
+
s.replaceStart <= s.replaceEnd &&
|
|
582
|
+
s.replaceEnd <= s.tokenEnd;
|
|
583
|
+
if (!inRange)
|
|
584
|
+
throw new Error(CODEX_SPLICE_ERROR_MESSAGE);
|
|
585
|
+
if (content.slice(s.tokenStart, s.tokenEnd) !== s.tokenText ||
|
|
586
|
+
content.slice(s.replaceStart, s.replaceEnd) !== s.replacedText) {
|
|
587
|
+
throw new Error(CODEX_SPLICE_ERROR_MESSAGE);
|
|
588
|
+
}
|
|
589
|
+
const shapeOk = s.needsAtSign
|
|
590
|
+
? s.tokenText === MCP_PACKAGE_NAME && s.replaceStart === s.tokenEnd && s.replaceEnd === s.tokenEnd
|
|
591
|
+
: s.tokenText.startsWith(`${MCP_PACKAGE_NAME}@`);
|
|
592
|
+
if (!shapeOk)
|
|
593
|
+
throw new Error(CODEX_SPLICE_ERROR_MESSAGE);
|
|
594
|
+
return (content.slice(0, s.replaceStart) +
|
|
595
|
+
(s.needsAtSign ? "@" : "") +
|
|
596
|
+
version +
|
|
597
|
+
content.slice(s.replaceEnd));
|
|
598
|
+
}
|
|
@@ -1,72 +1,26 @@
|
|
|
1
|
-
/**
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
};
|
|
1
|
+
/** Metadata-only compatibility probe for worktree command assets. */
|
|
2
|
+
import { buildPackagedAssetCatalog } from "./generated-assets-catalog.js";
|
|
3
|
+
import { inspectPackagedDependency } from "./generated-assets-health.js";
|
|
4
|
+
import { packagedAssetRemedy } from "./generated-assets-remedy.js";
|
|
5
|
+
export async function probeWorktreeCommandAssets(root, deps) {
|
|
6
|
+
if (!deps.metadata)
|
|
7
|
+
return { found: false, detail: "Command-asset inspection indeterminate (metadata access unavailable)." };
|
|
8
|
+
const catalog = buildPackagedAssetCatalog();
|
|
9
|
+
const assets = catalog.assets.filter(a => a.kind === "command" && a.hosts.includes("claude"));
|
|
10
|
+
if (!assets.length || catalog.packageProblems.length)
|
|
11
|
+
return { found: false, detail: "Packaged command bundle is empty or unavailable — rebuild or reinstall the package." };
|
|
12
|
+
const findings = [];
|
|
13
|
+
const deadline = Date.now() + 1000;
|
|
14
|
+
for (const asset of assets) {
|
|
15
|
+
if (Date.now() >= deadline)
|
|
16
|
+
return { found: false, detail: "Command-asset inspection indeterminate (time budget)." };
|
|
17
|
+
findings.push(await inspectPackagedDependency(root, asset, { ...deps.metadata, platform: deps.platform }, deadline - Date.now()));
|
|
66
18
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
detail: `${
|
|
70
|
-
|
|
71
|
-
|
|
19
|
+
const unusable = findings.filter(f => f.status !== "present");
|
|
20
|
+
if (!unusable.length)
|
|
21
|
+
return { found: true, detail: `${assets.length} packaged command assets present under .claude/commands/` };
|
|
22
|
+
const shown = unusable.slice(0, 5).map(f => `${f.destination.split("/").at(-1)}: ${f.status} (${f.reason})`).join(", ");
|
|
23
|
+
const missing = unusable.filter(f => f.status === "missing").length;
|
|
24
|
+
const guidance = missing ? packagedAssetRemedy(root, "claude", undefined, deps.platform).command : "Resolve path/permission problems; use explicit --project-root and --host for provisioning.";
|
|
25
|
+
return { found: false, detail: `${unusable.length} of ${assets.length} packaged command assets unusable under .claude/commands/ (${shown}${unusable.length > 5 ? `, +${unusable.length - 5} more` : ""}). ${guidance}` };
|
|
72
26
|
}
|