@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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Alpha AI Technologies Inc.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,323 @@
1
- # Temporary Holding Version
1
+ # Tavily CLI
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ `tvly` runs [Tavily](https://docs.tavily.com) search, extract, crawl, map, and research from the terminal, and sets up Tavily skills and the MCP server for Claude Code, Codex, and Cursor. It is built on the official JavaScript SDK, [`@tavily/core`](https://www.npmjs.com/package/@tavily/core), and the `tvly search` flags follow the SDK's parameter catalog.
4
+
5
+ - Rich output in a terminal, plain text when piped or run by an agent, and `--json` everywhere.
6
+ - Search and extract work without an account, within a fair-use rate limit.
7
+ - An interactive shell when run with no arguments.
8
+ - Self-update through the package manager that installed it.
9
+
10
+ ## Install
11
+
12
+ ```bash
13
+ npx @tavily/cli init
14
+ ```
15
+
16
+ This installs `tvly` globally, signs you in, sets up your coding agents, and verifies a live search. Requires Node.js 20.1 or later.
17
+
18
+ To install without running setup, use your package manager and run `tvly init` afterwards:
19
+
20
+ ```bash
21
+ npm install -g @tavily/cli
22
+ pnpm add -g @tavily/cli
23
+ yarn global add @tavily/cli
24
+ bun add -g @tavily/cli
25
+ ```
26
+
27
+ The shell installer does the same on macOS and Linux, and starts `tvly init` when it runs in an interactive terminal:
28
+
29
+ ```bash
30
+ curl -fsSL https://raw.githubusercontent.com/tavily-ai/cli/main/install.sh | sh
31
+ ```
32
+
33
+ ## Setup
34
+
35
+ `tvly init` walks through four steps:
36
+
37
+ 1. **Global install.** If `tvly` is not on your PATH, it is installed with the package manager that ran `init`. If the install directory is not on your PATH either, `init` offers to add it to your shell profile.
38
+ 2. **How do you want to sign in?** Browser login, paste an API key, or continue without a key. An existing credential is reused.
39
+ 3. **Which agents do you want to install to?** Every detected agent is pre-checked. `--agent` picks the agents and skips the question.
40
+ 4. **What should I install?** Tavily skills and the hosted MCP server are checked by default; the official workflows from `tavily-ai/official-workflows` are not.
41
+
42
+ It then verifies the CLI with a live search. Rerunning `tvly init` updates installed skills to the latest published version.
43
+
44
+ ```bash
45
+ tvly init # prompts for everything
46
+ tvly init --yes # accept every default
47
+ tvly init --agent codex # one agent, no agent question
48
+ tvly init --api-key tvly-YOUR_KEY # API key instead of browser login
49
+ tvly init --skip-auth # keyless setup when no credential exists
50
+ tvly init --skip-skills # sign in and verify, leave agents alone
51
+ ```
52
+
53
+ `--yes`, `--json`, and runs without a terminal ask nothing and take the defaults: every detected agent, with skills and the MCP server.
54
+
55
+ Skills are installed with the [skills.sh CLI](https://skills.sh) into `~/.agents/skills`, which Codex and Cursor read directly; Claude Code gets links in `~/.claude/skills`. The MCP server is added with `npx add-mcp`. Both need `npx` on your PATH. Restart your agent after setup.
56
+
57
+ ### Signing in
58
+
59
+ `tvly login` manages credentials without the rest of setup:
60
+
61
+ ```bash
62
+ tvly login # browser login
63
+ tvly login --no-browser # print the login URL instead of opening it
64
+ tvly login --api-key tvly-YOUR_KEY # store an API key
65
+ tvly auth # show authentication status
66
+ tvly logout # revoke tokens and clear stored credentials
67
+ ```
68
+
69
+ `TAVILY_API_KEY` in the environment takes priority over stored credentials, so use it for CI and SSH sessions. `--no-browser` still waits for a callback on `127.0.0.1`, so over SSH either forward the port or use an API key.
70
+
71
+ Browser login goes through Tavily's MCP endpoint, which rejects some search, crawl, and research options ([details](https://github.com/tavily-ai/tavily-cli/issues/24)). Use an API key for the full option set.
72
+
73
+ ## Usage
74
+
75
+ Search and extract work right after installation, with no sign-in, until a fair-use cap is reached. Crawl, map, and research require a credential.
76
+
77
+ ```bash
78
+ tvly search "latest AI trends"
79
+ tvly search "quantum computing" --depth advanced --max-results 10 --time-range week
80
+ tvly search "SEC filings for Apple" --include-domains sec.gov,reuters.com
81
+ tvly search "Rust 2024 edition" "Rust 1.85 release notes" # up to 5 queries, run in parallel
82
+
83
+ tvly extract https://example.com/article
84
+ tvly extract https://example.com https://other.com --query "pricing information"
85
+
86
+ tvly crawl https://docs.example.com --max-depth 2 --limit 100 --select-paths "/api/.*,/guides/.*"
87
+ tvly crawl https://docs.example.com --output-dir ./docs # one Markdown file per page
88
+
89
+ tvly map https://example.com --select-paths "/blog/.*" --limit 500
90
+
91
+ tvly research "Competitive landscape of AI code assistants"
92
+ tvly research "EV market analysis" --model pro --stream
93
+ tvly research "topic" --no-wait --json # returns a request_id
94
+ tvly research poll REQUEST_ID --json
95
+ ```
96
+
97
+ Running `tvly` with no arguments opens a shell where commands are typed without the `tvly` prefix.
98
+
99
+ ### Output
100
+
101
+ In a terminal, results are formatted and wrapped. When stdout is a pipe, a file, or an agent's shell, output is plain text: `search` prints each result's title, URL, and the first 600 characters of content on one line (`--max-chars` changes the length, `0` prints everything); `extract` and `crawl` print each page in full under a `# <url>` heading; `map` prints one URL per line; `research` prints the report and its sources. `--json` prints the API response unchanged.
102
+
103
+ `-o` / `--output` saves the full JSON response. With no value it writes under `.tavily/` with a generated name and prints the path; a new `.tavily/` folder gets its own `.gitignore`. `crawl --output-dir` saves each page as a Markdown file instead.
104
+
105
+ ```bash
106
+ tvly search "AI news" -o
107
+ tvly search "AI news" --output results/ai-news.json
108
+ tvly extract https://example.com --format markdown --output extracted.json
109
+ ```
110
+
111
+ ### Scripts and agents
112
+
113
+ Every command accepts `--json`. A `-` argument reads the query from stdin.
114
+
115
+ ```bash
116
+ echo "What is the latest funding for Anthropic?" | tvly search - --json
117
+ tvly init --yes --json # TAVILY_API_KEY supplied by the environment
118
+ tvly init --agent codex --skip-auth --yes --json # keyless setup
119
+ tvly --status --json # version and auth status
120
+ ```
121
+
122
+ Setup JSON includes `ok`, `mode`, `install`, `auth`, `skills`, `workflows`, `mcp`, and `verification`; on failure, `error.stage` names the failed step. Setup always runs a live search, so it needs network access.
123
+
124
+ | Exit code | Meaning |
125
+ |-----------|---------|
126
+ | 0 | Success |
127
+ | 1 | Local setup or update error |
128
+ | 2 | Invalid input or usage error |
129
+ | 3 | Authentication failure, keyless rate-limit cap, or API usage/plan limit |
130
+ | 4 | API error, setup live-search failure, or update-check error |
131
+
132
+ HTTP 401, 403, 429, and 432 exit 3 on every credential type.
133
+
134
+ ## Commands
135
+
136
+ ```
137
+ tvly
138
+ ├── (no command) Interactive shell
139
+ ├── init Install, sign in, set up agents, verify
140
+ ├── login Sign in with the browser or an API key
141
+ ├── logout Clear stored credentials
142
+ ├── auth Show authentication status
143
+ ├── search <query...> Web search
144
+ ├── extract <urls...> Extract content from URLs
145
+ ├── crawl <url> Crawl a website
146
+ ├── map <url> Discover URLs without content
147
+ ├── research <query> Deep research
148
+ │ ├── status <id> Check a research task
149
+ │ └── poll <id> Wait for a research task
150
+ ├── setup mcp Add the hosted Tavily MCP server to an agent
151
+ ├── set-default Disable built-in web search in Claude Code and Codex
152
+ ├── unset-default Restore it
153
+ └── update Check for or install a newer release
154
+ ```
155
+
156
+ ### `tvly init`
157
+
158
+ | Option | Description |
159
+ |--------|-------------|
160
+ | `--agent` | Set up `claude-code`, `codex`, or `cursor`; repeat for more than one |
161
+ | `--yes` | Accept every default without prompting |
162
+ | `--skip-auth` | Skip sign-in when no credential is configured |
163
+ | `--skip-skills` | Leave coding agents alone: no skills, workflows, or MCP server |
164
+ | `--skip-workflows` | Skip the official workflows |
165
+ | `--skip-install` | Skip the global `tvly` install |
166
+ | `--api-key` | Sign in with an API key |
167
+ | `--browser` / `--no-browser` | Open the login page or print its URL |
168
+
169
+ `--skip-auth` and `--api-key` cannot be combined.
170
+
171
+ ### `tvly setup mcp`
172
+
173
+ Adds the hosted Tavily MCP server (`https://mcp.tavily.com/mcp/`, named `tavily`) to Claude Code, Codex, and Cursor. Nothing runs locally.
174
+
175
+ ```bash
176
+ tvly setup mcp # every detected agent
177
+ tvly setup mcp --agent claude-code --agent cursor
178
+ tvly setup mcp --agent all
179
+ ```
180
+
181
+ | Option | Description |
182
+ |--------|-------------|
183
+ | `--agent` | `claude-code`, `codex`, `cursor`, or `all`; repeat for more than one |
184
+ | `--keyless` | Write the URL-only config so the agent signs in with OAuth even when `TAVILY_API_KEY` is set |
185
+
186
+ With `TAVILY_API_KEY` exported, the config references the variable (`${TAVILY_API_KEY}` for Claude Code, `${env:TAVILY_API_KEY}` for Cursor, `--bearer-token-env-var` for Codex) and the key itself is never written to disk. Without it, the agent opens a browser to sign in the first time it uses the server. A key stored by `tvly login` is not used for MCP. Agents that cannot be configured, such as Codex when `codex` is not on PATH, are skipped and listed; the command exits 1 only when nothing was configured.
187
+
188
+ ### `tvly search <query...>`
189
+
190
+ | Option | Description |
191
+ |--------|-------------|
192
+ | `--depth` | `basic`, `advanced`, `fast`, or `ultra-fast` |
193
+ | `--max-results` | Result count limit |
194
+ | `--topic` | `general`, `news`, or `finance` |
195
+ | `--time-range` | `year`, `month`, `week`, or `day` (or `y`, `m`, `w`, `d`) |
196
+ | `--start-date` / `--end-date` | Date window bounds (YYYY-MM-DD) |
197
+ | `--include-domains` / `--exclude-domains` | Domains to prefer or omit |
198
+ | `--include-domains-mode` | How `--include-domains` applies: `restrict` or `prefer` |
199
+ | `--country` | Country preference |
200
+ | `--language` | Preferred content language |
201
+ | `--filter-by-language` | Restrict results to the requested language |
202
+ | `--include-answer` | Generated answer: `basic`, `advanced`, `true`, or `false` |
203
+ | `--include-raw-content` | Page content as `markdown` or `text`, or `false` |
204
+ | `--include-images` / `--include-image-descriptions` | Images alongside results |
205
+ | `--include-favicon` | Site icons |
206
+ | `--include-published-date` | Detected publication dates |
207
+ | `--filter-by-published-date` | Drop undated results and results outside the date window |
208
+ | `--include-usage` | Credit usage details |
209
+ | `--chunks-per-source` | Content chunks per source for advanced search |
210
+ | `--auto-parameters` | Let Tavily choose search settings from the query |
211
+ | `--cache-fallback` | Allow cached content when live fetching fails |
212
+ | `--max-age-hours` | Maximum acceptable cache age |
213
+ | `--fetch-timeout` | Live-fetch time budget in seconds |
214
+ | `--days` | Lookback in days for news searches |
215
+ | `--exact-match` | Require exact matches for quoted phrases |
216
+ | `--safe-search` | Filter explicit content |
217
+ | `--max-chars` | Characters of content printed per result (default 600, `0` for all) |
218
+ | `-o` / `--output` | Save the full JSON response |
219
+ | `--client-name` | `client_name` for request attribution |
220
+
221
+ Up to 5 queries run in parallel with the same options. Each query's results print under a `=== [i/n] <query>` header; with `--json`, the output is a list in query order. A failing query reports its error without stopping the others.
222
+
223
+ ### `tvly extract <urls...>`
224
+
225
+ | Option | Description |
226
+ |--------|-------------|
227
+ | `--query` | Rerank chunks by relevance |
228
+ | `--chunks-per-source` | Chunks per source (1-5, requires `--query`) |
229
+ | `--extract-depth` | `basic` or `advanced` |
230
+ | `--format` | `markdown` or `text` |
231
+ | `--include-images` | Include image URLs |
232
+ | `--timeout` | Max wait (1-60 seconds) |
233
+ | `-o` / `--output` | Save the full JSON response |
234
+ | `--client-name` | `client_name` for request attribution |
235
+
236
+ ### `tvly crawl <url>` and `tvly map <url>`
237
+
238
+ | Option | Description |
239
+ |--------|-------------|
240
+ | `--max-depth` | Levels deep (1-5, default 1) |
241
+ | `--max-breadth` | Links per page (default 20) |
242
+ | `--limit` | Total pages or URLs (default 50) |
243
+ | `--instructions` | Natural-language guidance |
244
+ | `--select-paths` / `--exclude-paths` | Regex patterns for paths |
245
+ | `--select-domains` / `--exclude-domains` | Regex patterns for domains |
246
+ | `--allow-external` / `--no-external` | Follow or ignore links to other domains |
247
+ | `--timeout` | Max wait (10-150 seconds) |
248
+ | `-o` / `--output` | Save the full JSON response |
249
+ | `--client-name` | `client_name` for request attribution |
250
+
251
+ `crawl` only:
252
+
253
+ | Option | Description |
254
+ |--------|-------------|
255
+ | `--chunks-per-source` | Chunks per page (1-5) |
256
+ | `--extract-depth` | `basic` or `advanced` |
257
+ | `--format` | `markdown` or `text` |
258
+ | `--include-images` | Include images |
259
+ | `--output-dir` | Save each page as a Markdown file in a directory |
260
+
261
+ ### `tvly research <query>`
262
+
263
+ Streaming, structured output, and async research need an API key.
264
+
265
+ | Option | Description |
266
+ |--------|-------------|
267
+ | `--model` | `mini`, `pro`, or `auto` (default) |
268
+ | `--no-wait` | Return the request ID immediately |
269
+ | `--stream` | Stream results as they arrive |
270
+ | `--output-schema` | Path to a JSON schema for structured output |
271
+ | `--citation-format` | `numbered`, `mla`, `apa`, or `chicago` |
272
+ | `--poll-interval` | Seconds between status checks (default 10) |
273
+ | `--timeout` | Max wait in seconds (default 600) |
274
+ | `-o` / `--output` | Save the full JSON response |
275
+ | `--client-name` | `client_name` for request attribution |
276
+
277
+ `tvly research status <id>` checks a task; `tvly research poll <id>` waits for it with the same `--poll-interval`, `--timeout`, and `-o` options.
278
+
279
+ ### `tvly set-default` / `tvly unset-default`
280
+
281
+ Makes Tavily the web tool your coding agents use by disabling their built-in web search. Without flags, you are asked per agent. `unset-default` reverses the change but does not restore values you had set yourself before.
282
+
283
+ | Option | Description |
284
+ |--------|-------------|
285
+ | `--claude` | Add `WebSearch` and `WebFetch` to `permissions.deny` in `~/.claude/settings.json` (or `$CLAUDE_CONFIG_DIR`) |
286
+ | `--codex` | Set `web_search = "disabled"` in `~/.codex/config.toml` (or `$CODEX_HOME`) |
287
+ | `--project` | Apply to the current directory instead (`.claude/settings.local.json` and `.codex/config.toml`) |
288
+
289
+ ### `tvly update`
290
+
291
+ ```bash
292
+ tvly update --check # report without changing anything
293
+ tvly update # update through npm, pnpm, yarn, or bun, whichever installed it
294
+ ```
295
+
296
+ Source and direct-URL installations are reported but not modified. JSON output includes `can_update` and `blocked_reason`.
297
+
298
+ The CLI also checks for a newer release at most once a day and prints a one-line notice on stderr. It stays silent under `--json`, when stderr is not a terminal, when run through `npx`, or when `CI` or `NO_UPDATE_NOTIFIER` is set.
299
+
300
+ ## Environment variables
301
+
302
+ | Variable | Description |
303
+ |----------|-------------|
304
+ | `TAVILY_API_KEY` | API key; takes priority over stored credentials |
305
+ | `TAVILY_API_BASE_URL` | Override the API base URL (also `api_base_url` in `~/.tavily/config.json`) |
306
+ | `TAVILY_CLIENT_NAME` | Default for `--client-name` |
307
+ | `TAVILY_HUMAN_ID` | Identifier attached to every request for usage attribution (also `human_id` in the config file) |
308
+ | `TAVILY_PROJECT` | Project ID attached to requests |
309
+ | `TAVILY_ORG_ID` | Organization ID attached to requests |
310
+ | `TAVILY_HTTP_PROXY` / `TAVILY_HTTPS_PROXY` | Proxy URLs for API calls made with an API key |
311
+ | `NO_UPDATE_NOTIFIER` | Disable the daily update notice (`CI` disables it too) |
312
+
313
+ Browser login, OAuth-credentialed commands, and `tvly update` use Node's `fetch`, which ignores proxy variables unless Node 24 or later runs with `NODE_USE_ENV_PROXY=1`.
314
+
315
+ ## Related
316
+
317
+ - [Tavily CLI docs](https://docs.tavily.com/documentation/tavily-cli)
318
+ - [`@tavily/core`](https://www.npmjs.com/package/@tavily/core), the JavaScript SDK
319
+ - [Tavily API reference](https://docs.tavily.com)
320
+
321
+ ## License
322
+
323
+ MIT
@@ -0,0 +1,213 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ getApiKeyOrExit
4
+ } from "./chunk-U3B7BNFI.js";
5
+ import {
6
+ spinner
7
+ } from "./chunk-K76OUMXM.js";
8
+ import {
9
+ sanitizeControl
10
+ } from "./chunk-QHWWSCVM.js";
11
+ import {
12
+ authMethod,
13
+ clearCredentials,
14
+ configPaths,
15
+ getApiKey,
16
+ looksHeadless,
17
+ runBrowserLogin,
18
+ saveApiKey,
19
+ saveOauthSession
20
+ } from "./chunk-AERAIFZU.js";
21
+ import {
22
+ resolveJson
23
+ } from "./chunk-IRCF6F3S.js";
24
+ import {
25
+ LOGO,
26
+ colors,
27
+ err,
28
+ out
29
+ } from "./chunk-E4N27QWM.js";
30
+ import {
31
+ ExitError,
32
+ OAuthError
33
+ } from "./chunk-5JXZZSLS.js";
34
+
35
+ // src/commands/auth.ts
36
+ var ENV_CREDENTIAL_WARNING = "TAVILY_API_KEY is still set and cannot be removed from the parent environment by this process. Run: unset TAVILY_API_KEY";
37
+ async function runLogin(opts, cmd, host, hooks = {}) {
38
+ const json = resolveJson(cmd);
39
+ const configFile = configPaths(host.home).file;
40
+ if (opts.apiKey) {
41
+ await saveApiKey(opts.apiKey, host);
42
+ printLoginSuccess("API key", `Saved to ${configFile}`, json);
43
+ return;
44
+ }
45
+ const openBrowser = !(opts.browser === false || looksHeadless(host.env, host.platform));
46
+ const showUrl = (rawUrl) => {
47
+ const url = sanitizeControl(rawUrl);
48
+ if (json) {
49
+ err(JSON.stringify({ event: "authorization_required", authorization_url: url }));
50
+ return;
51
+ }
52
+ err();
53
+ if (openBrowser) {
54
+ err(` ${colors.aqua(">")} Opening the browser to sign in with Tavily.`);
55
+ err(` ${colors.dim("If nothing opens, visit:")}`);
56
+ } else {
57
+ err(` ${colors.aqua(">")} Open this URL in a browser on this machine:`);
58
+ }
59
+ err(` ${url}`);
60
+ err();
61
+ if (!openBrowser) {
62
+ err(
63
+ ` ${colors.dim("Headless / SSH sessions cannot complete browser login unless this machine can receive http://127.0.0.1 callbacks.")}`
64
+ );
65
+ err(
66
+ ` ${colors.dim("Use")} ${colors.green("tvly login --api-key tvly-YOUR_KEY")} ${colors.dim("instead.")}`
67
+ );
68
+ err();
69
+ }
70
+ };
71
+ const waiting = () => spinner(colors.aqua("Waiting for browser authorization..."), !json);
72
+ let spin = waiting();
73
+ const onStatus = (url) => {
74
+ spin.stop();
75
+ showUrl(url);
76
+ spin = waiting();
77
+ };
78
+ try {
79
+ const session = await (hooks.login ?? runBrowserLogin)({
80
+ openBrowser,
81
+ // Always report the URL: a browser launch can fail silently, even in --json mode.
82
+ onStatus,
83
+ fetchImpl: host.fetch,
84
+ openImpl: host.openUrl
85
+ });
86
+ spin.stop();
87
+ await saveOauthSession(session, host);
88
+ } catch (e) {
89
+ spin.stop();
90
+ if (e instanceof OAuthError) failLogin(e.message, json);
91
+ throw e;
92
+ }
93
+ printLoginSuccess("OAuth", `Token stored in ${configFile}`, json);
94
+ }
95
+ function printLoginSuccess(method, detail, json) {
96
+ if (json) {
97
+ out(
98
+ JSON.stringify({
99
+ authenticated: true,
100
+ method: method === "OAuth" ? "oauth" : "api_key",
101
+ detail
102
+ })
103
+ );
104
+ return;
105
+ }
106
+ out();
107
+ out(LOGO);
108
+ out();
109
+ out(` ${colors.green(`> Authenticated via ${method}`)}`);
110
+ out(` ${colors.dim(detail)}`);
111
+ out();
112
+ out(` ${colors.bold("Get started")}`);
113
+ out();
114
+ const hint = (cmd, arg) => out(` ${colors.green(cmd)} ${colors.dim(arg)}`);
115
+ hint("tvly search", '"your first query"');
116
+ hint("tvly extract", "<url>");
117
+ hint("tvly crawl", "<url>");
118
+ hint("tvly map", "<url>");
119
+ hint("tvly research", '"deep dive topic"');
120
+ out();
121
+ }
122
+ function failLogin(message, json) {
123
+ if (json) {
124
+ out(JSON.stringify({ authenticated: false, error: message }));
125
+ } else {
126
+ err();
127
+ err(` ${colors.pink(`> ${sanitizeControl(message)}`)}`);
128
+ err();
129
+ err(" Use an API key instead:");
130
+ err(` ${colors.green("tvly login --api-key tvly-YOUR_KEY")}`);
131
+ err();
132
+ }
133
+ throw new ExitError(3);
134
+ }
135
+ function effectiveAuthState(key, host) {
136
+ if (!key) return { key: null, method: null, source: null };
137
+ const method = authMethod(key, host);
138
+ const { file } = configPaths(host.home);
139
+ const source = {
140
+ env: "TAVILY_API_KEY environment variable",
141
+ oauth: `OAuth (${file})`,
142
+ oauth_legacy: "OAuth (~/.mcp-auth/)",
143
+ api_key: `config file (${file})`
144
+ }[method];
145
+ return { key, method, source };
146
+ }
147
+ async function runLogout(_opts, cmd, host) {
148
+ const json = resolveJson(cmd);
149
+ const result = await clearCredentials(host);
150
+ const { key, method, source } = effectiveAuthState(await getApiKey(host), host);
151
+ const envPresent = method === "env";
152
+ const payload = {
153
+ authenticated: key !== null,
154
+ method,
155
+ source,
156
+ local_credentials_cleared: true,
157
+ environment_credential_present: envPresent
158
+ };
159
+ if (envPresent) payload.warning = ENV_CREDENTIAL_WARNING;
160
+ const envStillSet = () => {
161
+ err(` ${colors.yellow(">")} TAVILY_API_KEY is still set, so the CLI remains authenticated.`);
162
+ err(` ${colors.dim("Run in your shell: unset TAVILY_API_KEY")}`);
163
+ };
164
+ if (result.revocationError) {
165
+ if (json) {
166
+ payload.server_revoked = false;
167
+ payload.error = result.revocationError;
168
+ out(JSON.stringify(payload));
169
+ } else {
170
+ err(` ${colors.yellow(">")} Local credentials cleared, but server revocation failed.`);
171
+ err(` ${colors.dim(sanitizeControl(result.revocationError))}`);
172
+ err(" The previous OAuth token may remain usable.");
173
+ if (envPresent) envStillSet();
174
+ }
175
+ throw new ExitError(3);
176
+ }
177
+ if (json) {
178
+ if (result.serverRevoked !== null) payload.server_revoked = result.serverRevoked;
179
+ out(JSON.stringify(payload));
180
+ return;
181
+ }
182
+ err(
183
+ colors.dim(
184
+ result.serverRevoked ? " Server session revoked and credentials cleared." : " Credentials cleared."
185
+ )
186
+ );
187
+ if (envPresent) envStillSet();
188
+ else err(` Run ${colors.green("tvly login")} to authenticate again.`);
189
+ }
190
+ async function runAuth(_opts, cmd, host) {
191
+ const json = resolveJson(cmd);
192
+ const { key, method, source } = effectiveAuthState(await getApiKeyOrExit(json, host), host);
193
+ if (json) {
194
+ out(JSON.stringify({ authenticated: key !== null, method, source }));
195
+ return;
196
+ }
197
+ out();
198
+ if (key) {
199
+ const masked = key.length > 12 ? `${key.slice(0, 8)}...${key.slice(-4)}` : "***";
200
+ out(` ${colors.green(">")} Authenticated via ${source}`);
201
+ out(` ${colors.dim(`Key: ${masked}`)}`);
202
+ } else {
203
+ out(` ${colors.pink(">")} Not authenticated`);
204
+ out();
205
+ out(` Run ${colors.green("tvly login")} to authenticate.`);
206
+ }
207
+ out();
208
+ }
209
+ export {
210
+ runAuth,
211
+ runLogin,
212
+ runLogout
213
+ };
@@ -0,0 +1,30 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/errors.ts
4
+ var ExitError = class extends Error {
5
+ constructor(code) {
6
+ super(`exit ${code}`);
7
+ this.code = code;
8
+ this.name = "ExitError";
9
+ }
10
+ code;
11
+ };
12
+ var OAuthError = class extends Error {
13
+ constructor(message) {
14
+ super(message);
15
+ this.name = "OAuthError";
16
+ }
17
+ };
18
+ function isObject(value) {
19
+ return typeof value === "object" && value !== null && !Array.isArray(value);
20
+ }
21
+ function messageOf(e) {
22
+ return e instanceof Error ? e.message : String(e);
23
+ }
24
+
25
+ export {
26
+ ExitError,
27
+ OAuthError,
28
+ isObject,
29
+ messageOf
30
+ };