unbrowse 1.1.1 → 1.1.3

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, 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, 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.
19
19
 
20
20
  For daily use:
21
21
 
@@ -30,7 +30,7 @@ If your agent host uses skills:
30
30
  npx skills add unbrowse-ai/unbrowse
31
31
  ```
32
32
 
33
- 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`.
33
+ 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
34
 
35
35
  Works with Claude Code, Open Code, Cursor, Codex, Windsurf, and any agent host that can call a local CLI or skill.
36
36
 
@@ -57,6 +57,10 @@ unbrowse search --intent "get stock prices"
57
57
  - For website tasks, keep the agent on Unbrowse instead of letting it drift into generic web search or ad hoc `curl`.
58
58
  - Reddit is still a harder target than most sites because of anti-bot protections. Prefer canonical `.json` routes when available.
59
59
 
60
+ ## Help shape the next eval
61
+
62
+ If you tried Unbrowse on a site or API and could not get it to work, add it to [Discussion #53](https://github.com/unbrowse-ai/unbrowse/discussions/53). We use that thread to collect missing or broken targets so we can turn them into requirements for the next eval pass.
63
+
60
64
  ## How it works
61
65
 
62
66
  When an agent asks for something, Unbrowse first searches the marketplace for an existing skill. If one exists with enough confidence, it executes immediately. If not, Unbrowse captures the site, learns the APIs behind it, publishes a reusable skill, and executes that instead.
@@ -96,7 +100,7 @@ A background verification loop runs every 6 hours, executing safe (GET) endpoint
96
100
 
97
101
  ## Authentication for gated sites
98
102
 
99
- For most sites, auth is automatic. If you're logged into a site in Chrome or Firefox, Unbrowse reads your cookies directly from the browser's SQLite database — no extra steps needed. Cookies are resolved fresh on every call, so sessions stay current.
103
+ For most sites, auth is automatic. If you're logged into a site in Chrome or Firefox, Unbrowse reads your cookies directly from the browser's SQLite database — no extra steps needed. Cookies are resolved fresh on every call, so sessions stay current. For Chromium-family apps and Electron shells, `/v1/auth/steal` also accepts a custom cookie DB path or user-data dir plus an optional macOS Safe Storage service name.
100
104
 
101
105
  | Strategy | How it works | When to use |
102
106
  | ------------------- | -------------------------------------------------- | ---------------------------------------------------- |
@@ -124,6 +128,7 @@ See [SKILL.md](./SKILL.md) for the full API reference including all endpoints, s
124
128
  | POST | `/v1/intent/resolve` | Search marketplace, capture if needed, execute |
125
129
  | POST | `/v1/skills/:id/execute` | Execute a specific skill |
126
130
  | POST | `/v1/auth/login` | Interactive browser login |
131
+ | POST | `/v1/auth/steal` | Import cookies from browser/Electron storage |
127
132
  | POST | `/v1/search` | Semantic search across all domains |
128
133
  | POST | `/v1/search/domain` | Semantic search scoped to a domain |
129
134
  | POST | `/v1/feedback` | Submit feedback (affects reliability scores) |
@@ -154,6 +159,7 @@ See [SKILL.md](./SKILL.md) for the full API reference including all endpoints, s
154
159
  | `HOST` | `127.0.0.1` | Server bind address |
155
160
  | `UNBROWSE_URL` | `http://localhost:6969` | Base URL for API calls |
156
161
  | `UNBROWSE_API_KEY` | auto-generated | API key override |
162
+ | `UNBROWSE_AGENT_EMAIL` | — | Preferred email-style agent name for registration |
157
163
  | `UNBROWSE_TOS_ACCEPTED` | — | Accept ToS non-interactively |
158
164
  | `UNBROWSE_NON_INTERACTIVE` | — | Skip readline prompts |
159
165
 
package/dist/cli.js CHANGED
@@ -22,13 +22,234 @@ var __require = /* @__PURE__ */ createRequire(import.meta.url);
22
22
  // ../../src/cli.ts
23
23
  import { config as loadEnv } from "dotenv";
24
24
 
25
+ // ../../src/client/index.ts
26
+ import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync } from "fs";
27
+ import { join } from "path";
28
+ import { homedir, hostname } from "os";
29
+ import { randomBytes } from "crypto";
30
+ import { createInterface } from "readline";
31
+ var API_URL = process.env.UNBROWSE_BACKEND_URL || "https://beta-api.unbrowse.ai";
32
+ var PROFILE_NAME = sanitizeProfileName(process.env.UNBROWSE_PROFILE ?? "");
33
+ var CONFIG_DIR = PROFILE_NAME ? join(homedir(), ".unbrowse", "profiles", PROFILE_NAME) : join(homedir(), ".unbrowse");
34
+ var CONFIG_PATH = join(CONFIG_DIR, "config.json");
35
+ var recentLocalSkills = new Map;
36
+ var LOCAL_ONLY = process.env.UNBROWSE_LOCAL_ONLY === "1";
37
+ function sanitizeProfileName(value) {
38
+ return value.trim().replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
39
+ }
40
+ function loadConfig() {
41
+ try {
42
+ if (existsSync(CONFIG_PATH)) {
43
+ return JSON.parse(readFileSync(CONFIG_PATH, "utf-8"));
44
+ }
45
+ } catch {}
46
+ return null;
47
+ }
48
+ function saveConfig(config) {
49
+ if (!existsSync(CONFIG_DIR))
50
+ mkdirSync(CONFIG_DIR, { recursive: true });
51
+ writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2), { mode: 384 });
52
+ }
53
+ var EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/i;
54
+ function normalizeAgentEmail(value) {
55
+ return value.trim().toLowerCase();
56
+ }
57
+ function isValidAgentEmail(value) {
58
+ return EMAIL_RE.test(normalizeAgentEmail(value));
59
+ }
60
+ function buildDefaultAgentName() {
61
+ return `${hostname()}-${randomBytes(3).toString("hex")}`;
62
+ }
63
+ function resolveAgentName(preferredEmail, fallbackName) {
64
+ const normalized = normalizeAgentEmail(preferredEmail ?? "");
65
+ return isValidAgentEmail(normalized) ? normalized : fallbackName;
66
+ }
67
+ function getApiKey() {
68
+ if (LOCAL_ONLY)
69
+ return "local-only";
70
+ if (process.env.UNBROWSE_API_KEY)
71
+ return process.env.UNBROWSE_API_KEY;
72
+ const config = loadConfig();
73
+ if (config?.api_key) {
74
+ process.env.UNBROWSE_API_KEY = config.api_key;
75
+ return config.api_key;
76
+ }
77
+ return "";
78
+ }
79
+ var API_TIMEOUT_MS = parseInt(process.env.UNBROWSE_API_TIMEOUT ?? "8000", 10);
80
+ async function api(method, path, body, opts) {
81
+ const key = opts?.noAuth ? "" : getApiKey();
82
+ const controller = new AbortController;
83
+ const timer = setTimeout(() => controller.abort(), API_TIMEOUT_MS);
84
+ let res;
85
+ try {
86
+ res = await fetch(`${API_URL}${path}`, {
87
+ method,
88
+ headers: {
89
+ "Content-Type": "application/json",
90
+ "Accept-Encoding": "gzip, deflate",
91
+ ...key ? { Authorization: `Bearer ${key}` } : {}
92
+ },
93
+ body: body ? JSON.stringify(body) : undefined,
94
+ signal: controller.signal
95
+ });
96
+ } finally {
97
+ clearTimeout(timer);
98
+ }
99
+ let data;
100
+ try {
101
+ data = await res.json();
102
+ } catch {
103
+ throw new Error(`API error ${res.status} from ${path}`);
104
+ }
105
+ if (res.status === 403 && data.error === "tos_update_required") {
106
+ console.warn(`
107
+ [unbrowse] The Terms of Service have been updated.`);
108
+ console.warn("[unbrowse] Please restart the unbrowse service to accept the new terms.");
109
+ throw new Error("ToS update required. Restart unbrowse to accept new terms.");
110
+ }
111
+ if (!res.ok) {
112
+ const errData = data;
113
+ const msg = errData.details?.length ? `${errData.error}: ${errData.details.join("; ")}` : errData.error ?? `API HTTP ${res.status}`;
114
+ throw new Error(msg);
115
+ }
116
+ return data;
117
+ }
118
+ async function promptTosAcceptance(summary, tosUrl) {
119
+ if (process.env.UNBROWSE_NON_INTERACTIVE === "1") {
120
+ if (process.env.UNBROWSE_TOS_ACCEPTED === "1") {
121
+ console.log("[unbrowse] ToS accepted by user via agent.");
122
+ return true;
123
+ }
124
+ console.log("[unbrowse] ToS acceptance required. Set UNBROWSE_TOS_ACCEPTED=1 after user consents.");
125
+ console.log(`[unbrowse] ToS summary:
126
+ ${summary}`);
127
+ console.log(`[unbrowse] Full terms: ${tosUrl}`);
128
+ return false;
129
+ }
130
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
131
+ console.log(`
132
+ ` + "=".repeat(60));
133
+ console.log("UNBROWSE TERMS OF SERVICE");
134
+ console.log("=".repeat(60));
135
+ console.log(summary);
136
+ console.log("=".repeat(60));
137
+ return new Promise((resolve) => {
138
+ rl.question(`
139
+ Do you accept the Terms of Service? (y/n): `, (answer) => {
140
+ rl.close();
141
+ resolve(answer.trim().toLowerCase() === "y" || answer.trim().toLowerCase() === "yes");
142
+ });
143
+ });
144
+ }
145
+ async function promptAgentEmail(defaultName) {
146
+ const envEmail = process.env.UNBROWSE_AGENT_EMAIL;
147
+ if (envEmail) {
148
+ const resolved = resolveAgentName(envEmail, defaultName);
149
+ if (resolved !== defaultName)
150
+ return resolved;
151
+ console.warn(`[unbrowse] Ignoring invalid UNBROWSE_AGENT_EMAIL: ${envEmail}`);
152
+ }
153
+ if (process.env.UNBROWSE_NON_INTERACTIVE === "1" || !process.stdin.isTTY || !process.stdout.isTTY) {
154
+ return defaultName;
155
+ }
156
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
157
+ try {
158
+ for (;; ) {
159
+ const answer = await new Promise((resolve) => {
160
+ rl.question(`
161
+ Email for this agent (leave blank to use a local device id): `, resolve);
162
+ });
163
+ const trimmed = answer.trim();
164
+ if (!trimmed)
165
+ return defaultName;
166
+ if (isValidAgentEmail(trimmed))
167
+ return normalizeAgentEmail(trimmed);
168
+ console.log("Please enter a valid email address or press Enter to skip.");
169
+ }
170
+ } finally {
171
+ rl.close();
172
+ }
173
+ }
174
+ async function checkTosStatus() {
175
+ const config = loadConfig();
176
+ let tosInfo;
177
+ try {
178
+ tosInfo = await api("GET", "/v1/tos/current");
179
+ } catch {
180
+ return;
181
+ }
182
+ if (config?.tos_accepted_version === tosInfo.version) {
183
+ return;
184
+ }
185
+ console.log(`
186
+ The Unbrowse Terms of Service have been updated.`);
187
+ const accepted = await promptTosAcceptance(tosInfo.summary, tosInfo.url);
188
+ if (!accepted) {
189
+ console.log("You must accept the updated Terms of Service to continue using Unbrowse.");
190
+ process.exit(1);
191
+ }
192
+ try {
193
+ await api("POST", "/v1/agents/accept-tos", { tos_version: tosInfo.version });
194
+ if (config) {
195
+ config.tos_accepted_version = tosInfo.version;
196
+ config.tos_accepted_at = new Date().toISOString();
197
+ saveConfig(config);
198
+ }
199
+ console.log("Terms of Service accepted.");
200
+ } catch (err) {
201
+ console.warn(`Failed to record ToS acceptance: ${err.message}`);
202
+ }
203
+ }
204
+ async function ensureRegistered(options) {
205
+ if (LOCAL_ONLY)
206
+ return;
207
+ if (getApiKey()) {
208
+ await checkTosStatus();
209
+ return;
210
+ }
211
+ let tosInfo;
212
+ try {
213
+ tosInfo = await api("GET", "/v1/tos/current");
214
+ } catch {
215
+ console.warn("[unbrowse] Cannot reach unbrowse API. Registration requires internet access.");
216
+ console.warn("[unbrowse] Set UNBROWSE_API_KEY manually or try again when online.");
217
+ return;
218
+ }
219
+ const accepted = await promptTosAcceptance(tosInfo.summary, tosInfo.url);
220
+ if (!accepted) {
221
+ console.log("You must accept the Terms of Service to use Unbrowse.");
222
+ process.exit(1);
223
+ }
224
+ const fallbackName = buildDefaultAgentName();
225
+ const name = options?.promptForEmail ? await promptAgentEmail(fallbackName) : resolveAgentName(process.env.UNBROWSE_AGENT_EMAIL, fallbackName);
226
+ console.log(`Registering as "${name}"...`);
227
+ try {
228
+ const { agent_id, api_key } = await api("POST", "/v1/agents/register", { name, tos_version: tosInfo.version });
229
+ process.env.UNBROWSE_API_KEY = api_key;
230
+ saveConfig({
231
+ api_key,
232
+ agent_id,
233
+ agent_name: name,
234
+ registered_at: new Date().toISOString(),
235
+ tos_accepted_version: tosInfo.version,
236
+ tos_accepted_at: new Date().toISOString()
237
+ });
238
+ console.log(`Registered as ${name}. API key saved to ~/.unbrowse/config.json`);
239
+ } catch (err) {
240
+ console.warn(`Registration failed: ${err.message}`);
241
+ console.warn("Set UNBROWSE_API_KEY manually or try again.");
242
+ process.exit(1);
243
+ }
244
+ }
245
+
25
246
  // ../../src/runtime/local-server.ts
