@gakim-digital/dexter-bridge 0.11.13 → 0.11.15

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 CHANGED
@@ -47,18 +47,15 @@ Native shell, filesystem, web, and unrelated-project access are disabled for
47
47
  Framer outcome runs. Publishing is denied unless a future assignment explicitly
48
48
  authorizes it.
49
49
 
50
- Bridges advertise `framer-interaction-verification-v3`,
51
- `framer-behavior-contract-v1`, and `framer-mechanism-verification-v1` only
52
- when interaction work is declared as observable behavior, checked against the
53
- selected Framer mechanism before mutation, and verified from live canonical
54
- Framer nodes afterward. Verification is tri-state: `verified`,
55
- `contradicted`, or `unknown`. Unknown evidence cannot trigger a repair or
56
- reject a mechanism. Native navigation is verified from link and component
57
- control destinations instead of requiring synthetic frame events. Repeated
58
- behavior is verified on one declared representative target before the
59
- remaining targets are changed. Only a concrete contradiction may receive one
60
- scoped `SET` repair. Repeated unchanged evidence is bounded and exits as a
61
- partial result rather than looping.
50
+ Bridges advertise `framer-interaction-verification-v4`. Dexter does not model
51
+ Framer's interaction features: the model builds behaviors with any capability
52
+ the Framer guidance documents, declares each behavior with the nodes that
53
+ should carry it, and then names the Framer property that carries it on each
54
+ node (for example `loopEffect` or `link`). The bridge confirms that property on
55
+ the live node. Results are `verified`, `contradicted` (the value differs), or
56
+ `unknown` (the property was not found). Unknown never blocks edits and is
57
+ reported as unverified; an unchecked target or a contradiction leaves the run
58
+ incomplete.
62
59
 
63
60
  Project connection is handled inside Dexter and completes before the model
64
61
  starts:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gakim-digital/dexter-bridge",
3
- "version": "0.11.13",
3
+ "version": "0.11.15",
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": {
@@ -39,12 +39,13 @@
39
39
  "@ai-sdk/openai-compatible": "^3.0.41",
40
40
  "@ai-sdk/xai": "^4.0.50",
41
41
  "@anthropic-ai/claude-agent-sdk": "^0.2.73",
42
- "@framer/agent": "0.0.44",
42
+ "@framer/agent": "0.0.46",
43
43
  "@modelcontextprotocol/sdk": "^1.30.0",
44
44
  "@opencode-ai/sdk": "^1.18.25",
45
45
  "@openrouter/ai-sdk-provider": "^3.0.0",
46
46
  "ai": "^7.0.85",
47
47
  "cross-spawn": "^7.0.6",
48
+ "js-tokens": "^10.0.0",
48
49
  "jsonrepair": "^3.15.0",
49
50
  "zod": "^4.5.4"
50
51
  }
package/src/agent.js CHANGED
@@ -51,6 +51,7 @@ import { createLocalAgentAdapter } from './providers/index.js';
51
51
  import { discoverClaudeCodeModels } from './providers/claudeAgentSdk.js';
