@gakim-digital/dexter-bridge 0.12.1 → 0.13.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/package.json +1 -1
- package/src/agent.js +144 -40
- package/src/agentOutput.js +6 -0
- package/src/api.js +75 -0
- package/src/cli.js +160 -110
- package/src/config.js +20 -0
- package/src/framerMcpClient.js +8 -8
- package/src/framerTools.js +151 -19
- package/src/protocol.js +63 -0
- package/src/providers/codexAppServer.js +50 -34
- package/src/taskEvidence.js +245 -0
- package/src/webflowTools.js +1045 -0
package/package.json
CHANGED
package/src/agent.js
CHANGED
|
@@ -6,7 +6,10 @@ import path from 'node:path';
|
|
|
6
6
|
import { fileURLToPath } from 'node:url';
|
|
7
7
|
import crossSpawn from 'cross-spawn';
|
|
8
8
|
import {
|
|
9
|
+
callWebflowPanelTool,
|
|
10
|
+
findWebflowImages,
|
|
9
11
|
getFramerProjectAuthorization,
|
|
12
|
+
webflowMcpEndpoint,
|
|
10
13
|
readFramerWebsite,
|
|
11
14
|
isCompanionPairingInvalidError,
|
|
12
15
|
postRunEvent,
|
|
@@ -29,6 +32,7 @@ import {
|
|
|
29
32
|
assertOutcomeResponseContract,
|
|
30
33
|
buildFramerOutcomePrompt,
|
|
31
34
|
buildOutcomePrompt,
|
|
35
|
+
buildWebflowOutcomePrompt,
|
|
32
36
|
buildModelTurnDeltaPrompt,
|
|
33
37
|
buildModelTurnFallbackPrompt,
|
|
34
38
|
buildModelTurnPrompt,
|
|
@@ -65,6 +69,7 @@ import {
|
|
|
65
69
|
createHarnessToolRuntime,
|
|
66
70
|
} from './harnessTools.js';
|
|
67
71
|
import { createFramerAgentToolRuntime } from './framerTools.js';
|
|
72
|
+
import { assertWebflowHarnessAgent, createWebflowToolRuntime } from './webflowTools.js';
|
|
68
73
|
|
|
69
74
|
const HARNESS_MCP_SERVER_PATH = fileURLToPath(
|
|
70
75
|
new URL('./harnessMcpServer.js', import.meta.url),
|
|
@@ -356,6 +361,18 @@ function writeClaudeSystemPromptFile(content) {
|
|
|
356
361
|
return file;
|
|
357
362
|
}
|
|
358
363
|
|
|
364
|
+
function claudeWebflowOutcomeSystemPrompt(product) {
|
|
365
|
+
const name = normalizeBridgeProduct(product).name;
|
|
366
|
+
return [
|
|
367
|
+
`You are an autonomous Webflow harness embedded inside ${name}.`,
|
|
368
|
+
'Work only through the Dexter Webflow tools connected to the user-selected site.',
|
|
369
|
+
'Inspect the site, perform the requested work, verify the result, and return the structured outcome.',
|
|
370
|
+
'Do not access local credentials, unrelated files, other sites, account settings, or billing.',
|
|
371
|
+
'Do not publish unless the assignment explicitly authorizes publishing.',
|
|
372
|
+
`Return only the structured outcome result requested by ${name}.`,
|
|
373
|
+
].join(' ');
|
|
374
|
+
}
|
|
375
|
+
|
|
359
376
|
function claudeOutcomeSystemPrompt(product) {
|
|
360
377
|
const name = normalizeBridgeProduct(product).name;
|
|
361
378
|
if (normalizeBridgeProduct(product).id === 'dexter') {
|
|
@@ -445,10 +462,11 @@ function argsWithClaudeIsolation(args, definition, options = {}) {
|
|
|
445
462
|
const harnessTools = harnessToolNames.map(
|
|
446
463
|
(name) => `mcp__instawebai__${name}`,
|
|
447
464
|
);
|
|
448
|
-
const
|
|
449
|
-
|
|
465
|
+
const designHarness = options.designHarness || options.framerHarness;
|
|
466
|
+
const webHosts = designHarness
|
|
467
|
+
? framerWebHosts(designHarness)
|
|
450
468
|
: [];
|
|
451
|
-
const nativeTools =
|
|
469
|
+
const nativeTools = designHarness
|
|
452
470
|
? [
|
|
453
471
|
...(options.nativeSkillsEnabled ? ['Skill'] : []),
|
|
454
472
|
// Web access is limited to the sites the user named. WebSearch cannot
|
|
@@ -482,10 +500,10 @@ function argsWithClaudeIsolation(args, definition, options = {}) {
|
|
|
482
500
|
const permissionMode = argsWithoutFlagValue(projectSettingsOnly, '--permission-mode');
|
|
483
501
|
permissionMode.push(
|
|
484
502
|
'--permission-mode',
|
|
485
|
-
options.harnessMode && !
|
|
503
|
+
options.harnessMode && !designHarness ? 'acceptEdits' : 'dontAsk',
|
|
486
504
|
);
|
|
487
505
|
if (options.harnessMode) {
|
|
488
|
-
const allowedTools =
|
|
506
|
+
const allowedTools = designHarness
|
|
489
507
|
? [
|
|
490
508
|
...(options.nativeSkillsEnabled ? ['Skill'] : []),
|
|
491
509
|
...webHosts.map((host) => `WebFetch(domain:${host})`),
|
|
@@ -505,8 +523,8 @@ function argsWithClaudeIsolation(args, definition, options = {}) {
|
|
|
505
523
|
// Framer run with skills leaves it off. The explicit tool list above is what
|
|
506
524
|
// keeps the shell and file tools out; it grants the Framer tools and Skill
|
|
507
525
|
// only, and settings stay limited to the workspace the bridge created.
|
|
508
|
-
const
|
|
509
|
-
if (options.harnessMode && !
|
|
526
|
+
const designRunWithSkills = Boolean(designHarness && options.nativeSkillsEnabled);
|
|
527
|
+
if (options.harnessMode && !designRunWithSkills && !argsIncludeFlag(configured, '--restricted')) {
|
|
510
528
|
configured.push('--restricted');
|
|
511
529
|
}
|
|
512
530
|
return configured;
|
|
@@ -1188,6 +1206,15 @@ export function agentFailureMessage(command, code, stdout = '', stderr = '') {
|
|
|
1188
1206
|
return agentFailureDetails(command, code, stdout, stderr).message;
|
|
1189
1207
|
}
|
|
1190
1208
|
|
|
1209
|
+
// Agent CLIs run in their own process group, so a signal that stops the
|
|
1210
|
+
// bridge never reaches them; stopping the bridge aborts them explicitly.
|
|
1211
|
+
const bridgeShutdown = new AbortController();
|
|
1212
|
+
|
|
1213
|
+
export function stopAgentProcesses() {
|
|
1214
|
+
if (bridgeShutdown.signal.aborted) return;
|
|
1215
|
+
bridgeShutdown.abort(new CompanionRunCancelledError('The Dexter bridge stopped.', 'device_disconnect'));
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1191
1218
|
function runProcess(
|
|
1192
1219
|
command,
|
|
1193
1220
|
args,
|
|
@@ -1206,8 +1233,9 @@ function runProcess(
|
|
|
1206
1233
|
} = {},
|
|
1207
1234
|
) {
|
|
1208
1235
|
return new Promise((resolve, reject) => {
|
|
1209
|
-
|
|
1210
|
-
|
|
1236
|
+
const stopped = [signal, bridgeShutdown.signal].find((candidate) => candidate?.aborted);
|
|
1237
|
+
if (stopped) {
|
|
1238
|
+
const error = stopped.reason instanceof Error ? stopped.reason : new Error('The model process was cancelled.');
|
|
1211
1239
|
error.code = error.code || 'RUN_CANCELLED';
|
|
1212
1240
|
reject(error);
|
|
1213
1241
|
return;
|
|
@@ -1278,6 +1306,7 @@ function runProcess(
|
|
|
1278
1306
|
clearTimeout(forceKillTimer);
|
|
1279
1307
|
clearTimeout(terminationFallbackTimer);
|
|
1280
1308
|
signal?.removeEventListener('abort', abort);
|
|
1309
|
+
bridgeShutdown.signal.removeEventListener('abort', abort);
|
|
1281
1310
|
error.stdout = stdout;
|
|
1282
1311
|
error.stderr = stderr;
|
|
1283
1312
|
reject(error);
|
|
@@ -1294,10 +1323,11 @@ function runProcess(
|
|
|
1294
1323
|
}, killGraceMs);
|
|
1295
1324
|
forceKillTimer.unref?.();
|
|
1296
1325
|
};
|
|
1297
|
-
const abort = () => {
|
|
1326
|
+
const abort = (event) => {
|
|
1298
1327
|
if (settled || terminationError) return;
|
|
1299
1328
|
clearTimers();
|
|
1300
|
-
const
|
|
1329
|
+
const reason = event?.target?.reason;
|
|
1330
|
+
const error = reason instanceof Error ? reason : new Error('The model process was cancelled.');
|
|
1301
1331
|
error.code = error.code || 'RUN_CANCELLED';
|
|
1302
1332
|
terminationError = error;
|
|
1303
1333
|
terminateChildTree(error);
|
|
@@ -1342,6 +1372,7 @@ function runProcess(
|
|
|
1342
1372
|
);
|
|
1343
1373
|
}, hardDeadlineMs);
|
|
1344
1374
|
signal?.addEventListener('abort', abort, { once: true });
|
|
1375
|
+
bridgeShutdown.signal.addEventListener('abort', abort, { once: true });
|
|
1345
1376
|
armInactivityTimer();
|
|
1346
1377
|
|
|
1347
1378
|
child.stdout.on('data', (chunk) => {
|
|
@@ -1392,6 +1423,7 @@ function runProcess(
|
|
|
1392
1423
|
clearTimeout(forceKillTimer);
|
|
1393
1424
|
clearTimeout(terminationFallbackTimer);
|
|
1394
1425
|
signal?.removeEventListener('abort', abort);
|
|
1426
|
+
bridgeShutdown.signal.removeEventListener('abort', abort);
|
|
1395
1427
|
stdoutProgress?.flush();
|
|
1396
1428
|
const meta = {
|
|
1397
1429
|
command,
|
|
@@ -1570,8 +1602,15 @@ class CompanionRunCancelledError extends Error {
|
|
|
1570
1602
|
}
|
|
1571
1603
|
|
|
1572
1604
|
function cancellationMessage(error) {
|
|
1605
|
+
const webflow = error?.webflow || error?.companionUsage?.webflow;
|
|
1606
|
+
if (webflow) {
|
|
1607
|
+
const mutationCalls = Number(webflow.mutationCalls) || 0;
|
|
1608
|
+
if (mutationCalls <= 0) return 'Stopped before Dexter completed a Webflow change.';
|
|
1609
|
+
return `Stopped after ${mutationCalls} successful Webflow change ${mutationCalls === 1 ? 'call' : 'calls'}. Those changes remain in Webflow.`;
|
|
1610
|
+
}
|
|
1573
1611
|
const framer = error?.framer || error?.companionUsage?.framer;
|
|
1574
|
-
|
|
1612
|
+
if (!framer) return 'Dexter stopped this request.';
|
|
1613
|
+
const mutationCalls = Number(framer.mutationCalls) || 0;
|
|
1575
1614
|
if (mutationCalls <= 0) {
|
|
1576
1615
|
return 'Stopped before Dexter completed a Framer change.';
|
|
1577
1616
|
}
|
|
@@ -1907,7 +1946,7 @@ async function callLocalJsonAgent(agent, prompt, options = {}) {
|
|
|
1907
1946
|
onProgress: options.onProgress,
|
|
1908
1947
|
stdoutProgress:
|
|
1909
1948
|
definition.id === 'claude-code'
|
|
1910
|
-
? createClaudeProgressParser(options.onProgress, { cwd: options.cwd })
|
|
1949
|
+
? createClaudeProgressParser(options.onProgress, { cwd: options.cwd, trace: options.trace })
|
|
1911
1950
|
: undefined,
|
|
1912
1951
|
});
|
|
1913
1952
|
}
|
|
@@ -2241,7 +2280,7 @@ async function executeModelTurn(run, send, agent, options = {}) {
|
|
|
2241
2280
|
});
|
|
2242
2281
|
} catch (error) {
|
|
2243
2282
|
if (cliCancelled || error?.code === 'RUN_CANCELLED') {
|
|
2244
|
-
throw new CompanionRunCancelledError();
|
|
2283
|
+
throw new CompanionRunCancelledError(undefined, error?.cancellationSource);
|
|
2245
2284
|
}
|
|
2246
2285
|
const failure = agentErrorWithOutputUsage(definition.id, error);
|
|
2247
2286
|
if (callContextMode !== 'delta' || !resumeFailure(failure)) throw failedModelCall(failure);
|
|
@@ -2273,7 +2312,7 @@ async function executeModelTurn(run, send, agent, options = {}) {
|
|
|
2273
2312
|
onProgress: reportCliProgress,
|
|
2274
2313
|
}).catch((error) => {
|
|
2275
2314
|
if (cliCancelled || error?.code === 'RUN_CANCELLED') {
|
|
2276
|
-
throw new CompanionRunCancelledError();
|
|
2315
|
+
throw new CompanionRunCancelledError(undefined, error?.cancellationSource);
|
|
2277
2316
|
}
|
|
2278
2317
|
throw failedModelCall(agentErrorWithOutputUsage(definition.id, error));
|
|
2279
2318
|
});
|
|
@@ -2383,10 +2422,13 @@ async function executeOutcomeRun(run, send, agent, options = {}) {
|
|
|
2383
2422
|
const framerAgentOutcome =
|
|
2384
2423
|
run?.outcome?.kind === 'framer-project'
|
|
2385
2424
|
|| run?.outcome?.context?.executionMode === 'framer-mcp';
|
|
2425
|
+
const webflowOutcome = run?.outcome?.kind === 'webflow-site';
|
|
2426
|
+
if (webflowOutcome) assertWebflowHarnessAgent(definition.id);
|
|
2427
|
+
const designOutcome = framerAgentOutcome || webflowOutcome;
|
|
2386
2428
|
// Every Dexter message starts a fresh provider session. The assignment
|
|
2387
2429
|
// already carries the recent conversation, and resuming would make every
|
|
2388
2430
|
// turn of this run re-read the whole previous run as well.
|
|
2389
|
-
const sessionKey =
|
|
2431
|
+
const sessionKey = designOutcome
|
|
2390
2432
|
? run?.runId || companionSessionKey(run)
|
|
2391
2433
|
: companionSessionKey(run);
|
|
2392
2434
|
const infrastructureAbortController = new AbortController();
|
|
@@ -2536,10 +2578,10 @@ async function executeOutcomeRun(run, send, agent, options = {}) {
|
|
|
2536
2578
|
Boolean(event.message || event.name) ||
|
|
2537
2579
|
event.kind === 'turn_started';
|
|
2538
2580
|
if (!meaningful) return;
|
|
2539
|
-
//
|
|
2581
|
+
// Design runs narrate in plain text rather than through a progress tool
|
|
2540
2582
|
// (a tool call costs a whole turn), so that text is the user's progress.
|
|
2541
2583
|
const narration =
|
|
2542
|
-
|
|
2584
|
+
designOutcome
|
|
2543
2585
|
&& event.kind === 'output_delta'
|
|
2544
2586
|
&& String(event.activity || '').startsWith('assistant:');
|
|
2545
2587
|
const userFacingUpdate =
|
|
@@ -2603,7 +2645,7 @@ async function executeOutcomeRun(run, send, agent, options = {}) {
|
|
|
2603
2645
|
}
|
|
2604
2646
|
};
|
|
2605
2647
|
const controlMonitor = monitorControl();
|
|
2606
|
-
const rememberedSession =
|
|
2648
|
+
const rememberedSession = designOutcome
|
|
2607
2649
|
? null
|
|
2608
2650
|
: rememberedCompanionSession(sessionKey) ||
|
|
2609
2651
|
(run?.outcome?.session?.providerSessionId
|
|
@@ -2704,6 +2746,61 @@ async function executeOutcomeRun(run, send, agent, options = {}) {
|
|
|
2704
2746
|
})
|
|
2705
2747
|
: undefined,
|
|
2706
2748
|
})
|
|
2749
|
+
: webflowOutcome
|
|
2750
|
+
? createWebflowToolRuntime({
|
|
2751
|
+
assignment: executionOutcome,
|
|
2752
|
+
trace: options.trace,
|
|
2753
|
+
onActivity: progress,
|
|
2754
|
+
mcpEndpoint:
|
|
2755
|
+
options.apiBaseUrl && options.deviceToken
|
|
2756
|
+
? webflowMcpEndpoint(options.apiBaseUrl, {
|
|
2757
|
+
deviceToken: options.deviceToken,
|
|
2758
|
+
runId: run.runId,
|
|
2759
|
+
allowInsecureHttp: options.allowInsecureHttp,
|
|
2760
|
+
})
|
|
2761
|
+
: undefined,
|
|
2762
|
+
callPanelTool:
|
|
2763
|
+
options.apiBaseUrl && options.deviceToken
|
|
2764
|
+
? ({ callId, name, arguments: toolArguments, signal }) =>
|
|
2765
|
+
callWebflowPanelTool(options.apiBaseUrl, {
|
|
2766
|
+
deviceToken: options.deviceToken,
|
|
2767
|
+
runId: run.runId,
|
|
2768
|
+
callId,
|
|
2769
|
+
name,
|
|
2770
|
+
arguments: toolArguments,
|
|
2771
|
+
fetchImpl: options.fetchImpl,
|
|
2772
|
+
signal,
|
|
2773
|
+
allowInsecureHttp: options.allowInsecureHttp,
|
|
2774
|
+
})
|
|
2775
|
+
: undefined,
|
|
2776
|
+
findImages:
|
|
2777
|
+
options.apiBaseUrl && options.deviceToken
|
|
2778
|
+
? ({ query, orientation, count, signal }) =>
|
|
2779
|
+
findWebflowImages(options.apiBaseUrl, {
|
|
2780
|
+
deviceToken: options.deviceToken,
|
|
2781
|
+
runId: run.runId,
|
|
2782
|
+
query,
|
|
2783
|
+
orientation,
|
|
2784
|
+
count,
|
|
2785
|
+
fetchImpl: options.fetchImpl,
|
|
2786
|
+
signal,
|
|
2787
|
+
allowInsecureHttp: options.allowInsecureHttp,
|
|
2788
|
+
})
|
|
2789
|
+
: undefined,
|
|
2790
|
+
readWebsite:
|
|
2791
|
+
options.apiBaseUrl && options.deviceToken
|
|
2792
|
+
? ({ url, screenshotTop, signal }) =>
|
|
2793
|
+
readFramerWebsite(options.apiBaseUrl, {
|
|
2794
|
+
deviceToken: options.deviceToken,
|
|
2795
|
+
runId: run.runId,
|
|
2796
|
+
url,
|
|
2797
|
+
screenshotTop,
|
|
2798
|
+
fetchImpl: options.fetchImpl,
|
|
2799
|
+
signal,
|
|
2800
|
+
allowInsecureHttp: options.allowInsecureHttp,
|
|
2801
|
+
})
|
|
2802
|
+
: undefined,
|
|
2803
|
+
})
|
|
2707
2804
|
: createHarnessToolRuntime({
|
|
2708
2805
|
assignment: executionOutcome,
|
|
2709
2806
|
materialized,
|
|
@@ -2725,10 +2822,10 @@ async function executeOutcomeRun(run, send, agent, options = {}) {
|
|
|
2725
2822
|
let providerAttempts = 0;
|
|
2726
2823
|
let result;
|
|
2727
2824
|
try {
|
|
2728
|
-
const preflight =
|
|
2825
|
+
const preflight = designOutcome
|
|
2729
2826
|
? await Promise.race([harnessTools.preflight(), cancellation])
|
|
2730
2827
|
: null;
|
|
2731
|
-
const
|
|
2828
|
+
const projectGuidance = designOutcome
|
|
2732
2829
|
? await Promise.race([
|
|
2733
2830
|
harnessTools.loadProjectGuidance(),
|
|
2734
2831
|
cancellation,
|
|
@@ -2737,8 +2834,9 @@ async function executeOutcomeRun(run, send, agent, options = {}) {
|
|
|
2737
2834
|
const promptContext = {
|
|
2738
2835
|
doctrine: run?.doctrine,
|
|
2739
2836
|
doctrineVersion: run?.doctrineVersion,
|
|
2740
|
-
framerGuidance:
|
|
2741
|
-
projectInventory:
|
|
2837
|
+
framerGuidance: framerAgentOutcome ? projectGuidance?.content : undefined,
|
|
2838
|
+
projectInventory: framerAgentOutcome ? projectGuidance?.inventory : undefined,
|
|
2839
|
+
webflowGuidance: webflowOutcome ? projectGuidance?.content : undefined,
|
|
2742
2840
|
};
|
|
2743
2841
|
// Claude Code gets the run-independent instructions as its system prompt,
|
|
2744
2842
|
// which it caches across sessions: later runs and follow-up messages read
|
|
@@ -2746,16 +2844,14 @@ async function executeOutcomeRun(run, send, agent, options = {}) {
|
|
|
2746
2844
|
let systemPromptFile = null;
|
|
2747
2845
|
let systemPromptBytes = 0;
|
|
2748
2846
|
if (
|
|
2749
|
-
|
|
2847
|
+
designOutcome
|
|
2750
2848
|
&& definition.id === 'claude-code'
|
|
2751
2849
|
&& !options.providerAdapter?.runOutcome
|
|
2752
2850
|
) {
|
|
2753
|
-
const { instructions, task } =
|
|
2754
|
-
executionOutcome,
|
|
2755
|
-
product,
|
|
2756
|
-
|
|
2757
|
-
);
|
|
2758
|
-
const systemPrompt = `${claudeOutcomeSystemPrompt(product)}\n\n${instructions}`;
|
|
2851
|
+
const { instructions, task } = webflowOutcome
|
|
2852
|
+
? buildWebflowOutcomePrompt(executionOutcome, product, promptContext)
|
|
2853
|
+
: buildFramerOutcomePrompt(executionOutcome, product, promptContext);
|
|
2854
|
+
const systemPrompt = `${(webflowOutcome ? claudeWebflowOutcomeSystemPrompt : claudeOutcomeSystemPrompt)(product)}\n\n${instructions}`;
|
|
2759
2855
|
systemPromptFile = writeClaudeSystemPromptFile(systemPrompt);
|
|
2760
2856
|
systemPromptBytes = Buffer.byteLength(systemPrompt, 'utf8');
|
|
2761
2857
|
prompt = task;
|
|
@@ -2837,7 +2933,8 @@ async function executeOutcomeRun(run, send, agent, options = {}) {
|
|
|
2837
2933
|
cwd: materialized.directory,
|
|
2838
2934
|
nativeSkills,
|
|
2839
2935
|
framerHarness: framerAgentOutcome ? executionOutcome : false,
|
|
2840
|
-
|
|
2936
|
+
designHarness: designOutcome ? executionOutcome : false,
|
|
2937
|
+
webHosts: designOutcome ? framerWebHosts(executionOutcome) : [],
|
|
2841
2938
|
imageFiles,
|
|
2842
2939
|
harnessTools: {
|
|
2843
2940
|
definitions: codexDynamicToolSpecs(harnessTools.definitions),
|
|
@@ -2880,6 +2977,7 @@ async function executeOutcomeRun(run, send, agent, options = {}) {
|
|
|
2880
2977
|
harnessMode: true,
|
|
2881
2978
|
systemPromptFile,
|
|
2882
2979
|
framerHarness: framerAgentOutcome ? executionOutcome : false,
|
|
2980
|
+
designHarness: designOutcome ? executionOutcome : false,
|
|
2883
2981
|
harnessToolNames: harnessTools.toolNames,
|
|
2884
2982
|
nativeSkillsEnabled: Boolean(materializedNativeSkills),
|
|
2885
2983
|
imageFiles,
|
|
@@ -2904,6 +3002,7 @@ async function executeOutcomeRun(run, send, agent, options = {}) {
|
|
|
2904
3002
|
harnessMode: true,
|
|
2905
3003
|
systemPromptFile,
|
|
2906
3004
|
framerHarness: framerAgentOutcome ? executionOutcome : false,
|
|
3005
|
+
designHarness: designOutcome ? executionOutcome : false,
|
|
2907
3006
|
harnessToolNames: harnessTools.toolNames,
|
|
2908
3007
|
nativeSkillsEnabled: Boolean(materializedNativeSkills),
|
|
2909
3008
|
imageFiles,
|
|
@@ -2993,14 +3092,16 @@ async function executeOutcomeRun(run, send, agent, options = {}) {
|
|
|
2993
3092
|
source: 'harness',
|
|
2994
3093
|
message:
|
|
2995
3094
|
repairPrompt === unappliedChangePrompt
|
|
2996
|
-
?
|
|
3095
|
+
? `Nothing has changed in ${webflowOutcome ? 'Webflow' : 'Framer'} yet, so I’m making the change before I finish.`
|
|
2997
3096
|
: rawCompletion.status === successStatus
|
|
2998
|
-
?
|
|
3097
|
+
? (webflowOutcome
|
|
3098
|
+
? 'I’m completing the final Webflow verification before I finish.'
|
|
3099
|
+
: 'I’m completing the final Framer verification before I finish.')
|
|
2999
3100
|
: 'Nothing is blocking this change, so I’m continuing the work.',
|
|
3000
3101
|
});
|
|
3001
3102
|
const repairResult = await Promise.race([
|
|
3002
3103
|
executeProviderOutcome(
|
|
3003
|
-
repairPrompt
|
|
3104
|
+
`${repairPrompt} The designer never sees this note: your summary still answers their original request and does not mention it.`,
|
|
3004
3105
|
providerSessionId
|
|
3005
3106
|
|| rememberedSession?.providerSessionId
|
|
3006
3107
|
|| sessionKey,
|
|
@@ -3055,7 +3156,7 @@ async function executeOutcomeRun(run, send, agent, options = {}) {
|
|
|
3055
3156
|
synchronization?.sourceHash || harnessTools.sourceHash() || null,
|
|
3056
3157
|
...(typeof harnessTools.summary === 'function'
|
|
3057
3158
|
? {
|
|
3058
|
-
framer: {
|
|
3159
|
+
[webflowOutcome ? 'webflow' : 'framer']: {
|
|
3059
3160
|
...harnessTools.summary(),
|
|
3060
3161
|
webCalls: webCallSummary(),
|
|
3061
3162
|
},
|
|
@@ -3098,19 +3199,22 @@ async function executeOutcomeRun(run, send, agent, options = {}) {
|
|
|
3098
3199
|
const failure =
|
|
3099
3200
|
error instanceof Error
|
|
3100
3201
|
? error
|
|
3101
|
-
: new Error(String(error || 'The
|
|
3202
|
+
: new Error(String(error || 'The Dexter run failed.'));
|
|
3102
3203
|
usageAccumulator.add(failure.companionUsage || {});
|
|
3103
|
-
const
|
|
3204
|
+
const harnessSummary =
|
|
3104
3205
|
typeof harnessTools.summary === 'function'
|
|
3105
3206
|
? {
|
|
3106
3207
|
...harnessTools.summary(),
|
|
3107
3208
|
webCalls: webCallSummary(),
|
|
3108
3209
|
}
|
|
3109
3210
|
: undefined;
|
|
3110
|
-
failure.
|
|
3211
|
+
if (webflowOutcome) failure.webflow = harnessSummary;
|
|
3212
|
+
else failure.framer = harnessSummary;
|
|
3111
3213
|
failure.companionUsage = {
|
|
3112
3214
|
...usageAccumulator.snapshot(),
|
|
3113
|
-
...(
|
|
3215
|
+
...(harnessSummary
|
|
3216
|
+
? { [webflowOutcome ? 'webflow' : 'framer']: harnessSummary }
|
|
3217
|
+
: {}),
|
|
3114
3218
|
};
|
|
3115
3219
|
throw failure;
|
|
3116
3220
|
} finally {
|
|
@@ -3118,7 +3222,7 @@ async function executeOutcomeRun(run, send, agent, options = {}) {
|
|
|
3118
3222
|
controlMonitorAbort.abort();
|
|
3119
3223
|
await controlMonitor.catch(() => undefined);
|
|
3120
3224
|
await harnessTools
|
|
3121
|
-
.close({ waitForPending: !cancelRequested })
|
|
3225
|
+
.close({ waitForPending: !cancelRequested && !bridgeShutdown.signal.aborted })
|
|
3122
3226
|
.catch(() => undefined);
|
|
3123
3227
|
removeOutcomeReferenceAssets(materialized);
|
|
3124
3228
|
removeOutcomeWorkspace(materialized);
|
package/src/agentOutput.js
CHANGED
|
@@ -521,6 +521,12 @@ export function createClaudeProgressParser(onProgress, options = {}) {
|
|
|
521
521
|
if (!trimmed) return;
|
|
522
522
|
try {
|
|
523
523
|
const row = JSON.parse(trimmed);
|
|
524
|
+
if (row?.type === 'system' && row.subtype !== 'init') {
|
|
525
|
+
options.trace?.warn?.('claude_system_event', { event: JSON.stringify(row).slice(0, 1_000) });
|
|
526
|
+
}
|
|
527
|
+
if (row?.type === 'rate_limit_event' && row.rate_limit_info?.status !== 'allowed') {
|
|
528
|
+
options.trace?.warn?.('claude_rate_limit', { rateLimit: row.rate_limit_info });
|
|
529
|
+
}
|
|
524
530
|
if (row?.type === 'stream_event') processStreamEvent(row);
|
|
525
531
|
if (row?.type === 'assistant') finishDrafting(row);
|
|
526
532
|
if (row?.type === 'assistant') processAssistant(row);
|
package/src/api.js
CHANGED
|
@@ -184,6 +184,7 @@ export function pollRun(apiBaseUrl, {
|
|
|
184
184
|
model,
|
|
185
185
|
metadata,
|
|
186
186
|
runtimeProfiles,
|
|
187
|
+
refreshingModelCatalog = false,
|
|
187
188
|
fetchImpl,
|
|
188
189
|
allowInsecureHttp,
|
|
189
190
|
}) {
|
|
@@ -202,6 +203,7 @@ export function pollRun(apiBaseUrl, {
|
|
|
202
203
|
status: 'polling',
|
|
203
204
|
metadata: bridgeMetadata(normalizedAgent, normalizedModel, metadata),
|
|
204
205
|
runtimeProfiles,
|
|
206
|
+
refreshingModelCatalog,
|
|
205
207
|
},
|
|
206
208
|
});
|
|
207
209
|
}
|
|
@@ -265,6 +267,79 @@ export function readFramerWebsite(apiBaseUrl, {
|
|
|
265
267
|
);
|
|
266
268
|
}
|
|
267
269
|
|
|
270
|
+
// The run's Webflow MCP endpoint on Dexter's API. The Webflow token never leaves the API.
|
|
271
|
+
export function webflowMcpEndpoint(apiBaseUrl, { deviceToken, runId, allowInsecureHttp }) {
|
|
272
|
+
return {
|
|
273
|
+
url: `${normalizeApiBaseUrl(apiBaseUrl, { allowInsecureHttp })}/ai/framer/companion/runs/${encodeURIComponent(runId)}/webflow-mcp`,
|
|
274
|
+
headers: {
|
|
275
|
+
...bearerHeaders(deviceToken),
|
|
276
|
+
'X-Dexter-Companion-Version': BRIDGE_VERSION,
|
|
277
|
+
'X-Dexter-Platform': process.platform,
|
|
278
|
+
},
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
export function callWebflowPanelTool(apiBaseUrl, {
|
|
283
|
+
deviceToken,
|
|
284
|
+
runId,
|
|
285
|
+
callId,
|
|
286
|
+
name,
|
|
287
|
+
arguments: toolArguments,
|
|
288
|
+
fetchImpl,
|
|
289
|
+
signal,
|
|
290
|
+
allowInsecureHttp,
|
|
291
|
+
}) {
|
|
292
|
+
return requestJson(
|
|
293
|
+
apiBaseUrl,
|
|
294
|
+
`/ai/framer/companion/runs/${encodeURIComponent(runId)}/panel-tools`,
|
|
295
|
+
{
|
|
296
|
+
method: 'POST',
|
|
297
|
+
deviceToken,
|
|
298
|
+
fetchImpl,
|
|
299
|
+
signal,
|
|
300
|
+
allowInsecureHttp,
|
|
301
|
+
timeoutMs: 45_000,
|
|
302
|
+
body: {
|
|
303
|
+
callId,
|
|
304
|
+
name,
|
|
305
|
+
arguments: toolArguments || {},
|
|
306
|
+
platform: process.platform,
|
|
307
|
+
companionVersion: BRIDGE_VERSION,
|
|
308
|
+
},
|
|
309
|
+
},
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
export function findWebflowImages(apiBaseUrl, {
|
|
314
|
+
deviceToken,
|
|
315
|
+
runId,
|
|
316
|
+
query,
|
|
317
|
+
orientation,
|
|
318
|
+
count,
|
|
319
|
+
fetchImpl,
|
|
320
|
+
signal,
|
|
321
|
+
allowInsecureHttp,
|
|
322
|
+
}) {
|
|
323
|
+
return requestJson(
|
|
324
|
+
apiBaseUrl,
|
|
325
|
+
`/ai/framer/companion/runs/${encodeURIComponent(runId)}/find-images`,
|
|
326
|
+
{
|
|
327
|
+
method: 'POST',
|
|
328
|
+
deviceToken,
|
|
329
|
+
fetchImpl,
|
|
330
|
+
signal,
|
|
331
|
+
allowInsecureHttp,
|
|
332
|
+
body: {
|
|
333
|
+
query,
|
|
334
|
+
...(orientation ? { orientation } : {}),
|
|
335
|
+
...(count ? { count } : {}),
|
|
336
|
+
platform: process.platform,
|
|
337
|
+
companionVersion: BRIDGE_VERSION,
|
|
338
|
+
},
|
|
339
|
+
},
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
|
|
268
343
|
export function postRunEvent(apiBaseUrl, {
|
|
269
344
|
deviceToken,
|
|
270
345
|
runId,
|