surf-skill 2.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/CHANGELOG.md ADDED
@@ -0,0 +1,175 @@
1
+ # Changelog
2
+
3
+ ## v2.0.0 — npm package, cross-OS install, library mode
4
+
5
+ ### What's new
6
+
7
+ - **One-liner cross-OS install via npm.** `npm i -g surf-skill` works on
8
+ Linux, macOS, and Windows. Postinstall script creates symlinks into all
9
+ 4 supported agent harnesses (Claude Code, OpenCode, Codex CLI, Pi
10
+ Coding Agent), initializes `~/.config/surf/keys.json`, and cleans up
11
+ legacy symlinks from prior versions. Falls back to recursive copy on
12
+ Windows without Developer Mode.
13
+ - **Library mode for Node / Next.js / Express.** Import named functions:
14
+ ```js
15
+ import { search, extract, research } from 'surf-skill';
16
+ const r = await search('claude api', { max: 3 });
17
+ ```
18
+ Auto-discovers keys from `opts → process.env → .env → ~/.config/surf/keys.json`
19
+ (each level can contribute; results merged + deduped).
20
+ - **Multi-key wizard.** `surf-skill setup` now prompts for N keys per
21
+ provider (Enter empty to finish that provider). Add 1+ Tavily + 1+
22
+ Parallel keys in one pass.
23
+ - **Auto-wizard on first TTY use.** Running any command that needs keys
24
+ in a TTY with empty config auto-launches the wizard, then resumes the
25
+ command. CI/non-TTY behavior unchanged (clear actionable error).
26
+ - **Batch search in the library too.** `search(['q1', 'q2', 'q3'], opts)`
27
+ returns `{ summary, data: { batches } }` — same shape as CLI batch.
28
+
29
+ ### Breaking changes
30
+
31
+ - **Distribution moved from `git clone + install.sh` to `npm i -g`.**
32
+ If you installed via the old `install.sh`:
33
+ ```bash
34
+ # Remove old install
35
+ rm -f ~/.local/bin/surf-skill
36
+ rm -rf ~/.agents/skills/surf-skill ~/.claude/skills/surf-skill \
37
+ ~/.codex/skills/surf-skill ~/.pi/agent/skills/surf-skill
38
+ # Install via npm
39
+ npm i -g surf-skill
40
+ # Your ~/.config/surf/keys.json is preserved.
41
+ surf-skill keys list
42
+ ```
43
+ - **Repo layout**: `skills/surf-skill/*` moved to root. The package now
44
+ lives directly at the repo root for npm publishing. The `install.sh`
45
+ script is gone (replaced by `src/install/postinstall.mjs`).
46
+ - **Package name** unchanged (`surf-skill`).
47
+ - **CLI surface unchanged** — all commands, flags, and behavior identical.
48
+ - **State location unchanged** — `~/.config/surf/keys.json` and
49
+ `~/.cache/surf/` preserved.
50
+
51
+ ### Files added
52
+
53
+ - `src/index.mjs` — library entry (named exports)
54
+ - `src/env.mjs` — key discovery hierarchy + dotenv loader
55
+ - `src/install/postinstall.mjs` — cross-OS postinstall (idempotent)
56
+ - `src/install/preuninstall.mjs` — clean up symlinks on `npm rm`
57
+ - `src/lib/harness-install.mjs` — `symlinkOrCopy` helper, legacy cleanup
58
+ - `src/lib/api/{search,extract,crawl,map,research}.mjs` — library wrappers
59
+
60
+ ### Files modified
61
+
62
+ - `package.json` — `type: module`, `bin`, `main`, `exports`, `files`,
63
+ `postinstall`/`preuninstall` scripts; version 1.0.0 → 2.0.0
64
+ - `bin/surf-skill.mjs` — imports point to `../src/lib/`; VERSION 2.0.0;
65
+ auto-wizard block on first TTY use
66
+ - `src/lib/setup.mjs` — multi-key loop (N keys per provider)
67
+ - `src/lib/dispatch.mjs` — accepts `runCtx.state` for in-memory library mode
68
+ - `README.md` — one-liner install, library section, bonus features
69
+ - `SKILL.md` — `metadata.version: "2.0.0"`, npm install in requires
70
+
71
+ ### Files removed
72
+
73
+ - `skills/surf-skill/install.sh` — replaced by postinstall.mjs
74
+ - `skills/` directory — dissolved into root (npm-friendly layout)
75
+ - All `CHANGELOG-v2.x.md` (consolidated into this CHANGELOG.md in v1.0.0)
76
+
77
+ ---
78
+
79
+ ## v1.0.0 — Initial release
80
+
81
+ `surf-skill` is a multi-provider web skill for AI coding agents that fronts
82
+ **Tavily** and **Parallel AI** behind a single bash CLI. The agent calling
83
+ this skill never picks the provider — `surf-skill` does, with automatic
84
+ key rotation, provider fallback, and last-known-good persistence.
85
+
86
+ ### Capabilities
87
+
88
+ | Operation | Tavily | Parallel | Default order |
89
+ |---|---|---|---|
90
+ | `search` | ✓ | ✓ | tavily → parallel |
91
+ | `extract` | ✓ | ✓ | tavily → parallel |
92
+ | `crawl` | ✓ | ✗ | tavily only |
93
+ | `map` | ✓ | ✗ | tavily only |
94
+ | `research-start` / `research` | ✓ | ✓ | parallel → tavily |
95
+ | `research-poll` | by `request_id` prefix | by `request_id` prefix | sticky |
96
+
97
+ ### Features
98
+
99
+ - **Multi-provider fallback.** Tavily ↔ Parallel AI by capability map.
100
+ - **Multi-key rotation per provider.** Burn on `401/403/402` or persistent
101
+ `5xx`; burned keys auto-reset on the first day of the next calendar month.
102
+ - **Provider chain memory.** `last_ok_provider` persisted in
103
+ `~/.config/surf/keys.json` so the next call starts on the hot path.
104
+ - **`--provider <tavily|parallel>`** forces a specific provider (disables
105
+ fallback for that call). `--no-fallback` pins to the default provider.
106
+ - **Batch search.** Pass multiple positional args to `search` and each is
107
+ an independent query, run sequentially, with partial failures reported
108
+ inline.
109
+ - **Progress logs to stderr.** One self-contained line per event
110
+ (`[surf HH:MM:SS] ▸/✓/✗/↻/⚠/⏱`). Stable format for agent parsing.
111
+ Stdout stays clean for JSON/Markdown. `--quiet` / `SURF_QUIET=1`
112
+ silences.
113
+ - **Interactive onboarding.** `surf-skill setup` (TTY) wizard prompts for
114
+ keys and persists to `~/.config/surf/keys.json` (chmod 600). On error,
115
+ TTY users see `→ Run 'surf-skill setup' to configure keys interactively.`
116
+ - **Per-project bash-timeout config.** `surf-skill project-config`
117
+ auto-detects the harness via `.github/`, `.claude/`, `.pi/` markers and
118
+ writes the right config to raise the bash timeout. Required for GH
119
+ Copilot CLI (default 30 s), recommended for Claude Code / Pi.
120
+ - **Self-budget timeout guard.** Reads the harness bash timeout from env
121
+ vars (`BASH_DEFAULT_TIMEOUT_MS`, `PI_BASH_DEFAULT_TIMEOUT_SECONDS`,
122
+ `OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS`, or `SURF_AGENT_BUDGET_MS`
123
+ override). Aborts early with `LikelyAgentTimeout` instead of being
124
+ killed silently by SIGTERM. Unknown harness → assume 30 s.
125
+ - **SIGTERM / SIGINT handler.** Defense in depth: surfaces a
126
+ `KilledBySignal` message with the same `project-config` hint before
127
+ exit 143.
128
+ - **Per-project config writer** detects `.github/`, `.claude/`, `.pi/` and
129
+ writes only what the harness in this project needs.
130
+ - **Local response cache** (`~/.cache/surf/`, TTL 6 h) keyed by
131
+ `sha256(operation, args)`; `--no-cache` bypasses; cache survives provider
132
+ fallbacks.
133
+ - **Local usage ledger** (`~/.cache/surf/usage.jsonl`) per-provider
134
+ breakdown via `surf-skill cost`.
135
+ - **Audit log** (`~/.cache/surf/audit.log`) records provider name and key
136
+ INDEX, never the key itself.
137
+ - **Cost guard.** Estimates > 10 credits are blocked unless
138
+ `--confirm-expensive` (or `SURF_ALLOW_EXPENSIVE=1`).
139
+ - **Predictable JSON.** `--json` returns a normalized envelope with the
140
+ same shape across providers. `--raw-json` exposes the provider response
141
+ for debugging.
142
+
143
+ ### Default behavior
144
+
145
+ - `search --depth` defaults to `advanced` (better quality, ~3–10 s,
146
+ 2 credits). Pass `--depth basic` for the cheaper/faster path.
147
+ - `surf-skill research` is capped at 50 s and refuses `--model pro`/`ultra`
148
+ (use `research-start` + `research-poll` for those).
149
+
150
+ ### Provider notes (verified 2026-05-20 against live APIs)
151
+
152
+ - **Tavily** `POST /search`: `Authorization: Bearer <key>`. Body accepts
153
+ `query`, `search_depth`, `max_results`, `topic`, `time_range`,
154
+ `include_domains`, `exclude_domains`, `country`, `include_answer`,
155
+ `include_raw_content`, etc.
156
+ - **Parallel AI** `POST /v1/search`: `x-api-key: <key>`. Body accepts
157
+ ONLY `{ objective, search_queries }`. Any other field (e.g. `processor`,
158
+ `max_results`) is rejected with `Extra inputs are not permitted`.
159
+ Tavily-only knobs are silently ignored when the call lands on Parallel.
160
+ - **Parallel** has no crawl / no URL map / no public usage endpoint.
161
+
162
+ ### Supported harnesses
163
+
164
+ | Harness | Default bash | Max | Coverage after install |
165
+ |---|---|---|---|
166
+ | **Claude Code** | 120 s | 600 s (hard) | 300 s default via `~/.claude/settings.json` |
167
+ | **Pi Coding Agent** | 120 s | 600 s | 300 s default via `~/.pi/agent/settings.json` |
168
+ | **GH Copilot CLI** | **30 s** | not documented | per-project `.github/copilot-hooks.json` (run `surf-skill project-config`) |
169
+ | **OpenCode** | varies | 600 s | 600 s default via `~/.config/opencode/opencode.json` |
170
+ | **Codex CLI** | n/a | n/a | symlinked under `~/.codex/skills/surf-skill/` |
171
+
172
+ ### Stack
173
+
174
+ Node ≥ 18, bash, **zero npm dependencies**. The full CLI is under 500 LOC
175
+ in `skills/surf-skill/bin/surf-skill.mjs` + `skills/surf-skill/lib/*.mjs`.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026
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 ADDED
@@ -0,0 +1,430 @@
1
+ # surf-skill
2
+
3
+ **One bash command. Two providers. Zero MCP.** A multi-provider web skill
4
+ for AI coding agents that fronts **Tavily** and **Parallel AI** behind a
5
+ single CLI (`surf-skill`). The agent calling this skill **never picks the
6
+ provider** — `surf-skill` does, with automatic key rotation, provider
7
+ fallback, and last-known-good persistence.
8
+
9
+ ```
10
+ search ─┐
11
+ extract ┤ ┌──▶ Tavily (search, extract, crawl, map, research)
12
+ crawl ──┼──▶ surf ───┤
13
+ map ───┤ └──▶ Parallel (search, extract, research async)
14
+ research┘
15
+ ```
16
+
17
+ | | |
18
+ |---|---|
19
+ | **Status** | v1.0.0 |
20
+ | **Runtime** | Node ≥ 18, bash. Zero npm deps. |
21
+ | **Storage** | `~/.config/surf/keys.json` (chmod 600). Never read from env at runtime. |
22
+ | **Supported agents** | Claude Code · GitHub Copilot CLI · Pi Coding Agent · OpenCode · Codex CLI |
23
+ | **Spec** | [Anthropic Agent Skills](https://docs.claude.com/en/docs/agents-and-tools/agent-skills) |
24
+
25
+ ---
26
+
27
+ ## Quickstart (30 seconds)
28
+
29
+ ```bash
30
+ # One-liner cross-OS install (Linux, macOS, Windows)
31
+ npm i -g surf-skill
32
+
33
+ # That's it — postinstall creates symlinks into all supported harnesses,
34
+ # initializes ~/.config/surf/keys.json, and prints a hint.
35
+ # On first run, an interactive wizard auto-launches in TTY:
36
+
37
+ surf-skill search "your query"
38
+ # → "No keys configured. Launching setup wizard…"
39
+ # → prompts for Tavily key #1, #2, …, Parallel key #1, #2, …
40
+ # → resumes your command
41
+
42
+ # In each project where you'll use surf-skill (REQUIRED for GH Copilot CLI):
43
+ cd path/to/your-project
44
+ surf-skill project-config
45
+ ```
46
+
47
+ You can also run `surf-skill setup` manually anytime to add more keys.
48
+
49
+ ### Use as a Node library
50
+
51
+ ```bash
52
+ npm i surf-skill
53
+ ```
54
+
55
+ ```js
56
+ import { search, extract, research } from 'surf-skill';
57
+
58
+ // Auto-discovers keys: opts > process.env > .env > ~/.config/surf/keys.json
59
+ const r = await search('claude api', { max: 3 });
60
+ console.log(r.data.results[0].url);
61
+
62
+ // Or pass keys explicitly (great for serverless / Next.js API routes)
63
+ const r2 = await search('x', {
64
+ tavilyKeys: [process.env.MY_TAVILY_1, process.env.MY_TAVILY_2],
65
+ depth: 'advanced',
66
+ });
67
+
68
+ // Batch search (single call, N queries, partial-failure tolerant)
69
+ const batch = await search(['topic A', 'topic B', 'topic C'], { max: 2 });
70
+
71
+ // Deep research
72
+ const job = await research('compare X vs Y', { model: 'mini' });
73
+ console.log(job.data.content);
74
+ ```
75
+
76
+ Library works server-side (Node / Next.js API routes / Express). Not for
77
+ browser bundles — Tavily and Parallel don't enable CORS for browser origins.
78
+
79
+ ---
80
+
81
+ ## Why this exists
82
+
83
+ You have a Tavily key. Maybe a Parallel one too. Maybe several Tavily keys
84
+ to spread cost across accounts. Today every agent skill is **1-to-1** with
85
+ a provider — when a key dies or a provider has an outage, your agent loop
86
+ breaks.
87
+
88
+ `surf-skill` is a connector:
89
+
90
+ - **Multi-key per provider.** Add as many keys as you want; rotation is
91
+ automatic on `401`/`403`/`402` (auth, insufficient credits) or persistent
92
+ `5xx`. Burned keys auto-reset on the first day of the next calendar
93
+ month (assuming monthly billing).
94
+ - **Provider fallback.** If all Tavily keys are burned, `search`/`extract`
95
+ fail over to Parallel — transparently. `crawl` and `map` stay on Tavily
96
+ (Parallel doesn't have them). `research` defaults to Parallel first
97
+ because its Task API is the strongest deep-research surface.
98
+ - **Hot-path memory.** The last successful provider/key is remembered in
99
+ `~/.config/surf/keys.json`. The next call starts there — no cold-start
100
+ cost.
101
+ - **Predictable output.** `--json` returns the same normalized envelope
102
+ no matter which provider answered.
103
+
104
+ ---
105
+
106
+ ## Supported agents
107
+
108
+ > The installer configures every harness it can. The user only has to
109
+ > manually configure GitHub Copilot CLI (per project) because it has no
110
+ > global timeout setting.
111
+
112
+ ### Claude Code
113
+
114
+ ```bash
115
+ npm i -g surf-skill
116
+ # Installer writes ~/.claude/settings.json:
117
+ # { "env": { "BASH_DEFAULT_TIMEOUT_MS": "300000",
118
+ # "BASH_MAX_TIMEOUT_MS": "600000" } }
119
+ ```
120
+
121
+ The skill becomes available at `~/.claude/skills/surf-skill/`. In a Claude
122
+ Code session, just ask: "search the web for X" — the agent will invoke
123
+ `surf-skill` via Bash. For commands that may exceed 5 min, the agent can
124
+ pass `timeout: 600000` on the Bash call (10 min hard cap), or set
125
+ `run_in_background: true` and monitor via `/tasks`.
126
+
127
+ ### GitHub Copilot CLI
128
+
129
+ ⚠️ **Default bash timeout is 30 s — the most fragile of the three.**
130
+
131
+ ```bash
132
+ npm i -g surf-skill
133
+ # Symlink created at ~/.copilot/skills/ (via ~/.agents/skills/surf-skill).
134
+ ```
135
+
136
+ **Per-project**, run inside the project root:
137
+
138
+ ```bash
139
+ surf-skill project-config
140
+ # writes .github/copilot-hooks.json with { "timeoutSec": 300 }
141
+ # detects .github/ automatically; use --harness copilot --yes to force
142
+ ```
143
+
144
+ Without this, any `surf-skill` command other than `--help`, `--version`,
145
+ `keys list/add`, or `search --max 1` will time out. With it, you can use
146
+ the full command set up to ~5 min per call.
147
+
148
+ For longer operations, use Copilot CLI's async pattern: `/delegate` the
149
+ `surf-skill research-start ...` call, then poll with `surf-skill
150
+ research-poll <id>` from a regular session.
151
+
152
+ If surf-skill detects the agent will likely kill the call before it can
153
+ finish, it now aborts early with `LikelyAgentTimeout` and tells the agent
154
+ to suggest `surf-skill project-config` to the user — instead of dying
155
+ silently to SIGTERM.
156
+
157
+ ### Pi Coding Agent
158
+
159
+ ```bash
160
+ npm i -g surf-skill
161
+ # Installer writes ~/.pi/agent/settings.json:
162
+ # { "env": { "PI_BASH_DEFAULT_TIMEOUT_SECONDS": "300",
163
+ # "PI_BASH_MAX_TIMEOUT_SECONDS": "600" } }
164
+ ```
165
+
166
+ The skill becomes available at `~/.pi/agent/skills/surf-skill/`. Pi reads
167
+ the timeout from env, so the settings.json above is enough. For
168
+ long-running work, Pi supports subagents with `--bg` and the `await` tool.
169
+
170
+ ### OpenCode & Codex CLI
171
+
172
+ Also auto-configured by the installer (`~/.agents/skills/surf-skill/` and
173
+ `~/.codex/skills/surf-skill/`). OpenCode gets `mcp_timeout` + `bash.timeout_ms`
174
+ set to 600 000 ms in `~/.config/opencode/opencode.json`.
175
+
176
+ ---
177
+
178
+ ## Timeouts at a glance
179
+
180
+ | Agent | Default bash | Max | After install | Most likely to time out? |
181
+ |---|---|---|---|---|
182
+ | **Claude Code** | 120 s | 600 s (hard) | 300 s default | Long crawls > 5 min |
183
+ | **GitHub Copilot CLI** | **30 s** | NÃO DOCUMENTADO | unchanged (no global config) | **YES — most commands** |
184
+ | **Pi Coding Agent** | 120 s | 600 s | 300 s default | Long crawls > 5 min |
185
+ | **OpenCode** | varies | 600 s | 600 s default | Rarely |
186
+
187
+ If you see timeouts, the order of fixes:
188
+
189
+ 1. Use `surf-skill research-start` + `research-poll` instead of sync
190
+ `research`.
191
+ 2. Reduce `--limit` / `--max` / `--max-depth`.
192
+ 3. Bump the per-harness timeout (see the relevant card above).
193
+ 4. Set `SURF_TIMEOUT_MS=300000` (caps the HTTP request itself at 5 min).
194
+
195
+ ---
196
+
197
+ ## Commands
198
+
199
+ | Command | What it does | Provider(s) |
200
+ |---|---|---|
201
+ | `setup` | Interactive wizard to add keys (TTY) | n/a |
202
+ | `project-config` | Write per-project bash-timeout config | n/a |
203
+ | `search <q> [q2 ...]` | Web search; multiple positional args = **batch** | tavily, parallel |
204
+ | `extract <url> ...` | Pull markdown from URLs | tavily, parallel |
205
+ | `crawl <url>` | Recursive site crawl | tavily |
206
+ | `map <url>` | Sitemap discovery | tavily |
207
+ | `research <topic>` | Sync deep research (50 s budget) | parallel, tavily |
208
+ | `research-start <topic>` | Start async research | parallel, tavily |
209
+ | `research-poll <id>` | Poll an async research job | (sticky to provider) |
210
+ | `usage --provider <name>` | Provider's usage endpoint | per provider |
211
+ | `cache-clear` | Purge response cache | n/a |
212
+ | `cost [--reset]` | Local credit ledger (per-provider) | n/a |
213
+ | `keys <subcmd>` | `add`, `remove`, `list`, `reset`, `clear` | n/a |
214
+
215
+ Full reference: `skills/surf-skill/SKILL.md`.
216
+
217
+ Global flags every command accepts:
218
+
219
+ ```
220
+ --provider <tavily|parallel> Force provider (disables fallback)
221
+ --no-fallback Keep default provider, no cross-provider fallback
222
+ --no-cache Skip response cache
223
+ --json Normalized envelope as JSON
224
+ --raw-json Raw provider response (bypasses cache)
225
+ --confirm-expensive Allow operations estimated > 10 credits
226
+ --quiet Silence progress logs (stderr)
227
+ ```
228
+
229
+ ---
230
+
231
+ ## Batch your queries
232
+
233
+ When you need to research **multiple angles** of the same topic, batch them
234
+ in a single call. Each positional arg is an independent query:
235
+
236
+ ```bash
237
+ surf-skill search "compare X vs Y" "alternatives to X" "X security issues"
238
+ ```
239
+
240
+ - Runs sequentially (avoids rate-limit thrashing on a single key).
241
+ - Partial failures are reported inline — the command exits `0` if at least
242
+ one query succeeded.
243
+ - Total credits and timing surface in the markdown header and `--json` envelope.
244
+ - Progress logs (see below) show `[i/N]` per query.
245
+
246
+ This is the recommended way for an agent to gather multi-source context in
247
+ one shot, instead of looping with N separate bash calls.
248
+
249
+ ---
250
+
251
+ ## Progress logs (stderr)
252
+
253
+ Every operation emits one self-contained line per event to **stderr**, so
254
+ both humans and the calling LLM can see what's happening without parsing
255
+ the main result on stdout.
256
+
257
+ ```
258
+ [surf 17:58:12] ▸ search → tavily (key #0)
259
+ [surf 17:58:14] ✓ search tavily 1234ms (2 credits)
260
+ [surf 17:58:14] ↻ tavily 429 — backoff 1500ms (attempt 1/3)
261
+ [surf 17:58:18] ⚠ tavily key #0 burned (401)
262
+ [surf 17:58:18] ▸ search → parallel (key #0)
263
+ [surf 17:58:20] ✓ search parallel 2102ms (2 credits)
264
+ [surf 17:58:20] ⏱ batch done: 3/3 ok, 0 failed (8200ms, 6 credits)
265
+ ```
266
+
267
+ The format is stable for grep/parse. Use `--quiet` or `SURF_QUIET=1` to
268
+ silence (CI, piping, tests). Stdout stays clean either way.
269
+
270
+ ---
271
+
272
+ ## Multi-key & fallback
273
+
274
+ ```
275
+ state.json (per provider):
276
+ keys: [key0, key1, key2]
277
+ current: 1 ← starts here next call
278
+ burned: [{ index: 0, reason: "401", at: "2026-05-15..." }]
279
+ ← auto-reset on the 1st of next month
280
+
281
+ call flow:
282
+ ┌─ load state, auto-reset burned ──┐
283
+ │ │
284
+ └─▶ chain = [last_ok_provider, ─┤
285
+ ...rest_of_capability_chain]
286
+
287
+ for provider in chain: │
288
+ for key in usable_keys(provider):│
289
+ try call │
290
+ 200 ─▶ save last_ok, return │
291
+ 401/403/402 ─▶ burn key, next│
292
+ 5xx x3 ─▶ burn key, next │
293
+ 429 ─▶ backoff, retry │
294
+ 4xx ─▶ raise (no fallback) │
295
+ (no usable keys) ─▶ next provider│
296
+ raise AllProvidersExhausted ───────┘
297
+ ```
298
+
299
+ Force a specific provider for debugging:
300
+
301
+ ```bash
302
+ surf-skill search "x" --provider parallel
303
+ # 'parallel' fails ⇒ command fails (no fallback when --provider is set)
304
+ ```
305
+
306
+ ---
307
+
308
+ ## Onboarding (3 ways)
309
+
310
+ ```bash
311
+ # 1. Wizard (recommended in a TTY)
312
+ surf-skill setup
313
+
314
+ # 2. Direct
315
+ surf-skill keys add --provider tavily tvly-...
316
+ surf-skill keys add --provider parallel <key>
317
+
318
+ # 3. Auto-launch in TTY: just run any command without keys
319
+ surf-skill search "test"
320
+ # → "No keys configured. Launching setup wizard…" → prompts → resumes search
321
+
322
+ # 4. Library mode: env vars / .env / explicit opts (no setup needed)
323
+ TAVILY_API_KEY=tvly-... node -e "import('surf-skill').then(m => m.search('x'))"
324
+ ```
325
+
326
+ Inspect what was stored (keys are masked):
327
+
328
+ ```bash
329
+ surf-skill keys list
330
+ # **Surf keys** (config: ~/.config/surf/keys.json)
331
+ # last_ok_provider: `tavily`
332
+ # ## tavily (2 keys)
333
+ # - [0] tvly-…ab12 *(current)*
334
+ # - [1] tvly-…cd34
335
+ ```
336
+
337
+ ---
338
+
339
+ ## Troubleshooting
340
+
341
+ **`❌ Error [NoProviderAvailable]: operation 'X' requires one of [...]`**
342
+ → The op needs a key for a provider you haven't configured. In a TTY the
343
+ error already suggests `surf-skill setup`. Outside TTY, run
344
+ `surf-skill keys add --provider <name> <key>`.
345
+
346
+ **`❌ Error [AllProvidersExhausted]: ...`**
347
+ → Every key on every eligible provider failed. Check `surf-skill keys list`
348
+ — if everything is `burned`, you've either rotated keys mid-billing-cycle
349
+ or the providers are down. Run `surf-skill keys reset` to retry.
350
+
351
+ **Command timed out in GH Copilot CLI**
352
+ → Run `surf-skill project-config` inside the project root. See the
353
+ Copilot CLI card above.
354
+
355
+ **`❌ Error [LikelyAgentTimeout]: ...`**
356
+ → surf-skill detected the harness will kill the call before it finishes
357
+ (typical on Copilot CLI without per-project config). Run `surf-skill
358
+ project-config` in the project, then retry. Don't retry the same call
359
+ without fixing the timeout first.
360
+
361
+ **`❌ Error [KilledBySignal]: surf-skill received SIGTERM/SIGINT`**
362
+ → The harness killed us mid-flight. Same fix as `LikelyAgentTimeout`. The
363
+ SIGTERM handler exists as a fallback — the self-budget check should fire
364
+ first when env vars are set.
365
+
366
+ **`❌ Error: EXPENSIVE_BLOCKED ...`**
367
+ → Pass `--confirm-expensive` after confirming the cost with the user. Or
368
+ export `SURF_ALLOW_EXPENSIVE=1` for the session.
369
+
370
+ **`Refusing sync research with model=pro`**
371
+ → Use `surf-skill research-start --model pro ...` then `surf-skill
372
+ research-poll <id>`. Sync research is capped at 50 s on purpose.
373
+
374
+ ---
375
+
376
+ ## Repository layout
377
+
378
+ ```text
379
+ .
380
+ ├── package.json
381
+ ├── README.md ← you're here
382
+ ├── CHANGELOG.md
383
+ ├── LICENSE
384
+ └── skills/
385
+ └── surf-skill/
386
+ ├── SKILL.md
387
+ ├── install.sh
388
+ ├── bin/
389
+ │ └── surf-skill.mjs
390
+ ├── lib/
391
+ │ ├── state.mjs ← keys.json I/O, monthly auto-reset
392
+ │ ├── cache.mjs ← TTL response cache
393
+ │ ├── audit.mjs ← audit + usage JSONL
394
+ │ ├── flags.mjs ← parsing + helpers
395
+ │ ├── cost.mjs ← estimateCredits + guard
396
+ │ ├── format.mjs ← markdown formatters
397
+ │ ├── dispatch.mjs ← provider/key fallback + self-budget
398
+ │ ├── keys-cmd.mjs ← surf-skill keys add/remove/...
399
+ │ ├── setup.mjs ← interactive onboarding
400
+ │ ├── project-config.mjs ← surf-skill project-config
401
+ │ ├── progress.mjs ← stderr progress events
402
+ │ └── providers/
403
+ │ ├── index.mjs
404
+ │ ├── tavily.mjs
405
+ │ └── parallel.mjs
406
+ └── references/
407
+ ├── tavily-api.md
408
+ ├── parallel-api.md
409
+ └── COSTS.md
410
+ ```
411
+
412
+ ---
413
+
414
+ ## Security
415
+
416
+ - This repository contains **no real API keys**. The installer only uses
417
+ placeholders.
418
+ - Keys are stored exclusively in `~/.config/surf/keys.json` (chmod 600).
419
+ `surf-skill` does not read keys from env at runtime.
420
+ - The audit log records only `provider` name and key **index**, never the
421
+ key itself. `surf-skill keys list` masks every key (`tvly-…ab12`).
422
+ - The skill never executes content returned from the web — it just prints it.
423
+ - Review any skill before installing. Skills can instruct agents to run
424
+ commands.
425
+
426
+ ---
427
+
428
+ ## License
429
+
430
+ MIT.