@gakim-digital/dexter-bridge 0.11.16 → 0.11.19
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 +1 -1
- package/src/agent.js +192 -33
- package/src/agentOutput.js +51 -1
- package/src/api.js +31 -0
- package/src/config.js +1 -0
- package/src/framerAgentTools.js +215 -77
- package/src/framerBehaviorContract.js +15 -1
- package/src/googleFontsFallback.js +196 -0
- package/src/protocol.js +89 -68
- package/src/providers/codexAppServer.js +105 -5
package/package.json
CHANGED
package/src/agent.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import crypto from 'node:crypto';
|
|
2
3
|
import fs from 'node:fs';
|
|
3
4
|
import os from 'node:os';
|
|
4
5
|
import path from 'node:path';
|
|
@@ -6,6 +7,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
6
7
|
import crossSpawn from 'cross-spawn';
|
|
7
8
|
import {
|
|
8
9
|
getFramerProjectAuthorization,
|
|
10
|
+
readFramerWebsite,
|
|
9
11
|
isCompanionPairingInvalidError,
|
|
10
12
|
postRunEvent,
|
|
11
13
|
reportFramerProjectAuthorizationVerification,
|
|
@@ -26,6 +28,7 @@ import {
|
|
|
26
28
|
} from './config.js';
|
|
27
29
|
import {
|
|
28
30
|
assertOutcomeResponseContract,
|
|
31
|
+
buildFramerOutcomePrompt,
|
|
29
32
|
buildOutcomePrompt,
|
|
30
33
|
buildModelTurnDeltaPrompt,
|
|
31
34
|
buildModelTurnFallbackPrompt,
|
|
@@ -327,6 +330,23 @@ function claudeModelEngineSystemPrompt(product) {
|
|
|
327
330
|
].join(' ');
|
|
328
331
|
}
|
|
329
332
|
|
|
333
|
+
// Content-addressed, so identical instructions reuse one file and stay
|
|
334
|
+
// byte-identical for Claude Code's prompt cache.
|
|
335
|
+
function writeClaudeSystemPromptFile(content) {
|
|
336
|
+
const directory = path.join(os.tmpdir(), 'instawebai-outcome-prompts');
|
|
337
|
+
const file = path.join(
|
|
338
|
+
directory,
|
|
339
|
+
`${crypto.createHash('sha256').update(content).digest('hex').slice(0, 32)}.md`,
|
|
340
|
+
);
|
|
341
|
+
if (!fs.existsSync(file)) {
|
|
342
|
+
fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
343
|
+
const partial = `${file}.${process.pid}.${crypto.randomUUID()}.tmp`;
|
|
344
|
+
fs.writeFileSync(partial, content, { mode: 0o600 });
|
|
345
|
+
fs.renameSync(partial, file);
|
|
346
|
+
}
|
|
347
|
+
return file;
|
|
348
|
+
}
|
|
349
|
+
|
|
330
350
|
function claudeOutcomeSystemPrompt(product) {
|
|
331
351
|
const name = normalizeBridgeProduct(product).name;
|
|
332
352
|
if (normalizeBridgeProduct(product).id === 'dexter') {
|
|
@@ -336,7 +356,6 @@ function claudeOutcomeSystemPrompt(product) {
|
|
|
336
356
|
'Inspect the project, perform the requested work, verify the result, and return the structured outcome.',
|
|
337
357
|
'Do not access local credentials, unrelated files, other projects, account settings, or billing.',
|
|
338
358
|
'Do not publish unless the assignment explicitly authorizes publishing.',
|
|
339
|
-
'Call progress_update before the first inspection or edit and at meaningful phase changes. Use one or two natural first-person sentences that name the specific page area, component, or behavior and the concrete change you are about to make or are making now; optionally say what you will check next. Avoid generic updates such as "working on the next step", "thinking", "loading", or "making changes".',
|
|
340
359
|
'If framer_write is rejected by the safety boundary, do not retry JavaScript variants. Switch to framer_apply_changes, inspect the relevant state, or report the concrete blocker.',
|
|
341
360
|
`Return only the structured outcome result requested by ${name}.`,
|
|
342
361
|
].join(' ');
|
|
@@ -383,6 +402,18 @@ function argsWithStructuredOutput(args, definition, env = process.env) {
|
|
|
383
402
|
return args;
|
|
384
403
|
}
|
|
385
404
|
|
|
405
|
+
function framerWebHosts(framerHarness) {
|
|
406
|
+
const hosts = framerHarness?.context?.webAccess?.hosts;
|
|
407
|
+
if (!Array.isArray(hosts)) return [];
|
|
408
|
+
return [...new Set(
|
|
409
|
+
hosts
|
|
410
|
+
.map((value) => String(value || '').trim().toLowerCase())
|
|
411
|
+
.filter((value) =>
|
|
412
|
+
value.length <= 253
|
|
413
|
+
&& /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]*[a-z0-9])?)+$/.test(value)),
|
|
414
|
+
)].slice(0, 20);
|
|
415
|
+
}
|
|
416
|
+
|
|
386
417
|
function argsWithClaudeIsolation(args, definition, options = {}) {
|
|
387
418
|
if (definition.id !== 'claude-code') return args;
|
|
388
419
|
const isolated = argsWithoutVariadicFlag(args, '--tools');
|
|
@@ -405,8 +436,17 @@ function argsWithClaudeIsolation(args, definition, options = {}) {
|
|
|
405
436
|
const harnessTools = harnessToolNames.map(
|
|
406
437
|
(name) => `mcp__instawebai__${name}`,
|
|
407
438
|
);
|
|
439
|
+
const webHosts = options.framerHarness
|
|
440
|
+
? framerWebHosts(options.framerHarness)
|
|
441
|
+
: [];
|
|
408
442
|
const nativeTools = options.framerHarness
|
|
409
|
-
? [
|
|
443
|
+
? [
|
|
444
|
+
...(options.nativeSkillsEnabled ? ['Skill'] : []),
|
|
445
|
+
// Web access is limited to the sites the user named. WebSearch cannot
|
|
446
|
+
// be limited by domain, so only WebFetch is offered.
|
|
447
|
+
...(webHosts.length ? ['WebFetch'] : []),
|
|
448
|
+
...harnessTools,
|
|
449
|
+
]
|
|
410
450
|
: [
|
|
411
451
|
'Read',
|
|
412
452
|
'Write',
|
|
@@ -435,7 +475,16 @@ function argsWithClaudeIsolation(args, definition, options = {}) {
|
|
|
435
475
|
'--permission-mode',
|
|
436
476
|
options.harnessMode && !options.framerHarness ? 'acceptEdits' : 'dontAsk',
|
|
437
477
|
);
|
|
438
|
-
if (options.harnessMode)
|
|
478
|
+
if (options.harnessMode) {
|
|
479
|
+
const allowedTools = options.framerHarness
|
|
480
|
+
? [
|
|
481
|
+
...(options.nativeSkillsEnabled ? ['Skill'] : []),
|
|
482
|
+
...webHosts.map((host) => `WebFetch(domain:${host})`),
|
|
483
|
+
...harnessTools,
|
|
484
|
+
]
|
|
485
|
+
: nativeTools;
|
|
486
|
+
permissionMode.push('--allowedTools', ...allowedTools);
|
|
487
|
+
}
|
|
439
488
|
if (!argsIncludeFlag(permissionMode, '--strict-mcp-config')) permissionMode.push('--strict-mcp-config');
|
|
440
489
|
if (!argsIncludeFlag(permissionMode, '--no-chrome')) permissionMode.push('--no-chrome');
|
|
441
490
|
let configured = permissionMode;
|
|
@@ -479,13 +528,18 @@ function argsWithClaudeModelEngine(args, definition, options = {}, env = process
|
|
|
479
528
|
let isolated = argsWithoutFlagValue(args, '--system-prompt');
|
|
480
529
|
isolated = argsWithoutFlagValue(isolated, '--append-system-prompt');
|
|
481
530
|
isolated = argsWithoutFlagValue(isolated, '--agent');
|
|
531
|
+
isolated = argsWithoutFlagValue(isolated, '--system-prompt-file');
|
|
482
532
|
isolated = argsWithoutFlagValue(isolated, '--effort');
|
|
483
533
|
return [
|
|
484
534
|
...isolated,
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
535
|
+
...(options.harnessMode && options.systemPromptFile
|
|
536
|
+
? ['--system-prompt-file', options.systemPromptFile]
|
|
537
|
+
: [
|
|
538
|
+
'--system-prompt',
|
|
539
|
+
options.harnessMode
|
|
540
|
+
? claudeOutcomeSystemPrompt(options.product)
|
|
541
|
+
: claudeModelEngineSystemPrompt(options.product),
|
|
542
|
+
]),
|
|
489
543
|
'--effort',
|
|
490
544
|
claudeEffortForStep(options.step, env),
|
|
491
545
|
];
|
|
@@ -2283,7 +2337,15 @@ async function executeOutcomeRun(run, send, agent, options = {}) {
|
|
|
2283
2337
|
1_000,
|
|
2284
2338
|
),
|
|
2285
2339
|
);
|
|
2286
|
-
const
|
|
2340
|
+
const framerAgentOutcome =
|
|
2341
|
+
run?.outcome?.kind === 'framer-project'
|
|
2342
|
+
|| run?.outcome?.context?.executionMode === 'framer-agent';
|
|
2343
|
+
// Every Dexter message starts a fresh provider session. The assignment
|
|
2344
|
+
// already carries the recent conversation, and resuming would make every
|
|
2345
|
+
// turn of this run re-read the whole previous run as well.
|
|
2346
|
+
const sessionKey = framerAgentOutcome
|
|
2347
|
+
? run?.runId || companionSessionKey(run)
|
|
2348
|
+
: companionSessionKey(run);
|
|
2287
2349
|
const infrastructureAbortController = new AbortController();
|
|
2288
2350
|
const controlMonitorAbort = new AbortController();
|
|
2289
2351
|
let finished = false;
|
|
@@ -2293,6 +2355,51 @@ async function executeOutcomeRun(run, send, agent, options = {}) {
|
|
|
2293
2355
|
let rejectCancellation;
|
|
2294
2356
|
let latestTokenUsage = normalizeCompanionTokenUsage();
|
|
2295
2357
|
let activityChain = Promise.resolve();
|
|
2358
|
+
const webCalls = new Map();
|
|
2359
|
+
const recordWebCall = (webCall) => {
|
|
2360
|
+
if (!webCall || typeof webCall !== 'object') return;
|
|
2361
|
+
const id = String(webCall.id || '').trim();
|
|
2362
|
+
const tool =
|
|
2363
|
+
webCall.tool === 'WebFetch'
|
|
2364
|
+
? 'WebFetch'
|
|
2365
|
+
: webCall.tool === 'WebSearch'
|
|
2366
|
+
? 'WebSearch'
|
|
2367
|
+
: '';
|
|
2368
|
+
if (!id || !tool) return;
|
|
2369
|
+
const current = webCalls.get(id) || {
|
|
2370
|
+
tool,
|
|
2371
|
+
site: null,
|
|
2372
|
+
status: 'started',
|
|
2373
|
+
};
|
|
2374
|
+
webCalls.set(id, {
|
|
2375
|
+
...current,
|
|
2376
|
+
tool,
|
|
2377
|
+
site:
|
|
2378
|
+
typeof webCall.site === 'string' && webCall.site.trim()
|
|
2379
|
+
? webCall.site.trim().toLowerCase().slice(0, 253)
|
|
2380
|
+
: current.site,
|
|
2381
|
+
status:
|
|
2382
|
+
webCall.status === 'succeeded' || webCall.status === 'failed'
|
|
2383
|
+
? webCall.status
|
|
2384
|
+
: current.status,
|
|
2385
|
+
errorMessage:
|
|
2386
|
+
webCall.status === 'failed' && typeof webCall.errorMessage === 'string'
|
|
2387
|
+
? clip(webCall.errorMessage, 300)
|
|
2388
|
+
: current.errorMessage,
|
|
2389
|
+
});
|
|
2390
|
+
if (webCalls.size > 100) {
|
|
2391
|
+
webCalls.delete(webCalls.keys().next().value);
|
|
2392
|
+
}
|
|
2393
|
+
};
|
|
2394
|
+
const webCallSummary = () =>
|
|
2395
|
+
[...webCalls.values()].map((call) => ({
|
|
2396
|
+
tool: call.tool,
|
|
2397
|
+
site: call.site,
|
|
2398
|
+
status: call.status === 'succeeded' ? 'succeeded' : 'failed',
|
|
2399
|
+
...(call.status === 'failed'
|
|
2400
|
+
? { errorMessage: call.errorMessage || 'The web call did not complete.' }
|
|
2401
|
+
: {}),
|
|
2402
|
+
}));
|
|
2296
2403
|
const cancellation = new Promise((_, reject) => {
|
|
2297
2404
|
rejectCancellation = reject;
|
|
2298
2405
|
});
|
|
@@ -2345,6 +2452,7 @@ async function executeOutcomeRun(run, send, agent, options = {}) {
|
|
|
2345
2452
|
return activityChain;
|
|
2346
2453
|
};
|
|
2347
2454
|
const progress = (event = {}) => {
|
|
2455
|
+
recordWebCall(event.webCall);
|
|
2348
2456
|
if (event.tokenUsage) {
|
|
2349
2457
|
latestTokenUsage = normalizeCompanionTokenUsage(event.tokenUsage);
|
|
2350
2458
|
}
|
|
@@ -2385,9 +2493,15 @@ async function executeOutcomeRun(run, send, agent, options = {}) {
|
|
|
2385
2493
|
Boolean(event.message || event.name) ||
|
|
2386
2494
|
event.kind === 'turn_started';
|
|
2387
2495
|
if (!meaningful) return;
|
|
2496
|
+
// Framer runs narrate in plain text rather than through a progress tool
|
|
2497
|
+
// (a tool call costs a whole turn), so that text is the user's progress.
|
|
2498
|
+
const narration =
|
|
2499
|
+
framerAgentOutcome
|
|
2500
|
+
&& event.kind === 'output_delta'
|
|
2501
|
+
&& String(event.activity || '').startsWith('assistant:');
|
|
2388
2502
|
const userFacingUpdate =
|
|
2389
|
-
event.kind === 'status_update'
|
|
2390
|
-
|
|
2503
|
+
(event.kind === 'status_update' && event.source === 'harness')
|
|
2504
|
+
|| narration;
|
|
2391
2505
|
void postActivity({
|
|
2392
2506
|
stage: 'outcome',
|
|
2393
2507
|
outcomeId: run?.outcome?.outcomeId,
|
|
@@ -2397,9 +2511,10 @@ async function executeOutcomeRun(run, send, agent, options = {}) {
|
|
|
2397
2511
|
provider: definition.id,
|
|
2398
2512
|
tool: event.name || undefined,
|
|
2399
2513
|
toolPhase: event.phase || undefined,
|
|
2400
|
-
activity:
|
|
2401
|
-
event.activity
|
|
2402
|
-
|
|
2514
|
+
activity: narration
|
|
2515
|
+
? event.activity.replace(/^assistant:/, 'assistant-progress:')
|
|
2516
|
+
: event.activity ||
|
|
2517
|
+
(userFacingUpdate ? `assistant-progress:${Date.now()}` : undefined),
|
|
2403
2518
|
paths: event.paths || undefined,
|
|
2404
2519
|
tokenUsage: event.tokenUsage || undefined,
|
|
2405
2520
|
occurredAt: event.occurredAt || new Date().toISOString(),
|
|
@@ -2445,18 +2560,16 @@ async function executeOutcomeRun(run, send, agent, options = {}) {
|
|
|
2445
2560
|
}
|
|
2446
2561
|
};
|
|
2447
2562
|
const controlMonitor = monitorControl();
|
|
2448
|
-
const rememberedSession =
|
|
2449
|
-
|
|
2450
|
-
(
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
run?.outcome?.kind === 'framer-project'
|
|
2459
|
-
|| run?.outcome?.context?.executionMode === 'framer-agent';
|
|
2563
|
+
const rememberedSession = framerAgentOutcome
|
|
2564
|
+
? null
|
|
2565
|
+
: rememberedCompanionSession(sessionKey) ||
|
|
2566
|
+
(run?.outcome?.session?.providerSessionId
|
|
2567
|
+
? {
|
|
2568
|
+
providerSessionId: run.outcome.session.providerSessionId,
|
|
2569
|
+
agent: definition.id,
|
|
2570
|
+
updatedAt: Date.now(),
|
|
2571
|
+
}
|
|
2572
|
+
: null);
|
|
2460
2573
|
const materialized = materializeOutcomeWorkspace(run?.outcome, {
|
|
2461
2574
|
sessionId: run?.outcome?.context?.harnessSessionId || sessionKey,
|
|
2462
2575
|
});
|
|
@@ -2536,6 +2649,18 @@ async function executeOutcomeRun(run, send, agent, options = {}) {
|
|
|
2536
2649
|
allowInsecureHttp: options.allowInsecureHttp,
|
|
2537
2650
|
})
|
|
2538
2651
|
: undefined,
|
|
2652
|
+
readWebsite:
|
|
2653
|
+
options.apiBaseUrl && options.deviceToken
|
|
2654
|
+
? ({ url, screenshotTop }) =>
|
|
2655
|
+
readFramerWebsite(options.apiBaseUrl, {
|
|
2656
|
+
deviceToken: options.deviceToken,
|
|
2657
|
+
runId: run.runId,
|
|
2658
|
+
url,
|
|
2659
|
+
screenshotTop,
|
|
2660
|
+
fetchImpl: options.fetchImpl,
|
|
2661
|
+
allowInsecureHttp: options.allowInsecureHttp,
|
|
2662
|
+
})
|
|
2663
|
+
: undefined,
|
|
2539
2664
|
verifyProjectAuthorization:
|
|
2540
2665
|
options.apiBaseUrl && options.deviceToken
|
|
2541
2666
|
? ({
|
|
@@ -2588,11 +2713,34 @@ async function executeOutcomeRun(run, send, agent, options = {}) {
|
|
|
2588
2713
|
cancellation,
|
|
2589
2714
|
])
|
|
2590
2715
|
: null;
|
|
2591
|
-
|
|
2716
|
+
const promptContext = {
|
|
2592
2717
|
doctrine: run?.doctrine,
|
|
2593
2718
|
doctrineVersion: run?.doctrineVersion,
|
|
2594
2719
|
framerGuidance: framerGuidance?.content,
|
|
2595
|
-
|
|
2720
|
+
projectInventory: framerGuidance?.inventory,
|
|
2721
|
+
};
|
|
2722
|
+
// Claude Code gets the run-independent instructions as its system prompt,
|
|
2723
|
+
// which it caches across sessions: later runs and follow-up messages read
|
|
2724
|
+
// them from the cache instead of writing them again.
|
|
2725
|
+
let systemPromptFile = null;
|
|
2726
|
+
let systemPromptBytes = 0;
|
|
2727
|
+
if (
|
|
2728
|
+
framerAgentOutcome
|
|
2729
|
+
&& definition.id === 'claude-code'
|
|
2730
|
+
&& !options.providerAdapter?.runOutcome
|
|
2731
|
+
) {
|
|
2732
|
+
const { instructions, task } = buildFramerOutcomePrompt(
|
|
2733
|
+
executionOutcome,
|
|
2734
|
+
product,
|
|
2735
|
+
promptContext,
|
|
2736
|
+
);
|
|
2737
|
+
const systemPrompt = `${claudeOutcomeSystemPrompt(product)}\n\n${instructions}`;
|
|
2738
|
+
systemPromptFile = writeClaudeSystemPromptFile(systemPrompt);
|
|
2739
|
+
systemPromptBytes = Buffer.byteLength(systemPrompt, 'utf8');
|
|
2740
|
+
prompt = task;
|
|
2741
|
+
} else {
|
|
2742
|
+
prompt = buildOutcomePrompt(executionOutcome, product, promptContext);
|
|
2743
|
+
}
|
|
2596
2744
|
const unresolvedTargets = preflight?.unresolvedTargets || [];
|
|
2597
2745
|
if (unresolvedTargets.length > 0) {
|
|
2598
2746
|
prompt = [
|
|
@@ -2607,7 +2755,7 @@ async function executeOutcomeRun(run, send, agent, options = {}) {
|
|
|
2607
2755
|
harnessTools.configurePromptContext?.({
|
|
2608
2756
|
doctrineLoaded: Boolean(run?.doctrine),
|
|
2609
2757
|
doctrineVersion: run?.doctrineVersion,
|
|
2610
|
-
promptBytes: Buffer.byteLength(prompt, 'utf8'),
|
|
2758
|
+
promptBytes: systemPromptBytes + Buffer.byteLength(prompt, 'utf8'),
|
|
2611
2759
|
});
|
|
2612
2760
|
const gateway =
|
|
2613
2761
|
definition.id === 'codex' ? null : await harnessTools.startGateway();
|
|
@@ -2667,7 +2815,8 @@ async function executeOutcomeRun(run, send, agent, options = {}) {
|
|
|
2667
2815
|
maxDurationMs: outcomeMaxDurationMs,
|
|
2668
2816
|
cwd: materialized.directory,
|
|
2669
2817
|
nativeSkills,
|
|
2670
|
-
framerHarness: framerAgentOutcome,
|
|
2818
|
+
framerHarness: framerAgentOutcome ? executionOutcome : false,
|
|
2819
|
+
webHosts: framerAgentOutcome ? framerWebHosts(executionOutcome) : [],
|
|
2671
2820
|
imageFiles,
|
|
2672
2821
|
harnessTools: {
|
|
2673
2822
|
definitions: codexDynamicToolSpecs(harnessTools.definitions),
|
|
@@ -2708,7 +2857,8 @@ async function executeOutcomeRun(run, send, agent, options = {}) {
|
|
|
2708
2857
|
maxDurationMs: outcomeMaxDurationMs,
|
|
2709
2858
|
cwd: materialized.directory,
|
|
2710
2859
|
harnessMode: true,
|
|
2711
|
-
|
|
2860
|
+
systemPromptFile,
|
|
2861
|
+
framerHarness: framerAgentOutcome ? executionOutcome : false,
|
|
2712
2862
|
harnessToolNames: harnessTools.toolNames,
|
|
2713
2863
|
nativeSkillsEnabled: Boolean(materializedNativeSkills),
|
|
2714
2864
|
imageFiles,
|
|
@@ -2731,7 +2881,8 @@ async function executeOutcomeRun(run, send, agent, options = {}) {
|
|
|
2731
2881
|
maxDurationMs: outcomeMaxDurationMs,
|
|
2732
2882
|
cwd: materialized.directory,
|
|
2733
2883
|
harnessMode: true,
|
|
2734
|
-
|
|
2884
|
+
systemPromptFile,
|
|
2885
|
+
framerHarness: framerAgentOutcome ? executionOutcome : false,
|
|
2735
2886
|
harnessToolNames: harnessTools.toolNames,
|
|
2736
2887
|
nativeSkillsEnabled: Boolean(materializedNativeSkills),
|
|
2737
2888
|
imageFiles,
|
|
@@ -2871,7 +3022,12 @@ async function executeOutcomeRun(run, send, agent, options = {}) {
|
|
|
2871
3022
|
sourceHash:
|
|
2872
3023
|
synchronization?.sourceHash || harnessTools.sourceHash() || null,
|
|
2873
3024
|
...(typeof harnessTools.summary === 'function'
|
|
2874
|
-
? {
|
|
3025
|
+
? {
|
|
3026
|
+
framer: {
|
|
3027
|
+
...harnessTools.summary(),
|
|
3028
|
+
webCalls: webCallSummary(),
|
|
3029
|
+
},
|
|
3030
|
+
}
|
|
2875
3031
|
: {}),
|
|
2876
3032
|
}
|
|
2877
3033
|
: completion;
|
|
@@ -2914,7 +3070,10 @@ async function executeOutcomeRun(run, send, agent, options = {}) {
|
|
|
2914
3070
|
usageAccumulator.add(failure.companionUsage || {});
|
|
2915
3071
|
const framer =
|
|
2916
3072
|
typeof harnessTools.summary === 'function'
|
|
2917
|
-
?
|
|
3073
|
+
? {
|
|
3074
|
+
...harnessTools.summary(),
|
|
3075
|
+
webCalls: webCallSummary(),
|
|
3076
|
+
}
|
|
2918
3077
|
: undefined;
|
|
2919
3078
|
failure.framer = framer;
|
|
2920
3079
|
failure.companionUsage = {
|
package/src/agentOutput.js
CHANGED
|
@@ -320,6 +320,30 @@ function claudeToolProgress(name, input = {}, cwd) {
|
|
|
320
320
|
};
|
|
321
321
|
}
|
|
322
322
|
|
|
323
|
+
function webSite(value) {
|
|
324
|
+
if (typeof value !== 'string' || !value.trim()) return null;
|
|
325
|
+
for (const candidate of value.match(/https?:\/\/[^\s"'<>]+/gi) || []) {
|
|
326
|
+
try {
|
|
327
|
+
return new URL(candidate).hostname.toLowerCase();
|
|
328
|
+
} catch {
|
|
329
|
+
// Keep looking for a valid URL.
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
return null;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function claudeToolResultMessage(part) {
|
|
336
|
+
if (typeof part?.content === 'string') return progressText(part.content, 300);
|
|
337
|
+
if (!Array.isArray(part?.content)) return '';
|
|
338
|
+
return progressText(
|
|
339
|
+
part.content
|
|
340
|
+
.map((item) => typeof item?.text === 'string' ? item.text : '')
|
|
341
|
+
.filter(Boolean)
|
|
342
|
+
.join(' '),
|
|
343
|
+
300,
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
|
|
323
347
|
function summedClaudeProgressUsage(turnUsage) {
|
|
324
348
|
const total = normalizeCompanionTokenUsage();
|
|
325
349
|
for (const usage of turnUsage.values()) {
|
|
@@ -403,7 +427,21 @@ export function createClaudeProgressParser(onProgress, options = {}) {
|
|
|
403
427
|
continue;
|
|
404
428
|
}
|
|
405
429
|
const progress = claudeToolProgress(part.name, input, options.cwd);
|
|
406
|
-
|
|
430
|
+
const nativeWebTool =
|
|
431
|
+
part.name === 'WebFetch'
|
|
432
|
+
? 'WebFetch'
|
|
433
|
+
: part.name === 'WebSearch'
|
|
434
|
+
? 'WebSearch'
|
|
435
|
+
: null;
|
|
436
|
+
const webCall = nativeWebTool
|
|
437
|
+
? {
|
|
438
|
+
id: part.id,
|
|
439
|
+
tool: nativeWebTool,
|
|
440
|
+
site: webSite(input.url || input.query),
|
|
441
|
+
status: 'started',
|
|
442
|
+
}
|
|
443
|
+
: null;
|
|
444
|
+
tools.set(part.id, { name: part.name, ...progress, webCall });
|
|
407
445
|
emit({
|
|
408
446
|
kind: 'item_started',
|
|
409
447
|
name: part.name,
|
|
@@ -411,6 +449,7 @@ export function createClaudeProgressParser(onProgress, options = {}) {
|
|
|
411
449
|
activity: `tool:${part.id}`,
|
|
412
450
|
paths: progress.paths,
|
|
413
451
|
message: progress.startedMessage,
|
|
452
|
+
...(webCall ? { webCall } : {}),
|
|
414
453
|
});
|
|
415
454
|
}
|
|
416
455
|
};
|
|
@@ -430,6 +469,17 @@ export function createClaudeProgressParser(onProgress, options = {}) {
|
|
|
430
469
|
activity: `tool:${part.tool_use_id}`,
|
|
431
470
|
paths: tool.paths,
|
|
432
471
|
message: failed ? tool.failedMessage : tool.completedMessage,
|
|
472
|
+
...(tool.webCall
|
|
473
|
+
? {
|
|
474
|
+
webCall: {
|
|
475
|
+
...tool.webCall,
|
|
476
|
+
status: failed ? 'failed' : 'succeeded',
|
|
477
|
+
...(failed
|
|
478
|
+
? { errorMessage: claudeToolResultMessage(part) || tool.failedMessage }
|
|
479
|
+
: {}),
|
|
480
|
+
},
|
|
481
|
+
}
|
|
482
|
+
: {}),
|
|
433
483
|
});
|
|
434
484
|
}
|
|
435
485
|
};
|
package/src/api.js
CHANGED
|
@@ -235,6 +235,37 @@ export function getFramerProjectAuthorization(apiBaseUrl, {
|
|
|
235
235
|
);
|
|
236
236
|
}
|
|
237
237
|
|
|
238
|
+
// Reads one page of a website the user named, through Dexter. A page render
|
|
239
|
+
// can take most of a minute, so this waits longer than other calls.
|
|
240
|
+
export function readFramerWebsite(apiBaseUrl, {
|
|
241
|
+
deviceToken,
|
|
242
|
+
runId,
|
|
243
|
+
url,
|
|
244
|
+
screenshotTop,
|
|
245
|
+
fetchImpl,
|
|
246
|
+
signal,
|
|
247
|
+
allowInsecureHttp,
|
|
248
|
+
}) {
|
|
249
|
+
return requestJson(
|
|
250
|
+
apiBaseUrl,
|
|
251
|
+
`/ai/framer/companion/runs/${encodeURIComponent(runId)}/read-website`,
|
|
252
|
+
{
|
|
253
|
+
method: 'POST',
|
|
254
|
+
deviceToken,
|
|
255
|
+
fetchImpl,
|
|
256
|
+
signal,
|
|
257
|
+
allowInsecureHttp,
|
|
258
|
+
timeoutMs: 150_000,
|
|
259
|
+
body: {
|
|
260
|
+
url,
|
|
261
|
+
...(Number.isInteger(screenshotTop) ? { screenshotTop } : {}),
|
|
262
|
+
platform: process.platform,
|
|
263
|
+
companionVersion: BRIDGE_VERSION,
|
|
264
|
+
},
|
|
265
|
+
},
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
|
|
238
269
|
export function reportFramerProjectAuthorizationVerification(apiBaseUrl, {
|
|
239
270
|
deviceToken,
|
|
240
271
|
runId,
|
package/src/config.js
CHANGED