@gakim-digital/dexter-bridge 0.5.21 → 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,5 +1,7 @@
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,
@@ -14,7 +16,7 @@ import {
14
16
  resolveCompanionModelName,
15
17
  saveConfigPatch,
16
18
  } from './config.js';
17
- import { checkAllAgents, executeRun } from './agent.js';
19
+ import { checkAgentAvailability, checkAllAgents, executeRun } from './agent.js';
18
20
  import {
19
21
  claimPairing,
20
22
  DexterBridgeApiError,
@@ -27,22 +29,30 @@ import {
27
29
  logLocationHint,
28
30
  } from './logger.js';
29
31
  import { createLocalAgentAdapter } from './providers/index.js';
32
+ import {
33
+ discoverRuntimeProfiles,
34
+ normalizeRuntimeSelection,
35
+ publicRuntimeProfile,
36
+ runtimeDriver,
37
+ runtimeProfileMap,
38
+ } from './runtimeProfiles.js';
30
39
 
31
40
  function usage() {
32
41
  return [
33
42
  'Local Agent Bridge for Dexter and InstaWebAI',
34
43
  '',
35
44
  'Usage:',
36
- ' 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>]',
37
46
  ' dexter-bridge pair <code-or-token> [--api <url>]',
38
- ' dexter-bridge start [--agent claude-code|codex|dry-run] [--model <model>] [--once] [--api <url>]',
39
- ' dexter-bridge status [--api <url>]',
40
- ' 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]',
41
50
  ' dexter-bridge logout',
42
51
  '',
43
52
  'Environment:',
44
53
  ' DEXTER_API_URL API base URL, e.g. https://api.example.com/iwm-api/0.0.1',
45
- ' 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',
46
56
  ' DEXTER_BRIDGE_MODEL Companion model, e.g. claude-code:sonnet or codex:gpt-5.5',
47
57
  ' DEXTER_BRIDGE_CLAUDE_BIN Claude CLI binary name/path',
48
58
  ' DEXTER_BRIDGE_CLAUDE_ARGS Claude CLI args, default: -p',
@@ -52,6 +62,39 @@ function usage() {
52
62
  ].join('\n');
53
63
  }
54
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
+
55
98
  function parseArgv(argv) {
56
99
  const flags = {};
57
100
  const positional = [];
@@ -111,6 +154,46 @@ function pollBackoffMs(failureCount, baseMs = 1000, maxMs = 30000) {
111
154
  return Math.min(maximum, base * (2 ** Math.min(10, failures - 1)));
112
155
  }
113
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
+
114
197
  async function executeClaimedRun({
115
198
  run,
116
199
  execute = executeRun,
@@ -118,7 +201,9 @@ async function executeClaimedRun({
118
201
  once = false,
119
202
  logger,
120
203
  product,
204
+ preventIdleSleep = false,
121
205
  }) {
206
+ const awakeGuard = preventIdleSleep ? startRunAwakeGuard() : null;
122
207
  try {
123
208
  await execute(run, executeOptions);
124
209
  return true;
@@ -131,6 +216,8 @@ async function executeClaimedRun({
131
216
  const productName = normalizeBridgeProduct(product).name;
132
217
  console.error(`${productName} run ${run?.runId || 'unknown'} failed; continuing to poll.`);
133
218
  return false;
219
+ } finally {
220
+ awakeGuard?.close();
134
221
  }
135
222
  }
136
223
 
@@ -177,17 +264,37 @@ function agentEnvironment(config = {}, baseEnv = process.env) {
177
264
  return env;
178
265
  }
179
266
 
180
- async function inspectAvailability(config = {}) {
267
+ async function inspectAvailability(config = {}, runtimeSelection = null) {
181
268
  try {
182
- const checks = await checkAllAgents({ env: agentEnvironment(config) });
183
- 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] : [])];
184
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 || []);
185
292
  return {
186
293
  metadata: {
187
294
  availableAgents: available.map((check) => check.agent).join(','),
188
- availableModels: available.flatMap((check) => check.models || []).join(','),
295
+ availableModels: profileModels.map((model) => model.id).join(','),
189
296
  availableModelDetails: JSON.stringify(
190
- available.flatMap((check) => check.modelDetails || []),
297
+ profileModels,
191
298
  ),
192
299
  agentVersions: available.map((check) => `${check.agent}=${check.version || 'unknown'}`).join(','),
193
300
  bridgeCapabilities: BRIDGE_CAPABILITIES.join(','),
@@ -202,6 +309,7 @@ async function inspectAvailability(config = {}) {
202
309
  ),
203
310
  agents: checks.agents,
204
311
  dryRun: checks.dryRun,
312
+ runtimeProfiles,
205
313
  };
206
314
  } catch {
207
315
  return {
@@ -217,19 +325,41 @@ async function inspectAvailability(config = {}) {
217
325
  agentCommands: {},
218
326
  agents: [],
219
327
  dryRun: null,
328
+ runtimeProfiles: [],
220
329
  };
221
330
  }
222
331
  }
223
332
 
224
- async function availabilityMetadata(config = {}) {
225
- return (await inspectAvailability(config)).metadata;
226
- }
227
-
228
333
  function selectedAgentCheck(availability, agent) {
229
334
  if (agent === 'dry-run') return availability.dryRun;
230
335
  return availability.agents.find((check) => check.agent === agent) || null;
231
336
  }
232
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
+
233
363
  function requireAvailableAgent(availability, agent, platform = process.platform) {
234
364
  const check = selectedAgentCheck(availability, agent);
235
365
  if (check?.ok) return check;
@@ -260,11 +390,26 @@ async function pairCommand({ apiBaseUrl, args, flags, configDir }) {
260
390
  if (!codeOrToken) throw new Error('Pairing code or token is required.');
261
391
  const isToken = /^dcpp_/i.test(codeOrToken);
262
392
  const config = readConfig(configDir);
263
- const agent = resolveAgentName({ flagValue: flags.agent, config });
264
- const requestedModel = resolveCompanionModelName({ flagValue: flags.model, config, agent });
265
- const availability = await inspectAvailability(config);
266
- const check = requireAvailableAgent(availability, agent);
267
- const model = availableModel(check, requestedModel);
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);
268
413
  const result = await claimPairing(apiBaseUrl, {
269
414
  pairingCode: isToken ? undefined : codeOrToken,
270
415
  pairingToken: isToken ? codeOrToken : undefined,
@@ -272,11 +417,14 @@ async function pairCommand({ apiBaseUrl, args, flags, configDir }) {
272
417
  agent,
273
418
  model,
274
419
  metadata: availability.metadata,
420
+ runtimeProfiles: availability.runtimeProfiles.map(publicRuntimeProfile),
421
+ runtimeSelection,
275
422
  });
276
423
  saveConfigPatch({
277
424
  apiBaseUrl,
278
425
  agent,
279
426
  model,
427
+ runtimeSelection,
280
428
  product: normalizeBridgeProduct(result.product),
281
429
  deviceToken: result.deviceToken,
282
430
  device: result.device,
@@ -285,14 +433,27 @@ async function pairCommand({ apiBaseUrl, args, flags, configDir }) {
285
433
  }, configDir);
286
434
  const product = normalizeBridgeProduct(result.product);
287
435
  console.log(`Paired ${product.name} Bridge: ${result.device?.name || 'device'}`);
436
+ if (runtimeSelection) {
437
+ console.log(`Runtime: ${runtimeProfile?.label || runtimeLabel(runtimeSelection)}`);
438
+ }
288
439
  console.log(`API: ${apiBaseUrl}`);
289
440
  }
290
441
 
291
442
  async function statusCommand({ apiBaseUrl, config, configDir }) {
292
443
  const product = normalizeBridgeProduct(config.product);
293
444
  const deviceToken = requireDeviceToken(config);
294
- const agent = resolveAgentName({ config });
295
- 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 });
296
457
  let result;
297
458
  try {
298
459
  result = await heartbeat(apiBaseUrl, {
@@ -300,7 +461,8 @@ async function statusCommand({ apiBaseUrl, config, configDir }) {
300
461
  status: 'ready',
301
462
  agent,
302
463
  model,
303
- metadata: await availabilityMetadata(config),
464
+ metadata: availability.metadata,
465
+ runtimeProfiles: availability.runtimeProfiles.map(publicRuntimeProfile),
304
466
  });
305
467
  } catch (error) {
306
468
  if (isInvalidPairingError(error)) throw clearInvalidPairing(configDir, error);
@@ -308,6 +470,9 @@ async function statusCommand({ apiBaseUrl, config, configDir }) {
308
470
  }
309
471
  console.log(`Status: ${result.device?.online ? 'online' : 'paired'}`);
310
472
  console.log(`Device: ${result.device?.name || `${product.name} Bridge`}`);
473
+ if (runtimeSelection) {
474
+ console.log(`Runtime: ${runtimeProfile?.label || runtimeLabel(runtimeSelection)}`);
475
+ }
311
476
  console.log(`Model: ${result.device?.model?.displayName || model}`);
312
477
  console.log(`API: ${apiBaseUrl}`);
313
478
  }
@@ -326,32 +491,59 @@ async function startCommand({ apiBaseUrl, config, flags, configDir }) {
326
491
  activeConfig = readConfig(configDir);
327
492
  deviceToken = activeConfig.deviceToken;
328
493
  }
329
- const agent = resolveAgentName({ flagValue: flags.agent, config: activeConfig });
330
494
  const product = normalizeBridgeProduct(activeConfig.product);
331
- const requestedModel = resolveCompanionModelName({ flagValue: flags.model, config: activeConfig, agent });
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 });
332
508
  const waitMs = Number(flags['wait-ms'] || 25000);
333
509
  const once = Boolean(flags.once);
334
510
  const bridgeEnv = agentEnvironment(activeConfig);
335
- const availability = await inspectAvailability(activeConfig);
336
- const check = requireAvailableAgent(availability, agent);
337
- const model = availableModel(check, requestedModel);
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);
338
522
  const metadata = availability.metadata;
339
523
  const pollLogger = createRunLogger({ runId: 'bridge-poll' });
524
+ const profiles = runtimeProfileMap(availability.runtimeProfiles);
340
525
  const providerAdapters = new Map();
341
- const providerAdapterForAgent = (runAgent) => {
342
- if (providerAdapters.has(runAgent)) return providerAdapters.get(runAgent);
343
- 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, {
344
531
  env: bridgeEnv,
345
532
  product,
346
533
  trace: pollLogger,
534
+ credentials: profile.credentials,
347
535
  });
348
- providerAdapters.set(runAgent, adapter);
536
+ providerAdapters.set(profile.id, adapter);
349
537
  return adapter;
350
538
  };
351
539
  let pollFailureCount = 0;
352
540
 
353
541
  console.log(`${product.name} Bridge connected to ${apiBaseUrl}`);
354
- console.log(`Agent: ${agent}`);
542
+ console.log(
543
+ `${runtimeSelection ? 'Runtime' : 'Agent'}: ${
544
+ runtimeSelection ? runtimeProfile?.label || runtimeLabel(runtimeSelection) : agent
545
+ }`,
546
+ );
355
547
  console.log(`Model: ${model}`);
356
548
  console.log(`Logs: ${logLocationHint()}`);
357
549
  console.log('Waiting for runs...');
@@ -360,7 +552,14 @@ async function startCommand({ apiBaseUrl, config, flags, configDir }) {
360
552
  while (true) {
361
553
  let poll;
362
554
  try {
363
- 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
+ });
364
563
  pollFailureCount = 0;
365
564
  } catch (error) {
366
565
  if (isInvalidPairingError(error)) {
@@ -382,10 +581,21 @@ async function startCommand({ apiBaseUrl, config, flags, configDir }) {
382
581
  }
383
582
  if (poll.run) {
384
583
  console.log(`Claimed run ${poll.run.runId}.`);
385
- 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
+ );
386
595
  await executeClaimedRun({
387
596
  run: poll.run,
388
597
  once,
598
+ preventIdleSleep: true,
389
599
  logger: pollLogger,
390
600
  product,
391
601
  executeOptions: {
@@ -393,7 +603,14 @@ async function startCommand({ apiBaseUrl, config, flags, configDir }) {
393
603
  deviceToken,
394
604
  agent: runAgent,
395
605
  selectedModel: model,
396
- providerAdapter: providerAdapterForAgent(runAgent),
606
+ runtimeProfile,
607
+ providerAdapter: runtimeProfile
608
+ ? providerAdapterForProfile(runtimeProfile)
609
+ : createLocalAgentAdapter(runAgent, {
610
+ env: bridgeEnv,
611
+ product,
612
+ trace: pollLogger,
613
+ }),
397
614
  product,
398
615
  env: bridgeEnv,
399
616
  maxSteps: flags['max-steps'],
@@ -413,7 +630,14 @@ async function startCommand({ apiBaseUrl, config, flags, configDir }) {
413
630
  }
414
631
  }
415
632
 
416
- 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
+ }
417
641
  const checks = await checkAllAgents();
418
642
  for (const check of checks.agents) {
419
643
  if (check.ok) console.log(`${check.command}: ok ${check.output || ''}`.trim());
@@ -428,7 +652,7 @@ export async function runCli(argv) {
428
652
  return;
429
653
  }
430
654
 
431
- const configDir = parsed.flags['config-dir'] || defaultConfigDir();
655
+ const configDir = configDirForFlags(parsed.flags);
432
656
  const config = readConfig(configDir);
433
657
  const apiBaseUrl = resolveApiBaseUrl({
434
658
  flagValue: parsed.flags.api,
@@ -460,7 +684,7 @@ export async function runCli(argv) {
460
684
  await statusCommand({ apiBaseUrl, config, configDir });
461
685
  return;
462
686
  case 'doctor':
463
- await doctorCommand();
687
+ await doctorCommand(requestedRuntime(parsed.flags, config));
464
688
  return;
465
689
  case 'logout':
466
690
  {
@@ -477,11 +701,15 @@ export async function runCli(argv) {
477
701
  export const __private__ = {
478
702
  agentEnvironment,
479
703
  clearInvalidPairing,
704
+ configDirForFlags,
480
705
  executeClaimedRun,
706
+ startRunAwakeGuard,
481
707
  isInvalidPairingError,
482
708
  parseArgv,
483
709
  pollBackoffMs,
710
+ requestedRuntime,
484
711
  availableModel,
712
+ requireAvailableRuntime,
485
713
  requireAvailableAgent,
486
714
  selectedAgentCheck,
487
715
  usage,
package/src/config.js CHANGED
@@ -17,6 +17,18 @@ export const BRIDGE_BUILD_FINGERPRINT = crypto
17
17
  './cli.js',
18
18
  './config.js',
19
19
  './protocol.js',
20
+ './nativeSkills.js',
21
+ './harnessTools.js',
22
+ './harnessMcpServer.js',
23
+ './framerAgentTools.js',
24
+ './outcomeWorkspace.js',
25
+ './runtimeProfiles.js',
26
+ './providers/index.js',
27
+ './providers/codexAppServer.js',
28
+ './providers/codexStructuredOutput.js',
29
+ './providers/directByok.js',
30
+ './providers/openCode.js',
31
+ './providers/acp.js',
20
32
  ].map((relativePath) => {
21
33
  const url = new URL(relativePath, import.meta.url);
22
34
  return `${relativePath}\u0000${fs.readFileSync(url, 'utf8')}`;
@@ -24,8 +36,16 @@ export const BRIDGE_BUILD_FINGERPRINT = crypto
24
36
  .digest('hex');
25
37
  export const BRIDGE_CAPABILITIES = [
26
38
  'model-turn-v1',
39
+ 'outcome-run-v1',
40
+ 'harness-tools-v1',
41
+ 'framer-agent-v1',
42
+ 'framer-project-bootstrap-v1',
43
+ 'project-shell-v1',
27
44
  'build-fingerprint-v1',
28
45
  'tool-schema-parity-v1',
46
+ 'runtime-profiles-v1',
47
+ 'native-skills-v1',
48
+ 'model-progress-v1',
29
49
  ];
30
50
  export const BRIDGE_PRODUCTS = {
31
51
  dexter: {
@@ -39,12 +59,11 @@ export const BRIDGE_PRODUCTS = {
39
59
  connectionScope: 'app-builder',
40
60
  },
41
61
  };
42
- // Codex is the default local agent: driving Claude Code from a user's Claude.ai
43
- // subscription needs prior written approval from Anthropic for commercial use, so
44
- // 'claude-code' only runs when the server offers it (see policy gating in
45
- // framerCompanionModels.ts and docs/dexter-connect-onboarding-plan.md §0).
62
+ // Codex remains the default local agent. Claude Code is enabled only when the
63
+ // user explicitly connects the locally installed CLI and chooses one of its
64
+ // reported models.
46
65
  export const DEFAULT_BRIDGE_AGENT = 'codex';
47
- export const BRIDGE_AGENTS = ['claude-code', 'codex', 'dry-run'];
66
+ export const BRIDGE_AGENTS = ['claude-code', 'codex', 'opencode', 'dry-run'];
48
67
  export const COMPANION_MODEL_DEFINITIONS = [
49
68
  {
50
69
  id: 'claude-code:fable',
@@ -179,7 +198,7 @@ export function configFilePath(configDir = defaultConfigDir()) {
179
198
  return path.join(configDir, 'config.json');
180
199
  }
181
200
 
182
- export function normalizeApiBaseUrl(value) {
201
+ export function normalizeApiBaseUrl(value, { allowInsecureHttp = false } = {}) {
183
202
  const raw = String(value || DEFAULT_API_BASE_URL).trim();
184
203
  let parsed;
185
204
  try {
@@ -196,7 +215,10 @@ export function normalizeApiBaseUrl(value) {
196
215
  if (parsed.username || parsed.password) {
197
216
  throw new Error('Bridge API URL must not contain embedded credentials.');
198
217
  }
199
- if (parsed.protocol !== 'https:' && !(parsed.protocol === 'http:' && loopback)) {
218
+ if (
219
+ parsed.protocol !== 'https:'
220
+ && !(parsed.protocol === 'http:' && (loopback || allowInsecureHttp))
221
+ ) {
200
222
  throw new Error('Bridge API URL must use HTTPS. HTTP is allowed only for localhost development.');
201
223
  }
202
224
  if (parsed.search || parsed.hash) {
@@ -209,6 +231,7 @@ export function normalizeAgentName(value, fallback = DEFAULT_BRIDGE_AGENT) {
209
231
  const raw = String(value || '').trim().toLowerCase();
210
232
  if (raw === 'claude' || raw === 'claude_code' || raw === 'claude-code') return 'claude-code';
211
233
  if (raw === 'openai-codex' || raw === 'openai_codex' || raw === 'codex') return 'codex';
234
+ if (raw === 'open-code' || raw === 'open_code' || raw === 'opencode') return 'opencode';
212
235
  if (raw === 'dryrun' || raw === 'dry_run' || raw === 'dry-run') return 'dry-run';
213
236
  return BRIDGE_AGENTS.includes(fallback) ? fallback : DEFAULT_BRIDGE_AGENT;
214
237
  }