browsentic 0.4.6 → 0.4.9

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/dist/cli.js CHANGED
@@ -28508,6 +28508,28 @@ var hoverElement = defineAction({
28508
28508
  }
28509
28509
  });
28510
28510
 
28511
+ // ../lib/actions/page/inject-code.ts
28512
+ var MAX_CODE_LENGTH = 32768;
28513
+ var injectCode = defineAction({
28514
+ name: "page.injectCode",
28515
+ description: "Install a small toolkit of JavaScript functions into the page, to be called later with page.runCode. Reach for it only when the ordinary tools are the wrong shape: a step sequence you are about to repeat three or more times with different inputs (create 20 tags, delete every row), or a capability no tool covers (seek a video, read a canvas, drive a bespoke editor API). The user reviews and approves the code before it runs \u2014 one approval covers every later page.runCode call and survives page reloads, so batch work needs no further prompts. The toolkit is bound to the tab and origin it was approved on; navigating to another site voids it. Installing goes through Chrome\u2019s debugger, so the browser shows a \u201CBrowsentic is debugging this browser\u201D bar for the moment it takes, it cannot install on a tab that has DevTools open, and it is unavailable on Firefox \u2014 the calls afterwards are cheap and show nothing. For a one-off click or fill, the ordinary tools are always the better choice.",
28516
+ input: external_exports.object({
28517
+ purpose: external_exports.string().min(1).max(200).describe(
28518
+ "One plain sentence saying what this toolkit does and why it is needed \u2014 shown to the user on the approval prompt, so write it for them, not for the page."
28519
+ ),
28520
+ code: external_exports.string().min(1).max(MAX_CODE_LENGTH).describe(
28521
+ "JavaScript source that assigns each entry point onto the provided `tools` object: `tools.addTag = async (name) => {\u2026}`. It runs once in the page\u2019s main world after the user approves it, so it can use the page\u2019s DOM, globals and same-origin fetch, but nothing of the extension. Keep it pure of data: anything that varies per call arrives as arguments through page.runCode, and secrets never belong in it. Async functions are awaited; return values must be JSON-serializable."
28522
+ ),
28523
+ call: external_exports.object({
28524
+ function: external_exports.string().min(1).describe("Name of a function the code assigns onto `tools`."),
28525
+ args: external_exports.array(external_exports.unknown()).default([]).describe("Arguments for that first call, JSON values only.")
28526
+ }).optional().describe("Call one of the new functions immediately after installing, saving a round trip when the first use is already known.")
28527
+ }),
28528
+ execute() {
28529
+ throw new ActionError("page.injectCode is resolved by the Browsentic extension, not in the page", "UNSUPPORTED");
28530
+ }
28531
+ });
28532
+
28511
28533
  // ../lib/actions/page/list-downloads.ts
