@gakim-digital/dexter-bridge 0.11.12 → 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 +38 -2
- package/src/agentOutput.js +39 -0
- package/src/cli.js +85 -17
- package/src/framerAgentTools.js +28 -8
- package/src/nativeSkills.js +3 -3
- 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,
|
|
@@ -405,7 +406,7 @@ function argsWithClaudeIsolation(args, definition, options = {}) {
|
|
|
405
406
|
(name) => `mcp__instawebai__${name}`,
|
|
406
407
|
);
|
|
407
408
|
const nativeTools = options.framerHarness
|
|
408
|
-
? harnessTools
|
|
409
|
+
? [...(options.nativeSkillsEnabled ? ['Skill'] : []), ...harnessTools]
|
|
409
410
|
: [
|
|
410
411
|
'Read',
|
|
411
412
|
'Write',
|
|
@@ -442,7 +443,12 @@ function argsWithClaudeIsolation(args, definition, options = {}) {
|
|
|
442
443
|
configured = argsWithoutFlagValue(configured, '--mcp-config');
|
|
443
444
|
configured.push('--mcp-config', JSON.stringify(options.mcpConfig));
|
|
444
445
|
}
|
|
445
|
-
|
|
446
|
+
// Restricted mode ignores the skills written into the run workspace, so a
|
|
447
|
+
// Framer run with skills leaves it off. The explicit tool list above is what
|
|
448
|
+
// keeps the shell and file tools out; it grants the Framer tools and Skill
|
|
449
|
+
// only, and settings stay limited to the workspace the bridge created.
|
|
450
|
+
const framerRunWithSkills = Boolean(options.framerHarness && options.nativeSkillsEnabled);
|
|
451
|
+
if (options.harnessMode && !framerRunWithSkills && !argsIncludeFlag(configured, '--restricted')) {
|
|
446
452
|
configured.push('--restricted');
|
|
447
453
|
}
|
|
448
454
|
return configured;
|
|
@@ -1824,6 +1830,15 @@ function agentErrorWithOutputUsage(agent, error) {
|
|
|
1824
1830
|
}
|
|
1825
1831
|
|
|
1826
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 = {}) {
|
|
1827
1842
|
const product = normalizeBridgeProduct(run?.product || options.product);
|
|
1828
1843
|
const productName = product.name;
|
|
1829
1844
|
if (normalizeAgentName(agent) === 'dry-run' && !options.providerAdapter) {
|
|
@@ -1949,6 +1964,7 @@ async function executeModelTurnRun(run, send, agent, options = {}) {
|
|
|
1949
1964
|
1000,
|
|
1950
1965
|
),
|
|
1951
1966
|
nativeSkills,
|
|
1967
|
+
imageFiles: options.imageFiles,
|
|
1952
1968
|
};
|
|
1953
1969
|
const adapterOptions = {
|
|
1954
1970
|
send,
|
|
@@ -2345,6 +2361,26 @@ async function executeOutcomeRun(run, send, agent, options = {}) {
|
|
|
2345
2361
|
});
|
|
2346
2362
|
return;
|
|
2347
2363
|
}
|
|
2364
|
+
if (event.kind === 'skill_used') {
|
|
2365
|
+
// The harness loaded a skill on its own. Dexter shows which one; the
|
|
2366
|
+
// aggregate of picked skills is already known to it.
|
|
2367
|
+
const skill = (materializedNativeSkills?.skills || []).find(
|
|
2368
|
+
(candidate) => candidate.nativeName === event.skillName,
|
|
2369
|
+
);
|
|
2370
|
+
if (!skill) return;
|
|
2371
|
+
void postActivity({
|
|
2372
|
+
stage: 'outcome',
|
|
2373
|
+
outcomeId: run?.outcome?.outcomeId,
|
|
2374
|
+
source: 'harness',
|
|
2375
|
+
meaningful: true,
|
|
2376
|
+
kind: 'skill_used',
|
|
2377
|
+
provider: definition.id,
|
|
2378
|
+
skillId: skill.skillId,
|
|
2379
|
+
occurredAt: event.occurredAt || new Date().toISOString(),
|
|
2380
|
+
message: `Using ${skill.name}`,
|
|
2381
|
+
});
|
|
2382
|
+
return;
|
|
2383
|
+
}
|
|
2348
2384
|
const meaningful =
|
|
2349
2385
|
Boolean(event.message || event.name) ||
|
|
2350
2386
|
event.kind === 'turn_started';
|
package/src/agentOutput.js
CHANGED
|
@@ -397,6 +397,11 @@ export function createClaudeProgressParser(onProgress, options = {}) {
|
|
|
397
397
|
});
|
|
398
398
|
continue;
|
|
399
399
|
}
|
|
400
|
+
if (part.name === 'Skill' && progressText(input.skill)) {
|
|
401
|
+
tools.set(part.id, { suppressResult: true });
|
|
402
|
+
emit({ kind: 'skill_used', skillName: progressText(input.skill) });
|
|
403
|
+
continue;
|
|
404
|
+
}
|
|
400
405
|
const progress = claudeToolProgress(part.name, input, options.cwd);
|
|
401
406
|
tools.set(part.id, { name: part.name, ...progress });
|
|
402
407
|
emit({
|
|
@@ -429,11 +434,45 @@ export function createClaudeProgressParser(onProgress, options = {}) {
|
|
|
429
434
|
}
|
|
430
435
|
};
|
|
431
436
|
|
|
437
|
+
// A large edit can take the model a minute to write, and nothing else is
|
|
438
|
+
// reported until it has finished writing. Report the moment it starts, and
|
|
439
|
+
// close that report when the finished tool call arrives.
|
|
440
|
+
const drafting = new Map();
|
|
441
|
+
const DRAFTING_MESSAGE = 'Preparing changes for the Framer project';
|
|
442
|
+
const processStreamEvent = (row) => {
|
|
443
|
+
const event = row.event;
|
|
444
|
+
const block = event?.type === 'content_block_start' ? event.content_block : null;
|
|
445
|
+
if (block?.type !== 'tool_use' || !block.id || drafting.has(block.id)) return;
|
|
446
|
+
const shortName = String(block.name || '').replace(/^mcp__instawebai__/, '');
|
|
447
|
+
if (!['framer_apply_changes', 'framer_update_code_file', 'framer_write'].includes(shortName)) return;
|
|
448
|
+
drafting.set(block.id, true);
|
|
449
|
+
emit({
|
|
450
|
+
kind: 'item_started',
|
|
451
|
+
phase: 'started',
|
|
452
|
+
activity: `tool-draft:${block.id}`,
|
|
453
|
+
message: DRAFTING_MESSAGE,
|
|
454
|
+
});
|
|
455
|
+
};
|
|
456
|
+
const finishDrafting = (row) => {
|
|
457
|
+
for (const part of Array.isArray(row.message?.content) ? row.message.content : []) {
|
|
458
|
+
if (part?.type !== 'tool_use' || drafting.get(part.id) !== true) continue;
|
|
459
|
+
drafting.set(part.id, false);
|
|
460
|
+
emit({
|
|
461
|
+
kind: 'item_completed',
|
|
462
|
+
phase: 'completed',
|
|
463
|
+
activity: `tool-draft:${part.id}`,
|
|
464
|
+
message: DRAFTING_MESSAGE,
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
};
|
|
468
|
+
|
|
432
469
|
const processLine = (line) => {
|
|
433
470
|
const trimmed = line.trim();
|
|
434
471
|
if (!trimmed) return;
|
|
435
472
|
try {
|
|
436
473
|
const row = JSON.parse(trimmed);
|
|
474
|
+
if (row?.type === 'stream_event') processStreamEvent(row);
|
|
475
|
+
if (row?.type === 'assistant') finishDrafting(row);
|
|
437
476
|
if (row?.type === 'assistant') processAssistant(row);
|
|
438
477
|
if (row?.type === 'user') processUser(row);
|
|
439
478
|
} catch {
|
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/nativeSkills.js
CHANGED
|
@@ -174,7 +174,7 @@ function renderAggregateClaudeSkill(skills, nativeName) {
|
|
|
174
174
|
return [
|
|
175
175
|
'---',
|
|
176
176
|
`name: ${yamlString(nativeName)}`,
|
|
177
|
-
`description: ${yamlString('Apply every
|
|
177
|
+
`description: ${yamlString('Apply every skill the user selected for this request.')}`,
|
|
178
178
|
'user-invocable: true',
|
|
179
179
|
'disable-model-invocation: true',
|
|
180
180
|
'---',
|
|
@@ -231,7 +231,7 @@ export function materializeNativeSkills(
|
|
|
231
231
|
['always', 'explicit', 'command'].includes(skill.trigger),
|
|
232
232
|
);
|
|
233
233
|
if (provider === 'claude-code' && requiredSkills.length > 0) {
|
|
234
|
-
aggregateName = `
|
|
234
|
+
aggregateName = `dexter-active-${digest.slice(0, 8)}`;
|
|
235
235
|
const aggregateDirectory = path.join(skillsRoot, aggregateName);
|
|
236
236
|
fs.mkdirSync(aggregateDirectory, { recursive: true, mode: 0o700 });
|
|
237
237
|
aggregatePath = path.join(aggregateDirectory, 'SKILL.md');
|
|
@@ -260,7 +260,7 @@ export function claudeNativeSkillPrompt(prompt, materialized) {
|
|
|
260
260
|
return [
|
|
261
261
|
materialized?.aggregateName ? `/${materialized.aggregateName}` : '',
|
|
262
262
|
catalog
|
|
263
|
-
? `Available optional skills; invoke one only when it is relevant:\n${catalog}`
|
|
263
|
+
? `Available optional skills; invoke one only when it is relevant, and do not use any skill that is not listed here:\n${catalog}`
|
|
264
264
|
: '',
|
|
265
265
|
String(prompt || ''),
|
|
266
266
|
].filter(Boolean).join('\n\n');
|
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.',
|