@loucompanion/forge-bridge 0.1.1-dev.7d43998c4521 → 0.1.1-dev.895607f4402a

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.
Files changed (40) hide show
  1. package/README.md +1 -0
  2. package/dist/bridge/bridge.service.js +84 -12
  3. package/dist/bridge/bridge.types.d.ts +16 -0
  4. package/dist/cli/cli.service.d.ts +27 -2
  5. package/dist/cli/cli.service.js +406 -16
  6. package/dist/cli/local-config.d.ts +4 -1
  7. package/dist/cli/local-config.js +60 -8
  8. package/dist/session/index.d.ts +2 -0
  9. package/dist/session/index.js +1 -0
  10. package/dist/session/internals.d.ts +5 -3
  11. package/dist/session/internals.js +2 -1
  12. package/dist/session/phase-completion.d.ts +111 -0
  13. package/dist/session/phase-completion.js +2350 -0
  14. package/dist/session/provider-cli/index.d.ts +2 -2
  15. package/dist/session/provider-cli/index.js +1 -1
  16. package/dist/session/provider-cli/provider-cli.base.d.ts +2 -2
  17. package/dist/session/provider-cli/provider-cli.service.d.ts +48 -3
  18. package/dist/session/provider-cli/provider-cli.service.js +1807 -48
  19. package/dist/session/provider-cli/provider-cli.types.d.ts +2 -0
  20. package/dist/session/provider-cli/provider-usage.d.ts +25 -0
  21. package/dist/session/provider-cli/provider-usage.js +64 -0
  22. package/dist/session/qa-sidecar-validation.d.ts +3 -0
  23. package/dist/session/qa-sidecar-validation.js +44 -0
  24. package/dist/session/session.service.js +57 -12
  25. package/dist/session/session.types.d.ts +66 -0
  26. package/dist/session/session.types.js +165 -2
  27. package/dist/shared/cursor-agent/cursor-agent.launch.d.ts +9 -0
  28. package/dist/shared/cursor-agent/cursor-agent.launch.js +66 -0
  29. package/dist/version.d.ts +1 -1
  30. package/dist/version.js +1 -1
  31. package/dist/worktree/artifact-injector.d.ts +12 -0
  32. package/dist/worktree/artifact-injector.js +133 -0
  33. package/dist/worktree/dependency-warmer.d.ts +32 -0
  34. package/dist/worktree/dependency-warmer.js +297 -0
  35. package/dist/worktree/index.d.ts +2 -0
  36. package/dist/worktree/index.js +1 -0
  37. package/dist/worktree/worktree.service.d.ts +4 -0
  38. package/dist/worktree/worktree.service.js +175 -11
  39. package/dist/worktree/worktree.types.d.ts +7 -1
  40. package/package.json +8 -3
package/README.md CHANGED
@@ -31,6 +31,7 @@ The bridge can also read connection settings from environment variables:
31
31
  - `FORGE_DEVICE_NAME`
32
32
  - `FORGE_REPO_ROOT`
33
33
  - `FORGE_CLI_PROVIDERS`
34
+ - `FORGE_CODEX_MODELS` / `FORGE_CURSOR_MODELS` (optional comma-separated model ids advertised in runner capabilities)
34
35
 
35
36
  ## Repo-local development
36
37
 
@@ -51,7 +51,7 @@ export class ForgeBridgeService {
51
51
  attempt = 0;
52
52
  await this.waitForClose(signal);
53
53
  }
