@tavily/cli 0.0.0-stage → 1.0.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.
@@ -0,0 +1,338 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ getApiBaseUrl,
4
+ getHumanId,
5
+ isOauthToken
6
+ } from "./chunk-AERAIFZU.js";
7
+ import {
8
+ isObject,
9
+ messageOf
10
+ } from "./chunk-5JXZZSLS.js";
11
+
12
+ // src/core/mcp-client.ts
13
+ import { TavilyRequestError } from "@tavily/core";
14
+
15
+ // src/core/sse.ts
16
+ async function* sseDataLines(chunks) {
17
+ const decoder = new TextDecoder("utf-8");
18
+ let buffer = "";
19
+ for await (const chunk of chunks) {
20
+ buffer += typeof chunk === "string" ? chunk : decoder.decode(chunk, { stream: true });
21
+ const lines = buffer.split("\n");
22
+ buffer = lines.pop() ?? "";
23
+ for (const line of lines) {
24
+ const payload2 = dataPayload(line);
25
+ if (payload2 !== void 0) yield payload2;
26
+ }
27
+ }
28
+ buffer += decoder.decode();
29
+ const payload = dataPayload(buffer);
30
+ if (payload !== void 0) yield payload;
31
+ }
32
+ function dataPayload(rawLine) {
33
+ const line = rawLine.endsWith("\r") ? rawLine.slice(0, -1) : rawLine;
34
+ if (!line.startsWith("data:")) return void 0;
35
+ const rest = line.slice(5);
36
+ return rest.startsWith(" ") ? rest.slice(1) : rest;
37
+ }
38
+
39
+ // src/core/mcp-client.ts
40
+ var MCP_URL = "https://mcp.tavily.com/mcp";
41
+ var TIMEOUT_MS = 18e4;
42
+ function raiseIfApiError(parsed, isError) {
43
+ if (!isObject(parsed) || !("error" in parsed)) return;
44
+ if (!isError && typeof parsed.status === "string") return;
45
+ const detail = parsed.detail;
46
+ const message = isObject(detail) && typeof detail.error === "string" ? detail.error : String(parsed.error);
47
+ throw new TavilyRequestError({
48
+ status: typeof parsed.status === "number" ? parsed.status : 0,
49
+ body: parsed,
50
+ message
51
+ });
52
+ }
53
+ function rpcError(error) {
54
+ return new Error(
55
+ isObject(error) && typeof error.message === "string" ? error.message : JSON.stringify(error)
56
+ );
57
+ }
58
+ function parseJson(text) {
59
+ try {
60
+ return JSON.parse(text);
61
+ } catch {
62
+ return void 0;
63
+ }
64
+ }
65
+ function unwrapResult(data) {
66
+ const result = isObject(data.result) ? data.result : {};
67
+ const isError = result.isError === true;
68
+ const structured = result.structuredContent;
69
+ if (structured && (typeof structured === "object" || typeof structured === "string")) {
70
+ const parsed = typeof structured === "string" ? parseJson(structured) : structured;
71
+ if (parsed === void 0) {
72
+ throw new TavilyRequestError({
73
+ status: 0,
74
+ body: structured,
75
+ message: "The Tavily MCP server returned malformed structuredContent (not valid JSON)."
76
+ });
77
+ }
78
+ raiseIfApiError(parsed, isError);
79
+ return parsed;
80
+ }
81
+ const content = result.content;
82
+ if (Array.isArray(content) && content.length > 0) {
83
+ const first = content[0];
84
+ const textVal = isObject(first) && typeof first.text === "string" ? first.text : "";
85
+ const parsed = parseJson(textVal);
86
+ if (parsed === void 0) return { raw: textVal };
87
+ raiseIfApiError(parsed, isError);
88
+ return parsed;
89
+ }
90
+ return result;
91
+ }
92
+ function createMcpClient(opts) {
93
+ const fetchImpl = opts.fetchImpl ?? globalThis.fetch;
94
+ const headers = {
95
+ Authorization: `Bearer ${opts.token}`,
96
+ "Content-Type": "application/json",
97
+ Accept: "application/json, text/event-stream",
98
+ "x-client-source": "tavily-cli",
99
+ ...opts.clientName ? { "x-client-name": opts.clientName } : {},
100
+ // mcp-session-id stops the remote MCP from generating its own session; X-Session-Id is
101
+ // forwarded to the Tavily API for session attribution.
102
+ "mcp-session-id": opts.sessionId,
103
+ "X-Session-Id": opts.sessionId,
104
+ ...opts.humanId ? { "X-Human-Id": opts.humanId } : {}
105
+ };
106
+ async function call(toolName, args) {
107
+ const requestBody = {
108
+ jsonrpc: "2.0",
109
+ id: 1,
110
+ method: "tools/call",
111
+ params: { name: toolName, arguments: args }
112
+ };
113
+ let status;
114
+ let text;
115
+ try {
116
+ const resp = await fetchImpl(MCP_URL, {
117
+ method: "POST",
118
+ headers,
119
+ body: JSON.stringify(requestBody),
120
+ signal: AbortSignal.timeout(TIMEOUT_MS),
121
+ redirect: "manual"
122
+ });
123
+ status = resp.status;
124
+ text = await resp.text();
125
+ } catch (e) {
126
+ throw new Error(`Could not reach the Tavily MCP server: ${messageOf(e)}`);
127
+ }
128
+ if (status < 200 || status >= 300) {
129
+ throw new TavilyRequestError({
130
+ status,
131
+ body: text,
132
+ message: `MCP request failed with status ${status}`
133
+ });
134
+ }
135
+ const payload = text.split("\n").map(dataPayload).find((p) => p !== void 0);
136
+ const message = parseJson(payload ?? text);
137
+ if (!isObject(message)) {
138
+ if (payload === void 0) throw new Error(`Unexpected MCP response: ${text.slice(0, 500)}`);
139
+ throw new TavilyRequestError({
140
+ status: 0,
141
+ body: payload,
142
+ message: "The Tavily MCP server returned a malformed SSE message (not a JSON object)."
143
+ });
144
+ }
145
+ if ("error" in message) throw rpcError(message.error);
146
+ return unwrapResult(message);
147
+ }
148
+ return {
149
+ search: (body) => call("tavily_search", body),
150
+ extract: (body) => call("tavily_extract", body),
151
+ crawl: (body) => call("tavily_crawl", body),
152
+ map: (body) => call("tavily_map", body),
153
+ research: (body) => call("tavily_research", body),
154
+ researchStream: () => Promise.reject(
155
+ new TavilyRequestError({
156
+ status: 0,
157
+ body: null,
158
+ message: "Streaming is not available with OAuth credentials; use an API key."
159
+ })
160
+ ),
161
+ getResearch: (requestId) => call("tavily_get_research", { request_id: requestId })
162
+ };
163
+ }
164
+
165
+ // src/core/session.ts
166
+ import { randomUUID } from "crypto";
167
+ import { mkdirSync, readFileSync, writeFileSync } from "fs";
168
+ import path from "path";
169
+ function newId() {
170
+ return randomUUID().replace(/-/g, "");
171
+ }
172
+ function parsePid(text) {
173
+ const trimmed = text.trim();
174
+ return /^\d+$/.test(trimmed) ? Number(trimmed) : null;
175
+ }
176
+ function pidAlive(pid) {
177
+ try {
178
+ process.kill(pid, 0);
179
+ return true;
180
+ } catch (err) {
181
+ return err.code === "EPERM";
182
+ }
183
+ }
184
+ function grandparentPid(ppid, host, readFile = (p) => readFileSync(p, "utf8")) {
185
+ try {
186
+ if (host.platform === "linux") {
187
+ const stat = readFile(`/proc/${ppid}/stat`);
188
+ const fields = stat.slice(stat.lastIndexOf(")") + 1).trim().split(/\s+/);
189
+ return parsePid(fields[1] ?? "");
190
+ }
191
+ const [cmd, args] = host.platform === "win32" ? [
192
+ "powershell",
193
+ [
194
+ "-NoProfile",
195
+ "-Command",
196
+ `(Get-CimInstance Win32_Process -Filter 'ProcessId=${ppid}').ParentProcessId`
197
+ ]
198
+ ] : ["ps", ["-o", "ppid=", "-p", String(ppid)]];
199
+ const res = host.exec.run(cmd, args, { timeoutMs: 2e3 });
200
+ return res.ok ? parsePid(res.stdout) : null;
201
+ } catch {
202
+ return null;
203
+ }
204
+ }
205
+ function readSessions(file) {
206
+ try {
207
+ const data = JSON.parse(readFileSync(file, "utf8"));
208
+ return isObject(data) ? data : {};
209
+ } catch {
210
+ return {};
211
+ }
212
+ }
213
+ function writeSessions(configDir, file, sessions) {
214
+ mkdirSync(configDir, { recursive: true, mode: 448 });
215
+ writeFileSync(file, `${JSON.stringify(sessions, null, 2)}
216
+ `, { mode: 384 });
217
+ }
218
+ function resolveSessionId(opts) {
219
+ const file = path.join(opts.configDir, "session.json");
220
+ const sessions = readSessions(file);
221
+ const ppid = String(opts.ppid);
222
+ const byParent = sessions[ppid];
223
+ if (byParent !== void 0) return byParent;
224
+ const gppid = opts.gppid();
225
+ const byGrandparent = gppid === null ? void 0 : sessions[String(gppid)];
226
+ if (gppid !== null && byGrandparent !== void 0 && opts.pidAlive(gppid)) return byGrandparent;
227
+ const alive = {};
228
+ for (const [pid, sid] of Object.entries(sessions)) {
229
+ if (opts.pidAlive(Number(pid))) alive[pid] = sid;
230
+ }
231
+ const id = newId();
232
+ alive[ppid] = id;
233
+ if (gppid !== null) alive[String(gppid)] = id;
234
+ writeSessions(opts.configDir, file, alive);
235
+ return id;
236
+ }
237
+ var cached;
238
+ function getSessionId(host) {
239
+ if (cached === void 0) {
240
+ try {
241
+ cached = resolveSessionId({
242
+ configDir: path.join(host.home, ".tavily"),
243
+ ppid: process.ppid,
244
+ gppid: () => grandparentPid(process.ppid, host),
245
+ pidAlive
246
+ });
247
+ } catch {
248
+ cached = newId();
249
+ }
250
+ }
251
+ return cached;
252
+ }
253
+
254
+ // src/core/tavily-client.ts
255
+ import { createTransport } from "@tavily/core";
256
+ var DEFAULT_HTTP_TIMEOUT = 60;
257
+ var SEARCH_MAX_TIMEOUT = 120;
258
+ function createTavilyClient(opts) {
259
+ const transport = opts.transport ?? createTransport({
260
+ ...opts.apiKey ? { apiKey: opts.apiKey } : {},
261
+ clientSource: opts.apiKey ? "tavily-cli" : "tavily-cli-keyless",
262
+ ...opts.apiBaseUrl ? { apiBaseURL: opts.apiBaseUrl } : {},
263
+ sessionId: opts.sessionId,
264
+ ...opts.humanId ? { humanId: opts.humanId } : {},
265
+ ...opts.clientName ? { clientName: opts.clientName } : {},
266
+ ...opts.projectId ? { projectId: opts.projectId } : {},
267
+ ...opts.orgId ? { orgId: opts.orgId } : {}
268
+ });
269
+ const withTimeout = (body, defaultTimeout) => {
270
+ const timeout = typeof body.timeout === "number" ? body.timeout : defaultTimeout;
271
+ return { body: { ...body, timeout }, timeout };
272
+ };
273
+ return {
274
+ async search(body) {
275
+ const timeout = typeof body.timeout === "number" ? body.timeout : DEFAULT_HTTP_TIMEOUT;
276
+ return await transport.post("search", body, {
277
+ timeoutSeconds: Math.min(timeout, SEARCH_MAX_TIMEOUT)
278
+ });
279
+ },
280
+ async extract(body) {
281
+ const r = withTimeout(body, 30);
282
+ return await transport.post("extract", r.body, { timeoutSeconds: r.timeout });
283
+ },
284
+ async crawl(body) {
285
+ const r = withTimeout(body, 150);
286
+ return await transport.post("crawl", r.body, { timeoutSeconds: r.timeout });
287
+ },
288
+ async map(body) {
289
+ const r = withTimeout(body, 150);
290
+ return await transport.post("map", r.body, { timeoutSeconds: r.timeout });
291
+ },
292
+ async research(body) {
293
+ return await transport.post(
294
+ "research",
295
+ { stream: false, citation_format: "numbered", ...body },
296
+ {}
297
+ );
298
+ },
299
+ async researchStream(body) {
300
+ const chunks = await transport.stream(
301
+ "research",
302
+ { citation_format: "numbered", ...body, stream: true },
303
+ {}
304
+ );
305
+ return sseDataLines(chunks);
306
+ },
307
+ async getResearch(requestId) {
308
+ return await transport.get(`research/${encodeURIComponent(requestId)}`, {
309
+ timeoutSeconds: DEFAULT_HTTP_TIMEOUT
310
+ });
311
+ }
312
+ };
313
+ }
314
+
315
+ // src/core/client-factory.ts
316
+ function buildClient(key, clientName, host) {
317
+ const humanId = getHumanId(host);
318
+ const ids = {
319
+ sessionId: getSessionId(host),
320
+ ...humanId ? { humanId } : {},
321
+ ...clientName ? { clientName } : {}
322
+ };
323
+ if (key && isOauthToken(key)) {
324
+ return createMcpClient({ token: key, ...ids, fetchImpl: host.fetch });
325
+ }
326
+ const apiBaseUrl = getApiBaseUrl(host);
327
+ const { TAVILY_PROJECT: projectId, TAVILY_ORG_ID: orgId } = host.env;
328
+ return createTavilyClient({
329
+ ...ids,
330
+ apiKey: key ?? void 0,
331
+ ...apiBaseUrl ? { apiBaseUrl } : {},
332
+ ...projectId ? { projectId } : {},
333
+ ...orgId ? { orgId } : {}
334
+ });
335
+ }
336
+ export {
337
+ buildClient
338
+ };
@@ -0,0 +1,204 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ resolveJson
4
+ } from "./chunk-IRCF6F3S.js";
5
+ import {
6
+ colors,
7
+ out
8
+ } from "./chunk-E4N27QWM.js";
9
+ import {
10
+ isObject
11
+ } from "./chunk-5JXZZSLS.js";
12
+
13
+ // src/commands/defaults.ts
14
+ import { InvalidArgumentError as InvalidArgumentError2 } from "commander";
15
+
16
+ // src/core/agent-defaults.ts
17
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
18
+ import { dirname, join, resolve } from "path";
19
+ import { InvalidArgumentError } from "commander";
20
+ var CLAUDE_DENIED_TOOLS = ["WebSearch", "WebFetch"];
21
+ var CODEX_WEB_SEARCH_LINE = 'web_search = "disabled"';
22
+ var TOML_TABLE_HEADER = /^\s*\[/;
23
+ var TOML_WEB_SEARCH = /^\s*web_search\s*=/;
24
+ var TOML_WEB_SEARCH_DISABLED = /^\s*web_search\s*=\s*["']disabled["']\s*(#.*)?$/;
25
+ function expandUser(p, home) {
26
+ return p === "~" || p.startsWith("~/") ? join(home, p.slice(1)) : p;
27
+ }
28
+ function configRoot(envVar, dirName, { home, env }) {
29
+ const raw = env[envVar];
30
+ return raw ? resolve(expandUser(raw, home)) : join(home, dirName);
31
+ }
32
+ function claudeSettingsPath(ctx) {
33
+ if (ctx.project) return join(ctx.cwd, ".claude", "settings.local.json");
34
+ return join(configRoot("CLAUDE_CONFIG_DIR", ".claude", ctx), "settings.json");
35
+ }
36
+ function codexConfigPath(ctx) {
37
+ if (ctx.project) return join(ctx.cwd, ".codex", "config.toml");
38
+ return join(configRoot("CODEX_HOME", ".codex", ctx), "config.toml");
39
+ }
40
+ function loadClaudeSettings(path) {
41
+ if (!existsSync(path)) return {};
42
+ const text = readFileSync(path, "utf8");
43
+ if (!text.trim()) return {};
44
+ let settings;
45
+ try {
46
+ settings = JSON.parse(text);
47
+ } catch (e) {
48
+ throw new InvalidArgumentError(`${path} is not valid JSON: ${e.message}`);
49
+ }
50
+ if (!isObject(settings)) throw new InvalidArgumentError(`${path} must contain a JSON object.`);
51
+ return settings;
52
+ }
53
+ function denyList(settings, path) {
54
+ settings.permissions ??= {};
55
+ const permissions = settings.permissions;
56
+ if (!isObject(permissions))
57
+ throw new InvalidArgumentError(`"permissions" in ${path} must be an object.`);
58
+ permissions.deny ??= [];
59
+ const deny = permissions.deny;
60
+ if (!Array.isArray(deny))
61
+ throw new InvalidArgumentError(`"permissions.deny" in ${path} must be an array.`);
62
+ return deny;
63
+ }
64
+ function writeClaudeSettings(path, settings) {
65
+ mkdirSync(dirname(path), { recursive: true });
66
+ writeFileSync(path, `${JSON.stringify(settings, null, 2)}
67
+ `, "utf8");
68
+ }
69
+ function setClaudeDefault(path) {
70
+ const settings = loadClaudeSettings(path);
71
+ const deny = denyList(settings, path);
72
+ const missing = CLAUDE_DENIED_TOOLS.filter((tool) => !deny.includes(tool));
73
+ if (missing.length === 0) return false;
74
+ deny.push(...missing);
75
+ writeClaudeSettings(path, settings);
76
+ return true;
77
+ }
78
+ function unsetClaudeDefault(path) {
79
+ const settings = loadClaudeSettings(path);
80
+ const permissions = settings.permissions;
81
+ if (!isObject(permissions) || !Array.isArray(permissions.deny)) return false;
82
+ const deny = permissions.deny;
83
+ const kept = deny.filter((tool) => !CLAUDE_DENIED_TOOLS.includes(tool));
84
+ if (kept.length === deny.length) return false;
85
+ if (kept.length > 0) {
86
+ permissions.deny = kept;
87
+ } else {
88
+ delete permissions.deny;
89
+ if (Object.keys(permissions).length === 0) delete settings.permissions;
90
+ }
91
+ writeClaudeSettings(path, settings);
92
+ return true;
93
+ }
94
+ var splitLines = (text) => text.match(/[^\n]*\n|[^\n]+/g) ?? [];
95
+ var stripEol = (line) => line.replace(/\r?\n$/, "");
96
+ function topLevelEnd(lines) {
97
+ const index = lines.findIndex((line) => TOML_TABLE_HEADER.test(line));
98
+ return index === -1 ? lines.length : index;
99
+ }
100
+ function setCodexDefault(path) {
101
+ const lines = splitLines(existsSync(path) ? readFileSync(path, "utf8") : "");
102
+ const end = topLevelEnd(lines);
103
+ const index = lines.slice(0, end).findIndex((line) => TOML_WEB_SEARCH.test(line));
104
+ if (index === -1) {
105
+ lines.unshift(`${CODEX_WEB_SEARCH_LINE}
106
+ `);
107
+ } else if (TOML_WEB_SEARCH_DISABLED.test(stripEol(lines[index]))) {
108
+ return false;
109
+ } else {
110
+ lines[index] = `${CODEX_WEB_SEARCH_LINE}
111
+ `;
112
+ }
113
+ mkdirSync(dirname(path), { recursive: true });
114
+ writeFileSync(path, lines.join(""), "utf8");
115
+ return true;
116
+ }
117
+ function unsetCodexDefault(path) {
118
+ if (!existsSync(path)) return false;
119
+ const lines = splitLines(readFileSync(path, "utf8"));
120
+ const end = topLevelEnd(lines);
121
+ const kept = lines.filter(
122
+ (line, index) => index >= end || !TOML_WEB_SEARCH_DISABLED.test(stripEol(line))
123
+ );
124
+ if (kept.length === lines.length) return false;
125
+ writeFileSync(path, kept.join(""), "utf8");
126
+ return true;
127
+ }
128
+
129
+ // src/commands/defaults.ts
130
+ var TARGETS = {
131
+ claude: { label: "Claude Code", path: claudeSettingsPath },
132
+ codex: { label: "Codex", path: codexConfigPath }
133
+ };
134
+ var SET = {
135
+ verb: "Make Tavily the default web tool",
136
+ actions: { claude: setClaudeDefault, codex: setCodexDefault },
137
+ changedMessage: "built-in web search disabled",
138
+ unchangedMessage: "built-in web search already disabled",
139
+ codexNote: "Codex only reads project config in projects you have marked as trusted."
140
+ };
141
+ var UNSET = {
142
+ verb: "Restore built-in web search",
143
+ actions: { claude: unsetClaudeDefault, codex: unsetCodexDefault },
144
+ changedMessage: "built-in web search restored",
145
+ unchangedMessage: "nothing to undo"
146
+ };
147
+ async function selectTargets(opts, mode, json, cmd, host) {
148
+ if (opts.claude || opts.codex) {
149
+ return ["claude", "codex"].filter((agent) => opts[agent]);
150
+ }
151
+ if (json || !host.stdinIsTTY) {
152
+ return cmd.error("error: Pass --claude and/or --codex in non-interactive runs.");
153
+ }
154
+ const selected = [];
155
+ for (const agent of ["claude", "codex"]) {
156
+ const scope = opts.project ? " in this project" : "";
157
+ if (await host.prompt.confirm(`${mode.verb} for ${TARGETS[agent].label}${scope}?`)) {
158
+ selected.push(agent);
159
+ }
160
+ }
161
+ return selected;
162
+ }
163
+ async function execute(opts, cmd, host, mode) {
164
+ const json = resolveJson(cmd);
165
+ const project = opts.project === true;
166
+ const targets = await selectTargets(opts, mode, json, cmd, host);
167
+ const results = [];
168
+ try {
169
+ for (const agent of targets) {
170
+ const { label, path: pathFor } = TARGETS[agent];
171
+ const path = pathFor({ ...host, project });
172
+ const changed = mode.actions[agent](path);
173
+ results.push({ agent, label, scope: project ? "project" : "global", path, changed });
174
+ }
175
+ } catch (e) {
176
+ if (e instanceof InvalidArgumentError2) cmd.error(`error: ${e.message}`);
177
+ throw e;
178
+ }
179
+ if (json) {
180
+ out(JSON.stringify({ ok: true, results }));
181
+ return;
182
+ }
183
+ if (results.length === 0) {
184
+ out(` ${colors.dim("Nothing selected.")}`);
185
+ return;
186
+ }
187
+ for (const r of results) {
188
+ const message = r.changed ? mode.changedMessage : mode.unchangedMessage;
189
+ const mark = r.changed ? colors.green("\u2713") : colors.dim("\u2022");
190
+ out(` ${mark} ${r.label}: ${message} ${colors.dim(`(${r.path})`)}`);
191
+ }
192
+ if (results.some((r) => r.changed)) {
193
+ out(` ${colors.dim("Restart the agent for the change to take effect.")}`);
194
+ }
195
+ if (project && mode.codexNote && results.some((r) => r.agent === "codex" && r.changed)) {
196
+ out(` ${colors.dim(mode.codexNote)}`);
197
+ }
198
+ }
199
+ var runSetDefault = (opts, cmd, host) => execute(opts, cmd, host, SET);
200
+ var runUnsetDefault = (opts, cmd, host) => execute(opts, cmd, host, UNSET);
201
+ export {
202
+ runSetDefault,
203
+ runUnsetDefault
204
+ };
@@ -0,0 +1,90 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ definedSnake,
4
+ echoPages,
5
+ emitIfRequested,
6
+ footer,
7
+ invoke,
8
+ list,
9
+ resolveOutput
10
+ } from "./chunk-KQFC5TMG.js";
11
+ import "./chunk-U3B7BNFI.js";
12
+ import "./chunk-K76OUMXM.js";
13
+ import {
14
+ domainOf,
15
+ hostOf,
16
+ isTerminal,
17
+ plain,
18
+ renderMarkdown,
19
+ sanitizeControl
20
+ } from "./chunk-QHWWSCVM.js";
21
+ import "./chunk-AERAIFZU.js";
22
+ import {
23
+ resolveJson
24
+ } from "./chunk-IRCF6F3S.js";
25
+ import {
26
+ colors,
27
+ out
28
+ } from "./chunk-E4N27QWM.js";
29
+ import "./chunk-5JXZZSLS.js";
30
+
31
+ // src/ui/render/extract.ts
32
+ async function printExtractResults(data, opts) {
33
+ if (emitIfRequested(data, opts)) return;
34
+ const results = list(data.results);
35
+ const failed = list(data.failed_results);
36
+ if (!isTerminal(process.stdout)) {
37
+ echoPages(results);
38
+ for (const f of failed) out(`Failed: ${plain(f.url)}: ${plain(f.error)}`);
39
+ return;
40
+ }
41
+ for (const r of results) {
42
+ const url = r.url ?? "";
43
+ const raw = r.raw_content ?? "";
44
+ out();
45
+ out(` ${colors.bold(colors.aqua(sanitizeControl(url)))}`);
46
+ out(
47
+ colors.dim(` ${sanitizeControl(hostOf(url))} (${raw.length.toLocaleString("en-US")} chars)`)
48
+ );
49
+ out();
50
+ out(raw ? await renderMarkdown(raw.slice(0, 3e3)) : ` ${colors.dim("No content")}`);
51
+ out();
52
+ }
53
+ if (failed.length > 0) {
54
+ out(colors.yellow("Failed extractions:"));
55
+ for (const f of failed) {
56
+ out(` ${colors.pink("x ")}${sanitizeControl(f.url)}: ${sanitizeControl(f.error)}`);
57
+ }
58
+ }
59
+ footer(results.length, `extracted, ${failed.length} failed`, data.response_time);
60
+ }
61
+
62
+ // src/commands/extract.ts
63
+ var MAX_URLS = 20;
64
+ async function run(urls, opts, cmd, host) {
65
+ const json = resolveJson(cmd);
66
+ if (urls.length > MAX_URLS) cmd.error(`error: Maximum ${MAX_URLS} URLs per request.`);
67
+ const outputFile = resolveOutput(opts.output, "extract", domainOf(urls[0] ?? ""));
68
+ const body = {
69
+ urls,
70
+ ...definedSnake(opts, [
71
+ "query",
72
+ "chunksPerSource",
73
+ "extractDepth",
74
+ "format",
75
+ "includeImages",
76
+ "timeout"
77
+ ])
78
+ };
79
+ const label = `Extracting ${urls.length} URL${urls.length > 1 ? "s" : ""}...`;
80
+ const response = await invoke(
81
+ cmd,
82
+ host,
83
+ { label, keyed: false, clientName: opts.clientName },
84
+ (client) => client.extract(body)
85
+ );
86
+ await printExtractResults(response, { json, outputFile });
87
+ }
88
+ export {
89
+ run
90
+ };