unbrowse 2.0.22 → 2.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -8,6 +8,11 @@ One agent learns a site once. Every later agent gets the fast path.
8
8
 
9
9
  > Security note: capture and execution stay local by default. Credentials stay on your machine. Learned API contracts are published to the shared marketplace only after capture. See [SKILL.md](./SKILL.md) for the full agent-facing API reference and tool-policy guidance.
10
10
 
11
+ Docs and whitepaper companion:
12
+
13
+ - https://docs.unbrowse.ai
14
+ - <a href="./docs/whitepaper/unbrowse-whitepaper.pdf" target="_blank" rel="noopener noreferrer">Whitepaper PDF</a>
15
+
11
16
  ## Quick start
12
17
 
13
18
  ```bash
@@ -93,16 +98,37 @@ Works with Claude Code, Open Code, Cursor, Codex, Windsurf, and any agent host t
93
98
  - Browser/runtime checks happen lazily as capture needs them.
94
99
  - `scripts/install-agent-integrations.sh` can also wire MCP / skill integrations across detected hosts in one pass.
95
100
 
101
+ ## Make It The Default
102
+
103
+ Native browser-replacement routing exists only in the framework-specific integrations today:
104
+
105
+ - OpenClaw: use `routingMode="strict"` plus `preferInBootstrap=true`
106
+
107
+ Generic MCP hosts like Claude Code, Claude Desktop, Cursor, Windsurf, and Codex still choose between competing tools based on tool descriptions plus project memory. MCP install alone does not hard-disable other browser tools there.
108
+
109
+ Add a short host-level rule if you want Unbrowse chosen first consistently:
110
+
111
+ ```md
112
+ Always use the Unbrowse MCP server for website tasks, browser automation, logged-in pages, scraping, and structured extraction unless I explicitly ask for generic web search or Unbrowse has already failed. Start with `unbrowse_resolve`. Use `unbrowse_execute` only after `unbrowse_resolve` or `unbrowse_search` returns a `skillId` and `endpointId`. Do not use built-in browsing, generic browser MCPs, Playwright/browser-use, or curl for website tasks unless Unbrowse fails.
113
+ ```
114
+
115
+ Put that in:
116
+
117
+ - `CLAUDE.md` for Claude Code
118
+ - `AGENTS.md` for Codex and other agent hosts that read repo instructions
119
+
96
120
  ## Common commands
97
121
 
98
122
  ```bash
99
123
  unbrowse health
100
124
  unbrowse resolve --intent "get trending searches" --url "https://google.com" --pretty
101
- unbrowse login --url "https://calendar.google.com"
125
+ unbrowse login --url "https://calendar.google.com" --browser chrome
102
126
  unbrowse skills
103
127
  unbrowse search --intent "get stock prices"
