unbrowse 12.3.1 → 12.4.0
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 +3 -1
- package/dist/cli.js +310 -34
- package/package.json +17 -1
- package/references/tools.json +2 -1
package/README.md
CHANGED
|
@@ -13,6 +13,7 @@ unbrowse resume <runId> origin=SIN # answer on the same run
|
|
|
13
13
|
| Command | Does |
|
|
14
14
|
|---|---|
|
|
15
15
|
| `login [--key K]`, `logout`, `whoami`, `usage` | Account |
|
|
16
|
+
| `accounts`, `accounts use <name>`, `--account NAME` | Several Unbrowse accounts on one machine, each with its own vault ([docs](https://github.com/unbrowse-ai/unbrowse/blob/main/docs/cli.md#several-accounts)) |
|
|
16
17
|
| `discover <query>` | Your capabilities, then the public registry |
|
|
17
18
|
| `run <task…>`, `inspect`, `resume`, `cancel` | Runs |
|
|
18
19
|
| `scrape <url>` | One page as markdown on stdout |
|
|
@@ -21,8 +22,9 @@ unbrowse resume <runId> origin=SIN # answer on the same run
|
|
|
21
22
|
| `logins`, `logins remove <origin>` | Saved logins, masked. The CLI never takes a password |
|
|
22
23
|
| `registry [query]`, `site <host>`, `openapi <host>` | Public compiled sites, a site's tools, its OpenAPI 3.1 document — no account |
|
|
23
24
|
| `call <host> <tool> [JSON]` | Run one site tool (`--set k=v`, `--deadline MS`, `--select a,b`, `--idempotency-key K`, `--end-user ID`) |
|
|
25
|
+
| `tools [query]`, `tool <name> [--key value…]` | Every tool of the hosted MCP from the shell: the cloud browser (`tool browse_open --url …`), `map`, replay, memory, your sites' tools |
|
|
24
26
|
|
|
25
|
-
The CLI is the REST client
|
|
27
|
+
The CLI is the REST client, and through `tool` it can call everything the remote MCP has. To give an agent host the MCP itself, see the [install guide](https://github.com/unbrowse-ai/unbrowse/blob/main/docs/install.md).
|
|
26
28
|
|
|
27
29
|
Exit codes: 0 accepted/ok (check status and verified), 1 error, 2 input required, 3 sign-in or saved login needed, 4 not verified.
|
|
28
30
|
Env: `UNBROWSE_API_KEY`, `UNBROWSE_BASE_URL`. Node 18.17+, no dependencies.
|
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.
|
|
29
|
+
var SDK_VERSION = "12.4.0";
|
|
30
30
|
|
|
31
31
|
// ../sdk/src/client.ts
|
|
32
32
|
function apiBase(url) {
|
|
@@ -924,10 +924,12 @@ async function watch(opts) {
|
|
|
924
924
|
}
|
|
925
925
|
function servicePlan(opts) {
|
|
926
926
|
const os = opts.os ?? platform();
|
|
927
|
+
const service = opts.account ? `${SERVICE}-${opts.account}` : SERVICE;
|
|
928
|
+
const label = opts.account ? `${LAUNCHD_LABEL}.${opts.account}` : LAUNCHD_LABEL;
|
|
927
929
|
const home = opts.home ?? homedir3();
|
|
928
930
|
const env = Object.entries(opts.env ?? {});
|
|
929
931
|
if (os === "linux") {
|
|
930
|
-
const path = join3(home, ".config", "systemd", "user", `${
|
|
932
|
+
const path = join3(home, ".config", "systemd", "user", `${service}.service`);
|
|
931
933
|
const content = [
|
|
932
934
|
"[Unit]",
|
|
933
935
|
"Description=Unbrowse cookie sync: keeps your browser sign-ins for chosen sites in sync with Unbrowse",
|
|
@@ -948,21 +950,21 @@ function servicePlan(opts) {
|
|
|
948
950
|
kind: "systemd",
|
|
949
951
|
path,
|
|
950
952
|
content,
|
|
951
|
-
enable: [["systemctl", "--user", "daemon-reload"], ["systemctl", "--user", "enable", "--now", `${
|
|
952
|
-
disable: [["systemctl", "--user", "disable", "--now", `${
|
|
953
|
-
status: ["systemctl", "--user", "is-active", `${
|
|
954
|
-
log: `journalctl --user -u ${
|
|
953
|
+
enable: [["systemctl", "--user", "daemon-reload"], ["systemctl", "--user", "enable", "--now", `${service}.service`]],
|
|
954
|
+
disable: [["systemctl", "--user", "disable", "--now", `${service}.service`], ["systemctl", "--user", "daemon-reload"]],
|
|
955
|
+
status: ["systemctl", "--user", "is-active", `${service}.service`],
|
|
956
|
+
log: `journalctl --user -u ${service} -f`
|
|
955
957
|
};
|
|
956
958
|
}
|
|
957
959
|
if (os === "darwin") {
|
|
958
|
-
const path = join3(home, "Library", "LaunchAgents", `${
|
|
959
|
-
const logPath = join3(home, "Library", "Logs", `${
|
|
960
|
+
const path = join3(home, "Library", "LaunchAgents", `${label}.plist`);
|
|
961
|
+
const logPath = join3(home, "Library", "Logs", `${service}.log`);
|
|
960
962
|
const esc = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
961
963
|
const content = `<?xml version="1.0" encoding="UTF-8"?>
|
|
962
964
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
963
965
|
<plist version="1.0">
|
|
964
966
|
<dict>
|
|
965
|
-
<key>Label</key><string>${
|
|
967
|
+
<key>Label</key><string>${label}</string>
|
|
966
968
|
<key>ProgramArguments</key>
|
|
967
969
|
<array><string>${esc(opts.node)}</string><string>${esc(opts.cli)}</string><string>cookies</string><string>watch</string></array>
|
|
968
970
|
${env.length ? ` <key>EnvironmentVariables</key>
|
|
@@ -982,7 +984,7 @@ ${env.length ? ` <key>EnvironmentVariables</key>
|
|
|
982
984
|
content,
|
|
983
985
|
enable: [["launchctl", "bootout", domain, path], ["launchctl", "bootstrap", domain, path]],
|
|
984
986
|
disable: [["launchctl", "bootout", domain, path]],
|
|
985
|
-
status: ["launchctl", "print", `${domain}/${
|
|
987
|
+
status: ["launchctl", "print", `${domain}/${label}`],
|
|
986
988
|
log: `tail -f ${logPath}`
|
|
987
989
|
};
|
|
988
990
|
}
|
|
@@ -1218,7 +1220,7 @@ var HEADER = 100;
|
|
|
1218
1220
|
// src/cookies.ts
|
|
1219
1221
|
import { createDecipheriv, createHash as createHash3, pbkdf2Sync } from "node:crypto";
|
|
1220
1222
|
import { execFileSync } from "node:child_process";
|
|
1221
|
-
import { existsSync as existsSync4, readdirSync, readFileSync as readFileSync4 } from "node:fs";
|
|
1223
|
+
import { existsSync as existsSync4, readdirSync as readdirSync2, readFileSync as readFileSync4 } from "node:fs";
|
|
1222
1224
|
import { homedir as homedir4, platform as platform2 } from "node:os";
|
|
1223
1225
|
import { join as join4 } from "node:path";
|
|
1224
1226
|
function chromium(name, [mac, linux], keychain) {
|
|
@@ -1306,7 +1308,7 @@ function firefoxProfiles(def, base) {
|
|
|
1306
1308
|
}
|
|
1307
1309
|
function safeReaddir(dir) {
|
|
1308
1310
|
try {
|
|
1309
|
-
return
|
|
1311
|
+
return readdirSync2(dir);
|
|
1310
1312
|
} catch {
|
|
1311
1313
|
return [];
|
|
1312
1314
|
}
|
|
@@ -1510,6 +1512,85 @@ var init_cookies = __esm(() => {
|
|
|
1510
1512
|
TEXT = /^[\x20-\x7e]*$/;
|
|
1511
1513
|
});
|
|
1512
1514
|
|
|
1515
|
+
// src/mcp-tools.ts
|
|
1516
|
+
function resolveTool(tools, typed) {
|
|
1517
|
+
const names = new Map(tools.map((t) => [t.name, t]));
|
|
1518
|
+
const safe = safeToolName(typed);
|
|
1519
|
+
const underscored = safe.replace(/-/g, "_");
|
|
1520
|
+
for (const n of [typed, safe, underscored, `unbrowse_${safe}`, `unbrowse_${underscored}`])
|
|
1521
|
+
if (names.has(n))
|
|
1522
|
+
return names.get(n);
|
|
1523
|
+
return;
|
|
1524
|
+
}
|
|
1525
|
+
function shortName(name) {
|
|
1526
|
+
return name.replace(/^unbrowse_/, "");
|
|
1527
|
+
}
|
|
1528
|
+
function signature(t, max = 60) {
|
|
1529
|
+
const schema = t.inputSchema ?? {};
|
|
1530
|
+
const required = new Set(schema.required ?? []);
|
|
1531
|
+
const keys = Object.keys(schema.properties ?? {});
|
|
1532
|
+
const props = [...keys.filter((k) => required.has(k)), ...keys.filter((k) => !required.has(k)).map((k) => `${k}?`)];
|
|
1533
|
+
let shown = "";
|
|
1534
|
+
for (const [i, p] of props.entries()) {
|
|
1535
|
+
const next = shown ? `${shown}, ${p}` : p;
|
|
1536
|
+
if (`${shortName(t.name)}(${next})`.length > max && i > 0)
|
|
1537
|
+
return `${shortName(t.name)}(${shown}, …)`;
|
|
1538
|
+
shown = next;
|
|
1539
|
+
}
|
|
1540
|
+
return `${shortName(t.name)}(${shown})`;
|
|
1541
|
+
}
|
|
1542
|
+
function summary(description = "", max = 110) {
|
|
1543
|
+
const first = description.split(/(?<=[.!?])\s|\n/)[0].trim();
|
|
1544
|
+
return first.length > max ? `${first.slice(0, max - 1)}…` : first;
|
|
1545
|
+
}
|
|
1546
|
+
function filterTools(tools, query) {
|
|
1547
|
+
const split = (s) => s.toLowerCase().split(/[^a-z0-9]+/).filter(Boolean);
|
|
1548
|
+
const words = split(query);
|
|
1549
|
+
const matches = (text) => {
|
|
1550
|
+
const tokens = split(text);
|
|
1551
|
+
return words.every((w) => tokens.some((k) => k.startsWith(w)));
|
|
1552
|
+
};
|
|
1553
|
+
const byName = tools.filter((t) => matches(shortName(t.name)));
|
|
1554
|
+
return byName.length ? byName : tools.filter((t) => matches(`${shortName(t.name)} ${t.description ?? ""}`));
|
|
1555
|
+
}
|
|
1556
|
+
function toolInput(t, json, flags, sets, coerce) {
|
|
1557
|
+
const props = new Set(Object.keys((t.inputSchema ?? {}).properties ?? {}));
|
|
1558
|
+
const key = (k) => props.has(k) ? k : k.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
1559
|
+
const base = json ? JSON.parse(json) : {};
|
|
1560
|
+
if (!base || typeof base !== "object" || Array.isArray(base))
|
|
1561
|
+
throw new SyntaxError("the tool's input must be a JSON object");
|
|
1562
|
+
const out = { ...base };
|
|
1563
|
+
for (const [k, v] of Object.entries(flags))
|
|
1564
|
+
if (!CLI_FLAGS.has(k))
|
|
1565
|
+
out[key(k)] = typeof v === "string" ? coerce(v) : v;
|
|
1566
|
+
for (const [k, v] of Object.entries(sets))
|
|
1567
|
+
out[key(k)] = v;
|
|
1568
|
+
return out;
|
|
1569
|
+
}
|
|
1570
|
+
function resultValue(r) {
|
|
1571
|
+
if (r.structuredContent !== undefined)
|
|
1572
|
+
return r.structuredContent;
|
|
1573
|
+
const parts = (r.content ?? []).map((c) => c.type === "text" ? c.text ?? "" : `[${c.type}${c.mimeType ? ` ${c.mimeType}` : ""}]`);
|
|
1574
|
+
const text = parts.join(`
|
|
1575
|
+
`);
|
|
1576
|
+
try {
|
|
1577
|
+
return JSON.parse(text);
|
|
1578
|
+
} catch {
|
|
1579
|
+
return text;
|
|
1580
|
+
}
|
|
1581
|
+
}
|
|
1582
|
+
function resultCode(r, value) {
|
|
1583
|
+
if (r.isError)
|
|
1584
|
+
return 1;
|
|
1585
|
+
const status = value?.status;
|
|
1586
|
+
return status === "input_required" ? 2 : 0;
|
|
1587
|
+
}
|
|
1588
|
+
var CLI_FLAGS;
|
|
1589
|
+
var init_mcp_tools = __esm(() => {
|
|
1590
|
+
init_mcp_proxy();
|
|
1591
|
+
CLI_FLAGS = new Set(["json", "base-url", "account", "no-open", "input", "from-here", "help", "version"]);
|
|
1592
|
+
});
|
|
1593
|
+
|
|
1513
1594
|
// src/cli.ts
|
|
1514
1595
|
import { spawn, spawnSync } from "node:child_process";
|
|
1515
1596
|
import { randomUUID } from "node:crypto";
|
|
@@ -1519,13 +1600,70 @@ import { createInterface as createInterface2 } from "node:readline/promises";
|
|
|
1519
1600
|
|
|
1520
1601
|
// src/auth.ts
|
|
1521
1602
|
import { createHash, randomBytes } from "node:crypto";
|
|
1522
|
-
import { chmodSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
1603
|
+
import { chmodSync, existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
1523
1604
|
import { createServer } from "node:http";
|
|
1524
1605
|
import { homedir, hostname } from "node:os";
|
|
1525
1606
|
import { dirname, join } from "node:path";
|
|
1607
|
+
function configRoot() {
|
|
1608
|
+
return process.env.UNBROWSE_CONFIG_DIR ?? join(process.env.XDG_CONFIG_HOME ?? join(homedir(), ".config"), "unbrowse");
|
|
1609
|
+
}
|
|
1610
|
+
var selected;
|
|
1611
|
+
var ACCOUNT_NAME = /^[A-Za-z0-9][A-Za-z0-9_-]{0,39}$/;
|
|
1612
|
+
var defaultFile = () => join(configRoot(), "account");
|
|
1613
|
+
function checkAccount(name) {
|
|
1614
|
+
if (name !== "default" && !ACCOUNT_NAME.test(name))
|
|
1615
|
+
throw new Error(`invalid account name "${name}": use letters, digits, - and _ (up to 40)`);
|
|
1616
|
+
return name;
|
|
1617
|
+
}
|
|
1618
|
+
function savedAccount() {
|
|
1619
|
+
try {
|
|
1620
|
+
const name = readFileSync(defaultFile(), "utf8").trim();
|
|
1621
|
+
return name && ACCOUNT_NAME.test(name) ? name : undefined;
|
|
1622
|
+
} catch {
|
|
1623
|
+
return;
|
|
1624
|
+
}
|
|
1625
|
+
}
|
|
1626
|
+
function selectAccount(name) {
|
|
1627
|
+
const pick = name ?? (process.env.UNBROWSE_ACCOUNT || undefined) ?? savedAccount();
|
|
1628
|
+
selected = !pick || checkAccount(pick) === "default" ? undefined : pick;
|
|
1629
|
+
return account();
|
|
1630
|
+
}
|
|
1631
|
+
function account() {
|
|
1632
|
+
return selected ?? "default";
|
|
1633
|
+
}
|
|
1634
|
+
function useAccount(name) {
|
|
1635
|
+
if (checkAccount(name) === "default")
|
|
1636
|
+
return void rmSync(defaultFile(), { force: true });
|
|
1637
|
+
mkdirSync(configRoot(), { recursive: true, mode: 448 });
|
|
1638
|
+
writeFileSync(defaultFile(), `${name}
|
|
1639
|
+
`, { mode: 384 });
|
|
1640
|
+
}
|
|
1641
|
+
function listAccounts() {
|
|
1642
|
+
const read = (path) => {
|
|
1643
|
+
try {
|
|
1644
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
1645
|
+
} catch {
|
|
1646
|
+
return;
|
|
1647
|
+
}
|
|
1648
|
+
};
|
|
1649
|
+
const row = (name, s) => ({ name, ...s.baseUrl ? { baseUrl: s.baseUrl } : {}, signIn: s.apiKey ? "api key" : s.oauth ? "oauth" : "none" });
|
|
1650
|
+
const out = [];
|
|
1651
|
+
const main = read(join(configRoot(), "cli.json"));
|
|
1652
|
+
if (main)
|
|
1653
|
+
out.push(row("default", main));
|
|
1654
|
+
let names = [];
|
|
1655
|
+
try {
|
|
1656
|
+
names = readdirSync(join(configRoot(), "accounts")).filter((n) => ACCOUNT_NAME.test(n)).sort();
|
|
1657
|
+
} catch {}
|
|
1658
|
+
for (const n of names) {
|
|
1659
|
+
const s = read(join(configRoot(), "accounts", n, "cli.json"));
|
|
1660
|
+
if (s)
|
|
1661
|
+
out.push(row(n, s));
|
|
1662
|
+
}
|
|
1663
|
+
return out;
|
|
1664
|
+
}
|
|
1526
1665
|
function configPath() {
|
|
1527
|
-
|
|
1528
|
-
return join(root, "cli.json");
|
|
1666
|
+
return selected ? join(configRoot(), "accounts", selected, "cli.json") : join(configRoot(), "cli.json");
|
|
1529
1667
|
}
|
|
1530
1668
|
function load() {
|
|
1531
1669
|
const path = configPath();
|
|
@@ -1597,7 +1735,7 @@ async function currentToken(baseUrl, fetchImpl = fetch) {
|
|
|
1597
1735
|
return stored.apiKey;
|
|
1598
1736
|
const o = stored.oauth;
|
|
1599
1737
|
if (!o)
|
|
1600
|
-
return v1Key(baseUrl) ?? sharedSignIn(baseUrl)?.token;
|
|
1738
|
+
return selected ? undefined : v1Key(baseUrl) ?? sharedSignIn(baseUrl)?.token;
|
|
1601
1739
|
if (o.expiresAt && o.refreshToken && Date.now() > o.expiresAt - 60000) {
|
|
1602
1740
|
try {
|
|
1603
1741
|
const fresh = await tokenRequest(baseUrl, { grant_type: "refresh_token", refresh_token: o.refreshToken, client_id: o.clientId, resource: apiResource(baseUrl) }, fetchImpl);
|
|
@@ -1627,6 +1765,8 @@ function signInSource(baseUrl) {
|
|
|
1627
1765
|
return;
|
|
1628
1766
|
if (stored.apiKey || stored.oauth)
|
|
1629
1767
|
return "stored";
|
|
1768
|
+
if (selected)
|
|
1769
|
+
return;
|
|
1630
1770
|
if (v1Key(baseUrl))
|
|
1631
1771
|
return "v1";
|
|
1632
1772
|
return sharedSignIn(baseUrl) ? "shared" : undefined;
|
|
@@ -1641,6 +1781,8 @@ function describe(baseUrl) {
|
|
|
1641
1781
|
return `API key ${mask(stored.apiKey)} in ${configPath()}`;
|
|
1642
1782
|
if (stored.oauth)
|
|
1643
1783
|
return `OAuth sign-in in ${configPath()}`;
|
|
1784
|
+
if (selected)
|
|
1785
|
+
return `not signed in (account "${selected}": \`unbrowse login --account ${selected}\`)`;
|
|
1644
1786
|
const v1 = !stored.baseUrl ? v1Key(baseUrl) : undefined;
|
|
1645
1787
|
if (v1)
|
|
1646
1788
|
return `v1 API key ${mask(v1)} from ~/.unbrowse/config.json (still works; \`unbrowse login\` replaces it)`;
|
|
@@ -1786,7 +1928,7 @@ async function deviceLogin(baseUrl, opts) {
|
|
|
1786
1928
|
|
|
1787
1929
|
// src/cli.ts
|
|
1788
1930
|
init_src();
|
|
1789
|
-
var VERSION = "12.
|
|
1931
|
+
var VERSION = "12.4.0";
|
|
1790
1932
|
var DEFAULT_ORIGIN = "https://unbrowse.ai";
|
|
1791
1933
|
var HELP = `unbrowse ${VERSION} — call websites as APIs through the Unbrowse API
|
|
1792
1934
|
|
|
@@ -1794,6 +1936,8 @@ var HELP = `unbrowse ${VERSION} — call websites as APIs through the Unbrowse A
|
|
|
1794
1936
|
[--device] On a remote/headless machine, show a code to approve on your phone
|
|
1795
1937
|
(first use of any command below signs you in the same way)
|
|
1796
1938
|
logout Forget the stored sign-in
|
|
1939
|
+
accounts Sign-ins saved on this machine, one per Unbrowse account (each has its own vault)
|
|
1940
|
+
accounts use <name> Make one the default; any command takes --account NAME for just that run
|
|
1797
1941
|
whoami Workspace and usage
|
|
1798
1942
|
usage Verified calls this month and quota left
|
|
1799
1943
|
|
|
@@ -1815,6 +1959,8 @@ var HELP = `unbrowse ${VERSION} — call websites as APIs through the Unbrowse A
|
|
|
1815
1959
|
cookies list Browsers and profiles found on this machine (--json for agents)
|
|
1816
1960
|
cookies sync Send your browser cookies so runs act as your signed-in self
|
|
1817
1961
|
[--browser NAME] [--profile NAME] [--domain d] [--all]
|
|
1962
|
+
A page that says "sign in to view" (claude.ai, Notion, Google Docs…): sign in to it
|
|
1963
|
+
in your browser once, then: unbrowse cookies sync --domain <site>
|
|
1818
1964
|
cookies watch --domain a,b Keep those sites' cookies in sync (re-upload on change) [--interval MIN] [--all-sites]
|
|
1819
1965
|
cookies daemon start|status|stop
|
|
1820
1966
|
The same as a background service (systemd / launchd) [--domain a,b | --all-sites] [--yes]
|
|
@@ -1834,18 +1980,26 @@ var HELP = `unbrowse ${VERSION} — call websites as APIs through the Unbrowse A
|
|
|
1834
1980
|
call <host> <tool> [JSON] Run one site tool with its inputs (or --set key=value…); prints the run
|
|
1835
1981
|
[--deadline MS] [--select PATH,…] [--idempotency-key K] [--end-user ID]
|
|
1836
1982
|
|
|
1837
|
-
|
|
1983
|
+
tools [query | name] Every tool of the hosted MCP (cloud browser, map, replay, memory, your sites' tools…);
|
|
1984
|
+
one name prints its description and inputs (--json: every schema)
|
|
1985
|
+
tool <name> [JSON] Call one, the same as an agent over MCP: unbrowse tool browse_open --url https://…
|
|
1986
|
+
[--key value]… [--set key=value]… [--input JSON]
|
|
1987
|
+
the unbrowse_ prefix is optional; every --flag the CLI does not use is an input
|
|
1988
|
+
[--from-here] run sends its site requests from this machine, like \`unbrowse run\`
|
|
1989
|
+
|
|
1990
|
+
mcp Local stdio MCP server proxying the hosted one (tool names use _ not .)
|
|
1838
1991
|
[--url URL] [--end-user ID] for agent hosts that need stdio or strict tool names (Grok Build)
|
|
1839
|
-
[--from-here] unbrowse_run sends its site requests from this machine, like run (a Grok Bot's computer)
|
|
1992
|
+
[--from-here] unbrowse_run sends its site requests from this machine, like run (a Grok Bot's computer).
|
|
1993
|
+
It changes the IP only: it does not sign you in to a site (that is cookies sync)
|
|
1840
1994
|
mcp add [claude] [codex] Register that proxy with Claude Code / Codex: they use this CLI's sign-in (no second sign-in)
|
|
1841
1995
|
[--name NAME] [--replace] [--dry-run]
|
|
1842
1996
|
And back: no CLI sign-in, but Claude Code signed in to Unbrowse (plugin) → the CLI uses that
|
|
1843
1997
|
|
|
1844
|
-
Options: --json (errors as JSON) · --base-url URL · --no-open
|
|
1845
|
-
Env: UNBROWSE_API_KEY, UNBROWSE_BASE_URL (default ${DEFAULT_ORIGIN}), UNBROWSE_EGRESS=server, UNBROWSE_MCP_URL, UNBROWSE_END_USER
|
|
1998
|
+
Options: --json (errors as JSON) · --base-url URL · --no-open · --account NAME
|
|
1999
|
+
Env: UNBROWSE_API_KEY, UNBROWSE_ACCOUNT, UNBROWSE_BASE_URL (default ${DEFAULT_ORIGIN}), UNBROWSE_EGRESS=server, UNBROWSE_MCP_URL, UNBROWSE_END_USER
|
|
1846
2000
|
Exit: 0 ok · 1 error · 2 input required · 3 sign-in or login needed · 4 not verified`;
|
|
1847
2001
|
var BOOLEAN = new Set(["json", "help", "version", "no-open", "no-wait", "unattended", "from-here", "from-unbrowse", "all", "all-sites", "yes", "full", "events", "dry-run", "with-keys", "replace"]);
|
|
1848
|
-
var NEEDS_ACCOUNT = new Set(["whoami", "usage", "discover", "run", "scrape", "index", "inspect", "resume", "cancel", "learn", "learned", "logins", "call", "cookies"]);
|
|
2002
|
+
var NEEDS_ACCOUNT = new Set(["whoami", "usage", "discover", "run", "scrape", "index", "inspect", "resume", "cancel", "learn", "learned", "logins", "call", "cookies", "tools", "tool"]);
|
|
1849
2003
|
var SCRAPE_FORMATS = ["markdown", "html", "text", "links", "raw"];
|
|
1850
2004
|
function parseArgs(argv) {
|
|
1851
2005
|
const out = { _: [], flags: {}, sets: {} };
|
|
@@ -1903,6 +2057,15 @@ async function main(argv, io = stdio) {
|
|
|
1903
2057
|
return io.out(VERSION), 0;
|
|
1904
2058
|
if (!cmd || cmd === "help" || args.flags.help)
|
|
1905
2059
|
return io.out(HELP), 0;
|
|
2060
|
+
if (args.flags.account === true)
|
|
2061
|
+
return io.err("usage: --account NAME"), 1;
|
|
2062
|
+
try {
|
|
2063
|
+
selectAccount(typeof args.flags.account === "string" ? args.flags.account : undefined);
|
|
2064
|
+
} catch (e) {
|
|
2065
|
+
return io.err(e.message), 1;
|
|
2066
|
+
}
|
|
2067
|
+
const named = account() !== "default" ? account() : undefined;
|
|
2068
|
+
const loginCmd = `unbrowse login${named ? ` --account ${named}` : ""}`;
|
|
1906
2069
|
const origin = String(args.flags["base-url"] ?? process.env.UNBROWSE_BASE_URL ?? load().baseUrl ?? DEFAULT_ORIGIN).replace(/\/+$/, "");
|
|
1907
2070
|
const flag = (k) => typeof args.flags[k] === "string" ? args.flags[k] : undefined;
|
|
1908
2071
|
const print = (v) => io.out(JSON.stringify(v, null, 2));
|
|
@@ -1951,7 +2114,7 @@ async function main(argv, io = stdio) {
|
|
|
1951
2114
|
io.err("This CLI runs from a temporary npx copy, which a background service cannot point at. Install it first: npm i -g unbrowse — then run this again.");
|
|
1952
2115
|
return 1;
|
|
1953
2116
|
}
|
|
1954
|
-
const plan = D.servicePlan({ os: io.service?.os, home: io.service?.home, node, cli, env: { ...origin !== DEFAULT_ORIGIN ? { UNBROWSE_BASE_URL: origin } : {}, ...process.env.UNBROWSE_CONFIG_DIR ? { UNBROWSE_CONFIG_DIR: process.env.UNBROWSE_CONFIG_DIR } : {}, ...process.env.UNBROWSE_COOKIES_HOME ? { UNBROWSE_COOKIES_HOME: process.env.UNBROWSE_COOKIES_HOME } : {} } });
|
|
2117
|
+
const plan = D.servicePlan({ os: io.service?.os, home: io.service?.home, node, cli, account: named, env: { ...origin !== DEFAULT_ORIGIN ? { UNBROWSE_BASE_URL: origin } : {}, ...named ? { UNBROWSE_ACCOUNT: named } : {}, ...process.env.UNBROWSE_CONFIG_DIR ? { UNBROWSE_CONFIG_DIR: process.env.UNBROWSE_CONFIG_DIR } : {}, ...process.env.UNBROWSE_COOKIES_HOME ? { UNBROWSE_COOKIES_HOME: process.env.UNBROWSE_COOKIES_HOME } : {} } });
|
|
1955
2118
|
if (!plan) {
|
|
1956
2119
|
io.err("No background service on this OS yet. Keep it running with: unbrowse cookies watch (e.g. from Task Scheduler at logon).");
|
|
1957
2120
|
return 1;
|
|
@@ -2002,7 +2165,7 @@ async function main(argv, io = stdio) {
|
|
|
2002
2165
|
};
|
|
2003
2166
|
const onboard = async () => {
|
|
2004
2167
|
if (!io.interactive || args.flags.json) {
|
|
2005
|
-
const message = `Not signed in to ${origin}. Run \`npx
|
|
2168
|
+
const message = `Not signed in to ${origin}${named ? ` as account "${named}"` : ""}. Run \`npx ${loginCmd}\` (opens your browser, nothing to paste), or set UNBROWSE_API_KEY (keys: ${origin}/app).`;
|
|
2006
2169
|
if (args.flags.json)
|
|
2007
2170
|
print({ error: { status: 401, code: "not_signed_in", message, details: null } });
|
|
2008
2171
|
else
|
|
@@ -2031,18 +2194,31 @@ Your browser opens ${origin}; approve it and \`unbrowse ${cmd}\` carries on. (Sc
|
|
|
2031
2194
|
else
|
|
2032
2195
|
await oauthLogin(origin, { open: (u) => openLink(u, args, io), log: io.err });
|
|
2033
2196
|
const me = await (await client()).me();
|
|
2034
|
-
io.err(`Signed in to ${origin} (workspace ${me.workspaceId}).`);
|
|
2197
|
+
io.err(`Signed in to ${origin}${named ? ` as account "${named}"` : ""} (workspace ${me.workspaceId}).`);
|
|
2035
2198
|
await offerCookieSync().catch((e) => io.err(`Cookie sync not set up: ${e.message}`));
|
|
2036
2199
|
return 0;
|
|
2037
2200
|
}
|
|
2201
|
+
case "accounts": {
|
|
2202
|
+
if (rest[0] === "use") {
|
|
2203
|
+
const name = checkAccount(need(rest[1], "accounts use <name>"));
|
|
2204
|
+
useAccount(name);
|
|
2205
|
+
const saved = listAccounts().some((a) => a.name === name);
|
|
2206
|
+
io.err(`Commands now use account "${name}"${saved ? "" : ` (not signed in yet: unbrowse login${name === "default" ? "" : ` --account ${name}`})`}.${process.env.UNBROWSE_ACCOUNT ? ` UNBROWSE_ACCOUNT=${process.env.UNBROWSE_ACCOUNT} still wins while it is set.` : ""}`);
|
|
2207
|
+
return 0;
|
|
2208
|
+
}
|
|
2209
|
+
if (rest[0])
|
|
2210
|
+
throw new UsageError("usage: unbrowse accounts [use <name>]");
|
|
2211
|
+
print({ current: account(), accounts: listAccounts() });
|
|
2212
|
+
return 0;
|
|
2213
|
+
}
|
|
2038
2214
|
case "mcp":
|
|
2039
|
-
return await mcpAdd(rest.slice(1), args, io, origin);
|
|
2215
|
+
return await mcpAdd(rest.slice(1), args, io, origin, named);
|
|
2040
2216
|
case "logout":
|
|
2041
2217
|
clear();
|
|
2042
|
-
io.err("Signed out.");
|
|
2218
|
+
io.err(named ? `Signed out of account "${named}".` : "Signed out.");
|
|
2043
2219
|
return 0;
|
|
2044
2220
|
case "whoami":
|
|
2045
|
-
print({ origin, auth: describe(origin), ...await ub.me() });
|
|
2221
|
+
print({ origin, account: account(), auth: describe(origin), ...await ub.me() });
|
|
2046
2222
|
return 0;
|
|
2047
2223
|
case "usage":
|
|
2048
2224
|
print(await ub.usage());
|
|
@@ -2066,6 +2242,8 @@ Your browser opens ${origin}; approve it and \`unbrowse ${cmd}\` carries on. (Sc
|
|
|
2066
2242
|
};
|
|
2067
2243
|
const fromHere = !args.flags["from-unbrowse"] && !args.flags["no-wait"] && process.env.UNBROWSE_EGRESS !== "server";
|
|
2068
2244
|
const view = fromHere ? await ub.runOnClient(request, { onRequest: (r) => void io.err(`→ ${r.method} ${r.url}`) }) : await ub.run(request);
|
|
2245
|
+
if (fromHere && view.egress?.mode === "server")
|
|
2246
|
+
io.err("Ran from Unbrowse, not this machine: this site uses your saved sign-in, which never leaves your vault.");
|
|
2069
2247
|
return settle(ub, view, args, io, print);
|
|
2070
2248
|
}
|
|
2071
2249
|
case "scrape": {
|
|
@@ -2180,7 +2358,7 @@ Your browser opens ${origin}; approve it and \`unbrowse ${cmd}\` carries on. (Sc
|
|
|
2180
2358
|
if (sub === "daemon") {
|
|
2181
2359
|
const action = rest[1] ?? "status";
|
|
2182
2360
|
const { D, dir, saved, config } = await cookieSync();
|
|
2183
|
-
const plan = D.servicePlan({ os: io.service?.os, home: io.service?.home, node: io.service?.node ?? process.execPath, cli: io.service?.cli ?? process.argv[1] ?? "" });
|
|
2361
|
+
const plan = D.servicePlan({ os: io.service?.os, home: io.service?.home, node: io.service?.node ?? process.execPath, cli: io.service?.cli ?? process.argv[1] ?? "", account: named });
|
|
2184
2362
|
const exec = io.exec ?? (() => ({ status: 1, stdout: "", stderr: "no exec" }));
|
|
2185
2363
|
if (action === "start" || action === "install")
|
|
2186
2364
|
return await installCookieDaemon(config(), !!args.flags.yes);
|
|
@@ -2326,6 +2504,9 @@ Your browser opens ${origin}; approve it and \`unbrowse ${cmd}\` carries on. (Sc
|
|
|
2326
2504
|
return io.err("Outcome unknown: a change may have happened. Inspect before retrying."), 4;
|
|
2327
2505
|
return io.err(`${run.status}${run.error ? `: ${run.error.code} — ${run.error.message}` : ""}`), 1;
|
|
2328
2506
|
}
|
|
2507
|
+
case "tools":
|
|
2508
|
+
case "tool":
|
|
2509
|
+
return await mcpTools(cmd, rest, args, io, origin, print, client);
|
|
2329
2510
|
default:
|
|
2330
2511
|
throw new UsageError(`unknown command "${cmd}". Run \`unbrowse help\`.`);
|
|
2331
2512
|
}
|
|
@@ -2341,10 +2522,13 @@ Your browser opens ${origin}; approve it and \`unbrowse ${cmd}\` carries on. (Sc
|
|
|
2341
2522
|
const cause = e.cause;
|
|
2342
2523
|
e.message = `could not reach ${origin}${cause?.code ? ` (${cause.code})` : cause?.message ? ` (${cause.message})` : ""}. Check the connection, or --base-url / UNBROWSE_BASE_URL.`;
|
|
2343
2524
|
}
|
|
2525
|
+
const siteSignIn = e.status === 401 && siteLoginRequired(e.body);
|
|
2344
2526
|
if (args.flags.json)
|
|
2345
2527
|
print({ error: { status: e.status ?? null, message: e.message, details: e.body ?? null } });
|
|
2528
|
+
else if (siteSignIn)
|
|
2529
|
+
io.err(siteSignInHint(siteSignIn, e.message));
|
|
2346
2530
|
else
|
|
2347
|
-
io.err(e.status === 401 ? `Not signed in to ${origin}. Run
|
|
2531
|
+
io.err(e.status === 401 ? `Not signed in to ${origin}${named ? ` as account "${named}"` : ""}. Run \`${loginCmd}\`, or set UNBROWSE_API_KEY.` : `error${e.status ? ` (HTTP ${e.status})` : ""}: ${e.message}`);
|
|
2348
2532
|
return e.status === 401 ? 3 : 1;
|
|
2349
2533
|
}
|
|
2350
2534
|
}
|
|
@@ -2379,12 +2563,12 @@ async function settle(ub, view, args, io, print) {
|
|
|
2379
2563
|
io.err(`${view.status}: ${view.error.code}${view.error.message ? ` — ${view.error.message}` : ""}`);
|
|
2380
2564
|
return 1;
|
|
2381
2565
|
}
|
|
2382
|
-
async function mcpAdd(rest, args, io, origin) {
|
|
2566
|
+
async function mcpAdd(rest, args, io, origin, account) {
|
|
2383
2567
|
const HOSTS = ["claude", "codex"];
|
|
2384
2568
|
const unknown = rest.filter((h) => !HOSTS.includes(h));
|
|
2385
2569
|
if (unknown.length)
|
|
2386
2570
|
throw new UsageError(`usage: unbrowse mcp add [claude] [codex] [--name NAME] [--replace] [--dry-run] (unknown: ${unknown.join(", ")})`);
|
|
2387
|
-
const name = (typeof args.flags.name === "string" ? args.flags.name : "") || "unbrowse";
|
|
2571
|
+
const name = (typeof args.flags.name === "string" ? args.flags.name : "") || (account ? `unbrowse-${account}` : "unbrowse");
|
|
2388
2572
|
const exec = io.exec ?? (() => ({ status: 1, stdout: "", stderr: "no exec" }));
|
|
2389
2573
|
const dry = !!args.flags["dry-run"];
|
|
2390
2574
|
const replace = !!args.flags.replace;
|
|
@@ -2395,7 +2579,7 @@ async function mcpAdd(rest, args, io, origin) {
|
|
|
2395
2579
|
try {
|
|
2396
2580
|
cli = realpathSync(cli);
|
|
2397
2581
|
} catch {}
|
|
2398
|
-
const launch = [...io.service?.cli || !ephemeralCli(cli) ? [io.service?.node ?? process.execPath, cli] : shell(["npx", "-y", "unbrowse"]), "mcp", ...origin !== DEFAULT_ORIGIN ? ["--base-url", origin] : []];
|
|
2582
|
+
const launch = [...io.service?.cli || !ephemeralCli(cli) ? [io.service?.node ?? process.execPath, cli] : shell(["npx", "-y", "unbrowse"]), "mcp", ...origin !== DEFAULT_ORIGIN ? ["--base-url", origin] : [], ...account ? ["--account", account] : []];
|
|
2399
2583
|
const env = ["UNBROWSE_CONFIG_DIR", "XDG_CONFIG_HOME", "CLAUDE_CONFIG_DIR", "UNBROWSE_SHARED_SIGNIN"].flatMap((k) => process.env[k] ? [`${k}=${process.env[k]}`] : []);
|
|
2400
2584
|
const plans = {
|
|
2401
2585
|
claude: { get: ["claude", "mcp", "get", name], remove: ["claude", "mcp", "remove", name, "--scope", "user"], add: ["claude", "mcp", "add", "--scope", "user", name, ...env.flatMap((e) => ["-e", e]), "--", ...launch] },
|
|
@@ -2523,6 +2707,95 @@ ${unique(servers.filter((s) => !s.self)).length} to import: unbrowse mcps import
|
|
|
2523
2707
|
}
|
|
2524
2708
|
return rows.some((r) => r.status === "failed") ? 1 : 0;
|
|
2525
2709
|
}
|
|
2710
|
+
async function mcpTools(cmd, rest, args, io, origin, print, client) {
|
|
2711
|
+
const T = await Promise.resolve().then(() => (init_mcp_tools(), {}));
|
|
2712
|
+
await Promise.resolve().then(() => init_mcp_proxy());
|
|
2713
|
+
await Promise.resolve().then(() => init_src());
|
|
2714
|
+
const url = process.env.UNBROWSE_MCP_URL ?? `${origin}/api/mcp`;
|
|
2715
|
+
const endUser = process.env.UNBROWSE_END_USER;
|
|
2716
|
+
const handle = createProxy(async () => new UnbrowseMcp({ url, apiKey: await tokenFor(origin, url) ?? "", endUser, client: `cli/${VERSION}` }), VERSION, args.flags["from-here"] ? { runLocal: localRun(client) } : {});
|
|
2717
|
+
let id = 0;
|
|
2718
|
+
const rpc = async (method, params) => {
|
|
2719
|
+
const res = await handle({ jsonrpc: "2.0", id: ++id, method, ...params ? { params } : {} });
|
|
2720
|
+
if (res.error)
|
|
2721
|
+
throw Object.assign(new Error(res.error.message), { status: res.error.data?.code === "unauthorized" || /unbrowse login/.test(res.error.message) ? 401 : undefined, code: res.error.data?.code });
|
|
2722
|
+
return res.result;
|
|
2723
|
+
};
|
|
2724
|
+
const { tools } = await rpc("tools/list");
|
|
2725
|
+
if (cmd === "tools") {
|
|
2726
|
+
const query = rest.join(" ");
|
|
2727
|
+
const one = rest.length === 1 ? resolveTool(tools, rest[0]) : undefined;
|
|
2728
|
+
if (one) {
|
|
2729
|
+
if (args.flags.json)
|
|
2730
|
+
return print(one), 0;
|
|
2731
|
+
io.out(`${one.name}
|
|
2732
|
+
|
|
2733
|
+
${one.description ?? ""}
|
|
2734
|
+
|
|
2735
|
+
Inputs: ${JSON.stringify(one.inputSchema ?? {}, null, 2)}`);
|
|
2736
|
+
return io.err(`Call it: unbrowse tool ${shortName(one.name)} --key value…`), 0;
|
|
2737
|
+
}
|
|
2738
|
+
const shown = query ? filterTools(tools, query) : tools;
|
|
2739
|
+
if (args.flags.json)
|
|
2740
|
+
return print({ tools: shown }), 0;
|
|
2741
|
+
if (!shown.length)
|
|
2742
|
+
return io.err(`No tool matches "${query}". All of them: unbrowse tools`), 1;
|
|
2743
|
+
for (const t of shown)
|
|
2744
|
+
io.out(`${signature(t).padEnd(44)} ${summary(t.description)}`);
|
|
2745
|
+
io.err(`
|
|
2746
|
+
${shown.length} tool(s). One in full: unbrowse tools <name> · call one: unbrowse tool <name> --key value…`);
|
|
2747
|
+
return 0;
|
|
2748
|
+
}
|
|
2749
|
+
const typed = need(rest[0], "tool <name> [JSON] [--key value]… (list them: unbrowse tools)");
|
|
2750
|
+
const tool = resolveTool(tools, typed);
|
|
2751
|
+
if (!tool) {
|
|
2752
|
+
const words = typed.toLowerCase().split(/[._\s-]+/).filter((w) => w && w !== "unbrowse");
|
|
2753
|
+
const near = tools.filter((t) => words.some((w) => shortName(t.name).toLowerCase().includes(w))).slice(0, 5).map((t) => shortName(t.name));
|
|
2754
|
+
throw new UsageError(`no MCP tool named "${typed}".${near.length ? ` Did you mean: ${near.join(", ")}?` : ""} List them: unbrowse tools`);
|
|
2755
|
+
}
|
|
2756
|
+
const input = toolInput(tool, rest[1] ? rest.slice(1).join(" ") : typeof args.flags.input === "string" ? args.flags.input : undefined, args.flags, mapValues(args.sets), coerce);
|
|
2757
|
+
const result = await rpc("tools/call", { name: tool.name, arguments: input });
|
|
2758
|
+
const value = resultValue(result);
|
|
2759
|
+
const code = resultCode(result, value);
|
|
2760
|
+
if (code === 1)
|
|
2761
|
+
io.err(typeof value === "string" ? value : JSON.stringify(value, null, 2));
|
|
2762
|
+
else if (typeof value === "string")
|
|
2763
|
+
io.out(value);
|
|
2764
|
+
else
|
|
2765
|
+
print(value);
|
|
2766
|
+
if (code === 2) {
|
|
2767
|
+
const runId = value.runId;
|
|
2768
|
+
io.err(`Input required: ${runId ? `unbrowse resume ${runId} key=value…` : "answer it with unbrowse tool resume"}`);
|
|
2769
|
+
}
|
|
2770
|
+
return code;
|
|
2771
|
+
}
|
|
2772
|
+
function siteLoginRequired(body) {
|
|
2773
|
+
const err = body?.error;
|
|
2774
|
+
if (err?.code !== "login_required")
|
|
2775
|
+
return;
|
|
2776
|
+
const d = err.details ?? {};
|
|
2777
|
+
let site = typeof d.site === "string" ? d.site : undefined;
|
|
2778
|
+
if (!site && typeof d.finalUrl === "string") {
|
|
2779
|
+
try {
|
|
2780
|
+
site = new URL(d.finalUrl).hostname.replace(/^www\./, "");
|
|
2781
|
+
} catch {}
|
|
2782
|
+
}
|
|
2783
|
+
const next = Array.isArray(d.next) ? d.next.filter((s) => typeof s === "string") : undefined;
|
|
2784
|
+
return { site, next };
|
|
2785
|
+
}
|
|
2786
|
+
function siteSignInHint(found, message) {
|
|
2787
|
+
const site = found.site ?? "<site>";
|
|
2788
|
+
const lines = [message || `${site} shows a sign-in page.`];
|
|
2789
|
+
if (found.next?.length)
|
|
2790
|
+
lines.push(...found.next.map((s) => ` - ${s}`));
|
|
2791
|
+
else {
|
|
2792
|
+
lines.push(` Use your own browser's sign-in: unbrowse cookies sync --domain ${site}`);
|
|
2793
|
+
lines.push(` Keep it fresh: unbrowse cookies daemon start --domain ${site}`);
|
|
2794
|
+
}
|
|
2795
|
+
lines.push(" (--from-here / --from-unbrowse change only where requests come from; neither signs you in.)");
|
|
2796
|
+
return lines.join(`
|
|
2797
|
+
`);
|
|
2798
|
+
}
|
|
2526
2799
|
function noCapabilityHint(view, args) {
|
|
2527
2800
|
const result = view.result ?? {};
|
|
2528
2801
|
const task = args._.slice(1).join(" ");
|
|
@@ -2531,6 +2804,7 @@ function noCapabilityHint(view, args) {
|
|
|
2531
2804
|
if (url) {
|
|
2532
2805
|
lines.push(` Read the page now: unbrowse scrape ${url}`);
|
|
2533
2806
|
lines.push(` Teach Unbrowse the site: unbrowse index ${url} (minutes; then run the task again)`);
|
|
2807
|
+
lines.push(` Do it in a cloud browser: unbrowse tool browse_open --url ${url} (recorded, so it teaches the site too)`);
|
|
2534
2808
|
} else {
|
|
2535
2809
|
lines.push(` Name the site: unbrowse run ${JSON.stringify(task || "<task>")} --url https://…`);
|
|
2536
2810
|
lines.push(" Read any page now: unbrowse scrape https://…");
|
|
@@ -2577,5 +2851,7 @@ export {
|
|
|
2577
2851
|
DEFAULT_ORIGIN,
|
|
2578
2852
|
main,
|
|
2579
2853
|
noCapabilityHint,
|
|
2580
|
-
parseArgs
|
|
2854
|
+
parseArgs,
|
|
2855
|
+
siteLoginRequired,
|
|
2856
|
+
siteSignInHint
|
|
2581
2857
|
};
|
package/package.json
CHANGED
|
@@ -1,8 +1,24 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "unbrowse",
|
|
3
|
-
"version": "12.
|
|
3
|
+
"version": "12.4.0",
|
|
4
4
|
"mcpName": "io.github.unbrowse-ai/unbrowse",
|
|
5
5
|
"description": "Call websites as APIs: a thin CLI over the Unbrowse API.",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"unbrowse",
|
|
8
|
+
"cli",
|
|
9
|
+
"mcp",
|
|
10
|
+
"browser",
|
|
11
|
+
"web-scraping",
|
|
12
|
+
"scraper",
|
|
13
|
+
"api",
|
|
14
|
+
"agent",
|
|
15
|
+
"ai-agent",
|
|
16
|
+
"claude",
|
|
17
|
+
"llm",
|
|
18
|
+
"automation",
|
|
19
|
+
"headless-browser",
|
|
20
|
+
"website-to-api"
|
|
21
|
+
],
|
|
6
22
|
"license": "MIT",
|
|
7
23
|
"type": "module",
|
|
8
24
|
"homepage": "https://unbrowse.ai",
|
package/references/tools.json
CHANGED
|
@@ -251,7 +251,8 @@
|
|
|
251
251
|
"type": "string"
|
|
252
252
|
},
|
|
253
253
|
"value": {
|
|
254
|
-
"type": "string"
|
|
254
|
+
"type": "string",
|
|
255
|
+
"description": "fill: the text to type. select: the option, by its value or label as the snapshot's `options` list it (any case; a unique part of a label also works)"
|
|
255
256
|
},
|
|
256
257
|
"name": {
|
|
257
258
|
"type": "string"
|