@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/README.md +128 -18
- package/package.json +19 -5
- package/src/agent.js +1500 -335
- package/src/agentOutput.js +209 -0
- package/src/api.js +49 -2
- package/src/cli.js +310 -54
- package/src/config.js +82 -15
- 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 +257 -10
- package/src/providers/acp.js +241 -0
- package/src/providers/codexAppServer.js +1074 -170
- 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,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
|
|
42
|
+
'Local Agent Bridge for Dexter and InstaWebAI',
|
|
33
43
|
'',
|
|
34
44
|
'Usage:',
|
|
35
|
-
' dexter-bridge connect [code-or-token]
|
|
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 [--
|
|
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
|
-
'
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
176
|
-
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] : [])];
|
|
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:
|
|
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`.
|
|
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
|
|
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
|
|
248
|
-
const
|
|
249
|
-
|
|
250
|
-
|
|
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
|
-
|
|
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
|
|
275
|
-
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 });
|
|
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:
|
|
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 ||
|
|
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('
|
|
303
|
-
console.log('Open the
|
|
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
|
|
311
|
-
const
|
|
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
|
-
|
|
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
|
|
321
|
-
|
|
322
|
-
|
|
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(
|
|
536
|
+
providerAdapters.set(profile.id, adapter);
|
|
327
537
|
return adapter;
|
|
328
538
|
};
|
|
329
539
|
let pollFailureCount = 0;
|
|
330
540
|
|
|
331
|
-
console.log(
|
|
332
|
-
console.log(
|
|
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, {
|
|
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(
|
|
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
|
|
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
|
-
|
|
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(
|
|
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
|
|
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(
|
|
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
|
-
|
|
443
|
-
|
|
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,
|