@loucompanion/forge-bridge 0.1.1-dev.7d43998c4521 → 0.1.1-dev.9acab970e5d1
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 +1 -0
- package/dist/bridge/bridge.service.js +83 -11
- package/dist/bridge/bridge.types.d.ts +16 -0
- package/dist/cli/cli.service.d.ts +27 -2
- package/dist/cli/cli.service.js +406 -16
- package/dist/cli/local-config.d.ts +4 -1
- package/dist/cli/local-config.js +60 -8
- package/dist/session/index.d.ts +2 -0
- package/dist/session/index.js +1 -0
- package/dist/session/internals.d.ts +5 -3
- package/dist/session/internals.js +2 -1
- package/dist/session/phase-completion.d.ts +111 -0
- package/dist/session/phase-completion.js +2350 -0
- package/dist/session/provider-cli/index.d.ts +2 -2
- package/dist/session/provider-cli/index.js +1 -1
- package/dist/session/provider-cli/provider-cli.base.d.ts +2 -2
- package/dist/session/provider-cli/provider-cli.service.d.ts +48 -3
- package/dist/session/provider-cli/provider-cli.service.js +1885 -45
- package/dist/session/provider-cli/provider-cli.types.d.ts +2 -0
- package/dist/session/provider-cli/provider-usage.d.ts +25 -0
- package/dist/session/provider-cli/provider-usage.js +64 -0
- package/dist/session/qa-sidecar-validation.d.ts +3 -0
- package/dist/session/qa-sidecar-validation.js +44 -0
- package/dist/session/session.service.js +57 -12
- package/dist/session/session.types.d.ts +66 -0
- package/dist/session/session.types.js +165 -2
- package/dist/shared/cursor-agent/cursor-agent.launch.d.ts +9 -0
- package/dist/shared/cursor-agent/cursor-agent.launch.js +66 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/worktree/artifact-injector.d.ts +12 -0
- package/dist/worktree/artifact-injector.js +133 -0
- package/dist/worktree/dependency-warmer.d.ts +32 -0
- package/dist/worktree/dependency-warmer.js +297 -0
- package/dist/worktree/index.d.ts +2 -0
- package/dist/worktree/index.js +1 -0
- package/dist/worktree/worktree.service.d.ts +4 -0
- package/dist/worktree/worktree.service.js +174 -11
- package/dist/worktree/worktree.types.d.ts +7 -1
- package/package.json +1 -1
package/dist/cli/cli.service.js
CHANGED
|
@@ -1,20 +1,26 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
2
|
import { execFile } from "node:child_process";
|
|
3
|
+
import os from "node:os";
|
|
3
4
|
import path from "node:path";
|
|
5
|
+
import { createInterface } from "node:readline/promises";
|
|
4
6
|
import { promisify } from "node:util";
|
|
7
|
+
import { resolveCursorAgentDirectLaunch } from "../shared/cursor-agent/cursor-agent.launch.js";
|
|
5
8
|
import { redactSecrets, redactText } from "../shared/redaction/redaction.js";
|
|
6
9
|
import { defaultBridgeServiceConfig, ForgeBridgeService } from "../bridge/bridge.service.js";
|
|
7
|
-
import { addRepoMapping, applyLocalConfigDefaults, readLocalConfigSync, removeRepoMapping, resolveLocalConfigPath } from "./local-config.js";
|
|
10
|
+
import { addRepoMapping, applyLocalConfigDefaults, providerStatusesFor, readLocalConfigSync, removeRepoMapping, resolveLocalConfigPath, writeLocalConfig } from "./local-config.js";
|
|
8
11
|
import { BridgeSessionService } from "../session/session.service.js";
|
|
12
|
+
import { parseCompletionMcpArgs, runCompletionMcpServer } from "../session/phase-completion.js";
|
|
9
13
|
import { failureResult, parseRunOnceDispatchPacket } from "../session/session.types.js";
|
|
10
14
|
const apiKeySecretGrantEnv = "FORGE_API_KEY_SECRET_GRANT";
|
|
11
15
|
const execFileAsync = promisify(execFile);
|
|
12
16
|
export class ForgeBridgeCliService {
|
|
13
17
|
sessions;
|
|
14
18
|
bridgeFactory;
|
|
15
|
-
|
|
19
|
+
pairRunner;
|
|
20
|
+
constructor(sessions = new BridgeSessionService(), bridgeFactory = (config) => new ForgeBridgeService(config), pairRunner = claimBridgeRunner) {
|
|
16
21
|
this.sessions = sessions;
|
|
17
22
|
this.bridgeFactory = bridgeFactory;
|
|
23
|
+
this.pairRunner = pairRunner;
|
|
18
24
|
}
|
|
19
25
|
async run(argv, io = { stdout: process.stdout, stderr: process.stderr }) {
|
|
20
26
|
const args = parseArgs(argv);
|
|
@@ -23,10 +29,11 @@ export class ForgeBridgeCliService {
|
|
|
23
29
|
return 2;
|
|
24
30
|
}
|
|
25
31
|
if (args.command === "connect") {
|
|
32
|
+
const config = await withDetectedCliProviders(args.config);
|
|
26
33
|
const missing = [
|
|
27
|
-
["server",
|
|
28
|
-
["runner id",
|
|
29
|
-
["device token",
|
|
34
|
+
["server", config.serverUrl],
|
|
35
|
+
["runner id", config.runnerId],
|
|
36
|
+
["device token", config.deviceToken]
|
|
30
37
|
].find(([, value]) => !value);
|
|
31
38
|
if (missing) {
|
|
32
39
|
io.stderr.write(`forge-bridge connect requires ${missing[0]}.\n`);
|
|
@@ -37,7 +44,7 @@ export class ForgeBridgeCliService {
|
|
|
37
44
|
process.once("SIGINT", stop);
|
|
38
45
|
process.once("SIGTERM", stop);
|
|
39
46
|
try {
|
|
40
|
-
await this.bridgeFactory(
|
|
47
|
+
await this.bridgeFactory(config).run(controller.signal);
|
|
41
48
|
return 0;
|
|
42
49
|
}
|
|
43
50
|
finally {
|
|
@@ -58,6 +65,27 @@ export class ForgeBridgeCliService {
|
|
|
58
65
|
return 2;
|
|
59
66
|
}
|
|
60
67
|
}
|
|
68
|
+
if (args.command === "pair") {
|
|
69
|
+
try {
|
|
70
|
+
const current = readLocalConfigSync();
|
|
71
|
+
const code = await resolvePairingCode(args.code);
|
|
72
|
+
const repoRoot = await resolvePairRepoRoot(args.repoRoot, current.defaultRepoRoot);
|
|
73
|
+
await fs.mkdir(repoRoot, { recursive: true });
|
|
74
|
+
const pairArgs = await withDetectedPairCliProviders({ ...args, code, repoRoot });
|
|
75
|
+
const claim = await this.pairRunner(pairArgs);
|
|
76
|
+
const next = pairedConfig(current, pairArgs, claim);
|
|
77
|
+
await writeLocalConfig(next);
|
|
78
|
+
io.stdout.write(`Paired runner ${claim.runnerId}.\n`);
|
|
79
|
+
io.stdout.write(`Repo folder: ${repoRoot}\n`);
|
|
80
|
+
io.stdout.write(`Config: ${resolveLocalConfigPath()}\n`);
|
|
81
|
+
io.stdout.write("Start the bridge with: forge-bridge connect\n");
|
|
82
|
+
return 0;
|
|
83
|
+
}
|
|
84
|
+
catch (error) {
|
|
85
|
+
io.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
|
86
|
+
return 1;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
61
89
|
if (args.command === "remove-repo") {
|
|
62
90
|
try {
|
|
63
91
|
const result = await removeRepoMapping(args.label);
|
|
@@ -76,6 +104,15 @@ export class ForgeBridgeCliService {
|
|
|
76
104
|
if (args.command === "cleanup") {
|
|
77
105
|
return await cleanupRepos(args.config, args.mode, args.force, io);
|
|
78
106
|
}
|
|
107
|
+
if (args.command === "mcp") {
|
|
108
|
+
const parsed = parseCompletionMcpArgs(args.argv);
|
|
109
|
+
if (!parsed.ok) {
|
|
110
|
+
io.stderr.write(`${parsed.error}\n`);
|
|
111
|
+
return 2;
|
|
112
|
+
}
|
|
113
|
+
await runCompletionMcpServer(parsed.config);
|
|
114
|
+
return 0;
|
|
115
|
+
}
|
|
79
116
|
let redactionSecrets = readEnvSecretValuesForRedaction();
|
|
80
117
|
try {
|
|
81
118
|
const dispatchJson = await fs.readFile(args.dispatchPath, "utf8");
|
|
@@ -157,6 +194,9 @@ export function parseArgs(argv) {
|
|
|
157
194
|
if (argv[0] === "connect") {
|
|
158
195
|
return parseConnectArgs(argv);
|
|
159
196
|
}
|
|
197
|
+
if (argv[0] === "pair") {
|
|
198
|
+
return parsePairArgs(argv);
|
|
199
|
+
}
|
|
160
200
|
if (argv[0] === "add-repo") {
|
|
161
201
|
if (!argv[1] || !argv[2] || argv.length > 3) {
|
|
162
202
|
return { ok: false, error: "Usage: forge-bridge add-repo <label> <path>" };
|
|
@@ -185,10 +225,13 @@ export function parseArgs(argv) {
|
|
|
185
225
|
if (argv[0] === "cleanup") {
|
|
186
226
|
return parseCleanupArgs(argv);
|
|
187
227
|
}
|
|
228
|
+
if (argv[0] === "mcp") {
|
|
229
|
+
return { ok: true, command: "mcp", argv };
|
|
230
|
+
}
|
|
188
231
|
if (argv[0] !== "run-once") {
|
|
189
232
|
return {
|
|
190
233
|
ok: false,
|
|
191
|
-
error:
|
|
234
|
+
error: usage()
|
|
192
235
|
};
|
|
193
236
|
}
|
|
194
237
|
let dispatchPath = null;
|
|
@@ -223,7 +266,7 @@ function parseConnectArgs(argv) {
|
|
|
223
266
|
let heartbeatIntervalMs = null;
|
|
224
267
|
const cliProviders = [];
|
|
225
268
|
const projectLabels = [];
|
|
226
|
-
let cleanupWorktrees =
|
|
269
|
+
let cleanupWorktrees = null;
|
|
227
270
|
for (let index = 1; index < argv.length; index += 1) {
|
|
228
271
|
const arg = argv[index];
|
|
229
272
|
if (arg === "--server") {
|
|
@@ -253,6 +296,9 @@ function parseConnectArgs(argv) {
|
|
|
253
296
|
else if (arg === "--no-cleanup-worktrees") {
|
|
254
297
|
cleanupWorktrees = false;
|
|
255
298
|
}
|
|
299
|
+
else if (arg === "--cleanup-worktrees") {
|
|
300
|
+
cleanupWorktrees = true;
|
|
301
|
+
}
|
|
256
302
|
else {
|
|
257
303
|
return { ok: false, error: `Unknown argument: ${arg}` };
|
|
258
304
|
}
|
|
@@ -261,7 +307,7 @@ function parseConnectArgs(argv) {
|
|
|
261
307
|
if (serverUrl === null && !process.env.FORGE_URL?.trim() && !localConfig.serverUrl?.trim()) {
|
|
262
308
|
return { ok: false, error: "forge-bridge connect requires --server or FORGE_URL." };
|
|
263
309
|
}
|
|
264
|
-
const overrides = {
|
|
310
|
+
const overrides = {};
|
|
265
311
|
if (serverUrl !== null)
|
|
266
312
|
overrides.serverUrl = serverUrl;
|
|
267
313
|
if (runnerId !== null)
|
|
@@ -276,12 +322,79 @@ function parseConnectArgs(argv) {
|
|
|
276
322
|
overrides.projectLabels = projectLabels;
|
|
277
323
|
if (heartbeatIntervalMs !== null)
|
|
278
324
|
overrides.heartbeatIntervalMs = heartbeatIntervalMs;
|
|
325
|
+
if (cleanupWorktrees !== null)
|
|
326
|
+
overrides.cleanupWorktrees = cleanupWorktrees;
|
|
279
327
|
return {
|
|
280
328
|
ok: true,
|
|
281
329
|
command: "connect",
|
|
282
330
|
config: configuredBridgeServiceConfig(overrides, localConfig)
|
|
283
331
|
};
|
|
284
332
|
}
|
|
333
|
+
function parsePairArgs(argv) {
|
|
334
|
+
const localConfig = readLocalConfigSync();
|
|
335
|
+
const defaultConfig = configuredBridgeServiceConfig({}, localConfig);
|
|
336
|
+
let serverUrl = defaultConfig.serverUrl;
|
|
337
|
+
let code = null;
|
|
338
|
+
let name = null;
|
|
339
|
+
let fingerprint = null;
|
|
340
|
+
let repoRoot = null;
|
|
341
|
+
let cleanupWorktrees = null;
|
|
342
|
+
const cliProviders = [];
|
|
343
|
+
const projectLabels = [];
|
|
344
|
+
for (let index = 1; index < argv.length; index += 1) {
|
|
345
|
+
const arg = argv[index];
|
|
346
|
+
if (arg === "--server") {
|
|
347
|
+
serverUrl = argv[++index] ?? "";
|
|
348
|
+
}
|
|
349
|
+
else if (arg === "--code") {
|
|
350
|
+
code = argv[++index] ?? null;
|
|
351
|
+
}
|
|
352
|
+
else if (arg === "--name") {
|
|
353
|
+
name = argv[++index] ?? null;
|
|
354
|
+
}
|
|
355
|
+
else if (arg === "--fingerprint") {
|
|
356
|
+
fingerprint = argv[++index] ?? null;
|
|
357
|
+
}
|
|
358
|
+
else if (arg === "--repo-root") {
|
|
359
|
+
repoRoot = argv[++index] ?? null;
|
|
360
|
+
}
|
|
361
|
+
else if (arg === "--cli-provider") {
|
|
362
|
+
cliProviders.push(...splitCsv(argv[++index] ?? ""));
|
|
363
|
+
}
|
|
364
|
+
else if (arg === "--project-label") {
|
|
365
|
+
projectLabels.push(...splitCsv(argv[++index] ?? ""));
|
|
366
|
+
}
|
|
367
|
+
else if (arg === "--cleanup-worktrees") {
|
|
368
|
+
cleanupWorktrees = true;
|
|
369
|
+
}
|
|
370
|
+
else if (arg === "--no-cleanup-worktrees") {
|
|
371
|
+
cleanupWorktrees = false;
|
|
372
|
+
}
|
|
373
|
+
else if (!arg.startsWith("-") && code === null) {
|
|
374
|
+
code = arg;
|
|
375
|
+
}
|
|
376
|
+
else {
|
|
377
|
+
return { ok: false, error: `Unknown argument: ${arg}` };
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
if (!serverUrl.trim()) {
|
|
381
|
+
return { ok: false, error: "forge-bridge pair requires --server or configured serverUrl." };
|
|
382
|
+
}
|
|
383
|
+
const defaultName = `Local bridge ${os.hostname()}`;
|
|
384
|
+
const user = os.userInfo().username;
|
|
385
|
+
return {
|
|
386
|
+
ok: true,
|
|
387
|
+
command: "pair",
|
|
388
|
+
serverUrl,
|
|
389
|
+
code: normalizePairingCode(code),
|
|
390
|
+
name: name?.trim() || defaultName,
|
|
391
|
+
fingerprint: fingerprint?.trim() || `local-bridge:${os.hostname()}:${user}`,
|
|
392
|
+
repoRoot: repoRoot?.trim() ? path.resolve(repoRoot) : undefined,
|
|
393
|
+
cleanupWorktrees: cleanupWorktrees ?? localConfig.cleanupWorktrees ?? false,
|
|
394
|
+
cliProviders: cliProviders.length > 0 ? cliProviders : localConfig.cliProviders && localConfig.cliProviders.length > 0 ? [...localConfig.cliProviders] : ["codex"],
|
|
395
|
+
projectLabels: projectLabels.length > 0 ? projectLabels : localConfig.projectLabels ? [...localConfig.projectLabels] : []
|
|
396
|
+
};
|
|
397
|
+
}
|
|
285
398
|
function parseCleanupArgs(argv) {
|
|
286
399
|
let mode = "stale";
|
|
287
400
|
let force = false;
|
|
@@ -317,6 +430,100 @@ function splitCsv(value) {
|
|
|
317
430
|
.map((entry) => entry.trim())
|
|
318
431
|
.filter((entry) => entry.length > 0);
|
|
319
432
|
}
|
|
433
|
+
async function claimBridgeRunner(args) {
|
|
434
|
+
if (!args.code) {
|
|
435
|
+
throw new Error("Pairing code is required.");
|
|
436
|
+
}
|
|
437
|
+
const response = await fetch(new URL("/api/runners/claim", normalizeServerUrl(args.serverUrl)), {
|
|
438
|
+
method: "POST",
|
|
439
|
+
headers: { "Content-Type": "application/json" },
|
|
440
|
+
body: JSON.stringify({
|
|
441
|
+
code: args.code,
|
|
442
|
+
name: args.name,
|
|
443
|
+
fingerprint: args.fingerprint,
|
|
444
|
+
cliProviders: args.cliProviders,
|
|
445
|
+
projectLabels: args.projectLabels
|
|
446
|
+
})
|
|
447
|
+
});
|
|
448
|
+
if (!response.ok) {
|
|
449
|
+
throw new Error(`Pairing failed (${response.status}): ${await response.text()}`);
|
|
450
|
+
}
|
|
451
|
+
const value = await response.json();
|
|
452
|
+
if (!value.runnerId || !value.deviceToken) {
|
|
453
|
+
throw new Error("Pairing response did not include runner credentials.");
|
|
454
|
+
}
|
|
455
|
+
return {
|
|
456
|
+
runnerId: value.runnerId,
|
|
457
|
+
deviceToken: value.deviceToken
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
function pairedConfig(current, args, claim) {
|
|
461
|
+
return {
|
|
462
|
+
...current,
|
|
463
|
+
schemaVersion: 2,
|
|
464
|
+
serverUrl: args.serverUrl,
|
|
465
|
+
runnerId: claim.runnerId,
|
|
466
|
+
deviceToken: claim.deviceToken,
|
|
467
|
+
deviceName: args.name,
|
|
468
|
+
defaultRepoRoot: args.repoRoot ?? current.defaultRepoRoot,
|
|
469
|
+
cleanupWorktrees: args.cleanupWorktrees,
|
|
470
|
+
cliProviders: [...args.cliProviders],
|
|
471
|
+
providerStatuses: current.providerStatuses ?? providerStatusesFor(args.cliProviders),
|
|
472
|
+
projectLabels: [...args.projectLabels],
|
|
473
|
+
supportsApiSecretGrant: current.supportsApiSecretGrant ?? true,
|
|
474
|
+
supportsManagedWorktrees: current.supportsManagedWorktrees ?? true,
|
|
475
|
+
supportsManagedForgeArtifacts: current.supportsManagedForgeArtifacts ?? true,
|
|
476
|
+
supportsRunnerAuthoredPhaseArtifacts: current.supportsRunnerAuthoredPhaseArtifacts ?? true,
|
|
477
|
+
repoMappings: current.repoMappings ?? []
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
async function resolvePairingCode(code) {
|
|
481
|
+
const normalized = normalizePairingCode(code);
|
|
482
|
+
if (normalized) {
|
|
483
|
+
return normalized;
|
|
484
|
+
}
|
|
485
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
486
|
+
throw new Error("Pairing code is required. Run `forge-bridge pair <code>` or use an interactive terminal.");
|
|
487
|
+
}
|
|
488
|
+
const prompt = createInterface({ input: process.stdin, output: process.stdout });
|
|
489
|
+
try {
|
|
490
|
+
const answer = await prompt.question("Pairing code: ");
|
|
491
|
+
const clean = normalizePairingCode(answer);
|
|
492
|
+
if (!clean) {
|
|
493
|
+
throw new Error("Pairing code is required.");
|
|
494
|
+
}
|
|
495
|
+
return clean;
|
|
496
|
+
}
|
|
497
|
+
finally {
|
|
498
|
+
prompt.close();
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
async function resolvePairRepoRoot(repoRoot, configuredRoot) {
|
|
502
|
+
const fallback = path.resolve(repoRoot ?? configuredRoot ?? path.join(os.homedir(), "ForgeRepos"));
|
|
503
|
+
if (repoRoot?.trim()) {
|
|
504
|
+
return fallback;
|
|
505
|
+
}
|
|
506
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
507
|
+
return fallback;
|
|
508
|
+
}
|
|
509
|
+
const prompt = createInterface({ input: process.stdin, output: process.stdout });
|
|
510
|
+
try {
|
|
511
|
+
const answer = await prompt.question(`Repo folder [${fallback}]: `);
|
|
512
|
+
return path.resolve(answer.trim() || fallback);
|
|
513
|
+
}
|
|
514
|
+
finally {
|
|
515
|
+
prompt.close();
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
function normalizePairingCode(code) {
|
|
519
|
+
return code?.replace(/[\s-]/g, "").trim() ?? "";
|
|
520
|
+
}
|
|
521
|
+
function normalizeServerUrl(serverUrl) {
|
|
522
|
+
return serverUrl.endsWith("/") ? serverUrl : `${serverUrl}/`;
|
|
523
|
+
}
|
|
524
|
+
function usage() {
|
|
525
|
+
return "Usage: forge-bridge run-once --dispatch <dispatch-json-path> --result <result-json-path> | forge-bridge mcp --worktree <path> --work-item-id <id> --phase <phase> | forge-bridge pair [code] [--server <url>] | forge-bridge connect --server <url> --runner-id <id> --device-token <token> | forge-bridge add-repo <label> <path> | forge-bridge remove-repo <label> | forge-bridge status [--check-updates] | forge-bridge cleanup [--stale|--merged|--all|--force]";
|
|
526
|
+
}
|
|
320
527
|
async function writeJsonAtomic(outputPath, value) {
|
|
321
528
|
await fs.mkdir(path.dirname(outputPath), { recursive: true });
|
|
322
529
|
const tempPath = `${outputPath}.${process.pid}.${Date.now()}.tmp`;
|
|
@@ -343,7 +550,8 @@ async function writeStatus(config, checkUpdates, io) {
|
|
|
343
550
|
}
|
|
344
551
|
io.stdout.write("Detected CLIs:\n");
|
|
345
552
|
for (const cli of detected) {
|
|
346
|
-
|
|
553
|
+
const models = cli.models.length > 0 ? `, ${cli.models.length} models` : "";
|
|
554
|
+
io.stdout.write(` ${cli.provider}: ${cli.found ? `${cli.version || "found"}${models}` : "not found"}\n`);
|
|
347
555
|
}
|
|
348
556
|
if (checkUpdates) {
|
|
349
557
|
io.stdout.write("Update check: no update channel configured.\n");
|
|
@@ -385,16 +593,198 @@ async function cleanupRepos(config, mode, force, io) {
|
|
|
385
593
|
async function detectCliProviders(providers) {
|
|
386
594
|
const results = [];
|
|
387
595
|
for (const provider of providers) {
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
596
|
+
let detected = null;
|
|
597
|
+
for (const probe of cliProbeCommandsFor(provider)) {
|
|
598
|
+
try {
|
|
599
|
+
detected = await execVersionCommand(probe);
|
|
600
|
+
break;
|
|
601
|
+
}
|
|
602
|
+
catch {
|
|
603
|
+
// Try the next provider-specific probe.
|
|
604
|
+
}
|
|
394
605
|
}
|
|
606
|
+
results.push({
|
|
607
|
+
provider,
|
|
608
|
+
found: detected !== null,
|
|
609
|
+
version: detected ? versionLine(`${detected.stdout || detected.stderr}`) : "",
|
|
610
|
+
models: detected ? await detectCliModels(provider) : []
|
|
611
|
+
});
|
|
395
612
|
}
|
|
396
613
|
return results;
|
|
397
614
|
}
|
|
615
|
+
function cliProbeCommandsFor(provider) {
|
|
616
|
+
const normalized = provider.toLowerCase();
|
|
617
|
+
if (normalized === "codex") {
|
|
618
|
+
return [
|
|
619
|
+
{ command: "codex", args: ["-V"] },
|
|
620
|
+
{ command: "codex", args: ["--version"] }
|
|
621
|
+
];
|
|
622
|
+
}
|
|
623
|
+
if (normalized === "cursor") {
|
|
624
|
+
return [
|
|
625
|
+
{ command: "cursor-agent", args: ["-v"] },
|
|
626
|
+
{ command: "agent", args: ["-v"] },
|
|
627
|
+
{ command: "cursor-agent", args: ["--version"] },
|
|
628
|
+
{ command: "agent", args: ["--version"] }
|
|
629
|
+
];
|
|
630
|
+
}
|
|
631
|
+
return [
|
|
632
|
+
{ command: provider, args: ["--version"] },
|
|
633
|
+
{ command: provider, args: ["-V"] }
|
|
634
|
+
];
|
|
635
|
+
}
|
|
636
|
+
async function execVersionCommand(probe, timeout = 2000) {
|
|
637
|
+
const directLaunch = process.platform === "win32"
|
|
638
|
+
? await resolveCursorAgentDirectLaunch(probe.command, probe.args)
|
|
639
|
+
: null;
|
|
640
|
+
if (directLaunch) {
|
|
641
|
+
return await execFileAsync(directLaunch.command, [...directLaunch.args], { timeout });
|
|
642
|
+
}
|
|
643
|
+
try {
|
|
644
|
+
return await execFileAsync(probe.command, probe.args, { timeout });
|
|
645
|
+
}
|
|
646
|
+
catch (error) {
|
|
647
|
+
if (process.platform !== "win32") {
|
|
648
|
+
throw error;
|
|
649
|
+
}
|
|
650
|
+
return await execFileAsync(process.env.ComSpec ?? "cmd.exe", ["/d", "/s", "/c", windowsCommandLine(probe)], { timeout });
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
function windowsCommandLine(probe) {
|
|
654
|
+
return [probe.command, ...probe.args].map(quoteWindowsCommandLineArg).join(" ");
|
|
655
|
+
}
|
|
656
|
+
function quoteWindowsCommandLineArg(value) {
|
|
657
|
+
if (value.length > 0 && !/[\s"]/.test(value)) {
|
|
658
|
+
return value;
|
|
659
|
+
}
|
|
660
|
+
return `"${value.replace(/(\\*)"/g, '$1$1\\"').replace(/(\\+)$/g, "$1$1")}"`;
|
|
661
|
+
}
|
|
662
|
+
function versionLine(output) {
|
|
663
|
+
const lines = output
|
|
664
|
+
.trim()
|
|
665
|
+
.split(/\r?\n/)
|
|
666
|
+
.map((line) => line.trim())
|
|
667
|
+
.filter((line) => line.length > 0);
|
|
668
|
+
return lines.at(-1) ?? "";
|
|
669
|
+
}
|
|
670
|
+
async function detectCliModels(provider) {
|
|
671
|
+
const normalized = provider.toLowerCase();
|
|
672
|
+
try {
|
|
673
|
+
if (normalized === "codex") {
|
|
674
|
+
const result = await execFileAsync("codex", ["debug", "models"], { timeout: 5000, maxBuffer: 10 * 1024 * 1024 });
|
|
675
|
+
return parseCodexModelCatalog(result.stdout);
|
|
676
|
+
}
|
|
677
|
+
if (normalized === "cursor") {
|
|
678
|
+
const result = await execVersionCommand({ command: "cursor-agent", args: ["models"] }, 10_000);
|
|
679
|
+
return parseCursorModelList(`${result.stdout || result.stderr}`);
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
catch {
|
|
683
|
+
return [];
|
|
684
|
+
}
|
|
685
|
+
return [];
|
|
686
|
+
}
|
|
687
|
+
export function parseCodexModelCatalog(raw) {
|
|
688
|
+
try {
|
|
689
|
+
const parsed = JSON.parse(raw);
|
|
690
|
+
if (!isRecord(parsed) || !Array.isArray(parsed.models)) {
|
|
691
|
+
return [];
|
|
692
|
+
}
|
|
693
|
+
const entries = parsed.models
|
|
694
|
+
.map((entry, index) => {
|
|
695
|
+
if (!isRecord(entry) || typeof entry.slug !== "string") {
|
|
696
|
+
return null;
|
|
697
|
+
}
|
|
698
|
+
const visibility = typeof entry.visibility === "string" ? entry.visibility.toLowerCase() : "list";
|
|
699
|
+
if (visibility !== "list") {
|
|
700
|
+
return null;
|
|
701
|
+
}
|
|
702
|
+
const priority = typeof entry.priority === "number" && Number.isFinite(entry.priority) ? entry.priority : index;
|
|
703
|
+
return { slug: entry.slug, priority, index };
|
|
704
|
+
})
|
|
705
|
+
.filter((entry) => entry !== null)
|
|
706
|
+
.sort((left, right) => left.priority - right.priority || left.index - right.index)
|
|
707
|
+
.map((entry) => entry.slug);
|
|
708
|
+
return uniqueModels(entries);
|
|
709
|
+
}
|
|
710
|
+
catch {
|
|
711
|
+
return [];
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
export function parseCursorModelList(raw) {
|
|
715
|
+
return uniqueModels(raw
|
|
716
|
+
.split(/\r?\n/)
|
|
717
|
+
.map((line) => line.trim())
|
|
718
|
+
.filter((line) => line.length > 0 && !line.toLowerCase().startsWith("available models"))
|
|
719
|
+
.map((line) => line.split(/\s+-\s+/, 1)[0]?.trim() ?? "")
|
|
720
|
+
.filter((model) => model.length > 0));
|
|
721
|
+
}
|
|
722
|
+
function uniqueModels(models) {
|
|
723
|
+
const seen = new Set();
|
|
724
|
+
const result = [];
|
|
725
|
+
for (const model of models) {
|
|
726
|
+
const trimmed = model.trim();
|
|
727
|
+
const key = trimmed.toLowerCase();
|
|
728
|
+
if (!trimmed || seen.has(key)) {
|
|
729
|
+
continue;
|
|
730
|
+
}
|
|
731
|
+
seen.add(key);
|
|
732
|
+
result.push(trimmed);
|
|
733
|
+
}
|
|
734
|
+
return result;
|
|
735
|
+
}
|
|
736
|
+
function isRecord(value) {
|
|
737
|
+
return typeof value === "object" && value !== null;
|
|
738
|
+
}
|
|
739
|
+
async function withDetectedCliProviders(config) {
|
|
740
|
+
const detected = await detectCliProviders(configuredProviderCandidates(config.cliProviders));
|
|
741
|
+
const found = detected.filter((result) => result.found);
|
|
742
|
+
if (found.length === 0) {
|
|
743
|
+
return config;
|
|
744
|
+
}
|
|
745
|
+
const cliProviders = uniqueProviders([...config.cliProviders, ...found.map((result) => result.provider)]);
|
|
746
|
+
const providerStatuses = { ...config.providerStatuses };
|
|
747
|
+
for (const result of found) {
|
|
748
|
+
const existing = providerStatuses[result.provider];
|
|
749
|
+
providerStatuses[result.provider] = {
|
|
750
|
+
signedIn: existing?.signedIn ?? true,
|
|
751
|
+
version: result.version || existing?.version || "",
|
|
752
|
+
models: result.models.length > 0
|
|
753
|
+
? result.models
|
|
754
|
+
: existing?.models ?? providerStatusesFor([result.provider])[result.provider]?.models ?? []
|
|
755
|
+
};
|
|
756
|
+
}
|
|
757
|
+
return {
|
|
758
|
+
...config,
|
|
759
|
+
cliProviders,
|
|
760
|
+
providerStatuses
|
|
761
|
+
};
|
|
762
|
+
}
|
|
763
|
+
async function withDetectedPairCliProviders(args) {
|
|
764
|
+
const detectedConfig = await withDetectedCliProviders({
|
|
765
|
+
...defaultBridgeServiceConfig({
|
|
766
|
+
serverUrl: args.serverUrl,
|
|
767
|
+
cliProviders: [...args.cliProviders],
|
|
768
|
+
providerStatuses: providerStatusesFor(args.cliProviders),
|
|
769
|
+
projectLabels: [...args.projectLabels],
|
|
770
|
+
cleanupWorktrees: args.cleanupWorktrees,
|
|
771
|
+
defaultRepoRoot: args.repoRoot ?? defaultBridgeServiceConfig().defaultRepoRoot
|
|
772
|
+
})
|
|
773
|
+
});
|
|
774
|
+
return {
|
|
775
|
+
...args,
|
|
776
|
+
cliProviders: detectedConfig.cliProviders
|
|
777
|
+
};
|
|
778
|
+
}
|
|
779
|
+
function configuredProviderCandidates(providers) {
|
|
780
|
+
return uniqueProviders([...providers, "codex", "cursor"]);
|
|
781
|
+
}
|
|
782
|
+
function uniqueProviders(providers) {
|
|
783
|
+
return providers
|
|
784
|
+
.map((provider) => provider.trim())
|
|
785
|
+
.filter((provider) => provider.length > 0)
|
|
786
|
+
.filter((provider, index, all) => all.findIndex((candidate) => candidate.toLowerCase() === provider.toLowerCase()) === index);
|
|
787
|
+
}
|
|
398
788
|
async function runGit(cwd, args) {
|
|
399
789
|
await execFileAsync("git", args, { cwd, timeout: 10_000 });
|
|
400
790
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { BridgeProviderStatus, BridgeRepoMapping, BridgeServiceConfig } from "../bridge/bridge.types.js";
|
|
2
2
|
export interface BridgeLocalConfig {
|
|
3
|
-
readonly schemaVersion: 1;
|
|
3
|
+
readonly schemaVersion: 1 | 2;
|
|
4
4
|
readonly serverUrl?: string;
|
|
5
5
|
readonly runnerId?: string;
|
|
6
6
|
readonly deviceToken?: string;
|
|
@@ -13,6 +13,9 @@ export interface BridgeLocalConfig {
|
|
|
13
13
|
readonly repoMappings?: readonly LocalRepoMapping[];
|
|
14
14
|
readonly sandboxProfiles?: readonly string[];
|
|
15
15
|
readonly supportsApiSecretGrant?: boolean;
|
|
16
|
+
readonly supportsManagedWorktrees?: boolean;
|
|
17
|
+
readonly supportsManagedForgeArtifacts?: boolean;
|
|
18
|
+
readonly supportsRunnerAuthoredPhaseArtifacts?: boolean;
|
|
16
19
|
}
|
|
17
20
|
export interface LocalRepoMapping extends BridgeRepoMapping {
|
|
18
21
|
readonly label: string;
|
package/dist/cli/local-config.js
CHANGED
|
@@ -46,11 +46,14 @@ export function applyLocalConfigDefaults(config, local) {
|
|
|
46
46
|
defaultRepoRoot: process.env.FORGE_REPO_ROOT ?? local.defaultRepoRoot ?? config.defaultRepoRoot,
|
|
47
47
|
cleanupWorktrees: local.cleanupWorktrees ?? config.cleanupWorktrees,
|
|
48
48
|
cliProviders,
|
|
49
|
-
providerStatuses: local.providerStatuses ?? providerStatusesFor(cliProviders),
|
|
49
|
+
providerStatuses: normalizeProviderStatuses(local.providerStatuses) ?? providerStatusesFor(cliProviders),
|
|
50
50
|
projectLabels: local.projectLabels ?? config.projectLabels,
|
|
51
51
|
repoMappings: local.repoMappings ?? config.repoMappings,
|
|
52
52
|
sandboxProfiles: local.sandboxProfiles ?? config.sandboxProfiles,
|
|
53
|
-
supportsApiSecretGrant: local.supportsApiSecretGrant ?? config.supportsApiSecretGrant
|
|
53
|
+
supportsApiSecretGrant: local.supportsApiSecretGrant ?? config.supportsApiSecretGrant,
|
|
54
|
+
supportsManagedWorktrees: local.supportsManagedWorktrees ?? config.supportsManagedWorktrees,
|
|
55
|
+
supportsManagedForgeArtifacts: local.supportsManagedForgeArtifacts ?? config.supportsManagedForgeArtifacts,
|
|
56
|
+
supportsRunnerAuthoredPhaseArtifacts: local.supportsRunnerAuthoredPhaseArtifacts ?? config.supportsRunnerAuthoredPhaseArtifacts
|
|
54
57
|
};
|
|
55
58
|
}
|
|
56
59
|
export async function addRepoMapping(label, repoPath) {
|
|
@@ -83,7 +86,7 @@ export async function removeRepoMapping(label) {
|
|
|
83
86
|
return { config: next, removed: after.length !== before.length };
|
|
84
87
|
}
|
|
85
88
|
export function providerStatusesFor(providers) {
|
|
86
|
-
return Object.fromEntries(providers.map((provider) => [provider, { signedIn: true, version: "" }]));
|
|
89
|
+
return Object.fromEntries(providers.map((provider) => [provider, { signedIn: true, version: "", models: configuredModelsFor(provider) }]));
|
|
87
90
|
}
|
|
88
91
|
function parseLocalConfig(raw) {
|
|
89
92
|
if (!raw.trim()) {
|
|
@@ -93,7 +96,7 @@ function parseLocalConfig(raw) {
|
|
|
93
96
|
}
|
|
94
97
|
function normalizeLocalConfig(config) {
|
|
95
98
|
return {
|
|
96
|
-
schemaVersion:
|
|
99
|
+
schemaVersion: 2,
|
|
97
100
|
serverUrl: cleanString(config.serverUrl),
|
|
98
101
|
runnerId: cleanString(config.runnerId),
|
|
99
102
|
deviceToken: cleanString(config.deviceToken),
|
|
@@ -101,19 +104,51 @@ function normalizeLocalConfig(config) {
|
|
|
101
104
|
defaultRepoRoot: cleanString(config.defaultRepoRoot),
|
|
102
105
|
cleanupWorktrees: config.cleanupWorktrees,
|
|
103
106
|
cliProviders: cleanList(config.cliProviders),
|
|
104
|
-
providerStatuses: config.providerStatuses,
|
|
107
|
+
providerStatuses: normalizeProviderStatuses(config.providerStatuses),
|
|
105
108
|
projectLabels: cleanList(config.projectLabels),
|
|
106
109
|
repoMappings: normalizeRepoMappings(config.repoMappings),
|
|
107
110
|
sandboxProfiles: cleanList(config.sandboxProfiles),
|
|
108
|
-
supportsApiSecretGrant: config.supportsApiSecretGrant
|
|
111
|
+
supportsApiSecretGrant: config.supportsApiSecretGrant,
|
|
112
|
+
supportsManagedWorktrees: config.supportsManagedWorktrees,
|
|
113
|
+
supportsManagedForgeArtifacts: config.supportsManagedForgeArtifacts,
|
|
114
|
+
supportsRunnerAuthoredPhaseArtifacts: config.supportsRunnerAuthoredPhaseArtifacts
|
|
109
115
|
};
|
|
110
116
|
}
|
|
111
117
|
function emptyConfig() {
|
|
112
118
|
return {
|
|
113
|
-
schemaVersion:
|
|
119
|
+
schemaVersion: 2,
|
|
114
120
|
repoMappings: []
|
|
115
121
|
};
|
|
116
122
|
}
|
|
123
|
+
function normalizeProviderStatuses(values) {
|
|
124
|
+
if (!values)
|
|
125
|
+
return undefined;
|
|
126
|
+
const entries = [];
|
|
127
|
+
for (const [provider, status] of Object.entries(values)) {
|
|
128
|
+
const key = provider.trim();
|
|
129
|
+
if (!key)
|
|
130
|
+
continue;
|
|
131
|
+
entries.push([
|
|
132
|
+
key,
|
|
133
|
+
{
|
|
134
|
+
signedIn: status?.signedIn ?? true,
|
|
135
|
+
version: cleanString(status?.version),
|
|
136
|
+
models: cleanList(status?.models) ?? configuredModelsFor(key)
|
|
137
|
+
}
|
|
138
|
+
]);
|
|
139
|
+
}
|
|
140
|
+
return entries.length > 0 ? Object.fromEntries(entries) : undefined;
|
|
141
|
+
}
|
|
142
|
+
function configuredModelsFor(provider) {
|
|
143
|
+
const envName = `FORGE_${provider.replace(/[^a-zA-Z0-9]/g, "_").toUpperCase()}_MODELS`;
|
|
144
|
+
return splitCsv(process.env[envName] ?? "");
|
|
145
|
+
}
|
|
146
|
+
function splitCsv(value) {
|
|
147
|
+
return value
|
|
148
|
+
.split(",")
|
|
149
|
+
.map((entry) => entry.trim())
|
|
150
|
+
.filter((entry) => entry.length > 0);
|
|
151
|
+
}
|
|
117
152
|
function normalizeRepoMappings(values) {
|
|
118
153
|
return (values ?? [])
|
|
119
154
|
.filter((mapping) => !!mapping.label?.trim() && !!mapping.localPath?.trim())
|
|
@@ -124,7 +159,8 @@ function normalizeRepoMappings(values) {
|
|
|
124
159
|
repoId: cleanString(mapping.repoId),
|
|
125
160
|
name: cleanString(mapping.name) ?? label,
|
|
126
161
|
remoteUrl: cleanString(mapping.remoteUrl),
|
|
127
|
-
localPath: path.resolve(mapping.localPath)
|
|
162
|
+
localPath: path.resolve(mapping.localPath),
|
|
163
|
+
qaAppLaunchTarget: normalizeQaAppLaunchTarget(mapping.qaAppLaunchTarget)
|
|
128
164
|
};
|
|
129
165
|
})
|
|
130
166
|
.sort((left, right) => left.label.localeCompare(right.label));
|
|
@@ -136,6 +172,22 @@ function normalizeLabel(value) {
|
|
|
136
172
|
}
|
|
137
173
|
return label;
|
|
138
174
|
}
|
|
175
|
+
function normalizeQaAppLaunchTarget(value) {
|
|
176
|
+
if (!value)
|
|
177
|
+
return undefined;
|
|
178
|
+
const routes = cleanList(value.routes);
|
|
179
|
+
const normalized = {
|
|
180
|
+
url: cleanString(value.url),
|
|
181
|
+
command: cleanString(value.command),
|
|
182
|
+
working_directory: cleanString(value.working_directory),
|
|
183
|
+
ready_url: cleanString(value.ready_url),
|
|
184
|
+
routes,
|
|
185
|
+
notes: cleanString(value.notes)
|
|
186
|
+
};
|
|
187
|
+
return normalized.url || normalized.command || normalized.ready_url || routes?.length
|
|
188
|
+
? normalized
|
|
189
|
+
: undefined;
|
|
190
|
+
}
|
|
139
191
|
function cleanString(value) {
|
|
140
192
|
const clean = value?.trim();
|
|
141
193
|
return clean ? clean : undefined;
|
package/dist/session/index.d.ts
CHANGED
|
@@ -1,2 +1,4 @@
|
|
|
1
1
|
export { BridgeSessionService } from "./session.service.js";
|
|
2
|
+
export { buildCompletionToolServerArgs, codexMcpConfigArgs, completionPhaseFromDispatch, expectedCompletionToolName, prepareCompletionToolRuntime, readCompletionArtifact, writeCompletionArtifact } from "./phase-completion.js";
|
|
3
|
+
export type { CompletionArtifactReference, CompletionPhase, CompletionToolRuntime } from "./phase-completion.js";
|
|
2
4
|
export type { SessionService } from "./session.base.js";
|