unbrowse 1.2.0 → 2.0.1

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/README.md CHANGED
@@ -15,7 +15,7 @@ One agent learns a site once. Every later agent gets the fast path.
15
15
  npx unbrowse setup
16
16
  ```
17
17
 
18
- `npx unbrowse setup` downloads the CLI on demand, installs browser assets, lets you register with an email-shaped display identity, registers the Open Code `/unbrowse` command when Open Code is detected, and starts the local server.
18
+ `npx unbrowse setup` downloads the CLI on demand, verifies the bundled Kuri runtime, lets you register with an email-shaped display identity, registers the Open Code `/unbrowse` command when Open Code is detected, and starts the local server.
19
19
 
20
20
  For daily use:
21
21
 
@@ -30,6 +30,25 @@ If your agent host uses skills:
30
30
  npx skills add unbrowse-ai/unbrowse
31
31
  ```
32
32
 
33
+ ## Upgrading
34
+
35
+ Unbrowse no longer self-updates at runtime. If you already have Unbrowse installed, upgrade to the latest version after each release or the new flow may not work on your machine.
36
+
37
+ If you installed the CLI globally:
38
+
39
+ ```bash
40
+ npm install -g unbrowse@latest
41
+ unbrowse setup
42
+ ```
43
+
44
+ If your agent host uses skills, rerun its skill install/update command too:
45
+
46
+ ```bash
47
+ npx skills add unbrowse-ai/unbrowse
48
+ ```
49
+
50
+ Need help or want release updates? Join the Discord: [discord.gg/VWugEeFNsG](https://discord.gg/VWugEeFNsG)
51
+
33
52
  Every CLI command auto-starts the local server on `http://localhost:6969` by default. Override with `UNBROWSE_URL`, `PORT`, or `HOST`. On first startup it auto-registers as an agent with the marketplace and caches credentials in `~/.unbrowse/config.json`. `unbrowse setup` now prompts for an email-shaped identity first; headless setups can provide `UNBROWSE_AGENT_EMAIL`.
34
53
 
35
54
  Works with Claude Code, Open Code, Cursor, Codex, Windsurf, and any agent host that can call a local CLI or skill.
@@ -37,7 +56,7 @@ Works with Claude Code, Open Code, Cursor, Codex, Windsurf, and any agent host t
37
56
  ## What setup does
38
57
 
39
58
  - Checks local prerequisites for the npm/npx flow.
40
- - Installs browser assets needed for live capture.
59
+ - Verifies the bundled Kuri binary, or builds it from the vendored Kuri source when working from repo source with Zig installed.
41
60
  - Registers the Open Code `/unbrowse` command when Open Code is present.
42
61
  - Starts the local Unbrowse server unless `--no-start` is passed.
43
62
 
package/dist/cli.js CHANGED
@@ -1,23 +1,5 @@
1
1
  #!/usr/bin/env node
2
2
  // @bun
3
- import { createRequire } from "node:module";
4
- var __create = Object.create;
5
- var __getProtoOf = Object.getPrototypeOf;
6
- var __defProp = Object.defineProperty;
7
- var __getOwnPropNames = Object.getOwnPropertyNames;
8
- var __hasOwnProp = Object.prototype.hasOwnProperty;
9
- var __toESM = (mod, isNodeMode, target) => {
10
- target = mod != null ? __create(__getProtoOf(mod)) : {};
11
- const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
12
- for (let key of __getOwnPropNames(mod))
13
- if (!__hasOwnProp.call(to, key))
14
- __defProp(to, key, {
15
- get: () => mod[key],
16
- enumerable: true
17
- });
18
- return to;
19
- };
20
- var __require = /* @__PURE__ */ createRequire(import.meta.url);
21
3
 
22
4
  // ../../src/cli.ts
23
5
  import { config as loadEnv } from "dotenv";
@@ -322,7 +304,7 @@ import { spawn } from "node:child_process";
322
304
  import { existsSync as existsSync2, mkdirSync as mkdirSync2, realpathSync } from "node:fs";
323
305
  import os from "node:os";
324
306
  import path from "node:path";
325
- import { createRequire as createRequire2 } from "node:module";
307
+ import { createRequire } from "node:module";
326
308
  import { fileURLToPath } from "node:url";
327
309
  function getModuleDir(metaUrl) {
328
310
  return path.dirname(fileURLToPath(metaUrl));
@@ -344,7 +326,7 @@ function runtimeArgsForEntrypoint(metaUrl, entrypoint) {
344
326
  if (process.versions.bun)
345
327
  return [entrypoint];
346
328
  try {
347
- const req = createRequire2(metaUrl);
329
+ const req = createRequire(metaUrl);
348
330
  const tsxPkg = req.resolve("tsx/package.json");
349
331
  const tsxLoader = path.join(path.dirname(tsxPkg), "dist", "loader.mjs");
350
332
  if (existsSync2(tsxLoader))
@@ -483,16 +465,88 @@ function isMainModule(metaUrl) {
483
465
  }
484
466
 
485
467
  // ../../src/runtime/setup.ts
486
- import { execFileSync } from "node:child_process";
487
- import { createRequire as createRequire3 } from "node:module";
488
- import { existsSync as existsSync4, mkdirSync as mkdirSync4, writeFileSync as writeFileSync3 } from "node:fs";
489
- import os2 from "node:os";
468
+ import { execFileSync as execFileSync2 } from "node:child_process";
469
+ import { existsSync as existsSync5, mkdirSync as mkdirSync4, writeFileSync as writeFileSync3 } from "node:fs";
470
+ import os3 from "node:os";
471
+ import path6 from "node:path";
472
+
473
+ // ../../src/kuri/client.ts
474
+ import { execFileSync, spawn as spawn2 } from "node:child_process";
475
+ import { existsSync as existsSync4 } from "node:fs";
476
+ import path5 from "node:path";
477
+
478
+ // ../../src/logger.ts
490
479
  import path4 from "node:path";
491
- var req = createRequire3(import.meta.url);
480
+ import os2 from "node:os";
481
+ var LOG_DIR = path4.join(os2.homedir(), ".unbrowse", "logs");
482
+
483
+ // ../../src/kuri/client.ts
484
+ function kuriBinaryName() {
485
+ return process.platform === "win32" ? "kuri.exe" : "kuri";
486
+ }
487
+ function currentBundledKuriTarget() {
488
+ if (process.platform === "darwin" && process.arch === "arm64")
489
+ return "darwin-arm64";
490
+ if (process.platform === "darwin" && process.arch === "x64")
491
+ return "darwin-x64";
492
+ if (process.platform === "linux" && process.arch === "arm64")
493
+ return "linux-arm64";
494
+ if (process.platform === "linux" && process.arch === "x64")
495
+ return "linux-x64";
496
+ return null;
497
+ }
498
+ function resolveBinaryOnPath(name) {
499
+ const checker = process.platform === "win32" ? "where" : "which";
500
+ try {
501
+ const output = execFileSync(checker, [name], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
502
+ const match = output.split(/\r?\n/).map((line) => line.trim()).find(Boolean);
503
+ return match || null;
504
+ } catch {
505
+ return null;
506
+ }
507
+ }
508
+ function addCandidate(candidates, candidate) {
509
+ if (!candidate)
510
+ return;
511
+ if (!candidates.includes(candidate))
512
+ candidates.push(candidate);
513
+ }
514
+ function getKuriSourceCandidates() {
515
+ const packageRoot = getPackageRoot(import.meta.url);
516
+ const candidates = [];
517
+ addCandidate(candidates, path5.join(packageRoot, "vendor", "kuri-src"));
518
+ addCandidate(candidates, path5.join(packageRoot, "submodules", "kuri"));
519
+ if (process.env.KURI_PATH)
520
+ addCandidate(candidates, process.env.KURI_PATH);
521
+ if (process.env.HOME)
522
+ addCandidate(candidates, path5.join(process.env.HOME, "kuri"));
523
+ return candidates;
524
+ }
525
+ function getKuriBinaryCandidates() {
526
+ const packageRoot = getPackageRoot(import.meta.url);
527
+ const binaryName = kuriBinaryName();
528
+ const target = currentBundledKuriTarget();
529
+ const candidates = [];
530
+ if (target)
531
+ addCandidate(candidates, path5.join(packageRoot, "vendor", "kuri", target, binaryName));
532
+ for (const sourceDir of getKuriSourceCandidates()) {
533
+ addCandidate(candidates, path5.join(sourceDir, "zig-out", "bin", binaryName));
534
+ }
535
+ addCandidate(candidates, resolveBinaryOnPath("kuri"));
536
+ return candidates;
537
+ }
538
+ function findKuriBinary() {
539
+ if (process.env.KURI_BIN)
540
+ return process.env.KURI_BIN;
541
+ const candidates = getKuriBinaryCandidates();
542
+ return candidates.find((candidate) => existsSync4(candidate)) ?? candidates[0] ?? kuriBinaryName();
543
+ }
544
+
545
+ // ../../src/runtime/setup.ts
492
546
  function hasBinary(name) {
493
547
  const checker = process.platform === "win32" ? "where" : "which";
494
548
  try {
495
- execFileSync(checker, [name], { stdio: "ignore" });
549
+ execFileSync2(checker, [name], { stdio: "ignore" });
496
550
  return true;
497
551
  } catch {
498
552
  return false;
@@ -508,18 +562,18 @@ function detectPackageManagers() {
508
562
  }
509
563
  function resolveConfigHome() {
510
564
  if (process.platform === "win32") {
511
- return process.env.APPDATA || path4.join(os2.homedir(), "AppData", "Roaming");
565
+ return process.env.APPDATA || path6.join(os3.homedir(), "AppData", "Roaming");
512
566
  }
513
- return process.env.XDG_CONFIG_HOME || path4.join(os2.homedir(), ".config");
567
+ return process.env.XDG_CONFIG_HOME || path6.join(os3.homedir(), ".config");
514
568
  }
515
569
  function getOpenCodeGlobalCommandsDir() {
516
- return path4.join(resolveConfigHome(), "opencode", "commands");
570
+ return path6.join(resolveConfigHome(), "opencode", "commands");
517
571
  }
518
572
  function getOpenCodeProjectCommandsDir(cwd) {
519
- return path4.join(cwd, ".opencode", "commands");
573
+ return path6.join(cwd, ".opencode", "commands");
520
574
  }
521
575
  function detectOpenCode(cwd) {
522
- return hasBinary("opencode") || existsSync4(path4.join(resolveConfigHome(), "opencode")) || existsSync4(path4.join(cwd, ".opencode"));
576
+ return hasBinary("opencode") || existsSync5(path6.join(resolveConfigHome(), "opencode")) || existsSync5(path6.join(cwd, ".opencode"));
523
577
  }
524
578
  function renderOpenCodeCommand() {
525
579
  return `---
@@ -547,12 +601,12 @@ function writeOpenCodeCommand(scope, cwd) {
547
601
  if (scope === "auto" && !detected) {
548
602
  return { detected: false, action: "not-detected", scope: "off" };
549
603
  }
550
- const resolvedScope = scope === "project" ? "project" : scope === "global" ? "global" : existsSync4(path4.join(cwd, ".opencode")) ? "project" : "global";
604
+ const resolvedScope = scope === "project" ? "project" : scope === "global" ? "global" : existsSync5(path6.join(cwd, ".opencode")) ? "project" : "global";
551
605
  const commandsDir = resolvedScope === "project" ? getOpenCodeProjectCommandsDir(cwd) : getOpenCodeGlobalCommandsDir();
552
- const commandFile = path4.join(ensureDir(commandsDir), "unbrowse.md");
606
+ const commandFile = path6.join(ensureDir(commandsDir), "unbrowse.md");
553
607
  const content = renderOpenCodeCommand();
554
- const action = existsSync4(commandFile) ? "updated" : "installed";
555
- mkdirSync4(path4.dirname(commandFile), { recursive: true });
608
+ const action = existsSync5(commandFile) ? "updated" : "installed";
609
+ mkdirSync4(path6.dirname(commandFile), { recursive: true });
556
610
  writeFileSync3(commandFile, content);
557
611
  return {
558
612
  detected: detected || scope !== "auto",
@@ -562,17 +616,44 @@ function writeOpenCodeCommand(scope, cwd) {
562
616
  };
563
617
  }
564
618
  async function ensureBrowserEngineInstalled() {
619
+ const binary = findKuriBinary();
620
+ if (existsSync5(binary)) {
621
+ return { installed: true, action: "already-installed" };
622
+ }
623
+ const sourceDir = getKuriSourceCandidates().find((candidate) => existsSync5(path6.join(candidate, "build.zig")));
624
+ if (!sourceDir) {
625
+ return {
626
+ installed: false,
627
+ action: "failed",
628
+ message: `Kuri binary not found. Checked ${binary}`
629
+ };
630
+ }
631
+ if (!hasBinary("zig")) {
632
+ return {
633
+ installed: false,
634
+ action: "failed",
635
+ message: `Kuri source found at ${sourceDir}, but Zig is not installed`
636
+ };
637
+ }
565
638
  try {
566
- const { chromium } = await import("playwright-core");
567
- if (existsSync4(chromium.executablePath())) {
568
- return { installed: true, action: "already-installed" };
569
- }
570
- const agentBrowserBin = req.resolve("agent-browser/bin/agent-browser.js");
571
- execFileSync(process.execPath, [agentBrowserBin, "install"], {
639
+ execFileSync2("zig", ["build", "-Doptimize=ReleaseFast"], {
640
+ cwd: sourceDir,
572
641
  stdio: "inherit",
573
642
  timeout: 300000
574
643
  });
575
- return { installed: true, action: "installed" };
644
+ const builtBinary = findKuriBinary();
645
+ if (existsSync5(builtBinary)) {
646
+ return {
647
+ installed: true,
648
+ action: "installed",
649
+ message: `Built Kuri from ${sourceDir}`
650
+ };
651
+ }
652
+ return {
653
+ installed: false,
654
+ action: "failed",
655
+ message: `Kuri build completed but ${builtBinary} was not created`
656
+ };
576
657
  } catch (error) {
577
658
  const message = error instanceof Error ? error.message : String(error);
578
659
  return { installed: false, action: "failed", message };
@@ -584,7 +665,7 @@ async function runSetup(options) {
584
665
  return {
585
666
  os: {
586
667
  platform: process.platform,
587
- release: os2.release(),
668
+ release: os3.release(),
588
669
  arch: process.arch
589
670
  },
590
671
  package_managers: detectPackageManagers(),
@@ -621,8 +702,8 @@ function parseArgs(argv) {
621
702
  }
622
703
  return { command, args: positional, flags };
623
704
  }
624
- async function api2(method, path5, body) {
625
- const res = await fetch(`${BASE_URL}${path5}`, {
705
+ async function api2(method, path7, body) {
706
+ const res = await fetch(`${BASE_URL}${path7}`, {
626
707
  method,
627
708
  headers: {
628
709
  ...body ? { "Content-Type": "application/json" } : {},
@@ -708,10 +789,10 @@ function detectEntityIndex(data) {
708
789
  }
709
790
  return best ? buildEntityIndex(best) : null;
710
791
  }
711
- function resolvePath(obj, path5, entityIndex) {
712
- if (!path5 || obj == null)
792
+ function resolvePath(obj, path7, entityIndex) {
793
+ if (!path7 || obj == null)
713
794
  return obj;
714
- const segments = path5.split(".");
795
+ const segments = path7.split(".");
715
796
  let cur = obj;
716
797
  for (let i = 0;i < segments.length; i++) {
717
798
  if (cur == null)
@@ -750,8 +831,8 @@ function extractFields(data, fields, entityIndex) {
750
831
  for (const f of fields) {
751
832
  const colonIdx = f.indexOf(":");
752
833
  const alias = colonIdx >= 0 ? f.slice(0, colonIdx) : f.split(".").pop();
753
- const path5 = colonIdx >= 0 ? f.slice(colonIdx + 1) : f;
754
- const resolved = resolvePath(item, path5, entityIndex ?? undefined) ?? [];
834
+ const path7 = colonIdx >= 0 ? f.slice(colonIdx + 1) : f;
835
+ const resolved = resolvePath(item, path7, entityIndex ?? undefined) ?? [];
755
836
  out[alias] = Array.isArray(resolved) ? resolved.length === 0 ? null : resolved.length === 1 ? resolved[0] : resolved : resolved;
756
837
  }
757
838
  return out;
@@ -1039,11 +1120,11 @@ async function cmdSearch(flags) {
1039
1120
  if (!intent)
1040
1121
  die("--intent is required");
1041
1122
  const domain = flags.domain;
1042
- const path5 = domain ? "/v1/search/domain" : "/v1/search";
1123
+ const path7 = domain ? "/v1/search/domain" : "/v1/search";
1043
1124
  const body = { intent, k: Number(flags.k) || 5 };
1044
1125
  if (domain)
1045
1126
  body.domain = domain;
1046
- output(await api2("POST", path5, body), !!flags.pretty);
1127
+ output(await api2("POST", path7, body), !!flags.pretty);
1047
1128
  }
1048
1129
  async function cmdSessions(flags) {
1049
1130
  const domain = flags.domain;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "unbrowse",
3
- "version": "1.2.0",
3
+ "version": "2.0.1",
4
4
  "description": "Reverse-engineer any website into reusable API skills. npm CLI + local engine.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -9,6 +9,7 @@
9
9
  "files": [
10
10
  "dist",
11
11
  "runtime-src",
12
+ "vendor/kuri",
12
13
  "README.md",
13
14
  "LICENSE"
14
15
  ],
@@ -25,8 +26,6 @@
25
26
  "cheerio": "^1.2.0",
26
27
  "dotenv": "^17.3.1",
27
28
  "nanoid": "^5.1.6",
28
- "agent-browser": "^0.13.0",
29
- "playwright-core": "^1.58.2",
30
29
  "tsx": "^4.20.6",
31
30
  "ws": "^8.19.0"
32
31
  },
@@ -1,5 +1,4 @@
1
- import { BrowserManager } from "agent-browser/dist/browser.js";
2
- import { executeCommand } from "agent-browser/dist/actions.js";
1
+ import * as kuri from "../kuri/client.js";
3
2
  import { storeCredential, getCredential, deleteCredential } from "../vault/index.js";
4
3
  import { nanoid } from "nanoid";
5
4
  import { isDomainMatch, getRegistrableDomain } from "../domain.js";
@@ -10,16 +9,16 @@ import fs from "node:fs";
10
9
 
11
10
  const LOGIN_TIMEOUT_MS = 300_000;
12
11
  const POLL_INTERVAL_MS = 2_000;
13
- const MIN_WAIT_MS = 15_000; // Always wait at least 15s so user has time to log in
12
+ const MIN_WAIT_MS = 15_000;
14
13
 
15
14
  /**
16
15
  * Returns the persistent profile directory for a given domain.
17
16
  * Stored under ~/.unbrowse/profiles/<registrableDomain>.
18
- * Exporting so capture/execute can also launch with the profile if needed.
19
17
  */
20
18
  export function getProfilePath(domain: string): string {
21
19
  return path.join(os.homedir(), ".unbrowse", "profiles", getRegistrableDomain(domain));
22
20
  }
21
+
23
22
  export interface LoginResult {
24
23
  success: boolean;
25
24
  domain: string;
@@ -29,8 +28,10 @@ export interface LoginResult {
29
28
 
30
29
  /**
31
30
  * Open a visible browser for the user to complete login.
32
- * Waits up to 120s for navigation back to the target domain, then captures cookies.
33
- * Uses an isolated persistent profile per domain.
31
+ * Uses Kuri to manage the browser tab, polls for login completion via cookies.
32
+ *
33
+ * Note: Kuri manages Chrome — for interactive login, the user's Chrome
34
+ * needs to be visible. We navigate to the login URL and poll for cookie changes.
34
35
  */
35
36
  export async function interactiveLogin(
36
37
  url: string,
@@ -39,26 +40,23 @@ export async function interactiveLogin(
39
40
  const targetDomain = domain ?? new URL(url).hostname;
40
41
  const profileDir = getProfilePath(targetDomain);
41
42
 
42
- const browser = new BrowserManager();
43
43
  log("auth", `interactiveLogin — url: ${url}, domain: ${targetDomain}`);
44
44
 
45
45
  try {
46
46
  fs.mkdirSync(profileDir, { recursive: true });
47
- await browser.launch({
48
- action: "launch",
49
- id: nanoid(),
50
- headless: false,
51
- profile: profileDir,
52
- userAgent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
53
- });
54
- await executeCommand({ action: "navigate", id: nanoid(), url }, browser);
55
-
56
- const page = browser.getPage();
47
+
48
+ // Start Kuri and get a tab
49
+ await kuri.start();
50
+ const tabId = await kuri.getDefaultTab();
51
+ await kuri.networkEnable(tabId);
52
+
53
+ // Navigate to login URL
54
+ await kuri.navigate(tabId, url);
55
+
57
56
  const startTime = Date.now();
58
57
 
59
- // Snapshot initial cookies so we can detect new ones after login
60
- const context = browser.getContext();
61
- const initialCookies = context ? await context.cookies() : [];
58
+ // Snapshot initial cookies
59
+ const initialCookies = await kuri.getCookies(tabId);
62
60
  const initialCookieCount = initialCookies.filter((c) => isDomainMatch(c.domain, targetDomain)).length;
63
61
  log("auth", `initial cookies for ${targetDomain}: ${initialCookieCount}`);
64
62
 
@@ -70,7 +68,7 @@ export async function interactiveLogin(
70
68
  const elapsed = Date.now() - startTime;
71
69
 
72
70
  try {
73
- const currentUrl = page.url();
71
+ const currentUrl = await kuri.getCurrentUrl(tabId);
74
72
  const currentDomain = new URL(currentUrl).hostname.toLowerCase();
75
73
  const targetNorm = targetDomain.toLowerCase();
76
74
 
@@ -79,15 +77,13 @@ export async function interactiveLogin(
79
77
  lastLoggedUrl = currentUrl;
80
78
  }
81
79
 
82
- // Don't even check for login completion until MIN_WAIT_MS has passed
83
80
  if (elapsed < MIN_WAIT_MS) continue;
84
81
 
85
82
  const isOnTarget = currentDomain === targetNorm || currentDomain.endsWith("." + targetNorm);
86
83
  if (isOnTarget) {
87
84
  const isStillLogin = /\/(login|signin|sign-in|sso|auth|oauth|uas\/login|checkpoint)/.test(new URL(currentUrl).pathname);
88
85
 
89
- // Check if new cookies appeared (the real signal that login happened)
90
- const currentCookies = context ? await context.cookies() : [];
86
+ const currentCookies = await kuri.getCookies(tabId);
91
87
  const currentCookieCount = currentCookies.filter((c) => isDomainMatch(c.domain, targetDomain)).length;
92
88
  const gotNewCookies = currentCookieCount > initialCookieCount;
93
89
 
@@ -97,9 +93,6 @@ export async function interactiveLogin(
97
93
  break;
98
94
  }
99
95
 
100
- // Handle "already logged in" — user was redirected away from login
101
- // to a non-login page, and cookies already exist (even if count didn't change).
102
- // This means the persistent profile already has the session.
103
96
  if (!isStillLogin && currentCookieCount > 0) {
104
97
  loggedIn = true;
105
98
  log("auth", `already logged in — ${currentUrl} (${currentCookieCount} cookies present)`);
@@ -110,14 +103,11 @@ export async function interactiveLogin(
110
103
  }
111
104
 
112
105
  if (!loggedIn) {
113
- // Even on timeout, grab whatever cookies we have — the user may have logged in
114
- // but the detection missed it
115
106
  log("auth", `login wait ended after ${Math.round((Date.now() - startTime) / 1000)}s — capturing cookies anyway`);
116
107
  }
117
108
 
118
109
  // Extract and store cookies
119
- const finalContext = browser.getContext();
120
- const cookies = finalContext ? await finalContext.cookies() : [];
110
+ const cookies = await kuri.getCookies(tabId);
121
111
  const domainCookies = cookies.filter((c) => isDomainMatch(c.domain, targetDomain));
122
112
 
123
113
  if (domainCookies.length === 0) {
@@ -135,33 +125,17 @@ export async function interactiveLogin(
135
125
 
136
126
  return { success: true, domain: targetDomain, cookies_stored: storableCookies.length };
137
127
  } finally {
138
- try {
139
- const context = browser.getContext();
140
- if (context) await Promise.race([context.close(), new Promise<void>((r) => setTimeout(r, 4000))]);
141
- } catch { /* ignore */ }
128
+ // Cleanup handled by Kuri's tab management
142
129
  }
143
130
  }
144
131
 
145
132
  /**
146
133
  * Extract cookies directly from Chrome/Firefox SQLite databases.
147
134
  * No browser launch needed, Chrome can stay open.
148
- * Stores extracted cookies in the vault for subsequent use.
149
- * Always stores under the registrable domain key for consistency.
150
135
  */
151
136
  export async function extractBrowserAuth(
152
137
  domain: string,
153
- opts?: {
154
- browser?: "auto" | "firefox" | "chrome" | "chromium";
155
- chromeProfile?: string;
156
- firefoxProfile?: string;
157
- chromium?: {
158
- profile?: string;
159
- userDataDir?: string;
160
- cookieDbPath?: string;
161
- safeStorageService?: string;
162
- browserName?: string;
163
- };
164
- }
138
+ opts?: { chromeProfile?: string; firefoxProfile?: string }
165
139
  ): Promise<LoginResult> {
166
140
  const { extractBrowserCookies } = await import("./browser-cookies.js");
167
141
 
@@ -176,7 +150,6 @@ export async function extractBrowserAuth(
176
150
  };
177
151
  }
178
152
 
179
- // Store in vault under same format as interactiveLogin
180
153
  const storableCookies = result.cookies.map((c) => ({
181
154
  name: c.name,
182
155
  value: c.value,
@@ -188,7 +161,6 @@ export async function extractBrowserAuth(
188
161
  expires: c.expires,
189
162
  }));
190
163
 
191
- // Normalize: always store under registrable domain for consistent lookups
192
164
  const vaultKey = `auth:${getRegistrableDomain(domain)}`;
193
165
  await storeCredential(
194
166
  vaultKey,
@@ -214,7 +186,7 @@ type AuthCookie = {
214
186
  function filterExpired(cookies: AuthCookie[]): AuthCookie[] {
215
187
  const now = Math.floor(Date.now() / 1000);
216
188
  return cookies.filter((c) => {
217
- if (c.expires == null || c.expires <= 0) return true; // session cookie
189
+ if (c.expires == null || c.expires <= 0) return true;
218
190
  return c.expires > now;
219
191
  });
220
192
  }
@@ -222,12 +194,10 @@ function filterExpired(cookies: AuthCookie[]): AuthCookie[] {
222
194
  /**
223
195
  * Retrieve stored auth cookies for a domain from the vault.
224
196
  * Filters out expired cookies automatically.
225
- * Checks both registrable domain key and exact domain key for backward compat.
226
197
  */
227
198
  export async function getStoredAuth(
228
199
  domain: string
229
200
  ): Promise<AuthCookie[] | null> {
230
- // Try registrable domain key first (new normalized format), then exact domain
231
201
  const regDomain = getRegistrableDomain(domain);
232
202
  const keysToTry = [`auth:${regDomain}`];
233
203
  if (domain !== regDomain) keysToTry.push(`auth:${domain}`);
@@ -263,22 +233,13 @@ export async function getStoredAuth(
263
233
  * Fallback chain:
264
234
  * 1. Vault cookies (fast path)
265
235
  * 2. Auto-extract from Chrome/Firefox SQLite (bird pattern — always fresh)
266
- *
267
- * This ensures cookies are available without requiring the user to manually
268
- * call /v1/auth/steal first.
269
236
  */
270
237
  export async function getAuthCookies(
271
- domain: string,
272
- opts?: {
273
- autoExtract?: boolean;
274
- },
238
+ domain: string
275
239
  ): Promise<AuthCookie[] | null> {
276
- // 1. Try vault (fast)
277
240
  const vaultCookies = await getStoredAuth(domain);
278
241
  if (vaultCookies && vaultCookies.length > 0) return vaultCookies;
279
- if (!opts?.autoExtract) return null;
280
242
 
281
- // 2. Auto-extract from browser (bird pattern)
282
243
  log("auth", `no vault cookies for ${domain} — auto-extracting from browser`);
283
244
  try {
284
245
  const result = await extractBrowserAuth(domain);