unbrowse 12.3.0 → 12.3.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/SKILL.md +2 -2
- package/dist/cli.js +84 -10
- package/package.json +1 -1
package/SKILL.md
CHANGED
|
@@ -40,7 +40,7 @@ From an agent's shell (no terminal for the person): run `npx unbrowse login --no
|
|
|
40
40
|
|
|
41
41
|
Then the first call: `npx unbrowse run 'top stories on hacker news'`. The site requests go from the person's own IP by default; `--from-unbrowse` sends them from Unbrowse instead.
|
|
42
42
|
|
|
43
|
-
`npx unbrowse mcp` (a stdio MCP for hosts that need one) uses the CLI sign-in.
|
|
43
|
+
`npx unbrowse mcp` (a stdio MCP for hosts that need one) uses the CLI sign-in; `unbrowse mcp add` registers it with Claude Code and Codex, so they run on that one sign-in. The other way: with no CLI sign-in, the CLI uses Claude Code's own Unbrowse MCP sign-in (the plugin's `https://unbrowse.ai/api/mcp`; a token for `/mcp` does not work on the REST API). No token is ever written into an MCP config.
|
|
44
44
|
|
|
45
45
|
For automation and CI without a browser, set `UNBROWSE_API_KEY` from the caller's secret manager (create a key in the signed-in console at https://unbrowse.ai/app). Never put keys into committed config, prompts, command arguments or logs.
|
|
46
46
|
|
|
@@ -81,7 +81,7 @@ Do not infer business success from HTTP 200, tool transport success, a screensho
|
|
|
81
81
|
|
|
82
82
|
When the person names a site:
|
|
83
83
|
|
|
84
|
-
1. If `unbrowse_discover` is not in this session's tool list, the MCP is not signed in. Stop. Name the configured MCP URL
|
|
84
|
+
1. If `unbrowse_discover` is not in this session's tool list, the MCP is not signed in. Stop. Name the configured MCP URL. A remote MCP needs the host's own OAuth; with the CLI signed in, `npx unbrowse mcp add` gives the host a server on that sign-in instead (it takes effect in a new session).
|
|
85
85
|
2. Look the host up (`unbrowse_sites`, or CLI `unbrowse site <host>`). Tools already compiled: use one. Stop.
|
|
86
86
|
3. No tools: `unbrowse_index {url, focus}` or `POST /api/v1/index`. Poll `unbrowse_index_status` or `GET /api/v1/index/{jobId}`.
|
|
87
87
|
4. `status: failed` and `error.code: model_unavailable`: the indexing model is out of credit. Report the job id and the message. Do not browse instead, and do not start the same job again in a loop.
|
package/dist/cli.js
CHANGED
|
@@ -26,7 +26,7 @@ var __esm = (fn, res, err) => () => {
|
|
|
26
26
|
};
|
|
27
27
|
|
|
28
28
|
// ../sdk/src/version.ts
|
|
29
|
-
var SDK_VERSION = "12.3.
|
|
29
|
+
var SDK_VERSION = "12.3.1";
|
|
30
30
|
|
|
31
31
|
// ../sdk/src/client.ts
|
|
32
32
|
function apiBase(url) {
|
|
@@ -992,7 +992,7 @@ function quoteSystemd(s) {
|
|
|
992
992
|
return /[\s"\\]/.test(s) ? `"${s.replace(/\\/g, "\\\\").replace(/"/g, "\\\"")}"` : s;
|
|
993
993
|
}
|
|
994
994
|
function ephemeralCli(cli) {
|
|
995
|
-
return /[\\/](_npx|\.npm[\\/]_npx|npx-[^\\/]+|tmp|Temp)[\\/]/i.test(cli);
|
|
995
|
+
return /[\\/](_npx|\.npm[\\/]_npx|npx-[^\\/]+|bunx-[^\\/]+|tmp|Temp)[\\/]/i.test(cli);
|
|
996
996
|
}
|
|
997
997
|
var ALL_SITES = "*", allSites = (cfg) => cfg.sites.includes(ALL_SITES), UPLOAD_BATCH = 500, TWO_LEVEL, DEFAULT_INTERVAL_MIN = 15, REFRESH_MS, SERVICE = "unbrowse-cookies", LAUNCHD_LABEL = "ai.unbrowse.cookies", configFile = (dir) => join3(dir, "cookie-sync.json"), stateFile = (dir) => join3(dir, "cookie-sync.state.json");
|
|
998
998
|
var init_cookie_daemon = __esm(() => {
|
|
@@ -1521,7 +1521,7 @@ import { createInterface as createInterface2 } from "node:readline/promises";
|
|
|
1521
1521
|
import { createHash, randomBytes } from "node:crypto";
|
|
1522
1522
|
import { chmodSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
1523
1523
|
import { createServer } from "node:http";
|
|
1524
|
-
import { homedir } from "node:os";
|
|
1524
|
+
import { homedir, hostname } from "node:os";
|
|
1525
1525
|
import { dirname, join } from "node:path";
|
|
1526
1526
|
function configPath() {
|
|
1527
1527
|
const root = process.env.UNBROWSE_CONFIG_DIR ?? join(process.env.XDG_CONFIG_HOME ?? join(homedir(), ".config"), "unbrowse");
|
|
@@ -1567,6 +1567,7 @@ function sharedSignIn(baseUrl, now = Date.now()) {
|
|
|
1567
1567
|
return;
|
|
1568
1568
|
}
|
|
1569
1569
|
const api = new URL(apiResource(baseUrl));
|
|
1570
|
+
let best;
|
|
1570
1571
|
for (const e of entries) {
|
|
1571
1572
|
if (typeof e?.serverUrl !== "string" || typeof e.accessToken !== "string" || !e.accessToken)
|
|
1572
1573
|
continue;
|
|
@@ -1580,9 +1581,11 @@ function sharedSignIn(baseUrl, now = Date.now()) {
|
|
|
1580
1581
|
}
|
|
1581
1582
|
if (url.origin !== api.origin || !(url.pathname === api.pathname || url.pathname.startsWith(`${api.pathname}/`)))
|
|
1582
1583
|
continue;
|
|
1583
|
-
|
|
1584
|
+
const expiresAt = typeof e.expiresAt === "number" ? e.expiresAt : 0;
|
|
1585
|
+
if (!best || expiresAt > best.expiresAt)
|
|
1586
|
+
best = { token: e.accessToken, from: `Claude Code's MCP sign-in (${String(e.serverName ?? url.origin + url.pathname)})`, expiresAt };
|
|
1584
1587
|
}
|
|
1585
|
-
return;
|
|
1588
|
+
return best && { token: best.token, from: best.from };
|
|
1586
1589
|
}
|
|
1587
1590
|
async function currentToken(baseUrl, fetchImpl = fetch) {
|
|
1588
1591
|
if (process.env.UNBROWSE_API_KEY)
|
|
@@ -1606,6 +1609,16 @@ async function currentToken(baseUrl, fetchImpl = fetch) {
|
|
|
1606
1609
|
}
|
|
1607
1610
|
return o.accessToken;
|
|
1608
1611
|
}
|
|
1612
|
+
async function tokenFor(baseUrl, target, fetchImpl = fetch) {
|
|
1613
|
+
const token = await currentToken(baseUrl, fetchImpl);
|
|
1614
|
+
if (!token)
|
|
1615
|
+
return;
|
|
1616
|
+
let same = false;
|
|
1617
|
+
try {
|
|
1618
|
+
same = new URL(target, baseUrl).origin === new URL(baseUrl).origin;
|
|
1619
|
+
} catch {}
|
|
1620
|
+
return same || signInSource(baseUrl) !== "shared" ? token : undefined;
|
|
1621
|
+
}
|
|
1609
1622
|
function signInSource(baseUrl) {
|
|
1610
1623
|
if (process.env.UNBROWSE_API_KEY)
|
|
1611
1624
|
return "env";
|
|
@@ -1715,14 +1728,70 @@ ${url.toString()}`);
|
|
|
1715
1728
|
server.close();
|
|
1716
1729
|
}
|
|
1717
1730
|
}
|
|
1731
|
+
async function deviceLogin(baseUrl, opts) {
|
|
1732
|
+
const fetchImpl = opts.fetch ?? fetch;
|
|
1733
|
+
const sleep = opts.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
1734
|
+
const now = opts.now ?? Date.now;
|
|
1735
|
+
const name = opts.clientName ?? `Unbrowse CLI (${hostname()})`;
|
|
1736
|
+
const redirectUri = `${baseUrl.replace(/\/+$/, "")}/device/done`;
|
|
1737
|
+
const reg = await fetchImpl(`${baseUrl}/oauth/register`, {
|
|
1738
|
+
method: "POST",
|
|
1739
|
+
headers: { "content-type": "application/json", accept: "application/json" },
|
|
1740
|
+
body: JSON.stringify({ client_name: name, redirect_uris: [redirectUri], grant_types: ["authorization_code", "refresh_token"], response_types: ["code"], token_endpoint_auth_method: "none" })
|
|
1741
|
+
});
|
|
1742
|
+
const client = await reg.json().catch(() => ({}));
|
|
1743
|
+
if (!reg.ok || !client.client_id)
|
|
1744
|
+
throw new Error(`client registration failed: ${client.error_description ?? `HTTP ${reg.status}`}`);
|
|
1745
|
+
const da = await fetchImpl(`${baseUrl}/oauth/device_authorization`, {
|
|
1746
|
+
method: "POST",
|
|
1747
|
+
headers: { "content-type": "application/x-www-form-urlencoded", accept: "application/json" },
|
|
1748
|
+
body: new URLSearchParams({ client_id: client.client_id, resource: apiResource(baseUrl) }).toString()
|
|
1749
|
+
});
|
|
1750
|
+
const dev = await da.json().catch(() => ({}));
|
|
1751
|
+
if (!da.ok || !dev.device_code || !dev.user_code)
|
|
1752
|
+
throw new Error(`could not start device sign-in: ${dev.error_description ?? dev.error ?? `HTTP ${da.status}`}`);
|
|
1753
|
+
opts.log(`To sign in, open ${dev.verification_uri} on any device and enter the code:
|
|
1754
|
+
|
|
1755
|
+
${dev.user_code}
|
|
1756
|
+
|
|
1757
|
+
(or open ${dev.verification_uri_complete} to skip typing it). Waiting for approval…`);
|
|
1758
|
+
const deadline = now() + (dev.expires_in ?? 600) * 1000;
|
|
1759
|
+
let interval = Math.max(1, dev.interval ?? 5);
|
|
1760
|
+
while (now() < deadline) {
|
|
1761
|
+
await sleep(interval * 1000);
|
|
1762
|
+
const res = await fetchImpl(`${baseUrl}/oauth/token`, {
|
|
1763
|
+
method: "POST",
|
|
1764
|
+
headers: { "content-type": "application/x-www-form-urlencoded", accept: "application/json" },
|
|
1765
|
+
body: new URLSearchParams({ grant_type: "urn:ietf:params:oauth:grant-type:device_code", device_code: dev.device_code, client_id: client.client_id }).toString()
|
|
1766
|
+
});
|
|
1767
|
+
const body = await res.json().catch(() => ({}));
|
|
1768
|
+
if (res.ok && body.access_token) {
|
|
1769
|
+
save({ baseUrl, oauth: { clientId: client.client_id, accessToken: body.access_token, refreshToken: body.refresh_token, expiresAt: body.expires_in ? now() + body.expires_in * 1000 : undefined } });
|
|
1770
|
+
return;
|
|
1771
|
+
}
|
|
1772
|
+
if (body.error === "authorization_pending")
|
|
1773
|
+
continue;
|
|
1774
|
+
if (body.error === "slow_down") {
|
|
1775
|
+
interval += 5;
|
|
1776
|
+
continue;
|
|
1777
|
+
}
|
|
1778
|
+
if (body.error === "access_denied")
|
|
1779
|
+
throw new Error("the sign-in was denied");
|
|
1780
|
+
if (body.error === "expired_token")
|
|
1781
|
+
throw new Error("the code expired before it was approved; run `unbrowse login` again");
|
|
1782
|
+
throw new Error(body.error_description ?? body.error ?? `device sign-in failed (HTTP ${res.status})`);
|
|
1783
|
+
}
|
|
1784
|
+
throw new Error("timed out waiting for approval");
|
|
1785
|
+
}
|
|
1718
1786
|
|
|
1719
1787
|
// src/cli.ts
|
|
1720
1788
|
init_src();
|
|
1721
|
-
var VERSION = "12.3.
|
|
1789
|
+
var VERSION = "12.3.1";
|
|
1722
1790
|
var DEFAULT_ORIGIN = "https://unbrowse.ai";
|
|
1723
1791
|
var HELP = `unbrowse ${VERSION} — call websites as APIs through the Unbrowse API
|
|
1724
1792
|
|
|
1725
1793
|
login [--key ub_live_…] Sign in in your browser (OAuth), or store an API key
|
|
1794
|
+
[--device] On a remote/headless machine, show a code to approve on your phone
|
|
1726
1795
|
(first use of any command below signs you in the same way)
|
|
1727
1796
|
logout Forget the stored sign-in
|
|
1728
1797
|
whoami Workspace and usage
|
|
@@ -1840,9 +1909,10 @@ async function main(argv, io = stdio) {
|
|
|
1840
1909
|
const client = async () => new Unbrowse({ apiKey: await currentToken(origin) ?? "", baseUrl: origin, client: `cli/${VERSION}` });
|
|
1841
1910
|
if (cmd === "mcp" && rest[0] !== "add") {
|
|
1842
1911
|
await Promise.resolve().then(() => init_mcp_proxy());
|
|
1912
|
+
const url = flag("url") ?? process.env.UNBROWSE_MCP_URL ?? `${origin}/api/mcp`;
|
|
1843
1913
|
await serveStdio({
|
|
1844
|
-
url
|
|
1845
|
-
token:
|
|
1914
|
+
url,
|
|
1915
|
+
token: () => tokenFor(origin, url),
|
|
1846
1916
|
version: VERSION,
|
|
1847
1917
|
endUser: flag("end-user") ?? process.env.UNBROWSE_END_USER,
|
|
1848
1918
|
...args.flags["from-here"] || process.env.UNBROWSE_EGRESS === "client" ? { fromHere: client } : {}
|
|
@@ -1956,6 +2026,8 @@ Your browser opens ${origin}; approve it and \`unbrowse ${cmd}\` carries on. (Sc
|
|
|
1956
2026
|
const key = flag("key");
|
|
1957
2027
|
if (key)
|
|
1958
2028
|
save({ baseUrl: origin, apiKey: key });
|
|
2029
|
+
else if (args.flags.device || !io.interactive)
|
|
2030
|
+
await deviceLogin(origin, { log: io.err });
|
|
1959
2031
|
else
|
|
1960
2032
|
await oauthLogin(origin, { open: (u) => openLink(u, args, io), log: io.err });
|
|
1961
2033
|
const me = await (await client()).me();
|
|
@@ -2354,9 +2426,11 @@ async function mcpAdd(rest, args, io, origin) {
|
|
|
2354
2426
|
}
|
|
2355
2427
|
}
|
|
2356
2428
|
const r = exec(shell(plan.add));
|
|
2357
|
-
if (r.status === 0)
|
|
2429
|
+
if (r.status === 0) {
|
|
2358
2430
|
io.err(`✓ ${h}: added "${name}" (stdio, on this CLI's sign-in)`);
|
|
2359
|
-
|
|
2431
|
+
if (h === "claude" && exec(shell(["claude", "mcp", "get", "plugin:unbrowse:unbrowse"])).status === 0)
|
|
2432
|
+
io.err(` Claude Code also has the Unbrowse plugin's server (plugin:unbrowse:unbrowse), so its tools now show twice. Keep one: disable that server in /mcp, or \`claude mcp remove ${name} --scope user\`.`);
|
|
2433
|
+
} else
|
|
2360
2434
|
failed++, io.err(`✗ ${h}: ${(r.stderr || r.stdout).trim() || `exit ${r.status}`}`);
|
|
2361
2435
|
}
|
|
2362
2436
|
if (!dry) {
|