@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 +9 -12
- package/package.json +3 -2
- package/src/agent.js +31 -9
- package/src/cli.js +85 -17
- package/src/config.js +1 -3
- package/src/framerAgentTools.js +348 -776
- package/src/framerBehaviorContract.js +124 -1326
- package/src/outcomeWorkspace.js +24 -0
- package/src/protocol.js +6 -6
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-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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/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-
|
|
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',
|