@gakim-digital/dexter-bridge 0.5.20 → 0.11.0

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/src/cli.js CHANGED
@@ -1,11 +1,14 @@
1
1
  import os from 'node:os';
2
+ import path from 'node:path';
2
3
  import readline from 'node:readline/promises';
4
+ import { spawn } from 'node:child_process';
3
5
  import {
4
6
  BRIDGE_BUILD_FINGERPRINT,
5
7
  BRIDGE_CAPABILITIES,
6
8
  BRIDGE_VERSION,
7
9
  clearConfig,
8
10
  defaultConfigDir,
11
+ normalizeBridgeProduct,
9
12
  normalizeAgentName,
10
13
  readConfig,
11
14
  resolveAgentName,
@@ -13,7 +16,7 @@ import {
13
16
  resolveCompanionModelName,
14
17
  saveConfigPatch,
15
18
  } from './config.js';
16
- import { checkAllAgents, executeRun } from './agent.js';
19
+ import { checkAgentAvailability, checkAllAgents, executeRun } from './agent.js';
17
20
  import {
18
21
  claimPairing,
19
22
  DexterBridgeApiError,
@@ -26,22 +29,30 @@ import {
26
29
  logLocationHint,
27
30
  } from './logger.js';
28
31
  import { createLocalAgentAdapter } from './providers/index.js';
32
+ import {
33
+ discoverRuntimeProfiles,
34
+ normalizeRuntimeSelection,
35
+ publicRuntimeProfile,
36
+ runtimeDriver,
37
+ runtimeProfileMap,
38
+ } from './runtimeProfiles.js';
29
39
 
30
40
  function usage() {
31
41
  return [
32
- 'Dexter Bridge',
42
+ 'Local Agent Bridge for Dexter and InstaWebAI',
33
43
  '',
34
44
  'Usage:',
35
- ' dexter-bridge connect [code-or-token] [--agent claude-code|codex|dry-run] [--model <model>] [--once] [--api <url>]',
45
+ ' dexter-bridge connect [code-or-token] --runtime codex|claude-code|opencode [--once] [--api <url>]',
36
46
  ' dexter-bridge pair <code-or-token> [--api <url>]',
37
- ' dexter-bridge start [--agent claude-code|codex|dry-run] [--model <model>] [--once] [--api <url>]',
38
- ' dexter-bridge status [--api <url>]',
39
- ' dexter-bridge doctor',
47
+ ' dexter-bridge start [--runtime codex|claude-code|opencode] [--once] [--api <url>]',
48
+ ' dexter-bridge status [--runtime codex|claude-code|opencode] [--api <url>]',
49
+ ' dexter-bridge doctor [--runtime codex|claude-code|opencode]',
40
50
  ' dexter-bridge logout',
41
51
  '',
42
52
  'Environment:',
43
53
  ' DEXTER_API_URL API base URL, e.g. https://api.example.com/iwm-api/0.0.1',
44
- ' DEXTER_BRIDGE_AGENT claude-code, codex, or dry-run',
54
+ ' DEXTER_BRIDGE_RUNTIME codex, claude-code, or opencode',
55
+ ' DEXTER_BRIDGE_AGENT claude-code, codex, opencode, or dry-run',
45
56
  ' DEXTER_BRIDGE_MODEL Companion model, e.g. claude-code:sonnet or codex:gpt-5.5',
46
57
  ' DEXTER_BRIDGE_CLAUDE_BIN Claude CLI binary name/path',
47
58
  ' DEXTER_BRIDGE_CLAUDE_ARGS Claude CLI args, default: -p',
@@ -51,6 +62,39 @@ function usage() {
51
62
  ].join('\n');
52
63
  }
53
64
 
65
+ function requestedRuntime(flags = {}, config = {}, env = process.env) {
66
+ const raw = flags.runtime || env.DEXTER_BRIDGE_RUNTIME || config.runtimeSelection;
67
+ if (!raw) return null;
68
+ const runtime = normalizeRuntimeSelection(raw);
69
+ if (runtime) return runtime;
70
+ const error = new Error('Runtime must be codex, claude-code, or opencode.');
71
+ error.code = 'DEXTER_RUNTIME_INVALID';
72
+ error.exitCode = 2;
73
+ throw error;
74
+ }
75
+
76
+ function runtimeAgent(runtime) {
77
+ if (runtime === 'codex') return 'codex';
78
+ if (runtime === 'claude-code') return 'claude-code';
79
+ return 'dry-run';
80
+ }
81
+
82
+ function runtimeLabel(runtime) {
83
+ if (runtime === 'codex') return 'Codex';
84
+ if (runtime === 'claude-code') return 'Claude Code';
85
+ if (runtime === 'opencode') return 'OpenCode';
86
+ return 'Local agent';
87
+ }
88
+
89
+ function configDirForFlags(flags, env = process.env) {
90
+ if (flags['config-dir']) return flags['config-dir'];
91
+ if (env.DEXTER_BRIDGE_CONFIG_DIR) return defaultConfigDir(env);
92
+ const runtime = requestedRuntime(flags, {}, env);
93
+ return runtime
94
+ ? path.join(defaultConfigDir(env), 'app-builder', runtime)
95
+ : defaultConfigDir(env);
96
+ }
97
+
54
98
  function parseArgv(argv) {
55
99
  const flags = {};
56
100
  const positional = [];
@@ -77,7 +121,10 @@ function parseArgv(argv) {
77
121
 
78
122
  function requireDeviceToken(config) {
79
123
  if (!config.deviceToken) {
80
- const error = new Error('Dexter Bridge is not paired. Run `dexter-bridge pair <code>` from Dexter Settings.');
124
+ const product = normalizeBridgeProduct(config.product);
125
+ const error = new Error(
126
+ `${product.name} Bridge is not paired. Generate a new bridge command from ${product.name}.`,
127
+ );
81
128
  error.exitCode = 2;
82
129
  throw error;
83
130
  }
@@ -87,7 +134,7 @@ function requireDeviceToken(config) {
87
134
  async function promptForPairingCode() {
88
135
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
89
136
  try {
90
- const answer = await rl.question('Paste the pairing code from the Dexter plugin and press Enter: ');
137
+ const answer = await rl.question('Paste the pairing code from the product connection screen and press Enter: ');
91
138
  return answer.trim();
92
139
  } catch {
93
140
  return '';
@@ -107,13 +154,56 @@ function pollBackoffMs(failureCount, baseMs = 1000, maxMs = 30000) {
107
154
  return Math.min(maximum, base * (2 ** Math.min(10, failures - 1)));
108
155
  }
109
156
 
157
+ function startRunAwakeGuard({
158
+ platform = process.platform,
159
+ env = process.env,
160
+ spawnImpl = spawn,
161
+ pid = process.pid,
162
+ } = {}) {
163
+ if (
164
+ platform !== 'darwin'
165
+ || ['0', 'false', 'no', 'off'].includes(
166
+ String(env.DEXTER_BRIDGE_PREVENT_IDLE_SLEEP || 'true').trim().toLowerCase(),
167
+ )
168
+ ) {
169
+ return null;
170
+ }
171
+ try {
172
+ const child = spawnImpl(
173
+ '/usr/bin/caffeinate',
174
+ ['-i', '-m', '-w', String(pid)],
175
+ {
176
+ stdio: 'ignore',
177
+ windowsHide: true,
178
+ },
179
+ );
180
+ child.once?.('error', () => undefined);
181
+ child.unref?.();
182
+ return {
183
+ close() {
184
+ if (child.killed) return;
185
+ try {
186
+ child.kill('SIGTERM');
187
+ } catch {
188
+ // The guard already exited with its parent process.
189
+ }
190
+ },
191
+ };
192
+ } catch {
193
+ return null;
194
+ }
195
+ }
196
+
110
197
  async function executeClaimedRun({
111
198
  run,
112
199
  execute = executeRun,
113
200
  executeOptions,
114
201
  once = false,
115
202
  logger,
203
+ product,
204
+ preventIdleSleep = false,
116
205
  }) {
206
+ const awakeGuard = preventIdleSleep ? startRunAwakeGuard() : null;
117
207
  try {
118
208
  await execute(run, executeOptions);
119
209
  return true;
@@ -123,8 +213,11 @@ async function executeClaimedRun({
123
213
  error: errorMeta(error),
124
214
  });
125
215
  if (once) throw error;
126
- console.error(`Dexter run ${run?.runId || 'unknown'} failed; continuing to poll.`);
216
+ const productName = normalizeBridgeProduct(product).name;
217
+ console.error(`${productName} run ${run?.runId || 'unknown'} failed; continuing to poll.`);
127
218
  return false;
219
+ } finally {
220
+ awakeGuard?.close();
128
221
  }
129
222
  }
130
223
 
@@ -142,13 +235,14 @@ function isInvalidPairingError(error) {
142
235
  }
143
236
 
144
237
  function clearInvalidPairing(configDir, cause) {
238
+ const product = normalizeBridgeProduct(readConfig(configDir).product);
145
239
  saveConfigPatch({
146
240
  deviceToken: null,
147
241
  device: null,
148
242
  pairedAt: null,
149
243
  }, configDir);
150
244
  const error = new Error(
151
- 'This Dexter Bridge pairing was disconnected. Reopen Dexter and run the new pairing command.',
245
+ `This ${product.name} Bridge pairing was disconnected. Reopen ${product.name} and run the new pairing command.`,
152
246
  { cause },
153
247
  );
154
248
  error.code = 'FRAMER_COMPANION_NOT_PAIRED';
@@ -170,15 +264,38 @@ function agentEnvironment(config = {}, baseEnv = process.env) {
170
264
  return env;
171
265
  }
172
266
 
173
- async function inspectAvailability(config = {}) {
267
+ async function inspectAvailability(config = {}, runtimeSelection = null) {
174
268
  try {
175
- const checks = await checkAllAgents({ env: agentEnvironment(config) });
176
- const allChecks = [...checks.agents, checks.dryRun];
269
+ const bridgeEnv = agentEnvironment(config);
270
+ const checks =
271
+ runtimeSelection === 'codex' || runtimeSelection === 'claude-code'
272
+ ? {
273
+ agents: [
274
+ await checkAgentAvailability(runtimeSelection, undefined, {
275
+ env: bridgeEnv,
276
+ }),
277
+ ],
278
+ dryRun: null,
279
+ }
280
+ : runtimeSelection === 'opencode'
281
+ ? { agents: [], dryRun: null }
282
+ : await checkAllAgents({ env: bridgeEnv });
283
+ const allChecks = [...checks.agents, ...(checks.dryRun ? [checks.dryRun] : [])];
177
284
  const available = allChecks.filter((check) => check.ok);
285
+ const runtimeProfiles = await discoverRuntimeProfiles({
286
+ agentChecks: checks.agents,
287
+ config,
288
+ env: bridgeEnv,
289
+ runtimeSelection,
290
+ });
291
+ const profileModels = runtimeProfiles.flatMap((profile) => profile.models || []);
178
292
  return {
179
293
  metadata: {
180
294
  availableAgents: available.map((check) => check.agent).join(','),
181
- availableModels: available.flatMap((check) => check.models || []).join(','),
295
+ availableModels: profileModels.map((model) => model.id).join(','),
296
+ availableModelDetails: JSON.stringify(
297
+ profileModels,
298
+ ),
182
299
  agentVersions: available.map((check) => `${check.agent}=${check.version || 'unknown'}`).join(','),
183
300
  bridgeCapabilities: BRIDGE_CAPABILITIES.join(','),
184
301
  bridgeVersion: BRIDGE_VERSION,
@@ -192,12 +309,14 @@ async function inspectAvailability(config = {}) {
192
309
  ),
193
310
  agents: checks.agents,
194
311
  dryRun: checks.dryRun,
312
+ runtimeProfiles,
195
313
  };
196
314
  } catch {
197
315
  return {
198
316
  metadata: {
199
317
  availableAgents: '',
200
318
  availableModels: '',
319
+ availableModelDetails: '[]',
201
320
  agentVersions: '',
202
321
  bridgeCapabilities: BRIDGE_CAPABILITIES.join(','),
203
322
  bridgeVersion: BRIDGE_VERSION,
@@ -206,19 +325,41 @@ async function inspectAvailability(config = {}) {
206
325
  agentCommands: {},
207
326
  agents: [],
208
327
  dryRun: null,
328
+ runtimeProfiles: [],
209
329
  };
210
330
  }
211
331
  }
212
332
 
213
- async function availabilityMetadata(config = {}) {
214
- return (await inspectAvailability(config)).metadata;
215
- }
216
-
217
333
  function selectedAgentCheck(availability, agent) {
218
334
  if (agent === 'dry-run') return availability.dryRun;
219
335
  return availability.agents.find((check) => check.agent === agent) || null;
220
336
  }
221
337
 
338
+ function requireAvailableRuntime(availability, runtimeSelection) {
339
+ const driver = runtimeDriver(runtimeSelection);
340
+ const profile = availability.runtimeProfiles.find(
341
+ (candidate) => candidate.driver === driver,
342
+ );
343
+ if (profile?.authState === 'ready' && profile.models?.length) return profile;
344
+
345
+ if (runtimeSelection === 'codex' || runtimeSelection === 'claude-code') {
346
+ requireAvailableAgent(
347
+ availability,
348
+ runtimeSelection,
349
+ process.platform,
350
+ );
351
+ }
352
+
353
+ const error = new Error(
354
+ runtimeSelection === 'opencode'
355
+ ? 'OpenCode is not ready. Install OpenCode, connect a model provider in OpenCode, then run this command again.'
356
+ : `${runtimeLabel(runtimeSelection)} did not advertise any usable models.`,
357
+ );
358
+ error.code = 'DEXTER_RUNTIME_NOT_READY';
359
+ error.exitCode = 2;
360
+ throw error;
361
+ }
362
+
222
363
  function requireAvailableAgent(availability, agent, platform = process.platform) {
223
364
  const check = selectedAgentCheck(availability, agent);
224
365
  if (check?.ok) return check;
@@ -229,8 +370,8 @@ function requireAvailableAgent(availability, agent, platform = process.platform)
229
370
  message = check.error || 'Claude Code is not signed in. Run `claude auth login`, then try again.';
230
371
  } else if (agent === 'claude-code' && (check?.code === 'DEXTER_AGENT_NOT_FOUND' || !check)) {
231
372
  message = platform === 'win32'
232
- ? 'Claude Code was not found. In PowerShell, run `claude --version`. Dexter also checks `%USERPROFILE%\\.local\\bin\\claude.exe`. If Claude is installed elsewhere, set `DEXTER_BRIDGE_CLAUDE_BIN` to its full path, then run the Dexter connection command again.'
233
- : 'Claude Code was not found. Run `claude --version` in this terminal. If Claude is installed elsewhere, set `DEXTER_BRIDGE_CLAUDE_BIN` to its full path, then run the Dexter connection command again.';
373
+ ? 'Claude Code was not found. In PowerShell, run `claude --version`. The bridge also checks `%USERPROFILE%\\.local\\bin\\claude.exe`. If Claude is installed elsewhere, set `DEXTER_BRIDGE_CLAUDE_BIN` to its full path, then run the connection command again.'
374
+ : 'Claude Code was not found. Run `claude --version` in this terminal. If Claude is installed elsewhere, set `DEXTER_BRIDGE_CLAUDE_BIN` to its full path, then run the connection command again.';
234
375
  }
235
376
 
236
377
  const error = new Error(message);
@@ -239,15 +380,36 @@ function requireAvailableAgent(availability, agent, platform = process.platform)
239
380
  throw error;
240
381
  }
241
382
 
383
+ function availableModel(check, requestedModel) {
384
+ const models = Array.isArray(check?.models) ? check.models : [];
385
+ return models.includes(requestedModel) ? requestedModel : models[0] || requestedModel;
386
+ }
387
+
242
388
  async function pairCommand({ apiBaseUrl, args, flags, configDir }) {
243
389
  const codeOrToken = args[0];
244
390
  if (!codeOrToken) throw new Error('Pairing code or token is required.');
245
391
  const isToken = /^dcpp_/i.test(codeOrToken);
246
392
  const config = readConfig(configDir);
247
- const agent = resolveAgentName({ flagValue: flags.agent, config });
248
- const model = resolveCompanionModelName({ flagValue: flags.model, config, agent });
249
- const availability = await inspectAvailability(config);
250
- requireAvailableAgent(availability, agent);
393
+ const runtimeSelection = requestedRuntime(flags, config);
394
+ const agent = runtimeSelection
395
+ ? runtimeAgent(runtimeSelection)
396
+ : resolveAgentName({ flagValue: flags.agent, config });
397
+ const requestedModel = runtimeSelection
398
+ ? flags.model || config.model
399
+ : resolveCompanionModelName({ flagValue: flags.model, config, agent });
400
+ const availability = await inspectAvailability(config, runtimeSelection);
401
+ const runtimeProfile = runtimeSelection
402
+ ? requireAvailableRuntime(availability, runtimeSelection)
403
+ : null;
404
+ const check = runtimeProfile
405
+ ? null
406
+ : availability.agents.find((candidate) => candidate.agent === agent && candidate.ok)
407
+ || availability.agents.find((candidate) => candidate.ok)
408
+ || availability.dryRun;
409
+ const model = runtimeProfile
410
+ ? runtimeProfile.models.find((candidate) => candidate.id === requestedModel)?.id
411
+ || runtimeProfile.models[0].id
412
+ : availableModel(check, requestedModel);
251
413
  const result = await claimPairing(apiBaseUrl, {
252
414
  pairingCode: isToken ? undefined : codeOrToken,
253
415
  pairingToken: isToken ? codeOrToken : undefined,
@@ -255,24 +417,43 @@ async function pairCommand({ apiBaseUrl, args, flags, configDir }) {
255
417
  agent,
256
418
  model,
257
419
  metadata: availability.metadata,
420
+ runtimeProfiles: availability.runtimeProfiles.map(publicRuntimeProfile),
421
+ runtimeSelection,
258
422
  });
259
423
  saveConfigPatch({
260
424
  apiBaseUrl,
261
425
  agent,
262
426
  model,
427
+ runtimeSelection,
428
+ product: normalizeBridgeProduct(result.product),
263
429
  deviceToken: result.deviceToken,
264
430
  device: result.device,
265
431
  agentCommands: availability.agentCommands,
266
432
  pairedAt: new Date().toISOString(),
267
433
  }, configDir);
268
- console.log(`Paired Dexter Bridge: ${result.device?.name || 'device'}`);
434
+ const product = normalizeBridgeProduct(result.product);
435
+ console.log(`Paired ${product.name} Bridge: ${result.device?.name || 'device'}`);
436
+ if (runtimeSelection) {
437
+ console.log(`Runtime: ${runtimeProfile?.label || runtimeLabel(runtimeSelection)}`);
438
+ }
269
439
  console.log(`API: ${apiBaseUrl}`);
270
440
  }
271
441
 
272
442
  async function statusCommand({ apiBaseUrl, config, configDir }) {
443
+ const product = normalizeBridgeProduct(config.product);
273
444
  const deviceToken = requireDeviceToken(config);
274
- const agent = resolveAgentName({ config });
275
- const model = resolveCompanionModelName({ config, agent });
445
+ const runtimeSelection = requestedRuntime({}, config);
446
+ const agent = runtimeSelection
447
+ ? runtimeAgent(runtimeSelection)
448
+ : resolveAgentName({ config });
449
+ const availability = await inspectAvailability(config, runtimeSelection);
450
+ const runtimeProfile = runtimeSelection
451
+ ? requireAvailableRuntime(availability, runtimeSelection)
452
+ : null;
453
+ const model = runtimeProfile
454
+ ? runtimeProfile.models.find((candidate) => candidate.id === config.model)?.id
455
+ || runtimeProfile.models[0].id
456
+ : resolveCompanionModelName({ config, agent });
276
457
  let result;
277
458
  try {
278
459
  result = await heartbeat(apiBaseUrl, {
@@ -280,14 +461,18 @@ async function statusCommand({ apiBaseUrl, config, configDir }) {
280
461
  status: 'ready',
281
462
  agent,
282
463
  model,
283
- metadata: await availabilityMetadata(config),
464
+ metadata: availability.metadata,
465
+ runtimeProfiles: availability.runtimeProfiles.map(publicRuntimeProfile),
284
466
  });
285
467
  } catch (error) {
286
468
  if (isInvalidPairingError(error)) throw clearInvalidPairing(configDir, error);
287
469
  throw error;
288
470
  }
289
471
  console.log(`Status: ${result.device?.online ? 'online' : 'paired'}`);
290
- console.log(`Device: ${result.device?.name || 'Dexter Bridge'}`);
472
+ console.log(`Device: ${result.device?.name || `${product.name} Bridge`}`);
473
+ if (runtimeSelection) {
474
+ console.log(`Runtime: ${runtimeProfile?.label || runtimeLabel(runtimeSelection)}`);
475
+ }
291
476
  console.log(`Model: ${result.device?.model?.displayName || model}`);
292
477
  console.log(`API: ${apiBaseUrl}`);
293
478
  }
@@ -297,39 +482,68 @@ async function startCommand({ apiBaseUrl, config, flags, configDir }) {
297
482
  let deviceToken = config.deviceToken;
298
483
  if (!deviceToken) {
299
484
  // First run: pair interactively so `npx @gakim-digital/dexter-bridge` alone is
300
- // enough — the Dexter plugin shows the code, the user pastes it here.
301
485
  if (!process.stdin.isTTY) requireDeviceToken(config);
302
- console.log('Dexter Bridge is not paired with your Dexter account yet.');
303
- console.log('Open the Dexter plugin in Framer and pick Connect → Codex to get a pairing code.');
486
+ console.log('This local agent bridge is not paired yet.');
487
+ console.log('Open the product connection screen and generate a new bridge command.');
304
488
  const code = await promptForPairingCode();
305
489
  if (!code) requireDeviceToken(config);
306
490
  await pairCommand({ apiBaseUrl, args: [code], flags, configDir });
307
491
  activeConfig = readConfig(configDir);
308
492
  deviceToken = activeConfig.deviceToken;
309
493
  }
310
- const agent = resolveAgentName({ flagValue: flags.agent, config: activeConfig });
311
- const model = resolveCompanionModelName({ flagValue: flags.model, config: activeConfig, agent });
494
+ const product = normalizeBridgeProduct(activeConfig.product);
495
+ const runtimeSelection = requestedRuntime(flags, activeConfig);
496
+ if (product.connectionScope === 'app-builder' && !runtimeSelection) {
497
+ const error = new Error('This App Builder connection requires --runtime codex, --runtime claude-code, or --runtime opencode.');
498
+ error.code = 'DEXTER_RUNTIME_REQUIRED';
499
+ error.exitCode = 2;
500
+ throw error;
501
+ }
502
+ const agent = runtimeSelection
503
+ ? runtimeAgent(runtimeSelection)
504
+ : resolveAgentName({ flagValue: flags.agent, config: activeConfig });
505
+ const requestedModel = runtimeSelection
506
+ ? flags.model || activeConfig.model
507
+ : resolveCompanionModelName({ flagValue: flags.model, config: activeConfig, agent });
312
508
  const waitMs = Number(flags['wait-ms'] || 25000);
313
509
  const once = Boolean(flags.once);
314
510
  const bridgeEnv = agentEnvironment(activeConfig);
315
- const availability = await inspectAvailability(activeConfig);
316
- requireAvailableAgent(availability, agent);
511
+ const availability = await inspectAvailability(activeConfig, runtimeSelection);
512
+ const runtimeProfile = runtimeSelection
513
+ ? requireAvailableRuntime(availability, runtimeSelection)
514
+ : null;
515
+ const check = runtimeProfile
516
+ ? null
517
+ : requireAvailableAgent(availability, agent);
518
+ const model = runtimeProfile
519
+ ? runtimeProfile.models.find((candidate) => candidate.id === requestedModel)?.id
520
+ || runtimeProfile.models[0].id
521
+ : availableModel(check, requestedModel);
317
522
  const metadata = availability.metadata;
318
523
  const pollLogger = createRunLogger({ runId: 'bridge-poll' });
524
+ const profiles = runtimeProfileMap(availability.runtimeProfiles);
319
525
  const providerAdapters = new Map();
320
- const providerAdapterForAgent = (runAgent) => {
321
- if (providerAdapters.has(runAgent)) return providerAdapters.get(runAgent);
322
- const adapter = createLocalAgentAdapter(runAgent, {
526
+ const providerAdapterForProfile = (runtimeProfile) => {
527
+ const profile = profiles.get(runtimeProfile?.id) || runtimeProfile;
528
+ if (!profile?.id) return null;
529
+ if (providerAdapters.has(profile.id)) return providerAdapters.get(profile.id);
530
+ const adapter = createLocalAgentAdapter(profile, {
323
531
  env: bridgeEnv,
532
+ product,
324
533
  trace: pollLogger,
534
+ credentials: profile.credentials,
325
535
  });
326
- providerAdapters.set(runAgent, adapter);
536
+ providerAdapters.set(profile.id, adapter);
327
537
  return adapter;
328
538
  };
329
539
  let pollFailureCount = 0;
330
540
 
331
- console.log(`Dexter Bridge connected to ${apiBaseUrl}`);
332
- console.log(`Agent: ${agent}`);
541
+ console.log(`${product.name} Bridge connected to ${apiBaseUrl}`);
542
+ console.log(
543
+ `${runtimeSelection ? 'Runtime' : 'Agent'}: ${
544
+ runtimeSelection ? runtimeProfile?.label || runtimeLabel(runtimeSelection) : agent
545
+ }`,
546
+ );
333
547
  console.log(`Model: ${model}`);
334
548
  console.log(`Logs: ${logLocationHint()}`);
335
549
  console.log('Waiting for runs...');
@@ -338,7 +552,14 @@ async function startCommand({ apiBaseUrl, config, flags, configDir }) {
338
552
  while (true) {
339
553
  let poll;
340
554
  try {
341
- poll = await pollRun(apiBaseUrl, { deviceToken, waitMs, agent, model, metadata });
555
+ poll = await pollRun(apiBaseUrl, {
556
+ deviceToken,
557
+ waitMs,
558
+ agent,
559
+ model,
560
+ metadata,
561
+ runtimeProfiles: availability.runtimeProfiles.map(publicRuntimeProfile),
562
+ });
342
563
  pollFailureCount = 0;
343
564
  } catch (error) {
344
565
  if (isInvalidPairingError(error)) {
@@ -353,24 +574,44 @@ async function startCommand({ apiBaseUrl, config, flags, configDir }) {
353
574
  const retryInMs = pollBackoffMs(pollFailureCount);
354
575
  pollLogger.error('poll_failed', { error: errorMeta(error) });
355
576
  if (pollFailureCount === 1 || (pollFailureCount & (pollFailureCount - 1)) === 0) {
356
- console.error(`Dexter poll failed; retrying in ${Math.round(retryInMs / 1000)}s.`);
577
+ console.error(`${product.name} polling failed; retrying in ${Math.round(retryInMs / 1000)}s.`);
357
578
  }
358
579
  await wait(retryInMs);
359
580
  continue;
360
581
  }
361
582
  if (poll.run) {
362
583
  console.log(`Claimed run ${poll.run.runId}.`);
363
- const runAgent = normalizeAgentName(poll.run?.companion?.agent || agent);
584
+ const runtimeProfile = poll.run?.runtimeProfile || null;
585
+ const runAgent = normalizeAgentName(
586
+ poll.run?.companion?.agent
587
+ || (
588
+ runtimeProfile?.driver === 'claude-code-cli'
589
+ ? 'claude-code'
590
+ : runtimeProfile?.driver === 'opencode'
591
+ ? 'opencode'
592
+ : 'codex'
593
+ ),
594
+ );
364
595
  await executeClaimedRun({
365
596
  run: poll.run,
366
597
  once,
598
+ preventIdleSleep: true,
367
599
  logger: pollLogger,
600
+ product,
368
601
  executeOptions: {
369
602
  apiBaseUrl,
370
603
  deviceToken,
371
604
  agent: runAgent,
372
605
  selectedModel: model,
373
- providerAdapter: providerAdapterForAgent(runAgent),
606
+ runtimeProfile,
607
+ providerAdapter: runtimeProfile
608
+ ? providerAdapterForProfile(runtimeProfile)
609
+ : createLocalAgentAdapter(runAgent, {
610
+ env: bridgeEnv,
611
+ product,
612
+ trace: pollLogger,
613
+ }),
614
+ product,
374
615
  env: bridgeEnv,
375
616
  maxSteps: flags['max-steps'],
376
617
  },
@@ -379,7 +620,7 @@ async function startCommand({ apiBaseUrl, config, flags, configDir }) {
379
620
  continue;
380
621
  }
381
622
  if (once) {
382
- console.log('No pending Dexter runs.');
623
+ console.log(`No pending ${product.name} runs.`);
383
624
  return;
384
625
  }
385
626
  }
@@ -389,7 +630,14 @@ async function startCommand({ apiBaseUrl, config, flags, configDir }) {
389
630
  }
390
631
  }
391
632
 
392
- async function doctorCommand() {
633
+ async function doctorCommand(runtimeSelection = null) {
634
+ if (runtimeSelection) {
635
+ const availability = await inspectAvailability({}, runtimeSelection);
636
+ const profile = requireAvailableRuntime(availability, runtimeSelection);
637
+ console.log(`${runtimeLabel(runtimeSelection)}: ready`);
638
+ console.log(`Models: ${profile.models.map((model) => model.displayName || model.id).join(', ')}`);
639
+ return;
640
+ }
393
641
  const checks = await checkAllAgents();
394
642
  for (const check of checks.agents) {
395
643
  if (check.ok) console.log(`${check.command}: ok ${check.output || ''}`.trim());
@@ -404,7 +652,7 @@ export async function runCli(argv) {
404
652
  return;
405
653
  }
406
654
 
407
- const configDir = parsed.flags['config-dir'] || defaultConfigDir();
655
+ const configDir = configDirForFlags(parsed.flags);
408
656
  const config = readConfig(configDir);
409
657
  const apiBaseUrl = resolveApiBaseUrl({
410
658
  flagValue: parsed.flags.api,
@@ -427,7 +675,7 @@ export async function runCli(argv) {
427
675
  return;
428
676
  case 'pair':
429
677
  await pairCommand({ apiBaseUrl, args: parsed.args, flags: parsed.flags, configDir });
430
- console.log('Run `dexter-bridge start` to go online.');
678
+ console.log(`Run \`dexter-bridge start\` to bring ${normalizeBridgeProduct(readConfig(configDir).product).name} online.`);
431
679
  return;
432
680
  case 'start':
433
681
  await startCommand({ apiBaseUrl, config: { ...config, apiBaseUrl }, flags: parsed.flags, configDir });
@@ -436,11 +684,14 @@ export async function runCli(argv) {
436
684
  await statusCommand({ apiBaseUrl, config, configDir });
437
685
  return;
438
686
  case 'doctor':
439
- await doctorCommand();
687
+ await doctorCommand(requestedRuntime(parsed.flags, config));
440
688
  return;
441
689
  case 'logout':
442
- clearConfig(configDir);
443
- console.log('Dexter Bridge local pairing removed.');
690
+ {
691
+ const product = normalizeBridgeProduct(config.product);
692
+ clearConfig(configDir);
693
+ console.log(`${product.name} Bridge local pairing removed.`);
694
+ }
444
695
  return;
445
696
  default:
446
697
  throw new Error(`Unknown command "${parsed.command}".\n\n${usage()}`);
@@ -450,10 +701,15 @@ export async function runCli(argv) {
450
701
  export const __private__ = {
451
702
  agentEnvironment,
452
703
  clearInvalidPairing,
704
+ configDirForFlags,
453
705
  executeClaimedRun,
706
+ startRunAwakeGuard,
454
707
  isInvalidPairingError,
455
708
  parseArgv,
456
709
  pollBackoffMs,
710
+ requestedRuntime,
711
+ availableModel,
712
+ requireAvailableRuntime,
457
713
  requireAvailableAgent,
458
714
  selectedAgentCheck,
459
715
  usage,