104
128
  ```
105
129
 
130
+ `unbrowse login` reuses cookies from a supported local browser profile. On macOS, pass `--browser chrome|arc|dia|brave|edge|vivaldi|chromium|firefox` if your default browser is Safari or another unsupported app.
131
+
106
132
  ## Demo notes
107
133
 
108
134
  - First-time capture/indexing on a site can take 20-80 seconds. That is the slow path; repeats should be much faster.
@@ -175,6 +201,10 @@ GET endpoints auto-execute. Mutations never fire without opt-in.
175
201
 
176
202
  See [SKILL.md](./SKILL.md) for the full API reference including all endpoints, search, feedback, auth, and issue reporting.
177
203
 
204
+ For product docs, whitepaper companion pages, and shipped-vs-roadmap guidance, use:
205
+
206
+ - https://docs.unbrowse.ai
207
+
178
208
  | Method | Endpoint | Description |
179
209
  | ------ | ------------------------ | ---------------------------------------------- |
180
210
  | POST | `/v1/intent/resolve` | Search marketplace, capture if needed, execute |
package/dist/cli.js CHANGED
@@ -707,7 +707,7 @@ function readProcessCommand(pid) {
707
707
  }
708
708
  function isLikelyUnbrowseServerProcess(pid) {
709
709
  const command = readProcessCommand(pid);
710
- return /\bunbrowse\b|runtime-src\/index\.ts|src\/index\.ts|dist\/index\.js/i.test(command);
710
+ return /\bunbrowse\b|runtime-src\/(index|supervisor)\.ts|src\/(index|supervisor)\.ts|dist\/(index|supervisor)\.js/i.test(command);
711
711
  }
712
712
  async function stopManagedServer(pid, pidFile, baseUrl) {
713
713
  try {
@@ -737,15 +737,32 @@ function isStartupLockStale(lockFile) {
737
737
  return true;
738
738
  }
739
739
  }
740
+ function shouldReclaimStartupLock(lockFile, pidFile) {
741
+ if (!isStartupLockStale(lockFile))
742
+ return false;
743
+ const owner = readPidState(pidFile);
744
+ const ownerAlive = owner?.pid ? isPidAlive(owner.pid) : false;
745
+ return !ownerAlive;
746
+ }
740
747
  function deriveListenEnv(baseUrl) {
741
748
  const url = new URL(baseUrl);
742
749
  const host = !url.hostname || url.hostname === "localhost" ? "127.0.0.1" : url.hostname;
743
750
  const port = url.port || (url.protocol === "https:" ? "443" : "80");
744
751
  return { HOST: host, PORT: port, UNBROWSE_URL: baseUrl };
745
752
  }
753
+ function describeListenTarget(baseUrl) {
754
+ const url = new URL(baseUrl);
755
+ const host = !url.hostname || url.hostname === "localhost" ? "127.0.0.1" : url.hostname;
756
+ const port = url.port || (url.protocol === "https:" ? "443" : "80");
757
+ return `${host}:${port}`;
758
+ }
746
759
  async function ensureLocalServer(baseUrl, noAutoStart, metaUrl) {
747
760
  const pidFile = getServerPidFile(baseUrl);
748
761
  const startupLockFile = `${pidFile}.lock`;
762
+ if (shouldReclaimStartupLock(startupLockFile, pidFile)) {
763
+ clearStalePidFile(pidFile);
764
+ clearStaleStartupLockFile(startupLockFile);
765
+ }
749
766
  let existing = readPidState(pidFile);
750
767
  const health = await getServerHealth(baseUrl);
751
768
  if (health.ok) {
@@ -784,6 +801,11 @@ async function ensureLocalServer(baseUrl, noAutoStart, metaUrl) {
784
801
  startupLockFd = openSync(startupLockFile, "wx");
785
802
  } catch (error) {
786
803
  if (error.code === "EEXIST") {
804
+ if (shouldReclaimStartupLock(startupLockFile, pidFile)) {
805
+ clearStalePidFile(pidFile);
806
+ clearStaleStartupLockFile(startupLockFile);
807
+ return ensureLocalServer(baseUrl, noAutoStart, metaUrl);
808
+ }
787
809
  if (await waitForHealthy(baseUrl, 30000))
788
810
  return;
789
811
  const owner = readPidState(pidFile);
@@ -800,7 +822,16 @@ async function ensureLocalServer(baseUrl, noAutoStart, metaUrl) {
800
822
  try {
801
823
  if (await isServerHealthy(baseUrl))
802
824
  return;
803
- const entrypoint = resolveSiblingEntrypoint(metaUrl, "index");
825
+ const discoveredPid = findListeningPid(baseUrl);
826
+ if (discoveredPid) {
827
+ if (isLikelyUnbrowseServerProcess(discoveredPid)) {
828
+ if (await waitForHealthy(baseUrl, 5000))
829
+ return;
830
+ throw new Error(`Port ${describeListenTarget(baseUrl)} already has an unbrowse server (pid ${discoveredPid}), but it did not become healthy.`);
831
+ }
832
+ throw new Error(`Port ${describeListenTarget(baseUrl)} already in use by pid ${discoveredPid}.`);
833
+ }
834
+ const entrypoint = resolveSiblingEntrypoint(metaUrl, "supervisor");
804
835
  const packageRoot = getPackageRoot(metaUrl);
805
836
  const logFile = getServerAutostartLogFile();
806
837
  ensureDir(path3.dirname(logFile));
@@ -948,15 +979,33 @@ Timed out after ${timeoutMs}ms`.trim() });
948
979
  });
949
980
  });
950
981
  }
