@loucompanion/forge-bridge 0.1.1-dev.03b8ba168599 → 0.1.1-dev.040867847f42
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/dist/bridge/bridge.service.d.ts +1 -0
- package/dist/bridge/bridge.service.js +133 -24
- package/dist/bridge/bridge.types.d.ts +13 -2
- package/dist/cli/cli.service.d.ts +4 -0
- package/dist/cli/cli.service.js +111 -10
- package/dist/cli/local-config.d.ts +1 -0
- package/dist/cli/local-config.js +22 -3
- package/dist/config.js +1 -1
- package/dist/session/index.d.ts +2 -0
- package/dist/session/index.js +1 -0
- package/dist/session/internals.d.ts +3 -1
- package/dist/session/internals.js +1 -0
- package/dist/session/phase-completion.d.ts +115 -0
- package/dist/session/phase-completion.js +2773 -0
- package/dist/session/provider-cli/provider-cli.base.d.ts +4 -3
- package/dist/session/provider-cli/provider-cli.service.d.ts +14 -5
- package/dist/session/provider-cli/provider-cli.service.js +2154 -242
- package/dist/session/provider-cli/provider-cli.types.d.ts +6 -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 +56 -16
- package/dist/session/session.types.d.ts +128 -0
- package/dist/session/session.types.js +261 -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 +2 -1
- package/dist/worktree/artifact-injector.js +86 -9
- package/dist/worktree/worktree.service.d.ts +1 -0
- package/dist/worktree/worktree.service.js +132 -6
- package/dist/worktree/worktree.types.d.ts +8 -1
- package/package.json +8 -3
|
@@ -15,6 +15,7 @@ export declare class ForgeBridgeService implements BridgeService {
|
|
|
15
15
|
run(signal?: AbortSignal): Promise<void>;
|
|
16
16
|
stop(): Promise<void>;
|
|
17
17
|
private handleDispatch;
|
|
18
|
+
private parseDispatchOrReport;
|
|
18
19
|
private handleCancel;
|
|
19
20
|
private abortActiveSessions;
|
|
20
21
|
private normalizeDispatch;
|
|
@@ -51,7 +51,7 @@ export class ForgeBridgeService {
|
|
|
51
51
|
attempt = 0;
|
|
52
52
|
await this.waitForClose(signal);
|
|
53
53
|
}
|
|
54
|
-
catch
|
|
54
|
+
catch {
|
|
55
55
|
if (this.stopped || signal?.aborted) {
|
|
56
56
|
break;
|
|
57
57
|
}
|
|
@@ -73,10 +73,14 @@ export class ForgeBridgeService {
|
|
|
73
73
|
this.abortActiveSessions("bridge stopping");
|
|
74
74
|
await this.stopConnection();
|
|
75
75
|
}
|
|
76
|
-
async handleDispatch(
|
|
76
|
+
async handleDispatch(rawDispatch) {
|
|
77
|
+
const dispatch = this.parseDispatchOrReport(rawDispatch);
|
|
78
|
+
if (!dispatch) {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
77
81
|
const leaseId = dispatch.lease.id;
|
|
78
82
|
const fencingToken = dispatch.lease.fencingToken;
|
|
79
|
-
const redactionSecrets = exactSecrets(dispatch);
|
|
83
|
+
const redactionSecrets = exactSecrets(dispatch, this.config);
|
|
80
84
|
if (this.activeSessions.has(leaseId)) {
|
|
81
85
|
await this.sendResult(leaseId, fencingToken, failureResult("Dispatch is already active on this bridge.")).catch(() => undefined);
|
|
82
86
|
return;
|
|
@@ -87,11 +91,20 @@ export class ForgeBridgeService {
|
|
|
87
91
|
const normalized = this.normalizeDispatch(dispatch);
|
|
88
92
|
const result = await this.sessions.runOnce(normalized, {
|
|
89
93
|
emit: async (event) => {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
94
|
+
try {
|
|
95
|
+
await this.invokeAck("DispatchEvent", {
|
|
96
|
+
leaseId,
|
|
97
|
+
fencingToken,
|
|
98
|
+
event: redactSecrets(event, redactionSecrets)
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
if (isDispatchNoLongerPending(error)) {
|
|
103
|
+
controller.abort(error instanceof Error ? error.message : "Dispatch is no longer pending.");
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
throw error;
|
|
107
|
+
}
|
|
95
108
|
}
|
|
96
109
|
}, controller.signal);
|
|
97
110
|
await this.sendResult(leaseId, fencingToken, redactSecrets(result, redactionSecrets));
|
|
@@ -106,6 +119,27 @@ export class ForgeBridgeService {
|
|
|
106
119
|
this.activeSessions.delete(leaseId);
|
|
107
120
|
}
|
|
108
121
|
}
|
|
122
|
+
parseDispatchOrReport(rawDispatch) {
|
|
123
|
+
try {
|
|
124
|
+
return parseRunOnceDispatchPacket(rawDispatch);
|
|
125
|
+
}
|
|
126
|
+
catch (error) {
|
|
127
|
+
const identifiers = readDispatchIdentifiers(rawDispatch);
|
|
128
|
+
if (!identifiers) {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
const summary = invalidDispatchSummary(error);
|
|
132
|
+
void this.sendResult(identifiers.leaseId, identifiers.fencingToken, failureResult(summary, [
|
|
133
|
+
{
|
|
134
|
+
role: "runner",
|
|
135
|
+
kind: "error",
|
|
136
|
+
content: summary,
|
|
137
|
+
eventType: "invalid_dispatch"
|
|
138
|
+
}
|
|
139
|
+
])).catch(() => undefined);
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
109
143
|
async handleCancel(request) {
|
|
110
144
|
const active = this.activeSessions.get(request.leaseId);
|
|
111
145
|
if (!active || active.fencingToken !== request.fencingToken) {
|
|
@@ -123,6 +157,7 @@ export class ForgeBridgeService {
|
|
|
123
157
|
const mapping = findRepoMapping(dispatch, this.config.repoMappings);
|
|
124
158
|
const localPath = mapping?.localPath?.trim();
|
|
125
159
|
const repoRoot = localPath ? path.dirname(path.resolve(localPath)) : this.config.defaultRepoRoot;
|
|
160
|
+
const qaLaunchTarget = normalizeQaAppLaunchTarget(mapping?.qaAppLaunchTarget) ?? dispatch.qaBrowser?.launchTarget ?? null;
|
|
126
161
|
return {
|
|
127
162
|
...dispatch,
|
|
128
163
|
repo: {
|
|
@@ -133,7 +168,13 @@ export class ForgeBridgeService {
|
|
|
133
168
|
serverLocal: {
|
|
134
169
|
repoRoot,
|
|
135
170
|
cleanupWorktrees: this.config.cleanupWorktrees
|
|
136
|
-
}
|
|
171
|
+
},
|
|
172
|
+
qaBrowser: dispatch.qaBrowser
|
|
173
|
+
? {
|
|
174
|
+
...dispatch.qaBrowser,
|
|
175
|
+
launchTarget: qaLaunchTarget
|
|
176
|
+
}
|
|
177
|
+
: dispatch.qaBrowser
|
|
137
178
|
};
|
|
138
179
|
}
|
|
139
180
|
async sendResult(leaseId, fencingToken, result) {
|
|
@@ -142,7 +183,15 @@ export class ForgeBridgeService {
|
|
|
142
183
|
fencingToken,
|
|
143
184
|
result
|
|
144
185
|
};
|
|
145
|
-
|
|
186
|
+
try {
|
|
187
|
+
await this.invokeAck("DispatchResult", payload);
|
|
188
|
+
}
|
|
189
|
+
catch (error) {
|
|
190
|
+
if (isDispatchNoLongerPending(error)) {
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
throw error;
|
|
194
|
+
}
|
|
146
195
|
}
|
|
147
196
|
startHeartbeat() {
|
|
148
197
|
this.stopHeartbeat();
|
|
@@ -179,7 +228,8 @@ export class ForgeBridgeService {
|
|
|
179
228
|
supportsApiSecretGrant: this.config.supportsApiSecretGrant,
|
|
180
229
|
supportsManagedWorktrees: this.config.supportsManagedWorktrees,
|
|
181
230
|
supportsManagedForgeArtifacts: this.config.supportsManagedForgeArtifacts,
|
|
182
|
-
supportsRunnerAuthoredPhaseArtifacts: this.config.supportsRunnerAuthoredPhaseArtifacts
|
|
231
|
+
supportsRunnerAuthoredPhaseArtifacts: this.config.supportsRunnerAuthoredPhaseArtifacts,
|
|
232
|
+
supportsDraftRunOnce: this.config.supportsDraftRunOnce
|
|
183
233
|
};
|
|
184
234
|
}
|
|
185
235
|
async invokeAck(methodName, payload) {
|
|
@@ -189,7 +239,7 @@ export class ForgeBridgeService {
|
|
|
189
239
|
}
|
|
190
240
|
const ack = await connection.invoke(methodName, payload);
|
|
191
241
|
if (!ack.accepted) {
|
|
192
|
-
throw new
|
|
242
|
+
throw new BridgeCallbackRejectedError(ack.code ?? null, ack.message ?? ack.code ?? `${methodName} callback was rejected.`);
|
|
193
243
|
}
|
|
194
244
|
return ack;
|
|
195
245
|
}
|
|
@@ -207,6 +257,20 @@ export class ForgeBridgeService {
|
|
|
207
257
|
}
|
|
208
258
|
}
|
|
209
259
|
}
|
|
260
|
+
class BridgeCallbackRejectedError extends Error {
|
|
261
|
+
code;
|
|
262
|
+
constructor(code, message) {
|
|
263
|
+
super(message);
|
|
264
|
+
this.code = code;
|
|
265
|
+
this.name = "BridgeCallbackRejectedError";
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
function isDispatchNoLongerPending(error) {
|
|
269
|
+
if (error instanceof BridgeCallbackRejectedError) {
|
|
270
|
+
return error.code === "bridge.dispatch_not_pending" || error.code === "bridge.stale_dispatch";
|
|
271
|
+
}
|
|
272
|
+
return error instanceof Error && error.message === "Dispatch is no longer pending.";
|
|
273
|
+
}
|
|
210
274
|
export class SignalRBridgeConnectionFactory {
|
|
211
275
|
create(config) {
|
|
212
276
|
return new SignalRBridgeConnection(config);
|
|
@@ -237,14 +301,7 @@ export class SignalRBridgeConnection {
|
|
|
237
301
|
}
|
|
238
302
|
onDispatch(handler) {
|
|
239
303
|
this.connection.on("Dispatch", (raw) => {
|
|
240
|
-
|
|
241
|
-
try {
|
|
242
|
-
dispatch = parseRunOnceDispatchPacket(raw);
|
|
243
|
-
}
|
|
244
|
-
catch {
|
|
245
|
-
return;
|
|
246
|
-
}
|
|
247
|
-
void handler(dispatch).catch(() => undefined);
|
|
304
|
+
void handler(raw).catch(() => undefined);
|
|
248
305
|
});
|
|
249
306
|
}
|
|
250
307
|
onCancel(handler) {
|
|
@@ -271,11 +328,12 @@ export function defaultBridgeServiceConfig(overrides = {}) {
|
|
|
271
328
|
projectLabels: splitCsv(process.env.FORGE_PROJECT_LABELS ?? ""),
|
|
272
329
|
repoMappings: [],
|
|
273
330
|
os: process.platform,
|
|
274
|
-
sandboxProfiles:
|
|
331
|
+
sandboxProfiles: overrides.sandboxProfiles,
|
|
275
332
|
supportsApiSecretGrant: true,
|
|
276
333
|
supportsManagedWorktrees: true,
|
|
277
334
|
supportsManagedForgeArtifacts: true,
|
|
278
335
|
supportsRunnerAuthoredPhaseArtifacts: true,
|
|
336
|
+
supportsDraftRunOnce: true,
|
|
279
337
|
version: process.env.FORGE_BRIDGE_VERSION ?? forgeBridgeVersion,
|
|
280
338
|
protocolVersion: 1,
|
|
281
339
|
minServerVersion: null,
|
|
@@ -285,6 +343,7 @@ export function defaultBridgeServiceConfig(overrides = {}) {
|
|
|
285
343
|
};
|
|
286
344
|
return {
|
|
287
345
|
...config,
|
|
346
|
+
sandboxProfiles: config.sandboxProfiles ?? defaultSandboxProfiles(config.cliProviders),
|
|
288
347
|
heartbeatIntervalMs: positiveFiniteNumber(config.heartbeatIntervalMs, 30_000)
|
|
289
348
|
};
|
|
290
349
|
}
|
|
@@ -303,6 +362,11 @@ function positiveFiniteNumber(value, fallback) {
|
|
|
303
362
|
function providerStatusesFor(providers) {
|
|
304
363
|
return Object.fromEntries(providers.map((provider) => [provider, { signedIn: true, version: "", models: configuredModelsFor(provider) }]));
|
|
305
364
|
}
|
|
365
|
+
function defaultSandboxProfiles(providers) {
|
|
366
|
+
return providers.some((provider) => provider.toLowerCase() === "codex")
|
|
367
|
+
? ["provider-workspace-write"]
|
|
368
|
+
: ["none"];
|
|
369
|
+
}
|
|
306
370
|
function configuredModelsFor(provider) {
|
|
307
371
|
const envName = `FORGE_${provider.replace(/[^a-zA-Z0-9]/g, "_").toUpperCase()}_MODELS`;
|
|
308
372
|
return splitCsv(process.env[envName] ?? "");
|
|
@@ -323,16 +387,58 @@ function findRepoMapping(dispatch, mappings) {
|
|
|
323
387
|
(!!mapping.remoteUrl?.trim() && equalsIgnoreCase(mapping.remoteUrl, dispatch.repo.remoteUrl)));
|
|
324
388
|
});
|
|
325
389
|
}
|
|
390
|
+
function normalizeQaAppLaunchTarget(target) {
|
|
391
|
+
if (!target) {
|
|
392
|
+
return null;
|
|
393
|
+
}
|
|
394
|
+
const routes = [...new Set((target.routes ?? []).map((route) => route.trim()).filter(Boolean))];
|
|
395
|
+
const normalized = {
|
|
396
|
+
url: cleanString(target.url),
|
|
397
|
+
command: cleanString(target.command),
|
|
398
|
+
working_directory: cleanString(target.working_directory),
|
|
399
|
+
ready_url: cleanString(target.ready_url),
|
|
400
|
+
routes,
|
|
401
|
+
notes: cleanString(target.notes)
|
|
402
|
+
};
|
|
403
|
+
return normalized.url || normalized.command || normalized.ready_url || routes.length > 0
|
|
404
|
+
? normalized
|
|
405
|
+
: null;
|
|
406
|
+
}
|
|
407
|
+
function cleanString(value) {
|
|
408
|
+
const clean = value?.trim();
|
|
409
|
+
return clean ? clean : null;
|
|
410
|
+
}
|
|
326
411
|
function equalsIgnoreCase(left, right) {
|
|
327
412
|
return !!left?.trim() && !!right?.trim() && left.trim().toLowerCase() === right.trim().toLowerCase();
|
|
328
413
|
}
|
|
414
|
+
function readDispatchIdentifiers(rawDispatch) {
|
|
415
|
+
if (!rawDispatch || typeof rawDispatch !== "object" || Array.isArray(rawDispatch)) {
|
|
416
|
+
return null;
|
|
417
|
+
}
|
|
418
|
+
const packet = rawDispatch;
|
|
419
|
+
const rawLease = packet.lease ?? packet.Lease;
|
|
420
|
+
if (!rawLease || typeof rawLease !== "object" || Array.isArray(rawLease)) {
|
|
421
|
+
return null;
|
|
422
|
+
}
|
|
423
|
+
const lease = rawLease;
|
|
424
|
+
const leaseId = readString(lease, "id", "Id");
|
|
425
|
+
const fencingToken = readString(lease, "fencingToken", "FencingToken");
|
|
426
|
+
return leaseId && fencingToken ? { leaseId, fencingToken } : null;
|
|
427
|
+
}
|
|
428
|
+
function readString(source, camelKey, pascalKey) {
|
|
429
|
+
const value = source[camelKey] ?? source[pascalKey];
|
|
430
|
+
return typeof value === "string" && value.trim() ? value : null;
|
|
431
|
+
}
|
|
432
|
+
function invalidDispatchSummary(error) {
|
|
433
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
434
|
+
return `Invalid bridge dispatch packet: ${detail}`;
|
|
435
|
+
}
|
|
329
436
|
function delay(ms, signal) {
|
|
330
437
|
if (signal?.aborted) {
|
|
331
438
|
return Promise.resolve();
|
|
332
439
|
}
|
|
333
440
|
return new Promise((resolve) => {
|
|
334
441
|
const timeout = setTimeout(resolve, ms);
|
|
335
|
-
timeout.unref?.();
|
|
336
442
|
signal?.addEventListener("abort", () => {
|
|
337
443
|
clearTimeout(timeout);
|
|
338
444
|
resolve();
|
|
@@ -348,6 +454,9 @@ function abortPromise(signal) {
|
|
|
348
454
|
}
|
|
349
455
|
return new Promise((resolve) => signal.addEventListener("abort", () => resolve(), { once: true }));
|
|
350
456
|
}
|
|
351
|
-
function exactSecrets(dispatch) {
|
|
352
|
-
return
|
|
457
|
+
function exactSecrets(dispatch, config) {
|
|
458
|
+
return [
|
|
459
|
+
dispatch.apiKeySecretGrant?.secret,
|
|
460
|
+
config.deviceToken
|
|
461
|
+
].filter((secret) => typeof secret === "string" && secret.length > 0);
|
|
353
462
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { RunOnceResultPacket, RunnerDispatchEvent } from "../session/session.types.js";
|
|
2
2
|
export interface BridgeServiceConfig {
|
|
3
3
|
readonly serverUrl: string;
|
|
4
4
|
readonly runnerId: string;
|
|
@@ -16,6 +16,7 @@ export interface BridgeServiceConfig {
|
|
|
16
16
|
readonly supportsManagedWorktrees: boolean;
|
|
17
17
|
readonly supportsManagedForgeArtifacts: boolean;
|
|
18
18
|
readonly supportsRunnerAuthoredPhaseArtifacts: boolean;
|
|
19
|
+
readonly supportsDraftRunOnce: boolean;
|
|
19
20
|
readonly version: string;
|
|
20
21
|
readonly protocolVersion: number;
|
|
21
22
|
readonly minServerVersion?: string | null;
|
|
@@ -27,6 +28,15 @@ export interface BridgeRepoMapping {
|
|
|
27
28
|
readonly name?: string | null;
|
|
28
29
|
readonly remoteUrl?: string | null;
|
|
29
30
|
readonly localPath?: string | null;
|
|
31
|
+
readonly qaAppLaunchTarget?: QaAppLaunchTarget | null;
|
|
32
|
+
}
|
|
33
|
+
export interface QaAppLaunchTarget {
|
|
34
|
+
readonly url?: string | null;
|
|
35
|
+
readonly command?: string | null;
|
|
36
|
+
readonly working_directory?: string | null;
|
|
37
|
+
readonly ready_url?: string | null;
|
|
38
|
+
readonly routes?: readonly string[] | null;
|
|
39
|
+
readonly notes?: string | null;
|
|
30
40
|
}
|
|
31
41
|
export interface BridgeProviderStatus {
|
|
32
42
|
readonly signedIn?: boolean | null;
|
|
@@ -46,6 +56,7 @@ export interface BridgeCapabilities {
|
|
|
46
56
|
readonly supportsManagedWorktrees?: boolean | null;
|
|
47
57
|
readonly supportsManagedForgeArtifacts?: boolean | null;
|
|
48
58
|
readonly supportsRunnerAuthoredPhaseArtifacts?: boolean | null;
|
|
59
|
+
readonly supportsDraftRunOnce?: boolean | null;
|
|
49
60
|
}
|
|
50
61
|
export interface BridgeHelloRequest {
|
|
51
62
|
readonly version: string;
|
|
@@ -79,5 +90,5 @@ export interface BridgeCancelRequest {
|
|
|
79
90
|
readonly fencingToken: string;
|
|
80
91
|
readonly reason?: string | null;
|
|
81
92
|
}
|
|
82
|
-
export type BridgeDispatchHandler = (dispatch:
|
|
93
|
+
export type BridgeDispatchHandler = (dispatch: unknown) => Promise<void>;
|
|
83
94
|
export type BridgeCancelHandler = (request: BridgeCancelRequest) => Promise<void>;
|
|
@@ -14,6 +14,10 @@ export declare function parseArgs(argv: readonly string[]): {
|
|
|
14
14
|
readonly command: "run-once";
|
|
15
15
|
readonly dispatchPath: string;
|
|
16
16
|
readonly resultPath: string;
|
|
17
|
+
} | {
|
|
18
|
+
readonly ok: true;
|
|
19
|
+
readonly command: "mcp";
|
|
20
|
+
readonly argv: readonly string[];
|
|
17
21
|
} | {
|
|
18
22
|
readonly ok: true;
|
|
19
23
|
readonly command: "connect";
|
package/dist/cli/cli.service.js
CHANGED
|
@@ -4,10 +4,13 @@ import os from "node:os";
|
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { createInterface } from "node:readline/promises";
|
|
6
6
|
import { promisify } from "node:util";
|
|
7
|
+
import { resolveCursorAgentDirectLaunch } from "../shared/cursor-agent/cursor-agent.launch.js";
|
|
7
8
|
import { redactSecrets, redactText } from "../shared/redaction/redaction.js";
|
|
8
9
|
import { defaultBridgeServiceConfig, ForgeBridgeService } from "../bridge/bridge.service.js";
|
|
10
|
+
import { forgeBridgeVersion } from "../version.js";
|
|
9
11
|
import { addRepoMapping, applyLocalConfigDefaults, providerStatusesFor, readLocalConfigSync, removeRepoMapping, resolveLocalConfigPath, writeLocalConfig } from "./local-config.js";
|
|
10
12
|
import { BridgeSessionService } from "../session/session.service.js";
|
|
13
|
+
import { parseCompletionMcpArgs, runCompletionMcpServer } from "../session/phase-completion.js";
|
|
11
14
|
import { failureResult, parseRunOnceDispatchPacket } from "../session/session.types.js";
|
|
12
15
|
const apiKeySecretGrantEnv = "FORGE_API_KEY_SECRET_GRANT";
|
|
13
16
|
const execFileAsync = promisify(execFile);
|
|
@@ -21,6 +24,14 @@ export class ForgeBridgeCliService {
|
|
|
21
24
|
this.pairRunner = pairRunner;
|
|
22
25
|
}
|
|
23
26
|
async run(argv, io = { stdout: process.stdout, stderr: process.stderr }) {
|
|
27
|
+
if (argv.length === 0 || isHelpArg(argv[0])) {
|
|
28
|
+
io.stdout.write(helpText());
|
|
29
|
+
return 0;
|
|
30
|
+
}
|
|
31
|
+
if (argv[0] === "version" || argv[0] === "--version" || argv[0] === "-v") {
|
|
32
|
+
io.stdout.write(`forge-bridge ${defaultBridgeServiceConfig().version ?? forgeBridgeVersion}\n`);
|
|
33
|
+
return 0;
|
|
34
|
+
}
|
|
24
35
|
const args = parseArgs(argv);
|
|
25
36
|
if (!args.ok) {
|
|
26
37
|
io.stderr.write(`${args.error}\n`);
|
|
@@ -42,6 +53,7 @@ export class ForgeBridgeCliService {
|
|
|
42
53
|
process.once("SIGINT", stop);
|
|
43
54
|
process.once("SIGTERM", stop);
|
|
44
55
|
try {
|
|
56
|
+
io.stdout.write(connectStartupText(config));
|
|
45
57
|
await this.bridgeFactory(config).run(controller.signal);
|
|
46
58
|
return 0;
|
|
47
59
|
}
|
|
@@ -102,6 +114,15 @@ export class ForgeBridgeCliService {
|
|
|
102
114
|
if (args.command === "cleanup") {
|
|
103
115
|
return await cleanupRepos(args.config, args.mode, args.force, io);
|
|
104
116
|
}
|
|
117
|
+
if (args.command === "mcp") {
|
|
118
|
+
const parsed = parseCompletionMcpArgs(args.argv);
|
|
119
|
+
if (!parsed.ok) {
|
|
120
|
+
io.stderr.write(`${parsed.error}\n`);
|
|
121
|
+
return 2;
|
|
122
|
+
}
|
|
123
|
+
await runCompletionMcpServer(parsed.config);
|
|
124
|
+
return 0;
|
|
125
|
+
}
|
|
105
126
|
let redactionSecrets = readEnvSecretValuesForRedaction();
|
|
106
127
|
try {
|
|
107
128
|
const dispatchJson = await fs.readFile(args.dispatchPath, "utf8");
|
|
@@ -165,19 +186,23 @@ function parseEnvSecretGrant(raw) {
|
|
|
165
186
|
};
|
|
166
187
|
}
|
|
167
188
|
function exactSecrets(dispatch) {
|
|
168
|
-
return dispatch.apiKeySecretGrant?.secret
|
|
189
|
+
return exactSecretValues(dispatch.apiKeySecretGrant?.secret, process.env.FORGE_DEVICE_TOKEN);
|
|
169
190
|
}
|
|
170
191
|
function readEnvSecretValuesForRedaction() {
|
|
192
|
+
const secrets = [];
|
|
171
193
|
const raw = process.env[apiKeySecretGrantEnv];
|
|
172
|
-
if (
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
return [];
|
|
194
|
+
if (raw) {
|
|
195
|
+
try {
|
|
196
|
+
secrets.push(parseEnvSecretGrant(raw).secret);
|
|
197
|
+
}
|
|
198
|
+
catch {
|
|
199
|
+
// Ignore malformed grant env here; command handling will report the parse error.
|
|
200
|
+
}
|
|
180
201
|
}
|
|
202
|
+
return exactSecretValues(...secrets, process.env.FORGE_DEVICE_TOKEN);
|
|
203
|
+
}
|
|
204
|
+
function exactSecretValues(...values) {
|
|
205
|
+
return values.filter((value) => typeof value === "string" && value.length > 0);
|
|
181
206
|
}
|
|
182
207
|
export function parseArgs(argv) {
|
|
183
208
|
if (argv[0] === "connect") {
|
|
@@ -214,6 +239,9 @@ export function parseArgs(argv) {
|
|
|
214
239
|
if (argv[0] === "cleanup") {
|
|
215
240
|
return parseCleanupArgs(argv);
|
|
216
241
|
}
|
|
242
|
+
if (argv[0] === "mcp") {
|
|
243
|
+
return { ok: true, command: "mcp", argv };
|
|
244
|
+
}
|
|
217
245
|
if (argv[0] !== "run-once") {
|
|
218
246
|
return {
|
|
219
247
|
ok: false,
|
|
@@ -460,6 +488,7 @@ function pairedConfig(current, args, claim) {
|
|
|
460
488
|
supportsManagedWorktrees: current.supportsManagedWorktrees ?? true,
|
|
461
489
|
supportsManagedForgeArtifacts: current.supportsManagedForgeArtifacts ?? true,
|
|
462
490
|
supportsRunnerAuthoredPhaseArtifacts: current.supportsRunnerAuthoredPhaseArtifacts ?? true,
|
|
491
|
+
supportsDraftRunOnce: current.supportsDraftRunOnce ?? true,
|
|
463
492
|
repoMappings: current.repoMappings ?? []
|
|
464
493
|
};
|
|
465
494
|
}
|
|
@@ -507,8 +536,74 @@ function normalizePairingCode(code) {
|
|
|
507
536
|
function normalizeServerUrl(serverUrl) {
|
|
508
537
|
return serverUrl.endsWith("/") ? serverUrl : `${serverUrl}/`;
|
|
509
538
|
}
|
|
539
|
+
function isHelpArg(arg) {
|
|
540
|
+
return arg === "help" || arg === "--help" || arg === "-h";
|
|
541
|
+
}
|
|
510
542
|
function usage() {
|
|
511
|
-
return
|
|
543
|
+
return helpText();
|
|
544
|
+
}
|
|
545
|
+
function helpText() {
|
|
546
|
+
const version = defaultBridgeServiceConfig().version ?? forgeBridgeVersion;
|
|
547
|
+
return [
|
|
548
|
+
`Forge Bridge ${version}`,
|
|
549
|
+
"Connect this machine to Forge as a local runner for agent dispatch.",
|
|
550
|
+
"",
|
|
551
|
+
"Usage:",
|
|
552
|
+
" forge-bridge <command> [options]",
|
|
553
|
+
"",
|
|
554
|
+
"Options:",
|
|
555
|
+
" -h, --help Show this help.",
|
|
556
|
+
" -v, --version Print the bridge version.",
|
|
557
|
+
"",
|
|
558
|
+
"Commands:",
|
|
559
|
+
" pair [code] Pair this machine with Forge and save runner credentials.",
|
|
560
|
+
" connect Start the bridge and wait for Forge dispatches.",
|
|
561
|
+
" status Show saved config, runner identity, repos, and detected CLIs.",
|
|
562
|
+
" add-repo Map a Forge project/repo label to a local checkout path.",
|
|
563
|
+
" remove-repo Remove a saved repo mapping.",
|
|
564
|
+
" cleanup Prune or remove managed bridge worktrees.",
|
|
565
|
+
" run-once Internal worker entrypoint used by Forge dispatch.",
|
|
566
|
+
" mcp Internal MCP server for phase completion.",
|
|
567
|
+
"",
|
|
568
|
+
"Examples:",
|
|
569
|
+
" npm install -g @loucompanion/forge-bridge@next",
|
|
570
|
+
" forge-bridge pair <code> --server https://forge.example.com --repo-root \"%USERPROFILE%\\ForgeRepos\"",
|
|
571
|
+
" forge-bridge connect",
|
|
572
|
+
" forge-bridge status --check-updates",
|
|
573
|
+
" forge-bridge add-repo Forge C:\\Users\\you\\src\\Forge",
|
|
574
|
+
"",
|
|
575
|
+
"Setup:",
|
|
576
|
+
" 1. In Forge, open Profile Settings -> Runners -> Pair runner.",
|
|
577
|
+
" 2. Copy the pairing code and command shown there.",
|
|
578
|
+
" 3. Run forge-bridge connect and keep the terminal open.",
|
|
579
|
+
"",
|
|
580
|
+
"Docs:",
|
|
581
|
+
" https://github.com/coleman399/Forge/tree/develop/tools/forge-bridge#readme",
|
|
582
|
+
"",
|
|
583
|
+
].join("\n");
|
|
584
|
+
}
|
|
585
|
+
function connectStartupText(config) {
|
|
586
|
+
const runner = config.runnerId
|
|
587
|
+
? `${config.runnerId}${config.deviceName ? ` (${config.deviceName})` : ""}`
|
|
588
|
+
: "not configured";
|
|
589
|
+
return [
|
|
590
|
+
`Forge Bridge ${config.version}`,
|
|
591
|
+
"Starting runner bridge...",
|
|
592
|
+
"",
|
|
593
|
+
"Startup:",
|
|
594
|
+
` server: ${config.serverUrl || "not configured"}`,
|
|
595
|
+
` runner: ${runner}`,
|
|
596
|
+
` config: ${resolveLocalConfigPath()}`,
|
|
597
|
+
` repo folder: ${config.defaultRepoRoot || "not configured"}`,
|
|
598
|
+
` providers: ${formatList(config.cliProviders)}`,
|
|
599
|
+
` cleanup worktrees: ${config.cleanupWorktrees ? "enabled" : "disabled"}`,
|
|
600
|
+
"",
|
|
601
|
+
"Press Ctrl+C to stop.",
|
|
602
|
+
"",
|
|
603
|
+
].join("\n");
|
|
604
|
+
}
|
|
605
|
+
function formatList(values) {
|
|
606
|
+
return values.length > 0 ? values.join(", ") : "none";
|
|
512
607
|
}
|
|
513
608
|
async function writeJsonAtomic(outputPath, value) {
|
|
514
609
|
await fs.mkdir(path.dirname(outputPath), { recursive: true });
|
|
@@ -620,6 +715,12 @@ function cliProbeCommandsFor(provider) {
|
|
|
620
715
|
];
|
|
621
716
|
}
|
|
622
717
|
async function execVersionCommand(probe, timeout = 2000) {
|
|
718
|
+
const directLaunch = process.platform === "win32"
|
|
719
|
+
? await resolveCursorAgentDirectLaunch(probe.command, probe.args)
|
|
720
|
+
: null;
|
|
721
|
+
if (directLaunch) {
|
|
722
|
+
return await execFileAsync(directLaunch.command, [...directLaunch.args], { timeout });
|
|
723
|
+
}
|
|
623
724
|
try {
|
|
624
725
|
return await execFileAsync(probe.command, probe.args, { timeout });
|
|
625
726
|
}
|
|
@@ -16,6 +16,7 @@ export interface BridgeLocalConfig {
|
|
|
16
16
|
readonly supportsManagedWorktrees?: boolean;
|
|
17
17
|
readonly supportsManagedForgeArtifacts?: boolean;
|
|
18
18
|
readonly supportsRunnerAuthoredPhaseArtifacts?: boolean;
|
|
19
|
+
readonly supportsDraftRunOnce?: boolean;
|
|
19
20
|
}
|
|
20
21
|
export interface LocalRepoMapping extends BridgeRepoMapping {
|
|
21
22
|
readonly label: string;
|
package/dist/cli/local-config.js
CHANGED
|
@@ -53,7 +53,8 @@ export function applyLocalConfigDefaults(config, local) {
|
|
|
53
53
|
supportsApiSecretGrant: local.supportsApiSecretGrant ?? config.supportsApiSecretGrant,
|
|
54
54
|
supportsManagedWorktrees: local.supportsManagedWorktrees ?? config.supportsManagedWorktrees,
|
|
55
55
|
supportsManagedForgeArtifacts: local.supportsManagedForgeArtifacts ?? config.supportsManagedForgeArtifacts,
|
|
56
|
-
supportsRunnerAuthoredPhaseArtifacts: local.supportsRunnerAuthoredPhaseArtifacts ?? config.supportsRunnerAuthoredPhaseArtifacts
|
|
56
|
+
supportsRunnerAuthoredPhaseArtifacts: local.supportsRunnerAuthoredPhaseArtifacts ?? config.supportsRunnerAuthoredPhaseArtifacts,
|
|
57
|
+
supportsDraftRunOnce: local.supportsDraftRunOnce ?? config.supportsDraftRunOnce
|
|
57
58
|
};
|
|
58
59
|
}
|
|
59
60
|
export async function addRepoMapping(label, repoPath) {
|
|
@@ -111,7 +112,8 @@ function normalizeLocalConfig(config) {
|
|
|
111
112
|
supportsApiSecretGrant: config.supportsApiSecretGrant,
|
|
112
113
|
supportsManagedWorktrees: config.supportsManagedWorktrees,
|
|
113
114
|
supportsManagedForgeArtifacts: config.supportsManagedForgeArtifacts,
|
|
114
|
-
supportsRunnerAuthoredPhaseArtifacts: config.supportsRunnerAuthoredPhaseArtifacts
|
|
115
|
+
supportsRunnerAuthoredPhaseArtifacts: config.supportsRunnerAuthoredPhaseArtifacts,
|
|
116
|
+
supportsDraftRunOnce: config.supportsDraftRunOnce
|
|
115
117
|
};
|
|
116
118
|
}
|
|
117
119
|
function emptyConfig() {
|
|
@@ -159,7 +161,8 @@ function normalizeRepoMappings(values) {
|
|
|
159
161
|
repoId: cleanString(mapping.repoId),
|
|
160
162
|
name: cleanString(mapping.name) ?? label,
|
|
161
163
|
remoteUrl: cleanString(mapping.remoteUrl),
|
|
162
|
-
localPath: path.resolve(mapping.localPath)
|
|
164
|
+
localPath: path.resolve(mapping.localPath),
|
|
165
|
+
qaAppLaunchTarget: normalizeQaAppLaunchTarget(mapping.qaAppLaunchTarget)
|
|
163
166
|
};
|
|
164
167
|
})
|
|
165
168
|
.sort((left, right) => left.label.localeCompare(right.label));
|
|
@@ -171,6 +174,22 @@ function normalizeLabel(value) {
|
|
|
171
174
|
}
|
|
172
175
|
return label;
|
|
173
176
|
}
|
|
177
|
+
function normalizeQaAppLaunchTarget(value) {
|
|
178
|
+
if (!value)
|
|
179
|
+
return undefined;
|
|
180
|
+
const routes = cleanList(value.routes);
|
|
181
|
+
const normalized = {
|
|
182
|
+
url: cleanString(value.url),
|
|
183
|
+
command: cleanString(value.command),
|
|
184
|
+
working_directory: cleanString(value.working_directory),
|
|
185
|
+
ready_url: cleanString(value.ready_url),
|
|
186
|
+
routes,
|
|
187
|
+
notes: cleanString(value.notes)
|
|
188
|
+
};
|
|
189
|
+
return normalized.url || normalized.command || normalized.ready_url || routes?.length
|
|
190
|
+
? normalized
|
|
191
|
+
: undefined;
|
|
192
|
+
}
|
|
174
193
|
function cleanString(value) {
|
|
175
194
|
const clean = value?.trim();
|
|
176
195
|
return clean ? clean : undefined;
|
package/dist/config.js
CHANGED
|
@@ -2,7 +2,7 @@ import os from "node:os";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
export function defaultForgeBridgeConfig() {
|
|
4
4
|
return {
|
|
5
|
-
defaultRepoRoot: path.join(os.tmpdir(), "forge-
|
|
5
|
+
defaultRepoRoot: path.join(os.tmpdir(), "forge-repos"),
|
|
6
6
|
cleanupWorktrees: true
|
|
7
7
|
};
|
|
8
8
|
}
|
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";
|
package/dist/session/index.js
CHANGED
|
@@ -1 +1,2 @@
|
|
|
1
1
|
export { BridgeSessionService } from "./session.service.js";
|
|
2
|
+
export { buildCompletionToolServerArgs, codexMcpConfigArgs, completionPhaseFromDispatch, expectedCompletionToolName, prepareCompletionToolRuntime, readCompletionArtifact, writeCompletionArtifact } from "./phase-completion.js";
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
export { parseRunOnceDispatchPacket, failureResult } from "./session.types.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 { SessionEventSink, SessionService } from "./session.base.js";
|
|
3
|
-
export type { ApiKeySecretGrant, DispatchCycleRun, DispatchExecution, DispatchLease, DispatchSession, DispatchRepo, DispatchServerLocalConfig, DispatchWorkItem, RunOnceDispatchPacket, RunOnceResultPacket, RunnerDispatchArtifact, RunnerDispatchEvent, RunnerDispatchSpec, RunnerEventLine } from "./session.types.js";
|
|
5
|
+
export type { ApiKeySecretGrant, DispatchCycleRun, DispatchExecution, DispatchLease, DispatchPullRequestContext, DispatchSession, DispatchRepo, DispatchServerLocalConfig, DispatchWorkItem, RunOnceDispatchPacket, RunOnceResultPacket, RunnerDispatchArtifact, RunnerDispatchEvent, RunnerDispatchSpec, RunnerEventLine } from "./session.types.js";
|
|
4
6
|
export { DeterministicProviderCliService, ProcessBackedProviderCliService, RoutingProviderCliService, buildProviderCliCommand } from "./provider-cli/provider-cli.service.js";
|
|
5
7
|
export type { ProviderCliService } from "./provider-cli/provider-cli.base.js";
|
|
6
8
|
export type { ProviderCliCommand, ProviderProcessRunner, ProviderProcessRunRequest, ProviderProcessRunResult } from "./provider-cli/provider-cli.service.js";
|
|
@@ -1,2 +1,3 @@
|
|
|
1
1
|
export { parseRunOnceDispatchPacket, failureResult } from "./session.types.js";
|
|
2
|
+
export { buildCompletionToolServerArgs, codexMcpConfigArgs, completionPhaseFromDispatch, expectedCompletionToolName, prepareCompletionToolRuntime, readCompletionArtifact, writeCompletionArtifact } from "./phase-completion.js";
|
|
2
3
|
export { DeterministicProviderCliService, ProcessBackedProviderCliService, RoutingProviderCliService, buildProviderCliCommand } from "./provider-cli/provider-cli.service.js";
|