@gakim-digital/dexter-bridge 0.11.13 → 0.11.14

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gakim-digital/dexter-bridge",
3
- "version": "0.11.13",
3
+ "version": "0.11.14",
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,7 +39,7 @@
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",
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,
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;
@@ -619,7 +619,7 @@ function installedFramerAgentVersion() {
619
619
  return null;
620
620
  }
621
621
 
622
- function readGuidanceFile(projectRoot, relativePath, remainingBytes) {
622
+ function readGuidanceFile(projectRoot, relativePath, remainingBytes, section) {
623
623
  const normalized = relativePath.replaceAll('\\', '/');
624
624
  if (
625
625
  normalized.startsWith('/')
@@ -635,12 +635,27 @@ function readGuidanceFile(projectRoot, relativePath, remainingBytes) {
635
635
  }
636
636
  const maximum = Math.min(MAX_GUIDANCE_FILE_BYTES, remainingBytes);
637
637
  if (maximum <= 0) return null;
638
+ let content = fs.readFileSync(absolutePath, 'utf8');
639
+ if (section) {
640
+ content = markdownSection(content, section);
641
+ if (!content) return null;
642
+ }
638
643
  return {
639
644
  path: normalized,
640
- content: boundedText(fs.readFileSync(absolutePath, 'utf8'), maximum),
645
+ content: boundedText(content, maximum),
641
646
  };
642
647
  }
643
648
 
649
+ function markdownSection(markdown, heading) {
650
+ const lines = markdown.split('\n');
651
+ const start = lines.findIndex((line) => line.trim() === `## ${heading}`);
652
+ if (start < 0) return '';
653
+ const end = lines.findIndex(
654
+ (line, index) => index > start && line.startsWith('## '),
655
+ );
656
+ return lines.slice(start, end < 0 ? undefined : end).join('\n').trim();
657
+ }
658
+
644
659
  function normalizeProjectId(value) {
645
660
  const projectId = String(value || '').trim();
646
661
  if (!PROJECT_ID_PATTERN.test(projectId)) {
@@ -1650,12 +1665,16 @@ export function createFramerAgentToolRuntime({
1650
1665
  { root: projectRoot, path: 'prompt/overview.md' },
1651
1666
  { root: projectRoot, path: 'prompt/guardrails.md' },
1652
1667
  { root: projectRoot, path: 'prompt/critical-reminders.md' },
1653
- ...(visualVerificationRequired
1654
- ? [
1655
- { root: projectRoot, path: 'prompt/design-rules.md' },
1656
- { root: projectRoot, path: 'prompt/how-projects-work.md' },
1657
- ]
1658
- : []),
1668
+ // The byte cap clips how-projects-work.md before its 3D transform
1669
+ // guidance, so that section loads on its own first.
1670
+ {
1671
+ root: projectRoot,
1672
+ path: 'prompt/how-projects-work.md',
1673
+ section: 'Transforms',
1674
+ label: 'prompt/how-projects-work.md#transforms',
1675
+ },
1676
+ { root: projectRoot, path: 'prompt/design-rules.md' },
1677
+ { root: projectRoot, path: 'prompt/how-projects-work.md' },
1659
1678
  ];
1660
1679
  const files = [];
1661
1680
  let remainingBytes = MAX_GUIDANCE_BYTES;
@@ -1664,6 +1683,7 @@ export function createFramerAgentToolRuntime({
1664
1683
  requestedFile.root,
1665
1684
  requestedFile.path,
1666
1685
  remainingBytes,
1686
+ requestedFile.section,
1667
1687
  );
1668
1688
  if (!loaded) continue;
1669
1689
  files.push({
@@ -184,6 +184,30 @@ export function materializeOutcomeReferenceAssets(materialized, attachments) {
184
184
  return output;
185
185
  }
186
186
 
187
+ // Image parts in a server-owned model turn become native image input for the
188
+ // provider CLI; the caller removes the directory when the turn settles.
189
+ export function materializeModelTurnImages(messages) {
190
+ const decoded = (Array.isArray(messages) ? messages : [])
191
+ .flatMap((message) => (Array.isArray(message?.content) ? message.content : []))
192
+ .filter((part) => part?.type === 'image' && typeof part.base64 === 'string')
193
+ .slice(0, 4)
194
+ .map((part) => decodedReferenceAsset({ dataUrl: `data:${part.mimeType};base64,${part.base64}` }))
195
+ .filter(Boolean);
196
+ if (!decoded.length) return { files: [], remove() {} };
197
+ const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'dexter-turn-images-'));
198
+ const files = decoded.map((image, index) => {
199
+ const file = path.join(directory, `${String(index + 1).padStart(2, '0')}.${image.extension}`);
200
+ fs.writeFileSync(file, image.buffer, { mode: 0o600 });
201
+ return file;
202
+ });
203
+ return {
204
+ files,
205
+ remove() {
206
+ fs.rmSync(directory, { recursive: true, force: true });
207
+ },
208
+ };
209
+ }
210
+
187
211
  export function removeOutcomeReferenceAssets(materialized) {
188
212
  if (!materialized?.directory) return;
189
213
  fs.rmSync(
package/src/protocol.js CHANGED
@@ -141,11 +141,12 @@ function compactModelTurnContent(content) {
141
141
  if (!Array.isArray(content)) return content;
142
142
  return content.map((part) => {
143
143
  if (!isRecord(part) || part.type !== 'image') return part;
144
+ // Inline image bytes travel as native image input next to this prompt.
144
145
  return {
145
146
  type: 'image',
146
147
  mimeType: part.mimeType,
147
148
  url: part.url,
148
- omittedBase64Bytes: typeof part.base64 === 'string' ? part.base64.length : undefined,
149
+ ...(typeof part.base64 === 'string' ? { attachedAsImageInput: true } : {}),
149
150
  };
150
151
  });
151
152
  }
@@ -494,6 +495,7 @@ export function buildOutcomePrompt(outcome = {}, product, promptContext = {}) {
494
495
  'For repeated behavior, declare every targetNodeId and one representativeTargetNodeId in the mechanism plan. Implement and verify only that representative before fan-out, then verify complete target coverage after the final mutation. Only a contradicted result permits one evidence-scoped repair. Repeated unchanged evidence or exhausted verification budget must end as partial rather than starting another loop.',
495
496
  'Do not reuse a mechanism recorded as rejected by the user or repeated contradictory live evidence. Unknown evidence never rejects a mechanism.',
496
497
  'For visual work, evaluate hierarchy, spacing, contrast, typography, balance, imagery, interaction states, and responsive integrity.',
498
+ 'For depth and 3D, use native transforms through framer_apply_changes as described in the Framer guidance Transforms section: perspective on the parent, preserve3d on a container whose children must stay in 3D space, rotateX, rotateY, rotateZ, translateZ, and transformOrigin on the moving layers, and backfaceVisibility for flip faces. Rotations without a parent perspective render flat. Spend 3D on one signature moment, such as a tilted product mockup, layered card stack, flip card, or hover tilt; keep text readable, reduce or flatten the effect on smaller breakpoints, and confirm it in a screenshot. Do not fake 3D with images or code components when native transforms can achieve it.',
497
499
  'You decide when the run is complete. Avoid repeated inspection of unchanged evidence and stop when the requested outcome has been implemented and verified.',
498
500
  'Dexter will not perform a second design verification after you finish. Structural verification must be described honestly and must never be called runtime-tested unless an interaction was actually exercised.',
499
501
  'If Framer authorization or a required user decision is missing, return blocked with a concise explanation. Otherwise return completed only after you have finished and verified the work.',