@gakim-digital/dexter-bridge 0.12.2 → 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/LICENSE ADDED
@@ -0,0 +1,16 @@
1
+ Dexter Bridge License
2
+
3
+ Copyright (c) 2026 InstaWeb AI. All rights reserved.
4
+
5
+ Permission is granted to download, install, and run this software solely to
6
+ connect your local machine to the Dexter / InstaWeb AI service. You may not
7
+ modify, redistribute, sublicense, or sell this software or use it to build a
8
+ competing service.
9
+
10
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
11
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
12
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
13
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
14
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
15
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
16
+ SOFTWARE.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gakim-digital/dexter-bridge",
3
- "version": "0.12.2",
3
+ "version": "0.13.0",
4
4
  "description": "Local bridge for InstaWebAI and Dexter — runs Codex, Claude Code, or OpenCode on your machine.",
5
5
  "type": "module",
6
6
  "bin": {
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 webHosts = options.framerHarness
449
- ? framerWebHosts(options.framerHarness)
465
+ const designHarness = options.designHarness || options.framerHarness;
466
+ const webHosts = designHarness
467
+ ? framerWebHosts(designHarness)
450
468
  : [];
451
- const nativeTools = options.framerHarness
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 && !options.framerHarness ? 'acceptEdits' : 'dontAsk',
503
+ options.harnessMode && !designHarness ? 'acceptEdits' : 'dontAsk',
486
504
  );
487
505
  if (options.harnessMode) {
488
- const allowedTools = options.framerHarness
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 framerRunWithSkills = Boolean(options.framerHarness && options.nativeSkillsEnabled);
509
- if (options.harnessMode && !framerRunWithSkills && !argsIncludeFlag(configured, '--restricted')) {
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
- if (signal?.aborted) {
1210
- const error = signal.reason instanceof Error ? signal.reason : new Error('The model process was cancelled.');
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 error = signal?.reason instanceof Error ? signal.reason : new Error('The model process was cancelled.');
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
- const mutationCalls = Number(framer?.mutationCalls) || 0;
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 = framerAgentOutcome
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
- // Framer runs narrate in plain text rather than through a progress tool
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
- framerAgentOutcome
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 = framerAgentOutcome
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 = framerAgentOutcome
2825
+ const preflight = designOutcome
2729
2826
  ? await Promise.race([harnessTools.preflight(), cancellation])
2730
2827
  : null;
2731
- const framerGuidance = framerAgentOutcome
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: framerGuidance?.content,
2741
- projectInventory: framerGuidance?.inventory,
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
- framerAgentOutcome
2847
+ designOutcome
2750
2848
  && definition.id === 'claude-code'
2751
2849
  && !options.providerAdapter?.runOutcome
2752
2850
  ) {
2753
- const { instructions, task } = buildFramerOutcomePrompt(
2754
- executionOutcome,
2755
- product,
2756
- promptContext,
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
- webHosts: framerAgentOutcome ? framerWebHosts(executionOutcome) : [],
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,9 +3092,11 @@ async function executeOutcomeRun(run, send, agent, options = {}) {
2993
3092
  source: 'harness',
2994
3093
  message:
2995
3094
  repairPrompt === unappliedChangePrompt
2996
- ? 'Nothing has changed in Framer yet, so I’m making the change before I finish.'
3095
+ ? `Nothing has changed in ${webflowOutcome ? 'Webflow' : 'Framer'} yet, so I’m making the change before I finish.`
2997
3096
  : rawCompletion.status === successStatus
2998
- ? 'I’m completing the final Framer verification before I finish.'
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([
@@ -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 Framer outcome failed.'));
3202
+ : new Error(String(error || 'The Dexter run failed.'));
3102
3203
  usageAccumulator.add(failure.companionUsage || {});
3103
- const framer =
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.framer = framer;
3211
+ if (webflowOutcome) failure.webflow = harnessSummary;
3212
+ else failure.framer = harnessSummary;
3111
3213
  failure.companionUsage = {
3112
3214
  ...usageAccumulator.snapshot(),
3113
- ...(framer ? { framer } : {}),
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);
@@ -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,