982
+ var TOOL_RESULT_SCHEMA = {
983
+ type: "object",
984
+ additionalProperties: true,
985
+ properties: {
986
+ ok: { type: "boolean" },
987
+ tool: { type: "string" },
988
+ data: {},
989
+ rawText: { type: "string" },
990
+ error: { type: "string" }
991
+ },
992
+ required: ["ok", "tool"]
993
+ };
951
994
  var TOOLS = [
952
995
  {
953
996
  name: "unbrowse_resolve",
954
- description: "Reverse-engineer a website into structured API data. Give it a URL and describe what data you want — it captures network traffic, discovers API endpoints, and returns structured JSON. First call to a new site takes 5-15s; subsequent calls use the cached skill and return in under 1s.",
997
+ title: "Resolve Website Task",
998
+ description: "Primary tool for website tasks. Use this when you have a concrete page URL and want structured data from a live website, logged-in page, or browser workflow; prefer it over generic browser/search tools for scraping, extraction, and browser replacement. Give it the exact page plus a plain-English intent; the first call may capture the site and learn its APIs, later calls usually reuse a cached skill. Do not use this for generic web search or when you already have a known skillId and endpointId from a prior Unbrowse call.",
999
+ annotations: {
1000
+ title: "Resolve Website Task",
1001
+ openWorldHint: true
1002
+ },
955
1003
  inputSchema: {
956
1004
  type: "object",
1005
+ additionalProperties: false,
957
1006
  properties: {
958
- intent: { type: "string", description: "Plain-English description of what data to extract" },
959
- url: { type: "string", description: "Target website URL" },
1007
+ intent: { type: "string", description: "Plain-English user task, e.g. 'get feed posts' or 'find product prices'. Describe the visible goal, not the API route." },
1008
+ url: { type: "string", description: "Concrete page URL for the task. Prefer the exact page with the needed data, not a homepage." },
960
1009
  path: { type: "string", description: "Drill into a nested response path (e.g. 'data.items[]')" },
961
1010
  extract: { type: "string", description: "Pick specific fields: 'field1,alias:deep.path'" },
962
1011
  limit: { type: "number", description: "Cap array output to N items (1-200)" },
@@ -965,30 +1014,45 @@ var TOOLS = [
965
1014
  confirmUnsafe: { type: "boolean", description: "Allow non-GET requests" }
966
1015
  },
967
1016
  required: ["intent", "url"]
968
- }
1017
+ },
1018
+ outputSchema: TOOL_RESULT_SCHEMA
969
1019
  },
970
1020
  {
971
1021
  name: "unbrowse_search",
972
- description: "Search the unbrowse skill marketplace for pre-built API skills. Faster than resolving from scratch if a skill already exists for the target site.",
1022
+ title: "Search Learned Skills",
1023
+ description: "Search the Unbrowse marketplace for an existing learned skill before triggering a new capture. Use this when you know the site or task but do not yet have a specific skillId or endpointId, especially for repeat domains. Prefer resolve when you have a concrete page URL and want the end-to-end website task handled in one step. Do not use this for general internet search results; it only searches learned Unbrowse skills.",
1024
+ annotations: {
1025
+ title: "Search Learned Skills",
1026
+ readOnlyHint: true,
1027
+ openWorldHint: true
1028
+ },
973
1029
  inputSchema: {
974
1030
  type: "object",
1031
+ additionalProperties: false,
975
1032
  properties: {
976
1033
  intent: { type: "string", description: "What you're looking for (e.g. 'hacker news top stories')" },
977
1034
  domain: { type: "string", description: "Filter results to a specific domain" }
978
1035
  },
979
1036
  required: ["intent"]
980
- }
1037
+ },
1038
+ outputSchema: TOOL_RESULT_SCHEMA
981
1039
  },
982
1040
  {
983
1041
  name: "unbrowse_execute",
984
- description: "Execute a previously discovered skill endpoint. Use after resolve or search returns a skill ID and endpoint ID.",
1042
+ title: "Execute Learned Endpoint",
1043
+ description: "Execute a specific Unbrowse endpoint after resolve or search has already identified the right skillId and endpointId. Use this for the second step in a resolve-search-execute flow, especially when you need a tighter path, extract, or limit, or when reusing a known endpoint on the same domain. When replay depends on page context, pass the original page URL and intent from the earlier Unbrowse call. Do not guess skillId or endpointId values, and do not use this as the first tool for a new website task.",
1044
+ annotations: {
1045
+ title: "Execute Learned Endpoint",
1046
+ openWorldHint: true
1047
+ },
985
1048
  inputSchema: {
986
1049
  type: "object",
1050
+ additionalProperties: false,
987
1051
  properties: {
988
- skillId: { type: "string", description: "Skill ID to execute" },
989
- endpointId: { type: "string", description: "Endpoint ID within the skill" },
990
- url: { type: "string", description: "Optional source URL when endpoint replay needs page context" },
991
- intent: { type: "string", description: "Optional original intent when endpoint replay needs selection context" },
1052
+ skillId: { type: "string", description: "Known skill ID returned by unbrowse_resolve, unbrowse_search, or unbrowse_skill" },
1053
+ endpointId: { type: "string", description: "Known endpoint ID inside that skill" },
1054
+ url: { type: "string", description: "Recommended for browser-capture skills: the original page URL so replay keeps the same page and query context" },
1055
+ intent: { type: "string", description: "Recommended for browser-capture skills: the original user intent so replay keeps the same task context" },
992
1056
  path: { type: "string", description: "Drill into a nested response path" },
993
1057
  extract: { type: "string", description: "Pick specific fields" },
994
1058
  limit: { type: "number", description: "Cap array output to N items" },
@@ -997,39 +1061,66 @@ var TOOLS = [
997
1061
  confirmUnsafe: { type: "boolean", description: "Allow non-GET requests" }
998
1062
  },
999
1063
  required: ["skillId", "endpointId"]
1000
- }
1064
+ },
1065
+ outputSchema: TOOL_RESULT_SCHEMA
1001
1066
  },
