@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gakim-digital/dexter-bridge",
3
- "version": "0.11.16",
3
+ "version": "0.11.19",
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": {
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
- ? [...(options.nativeSkillsEnabled ? ['Skill'] : []), ...harnessTools]
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) permissionMode.push('--allowedTools', nativeTools.join(','));
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
- '--system-prompt',
486
- options.harnessMode
487
- ? claudeOutcomeSystemPrompt(options.product)
488
- : claudeModelEngineSystemPrompt(options.product),
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 sessionKey = companionSessionKey(run);
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
- && event.source === 'harness';
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
- (userFacingUpdate ? `assistant-progress:${Date.now()}` : undefined),
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
- rememberedCompanionSession(sessionKey) ||
2450
- (run?.outcome?.session?.providerSessionId
2451
- ? {
2452
- providerSessionId: run.outcome.session.providerSessionId,
2453
- agent: definition.id,
2454
- updatedAt: Date.now(),
2455
- }
2456
- : null);
2457
- const framerAgentOutcome =
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
- prompt = buildOutcomePrompt(executionOutcome, product, {
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
- framerHarness: framerAgentOutcome,
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
- framerHarness: framerAgentOutcome,
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
- ? { framer: harnessTools.summary() }
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
- ? harnessTools.summary()
3073
+ ? {
3074
+ ...harnessTools.summary(),
3075
+ webCalls: webCallSummary(),
3076
+ }
2918
3077
  : undefined;
2919
3078
  failure.framer = framer;
2920
3079
  failure.companionUsage = {
@@ -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
- tools.set(part.id, { name: part.name, ...progress });
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
@@ -44,6 +44,7 @@ export const BRIDGE_CAPABILITIES = [
44
44
  'framer-project-bootstrap-v1',
45
45
  'framer-project-verification-v1',
46
46
  'framer-interaction-verification-v4',
47
+ 'framer-read-website-v1',
47
48
  'project-shell-v1',
48
49
  'build-fingerprint-v1',
49
50
  'tool-schema-parity-v1',