52
52
  import {
53
53
  collectOutcomeWorkspaceChanges,
54
+ materializeModelTurnImages,
54
55
  materializeOutcomeReferenceAssets,
55
56
  materializeOutcomeWorkspace,
56
57
  removeOutcomeReferenceAssets,
@@ -1829,6 +1830,15 @@ function agentErrorWithOutputUsage(agent, error) {
1829
1830
  }
1830
1831
 
1831
1832
  async function executeModelTurnRun(run, send, agent, options = {}) {
1833
+ const images = materializeModelTurnImages(run?.modelTurn?.messages);
1834
+ try {
1835
+ return await executeModelTurn(run, send, agent, { ...options, imageFiles: images.files });
1836
+ } finally {
1837
+ images.remove();
1838
+ }
1839
+ }
1840
+
1841
+ async function executeModelTurn(run, send, agent, options = {}) {
1832
1842
  const product = normalizeBridgeProduct(run?.product || options.product);
1833
1843
  const productName = product.name;
1834
1844
  if (normalizeAgentName(agent) === 'dry-run' && !options.providerAdapter) {
@@ -1954,6 +1964,7 @@ async function executeModelTurnRun(run, send, agent, options = {}) {
1954
1964
  1000,
1955
1965
  ),
1956
1966
  nativeSkills,
1967
+ imageFiles: options.imageFiles,
1957
1968
  };
1958
1969
  const adapterOptions = {
1959
1970
  send,
@@ -2568,27 +2579,36 @@ async function executeOutcomeRun(run, send, agent, options = {}) {
2568
2579
  let providerAttempts = 0;
2569
2580
  let result;
2570
2581
  try {
2571
- if (framerAgentOutcome) {
2572
- await Promise.race([harnessTools.preflight(), cancellation]);
2573
- }
2582
+ const preflight = framerAgentOutcome
2583
+ ? await Promise.race([harnessTools.preflight(), cancellation])
2584
+ : null;
2574
2585
  const framerGuidance = framerAgentOutcome
2575
2586
  ? await Promise.race([
2576
2587
  harnessTools.loadProjectGuidance(),
2577
2588
  cancellation,
2578
2589
  ])
2579
2590
  : null;
2580
- harnessTools.configurePromptContext?.({
2581
- doctrineLoaded: Boolean(run?.doctrine),
2582
- doctrineVersion: run?.doctrineVersion,
2583
- });
2584
2591
  prompt = buildOutcomePrompt(executionOutcome, product, {
2585
2592
  doctrine: run?.doctrine,
2586
2593
  doctrineVersion: run?.doctrineVersion,
2587
2594
  framerGuidance: framerGuidance?.content,
2588
2595
  });
2596
+ const unresolvedTargets = preflight?.unresolvedTargets || [];
2597
+ if (unresolvedTargets.length > 0) {
2598
+ prompt = [
2599
+ prompt,
2600
+ '',
2601
+ `These selected layer IDs do not resolve to native nodes on the active page: ${unresolvedTargets.map((target) => target.id).join(', ')}. The user still means those layers. Locate them by name and position in the provided context and the live tree, treat the matching native nodes as the selection, and ask only if no layer clearly matches.`,
2602
+ ].join('\n');
2603
+ }
2589
2604
  if (materializedNativeSkills) {
2590
2605
  prompt = claudeNativeSkillPrompt(prompt, materializedNativeSkills);
2591
2606
  }
2607
+ harnessTools.configurePromptContext?.({
2608
+ doctrineLoaded: Boolean(run?.doctrine),
2609
+ doctrineVersion: run?.doctrineVersion,
2610
+ promptBytes: Buffer.byteLength(prompt, 'utf8'),
2611
+ });
2592
2612
  const gateway =
2593
2613
  definition.id === 'codex' ? null : await harnessTools.startGateway();
2594
2614
  const mcpServer = gateway
@@ -2785,7 +2805,7 @@ async function executeOutcomeRun(run, send, agent, options = {}) {
2785
2805
  rawCompletion.status === successStatus
2786
2806
  && typeof harnessTools.verificationRepairPrompt === 'function'
2787
2807
  ? harnessTools.verificationRepairPrompt()
2788
- : null;
2808
+ : harnessTools.unblockedFailurePrompt?.(rawCompletion) || null;
2789
2809
  if (
2790
2810
  repairPrompt
2791
2811
  && Date.now() - callStartedAt < outcomeMaxDurationMs - 5_000
@@ -2794,7 +2814,9 @@ async function executeOutcomeRun(run, send, agent, options = {}) {
2794
2814
  kind: 'status_update',
2795
2815
  source: 'harness',
2796
2816
  message:
2797
- 'I’m completing the final Framer verification before I finish.',
2817
+ rawCompletion.status === successStatus
2818
+ ? 'I’m completing the final Framer verification before I finish.'
2819
+ : 'Nothing is blocking this change, so I’m continuing the work.',
2798
2820
  });
2799
2821
  const repairResult = await Promise.race([
2800
2822
  executeProviderOutcome(
package/src/cli.js CHANGED
@@ -232,13 +232,63 @@ function isInvalidPairingError(error) {
232
232
  return isCompanionPairingInvalidError(error);
233
233
  }
234
234
 
235
- function clearInvalidPairing(configDir, cause) {
236
- const product = normalizeBridgeProduct(readConfig(configDir).product);
237
- saveConfigPatch({
238
- deviceToken: null,
239
- device: null,
240
- pairedAt: null,
241
- }, configDir);
235
+ const HOMEBREW_FORMULA = 'gakim-digital/tap/dexter-bridge';
236
+ const SELF_UPDATE_INTERVAL_MS = 60 * 60 * 1000;
237
+ const PAIRING_WAIT_POLL_MS = 5000;
238
+
239
+ // The Homebrew background service must recover on its own: the user never sees
240
+ // its terminal, so exiting would leave Dexter silently offline.
241
+ function runsAsBackgroundService() {
242
+ return process.env.DEXTER_BRIDGE_INSTALL_METHOD === 'homebrew' && !process.stdin.isTTY;
243
+ }
244
+
245
+ function isUpdateRequiredError(error) {
246
+ return error?.status === 426 || error?.body?.code === 'FRAMER_COMPANION_UPDATE_REQUIRED';
247
+ }
248
+
249
+ function runCommand(command, args) {
250
+ return new Promise((resolve) => {
251
+ const child = spawn(command, args, { stdio: 'inherit' });
252
+ child.on('error', () => resolve(false));
253
+ child.on('exit', (code) => resolve(code === 0));
254
+ });
255
+ }
256
+
257
+ // Upgrades the Homebrew install at most once an hour. On success the process
258
+ // exits non-zero so launchd restarts the service on the new version.
259
+ async function selfUpdateWithHomebrew(configDir, logger) {
260
+ const lastAttempt = Date.parse(readConfig(configDir).lastSelfUpdateAt || '');
261
+ if (Number.isFinite(lastAttempt) && Date.now() - lastAttempt < SELF_UPDATE_INTERVAL_MS) return;
262
+ saveConfigPatch({ lastSelfUpdateAt: new Date().toISOString() }, configDir);
263
+ console.log('A newer Dexter Bridge is required. Updating with Homebrew...');
264
+ const updated = await runCommand('brew', ['upgrade', HOMEBREW_FORMULA]);
265
+ logger.info('self_update_finished', { updated });
266
+ if (!updated) return;
267
+ const restart = new Error('Dexter Bridge updated. Restarting on the new version.');
268
+ restart.exitCode = 75;
269
+ throw restart;
270
+ }
271
+
272
+ async function waitForPairing(configDir) {
273
+ console.log('Waiting for a new pairing from Dexter...');
274
+ while (true) {
275
+ const token = readConfig(configDir).deviceToken;
276
+ if (token) return token;
277
+ await wait(PAIRING_WAIT_POLL_MS);
278
+ }
279
+ }
280
+
281
+ function clearInvalidPairing(configDir, cause, rejectedToken) {
282
+ const current = readConfig(configDir);
283
+ const product = normalizeBridgeProduct(current.product);
284
+ // Another bridge command may have just saved a fresh pairing; keep it.
285
+ if (!rejectedToken || current.deviceToken === rejectedToken) {
286
+ saveConfigPatch({
287
+ deviceToken: null,
288
+ device: null,
289
+ pairedAt: null,
290
+ }, configDir);
291
+ }
242
292
  const error = new Error(
243
293
  `This ${product.name} Bridge pairing was disconnected. Reopen ${product.name} and run the new pairing command.`,
244
294
  { cause },
@@ -534,14 +584,19 @@ async function startCommand({ apiBaseUrl, config, flags, configDir }) {
534
584
  let deviceToken = config.deviceToken;
535
585
  if (!deviceToken) {
536
586
  // First run: pair interactively so `npx @gakim-digital/dexter-bridge` alone is
537
- if (!process.stdin.isTTY) requireDeviceToken(config);
538
- console.log('This local agent bridge is not paired yet.');
539
- console.log('Open the product connection screen and generate a new bridge command.');
540
- const code = await promptForPairingCode();
541
- if (!code) requireDeviceToken(config);
542
- await pairCommand({ apiBaseUrl, args: [code], flags, configDir });
543
- activeConfig = readConfig(configDir);
544
- deviceToken = activeConfig.deviceToken;
587
+ if (runsAsBackgroundService()) {
588
+ deviceToken = await waitForPairing(configDir);
589
+ activeConfig = readConfig(configDir);
590
+ } else {
591
+ if (!process.stdin.isTTY) requireDeviceToken(config);
592
+ console.log('This local agent bridge is not paired yet.');
593
+ console.log('Open the product connection screen and generate a new bridge command.');
594
+ const code = await promptForPairingCode();
595
+ if (!code) requireDeviceToken(config);
596
+ await pairCommand({ apiBaseUrl, args: [code], flags, configDir });
597
+ activeConfig = readConfig(configDir);
598
+ deviceToken = activeConfig.deviceToken;
599
+ }
545
600
  }
546
601
  const product = normalizeBridgeProduct(activeConfig.product);
547
602
  const runtimeSelection = requestedRuntime(flags, activeConfig);
@@ -651,9 +706,22 @@ async function startCommand({ apiBaseUrl, config, flags, configDir }) {
651
706
  status: error.status,
652
707
  code: error.body?.code || null,
653
708
  });
654
- const disconnected = clearInvalidPairing(configDir, error);
709
+ const savedToken = readConfig(configDir).deviceToken;
710
+ if (savedToken && savedToken !== deviceToken) {
711
+ // Re-paired by another bridge command; adopt the new pairing.
712
+ deviceToken = savedToken;
713
+ pollFailureCount = 0;
714
+ continue;
715
+ }
716
+ const disconnected = clearInvalidPairing(configDir, error, deviceToken);
655
717
  console.log(disconnected.message);
656
- return;
718
+ if (!runsAsBackgroundService()) return;
719
+ deviceToken = await waitForPairing(configDir);
720
+ pollFailureCount = 0;
721
+ continue;
722
+ }
723
+ if (isUpdateRequiredError(error) && runsAsBackgroundService()) {
724
+ await selfUpdateWithHomebrew(configDir, pollLogger);
657
725
  }
658
726
  if (once) throw error;
659
727
  pollFailureCount += 1;
package/src/config.js CHANGED
@@ -43,9 +43,7 @@ export const BRIDGE_CAPABILITIES = [
43
43
  'framer-agent-v1',
44
44
  'framer-project-bootstrap-v1',
45
45
  'framer-project-verification-v1',
46
- 'framer-interaction-verification-v3',
47
- 'framer-behavior-contract-v1',
48
- 'framer-mechanism-verification-v1',
46
+ 'framer-interaction-verification-v4',
49
47
  'project-shell-v1',
50
48
  'build-fingerprint-v1',
51
49
  'tool-schema-parity-v1',