@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 +2 -2
- package/src/agent.js +11 -0
- package/src/cli.js +85 -17
- package/src/framerAgentTools.js +28 -8
- package/src/outcomeWorkspace.js +24 -0
- package/src/protocol.js +3 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gakim-digital/dexter-bridge",
|
|
3
|
-
"version": "0.11.
|
|
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.
|
|
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
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
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 (
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
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
|
|
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/framerAgentTools.js
CHANGED
|
@@ -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(
|
|
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
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
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({
|
package/src/outcomeWorkspace.js
CHANGED
|
@@ -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
|
-
|
|
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.',
|