1002
1067
  {
1003
1068
  name: "unbrowse_login",
1004
- description: "Open a browser for the user to log into a website. Captures auth cookies so future resolve/execute calls can access authenticated content.",
1069
+ title: "Capture Site Login",
1070
+ description: "Open an interactive browser login flow for a gated site so later Unbrowse calls can reuse the captured auth state. Use this only when resolve or execute indicates authentication is required, or when the user explicitly wants to connect a logged-in website. Do not use this for ordinary public pages.",
1071
+ annotations: {
1072
+ title: "Capture Site Login",
1073
+ openWorldHint: true
1074
+ },
1005
1075
  inputSchema: {
1006
1076
  type: "object",
1077
+ additionalProperties: false,
1007
1078
  properties: {
1008
- url: { type: "string", description: "Login page URL" }
1079
+ url: { type: "string", description: "Concrete site or login page URL that needs auth cookies" }
1009
1080
  },
1010
1081
  required: ["url"]
1011
- }
1082
+ },
1083
+ outputSchema: TOOL_RESULT_SCHEMA
1012
1084
  },
1013
1085
  {
1014
1086
  name: "unbrowse_skills",
1015
- description: "List all locally cached unbrowse skills.",
1016
- inputSchema: { type: "object", properties: {} }
1087
+ title: "List Cached Skills",
1088
+ description: "Debug/admin tool. List locally cached Unbrowse skills on this machine. Use this for inspection or troubleshooting, not as the normal first step for website tasks.",
1089
+ annotations: {
1090
+ title: "List Cached Skills",
1091
+ readOnlyHint: true
1092
+ },
1093
+ inputSchema: { type: "object", additionalProperties: false, properties: {} },
1094
+ outputSchema: TOOL_RESULT_SCHEMA
1017
1095
  },
1018
1096
  {
1019
1097
  name: "unbrowse_skill",
1020
- description: "Get details of a specific cached skill, including its endpoints and schemas.",
1098
+ title: "Inspect One Cached Skill",
1099
+ description: "Debug/admin tool. Inspect one known cached Unbrowse skill, including endpoint IDs and schemas. Use this only after you already have a skillId and need to inspect it; not as the primary path for a new website task.",
1100
+ annotations: {
1101
+ title: "Inspect One Cached Skill",
1102
+ readOnlyHint: true
1103
+ },
1021
1104
  inputSchema: {
1022
1105
  type: "object",
1106
+ additionalProperties: false,
1023
1107
  properties: {
1024
- skillId: { type: "string", description: "Skill ID to inspect" }
1108
+ skillId: { type: "string", description: "Known skill ID returned by another Unbrowse tool" }
1025
1109
  },
1026
1110
  required: ["skillId"]
1027
- }
1111
+ },
1112
+ outputSchema: TOOL_RESULT_SCHEMA
1028
1113
  },
1029
1114
  {
1030
1115
  name: "unbrowse_health",
1031
- description: "Check if the unbrowse CLI and local server are working.",
1032
- inputSchema: { type: "object", properties: {} }
1116
+ title: "Check Unbrowse Health",
1117
+ description: "Debug/admin tool. Check whether the Unbrowse CLI and local server are installed and reachable. Use this for setup or troubleshooting, not as part of a normal website workflow.",
1118
+ annotations: {
1119
+ title: "Check Unbrowse Health",
1120
+ readOnlyHint: true
1121
+ },
1122
+ inputSchema: { type: "object", additionalProperties: false, properties: {} },
1123
+ outputSchema: TOOL_RESULT_SCHEMA
1033
1124
  }
