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 +31 -1
- package/dist/cli.js +190 -41
- package/dist/index.js +16404 -18
- package/dist/supervisor.js +230 -0
- package/package.json +1 -1
- package/runtime-src/api/routes.ts +4 -3
- package/runtime-src/auth/browser-cookies.ts +55 -27
- package/runtime-src/auth/index.ts +239 -18
- package/runtime-src/capture/form-submit.ts +332 -0
- package/runtime-src/capture/index.ts +260 -106
- package/runtime-src/capture/interaction.ts +128 -0
- package/runtime-src/cli.ts +33 -8
- package/runtime-src/client/index.ts +40 -3
- package/runtime-src/execution/index.ts +367 -104
- package/runtime-src/graph/index.ts +29 -3
- package/runtime-src/kuri/client.ts +80 -0
- package/runtime-src/mcp.ts +153 -29
- package/runtime-src/orchestrator/index.ts +335 -47
- package/runtime-src/reverse-engineer/index.ts +85 -6
- package/runtime-src/runtime/local-server.ts +39 -2
- package/runtime-src/supervisor.ts +182 -0
- package/runtime-src/template-params.ts +156 -0
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
|
|
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
|
-
|
|
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
|
|
959
|
-
url: { type: "string", description: "
|
|
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
|
-
|
|
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
|
-
|
|
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: "
|
|
989
|
-
endpointId: { type: "string", description: "
|
|
990
|
-
url: { type: "string", description: "
|
|
991
|
-
intent: { type: "string", description: "
|
|
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
|
-
|
|
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: "
|
|
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
|
-
|
|
1016
|
-
|
|
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
|
-
|
|
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: "
|
|
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
|
-
|
|
1032
|
-
|
|
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(
|
|
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
|
-
|
|
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',
|