28512
28534
  var listDownloads = defineAction({
28513
28535
  name: "page.listDownloads",
@@ -28636,12 +28658,9 @@ var CURSOR_PATHS = [
28636
28658
  ];
28637
28659
  var CURSOR2 = `url("data:image/svg+xml,${encodeURIComponent(cursorSvg())}") 14 14, crosshair`;
28638
28660
  var DEFAULT_HINT = "Click the element you mean";
28639
- var picking = false;
28640
- function lensIsUp() {
28641
- return picking;
28642
- }
28661
+ var dismissCurrent = null;
28643
28662
  function pickWithLens({ hint, timeoutMs }) {
28644
- picking = true;
28663
+ dismissCurrent?.();
28645
28664
  const host = document.createElement("div");
28646
28665
  host.id = HOST_ID;
28647
28666
  host.style.cssText = "all: initial; position: static;";
@@ -28656,6 +28675,8 @@ function pickWithLens({ hint, timeoutMs }) {
28656
28675
  const chip = root.querySelector(".chip");
28657
28676
  let hovered = null;
28658
28677
  return new Promise((resolve) => {
28678
+ const dismiss = () => settle2({ cancelled: true });
28679
+ dismissCurrent = dismiss;
28659
28680
  const timer = setTimeout(() => settle2({ timedOut: true }), timeoutMs);
28660
28681
  const mute = (event) => {
28661
28682
  event.stopPropagation();
@@ -28720,7 +28741,7 @@ function pickWithLens({ hint, timeoutMs }) {
28720
28741
  }
28721
28742
  host.remove();
28722
28743
  cursor.remove();
28723
- picking = false;
28744
+ if (dismissCurrent === dismiss) dismissCurrent = null;
28724
28745
  resolve(outcome);
28725
28746
  }
28726
28747
  });
@@ -28799,18 +28820,16 @@ function styles() {
28799
28820
 
28800
28821
  // ../lib/actions/page/pick-element.ts
28801
28822
  var MAX_CONTENT = 2e4;
28823
+ var PICK_DEFAULT_TIMEOUT_MS = 6e4;
28802
28824
  var pickElement = defineAction({
28803
28825
  name: "page.pickElement",
28804
- description: "Ask the user to point at an element \u2014 A-Eye. Their cursor becomes a lens, whatever they hover is outlined, and the element they click comes back with its selector, its role and its rendered text. Use it when a target is genuinely ambiguous \u2014 several things share a label, or the user said \u201Cthis one\u201D about something you cannot see \u2014 and pointing is faster than describing. It takes over the page and waits for a person, so never call it to explore, and never call it twice in a row.",
28826
+ description: "Ask the user to point at an element \u2014 A-Eye. Their cursor becomes a lens, whatever they hover is outlined, and the element they click comes back with its selector, its role, its rendered text and a screenshot of it exactly as they saw it. Use it when a target is genuinely ambiguous \u2014 several things share a label, or the user said \u201Cthis one\u201D about something you cannot see \u2014 and pointing is faster than describing. It takes over the page and waits for a person, so never call it to explore; a new call dismisses a pick already waiting.",
28805
28827
  input: external_exports.object({
28806
28828
  hint: external_exports.string().max(120).optional().describe("One line shown over the page saying what to point at, e.g. \u201CPoint at the price you mean\u201D"),
28807
28829
  maxContentLength: external_exports.number().int().positive().max(MAX_CONTENT).default(2e3).describe('Characters of the element\u2019s rendered text to return; past that it is cut and "truncated" comes back true'),
28808
- timeoutMs: external_exports.number().int().min(5e3).max(3e5).default(6e4).describe("How long to wait for the user to click before giving up")
28830
+ timeoutMs: external_exports.number().int().min(5e3).max(3e5).default(PICK_DEFAULT_TIMEOUT_MS).describe("How long to wait for the user to click before giving up")
28809
28831
  }),
28810
28832
  async execute({ hint, maxContentLength, timeoutMs }) {
28811
- if (lensIsUp()) {
28812
- throw new ActionError("A-Eye is already waiting for the user to point at something", "ACTION_FAILED");
28813
- }
28814
28833
  const outcome = await pickWithLens({ hint, timeoutMs });
28815
28834
  if ("timedOut" in outcome) {
28816
28835
  throw new ActionError(
@@ -28824,12 +28843,18 @@ var pickElement = defineAction({
28824
28843
  const element = outcome.picked;
28825
28844
  const rendered2 = element instanceof HTMLElement ? element.innerText : element.textContent ?? "";
28826
28845
  const content = rendered2.replace(/\n{3,}/g, "\n\n").trim() || accessibleText(element);
28846
+ const rect = element.getBoundingClientRect();
28827
28847
  return {
28828
28848
  element: describeElement(element),
28829
28849
  content: content.slice(0, maxContentLength),
28830
28850
  truncated: content.length > maxContentLength,
28831
28851
  url: location.href,
28832
- title: document.title
28852
+ title: document.title,
28853
+ capture: {
28854
+ region: { x: rect.x, y: rect.y, w: rect.width, h: rect.height },
28855
+ viewport: { w: window.innerWidth, h: window.innerHeight },
28856
+ dpr: window.devicePixelRatio || 1
28857
+ }
28833
28858
  };
28834
28859
  }
28835
28860
  });
@@ -29141,6 +29166,64 @@ function renderDiagram2(tree) {
29141
29166
  return lines.join("\n");
29142
29167
  }
29143
29168
 
29169
+ // ../lib/actions/page/toolkit.ts
29170
+ var TOOLKIT_ATTRIBUTE = "data-browsentic-toolkit";
29171
+ var TOOLKIT_CALL_EVENT = "browsentic:toolkit:call";
29172
+ var TOOLKIT_RESULT_EVENT = "browsentic:toolkit:result";
29173
+ var TOOLKIT_MISSING = "TOOLKIT_MISSING";
29174
+ var CODE_ERROR = "CODE_ERROR";
29175
+ function callToolkit(fn, args, timeoutMs) {
29176
+ if (!document.documentElement.hasAttribute(TOOLKIT_ATTRIBUTE)) {
29177
+ throw new ActionError("No toolkit is installed in this page.", TOOLKIT_MISSING);
29178
+ }
29179
+ const callId = crypto.randomUUID();
29180
+ return new Promise((resolve, reject) => {
29181
+ const onResult = (event) => {
29182
+ const reply = parseReply(event.detail);
29183
+ if (reply?.id !== callId) return;
29184
+ stop2();
29185
+ if (reply.ok) resolve(reply.value ?? null);
29186
+ else reject(new ActionError(reply.error ?? "The function threw.", CODE_ERROR));
29187
+ };
29188
+ const timer = setTimeout(() => {
29189
+ stop2();
29190
+ reject(new ActionError(`\u201C${fn}\u201D did not finish within ${timeoutMs}ms.`, "TIMEOUT"));
29191
+ }, timeoutMs);
29192
+ function stop2() {
29193
+ clearTimeout(timer);
29194
+ window.removeEventListener(TOOLKIT_RESULT_EVENT, onResult);
29195
+ }
29196
+ window.addEventListener(TOOLKIT_RESULT_EVENT, onResult);
29197
+ window.dispatchEvent(
29198
+ new CustomEvent(TOOLKIT_CALL_EVENT, { detail: JSON.stringify({ id: callId, fn, args }) })
29199
+ );
29200
+ });
29201
+ }
29202
+ function parseReply(detail) {
29203
+ if (typeof detail !== "string") return null;
29204
+ try {
29205
+ return JSON.parse(detail);
29206
+ } catch {
29207
+ return null;
29208
+ }
29209
+ }
29210
+
29211
+ // ../lib/actions/page/run-code.ts
29212
+ var runCode = defineAction({
29213
+ name: "page.runCode",
29214
+ description: "Call one function from the toolkit page.injectCode installed in this tab, with fresh arguments. This is the cheap, repeatable half of the pair: the user approved the code once, so every call runs without another prompt, and a page reload re-installs the approved toolkit on its own. It refuses if nothing is installed here, or if the tab has moved to a different site than the one the code was approved on \u2014 inject again in either case. The function\u2019s return value comes back as JSON.",
29215
+ input: external_exports.object({
29216
+ function: external_exports.string().min(1).describe("Name of a function the installed toolkit assigned onto `tools`."),
29217
+ args: external_exports.array(external_exports.unknown()).default([]).describe(
29218
+ "Arguments passed to the function, in order. JSON values only \u2014 this is where per-call data like a tag name belongs."
29219
+ ),
29220
+ timeoutMs: external_exports.number().int().min(100).max(12e4).default(1e4).describe("How long the call may run before it is abandoned. Raise it for functions that wait on the page.")
29221
+ }),
29222
+ async execute({ function: fn, args, timeoutMs }) {
29223
+ return { function: fn, returned: await callToolkit(fn, args, timeoutMs) };
29224
+ }
29225
+ });
29226
+
29144
29227
  // ../lib/actions/page/screenshot.ts
29145
29228
  var screenshot = defineAction({
29146
29229
  name: "page.screenshot",
@@ -29846,6 +29929,8 @@ var actions = new Map(
29846
29929
  pressKey,
29847
29930
  submitForm,
29848
29931
  waitForElement,
29932
+ injectCode,
29933
+ runCode,
29849
29934
  findProgress,
29850
29935
  findSearch,
29851
29936
  readTheme,
@@ -29895,7 +29980,8 @@ var AGENTS = {
29895
29980
  vendor: "Anthropic",
29896
29981
  bin: "claude",
29897
29982
  install: "npm i -g @anthropic-ai/claude-code",
29898
- docs: "https://claude.com/claude-code"
29983
+ docs: "https://claude.com/claude-code",
29984
+ models: ["claude-fable-5", "claude-opus-5", "claude-sonnet-5", "claude-haiku-4-5"]
29899
29985
  },
29900
29986
  codex: {
29901
29987
  kind: "codex",
@@ -29903,7 +29989,8 @@ var AGENTS = {
29903
29989
  vendor: "OpenAI",
29904
29990
  bin: "codex",
29905
29991
  install: "npm i -g @openai/codex",
29906
- docs: "https://developers.openai.com/codex/cli"
29992
+ docs: "https://developers.openai.com/codex/cli",
29993
+ models: ["gpt-5.6-terra", "gpt-5.1-codex-max", "gpt-5.1-codex", "gpt-5.1-codex-mini"]
29907
29994
  },
29908
29995
  antigravity: {
29909
29996
  kind: "antigravity",
@@ -29911,7 +29998,8 @@ var AGENTS = {
29911
29998
  vendor: "Google",
29912
29999
  bin: "agy",
29913
30000
  install: "https://antigravity.google/docs/cli/install",
29914
- docs: "https://antigravity.google/docs/cli"
30001
+ docs: "https://antigravity.google/docs/cli",
30002
+ models: ["gemini-3-pro", "gemini-3-flash"]
29915
30003
  }
29916
30004
  };
29917
30005
  var AGENT_LIST = AGENT_KINDS.map((kind) => AGENTS[kind]);
@@ -29925,11 +30013,13 @@ var SAVE_SITE_MAP_ACTION = `${RESERVED_PREFIX}saveSiteMap`;
29925
30013
  var START_RECORDING_ACTION = `${RESERVED_PREFIX}startRecording`;
29926
30014
  var STOP_RECORDING_ACTION = `${RESERVED_PREFIX}stopRecording`;
29927
30015
  var READ_SITEMAP_ACTION = `${RESERVED_PREFIX}readSitemap`;
30016
+ var FOCUS_SHOT_ACTION = `${RESERVED_PREFIX}focusShot`;
29928
30017
  var RESERVED_ACTIONS = [
29929
30018
  SAVE_SITE_MAP_ACTION,
29930
30019
  START_RECORDING_ACTION,
29931
30020
  STOP_RECORDING_ACTION,
29932
- READ_SITEMAP_ACTION
30021
+ READ_SITEMAP_ACTION,
30022
+ FOCUS_SHOT_ACTION
29933
30023
  ];
29934
30024
 
29935
30025
  // ../lib/actions/tool-names.ts
@@ -30243,6 +30333,15 @@ var claudeRunner = {
30243
30333
  };
30244
30334
  },
30245
30335
  reader() {
30336
+ let generated = 0;
30337
+ const report = (usage, sink) => {
30338
+ if (!usage) return;
30339
+ generated += usage.output_tokens ?? 0;
30340
+ sink.usage({
30341
+ contextTokens: (usage.input_tokens ?? 0) + (usage.cache_read_input_tokens ?? 0) + (usage.cache_creation_input_tokens ?? 0) + (usage.output_tokens ?? 0),
30342
+ outputTokens: generated
30343
+ });
30344
+ };
30246
30345
  return (line, sink) => {
30247
30346
  const message = parseJsonLine(line);
30248
30347
  if (!message) return;
@@ -30266,10 +30365,14 @@ var claudeRunner = {
30266
30365
  }
30267
30366
  return;
30268
30367
  }
30368
+ case "assistant":
30369
+ if (!message.parent_tool_use_id) report(message.message?.usage, sink);
30370
+ return;
30269
30371
  case "result":
30270
30372
  if (message.is_error) {
30271
30373
  return sink.fail("AGENT_FAILED", message.result || message.subtype || "Claude Code reported an error");
30272
30374
  }
30375
+ if (!generated) report(message.usage, sink);
30273
30376
  return sink.done(message.stop_reason || "end_turn");
30274
30377
  }
30275
30378
  };
@@ -30580,6 +30683,17 @@ var codexRunner = {
30580
30683
  return finish(msg.message, sink);
30581
30684
  case "web_search_begin":
30582
30685
  return sink.tool(randomUUID3(), WEB_TOOL);
30686
+ case "token_count": {
30687
+ const last = msg.info?.last_token_usage ?? msg.info?.total_token_usage;
30688
+ const total = msg.info?.total_token_usage ?? last;
30689
+ if (last) {
30690
+ sink.usage({
30691
+ contextTokens: (last.input_tokens ?? 0) + (last.output_tokens ?? 0),
30692
+ outputTokens: total?.output_tokens ?? 0
30693
+ });
30694
+ }
30695
+ return;
30696
+ }
30583
30697
  case "task_complete":
30584
30698
  return sink.done("end_turn");
30585
30699
  case "error":
@@ -30603,8 +30717,16 @@ var codexRunner = {
30603
30717
  if (kind === "web_search") return sink.tool(item?.id ?? randomUUID3(), WEB_TOOL);
30604
30718
  return;
30605
30719
  }
30606
- case "turn.completed":
30720
+ case "turn.completed": {
30721
+ const usage = frame.usage;
30722
+ if (usage) {
30723
+ sink.usage({
30724
+ contextTokens: (usage.input_tokens ?? 0) + (usage.output_tokens ?? 0),
30725
+ outputTokens: usage.output_tokens ?? 0
30726
+ });
30727
+ }
30607
30728
  return sink.done("end_turn");
30729
+ }
30608
30730
  case "turn.failed":
30609
30731
  return sink.fail("AGENT_FAILED", frame.error?.message || "Codex could not finish the turn");
30610
30732
  case "error":
@@ -31181,6 +31303,26 @@ var DEFAULT_RULES = [
31181
31303
  title: "Saves a file from the page to disk",
31182
31304
  reason: "That writes a file the page chose into the user\u2019s download folder."
31183
31305
  },
31306
+ {
31307
+ // The most powerful thing an agent can ask for, and the one gate that has to show
31308
+ // its work: the panel puts the source behind a Review button, because "allow this
31309
+ // action?" is not a question anyone can answer about code they have not read. It
31310
+ // confirms rather than denies because a reviewed function is how twenty repetitions
31311
+ // stop being twenty round trips — and `unattended: deny` keeps an external MCP
31312
+ // client, which has nobody to show the code to, from installing any at all.
31313
+ id: "code-injection",
31314
+ when: "injectsCode",
31315
+ effect: "confirm",
31316
+ title: "Runs code it wrote in the page",
31317
+ reason: "That installs JavaScript the agent wrote into the page, with your logged-in session."
31318
+ },
31319
+ {
31320
+ id: "external-code-execution",
31321
+ when: "runsCodeOutsideThePanel",
31322
+ effect: "deny",
31323
+ title: "Calls injected code from outside the panel",
31324
+ reason: "Code installed by page.injectCode was reviewed and approved for the side-panel conversation that asked for it. It is not available to an MCP client."
31325
+ },
31184
31326
  {
31185
31327
  id: "leaves-pinned-tab",
31186
31328
  when: "leavesPinnedTab",
@@ -31707,6 +31849,7 @@ function invokeTimeoutFor(action, input2) {
31707
31849
  const declared = input2?.timeoutMs;
31708
31850
  if (typeof declared === "number" && declared > 0) return declared + 1e4;
31709
31851
  if (action === awaitMonitor.name) return AWAIT_DEFAULT_TIMEOUT_MS + 1e4;
31852
+ if (action === pickElement.name) return PICK_DEFAULT_TIMEOUT_MS + 1e4;
31710
31853
  return REQUEST_TIMEOUT_MS;
31711
31854
  }
31712
31855
 
@@ -33455,6 +33598,12 @@ var Server = class extends Protocol {
33455
33598
  // server.ts
33456
33599
  var STATUS_TOOL = toolNameFor(`${RESERVED_PREFIX}status`);
33457
33600
  var SCREENSHOT_TOOL = "page_screenshot";
33601
+ var PICK_TOOL = "page_pickElement";
33602
+ var FOCUS_SHOT_TOOL = {
33603
+ name: toolNameFor(FOCUS_SHOT_ACTION),
33604
+ description: "Show the screenshot of the element the user pointed at with A-Eye, taken at the instant they picked it. Only answers when the current instruction arrived with a pick attached.",
33605
+ inputSchema: { type: "object", properties: {}, additionalProperties: false }
33606
+ };
33458
33607
  var RESOURCES = [
33459
33608
  {
33460
33609
  uri: "browsentic://page/current",
@@ -33568,7 +33717,7 @@ function createMcpServer(bridge, version2, opts = {}) {
33568
33717
  description: "Report whether the Browsentic browser extension is connected, its version, and the active tab. Use this first if a page tool fails.",
33569
33718
  inputSchema: { type: "object", properties: {}, additionalProperties: false }
33570
33719
  },
33571
- ...opts.agentRun ? [SAVE_SITE_MAP_TOOL] : []
33720
+ ...opts.agentRun ? [SAVE_SITE_MAP_TOOL, FOCUS_SHOT_TOOL] : []
33572
33721
  ]
33573
33722
  };
33574
33723
  });
@@ -33577,6 +33726,8 @@ function createMcpServer(bridge, version2, opts = {}) {
33577
33726
  const action = actionNameFor(params.name);
33578
33727
  const result = await bridge.invoke(action, params.arguments ?? {});
33579
33728
  if (params.name === SCREENSHOT_TOOL) return renderScreenshot(result);
33729
+ if (params.name === FOCUS_SHOT_TOOL.name) return renderFocusShot(result);
33730
+ if (params.name === PICK_TOOL) return renderPick(result, shouldFence(action, policy) ? tag2 : void 0);
33580
33731
  return render(result, shouldFence(action, policy) ? tag2 : void 0);
33581
33732
  });
33582
33733
  server.setRequestHandler(ListResourcesRequestSchema, async () => ({ resources: [...RESOURCES] }));
@@ -33654,6 +33805,38 @@ function renderScreenshot(result) {
33654
33805
  ]
33655
33806
  };
33656
33807
  }
33808
+ function renderPick(result, fenceWith) {
33809
+ if (!result.ok) return render(result, fenceWith);
33810
+ const { shot, ...rest } = result.data;
33811
+ if (typeof shot?.dataUrl !== "string") return render(result, fenceWith);
33812
+ const [mimeType, base643] = splitDataUrl(shot.dataUrl);
33813
+ const rendered2 = render({ ok: true, data: rest }, fenceWith);
33814
+ return {
33815
+ content: [
33816
+ ...rendered2.content,
33817
+ { type: "image", data: base643, mimeType },
33818
+ {
33819
+ type: "text",
33820
+ text: `${IMAGE_NOTE} This is the picked element photographed at the instant the user clicked it.`
33821
+ }
33822
+ ]
33823
+ };
33824
+ }
33825
+ function renderFocusShot(result) {
33826
+ if (!result.ok) return render(result);
33827
+ const dataUrl = result.data?.dataUrl;
33828
+ if (typeof dataUrl !== "string") return render(result);
33829
+ const [mimeType, base643] = splitDataUrl(dataUrl);
33830
+ return {
33831
+ content: [
33832
+ { type: "image", data: base643, mimeType },
33833
+ {
33834
+ type: "text",
33835
+ text: `${IMAGE_NOTE} This is the element the user pointed at with A-Eye, as it stood when they picked it.`
33836
+ }
33837
+ ]
33838
+ };
33839
+ }
33657
33840
  function splitDataUrl(dataUrl) {
33658
33841
  const match = /^data:([^;,]+);base64,(.*)$/s.exec(dataUrl);
33659
33842
  return match ? [match[1], match[2]] : ["image/png", ""];
@@ -33679,7 +33862,7 @@ function text2(uri, mimeType, body) {
33679
33862
  // package.json
33680
33863
  var package_default = {
33681
33864
  name: "browsentic",
33682
- version: "0.4.6",
33865
+ version: "0.4.9",
33683
33866
  description: "Hand your real, logged-in browser to the AI agent you already run. Installs the browser extension, runs the local daemon, and speaks MCP.",
33684
33867
  type: "module",
33685
33868
  license: "MIT",