54
- catch (error) {
54
+ catch {
55
55
  if (this.stopped || signal?.aborted) {
56
56
  break;
57
57
  }
@@ -87,11 +87,20 @@ export class ForgeBridgeService {
87
87
  const normalized = this.normalizeDispatch(dispatch);
88
88
  const result = await this.sessions.runOnce(normalized, {
89
89
  emit: async (event) => {
90
- await this.invokeAck("DispatchEvent", {
91
- leaseId,
92
- fencingToken,
93
- event: redactSecrets(event, redactionSecrets)
94
- });
90
+ try {
91
+ await this.invokeAck("DispatchEvent", {
92
+ leaseId,
93
+ fencingToken,
94
+ event: redactSecrets(event, redactionSecrets)
95
+ });
96
+ }
97
+ catch (error) {
98
+ if (isDispatchNoLongerPending(error)) {
99
+ controller.abort(error instanceof Error ? error.message : "Dispatch is no longer pending.");
100
+ return;
101
+ }
102
+ throw error;
103
+ }
95
104
  }
96
105
  }, controller.signal);
97
106
  await this.sendResult(leaseId, fencingToken, redactSecrets(result, redactionSecrets));
@@ -123,6 +132,7 @@ export class ForgeBridgeService {
123
132
  const mapping = findRepoMapping(dispatch, this.config.repoMappings);
124
133
  const localPath = mapping?.localPath?.trim();
125
134
  const repoRoot = localPath ? path.dirname(path.resolve(localPath)) : this.config.defaultRepoRoot;
135
+ const qaLaunchTarget = normalizeQaAppLaunchTarget(mapping?.qaAppLaunchTarget) ?? dispatch.qaBrowser?.launchTarget ?? null;
126
136
  return {
127
137
  ...dispatch,
128
138
  repo: {
@@ -133,7 +143,13 @@ export class ForgeBridgeService {
133
143
  serverLocal: {
134
144
  repoRoot,
135
145
  cleanupWorktrees: this.config.cleanupWorktrees
136
- }
146
+ },
147
+ qaBrowser: dispatch.qaBrowser
148
+ ? {
149
+ ...dispatch.qaBrowser,
150
+ launchTarget: qaLaunchTarget
151
+ }
152
+ : dispatch.qaBrowser
137
153
  };
138
154
  }
139
155
  async sendResult(leaseId, fencingToken, result) {
@@ -142,7 +158,15 @@ export class ForgeBridgeService {
142
158
  fencingToken,
143
159
  result
144
160
  };
145
- await this.invokeAck("DispatchResult", payload);
161
+ try {
162
+ await this.invokeAck("DispatchResult", payload);
163
+ }
164
+ catch (error) {
165
+ if (isDispatchNoLongerPending(error)) {
166
+ return;
167
+ }
168
+ throw error;
169
+ }
146
170
  }
147
171
  startHeartbeat() {
148
172
  this.stopHeartbeat();
@@ -168,7 +192,7 @@ export class ForgeBridgeService {
168
192
  }
169
193
  capabilities() {
170
194
  return {
171
- schemaVersion: 1,
195
+ schemaVersion: 2,
172
196
  activeSessionCount: this.activeSessions.size,
173
197
  cliProviders: this.config.cliProviders,
174
198
  providerStatuses: this.config.providerStatuses,
@@ -176,7 +200,10 @@ export class ForgeBridgeService {
176
200
  repoMappings: this.config.repoMappings,
177
201
  os: this.config.os,
178
202
  sandboxProfiles: this.config.sandboxProfiles,
179
- supportsApiSecretGrant: this.config.supportsApiSecretGrant
203
+ supportsApiSecretGrant: this.config.supportsApiSecretGrant,
204
+ supportsManagedWorktrees: this.config.supportsManagedWorktrees,
205
+ supportsManagedForgeArtifacts: this.config.supportsManagedForgeArtifacts,
206
+ supportsRunnerAuthoredPhaseArtifacts: this.config.supportsRunnerAuthoredPhaseArtifacts
180
207
  };
181
208
  }
182
209
  async invokeAck(methodName, payload) {
@@ -186,7 +213,7 @@ export class ForgeBridgeService {
186
213
  }
187
214
  const ack = await connection.invoke(methodName, payload);
188
215
  if (!ack.accepted) {
189
- throw new Error(ack.message ?? ack.code ?? `${methodName} callback was rejected.`);
216
+ throw new BridgeCallbackRejectedError(ack.code ?? null, ack.message ?? ack.code ?? `${methodName} callback was rejected.`);
190
217
  }
191
218
  return ack;
192
219
  }
@@ -204,6 +231,20 @@ export class ForgeBridgeService {
204
231
  }
205
232
  }
206
233
  }
234
+ class BridgeCallbackRejectedError extends Error {
235
+ code;
236
+ constructor(code, message) {
237
+ super(message);
238
+ this.code = code;
239
+ this.name = "BridgeCallbackRejectedError";
240
+ }
241
+ }
242
+ function isDispatchNoLongerPending(error) {
243
+ if (error instanceof BridgeCallbackRejectedError) {
244
+ return error.code === "bridge.dispatch_not_pending" || error.code === "bridge.stale_dispatch";
245
+ }
246
+ return error instanceof Error && error.message === "Dispatch is no longer pending.";
247
+ }
207
248
  export class SignalRBridgeConnectionFactory {
208
249
  create(config) {
209
250
  return new SignalRBridgeConnection(config);
@@ -264,12 +305,15 @@ export function defaultBridgeServiceConfig(overrides = {}) {
264
305
  defaultRepoRoot: process.env.FORGE_REPO_ROOT ?? defaults.defaultRepoRoot,
265
306
  cleanupWorktrees: process.env.FORGE_CLEANUP_WORKTREES !== "false" && defaults.cleanupWorktrees,
266
307
  cliProviders,
267
- providerStatuses: Object.fromEntries(cliProviders.map((provider) => [provider, { signedIn: true, version: "" }])),
308
+ providerStatuses: providerStatusesFor(cliProviders),
268
309
  projectLabels: splitCsv(process.env.FORGE_PROJECT_LABELS ?? ""),
269
310
  repoMappings: [],
270
311
  os: process.platform,
271
312
  sandboxProfiles: ["default"],
272
313
  supportsApiSecretGrant: true,
314
+ supportsManagedWorktrees: true,
315
+ supportsManagedForgeArtifacts: true,
316
+ supportsRunnerAuthoredPhaseArtifacts: true,
273
317
  version: process.env.FORGE_BRIDGE_VERSION ?? forgeBridgeVersion,
274
318
  protocolVersion: 1,
275
319
  minServerVersion: null,
@@ -294,6 +338,13 @@ function normalizeServerUrl(serverUrl) {
294
338
  function positiveFiniteNumber(value, fallback) {
295
339
  return Number.isFinite(value) && value > 0 ? value : fallback;
296
340
  }
341
+ function providerStatusesFor(providers) {
342
+ return Object.fromEntries(providers.map((provider) => [provider, { signedIn: true, version: "", models: configuredModelsFor(provider) }]));
343
+ }
344
+ function configuredModelsFor(provider) {
345
+ const envName = `FORGE_${provider.replace(/[^a-zA-Z0-9]/g, "_").toUpperCase()}_MODELS`;
346
+ return splitCsv(process.env[envName] ?? "");
347
+ }
297
348
  function splitCsv(value) {
298
349
  return value
299
350
  .split(",")
@@ -310,6 +361,27 @@ function findRepoMapping(dispatch, mappings) {
310
361
  (!!mapping.remoteUrl?.trim() && equalsIgnoreCase(mapping.remoteUrl, dispatch.repo.remoteUrl)));
311
362
  });
312
363
  }
364
+ function normalizeQaAppLaunchTarget(target) {
365
+ if (!target) {
366
+ return null;
367
+ }
368
+ const routes = [...new Set((target.routes ?? []).map((route) => route.trim()).filter(Boolean))];
369
+ const normalized = {
370
+ url: cleanString(target.url),
371
+ command: cleanString(target.command),
372
+ working_directory: cleanString(target.working_directory),
373
+ ready_url: cleanString(target.ready_url),
374
+ routes,
375
+ notes: cleanString(target.notes)
376
+ };
377
+ return normalized.url || normalized.command || normalized.ready_url || routes.length > 0
378
+ ? normalized
379
+ : null;
380
+ }
381
+ function cleanString(value) {
382
+ const clean = value?.trim();
383
+ return clean ? clean : null;
384
+ }
313
385
  function equalsIgnoreCase(left, right) {
314
386
  return !!left?.trim() && !!right?.trim() && left.trim().toLowerCase() === right.trim().toLowerCase();
315
387
  }
@@ -13,6 +13,9 @@ export interface BridgeServiceConfig {
13
13
  readonly os: string;
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
  readonly version: string;
17
20
  readonly protocolVersion: number;
18
21
  readonly minServerVersion?: string | null;
@@ -24,10 +27,20 @@ export interface BridgeRepoMapping {
24
27
  readonly name?: string | null;
25
28
  readonly remoteUrl?: string | null;
26
29
  readonly localPath?: string | null;
30
+ readonly qaAppLaunchTarget?: QaAppLaunchTarget | null;
31
+ }
32
+ export interface QaAppLaunchTarget {
33
+ readonly url?: string | null;
34
+ readonly command?: string | null;
35
+ readonly working_directory?: string | null;
36
+ readonly ready_url?: string | null;
37
+ readonly routes?: readonly string[] | null;
38
+ readonly notes?: string | null;
27
39
  }
28
40
  export interface BridgeProviderStatus {
29
41
  readonly signedIn?: boolean | null;
30
42
  readonly version?: string | null;
43
+ readonly models?: readonly string[] | null;
31
44
  }
32
45
  export interface BridgeCapabilities {
33
46
  readonly schemaVersion?: number | null;
@@ -39,6 +52,9 @@ export interface BridgeCapabilities {
39
52
  readonly os?: string | null;
40
53
  readonly sandboxProfiles?: readonly string[] | null;
41
54
  readonly supportsApiSecretGrant?: boolean | null;
55
+ readonly supportsManagedWorktrees?: boolean | null;
56
+ readonly supportsManagedForgeArtifacts?: boolean | null;
57
+ readonly supportsRunnerAuthoredPhaseArtifacts?: boolean | null;
42
58
  }
43
59
  export interface BridgeHelloRequest {
44
60
  readonly version: string;
@@ -5,7 +5,8 @@ import type { CliIo, CliService } from "./cli.base.js";
5
5
  export declare class ForgeBridgeCliService implements CliService {
6
6
  private readonly sessions;
7
7
  private readonly bridgeFactory;
8
- constructor(sessions?: SessionService, bridgeFactory?: (config: BridgeServiceConfig) => BridgeService);
8
+ private readonly pairRunner;
9
+ constructor(sessions?: SessionService, bridgeFactory?: (config: BridgeServiceConfig) => BridgeService, pairRunner?: PairRunnerClient);
9
10
  run(argv: readonly string[], io?: CliIo): Promise<number>;
10
11
  }
11
12
  export declare function parseArgs(argv: readonly string[]): {
@@ -13,11 +14,18 @@ export declare function parseArgs(argv: readonly string[]): {
13
14
  readonly command: "run-once";
14
15
  readonly dispatchPath: string;
15
16
  readonly resultPath: string;
17
+ } | {
18
+ readonly ok: true;
19
+ readonly command: "mcp";
20
+ readonly argv: readonly string[];
16
21
  } | {
17
22
  readonly ok: true;
18
23
  readonly command: "connect";
19
24
  readonly config: BridgeServiceConfig;
20
- } | {
25
+ } | ({
26
+ readonly ok: true;
27
+ readonly command: "pair";
28
+ } & PairArgs) | {
21
29
  readonly ok: true;
22
30
  readonly command: "add-repo";
23
31
  readonly label: string;
@@ -41,5 +49,22 @@ export declare function parseArgs(argv: readonly string[]): {
41
49
  readonly ok: false;
42
50
  readonly error: string;
43
51
  };
52
+ interface PairArgs {
53
+ readonly serverUrl: string;
54
+ readonly code?: string;
55
+ readonly name: string;
56
+ readonly fingerprint: string;
57
+ readonly repoRoot?: string;
58
+ readonly cleanupWorktrees: boolean;
59
+ readonly cliProviders: readonly string[];
60
+ readonly projectLabels: readonly string[];
61
+ }
62
+ interface PairRunnerClaim {
63
+ readonly runnerId: string;
64
+ readonly deviceToken: string;
65
+ }
66
+ type PairRunnerClient = (args: PairArgs) => Promise<PairRunnerClaim>;
67
+ export declare function parseCodexModelCatalog(raw: string): string[];
68
+ export declare function parseCursorModelList(raw: string): string[];
44
69
  type CleanupMode = "stale" | "merged" | "all";
45
70
  export {};