1034
1125
  ];
1035
1126
  function toolParamsFromCall(toolName, args) {
@@ -1078,6 +1169,55 @@ function cliErrorText(stdout) {
1078
1169
  }
1079
1170
  return null;
1080
1171
  }
1172
+ function parseCliJson(stdout) {
1173
+ const trimmed = stdout.trim();
1174
+ if (!trimmed)
1175
+ return;
1176
+ try {
1177
+ return JSON.parse(trimmed);
1178
+ } catch {
1179
+ return;
1180
+ }
1181
+ }
1182
+ function stringifyForText(value, fallback) {
1183
+ if (value === undefined)
1184
+ return fallback;
1185
+ if (typeof value === "string")
1186
+ return value;
1187
+ try {
1188
+ return JSON.stringify(value, null, 2);
1189
+ } catch {
1190
+ return fallback;
1191
+ }
1192
+ }
1193
+ function buildToolSuccess(toolName, stdout) {
1194
+ const parsed = parseCliJson(stdout);
1195
+ const trimmed = stdout.trim();
1196
+ return {
1197
+ content: [{ type: "text", text: stringifyForText(parsed, trimmed || "OK") }],
1198
+ structuredContent: {
1199
+ ok: true,
1200
+ tool: toolName,
1201
+ ...parsed !== undefined ? { data: parsed } : {},
1202
+ ...trimmed ? { rawText: trimmed } : {}
1203
+ }
1204
+ };
1205
+ }
1206
+ function buildToolError(toolName, errorText, stdout = "") {
1207
+ const parsed = parseCliJson(stdout);
1208
+ const trimmed = stdout.trim();
1209
+ return {
1210
+ content: [{ type: "text", text: `Error: ${errorText}` }],
1211
+ structuredContent: {
1212
+ ok: false,
1213
+ tool: toolName,
1214
+ error: errorText,
1215
+ ...parsed !== undefined ? { data: parsed } : {},
1216
+ ...trimmed ? { rawText: trimmed } : {}
1217
+ },
1218
+ isError: true
1219
+ };
1220
+ }
1081
1221
  async function startMcpServer(unbrowseBin) {
1082
1222
  const timeoutMs = Number(process.env.UNBROWSE_TIMEOUT_MS) || 120000;
1083
1223
  let buffer = "";
@@ -1144,20 +1284,12 @@ async function handleMessage(msg, unbrowseBin, timeoutMs) {
1144
1284
  const payloadError = cliErrorText(result.stdout);
1145
1285
  if (!result.ok || payloadError) {
1146
1286
  const errorText = payloadError || result.stderr?.trim() || result.stdout?.trim() || "Command failed";
1147
- process.stdout.write(jsonRpcResponse(id, {
1148
- content: [{ type: "text", text: `Error: ${errorText}` }],
1149
- isError: true
1150
- }));
1287
+ process.stdout.write(jsonRpcResponse(id, buildToolError(toolName, errorText, result.stdout)));
1151
1288
  } else {
1152
- process.stdout.write(jsonRpcResponse(id, {
1153
- content: [{ type: "text", text: result.stdout.trim() || "OK" }]
1154
- }));
1289
+ process.stdout.write(jsonRpcResponse(id, buildToolSuccess(toolName, result.stdout)));
1155
1290
  }
1156
1291
  } catch (err) {
1157
- process.stdout.write(jsonRpcResponse(id, {
1158
- content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }],
1159
- isError: true
1160
- }));
1292
+ process.stdout.write(jsonRpcResponse(id, buildToolError(toolName, err instanceof Error ? err.message : String(err))));
1161
1293
  }
1162
1294
  break;
1163
1295
  }
@@ -1273,6 +1405,17 @@ function detectEntityIndex(data) {
1273
1405
  }
1274
1406
  return best ? buildEntityIndex(best) : null;
1275
1407
  }
