@loucompanion/forge-bridge 0.1.1-dev.8dcbc6245e51 → 0.1.1-dev.9219f8477b8a

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 (37) hide show
  1. package/dist/bridge/bridge.service.d.ts +1 -0
  2. package/dist/bridge/bridge.service.js +219 -25
  3. package/dist/bridge/bridge.types.d.ts +19 -2
  4. package/dist/cli/cli.service.d.ts +27 -2
  5. package/dist/cli/cli.service.js +630 -25
  6. package/dist/cli/local-config.d.ts +4 -0
  7. package/dist/cli/local-config.js +42 -4
  8. package/dist/config.js +1 -1
  9. package/dist/session/index.d.ts +2 -0
  10. package/dist/session/index.js +1 -0
  11. package/dist/session/internals.d.ts +5 -3
  12. package/dist/session/internals.js +2 -1
  13. package/dist/session/phase-completion.d.ts +126 -0
  14. package/dist/session/phase-completion.js +3233 -0
  15. package/dist/session/provider-cli/index.d.ts +2 -2
  16. package/dist/session/provider-cli/index.js +1 -1
  17. package/dist/session/provider-cli/provider-cli.base.d.ts +5 -3
  18. package/dist/session/provider-cli/provider-cli.service.d.ts +64 -4
  19. package/dist/session/provider-cli/provider-cli.service.js +2787 -51
  20. package/dist/session/provider-cli/provider-cli.types.d.ts +7 -0
  21. package/dist/session/provider-cli/provider-usage.d.ts +25 -0
  22. package/dist/session/provider-cli/provider-usage.js +64 -0
  23. package/dist/session/qa-sidecar-validation.d.ts +3 -0
  24. package/dist/session/qa-sidecar-validation.js +44 -0
  25. package/dist/session/session.service.js +62 -11
  26. package/dist/session/session.types.d.ts +132 -0
  27. package/dist/session/session.types.js +261 -2
  28. package/dist/shared/cursor-agent/cursor-agent.launch.d.ts +9 -0
  29. package/dist/shared/cursor-agent/cursor-agent.launch.js +66 -0
  30. package/dist/version.d.ts +1 -1
  31. package/dist/version.js +1 -1
  32. package/dist/worktree/artifact-injector.d.ts +2 -1
  33. package/dist/worktree/artifact-injector.js +125 -9
  34. package/dist/worktree/worktree.service.d.ts +3 -0
  35. package/dist/worktree/worktree.service.js +239 -8
  36. package/dist/worktree/worktree.types.d.ts +8 -1
  37. 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;
@@ -1,4 +1,5 @@
1
1
  import { HubConnectionBuilder, LogLevel } from "@microsoft/signalr";
2
+ import fs from "node:fs";
2
3
  import path from "node:path";
3
4
  import { defaultForgeBridgeConfig } from "../config.js";
4
5
  import { redactSecrets, redactText } from "../shared/redaction/redaction.js";