26
- import { openSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
247
+ import { openSync, readFileSync as readFileSync2, unlinkSync, writeFileSync as writeFileSync2 } from "node:fs";
27
248
  import path2 from "node:path";
28
249
  import { spawn } from "node:child_process";
29
250
 
30
251
  // ../../src/runtime/paths.ts
31
- import { existsSync, mkdirSync, realpathSync } from "node:fs";
252
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2, realpathSync } from "node:fs";
32
253
  import os from "node:os";
33
254
  import path from "node:path";
34
255
  import { createRequire as createRequire2 } from "node:module";
@@ -56,7 +277,7 @@ function runtimeArgsForEntrypoint(metaUrl, entrypoint) {
56
277
  const req = createRequire2(metaUrl);
57
278
  const tsxPkg = req.resolve("tsx/package.json");
58
279
  const tsxLoader = path.join(path.dirname(tsxPkg), "dist", "loader.mjs");
59
- if (existsSync(tsxLoader))
280
+ if (existsSync2(tsxLoader))
60
281
  return ["--import", tsxLoader, entrypoint];
61
282
  } catch {}
62
283
  return ["--import", "tsx", entrypoint];
@@ -65,8 +286,8 @@ function getUnbrowseHome() {
65
286
  return path.join(os.homedir(), ".unbrowse");
66
287
  }
