browsentic 0.4.12 → 0.4.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/dist/cli.js CHANGED
@@ -2276,7 +2276,7 @@ var require_websocket = __commonJS({
2276
2276
  var http = __require("http");
2277
2277
  var net = __require("net");
2278
2278
  var tls = __require("tls");
2279
- var { randomBytes: randomBytes3, createHash: createHash3 } = __require("crypto");
2279
+ var { randomBytes: randomBytes4, createHash: createHash3 } = __require("crypto");
2280
2280
  var { Duplex, Readable } = __require("stream");
2281
2281
  var { URL: URL2 } = __require("url");
2282
2282
  var PerMessageDeflate2 = require_permessage_deflate();
@@ -2814,7 +2814,7 @@ var require_websocket = __commonJS({
2814
2814
  }
2815
2815
  }
2816
2816
  const defaultPort = isSecure ? 443 : 80;
2817
- const key = randomBytes3(16).toString("base64");
2817
+ const key = randomBytes4(16).toString("base64");
2818
2818
  const request = isSecure ? https.request : http.request;
2819
2819
  const protocolSet = /* @__PURE__ */ new Set();
2820
2820
  let perMessageDeflate;
@@ -6658,7 +6658,7 @@ var require_compile = __commonJS({
6658
6658
  const schOrFunc = root.refs[ref];
6659
6659
  if (schOrFunc)
6660
6660
  return schOrFunc;
6661
- let _sch = resolve.call(this, root, ref);
6661
+ let _sch = resolve3.call(this, root, ref);
6662
6662
  if (_sch === void 0) {
6663
6663
  const schema = (_a3 = root.localRefs) === null || _a3 === void 0 ? void 0 : _a3[ref];
6664
6664
  const { schemaId } = this.opts;
@@ -6685,7 +6685,7 @@ var require_compile = __commonJS({
6685
6685
  function sameSchemaEnv(s1, s2) {
6686
6686
  return s1.schema === s2.schema && s1.root === s2.root && s1.baseId === s2.baseId;
6687
6687
  }
6688
- function resolve(root, ref) {
6688
+ function resolve3(root, ref) {
6689
6689
  let sch;
6690
6690
  while (typeof (sch = this.refs[ref]) == "string")
6691
6691
  ref = sch;
@@ -7316,55 +7316,55 @@ var require_fast_uri = __commonJS({
7316
7316
  }
7317
7317
  return uri;
7318
7318
  }
7319
- function resolve(baseURI, relativeURI, options) {
7319
+ function resolve3(baseURI, relativeURI, options) {
7320
7320
  const schemelessOptions = options ? Object.assign({ scheme: "null" }, options) : { scheme: "null" };
7321
7321
  const resolved = resolveComponent(parse3(baseURI, schemelessOptions), parse3(relativeURI, schemelessOptions), schemelessOptions, true);
7322
7322
  schemelessOptions.skipEscape = true;
7323
7323
  return serialize(resolved, schemelessOptions);
7324
7324
  }
7325
- function resolveComponent(base, relative2, options, skipNormalization) {
7325
+ function resolveComponent(base, relative3, options, skipNormalization) {
7326
7326
  const target = {};
7327
7327
  if (!skipNormalization) {
7328
7328
  base = parse3(serialize(base, options), options);
7329
- relative2 = parse3(serialize(relative2, options), options);
7329
+ relative3 = parse3(serialize(relative3, options), options);
7330
7330
  }
7331
7331
  options = options || {};
7332
- if (!options.tolerant && relative2.scheme) {
7333
- target.scheme = relative2.scheme;
7334
- target.userinfo = relative2.userinfo;
7335
- target.host = relative2.host;
7336
- target.port = relative2.port;
7337
- target.path = removeDotSegments(relative2.path || "");
7338
- target.query = relative2.query;
7332
+ if (!options.tolerant && relative3.scheme) {
7333
+ target.scheme = relative3.scheme;
7334
+ target.userinfo = relative3.userinfo;
7335
+ target.host = relative3.host;
7336
+ target.port = relative3.port;
7337
+ target.path = removeDotSegments(relative3.path || "");
7338
+ target.query = relative3.query;
7339
7339
  } else {
7340
- if (relative2.userinfo !== void 0 || relative2.host !== void 0 || relative2.port !== void 0) {
7341
- target.userinfo = relative2.userinfo;
7342
- target.host = relative2.host;
7343
- target.port = relative2.port;
7344
- target.path = removeDotSegments(relative2.path || "");
7345
- target.query = relative2.query;
7340
+ if (relative3.userinfo !== void 0 || relative3.host !== void 0 || relative3.port !== void 0) {
7341
+ target.userinfo = relative3.userinfo;
7342
+ target.host = relative3.host;
7343
+ target.port = relative3.port;
7344
+ target.path = removeDotSegments(relative3.path || "");
7345
+ target.query = relative3.query;
7346
7346
  } else {
7347
- if (!relative2.path) {
7347
+ if (!relative3.path) {
7348
7348
  target.path = base.path;
7349
- if (relative2.query !== void 0) {
7350
- target.query = relative2.query;
7349
+ if (relative3.query !== void 0) {
7350
+ target.query = relative3.query;
7351
7351
  } else {
7352
7352
  target.query = base.query;
7353
7353
  }
7354
7354
  } else {
7355
- if (relative2.path[0] === "/") {
7356
- target.path = removeDotSegments(relative2.path);
7355
+ if (relative3.path[0] === "/") {
7356
+ target.path = removeDotSegments(relative3.path);
7357
7357
  } else {
7358
7358
  if ((base.userinfo !== void 0 || base.host !== void 0 || base.port !== void 0) && !base.path) {
7359
- target.path = "/" + relative2.path;
7359
+ target.path = "/" + relative3.path;
7360
7360
  } else if (!base.path) {
7361
- target.path = relative2.path;
7361
+ target.path = relative3.path;
7362
7362
  } else {
7363
- target.path = base.path.slice(0, base.path.lastIndexOf("/") + 1) + relative2.path;
7363
+ target.path = base.path.slice(0, base.path.lastIndexOf("/") + 1) + relative3.path;
7364
7364
  }
7365
7365
  target.path = removeDotSegments(target.path);
7366
7366
  }
7367
- target.query = relative2.query;
7367
+ target.query = relative3.query;
7368
7368
  }
7369
7369
  target.userinfo = base.userinfo;
7370
7370
  target.host = base.host;
@@ -7372,7 +7372,7 @@ var require_fast_uri = __commonJS({
7372
7372
  }
7373
7373
  target.scheme = base.scheme;
7374
7374
  }
7375
- target.fragment = relative2.fragment;
7375
+ target.fragment = relative3.fragment;
7376
7376
  return target;
7377
7377
  }
7378
7378
  function equal(uriA, uriB, options) {
@@ -7580,7 +7580,7 @@ var require_fast_uri = __commonJS({
7580
7580
  var fastUri = {
7581
7581
  SCHEMES,
7582
7582
  normalize,
7583
- resolve,
7583
+ resolve: resolve3,
7584
7584
  resolveComponent,
7585
7585
  equal,
7586
7586
  serialize,
@@ -10570,7 +10570,8 @@ var require_dist = __commonJS({
10570
10570
  });
10571
10571
 
10572
10572
  // cli.ts
10573
- import { readFileSync as readFileSync9 } from "fs";
10573
+ import { readFileSync as readFileSync10 } from "fs";
10574
+ import { createInterface } from "readline/promises";
10574
10575
 
10575
10576
  // node_modules/@modelcontextprotocol/sdk/dist/esm/server/stdio.js
10576
10577
  import process3 from "process";
@@ -26686,12 +26687,12 @@ var StdioServerTransport = class {
26686
26687
  this.onclose?.();
26687
26688
  }
26688
26689
  send(message) {
26689
- return new Promise((resolve) => {
26690
+ return new Promise((resolve3) => {
26690
26691
  const json2 = serializeMessage(message);
26691
26692
  if (this._stdout.write(json2)) {
26692
- resolve();
26693
+ resolve3();
26693
26694
  } else {
26694
- this._stdout.once("drain", resolve);
26695
+ this._stdout.once("drain", resolve3);
26695
26696
  }
26696
26697
  });
26697
26698
  }
@@ -27680,7 +27681,7 @@ function hoverSequence(el, at) {
27680
27681
 
27681
27682
  // ../lib/actions/page/drag-element.ts
27682
27683
  var FRAME_MS = 16;
27683
- var settle = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
27684
+ var settle = (ms) => new Promise((resolve3) => setTimeout(resolve3, ms));
27684
27685
  function mouseInit(at, buttons) {
27685
27686
  return {
27686
27687
  clientX: at.x,
@@ -28392,8 +28393,8 @@ function renderDiagram(regions) {
28392
28393
  const walk2 = (nodes, indent) => {
28393
28394
  nodes.forEach((region, i) => {
28394
28395
  const last = i === nodes.length - 1;
28395
- const { bounds, contains } = region;
28396
- const counts = ["links", "buttons", "fields"].filter((kind) => contains[kind] > 0).map((kind) => ` \xB7 ${contains[kind]} ${kind}`).join("");
28396
+ const { bounds, contains: contains2 } = region;
28397
+ const counts = ["links", "buttons", "fields"].filter((kind) => contains2[kind] > 0).map((kind) => ` \xB7 ${contains2[kind]} ${kind}`).join("");
28397
28398
  lines.push(
28398
28399
  `${indent}${last ? "\u2514" : "\u251C"} ${regionName(region)} \xB7 ${bounds.width}\xD7${bounds.height} @ (${bounds.x},${bounds.y})${counts}`
28399
28400
  );
@@ -28698,7 +28699,7 @@ function pickWithLens({ hint, timeoutMs }) {
28698
28699
  const box = root.querySelector(".box");
28699
28700
  const chip = root.querySelector(".chip");
28700
28701
  let hovered = null;
28701
- return new Promise((resolve) => {
28702
+ return new Promise((resolve3) => {
28702
28703
  const dismiss = () => settle2({ cancelled: true });
28703
28704
  dismissCurrent = dismiss;
28704
28705
  const timer = setTimeout(() => settle2({ timedOut: true }), timeoutMs);
@@ -28766,7 +28767,7 @@ function pickWithLens({ hint, timeoutMs }) {
28766
28767
  host.remove();
28767
28768
  cursor.remove();
28768
28769
  if (dismissCurrent === dismiss) dismissCurrent = null;
28769
- resolve(outcome);
28770
+ resolve3(outcome);
28770
28771
  }
28771
28772
  });
28772
28773
  }
@@ -29201,12 +29202,12 @@ function callToolkit(fn, args, timeoutMs) {
29201
29202
  throw new ActionError("No toolkit is installed in this page.", TOOLKIT_MISSING);
29202
29203
  }
29203
29204
  const callId = crypto.randomUUID();
29204
- return new Promise((resolve, reject) => {
29205
+ return new Promise((resolve3, reject) => {
29205
29206
  const onResult = (event) => {
29206
29207
  const reply = parseReply(event.detail);
29207
29208
  if (reply?.id !== callId) return;
29208
29209
  stop2();
29209
- if (reply.ok) resolve(reply.value ?? null);
29210
+ if (reply.ok) resolve3(reply.value ?? null);
29210
29211
  else reject(new ActionError(reply.error ?? "The function threw.", CODE_ERROR));
29211
29212
  };
29212
29213
  const timer = setTimeout(() => {
@@ -29330,11 +29331,11 @@ var scrollTo = defineAction({
29330
29331
  }
29331
29332
  });
29332
29333
  function scrollSettled() {
29333
- return new Promise((resolve) => {
29334
+ return new Promise((resolve3) => {
29334
29335
  const done = () => {
29335
29336
  window.removeEventListener("scrollend", done);
29336
29337
  clearTimeout(timer);
29337
- resolve();
29338
+ resolve3();
29338
29339
  };
29339
29340
  const timer = setTimeout(done, 1e3);
29340
29341
  window.addEventListener("scrollend", done);
@@ -29748,7 +29749,7 @@ var LINGER_AFTER = {
29748
29749
  var MAX_DURATION_MS = 12e4;
29749
29750
  var keyDelays = (text3, base) => [...text3].map((char) => base * (LINGER_AFTER[char] ?? 1));
29750
29751
  var budgetFor = (delays, jitter) => Math.round(delays.reduce((total, ms) => total + ms, 0) * (1 + jitter));
29751
- var pause = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
29752
+ var pause = (ms) => new Promise((resolve3) => setTimeout(resolve3, ms));
29752
29753
  var typeText = defineAction({
29753
29754
  name: "page.typeText",
29754
29755
  description: "Stream text into a field one keystroke at a time, at a human pace \u2014 a real key event per character, pauses that vary, and longer breaths after punctuation. Use page.fillInput when you just need the value in the field; use this when the page should watch someone type.",
@@ -29898,12 +29899,12 @@ var waitForElement = defineAction({
29898
29899
  if (satisfied(element)) {
29899
29900
  return { state, elapsedMs: 0, element: element ? describeElement(element) : void 0 };
29900
29901
  }
29901
- await new Promise((resolve, reject) => {
29902
+ await new Promise((resolve3, reject) => {
29902
29903
  const check2 = () => {
29903
29904
  element = find();
29904
29905
  if (satisfied(element)) {
29905
29906
  cleanup();
29906
- resolve();
29907
+ resolve3();
29907
29908
  }
29908
29909
  };
29909
29910
  const observer = new MutationObserver(check2);
@@ -30078,7 +30079,7 @@ function assertToolNamesRoundTrip(actionNames) {
30078
30079
  }
30079
30080
 
30080
30081
  // cli.ts
30081
- import { basename as basename2, join as join15 } from "path";
30082
+ import { basename as basename3, join as join18 } from "path";
30082
30083
 
30083
30084
  // agent/agent-skills.ts
30084
30085
  import { createHash } from "crypto";
@@ -31033,6 +31034,10 @@ var failure = (code, message) => ({
31033
31034
  error: { code, message }
31034
31035
  });
31035
31036
 
31037
+ // guardrails/scope.ts
31038
+ var PROBES = ["https://one.probe.invalid/", "https://two.probe.invalid/"];
31039
+ var PROBE_HOSTS = PROBES.map((base) => new URL(base).hostname);
31040
+
31036
31041
  // ../lib/recordings/events.ts
31037
31042
  var MAX_RECORDING_MS = 15 * 6e4;
31038
31043
  var WARN_AT_MS = 13 * 6e4;
@@ -31291,6 +31296,18 @@ var DEFAULT_RULES = [
31291
31296
  title: "Non-http navigation",
31292
31297
  reason: "Only http(s) URLs can be opened."
31293
31298
  },
31299
+ {
31300
+ // A URL is a destination or it is nothing, and this is the rule that makes that true.
31301
+ // `//evil.com/x` used to arrive here as a null every url condition skipped, while the
31302
+ // page it was typed on resolved it and left the site. Classifying fixed that spelling;
31303
+ // refusing what still will not classify is what stops the next one, without anybody
31304
+ // having to think of it first.
31305
+ id: "unreadable-navigation",
31306
+ when: "unreadableNavigation",
31307
+ effect: "deny",
31308
+ title: "URL with no readable destination",
31309
+ reason: "That URL does not resolve to a destination Browsentic can check. Pass an absolute https:// URL."
31310
+ },
31294
31311
  {
31295
31312
  id: "off-scope-navigation",
31296
31313
  when: "navigatesOffScope",
@@ -31655,6 +31672,38 @@ async function probeExisting() {
31655
31672
  }
31656
31673
  return null;
31657
31674
  }
31675
+ async function runningDaemons() {
31676
+ const found = /* @__PURE__ */ new Map();
31677
+ for (const port of DAEMON_PORTS) {
31678
+ const pid = await healthyPid(port);
31679
+ if (pid !== null && !found.has(pid)) found.set(pid, port);
31680
+ }
31681
+ return [...found].map(([pid, port]) => ({ pid, port }));
31682
+ }
31683
+ async function stopDaemons(graceMs = 5e3) {
31684
+ const daemons = await runningDaemons();
31685
+ for (const { pid } of daemons) kill(pid, "SIGTERM");
31686
+ const remaining = await waitForExit(daemons, graceMs);
31687
+ for (const { pid } of remaining) kill(pid, "SIGKILL");
31688
+ const stubborn = await waitForExit(remaining, 2e3);
31689
+ clearLockfile();
31690
+ return { stopped: daemons.filter((daemon) => !stubborn.includes(daemon)), stubborn };
31691
+ }
31692
+ function kill(pid, signal) {
31693
+ try {
31694
+ process.kill(pid, signal);
31695
+ } catch {
31696
+ }
31697
+ }
31698
+ async function waitForExit(daemons, timeoutMs) {
31699
+ const deadline = Date.now() + timeoutMs;
31700
+ let alive = daemons.filter((daemon) => isRunning(daemon.pid));
31701
+ while (alive.length && Date.now() < deadline) {
31702
+ await delay(POLL_INTERVAL_MS);
31703
+ alive = alive.filter((daemon) => isRunning(daemon.pid));
31704
+ }
31705
+ return alive;
31706
+ }
31658
31707
  async function healthyPid(port) {
31659
31708
  try {
31660
31709
  const response = await fetch(`http://127.0.0.1:${port}/health`, {
@@ -31668,7 +31717,7 @@ async function healthyPid(port) {
31668
31717
  }
31669
31718
  }
31670
31719
  function delay(ms) {
31671
- return new Promise((resolve) => setTimeout(resolve, ms));
31720
+ return new Promise((resolve3) => setTimeout(resolve3, ms));
31672
31721
  }
31673
31722
 
31674
31723
  // install.ts
@@ -31781,6 +31830,71 @@ function install(dir, force = false) {
31781
31830
  return { dir, version: version2, source: packaged.source, files: sources.length, changed, alreadyCurrent: false };
31782
31831
  }
31783
31832
 
31833
+ // npx.ts
31834
+ import { existsSync as existsSync5, readFileSync as readFileSync9, readdirSync as readdirSync5, realpathSync } from "fs";
31835
+ import { homedir as homedir7 } from "os";
31836
+ import { dirname as dirname6, join as join15, resolve, sep as sep2 } from "path";
31837
+ import { fileURLToPath as fileURLToPath5 } from "url";
31838
+ var packageRoot = resolve(dirname6(fileURLToPath5(import.meta.url)), "..");
31839
+ var inNpxCache = (path) => path.split(sep2).includes("_npx");
31840
+ function installKind() {
31841
+ if (inNpxCache(packageRoot)) return "npx";
31842
+ if (existsSync5(join15(packageRoot, "tsup.config.ts"))) return "repo";
31843
+ return "global";
31844
+ }
31845
+ function real(path) {
31846
+ try {
31847
+ return realpathSync(path);
31848
+ } catch {
31849
+ return path;
31850
+ }
31851
+ }
31852
+ function cacheRoots() {
31853
+ const roots = [
31854
+ process.env.npm_config_cache,
31855
+ process.platform === "win32" ? join15(process.env.LOCALAPPDATA ?? homedir7(), "npm-cache") : null,
31856
+ join15(homedir7(), ".npm")
31857
+ ].filter((root) => !!root);
31858
+ return [...new Set(roots.map((root) => join15(root, "_npx")))];
31859
+ }
31860
+ function readJson(path) {
31861
+ try {
31862
+ return JSON.parse(readFileSync9(path, "utf8"));
31863
+ } catch {
31864
+ return null;
31865
+ }
31866
+ }
31867
+ function npxEntries() {
31868
+ const own = inNpxCache(packageRoot) ? real(resolve(packageRoot, "..", "..")) : null;
31869
+ const scanned = cacheRoots().flatMap((root) => {
31870
+ try {
31871
+ return readdirSync5(root, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => real(join15(root, entry.name)));
31872
+ } catch {
31873
+ return [];
31874
+ }
31875
+ });
31876
+ const entries = /* @__PURE__ */ new Map();
31877
+ for (const dir of [...scanned, ...own ? [own] : []]) {
31878
+ if (entries.has(dir)) continue;
31879
+ const manifest = readJson(join15(dir, "node_modules", "browsentic", "package.json"));
31880
+ if (!manifest) continue;
31881
+ const requested = readJson(join15(dir, "package.json"))?._npx;
31882
+ entries.set(dir, {
31883
+ dir,
31884
+ version: typeof manifest.version === "string" ? manifest.version : null,
31885
+ packages: Array.isArray(requested?.packages) ? requested.packages : [],
31886
+ running: dir === own
31887
+ });
31888
+ }
31889
+ return [...entries.values()];
31890
+ }
31891
+ function pinnedVersion() {
31892
+ const own = npxEntries().find((entry) => entry.running);
31893
+ const spec = own?.packages.find((name) => name === "browsentic" || name.startsWith("browsentic@"));
31894
+ const requested = spec?.slice("browsentic@".length);
31895
+ return requested && /^\d+\.\d+\.\d+/.test(requested) ? requested : null;
31896
+ }
31897
+
31784
31898
  // remote-bridge.ts
31785
31899
  import { randomUUID as randomUUID5 } from "crypto";
31786
31900
 
@@ -31807,11 +31921,11 @@ var RemoteBridge = class _RemoteBridge {
31807
31921
  pending = /* @__PURE__ */ new Map();
31808
31922
  manifestListeners = /* @__PURE__ */ new Set();
31809
31923
  static connect(port, token, runId) {
31810
- return new Promise((resolve, reject) => {
31924
+ return new Promise((resolve3, reject) => {
31811
31925
  const socket = new import_websocket.default(`ws://127.0.0.1:${port}/control`, {
31812
31926
  headers: { authorization: `Bearer ${token}` }
31813
31927
  });
31814
- socket.once("open", () => resolve(new _RemoteBridge(socket, runId)));
31928
+ socket.once("open", () => resolve3(new _RemoteBridge(socket, runId)));
31815
31929
  socket.once("error", reject);
31816
31930
  });
31817
31931
  }
@@ -31858,14 +31972,14 @@ var RemoteBridge = class _RemoteBridge {
31858
31972
  }
31859
31973
  request(request, timeoutMs = REQUEST_TIMEOUT_MS) {
31860
31974
  if (this.socket.readyState !== import_websocket.default.OPEN) return Promise.resolve(null);
31861
- return new Promise((resolve) => {
31975
+ return new Promise((resolve3) => {
31862
31976
  const timer = setTimeout(() => {
31863
31977
  this.pending.delete(request.id);
31864
- resolve(null);
31978
+ resolve3(null);
31865
31979
  }, timeoutMs);
31866
31980
  this.pending.set(request.id, (message) => {
31867
31981
  clearTimeout(timer);
31868
- resolve(message);
31982
+ resolve3(message);
31869
31983
  });
31870
31984
  this.socket.send(JSON.stringify(request));
31871
31985
  });
@@ -31896,6 +32010,83 @@ function invokeTimeoutFor(action, input2) {
31896
32010
  return REQUEST_TIMEOUT_MS;
31897
32011
  }
31898
32012
 
32013
+ // self-update.ts
32014
+ import { spawnSync } from "child_process";
32015
+ import { rmSync as rmSync5 } from "fs";
32016
+ var REGISTRY = process.env.BROWSENTIC_REGISTRY ?? "https://registry.npmjs.org";
32017
+ async function publishedVersion(timeoutMs = 4e3) {
32018
+ try {
32019
+ const response = await fetch(`${REGISTRY}/browsentic/latest`, {
32020
+ headers: { accept: "application/vnd.npm.install-v1+json" },
32021
+ signal: AbortSignal.timeout(timeoutMs)
32022
+ });
32023
+ if (!response.ok) return null;
32024
+ const manifest = await response.json();
32025
+ return typeof manifest.version === "string" ? manifest.version : null;
32026
+ } catch {
32027
+ return null;
32028
+ }
32029
+ }
32030
+ function isNewer(candidate, current) {
32031
+ const release = (version2) => version2.split("-")[0].split(".").map((part) => Number.parseInt(part, 10) || 0);
32032
+ const [a, b] = [release(candidate), release(current)];
32033
+ for (let i = 0; i < 3; i++) if ((a[i] ?? 0) !== (b[i] ?? 0)) return (a[i] ?? 0) > (b[i] ?? 0);
32034
+ const [pre, currentPre] = [candidate.split("-")[1], current.split("-")[1]];
32035
+ if (pre === currentPre) return false;
32036
+ if (!pre) return true;
32037
+ if (!currentPre) return false;
32038
+ return pre > currentPre;
32039
+ }
32040
+ async function upgradeCli(current, forward) {
32041
+ const pinned = pinnedVersion();
32042
+ if (pinned) return null;
32043
+ const latest = await publishedVersion();
32044
+ if (!latest || !isNewer(latest, current)) return null;
32045
+ const kind = installKind();
32046
+ if (kind === "repo") {
32047
+ console.error(`
32048
+ ! browsentic ${latest} is published; this is ${current}, from a source checkout.`);
32049
+ console.error(` Update it with "git pull && yarn setup".
32050
+ `);
32051
+ return null;
32052
+ }
32053
+ console.error(`
32054
+ \u21BB Updating the command itself, ${current} \u2192 ${latest}
32055
+ `);
32056
+ const rerun = [...forward, "--no-self-update"];
32057
+ if (kind === "npx") {
32058
+ for (const entry of npxEntries()) rmSync5(entry.dir, { recursive: true, force: true });
32059
+ const code = run("npx", ["-y", `browsentic@${latest}`, ...rerun]);
32060
+ if (code !== 0) {
32061
+ console.error(`
32062
+ That did not run, but nothing is broken: the cache is cleared, so a plain`);
32063
+ console.error(` \`npx browsentic ${forward[0] ?? "setup"}\` fetches ${latest} fresh next time.
32064
+ `);
32065
+ }
32066
+ return code;
32067
+ }
32068
+ const installed = run("npm", ["install", "-g", `browsentic@${latest}`]);
32069
+ if (installed !== 0) {
32070
+ console.error(`
32071
+ Could not replace the global install. Run this yourself, then try again:
32072
+ `);
32073
+ console.error(` npm install -g browsentic@${latest}
32074
+ `);
32075
+ return installed;
32076
+ }
32077
+ return run("browsentic", rerun);
32078
+ }
32079
+ function run(command2, args) {
32080
+ const result = spawnSync(command2, args, { stdio: "inherit", shell: process.platform === "win32" });
32081
+ if (result.error) {
32082
+ console.error(`
32083
+ Could not run \`${command2}\`: ${result.error.message}
32084
+ `);
32085
+ return 1;
32086
+ }
32087
+ return result.status ?? 1;
32088
+ }
32089
+
31899
32090
  // node_modules/@modelcontextprotocol/sdk/dist/esm/server/zod-compat.js
31900
32091
  function isZ4Schema(s) {
31901
32092
  const schema = s;
@@ -32488,7 +32679,7 @@ var Protocol = class {
32488
32679
  return;
32489
32680
  }
32490
32681
  const pollInterval = task2.pollInterval ?? this._options?.defaultTaskPollInterval ?? 1e3;
32491
- await new Promise((resolve) => setTimeout(resolve, pollInterval));
32682
+ await new Promise((resolve3) => setTimeout(resolve3, pollInterval));
32492
32683
  options?.signal?.throwIfAborted();
32493
32684
  }
32494
32685
  } catch (error51) {
@@ -32505,7 +32696,7 @@ var Protocol = class {
32505
32696
  */
32506
32697
  request(request, resultSchema, options) {
32507
32698
  const { relatedRequestId, resumptionToken, onresumptiontoken, task, relatedTask } = options ?? {};
32508
- return new Promise((resolve, reject) => {
32699
+ return new Promise((resolve3, reject) => {
32509
32700
  const earlyReject = (error51) => {
32510
32701
  reject(error51);
32511
32702
  };
@@ -32583,7 +32774,7 @@ var Protocol = class {
32583
32774
  if (!parseResult.success) {
32584
32775
  reject(parseResult.error);
32585
32776
  } else {
32586
- resolve(parseResult.data);
32777
+ resolve3(parseResult.data);
32587
32778
  }
32588
32779
  } catch (error51) {
32589
32780
  reject(error51);
@@ -32844,12 +33035,12 @@ var Protocol = class {
32844
33035
  }
32845
33036
  } catch {
32846
33037
  }
32847
- return new Promise((resolve, reject) => {
33038
+ return new Promise((resolve3, reject) => {
32848
33039
  if (signal.aborted) {
32849
33040
  reject(new McpError(ErrorCode.InvalidRequest, "Request cancelled"));
32850
33041
  return;
32851
33042
  }
32852
- const timeoutId = setTimeout(resolve, interval);
33043
+ const timeoutId = setTimeout(resolve3, interval);
32853
33044
  signal.addEventListener("abort", () => {
32854
33045
  clearTimeout(timeoutId);
32855
33046
  reject(new McpError(ErrorCode.InvalidRequest, "Request cancelled"));
@@ -33902,10 +34093,98 @@ function text2(uri, mimeType, body) {
33902
34093
  return { contents: [{ uri, mimeType, text: body }] };
33903
34094
  }
33904
34095
 
34096
+ // uninstall.ts
34097
+ import { existsSync as existsSync6, readdirSync as readdirSync6, rmSync as rmSync6 } from "fs";
34098
+ import { isAbsolute as isAbsolute4, join as join17, relative as relative2 } from "path";
34099
+
34100
+ // screenshots.ts
34101
+ import { randomBytes as randomBytes3 } from "crypto";
34102
+ import { chmodSync as chmodSync5, mkdirSync as mkdirSync8, writeFileSync as writeFileSync7 } from "fs";
34103
+ import { homedir as homedir8 } from "os";
34104
+ import { basename as basename2, isAbsolute as isAbsolute3, join as join16, resolve as resolve2, sep as sep3 } from "path";
34105
+ function screenshotDir() {
34106
+ const configured = readAgentConfig().screenshotDir;
34107
+ if (typeof configured === "string" && configured.trim()) return expandHome3(configured.trim());
34108
+ return join16(homedir8(), "browsentic", "screenshot");
34109
+ }
34110
+ function expandHome3(p) {
34111
+ if (p === "~") return homedir8();
34112
+ if (p.startsWith("~/")) return join16(homedir8(), p.slice(2));
34113
+ return isAbsolute3(p) ? p : join16(homedir8(), p);
34114
+ }
34115
+
34116
+ // uninstall.ts
34117
+ function contains(root, path) {
34118
+ const inside = relative2(root, path);
34119
+ return !inside.startsWith("..") && !isAbsolute4(inside);
34120
+ }
34121
+ function planUninstall(options = {}) {
34122
+ const config2 = readAgentConfig();
34123
+ const extension2 = extensionDir(config2.extensionDir);
34124
+ const roots = [stateDir, userDir];
34125
+ const removals = [];
34126
+ if (!roots.some((root) => contains(root, extension2)) && existsSync6(extension2)) {
34127
+ removals.push({ label: "extension", path: extension2, holds: "the unpacked build Chrome loads" });
34128
+ }
34129
+ removals.push({
34130
+ label: "state",
34131
+ path: stateDir,
34132
+ holds: "pairing keys, config, approvals, logs",
34133
+ keep: options.keepSkills ? ["skills"] : void 0
34134
+ });
34135
+ removals.push({
34136
+ label: "files",
34137
+ path: userDir,
34138
+ holds: "the extension, skills, site maps, screenshots, captured downloads",
34139
+ keep: options.keepSkills ? ["skills"] : void 0
34140
+ });
34141
+ const elsewhere = [];
34142
+ const outside = (label2, path, holds) => {
34143
+ if (!roots.some((root) => contains(root, path)) && existsSync6(path)) elsewhere.push({ label: label2, path, holds });
34144
+ };
34145
+ outside("skills", uploadedSkillsDir(), "site maps and uploaded skills");
34146
+ outside("screenshots", screenshotDir(), "captures taken with save: true");
34147
+ outside("downloads", downloadDir(), "files captured from pages");
34148
+ return {
34149
+ removals: removals.filter((removal) => existsSync6(removal.path)),
34150
+ elsewhere,
34151
+ npx: npxEntries()
34152
+ };
34153
+ }
34154
+ function removeAll(removals) {
34155
+ return removals.map((removal) => {
34156
+ try {
34157
+ const kept = removal.keep?.length ? keepingSome(removal.path, removal.keep) : [];
34158
+ if (!kept.length) rmSync6(removal.path, { recursive: true, force: true });
34159
+ return { removal, removed: true, kept };
34160
+ } catch (error51) {
34161
+ return { removal, removed: false, kept: [], error: error51.message };
34162
+ }
34163
+ });
34164
+ }
34165
+ function keepingSome(dir, keep) {
34166
+ const kept = [];
34167
+ for (const entry of readdirSync6(dir)) {
34168
+ if (keep.includes(entry)) kept.push(entry);
34169
+ else rmSync6(join17(dir, entry), { recursive: true, force: true });
34170
+ }
34171
+ return kept;
34172
+ }
34173
+ function purgeNpxCache(entries) {
34174
+ return entries.map((entry) => {
34175
+ try {
34176
+ rmSync6(entry.dir, { recursive: true, force: true });
34177
+ return { entry, removed: true };
34178
+ } catch (error51) {
34179
+ return { entry, removed: false, error: error51.message };
34180
+ }
34181
+ });
34182
+ }
34183
+
33905
34184
  // package.json
33906
34185
  var package_default = {
33907
34186
  name: "browsentic",
33908
- version: "0.4.12",
34187
+ version: "0.4.14",
33909
34188
  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.",
33910
34189
  type: "module",
33911
34190
  license: "MIT",
@@ -33962,7 +34241,8 @@ var package_default = {
33962
34241
  var USAGE = `browsentic ${package_default.version} \u2014 hand your real browser to the agent you already run
33963
34242
 
33964
34243
  browsentic setup install the extension, start the daemon, print a pairing code
33965
- browsentic update refresh the installed extension and restart the daemon
34244
+ browsentic update pull the newest build \u2014 the command itself, then the extension
34245
+ browsentic uninstall stop the daemon and remove everything Browsentic wrote
33966
34246
  browsentic pair issue a one-time code to type into the extension
33967
34247
  browsentic status daemon, extension and agent state
33968
34248
  browsentic sessions list paired browsers
@@ -33990,7 +34270,7 @@ For MCP clients
33990
34270
  browsentic mcp serve MCP over stdio \u2014 what a client runs, not what you type
33991
34271
  claude mcp add browsentic -- browsentic mcp
33992
34272
  `;
33993
- var invokedAs = basename2(process.argv[1] ?? "").replace(/\.(?:js|cjs|mjs|exe|cmd|ps1)$/i, "");
34273
+ var invokedAs = basename3(process.argv[1] ?? "").replace(/\.(?:js|cjs|mjs|exe|cmd|ps1)$/i, "");
33994
34274
  var servesBare = invokedAs === "browsentic-mcp" || !!process.env.BROWSENTIC_AGENT_RUN;
33995
34275
  var [command] = process.argv.slice(2);
33996
34276
  switch (command) {
@@ -34007,6 +34287,9 @@ switch (command) {
34007
34287
  case "update":
34008
34288
  await setup(["--no-pair", "--restart", ...process.argv.slice(3)]);
34009
34289
  break;
34290
+ case "uninstall":
34291
+ await uninstall(process.argv.slice(3));
34292
+ break;
34010
34293
  case "pair":
34011
34294
  await pair();
34012
34295
  break;
@@ -34026,7 +34309,7 @@ switch (command) {
34026
34309
  await showStatus();
34027
34310
  break;
34028
34311
  case "stop":
34029
- stop();
34312
+ await stop();
34030
34313
  break;
34031
34314
  case "restart":
34032
34315
  await restart();
@@ -34102,7 +34385,7 @@ function printSkills() {
34102
34385
  ].filter(Boolean);
34103
34386
  console.log(`${skill.name} (${tags.join(" \xB7 ")})`);
34104
34387
  if (skill.description) console.log(` ${skill.description}`);
34105
- if (skill.provenance === "generated") console.log(` ${join15(uploadedSkillsDir(), skill.name)}/`);
34388
+ if (skill.provenance === "generated") console.log(` ${join18(uploadedSkillsDir(), skill.name)}/`);
34106
34389
  }
34107
34390
  console.log(`
34108
34391
  Read in order: ${skillDirNames().join(" \u2192 ")} (a later one shadows an earlier one by name)`);
@@ -34148,34 +34431,27 @@ async function showStatus() {
34148
34431
  );
34149
34432
  if (!status2.pairedBrowsers) console.log('\nRun "browsentic setup" to install the extension, or "browsentic pair" if it is already loaded.');
34150
34433
  }
34151
- function stop() {
34152
- const lock = readLockfile();
34153
- if (!lock) return console.log("No daemon lockfile; nothing to stop.");
34154
- try {
34155
- process.kill(lock.pid, "SIGTERM");
34156
- console.log(`Stopped daemon (pid ${lock.pid}).`);
34157
- } catch {
34158
- console.log(`Daemon (pid ${lock.pid}) was not running; clearing the lockfile.`);
34159
- clearLockfile();
34434
+ async function stop() {
34435
+ const { stopped, stubborn } = await stopDaemons();
34436
+ for (const daemon of stopped) console.log(`Stopped daemon (pid ${daemon.pid}) on 127.0.0.1:${daemon.port}.`);
34437
+ for (const daemon of stubborn) {
34438
+ console.error(`Daemon (pid ${daemon.pid}) on 127.0.0.1:${daemon.port} would not exit \u2014 kill it by hand.`);
34160
34439
  }
34440
+ if (!stopped.length && !stubborn.length) console.log("No daemon is answering; nothing to stop.");
34441
+ if (stubborn.length) process.exitCode = 1;
34161
34442
  }
34162
34443
  async function restart() {
34163
- const lock = readLockfile();
34164
- stop();
34165
- const deadline = Date.now() + 5e3;
34166
- while (lock && isRunning(lock.pid)) {
34167
- if (Date.now() > deadline) {
34168
- console.error(`Daemon (pid ${lock.pid}) is still exiting \u2014 try again in a moment.`);
34169
- process.exit(1);
34170
- }
34171
- await new Promise((resolve) => setTimeout(resolve, 150));
34444
+ const { stubborn } = await stopDaemons();
34445
+ if (stubborn.length) {
34446
+ console.error(`Daemon (pid ${stubborn[0].pid}) is still exiting \u2014 try again in a moment.`);
34447
+ process.exit(1);
34172
34448
  }
34173
34449
  const fresh = await ensureDaemon();
34174
34450
  console.log(`Daemon running on 127.0.0.1:${fresh.port} (pid ${fresh.pid}, v${fresh.daemonVersion}).`);
34175
34451
  }
34176
34452
  function showLogs() {
34177
34453
  try {
34178
- process.stdout.write(readFileSync9(logPath, "utf8"));
34454
+ process.stdout.write(readFileSync10(logPath, "utf8"));
34179
34455
  } catch {
34180
34456
  console.log(`No log at ${logPath} yet.`);
34181
34457
  }
@@ -34206,6 +34482,10 @@ async function setup(argv) {
34206
34482
  const at = argv.indexOf(`--${name}`);
34207
34483
  return at === -1 ? void 0 : argv[at + 1];
34208
34484
  };
34485
+ if (!flag("no-self-update")) {
34486
+ const code2 = await upgradeCli(package_default.version, process.argv.slice(2));
34487
+ if (code2 !== null) process.exit(code2);
34488
+ }
34209
34489
  const browser = valueOf("browser") ?? "chrome";
34210
34490
  if (browser === "firefox") {
34211
34491
  console.log(`
@@ -34244,15 +34524,11 @@ async function setup(argv) {
34244
34524
  }
34245
34525
  if (flag("restart")) await restart();
34246
34526
  const lock = await ensureDaemon();
34247
- let code;
34248
- let alreadyPaired = false;
34249
- if (!flag("no-pair")) {
34250
- const bridge = await RemoteBridge.connect(lock.port, lock.token);
34251
- const sessions = await bridge.sessions();
34252
- alreadyPaired = sessions.some((session) => session.origin.startsWith("chrome-extension://"));
34253
- if (!alreadyPaired) code = (await bridge.pair()).code;
34254
- await bridge.close();
34255
- }
34527
+ const bridge = await RemoteBridge.connect(lock.port, lock.token);
34528
+ const sessions = await bridge.sessions();
34529
+ const alreadyPaired = sessions.some((session) => session.origin.startsWith("chrome-extension://"));
34530
+ const code = alreadyPaired || flag("no-pair") ? void 0 : (await bridge.pair()).code;
34531
+ await bridge.close();
34256
34532
  if (json2) {
34257
34533
  console.log(
34258
34534
  JSON.stringify(
@@ -34292,11 +34568,91 @@ async function setup(argv) {
34292
34568
  console.log(` ${groupCode(code)}
34293
34569
  `);
34294
34570
  console.log(` Single use, expires in 10 minutes. Need another? "browsentic pair"
34571
+ `);
34572
+ } else {
34573
+ console.log(` 2. Run "browsentic pair" and paste the code into the Browsentic popup.
34295
34574
  `);
34296
34575
  }
34297
34576
  console.log(` Then open the side panel and say what you want.
34298
34577
  `);
34299
34578
  }
34579
+ async function uninstall(argv) {
34580
+ const flag = (name) => argv.includes(`--${name}`);
34581
+ const plan = planUninstall({ keepSkills: flag("keep-skills") });
34582
+ const daemons = await runningDaemons();
34583
+ const kind = installKind();
34584
+ console.log(`
34585
+ Browsentic ${package_default.version} \u2014 uninstall
34586
+ `);
34587
+ if (!daemons.length && !plan.removals.length && !plan.npx.length) {
34588
+ console.log(" Nothing to remove; this machine is already clean.\n");
34589
+ return;
34590
+ }
34591
+ console.log(" This removes:\n");
34592
+ for (const daemon of daemons) console.log(` daemon 127.0.0.1:${daemon.port}, pid ${daemon.pid}`);
34593
+ for (const removal of plan.removals) {
34594
+ console.log(` ${removal.label.padEnd(11)} ${removal.path}`);
34595
+ console.log(` ${removal.holds}${removal.keep ? " \u2014 keeping skills/" : ""}`);
34596
+ }
34597
+ for (const entry of plan.npx) {
34598
+ const running = entry.running ? ", the copy running right now" : "";
34599
+ console.log(` npx cache ${entry.dir}`);
34600
+ console.log(` browsentic ${entry.version ?? "unknown"}${running}`);
34601
+ }
34602
+ if (plan.elsewhere.length) {
34603
+ console.log("\n Left alone \u2014 configuration put these outside the two roots:\n");
34604
+ for (const dir of plan.elsewhere) console.log(` ${dir.label.padEnd(11)} ${dir.path} (${dir.holds})`);
34605
+ }
34606
+ console.log("\n You will have to remove these yourself:\n");
34607
+ console.log(" the Browsentic card at chrome://extensions. Do that first \u2014 remove the");
34608
+ console.log(" directory while the card is loaded and the browser is left holding a");
34609
+ console.log(" broken one. It is also what clears recordings and held secrets, which");
34610
+ console.log(" live in extension storage rather than on disk.");
34611
+ if (kind === "global") console.log("\n the command itself: npm rm -g browsentic");
34612
+ if (kind === "repo") console.log("\n the global link: yarn daemon:unlink");
34613
+ console.log("\n the entry in your MCP client, e.g. claude mcp remove browsentic");
34614
+ if (flag("dry-run")) {
34615
+ console.log("\n --dry-run: nothing was removed.\n");
34616
+ return;
34617
+ }
34618
+ if (!flag("yes") && !argv.includes("-y")) {
34619
+ if (!process.stdin.isTTY) {
34620
+ console.error("\n Nothing was removed \u2014 this is not a terminal, so there is nobody to ask.");
34621
+ console.error(" Re-run it with --yes.\n");
34622
+ process.exit(1);
34623
+ }
34624
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
34625
+ const answer = await rl.question("\n Remove all of it? [y/N] ");
34626
+ rl.close();
34627
+ if (!/^y(es)?$/i.test(answer.trim())) return console.log("\n Nothing was removed.\n");
34628
+ }
34629
+ console.log();
34630
+ const lock = await probeExisting();
34631
+ if (lock) {
34632
+ try {
34633
+ const bridge = await RemoteBridge.connect(lock.port, lock.token);
34634
+ const revoked = await bridge.revoke();
34635
+ await bridge.close();
34636
+ if (revoked) console.log(` \u2713 Unpaired ${revoked} browser${revoked === 1 ? "" : "s"}`);
34637
+ } catch {
34638
+ console.log(" \xB7 Unpair skipped, the daemon did not answer");
34639
+ }
34640
+ }
34641
+ const { stopped, stubborn } = await stopDaemons();
34642
+ if (stopped.length) console.log(` \u2713 Daemon stopped (pid ${stopped.map((daemon) => daemon.pid).join(", ")})`);
34643
+ for (const daemon of stubborn) console.log(` \u2717 Daemon pid ${daemon.pid} would not exit \u2014 kill it by hand`);
34644
+ for (const outcome of removeAll(plan.removals)) {
34645
+ if (!outcome.removed) console.log(` \u2717 ${outcome.removal.path} \u2014 ${outcome.error}`);
34646
+ else if (outcome.kept.length) console.log(` \u2713 Emptied ${outcome.removal.path} \u2014 kept ${outcome.kept.join(", ")}/`);
34647
+ else console.log(` \u2713 Removed ${outcome.removal.path}`);
34648
+ }
34649
+ for (const purge of purgeNpxCache(plan.npx)) {
34650
+ if (purge.removed) console.log(` \u2713 Cleared ${purge.entry.dir}`);
34651
+ else console.log(` \u2717 ${purge.entry.dir} \u2014 ${purge.error}`);
34652
+ }
34653
+ console.log("\n Done. Remove the card at chrome://extensions if you have not.\n");
34654
+ if (stubborn.length) process.exitCode = 1;
34655
+ }
34300
34656
  async function showSessions() {
34301
34657
  const bridge = await connect();
34302
34658
  const sessions = await bridge.sessions();