@@ -48,8 +49,14 @@ export class ForgeBridgeService {
48
49
  while (!this.stopped && !signal?.aborted) {
49
50
  try {
50
51
  await this.connectOnce();
52
+ if (attempt > 0) {
53
+ logBridgeStatus("Connected to Forge. Waiting for dispatches...");
54
+ }
51
55
  attempt = 0;
52
56
  await this.waitForClose(signal);
57
+ if (!this.stopped && !signal?.aborted) {
58
+ logBridgeStatus("Connection closed. Reconnecting...");
59
+ }
53
60
  }
54
61
  catch (error) {
55
62
  if (this.stopped || signal?.aborted) {
@@ -57,6 +64,7 @@ export class ForgeBridgeService {
57
64
  }
58
65
  const backoff = this.config.reconnectBackoffMs[Math.min(attempt, this.config.reconnectBackoffMs.length - 1)] ?? 5000;
59
66
  attempt += 1;
67
+ logBridgeStatus(`Connect attempt ${attempt} failed: ${errorMessage(error)}. Retrying in ${formatDelay(backoff)}...`);
60
68
  await delay(backoff, signal);
61
69
  }
62
70
  }
@@ -73,10 +81,14 @@ export class ForgeBridgeService {
73
81
  this.abortActiveSessions("bridge stopping");
74
82
  await this.stopConnection();
75
83
  }
76
- async handleDispatch(dispatch) {
84
+ async handleDispatch(rawDispatch) {
85
+ const dispatch = this.parseDispatchOrReport(rawDispatch);
86
+ if (!dispatch) {
87
+ return;
88
+ }
77
89
  const leaseId = dispatch.lease.id;
78
90
  const fencingToken = dispatch.lease.fencingToken;
79
- const redactionSecrets = exactSecrets(dispatch);
91
+ const redactionSecrets = exactSecrets(dispatch, this.config);
80
92
  if (this.activeSessions.has(leaseId)) {
81
93
  await this.sendResult(leaseId, fencingToken, failureResult("Dispatch is already active on this bridge.")).catch(() => undefined);
82
94
  return;
@@ -87,11 +99,20 @@ export class ForgeBridgeService {
87
99
  const normalized = this.normalizeDispatch(dispatch);
88
100
  const result = await this.sessions.runOnce(normalized, {
89
101
  emit: async (event) => {
90
- await this.invokeAck("DispatchEvent", {
91
- leaseId,
92
- fencingToken,
93
- event: redactSecrets(event, redactionSecrets)
94
- });
102
+ try {
103
+ await this.invokeAck("DispatchEvent", {
104
+ leaseId,
105
+ fencingToken,
106
+ event: redactSecrets(event, redactionSecrets)
107
+ });
108
+ }
109
+ catch (error) {
110
+ if (isDispatchNoLongerPending(error)) {
111
+ controller.abort(error instanceof Error ? error.message : "Dispatch is no longer pending.");
112
+ return;
113
+ }
114
+ throw error;
115
+ }
95
116
  }
96
117
  }, controller.signal);
97
118
  await this.sendResult(leaseId, fencingToken, redactSecrets(result, redactionSecrets));
@@ -106,6 +127,27 @@ export class ForgeBridgeService {
106
127
  this.activeSessions.delete(leaseId);
107
128
  }
108
129
  }
130
+ parseDispatchOrReport(rawDispatch) {
131
+ try {
132
+ return parseRunOnceDispatchPacket(rawDispatch);
133
+ }
134
+ catch (error) {
135
+ const identifiers = readDispatchIdentifiers(rawDispatch);
136
+ if (!identifiers) {
137
+ return null;
138
+ }
139
+ const summary = invalidDispatchSummary(error);
140
+ void this.sendResult(identifiers.leaseId, identifiers.fencingToken, failureResult(summary, [
141
+ {
142
+ role: "runner",
143
+ kind: "error",
144
+ content: summary,
145
+ eventType: "invalid_dispatch"
146
+ }
147
+ ])).catch(() => undefined);
148
+ return null;
149
+ }
150
+ }
109
151
  async handleCancel(request) {
110
152
  const active = this.activeSessions.get(request.leaseId);
111
153
  if (!active || active.fencingToken !== request.fencingToken) {
@@ -120,9 +162,10 @@ export class ForgeBridgeService {
120
162
  this.activeSessions.clear();
121
163
  }
122
164
  normalizeDispatch(dispatch) {
123
- const mapping = findRepoMapping(dispatch, this.config.repoMappings);
165
+ const mapping = findRepoMapping(dispatch, repoMappingsFor(this.config));
124
166
  const localPath = mapping?.localPath?.trim();
125
167
  const repoRoot = localPath ? path.dirname(path.resolve(localPath)) : this.config.defaultRepoRoot;
168
+ const qaLaunchTarget = normalizeQaAppLaunchTarget(mapping?.qaAppLaunchTarget) ?? dispatch.qaBrowser?.launchTarget ?? null;
126
169
  return {
127
170
  ...dispatch,
128
171
  repo: {
@@ -133,7 +176,13 @@ export class ForgeBridgeService {
133
176
  serverLocal: {
134
177
  repoRoot,
135
178
  cleanupWorktrees: this.config.cleanupWorktrees
136
- }
179
+ },
180
+ qaBrowser: dispatch.qaBrowser
181
+ ? {
182
+ ...dispatch.qaBrowser,
183
+ launchTarget: qaLaunchTarget
184
+ }
185
+ : dispatch.qaBrowser
137
186
  };
138
187
  }
139
188
  async sendResult(leaseId, fencingToken, result) {
@@ -142,7 +191,15 @@ export class ForgeBridgeService {
142
191
  fencingToken,
143
192
  result
144
193
  };
145
- await this.invokeAck("DispatchResult", payload);
194
+ try {
195
+ await this.invokeAck("DispatchResult", payload);
196
+ }
197
+ catch (error) {
198
+ if (isDispatchNoLongerPending(error)) {
199
+ return;
200
+ }
201
+ throw error;
202
+ }
146
203
  }
147
204
  startHeartbeat() {
148
205
  this.stopHeartbeat();
@@ -173,10 +230,14 @@ export class ForgeBridgeService {
173
230
  cliProviders: this.config.cliProviders,
174
231
  providerStatuses: this.config.providerStatuses,
175
232
  projectLabels: this.config.projectLabels,
176
- repoMappings: this.config.repoMappings,
233
+ repoMappings: repoMappingsFor(this.config),
177
234
  os: this.config.os,
178
235
  sandboxProfiles: this.config.sandboxProfiles,
179
- supportsApiSecretGrant: this.config.supportsApiSecretGrant
236
+ supportsApiSecretGrant: this.config.supportsApiSecretGrant,
237
+ supportsManagedWorktrees: this.config.supportsManagedWorktrees,
238
+ supportsManagedForgeArtifacts: this.config.supportsManagedForgeArtifacts,
239
+ supportsRunnerAuthoredPhaseArtifacts: this.config.supportsRunnerAuthoredPhaseArtifacts,
240
+ supportsDraftRunOnce: this.config.supportsDraftRunOnce
180
241
  };
181
242
  }
182
243
  async invokeAck(methodName, payload) {
@@ -186,7 +247,7 @@ export class ForgeBridgeService {
186
247
  }
187
248
  const ack = await connection.invoke(methodName, payload);
188
249
  if (!ack.accepted) {
189
- throw new Error(ack.message ?? ack.code ?? `${methodName} callback was rejected.`);
250
+ throw new BridgeCallbackRejectedError(ack.code ?? null, ack.message ?? ack.code ?? `${methodName} callback was rejected.`);
190
251
  }
191
252
  return ack;
192
253
  }
@@ -204,6 +265,20 @@ export class ForgeBridgeService {
204
265
  }
205
266
  }
206
267
  }
268
+ class BridgeCallbackRejectedError extends Error {
269
+ code;
270
+ constructor(code, message) {
271
+ super(message);
272
+ this.code = code;
273
+ this.name = "BridgeCallbackRejectedError";
274
+ }
275
+ }
276
+ function isDispatchNoLongerPending(error) {
277
+ if (error instanceof BridgeCallbackRejectedError) {
278
+ return error.code === "bridge.dispatch_not_pending" || error.code === "bridge.stale_dispatch";
279
+ }
280
+ return error instanceof Error && error.message === "Dispatch is no longer pending.";
281
+ }
207
282
  export class SignalRBridgeConnectionFactory {
208
283
  create(config) {
209
284
  return new SignalRBridgeConnection(config);
@@ -234,14 +309,7 @@ export class SignalRBridgeConnection {
234
309
  }
235
310
  onDispatch(handler) {
236
311
  this.connection.on("Dispatch", (raw) => {
237
- let dispatch;
238
- try {
239
- dispatch = parseRunOnceDispatchPacket(raw);
240
- }
241
- catch {
242
- return;
243
- }
244
- void handler(dispatch).catch(() => undefined);
312
+ void handler(raw).catch(() => undefined);
245
313
  });
246
314
  }
247
315
  onCancel(handler) {
@@ -268,8 +336,12 @@ export function defaultBridgeServiceConfig(overrides = {}) {
268
336
  projectLabels: splitCsv(process.env.FORGE_PROJECT_LABELS ?? ""),
269
337
  repoMappings: [],
270
338
  os: process.platform,
271
- sandboxProfiles: ["default"],
339
+ sandboxProfiles: overrides.sandboxProfiles,
272
340
  supportsApiSecretGrant: true,
341
+ supportsManagedWorktrees: true,
342
+ supportsManagedForgeArtifacts: true,
343
+ supportsRunnerAuthoredPhaseArtifacts: true,
344
+ supportsDraftRunOnce: true,
273
345
  version: process.env.FORGE_BRIDGE_VERSION ?? forgeBridgeVersion,
274
346
  protocolVersion: 1,
275
347
  minServerVersion: null,
@@ -279,6 +351,7 @@ export function defaultBridgeServiceConfig(overrides = {}) {
279
351
  };
280
352
  return {
281
353
  ...config,
354
+ sandboxProfiles: config.sandboxProfiles ?? defaultSandboxProfiles(config.cliProviders),
282
355
  heartbeatIntervalMs: positiveFiniteNumber(config.heartbeatIntervalMs, 30_000)
283
356
  };
284
357
  }
@@ -297,6 +370,14 @@ function positiveFiniteNumber(value, fallback) {
297
370
  function providerStatusesFor(providers) {
298
371
  return Object.fromEntries(providers.map((provider) => [provider, { signedIn: true, version: "", models: configuredModelsFor(provider) }]));
299
372
  }
373
+ function defaultSandboxProfiles(providers, platform = process.platform) {
374
+ if (!providers.some((provider) => provider.toLowerCase() === "codex")) {
375
+ return ["none"];
376
+ }
377
+ return platform === "win32"
378
+ ? ["danger-full-access"]
379
+ : ["provider-workspace-write"];
380
+ }
300
381
  function configuredModelsFor(provider) {
301
382
  const envName = `FORGE_${provider.replace(/[^a-zA-Z0-9]/g, "_").toUpperCase()}_MODELS`;
302
383
  return splitCsv(process.env[envName] ?? "");
@@ -307,6 +388,62 @@ function splitCsv(value) {
307
388
  .map((entry) => entry.trim())
308
389
  .filter((entry) => entry.length > 0);
309
390
  }
391
+ function repoMappingsFor(config) {
392
+ const explicit = [...config.repoMappings];
393
+ const discovered = discoverRepoRootMappings(config.defaultRepoRoot);
394
+ if (explicit.length === 0) {
395
+ return discovered;
396
+ }
397
+ const explicitKeys = new Set(explicit.flatMap(mappingKeys));
398
+ return [
399
+ ...explicit,
400
+ ...discovered.filter((mapping) => !mappingKeys(mapping).some((key) => explicitKeys.has(key)))
401
+ ];
402
+ }
403
+ function discoverRepoRootMappings(defaultRepoRoot) {
404
+ const root = defaultRepoRoot.trim();
405
+ if (!root) {
406
+ return [];
407
+ }
408
+ const resolvedRoot = path.resolve(root);
409
+ const mappings = [];
410
+ if (isGitCheckout(resolvedRoot)) {
411
+ mappings.push({ name: path.basename(resolvedRoot), localPath: resolvedRoot });
412
+ }
413
+ let entries;
414
+ try {
415
+ entries = fs.readdirSync(resolvedRoot, { withFileTypes: true });
416
+ }
417
+ catch {
418
+ return mappings;
419
+ }
420
+ for (const entry of entries) {
421
+ if (!entry.isDirectory()) {
422
+ continue;
423
+ }
424
+ const localPath = path.join(resolvedRoot, entry.name);
425
+ if (isGitCheckout(localPath)) {
426
+ mappings.push({ name: entry.name, localPath });
427
+ }
428
+ }
429
+ return mappings.sort((a, b) => (a.name ?? "").localeCompare(b.name ?? ""));
430
+ }
431
+ function isGitCheckout(localPath) {
432
+ try {
433
+ return fs.existsSync(path.join(localPath, ".git"));
434
+ }
435
+ catch {
436
+ return false;
437
+ }
438
+ }
439
+ function mappingKeys(mapping) {
440
+ return [
441
+ mapping.repoId ? `id:${mapping.repoId.toLowerCase()}` : null,
442
+ mapping.name ? `name:${mapping.name.trim().toLowerCase()}` : null,
443
+ mapping.remoteUrl ? `remote:${mapping.remoteUrl.trim().toLowerCase()}` : null,
444
+ mapping.localPath ? `path:${path.resolve(mapping.localPath).toLowerCase()}` : null
445
+ ].filter((key) => Boolean(key));
446
+ }
310
447
  function findRepoMapping(dispatch, mappings) {
311
448
  return mappings.find((mapping) => {
312
449
  if (!mapping.localPath?.trim()) {
@@ -317,16 +454,70 @@ function findRepoMapping(dispatch, mappings) {
317
454
  (!!mapping.remoteUrl?.trim() && equalsIgnoreCase(mapping.remoteUrl, dispatch.repo.remoteUrl)));
318
455
  });
319
456
  }
457
+ function normalizeQaAppLaunchTarget(target) {
458
+ if (!target) {
459
+ return null;
460
+ }
461
+ const routes = [...new Set((target.routes ?? []).map((route) => route.trim()).filter(Boolean))];
462
+ const normalized = {
463
+ url: cleanString(target.url),
464
+ command: cleanString(target.command),
465
+ working_directory: cleanString(target.working_directory),
466
+ ready_url: cleanString(target.ready_url),
467
+ routes,
468
+ notes: cleanString(target.notes)
469
+ };
470
+ return normalized.url || normalized.command || normalized.ready_url || routes.length > 0
471
+ ? normalized
472
+ : null;
473
+ }
474
+ function cleanString(value) {
475
+ const clean = value?.trim();
476
+ return clean ? clean : null;
477
+ }
320
478
  function equalsIgnoreCase(left, right) {
321
479
  return !!left?.trim() && !!right?.trim() && left.trim().toLowerCase() === right.trim().toLowerCase();
322
480
  }
481
+ function readDispatchIdentifiers(rawDispatch) {
482
+ if (!rawDispatch || typeof rawDispatch !== "object" || Array.isArray(rawDispatch)) {
483
+ return null;
484
+ }
485
+ const packet = rawDispatch;
486
+ const rawLease = packet.lease ?? packet.Lease;
487
+ if (!rawLease || typeof rawLease !== "object" || Array.isArray(rawLease)) {
488
+ return null;
489
+ }
490
+ const lease = rawLease;
491
+ const leaseId = readString(lease, "id", "Id");
492
+ const fencingToken = readString(lease, "fencingToken", "FencingToken");
493
+ return leaseId && fencingToken ? { leaseId, fencingToken } : null;
494
+ }
495
+ function readString(source, camelKey, pascalKey) {
496
+ const value = source[camelKey] ?? source[pascalKey];
497
+ return typeof value === "string" && value.trim() ? value : null;
498
+ }
499
+ function invalidDispatchSummary(error) {
500
+ const detail = error instanceof Error ? error.message : String(error);
501
+ return `Invalid bridge dispatch packet: ${detail}`;
502
+ }
503
+ function errorMessage(error) {
504
+ return error instanceof Error ? error.message : String(error);
505
+ }
506
+ function formatDelay(ms) {
507
+ if (ms >= 1000 && ms % 1000 === 0) {
508
+ return `${ms / 1000}s`;
509
+ }
510
+ return `${ms}ms`;
511
+ }
512
+ function logBridgeStatus(message) {
513
+ process.stderr.write(`[${new Date().toISOString()}] ${message}\n`);
514
+ }
323
515
  function delay(ms, signal) {
324
516
  if (signal?.aborted) {
325
517
  return Promise.resolve();
326
518
  }
327
519
  return new Promise((resolve) => {
328
520
  const timeout = setTimeout(resolve, ms);
329
- timeout.unref?.();
330
521
  signal?.addEventListener("abort", () => {
331
522
  clearTimeout(timeout);
332
523
  resolve();
@@ -342,6 +533,9 @@ function abortPromise(signal) {
342
533
  }
343
534
  return new Promise((resolve) => signal.addEventListener("abort", () => resolve(), { once: true }));
344
535
  }
345
- function exactSecrets(dispatch) {
346
- return dispatch.apiKeySecretGrant?.secret ? [dispatch.apiKeySecretGrant.secret] : [];
536
+ function exactSecrets(dispatch, config) {
537
+ return [
538
+ dispatch.apiKeySecretGrant?.secret,
539
+ config.deviceToken
540
+ ].filter((secret) => typeof secret === "string" && secret.length > 0);
347
541
  }
@@ -1,4 +1,4 @@
1
- import type { RunOnceDispatchPacket, RunOnceResultPacket, RunnerDispatchEvent } from "../session/session.types.js";
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;
@@ -13,6 +13,10 @@ 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;
19
+ readonly supportsDraftRunOnce: boolean;
16
20
  readonly version: string;
17
21
  readonly protocolVersion: number;
18
22
  readonly minServerVersion?: string | null;
@@ -24,6 +28,15 @@ export interface BridgeRepoMapping {
24
28
  readonly name?: string | null;
25
29
  readonly remoteUrl?: string | null;
26
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;
27
40
  }
28
41
  export interface BridgeProviderStatus {
29
42
  readonly signedIn?: boolean | null;
@@ -40,6 +53,10 @@ export interface BridgeCapabilities {
40
53
  readonly os?: string | null;
41
54
  readonly sandboxProfiles?: readonly string[] | null;
42
55
  readonly supportsApiSecretGrant?: boolean | null;
56
+ readonly supportsManagedWorktrees?: boolean | null;
57
+ readonly supportsManagedForgeArtifacts?: boolean | null;
58
+ readonly supportsRunnerAuthoredPhaseArtifacts?: boolean | null;
59
+ readonly supportsDraftRunOnce?: boolean | null;
43
60
  }
44
61
  export interface BridgeHelloRequest {
45
62
  readonly version: string;
@@ -73,5 +90,5 @@ export interface BridgeCancelRequest {
73
90
  readonly fencingToken: string;
74
91
  readonly reason?: string | null;
75
92
  }
76
- export type BridgeDispatchHandler = (dispatch: RunOnceDispatchPacket) => Promise<void>;
93
+ export type BridgeDispatchHandler = (dispatch: unknown) => Promise<void>;
77
94
  export type BridgeCancelHandler = (request: BridgeCancelRequest) => Promise<void>;
@@ -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 {};