67
288
  function ensureDir(dir) {
68
- if (!existsSync(dir))
69
- mkdirSync(dir, { recursive: true });
289
+ if (!existsSync2(dir))
290
+ mkdirSync2(dir, { recursive: true });
70
291
  return dir;
71
292
  }
72
293
  function getLogsDir() {
@@ -116,7 +337,7 @@ function isPidAlive(pid) {
116
337
  }
117
338
  function readPidState(pidFile) {
118
339
  try {
119
- return JSON.parse(readFileSync(pidFile, "utf-8"));
340
+ return JSON.parse(readFileSync2(pidFile, "utf-8"));
120
341
  } catch {
121
342
  return null;
122
343
  }
@@ -164,7 +385,7 @@ async function ensureLocalServer(baseUrl, noAutoStart, metaUrl) {
164
385
  }
165
386
  });
166
387
  child.unref();
167
- writeFileSync(pidFile, JSON.stringify({
388
+ writeFileSync2(pidFile, JSON.stringify({
168
389
  pid: child.pid,
169
390
  base_url: baseUrl,
170
391
  started_at: new Date().toISOString(),
@@ -176,7 +397,7 @@ async function ensureLocalServer(baseUrl, noAutoStart, metaUrl) {
176
397
  }
177
398
 
178
399
  // ../../src/runtime/paths.ts
179
- import { existsSync as existsSync2, mkdirSync as mkdirSync2, realpathSync as realpathSync2 } from "node:fs";
400
+ import { existsSync as existsSync3, mkdirSync as mkdirSync3, realpathSync as realpathSync2 } from "node:fs";
180
401
  import path3 from "node:path";
181
402
  import { fileURLToPath as fileURLToPath2 } from "node:url";
182
403
  function isMainModule(metaUrl) {
@@ -194,7 +415,7 @@ function isMainModule(metaUrl) {
194
415
  // ../../src/runtime/setup.ts
195
416
  import { execFileSync } from "node:child_process";
196
417
  import { createRequire as createRequire3 } from "node:module";
197
- import { existsSync as existsSync3, mkdirSync as mkdirSync3, writeFileSync as writeFileSync2 } from "node:fs";
418
+ import { existsSync as existsSync4, mkdirSync as mkdirSync4, writeFileSync as writeFileSync3 } from "node:fs";
198
419
  import os2 from "node:os";
199
420
  import path4 from "node:path";
200
421
  var req = createRequire3(import.meta.url);
@@ -228,7 +449,7 @@ function getOpenCodeProjectCommandsDir(cwd) {
228
449
  return path4.join(cwd, ".opencode", "commands");
229
450
  }
230
451
  function detectOpenCode(cwd) {
231
- return hasBinary("opencode") || existsSync3(path4.join(resolveConfigHome(), "opencode")) || existsSync3(path4.join(cwd, ".opencode"));
452
+ return hasBinary("opencode") || existsSync4(path4.join(resolveConfigHome(), "opencode")) || existsSync4(path4.join(cwd, ".opencode"));
232
453
  }
233
454
  function renderOpenCodeCommand() {
234
455
  return `---
@@ -256,13 +477,13 @@ function writeOpenCodeCommand(scope, cwd) {
256
477
  if (scope === "auto" && !detected) {
257
478
  return { detected: false, action: "not-detected", scope: "off" };
258
479
  }
259
- const resolvedScope = scope === "project" ? "project" : scope === "global" ? "global" : existsSync3(path4.join(cwd, ".opencode")) ? "project" : "global";
480
+ const resolvedScope = scope === "project" ? "project" : scope === "global" ? "global" : existsSync4(path4.join(cwd, ".opencode")) ? "project" : "global";
260
481
  const commandsDir = resolvedScope === "project" ? getOpenCodeProjectCommandsDir(cwd) : getOpenCodeGlobalCommandsDir();
261
482
  const commandFile = path4.join(ensureDir(commandsDir), "unbrowse.md");
262
483
  const content = renderOpenCodeCommand();
263
- const action = existsSync3(commandFile) ? "updated" : "installed";
264
- mkdirSync3(path4.dirname(commandFile), { recursive: true });
265
- writeFileSync2(commandFile, content);
484
+ const action = existsSync4(commandFile) ? "updated" : "installed";
485
+ mkdirSync4(path4.dirname(commandFile), { recursive: true });
486
+ writeFileSync3(commandFile, content);
266
487
  return {
267
488
  detected: detected || scope !== "auto",
268
489
  action,
@@ -273,7 +494,7 @@ function writeOpenCodeCommand(scope, cwd) {
273
494
  async function ensureBrowserEngineInstalled() {
274
495
  try {
275
496
  const { chromium } = await import("playwright-core");
276
- if (existsSync3(chromium.executablePath())) {
497
+ if (existsSync4(chromium.executablePath())) {
277
498
  return { installed: true, action: "already-installed" };
278
499
  }
279
500
  const agentBrowserBin = req.resolve("agent-browser/bin/agent-browser.js");
@@ -330,7 +551,7 @@ function parseArgs(argv) {
330
551
  }
331
552
  return { command, args: positional, flags };
332
553
  }
333
- async function api(method, path5, body) {
554
+ async function api2(method, path5, body) {
334
555
  const res = await fetch(`${BASE_URL}${path5}`, {
335
556
  method,
336
557
  headers: {
@@ -486,6 +707,41 @@ function hasMeaningfulValue(value) {
486
707
  return Object.values(value).some((item) => hasMeaningfulValue(item));
487
708
  return false;
488
709
  }
710
+ function isPlainRecord(value) {
711
+ return value != null && typeof value === "object" && !Array.isArray(value);
712
+ }
713
+ function isScalarLike(value) {
714
+ if (value == null)
715
+ return false;
716
+ if (typeof value === "string")
717
+ return value.trim().length > 0;
718
+ if (typeof value === "number" || typeof value === "boolean")
719
+ return true;
720
+ if (Array.isArray(value)) {
721
+ return value.length > 0 && value.every((item) => item == null || typeof item === "string" || typeof item === "number" || typeof item === "boolean");
722
+ }
723
+ return false;
724
+ }
725
+ function looksStructuredForDirectOutput(value) {
726
+ if (Array.isArray(value)) {
727
+ const sample = value.filter(isPlainRecord).slice(0, 3);
728
+ if (sample.length === 0)
729
+ return false;
730
+ const simpleRows = sample.filter((row) => {
731
+ const keys2 = Object.keys(row);
732
+ const scalarFields2 = Object.values(row).filter(isScalarLike).length;
733
+ return keys2.length > 0 && keys2.length <= 20 && scalarFields2 >= 2;
734
+ });
735
+ return simpleRows.length >= Math.ceil(sample.length / 2);
736
+ }
737
+ if (!isPlainRecord(value))
738
+ return false;
739
+ const keys = Object.keys(value);
740
+ if (keys.length === 0 || keys.length > 20)
741
+ return false;
742
+ const scalarFields = Object.values(value).filter(isScalarLike).length;
743
+ return scalarFields >= 2;
744
+ }
489
745
  function applyTransforms(result, flags) {
490
746
  let data = result;
491
747
  const entityIndex = detectEntityIndex(result);
@@ -561,6 +817,9 @@ function autoExtractOrWrap(obj) {
561
817
  const resultStr = JSON.stringify(obj.result ?? "");
562
818
  if (resultStr.length < 2000)
563
819
  return obj;
820
+ if (looksStructuredForDirectOutput(obj.result)) {
821
+ return slimTrace({ ...obj, extraction_hints: undefined, response_schema: undefined });
822
+ }
564
823
  if (!hints)
565
824
  return obj;
566
825
  if (hints.confidence === "high") {
@@ -585,7 +844,7 @@ function autoExtractOrWrap(obj) {
585
844
  return wrapWithHints(obj);
586
845
  }
587
846
  async function cmdHealth(flags) {
588
- output(await api("GET", "/health"), !!flags.pretty);
847
+ output(await api2("GET", "/health"), !!flags.pretty);
589
848
  }
590
849
  async function cmdResolve(flags) {
591
850
  const intent = flags.intent;
@@ -615,7 +874,7 @@ async function cmdResolve(flags) {
615
874
  if (flags.raw || hasTransforms)
616
875
  body.projection = { raw: true };
617
876
  const startedAt = Date.now();
618
- let result = await withPendingNotice(api("POST", "/v1/intent/resolve", body), "Still working. First-time capture/indexing for a site can take 20-80s. Waiting is usually better than falling back.");
877
+ let result = await withPendingNotice(api2("POST", "/v1/intent/resolve", body), "Still working. First-time capture/indexing for a site can take 20-80s. Waiting is usually better than falling back.");
619
878
  if (flags.schema) {
620
879
  output(schemaOnly(result), !!flags.pretty);
621
880
  return;
@@ -657,7 +916,7 @@ async function cmdExecute(flags) {
657
916
  const hasTransforms = !!(flags.path || flags.extract);
658
917
  if (flags.raw || hasTransforms)
659
918
  body.projection = { raw: true };
660
- let result = await withPendingNotice(api("POST", `/v1/skills/${skillId}/execute`, body), "Still working. This endpoint may require browser replay or first-time auth/capture setup.");
919
+ let result = await withPendingNotice(api2("POST", `/v1/skills/${skillId}/execute`, body), "Still working. This endpoint may require browser replay or first-time auth/capture setup.");
661
920
  if (flags.schema) {
662
921
  output(schemaOnly(result), !!flags.pretty);
663
922
  return;
@@ -684,22 +943,22 @@ async function cmdFeedback(flags) {
684
943
  body.outcome = flags.outcome;
685
944
  if (flags.diagnostics)
686
945
  body.diagnostics = JSON.parse(flags.diagnostics);
687
- output(await api("POST", "/v1/feedback", body), !!flags.pretty);
946
+ output(await api2("POST", "/v1/feedback", body), !!flags.pretty);
688
947
  }
689
948
  async function cmdLogin(flags) {
690
949
  const url = flags.url;
691
950
  if (!url)
692
951
  die("--url is required");
693
- output(await api("POST", "/v1/auth/login", { url }), !!flags.pretty);
952
+ output(await api2("POST", "/v1/auth/login", { url }), !!flags.pretty);
694
953
  }
695
954
  async function cmdSkills(flags) {
696
- output(await api("GET", "/v1/skills"), !!flags.pretty);
955
+ output(await api2("GET", "/v1/skills"), !!flags.pretty);
697
956
  }
698
957
  async function cmdSkill(args, flags) {
699
958
  const id = args[0] ?? flags.id;
700
959
  if (!id)
701
960
  die("skill <id> or --id required");
702
- output(await api("GET", `/v1/skills/${id}`), !!flags.pretty);
961
+ output(await api2("GET", `/v1/skills/${id}`), !!flags.pretty);
703
962
  }
704
963
  async function cmdSearch(flags) {
705
964
  const intent = flags.intent;
@@ -710,14 +969,14 @@ async function cmdSearch(flags) {
710
969
  const body = { intent, k: Number(flags.k) || 5 };
711
970
  if (domain)
712
971
  body.domain = domain;
713
- output(await api("POST", path5, body), !!flags.pretty);
972
+ output(await api2("POST", path5, body), !!flags.pretty);
714
973
  }
715
974
  async function cmdSessions(flags) {
716
975
  const domain = flags.domain;
717
976
  if (!domain)
718
977
  die("--domain is required");
719
978
  const limit = flags.limit ?? "10";
720
- output(await api("GET", `/v1/sessions/${domain}?limit=${limit}`), !!flags.pretty);
979
+ output(await api2("GET", `/v1/sessions/${domain}?limit=${limit}`), !!flags.pretty);
721
980
  }
722
981
  async function cmdSetup(flags) {
723
982
  info("Running setup checks");
@@ -741,6 +1000,9 @@ async function cmdSetup(flags) {
741
1000
  process.exit(1);
742
1001
  return;
743
1002
  }
1003
+ if (!getApiKey()) {
1004
+ await ensureRegistered({ promptForEmail: true });
1005
+ }
744
1006
  try {
745
1007
  await ensureLocalServer(BASE_URL, false, import.meta.url);
746
1008
  report.server = { started: true, base_url: BASE_URL };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "unbrowse",
3
- "version": "1.1.1",
3
+ "version": "1.1.3",
4
4
  "description": "Reverse-engineer any website into reusable API skills. npm CLI + local engine.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -10,6 +10,7 @@ import { recordFeedback, recordDiagnostics, getApiKey, getRecentLocalSkill } fro
10
10
  import { ROUTE_LIMITS } from "../ratelimit/index.js";
11
11
  import type { ProjectionOptions } from "../types/index.js";
12
12
  import { getSkillChunk, toAgentSkillChunkView } from "../graph/index.js";
13
+ import { listRecentSessionsForDomain } from "../session-logs.js";
13
14
  import { writeFileSync, existsSync, mkdirSync } from "fs";
14
15
  import { join } from "path";
15
16
 
@@ -206,20 +207,44 @@ export async function registerRoutes(app: FastifyInstance) {
206
207
  }
207
208
  });
208
209
 
209
- // POST /v1/auth/steal — extract cookies from Chrome/Firefox SQLite DBs.
210
+ // POST /v1/auth/steal — extract cookies from Firefox/Chrome/custom Chromium-family SQLite DBs.
210
211
  // No browser launch, Chrome can stay open. Higher rate limit since it's instant.
211
212
  app.post("/v1/auth/steal", { config: { rateLimit: { max: 30, timeWindow: "1 minute" } } }, async (req, reply) => {
212
- const { url, chrome_profile, firefox_profile } = req.body as {
213
+ const {
214
+ url,
215
+ browser,
216
+ chrome_profile,
217
+ firefox_profile,
218
+ chromium_profile,
219
+ chromium_user_data_dir,
220
+ chromium_cookie_db_path,
221
+ safe_storage_service,
222
+ browser_name,
223
+ } = req.body as {
213
224
  url: string;
225
+ browser?: "auto" | "firefox" | "chrome" | "chromium";
214
226
  chrome_profile?: string;
215
227
  firefox_profile?: string;
228
+ chromium_profile?: string;
229
+ chromium_user_data_dir?: string;
230
+ chromium_cookie_db_path?: string;
231
+ safe_storage_service?: string;
232
+ browser_name?: string;
216
233
  };
217
234
  if (!url) return reply.code(400).send({ error: "url required" });
218
235
  try {
219
236
  const domain = new URL(url).hostname;
220
237
  const result = await extractBrowserAuth(domain, {
238
+ browser,
221
239
  chromeProfile: chrome_profile,
222
240
  firefoxProfile: firefox_profile,
241
+ chromium: {
242
+ profile: chromium_profile,
243
+ userDataDir: chromium_user_data_dir,
244
+ cookieDbPath: chromium_cookie_db_path,
245
+ safeStorageService: safe_storage_service,
246
+ browserName: browser_name,
247
+ },
223
248
  });
224
249
  return reply.send(result);
225
250
  } catch (err) {
@@ -277,6 +302,18 @@ export async function registerRoutes(app: FastifyInstance) {
277
302
  // GET /health
278
303
  app.get("/health", async (_req, reply) => reply.send({ status: "ok", trace_version: TRACE_VERSION, code_hash: CODE_HASH, git_sha: GIT_SHA }));
279
304
 
305
+ // GET /v1/sessions/:domain — read local trace/debug files instead of proxying to backend
306
+ app.get("/v1/sessions/:domain", async (req, reply) => {
307
+ const { domain } = req.params as { domain: string };
308
+ const query = req.query as { limit?: string | number };
309
+ const limitRaw = typeof query.limit === "number" ? query.limit : Number(query.limit ?? 10);
310
+ const limit = Number.isFinite(limitRaw) ? Math.min(Math.max(limitRaw, 1), 50) : 10;
311
+ return reply.send({
312
+ domain,
313
+ sessions: listRecentSessionsForDomain(TRACES_DIR, domain, limit),
314
+ });
315
+ });
316
+
280
317
  // Catch-all proxy: forward unmatched /v1/* routes to beta-api.unbrowse.ai
281
318
  app.all("/v1/*", async (req, reply) => {
282
319
  const key = getApiKey();