@gakim-digital/dexter-bridge 0.11.6 → 0.11.9
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/package.json +1 -1
- package/src/agent.js +137 -22
- package/src/agentOutput.js +5 -0
- package/src/api.js +9 -0
- package/src/cli.js +126 -23
- package/src/config.js +1 -0
- package/src/framerAgentTools.js +182 -17
- package/src/harnessMcpServer.js +13 -5
- package/src/harnessTools.js +114 -1
- package/src/protocol.js +6 -3
- package/src/providers/codexAppServer.js +16 -5
- package/src/runtimeProfiles.js +127 -2
package/package.json
CHANGED
package/src/agent.js
CHANGED
|
@@ -6,6 +6,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
6
6
|
import crossSpawn from 'cross-spawn';
|
|
7
7
|
import {
|
|
8
8
|
getFramerProjectAuthorization,
|
|
9
|
+
isCompanionPairingInvalidError,
|
|
9
10
|
postRunEvent,
|
|
10
11
|
reportFramerProjectAuthorizationVerification,
|
|
11
12
|
} from './api.js';
|
|
@@ -182,6 +183,10 @@ function nowIso() {
|
|
|
182
183
|
const TERMINAL_EVENT_TYPES = new Set(['done', 'error']);
|
|
183
184
|
const TERMINAL_EVENT_MAX_ATTEMPTS = 5;
|
|
184
185
|
const TOOL_EVENT_MAX_ATTEMPTS = 3;
|
|
186
|
+
// Progress/status events are retried too, but briefly: they are posted in
|
|
187
|
+
// order on one chain, so a long backoff would stall every later event.
|
|
188
|
+
const STREAM_EVENT_MAX_ATTEMPTS = 3;
|
|
189
|
+
const STREAM_EVENT_MAX_RETRY_DELAY_MS = 2_000;
|
|
185
190
|
|
|
186
191
|
function waitForRetry(delayMs) {
|
|
187
192
|
return new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
@@ -205,14 +210,20 @@ function createEventPoster({
|
|
|
205
210
|
trace,
|
|
206
211
|
allowInsecureHttp = false,
|
|
207
212
|
}) {
|
|
213
|
+
// Stream events carry a per-run sequence so the API can drop a retried
|
|
214
|
+
// event it already published (for example after a lost response). Tool
|
|
215
|
+
// calls are idempotent by callId and terminal events by run completion.
|
|
216
|
+
let sequence = 0;
|
|
208
217
|
return async function send(type, payload = {}) {
|
|
209
218
|
const eventId = `${type}_${Date.now()}_${Math.random().toString(36).slice(2)}`;
|
|
219
|
+
const stream = !TERMINAL_EVENT_TYPES.has(type) && type !== 'tool_call';
|
|
220
|
+
const eventSequence = stream ? (sequence += 1) : undefined;
|
|
210
221
|
const started = Date.now();
|
|
211
222
|
const maxAttempts = TERMINAL_EVENT_TYPES.has(type)
|
|
212
223
|
? TERMINAL_EVENT_MAX_ATTEMPTS
|
|
213
224
|
: type === 'tool_call'
|
|
214
225
|
? TOOL_EVENT_MAX_ATTEMPTS
|
|
215
|
-
:
|
|
226
|
+
: STREAM_EVENT_MAX_ATTEMPTS;
|
|
216
227
|
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
|
217
228
|
trace?.info('event_post_start', {
|
|
218
229
|
type,
|
|
@@ -229,6 +240,7 @@ function createEventPoster({
|
|
|
229
240
|
event: {
|
|
230
241
|
type,
|
|
231
242
|
eventId,
|
|
243
|
+
...(eventSequence ? { sequence: eventSequence } : {}),
|
|
232
244
|
payload,
|
|
233
245
|
},
|
|
234
246
|
});
|
|
@@ -244,7 +256,11 @@ function createEventPoster({
|
|
|
244
256
|
});
|
|
245
257
|
return response;
|
|
246
258
|
} catch (error) {
|
|
247
|
-
const
|
|
259
|
+
const baseDelayMs = attempt < maxAttempts ? terminalEventRetryDelay(error, attempt) : null;
|
|
260
|
+
const retryDelayMs =
|
|
261
|
+
stream && baseDelayMs !== null
|
|
262
|
+
? Math.min(STREAM_EVENT_MAX_RETRY_DELAY_MS, baseDelayMs)
|
|
263
|
+
: baseDelayMs;
|
|
248
264
|
trace?.error('event_post_failed', {
|
|
249
265
|
type,
|
|
250
266
|
eventId,
|
|
@@ -378,7 +394,12 @@ function argsWithClaudeIsolation(args, definition, options = {}) {
|
|
|
378
394
|
'preview_control',
|
|
379
395
|
'browser_control',
|
|
380
396
|
'data_inspect',
|
|
397
|
+
'data_schema_get',
|
|
398
|
+
'data_schema_apply',
|
|
381
399
|
'verification_run',
|
|
400
|
+
'platform_catalog_search',
|
|
401
|
+
'platform_resource_configure',
|
|
402
|
+
'platform_resource_status',
|
|
382
403
|
];
|
|
383
404
|
const harnessTools = harnessToolNames.map(
|
|
384
405
|
(name) => `mcp__instawebai__${name}`,
|
|
@@ -673,7 +694,40 @@ export function buildAgentArgs(definition, modelDefinition, env = process.env, o
|
|
|
673
694
|
options.responseContract,
|
|
674
695
|
);
|
|
675
696
|
const resumedArgs = argsWithResumedSession(schemaArgs, definition, options.resumeSessionId);
|
|
676
|
-
return argsWithPromptInput(resumedArgs, definition);
|
|
697
|
+
return argsWithPromptInput(argsWithImages(resumedArgs, definition, options.imageFiles), definition);
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
function argsWithImages(args, definition, imageFiles = []) {
|
|
701
|
+
if (!imageFiles.length) return args;
|
|
702
|
+
if (definition.id === 'codex') {
|
|
703
|
+
const withoutStdin = args.filter((arg) => arg !== '-');
|
|
704
|
+
return [...withoutStdin, ...imageFiles.map((file) => `--image=${file}`)];
|
|
705
|
+
}
|
|
706
|
+
if (definition.id === 'claude-code') {
|
|
707
|
+
const streamed = argsWithoutFlagValue(argsWithoutFlagValue(args, '--input-format'), '--output-format');
|
|
708
|
+
streamed.push('--input-format', 'stream-json', '--output-format', 'stream-json');
|
|
709
|
+
if (!argsIncludeFlag(streamed, '--verbose')) streamed.push('--verbose');
|
|
710
|
+
return streamed;
|
|
711
|
+
}
|
|
712
|
+
return args;
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
const IMAGE_MEDIA_TYPES = { '.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.webp': 'image/webp' };
|
|
716
|
+
|
|
717
|
+
// Claude Code's stream-json input carries images as native content blocks.
|
|
718
|
+
function claudeStreamJsonPrompt(prompt, imageFiles) {
|
|
719
|
+
const content = [
|
|
720
|
+
...imageFiles.map((file) => ({
|
|
721
|
+
type: 'image',
|
|
722
|
+
source: {
|
|
723
|
+
type: 'base64',
|
|
724
|
+
media_type: IMAGE_MEDIA_TYPES[path.extname(file).toLowerCase()] || 'image/png',
|
|
725
|
+
data: fs.readFileSync(file).toString('base64'),
|
|
726
|
+
},
|
|
727
|
+
})),
|
|
728
|
+
{ type: 'text', text: prompt },
|
|
729
|
+
];
|
|
730
|
+
return `${JSON.stringify({ type: 'user', message: { role: 'user', content } })}\n`;
|
|
677
731
|
}
|
|
678
732
|
|
|
679
733
|
export function parseEnvOutput(output) {
|
|
@@ -1523,6 +1577,9 @@ async function callProviderAdapter(
|
|
|
1523
1577
|
if (controlRequestsCancellation(response)) await requestCancel();
|
|
1524
1578
|
})
|
|
1525
1579
|
.catch((error) => {
|
|
1580
|
+
if (isCompanionPairingInvalidError(error)) {
|
|
1581
|
+
void requestCancel();
|
|
1582
|
+
}
|
|
1526
1583
|
if (!finished) {
|
|
1527
1584
|
trace?.warn('adapter_activity_post_failed', {
|
|
1528
1585
|
error: errorMeta(error),
|
|
@@ -1562,6 +1619,10 @@ async function callProviderAdapter(
|
|
|
1562
1619
|
if (controlRequestsCancellation(response)) await requestCancel();
|
|
1563
1620
|
} catch (error) {
|
|
1564
1621
|
if (monitorAbort.signal.aborted) return;
|
|
1622
|
+
if (isCompanionPairingInvalidError(error)) {
|
|
1623
|
+
await requestCancel();
|
|
1624
|
+
return;
|
|
1625
|
+
}
|
|
1565
1626
|
trace?.warn('adapter_control_poll_failed', { error: errorMeta(error) });
|
|
1566
1627
|
}
|
|
1567
1628
|
}
|
|
@@ -1713,17 +1774,11 @@ async function callLocalJsonAgent(agent, prompt, options = {}) {
|
|
|
1713
1774
|
const definition = definitionForAgent(agent);
|
|
1714
1775
|
const command = options.runtime?.command || commandFromEnv(definition.commandEnv, definition.fallbackCommand);
|
|
1715
1776
|
const modelDefinition = companionModelDefinition(options.model, definition.id);
|
|
1716
|
-
const args = buildAgentArgs(definition, modelDefinition, process.env,
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
product: options.product,
|
|
1722
|
-
nativeSkillsEnabled: options.nativeSkillsEnabled,
|
|
1723
|
-
harnessMode: options.harnessMode,
|
|
1724
|
-
mcpConfig: options.mcpConfig,
|
|
1725
|
-
harnessToolNames: options.harnessToolNames,
|
|
1726
|
-
});
|
|
1777
|
+
const args = buildAgentArgs(definition, modelDefinition, process.env, options);
|
|
1778
|
+
const processInput =
|
|
1779
|
+
definition.id === 'claude-code' && options.imageFiles?.length
|
|
1780
|
+
? claudeStreamJsonPrompt(prompt, options.imageFiles)
|
|
1781
|
+
: prompt;
|
|
1727
1782
|
const timeoutMs = options.timeoutMs || Number(process.env.DEXTER_BRIDGE_AGENT_TIMEOUT_MS || 120000);
|
|
1728
1783
|
const maxDurationMs = boundedDurationMs(
|
|
1729
1784
|
options.maxDurationMs ?? process.env.DEXTER_BRIDGE_AGENT_MAX_DURATION_MS,
|
|
@@ -1743,7 +1798,7 @@ async function callLocalJsonAgent(agent, prompt, options = {}) {
|
|
|
1743
1798
|
timeoutMs,
|
|
1744
1799
|
maxDurationMs,
|
|
1745
1800
|
});
|
|
1746
|
-
return runProcess(command, args,
|
|
1801
|
+
return runProcess(command, args, processInput, {
|
|
1747
1802
|
timeoutMs,
|
|
1748
1803
|
maxDurationMs,
|
|
1749
1804
|
trace: options.trace,
|
|
@@ -1847,7 +1902,7 @@ async function executeModelTurnRun(run, send, agent, options = {}) {
|
|
|
1847
1902
|
callId: run.runId,
|
|
1848
1903
|
step: Number(run?.modelTurn?.step ?? 0),
|
|
1849
1904
|
model: selectedModelId,
|
|
1850
|
-
|
|
1905
|
+
callStatus: 'failed',
|
|
1851
1906
|
durationMs: Date.now() - callStartedAt,
|
|
1852
1907
|
promptChars: prompt.length,
|
|
1853
1908
|
requestedContextMode: deltaRequested ? 'delta' : 'full',
|
|
@@ -2006,6 +2061,9 @@ async function executeModelTurnRun(run, send, agent, options = {}) {
|
|
|
2006
2061
|
if (controlRequestsCancellation(response)) requestCliCancel();
|
|
2007
2062
|
})
|
|
2008
2063
|
.catch((error) => {
|
|
2064
|
+
if (isCompanionPairingInvalidError(error)) {
|
|
2065
|
+
requestCliCancel();
|
|
2066
|
+
}
|
|
2009
2067
|
if (!cliFinished) {
|
|
2010
2068
|
options.trace?.warn('cli_activity_post_failed', {
|
|
2011
2069
|
error: errorMeta(error),
|
|
@@ -2044,6 +2102,10 @@ async function executeModelTurnRun(run, send, agent, options = {}) {
|
|
|
2044
2102
|
if (cliCancelled) return;
|
|
2045
2103
|
} catch (error) {
|
|
2046
2104
|
if (monitorAbort.signal.aborted) return;
|
|
2105
|
+
if (isCompanionPairingInvalidError(error)) {
|
|
2106
|
+
requestCliCancel();
|
|
2107
|
+
return;
|
|
2108
|
+
}
|
|
2047
2109
|
options.trace?.warn('cli_control_poll_failed', {
|
|
2048
2110
|
error: errorMeta(error),
|
|
2049
2111
|
});
|
|
@@ -2150,7 +2212,7 @@ async function executeModelTurnRun(run, send, agent, options = {}) {
|
|
|
2150
2212
|
};
|
|
2151
2213
|
const usage = {
|
|
2152
2214
|
...usageSnapshot,
|
|
2153
|
-
modelCalls: [{ ...callTelemetry,
|
|
2215
|
+
modelCalls: [{ ...callTelemetry, callStatus: 'succeeded' }],
|
|
2154
2216
|
};
|
|
2155
2217
|
let completion;
|
|
2156
2218
|
try {
|
|
@@ -2158,7 +2220,7 @@ async function executeModelTurnRun(run, send, agent, options = {}) {
|
|
|
2158
2220
|
} catch (error) {
|
|
2159
2221
|
error.companionUsage = {
|
|
2160
2222
|
...usage,
|
|
2161
|
-
modelCalls: [{ ...callTelemetry,
|
|
2223
|
+
modelCalls: [{ ...callTelemetry, callStatus: 'rejected' }],
|
|
2162
2224
|
};
|
|
2163
2225
|
throw error;
|
|
2164
2226
|
}
|
|
@@ -2255,6 +2317,9 @@ async function executeOutcomeRun(run, send, agent, options = {}) {
|
|
|
2255
2317
|
}
|
|
2256
2318
|
})
|
|
2257
2319
|
.catch((error) => {
|
|
2320
|
+
if (isCompanionPairingInvalidError(error)) {
|
|
2321
|
+
void requestCancel('device_disconnect');
|
|
2322
|
+
}
|
|
2258
2323
|
if (!finished) {
|
|
2259
2324
|
options.trace?.warn?.('outcome_activity_post_failed', {
|
|
2260
2325
|
error: errorMeta(error),
|
|
@@ -2331,6 +2396,10 @@ async function executeOutcomeRun(run, send, agent, options = {}) {
|
|
|
2331
2396
|
await requestCancel(controlCancellationSource(response));
|
|
2332
2397
|
}
|
|
2333
2398
|
} catch (error) {
|
|
2399
|
+
if (isCompanionPairingInvalidError(error)) {
|
|
2400
|
+
await requestCancel('device_disconnect');
|
|
2401
|
+
return;
|
|
2402
|
+
}
|
|
2334
2403
|
if (!controlMonitorAbort.signal.aborted && !cancelRequested) {
|
|
2335
2404
|
options.trace?.warn?.('outcome_control_poll_failed', {
|
|
2336
2405
|
error: errorMeta(error),
|
|
@@ -2374,6 +2443,23 @@ async function executeOutcomeRun(run, send, agent, options = {}) {
|
|
|
2374
2443
|
},
|
|
2375
2444
|
}
|
|
2376
2445
|
: run.outcome;
|
|
2446
|
+
const referenceImagePaths = referenceAssetFiles.map((file) =>
|
|
2447
|
+
path.join(materialized.directory, file.path),
|
|
2448
|
+
);
|
|
2449
|
+
const materializedReferenceIds = new Set(
|
|
2450
|
+
referenceAssetFiles.map((file) => file.id),
|
|
2451
|
+
);
|
|
2452
|
+
// Materialized images ride on the first provider turn as native image input.
|
|
2453
|
+
const referenceAssets = [
|
|
2454
|
+
...new Set(
|
|
2455
|
+
requestAttachments
|
|
2456
|
+
.map((attachment) => String(attachment?.id || '').slice(0, 180))
|
|
2457
|
+
.filter(Boolean),
|
|
2458
|
+
),
|
|
2459
|
+
].map((id) => ({
|
|
2460
|
+
id,
|
|
2461
|
+
materialized: materializedReferenceIds.has(id),
|
|
2462
|
+
}));
|
|
2377
2463
|
const nativeSkills = normalizeNativeSkills(run?.outcome?.nativeSkills);
|
|
2378
2464
|
const nativeProvider =
|
|
2379
2465
|
definition.id === 'claude-code'
|
|
@@ -2400,6 +2486,7 @@ async function executeOutcomeRun(run, send, agent, options = {}) {
|
|
|
2400
2486
|
trace: options.trace,
|
|
2401
2487
|
onActivity: progress,
|
|
2402
2488
|
runCli: options.framerAgentRunCli,
|
|
2489
|
+
fetchImpl: options.framerScreenshotFetch,
|
|
2403
2490
|
authorizeProject:
|
|
2404
2491
|
options.apiBaseUrl && options.deviceToken
|
|
2405
2492
|
? ({ initiate, forceRefresh, signal }) =>
|
|
@@ -2519,6 +2606,9 @@ async function executeOutcomeRun(run, send, agent, options = {}) {
|
|
|
2519
2606
|
resumeSessionIdOverride,
|
|
2520
2607
|
step = 0,
|
|
2521
2608
|
) => {
|
|
2609
|
+
// User attachments ride on the first turn as native image input, exactly
|
|
2610
|
+
// as if pasted into the agent; follow-up turns already have them in context.
|
|
2611
|
+
const imageFiles = step === 0 ? referenceImagePaths : [];
|
|
2522
2612
|
if (options.providerAdapter?.runOutcome) {
|
|
2523
2613
|
const providerResult = await options.providerAdapter.runOutcome({
|
|
2524
2614
|
runId: run.runId,
|
|
@@ -2533,6 +2623,7 @@ async function executeOutcomeRun(run, send, agent, options = {}) {
|
|
|
2533
2623
|
cwd: materialized.directory,
|
|
2534
2624
|
nativeSkills,
|
|
2535
2625
|
framerHarness: framerAgentOutcome,
|
|
2626
|
+
imageFiles,
|
|
2536
2627
|
harnessTools: {
|
|
2537
2628
|
definitions: codexDynamicToolSpecs(harnessTools.definitions),
|
|
2538
2629
|
invoke: harnessTools.invoke,
|
|
@@ -2575,6 +2666,7 @@ async function executeOutcomeRun(run, send, agent, options = {}) {
|
|
|
2575
2666
|
framerHarness: framerAgentOutcome,
|
|
2576
2667
|
harnessToolNames: harnessTools.toolNames,
|
|
2577
2668
|
nativeSkillsEnabled: Boolean(materializedNativeSkills),
|
|
2669
|
+
imageFiles,
|
|
2578
2670
|
signal: infrastructureAbortController.signal,
|
|
2579
2671
|
...(mcpConfig ? { mcpConfig } : {}),
|
|
2580
2672
|
onProgress: progress,
|
|
@@ -2597,6 +2689,7 @@ async function executeOutcomeRun(run, send, agent, options = {}) {
|
|
|
2597
2689
|
framerHarness: framerAgentOutcome,
|
|
2598
2690
|
harnessToolNames: harnessTools.toolNames,
|
|
2599
2691
|
nativeSkillsEnabled: Boolean(materializedNativeSkills),
|
|
2692
|
+
imageFiles,
|
|
2600
2693
|
signal: infrastructureAbortController.signal,
|
|
2601
2694
|
...(mcpConfig ? { mcpConfig } : {}),
|
|
2602
2695
|
onProgress: progress,
|
|
@@ -2741,6 +2834,7 @@ async function executeOutcomeRun(run, send, agent, options = {}) {
|
|
|
2741
2834
|
outcome: {
|
|
2742
2835
|
...normalized,
|
|
2743
2836
|
files,
|
|
2837
|
+
referenceAssets,
|
|
2744
2838
|
},
|
|
2745
2839
|
model: selectedModelId,
|
|
2746
2840
|
providerSessionId:
|
|
@@ -2753,9 +2847,10 @@ async function executeOutcomeRun(run, send, agent, options = {}) {
|
|
|
2753
2847
|
callId: run.runId,
|
|
2754
2848
|
outcomeId: run?.outcome?.outcomeId,
|
|
2755
2849
|
model: selectedModelId,
|
|
2756
|
-
|
|
2850
|
+
callStatus: 'succeeded',
|
|
2851
|
+
outcomeStatus:
|
|
2757
2852
|
normalized.status === successStatus
|
|
2758
|
-
? '
|
|
2853
|
+
? 'completed'
|
|
2759
2854
|
: normalized.status,
|
|
2760
2855
|
durationMs: Date.now() - callStartedAt,
|
|
2761
2856
|
providerAttemptCount: Math.max(1, providerAttempts),
|
|
@@ -2810,6 +2905,7 @@ export async function executeRun(
|
|
|
2810
2905
|
inspectAgentAuthentication,
|
|
2811
2906
|
runtimeProfile: suppliedRuntimeProfile,
|
|
2812
2907
|
framerAgentRunCli,
|
|
2908
|
+
framerScreenshotFetch,
|
|
2813
2909
|
allowInsecureHttp = false,
|
|
2814
2910
|
} = {},
|
|
2815
2911
|
) {
|
|
@@ -2928,6 +3024,7 @@ export async function executeRun(
|
|
|
2928
3024
|
product,
|
|
2929
3025
|
runtimeProfile,
|
|
2930
3026
|
framerAgentRunCli,
|
|
3027
|
+
framerScreenshotFetch,
|
|
2931
3028
|
apiBaseUrl,
|
|
2932
3029
|
deviceToken,
|
|
2933
3030
|
fetchImpl,
|
|
@@ -2991,6 +3088,7 @@ export function checkCommand(command, args = ['--version'], timeoutMs = 10000) {
|
|
|
2991
3088
|
|
|
2992
3089
|
export async function checkAgentAvailability(agent, model, options = {}) {
|
|
2993
3090
|
const normalizedAgent = normalizeAgentName(agent);
|
|
3091
|
+
const modelCatalogRefreshedAt = nowIso();
|
|
2994
3092
|
if (normalizedAgent === 'dry-run') {
|
|
2995
3093
|
return {
|
|
2996
3094
|
ok: true,
|
|
@@ -3002,6 +3100,8 @@ export async function checkAgentAvailability(agent, model, options = {}) {
|
|
|
3002
3100
|
installed: true,
|
|
3003
3101
|
signedIn: true,
|
|
3004
3102
|
status: 'ready',
|
|
3103
|
+
modelCatalogSource: 'runtime',
|
|
3104
|
+
modelCatalogRefreshedAt,
|
|
3005
3105
|
};
|
|
3006
3106
|
}
|
|
3007
3107
|
const definition = definitionForAgent(normalizedAgent);
|
|
@@ -3025,6 +3125,8 @@ export async function checkAgentAvailability(agent, model, options = {}) {
|
|
|
3025
3125
|
installed: false,
|
|
3026
3126
|
signedIn: false,
|
|
3027
3127
|
status: 'unavailable',
|
|
3128
|
+
modelCatalogSource: 'fallback',
|
|
3129
|
+
modelCatalogRefreshedAt,
|
|
3028
3130
|
};
|
|
3029
3131
|
}
|
|
3030
3132
|
if (normalizedAgent === 'codex') {
|
|
@@ -3051,9 +3153,19 @@ export async function checkAgentAvailability(agent, model, options = {}) {
|
|
|
3051
3153
|
error:
|
|
3052
3154
|
detection.error
|
|
3053
3155
|
|| 'Codex is not signed in. Run `codex login` on this computer, then run the bridge command again.',
|
|
3156
|
+
modelCatalogSource: 'fallback',
|
|
3157
|
+
modelCatalogRefreshedAt,
|
|
3054
3158
|
};
|
|
3055
3159
|
}
|
|
3056
|
-
|
|
3160
|
+
let reportedModels = supportedModels;
|
|
3161
|
+
let modelCatalogSource = 'fallback';
|
|
3162
|
+
let modelCatalogError = null;
|
|
3163
|
+
try {
|
|
3164
|
+
reportedModels = await adapter.models();
|
|
3165
|
+
modelCatalogSource = 'runtime';
|
|
3166
|
+
} catch (error) {
|
|
3167
|
+
modelCatalogError = error?.message || String(error);
|
|
3168
|
+
}
|
|
3057
3169
|
return {
|
|
3058
3170
|
...runtime,
|
|
3059
3171
|
...detection,
|
|
@@ -3062,7 +3174,9 @@ export async function checkAgentAvailability(agent, model, options = {}) {
|
|
|
3062
3174
|
label: definition.label,
|
|
3063
3175
|
models: reportedModels.map((candidate) => candidate.id),
|
|
3064
3176
|
modelDetails: reportedModels,
|
|
3065
|
-
modelCatalogSource
|
|
3177
|
+
modelCatalogSource,
|
|
3178
|
+
modelCatalogRefreshedAt,
|
|
3179
|
+
...(modelCatalogError ? { modelCatalogError } : {}),
|
|
3066
3180
|
installed: true,
|
|
3067
3181
|
signedIn: true,
|
|
3068
3182
|
status: 'ready',
|
|
@@ -3108,6 +3222,7 @@ export async function checkAgentAvailability(agent, model, options = {}) {
|
|
|
3108
3222
|
models: reportedModels.map((candidate) => candidate.id),
|
|
3109
3223
|
modelDetails: reportedModels,
|
|
3110
3224
|
modelCatalogSource,
|
|
3225
|
+
modelCatalogRefreshedAt,
|
|
3111
3226
|
...(modelCatalogError ? { modelCatalogError } : {}),
|
|
3112
3227
|
installed: true,
|
|
3113
3228
|
};
|
package/src/agentOutput.js
CHANGED
|
@@ -301,6 +301,11 @@ function claudeToolProgress(name, input = {}, cwd) {
|
|
|
301
301
|
preview_control: ['Starting the live preview', 'Started the live preview'],
|
|
302
302
|
browser_control: ['Checking the app in the browser', 'Finished checking the app in the browser'],
|
|
303
303
|
data_inspect: ['Checking app data', 'Finished checking app data'],
|
|
304
|
+
data_schema_get: ['Reading the app tables', 'Finished reading the app tables'],
|
|
305
|
+
data_schema_apply: ['Updating the app tables', 'Finished updating the app tables'],
|
|
306
|
+
platform_catalog_search: ['Looking up connections', 'Found the available connections'],
|
|
307
|
+
platform_resource_configure: ['Setting up a connection', 'Finished setting up the connection'],
|
|
308
|
+
platform_resource_status: ['Checking a connection', 'Finished checking the connection'],
|
|
304
309
|
verification_run: ['Running final verification', 'Finished final verification'],
|
|
305
310
|
};
|
|
306
311
|
const [startedMessage, completedMessage] = messages[shortName] || [
|
package/src/api.js
CHANGED
|
@@ -16,6 +16,15 @@ export class DexterBridgeApiError extends Error {
|
|
|
16
16
|
}
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
export function isCompanionPairingInvalidError(error) {
|
|
20
|
+
return error instanceof DexterBridgeApiError
|
|
21
|
+
&& error.status === 401
|
|
22
|
+
&& (
|
|
23
|
+
error.body?.code === 'FRAMER_COMPANION_NOT_PAIRED'
|
|
24
|
+
|| /not paired|pair again/i.test(error.message)
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
19
28
|
function bearerHeaders(deviceToken) {
|
|
20
29
|
return deviceToken ? { Authorization: `Bearer ${deviceToken}` } : {};
|
|
21
30
|
}
|
package/src/cli.js
CHANGED
|
@@ -19,8 +19,8 @@ import {
|
|
|
19
19
|
import { checkAgentAvailability, checkAllAgents, executeRun } from './agent.js';
|
|
20
20
|
import {
|
|
21
21
|
claimPairing,
|
|
22
|
-
DexterBridgeApiError,
|
|
23
22
|
heartbeat,
|
|
23
|
+
isCompanionPairingInvalidError,
|
|
24
24
|
pollRun,
|
|
25
25
|
} from './api.js';
|
|
26
26
|
import {
|
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
} from './logger.js';
|
|
31
31
|
import { createLocalAgentAdapter } from './providers/index.js';
|
|
32
32
|
import {
|
|
33
|
+
diagnosticsFromAgentCheck,
|
|
33
34
|
discoverRuntimeProfiles,
|
|
34
35
|
normalizeRuntimeSelection,
|
|
35
36
|
publicRuntimeProfile,
|
|
@@ -228,12 +229,7 @@ function wait(delayMs) {
|
|
|
228
229
|
}
|
|
229
230
|
|
|
230
231
|
function isInvalidPairingError(error) {
|
|
231
|
-
return error
|
|
232
|
-
&& error.status === 401
|
|
233
|
-
&& (
|
|
234
|
-
error.body?.code === 'FRAMER_COMPANION_NOT_PAIRED'
|
|
235
|
-
|| /not paired|pair again/i.test(error.message)
|
|
236
|
-
);
|
|
232
|
+
return isCompanionPairingInvalidError(error);
|
|
237
233
|
}
|
|
238
234
|
|
|
239
235
|
function clearInvalidPairing(configDir, cause) {
|
|
@@ -266,7 +262,48 @@ function agentEnvironment(config = {}, baseEnv = process.env) {
|
|
|
266
262
|
return env;
|
|
267
263
|
}
|
|
268
264
|
|
|
269
|
-
|
|
265
|
+
function modelCatalogStatus(runtimeProfiles = []) {
|
|
266
|
+
return Object.fromEntries(
|
|
267
|
+
runtimeProfiles.flatMap((profile) => {
|
|
268
|
+
const agent =
|
|
269
|
+
profile.driver === 'codex-app-server'
|
|
270
|
+
? 'codex'
|
|
271
|
+
: profile.driver === 'claude-code-cli'
|
|
272
|
+
? 'claude-code'
|
|
273
|
+
: null;
|
|
274
|
+
return agent ? [[agent, profile.modelCatalog]] : [];
|
|
275
|
+
}),
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function preserveLastKnownModelCatalog(previousProfiles = [], nextProfiles = []) {
|
|
280
|
+
const previousById = new Map(previousProfiles.map((profile) => [profile.id, profile]));
|
|
281
|
+
return nextProfiles.map((profile) => {
|
|
282
|
+
const previous = previousById.get(profile.id);
|
|
283
|
+
if (
|
|
284
|
+
!previous?.models?.length
|
|
285
|
+
|| profile.modelCatalog?.source === 'runtime'
|
|
286
|
+
|| !profile.modelCatalog?.error
|
|
287
|
+
) {
|
|
288
|
+
return profile;
|
|
289
|
+
}
|
|
290
|
+
return {
|
|
291
|
+
...profile,
|
|
292
|
+
models: previous.models,
|
|
293
|
+
modelCatalog: {
|
|
294
|
+
...profile.modelCatalog,
|
|
295
|
+
source: 'stale',
|
|
296
|
+
modelCount: previous.models.length,
|
|
297
|
+
},
|
|
298
|
+
};
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
async function inspectAvailability(
|
|
303
|
+
config = {},
|
|
304
|
+
runtimeSelection = null,
|
|
305
|
+
{ forceModelRefresh = false, previousRuntimeProfiles = [] } = {},
|
|
306
|
+
) {
|
|
270
307
|
try {
|
|
271
308
|
const bridgeEnv = agentEnvironment(config);
|
|
272
309
|
const checks =
|
|
@@ -275,22 +312,33 @@ async function inspectAvailability(config = {}, runtimeSelection = null) {
|
|
|
275
312
|
agents: [
|
|
276
313
|
await checkAgentAvailability(runtimeSelection, undefined, {
|
|
277
314
|
env: bridgeEnv,
|
|
315
|
+
forceModelRefresh,
|
|
278
316
|
}),
|
|
279
317
|
],
|
|
280
318
|
dryRun: null,
|
|
281
319
|
}
|
|
282
320
|
: runtimeSelection === 'opencode'
|
|
283
321
|
? { agents: [], dryRun: null }
|
|
284
|
-
: await checkAllAgents({ env: bridgeEnv });
|
|
322
|
+
: await checkAllAgents({ env: bridgeEnv, forceModelRefresh });
|
|
285
323
|
const allChecks = [...checks.agents, ...(checks.dryRun ? [checks.dryRun] : [])];
|
|
286
324
|
const available = allChecks.filter((check) => check.ok);
|
|
287
|
-
const
|
|
325
|
+
const discoveredRuntimeProfiles = await discoverRuntimeProfiles({
|
|
288
326
|
agentChecks: checks.agents,
|
|
289
327
|
config,
|
|
290
328
|
env: bridgeEnv,
|
|
291
329
|
runtimeSelection,
|
|
292
330
|
});
|
|
331
|
+
const runtimeProfiles = preserveLastKnownModelCatalog(
|
|
332
|
+
previousRuntimeProfiles,
|
|
333
|
+
discoveredRuntimeProfiles,
|
|
334
|
+
);
|
|
293
335
|
const profileModels = runtimeProfiles.flatMap((profile) => profile.models || []);
|
|
336
|
+
const catalogStatus = modelCatalogStatus(runtimeProfiles);
|
|
337
|
+
const refreshedAt = Object.values(catalogStatus)
|
|
338
|
+
.map((status) => status?.refreshedAt)
|
|
339
|
+
.filter(Boolean)
|
|
340
|
+
.sort()
|
|
341
|
+
.at(-1);
|
|
294
342
|
return {
|
|
295
343
|
metadata: {
|
|
296
344
|
availableAgents: available.map((check) => check.agent).join(','),
|
|
@@ -298,6 +346,8 @@ async function inspectAvailability(config = {}, runtimeSelection = null) {
|
|
|
298
346
|
availableModelDetails: JSON.stringify(
|
|
299
347
|
profileModels,
|
|
300
348
|
),
|
|
349
|
+
modelCatalogStatus: JSON.stringify(catalogStatus),
|
|
350
|
+
...(refreshedAt ? { modelCatalogRefreshedAt: refreshedAt } : {}),
|
|
301
351
|
agentVersions: available.map((check) => `${check.agent}=${check.version || 'unknown'}`).join(','),
|
|
302
352
|
bridgeCapabilities: BRIDGE_CAPABILITIES.join(','),
|
|
303
353
|
bridgeVersion: BRIDGE_VERSION,
|
|
@@ -555,7 +605,10 @@ async function startCommand({ apiBaseUrl, config, flags, configDir }) {
|
|
|
555
605
|
while (true) {
|
|
556
606
|
if (Date.now() >= nextCatalogRefreshAt) {
|
|
557
607
|
try {
|
|
558
|
-
const refreshedAvailability = await inspectAvailability(activeConfig, runtimeSelection
|
|
608
|
+
const refreshedAvailability = await inspectAvailability(activeConfig, runtimeSelection, {
|
|
609
|
+
forceModelRefresh: true,
|
|
610
|
+
previousRuntimeProfiles: availability.runtimeProfiles,
|
|
611
|
+
});
|
|
559
612
|
const refreshedRuntimeProfile = runtimeSelection
|
|
560
613
|
? requireAvailableRuntime(refreshedAvailability, runtimeSelection)
|
|
561
614
|
: null;
|
|
@@ -598,7 +651,9 @@ async function startCommand({ apiBaseUrl, config, flags, configDir }) {
|
|
|
598
651
|
status: error.status,
|
|
599
652
|
code: error.body?.code || null,
|
|
600
653
|
});
|
|
601
|
-
|
|
654
|
+
const disconnected = clearInvalidPairing(configDir, error);
|
|
655
|
+
console.log(disconnected.message);
|
|
656
|
+
return;
|
|
602
657
|
}
|
|
603
658
|
if (once) throw error;
|
|
604
659
|
pollFailureCount += 1;
|
|
@@ -610,6 +665,45 @@ async function startCommand({ apiBaseUrl, config, flags, configDir }) {
|
|
|
610
665
|
await wait(retryInMs);
|
|
611
666
|
continue;
|
|
612
667
|
}
|
|
668
|
+
if (poll?.control?.refreshModelCatalog) {
|
|
669
|
+
try {
|
|
670
|
+
const refreshedAvailability = await inspectAvailability(activeConfig, runtimeSelection, {
|
|
671
|
+
forceModelRefresh: true,
|
|
672
|
+
previousRuntimeProfiles: availability.runtimeProfiles,
|
|
673
|
+
});
|
|
674
|
+
availability = refreshedAvailability;
|
|
675
|
+
runtimeProfile = runtimeSelection
|
|
676
|
+
? requireAvailableRuntime(refreshedAvailability, runtimeSelection)
|
|
677
|
+
: null;
|
|
678
|
+
check = runtimeProfile
|
|
679
|
+
? null
|
|
680
|
+
: requireAvailableAgent(refreshedAvailability, agent);
|
|
681
|
+
model = runtimeProfile
|
|
682
|
+
? runtimeProfile.models.find((candidate) => candidate.id === requestedModel)?.id
|
|
683
|
+
|| runtimeProfile.models[0].id
|
|
684
|
+
: availableModel(check, requestedModel);
|
|
685
|
+
metadata = availability.metadata;
|
|
686
|
+
profiles = runtimeProfileMap(availability.runtimeProfiles);
|
|
687
|
+
await heartbeat(apiBaseUrl, {
|
|
688
|
+
deviceToken,
|
|
689
|
+
status: 'ready',
|
|
690
|
+
agent,
|
|
691
|
+
model,
|
|
692
|
+
metadata,
|
|
693
|
+
runtimeProfiles: availability.runtimeProfiles.map(publicRuntimeProfile),
|
|
694
|
+
allowInsecureHttp: flags['allow-insecure-http'],
|
|
695
|
+
});
|
|
696
|
+
pollLogger.info('model_catalog_refresh_completed', {
|
|
697
|
+
requestedAt: poll.control.refreshModelCatalog.requestedAt,
|
|
698
|
+
models: availability.runtimeProfiles.flatMap((profile) =>
|
|
699
|
+
profile.models.map((candidate) => candidate.id)),
|
|
700
|
+
});
|
|
701
|
+
} catch (error) {
|
|
702
|
+
pollLogger.warn('model_catalog_refresh_failed', { error: errorMeta(error) });
|
|
703
|
+
} finally {
|
|
704
|
+
nextCatalogRefreshAt = Date.now() + MODEL_CATALOG_REFRESH_MS;
|
|
705
|
+
}
|
|
706
|
+
}
|
|
613
707
|
if (poll.run) {
|
|
614
708
|
console.log(`Claimed run ${poll.run.runId}.`);
|
|
615
709
|
const runtimeProfile = poll.run?.runtimeProfile || null;
|
|
@@ -661,19 +755,28 @@ async function startCommand({ apiBaseUrl, config, flags, configDir }) {
|
|
|
661
755
|
}
|
|
662
756
|
}
|
|
663
757
|
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
console.log(
|
|
669
|
-
console.log(`Models: ${profile.models.map((model) => model.displayName || model.id).join(', ')}`);
|
|
670
|
-
return;
|
|
758
|
+
function printDiagnostics(diagnostics) {
|
|
759
|
+
const marks = { info: '✓', warn: '!', error: '✗' };
|
|
760
|
+
for (const check of diagnostics.checks) {
|
|
761
|
+
console.log(` ${marks[check.level] || '-'} ${check.message}`);
|
|
762
|
+
if (check.hint) console.log(` ${check.hint}`);
|
|
671
763
|
}
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
async function doctorCommand(runtimeSelection = null) {
|
|
767
|
+
const bridgeEnv = agentEnvironment({});
|
|
768
|
+
const checks =
|
|
769
|
+
runtimeSelection === 'codex' || runtimeSelection === 'claude-code'
|
|
770
|
+
? [await checkAgentAvailability(runtimeSelection, undefined, { env: bridgeEnv })]
|
|
771
|
+
: (await checkAllAgents({ env: bridgeEnv })).agents;
|
|
772
|
+
let failed = false;
|
|
773
|
+
for (const check of checks) {
|
|
774
|
+
const diagnostics = diagnosticsFromAgentCheck(check, bridgeEnv);
|
|
775
|
+
failed ||= diagnostics.status === 'fail';
|
|
776
|
+
console.log(`${check.label || check.agent}: ${diagnostics.status}`);
|
|
777
|
+
printDiagnostics(diagnostics);
|
|
676
778
|
}
|
|
779
|
+
if (failed && runtimeSelection) process.exitCode = 1;
|
|
677
780
|
}
|
|
678
781
|
|
|
679
782
|
export async function runCli(argv) {
|