@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/README.md +116 -35
- package/package.json +19 -5
- package/src/agent.js +1351 -331
- package/src/agentOutput.js +209 -0
- package/src/api.js +48 -1
- package/src/cli.js +267 -39
- package/src/config.js +30 -7
- package/src/framerAgentTools.js +1108 -0
- package/src/harnessMcpServer.js +240 -0
- package/src/harnessTools.js +548 -0
- package/src/logger.js +1 -1
- package/src/nativeSkills.js +295 -0
- package/src/outcomeWorkspace.js +351 -0
- package/src/protocol.js +239 -0
- package/src/providers/acp.js +241 -0
- package/src/providers/codexAppServer.js +1050 -156
- package/src/providers/codexStructuredOutput.js +243 -16
- package/src/providers/directByok.js +197 -0
- package/src/providers/index.js +33 -7
- package/src/providers/openCode.js +607 -0
- package/src/runtimeProfiles.js +284 -0
- package/src/providers/claudeAgentSdk.js +0 -507
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]
|
|
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 [--
|
|
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
|
-
'
|
|
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
|
|
183
|
-
const
|
|
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:
|
|
295
|
+
availableModels: profileModels.map((model) => model.id).join(','),
|
|
189
296
|
availableModelDetails: JSON.stringify(
|
|
190
|
-
|
|
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
|
|
264
|
-
const
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
const
|
|
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
|
|
295
|
-
const
|
|
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:
|
|
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
|
|
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
|
|
337
|
-
|
|
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
|
|
342
|
-
|
|
343
|
-
|
|
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(
|
|
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(
|
|
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, {
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
43
|
-
//
|
|
44
|
-
//
|
|
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 (
|
|
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
|
}
|