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 +9 -3
- package/dist/cli.js +287 -25
- package/package.json +1 -1
- package/runtime-src/api/routes.ts +39 -2
- package/runtime-src/auth/browser-cookies.ts +106 -23
- package/runtime-src/auth/index.ts +17 -4
- package/runtime-src/capture/index.ts +68 -2
- package/runtime-src/cli.ts +44 -0
- package/runtime-src/client/index.ts +51 -3
- package/runtime-src/execution/index.ts +485 -45
- package/runtime-src/extraction/index.ts +198 -6
- package/runtime-src/graph/index.ts +323 -22
- package/runtime-src/graph/local-fixtures.ts +90 -1
- package/runtime-src/graph/local-harness.ts +9 -1
- package/runtime-src/index.ts +10 -76
- package/runtime-src/intent-match.ts +485 -5
- package/runtime-src/marketplace/index.ts +30 -3
- package/runtime-src/orchestrator/index.ts +53 -144
- package/runtime-src/reverse-engineer/index.ts +18 -5
- package/runtime-src/server.ts +100 -0
- package/runtime-src/session-logs.ts +142 -0
- package/runtime-src/template-params.ts +53 -0
- package/runtime-src/vault/index.ts +55 -10
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 (
|
|
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 (!
|
|
69
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
|
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
|
|
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") ||
|
|
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" :
|
|
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 =
|
|
264
|
-
|
|
265
|
-
|
|
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 (
|
|
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
|
|
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
|
|
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(
|
|
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(
|
|
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
|
|
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
|
|
952
|
+
output(await api2("POST", "/v1/auth/login", { url }), !!flags.pretty);
|
|
694
953
|
}
|
|
695
954
|
async function cmdSkills(flags) {
|
|
696
|
-
output(await
|
|
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
|
|
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
|
|
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
|
|
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
|
@@ -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/
|
|
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 {
|
|
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();
|