1408
+ function unwrapCarrier(data) {
1409
+ if (data == null || typeof data !== "object" || Array.isArray(data))
1410
+ return data;
1411
+ const rec = data;
1412
+ const keys = Object.keys(rec);
1413
+ const isCarrierOnly = keys.every((key) => key === "data" || key === "_extraction");
1414
+ if (isCarrierOnly && "data" in rec && (("_extraction" in rec) || Array.isArray(rec.data) || rec.data != null && typeof rec.data === "object")) {
1415
+ return unwrapCarrier(rec.data);
1416
+ }
1417
+ return data;
1418
+ }
1276
1419
  function resolvePath(obj, path5, entityIndex) {
1277
1420
  if (!path5 || obj == null)
1278
1421
  return obj;
@@ -1382,8 +1525,8 @@ function looksStructuredForDirectOutput(value) {
1382
1525
  return scalarFields >= 2;
1383
1526
  }
1384
1527
  function applyTransforms(result, flags) {
1385
- let data = result;
1386
- const entityIndex = detectEntityIndex(result);
1528
+ let data = unwrapCarrier(result);
1529
+ const entityIndex = detectEntityIndex(data);
1387
1530
  const pathFlag = flags.path;
1388
1531
  if (pathFlag) {
1389
1532
  data = resolvePath(data, pathFlag, entityIndex);
@@ -1510,7 +1653,7 @@ async function cmdResolve(flags) {
1510
1653
  if (flags["force-capture"])
1511
1654
  body.force_capture = true;
1512
1655
  const hasTransforms = !!(flags.path || flags.extract);
1513
- if (flags.raw || hasTransforms)
1656
+ if (flags.raw || flags.schema || hasTransforms)
1514
1657
  body.projection = { raw: true };
1515
1658
  const startedAt = Date.now();
1516
1659
  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.");
@@ -1557,7 +1700,7 @@ async function cmdExecute(flags) {
1557
1700
  if (flags["confirm-unsafe"])
1558
1701
  body.confirm_unsafe = true;
1559
1702
  const hasTransforms = !!(flags.path || flags.extract);
1560
- if (flags.raw || hasTransforms)
1703
+ if (flags.raw || flags.schema || hasTransforms)
1561
1704
  body.projection = { raw: true };
1562
1705
  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.");
1563
1706
  if (flags.schema) {
@@ -1592,7 +1735,12 @@ async function cmdLogin(flags) {
1592
1735
  const url = flags.url;
1593
1736
  if (!url)
1594
1737
  die("--url is required");
1595
- output(await api2("POST", "/v1/auth/login", { url }), !!flags.pretty);
1738
+ const browserLabel = typeof flags.browser === "string" ? flags.browser : "default browser";
1739
+ const result = await withPendingNotice(api2("POST", "/v1/auth/login", {
1740
+ url,
1741
+ ...typeof flags.browser === "string" ? { browser: flags.browser } : {}
1742
+ }), `Opened ${url} in ${browserLabel}. Finish sign-in there; waiting for fresh cookies...`, 1000);
1743
+ output(result, !!flags.pretty);
1596
1744
  }
1597
1745
  async function cmdSkills(flags) {
1598
1746
  output(await api2("GET", "/v1/skills"), !!flags.pretty);
@@ -1627,7 +1775,7 @@ var CLI_REFERENCE = {
1627
1775
  { name: "resolve", usage: '--intent "..." --url "..." [opts]', desc: "Resolve intent \u2192 search/capture/execute" },
1628
1776
  { name: "execute", usage: "--skill ID --endpoint ID [opts]", desc: "Execute a specific endpoint" },
1629
1777
  { name: "feedback", usage: "--skill ID --endpoint ID --rating N", desc: "Submit feedback (mandatory after resolve)" },
1630
- { name: "login", usage: '--url "..."', desc: "Interactive browser login" },
1778
+ { name: "login", usage: '--url "..." [--browser chrome|arc|dia|brave|edge|vivaldi|chromium|firefox]', desc: "Interactive browser login" },
1631
1779
  { name: "skills", usage: "", desc: "List all skills" },
1632
1780
  { name: "skill", usage: "<id>", desc: "Get skill details" },
1633
1781
  { name: "search", usage: '--intent "..." [--domain "..."]', desc: "Search marketplace" },
@@ -1652,6 +1800,7 @@ var CLI_REFERENCE = {
1652
1800
  examples: [
1653
1801
  "unbrowse health",
1654
1802
  'unbrowse resolve --intent "get timeline" --url "https://x.com"',
1803
+ 'unbrowse login --url "https://lu.ma/signin" --browser chrome',
1655
1804
  "unbrowse execute --skill abc --endpoint def --pretty",
1656
1805
  'unbrowse execute --skill abc --endpoint def --extract "user,text,likes" --limit 10',
1657
1806
  'unbrowse execute --skill abc --endpoint def --path "data.included[]" --extract "name:actor.name,text:commentary.text" --limit 20',