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 +175 -0
- package/LICENSE +21 -0
- package/README.md +430 -0
- package/SKILL.md +278 -0
- package/bin/surf-skill.mjs +539 -0
- package/package.json +55 -0
- package/references/COSTS.md +72 -0
- package/references/parallel-api.md +155 -0
- package/references/tavily-api.md +90 -0
- package/src/env.mjs +125 -0
- package/src/index.mjs +22 -0
- package/src/install/postinstall.mjs +73 -0
- package/src/install/preuninstall.mjs +25 -0
- package/src/lib/api/crawl.mjs +55 -0
- package/src/lib/api/extract.mjs +46 -0
- package/src/lib/api/map.mjs +43 -0
- package/src/lib/api/research.mjs +96 -0
- package/src/lib/api/search.mjs +92 -0
- package/src/lib/audit.mjs +34 -0
- package/src/lib/cache.mjs +46 -0
- package/src/lib/cost.mjs +90 -0
- package/src/lib/dispatch.mjs +320 -0
- package/src/lib/flags.mjs +63 -0
- package/src/lib/format.mjs +110 -0
- package/src/lib/harness-install.mjs +149 -0
- package/src/lib/keys-cmd.mjs +138 -0
- package/src/lib/progress.mjs +81 -0
- package/src/lib/project-config.mjs +145 -0
- package/src/lib/providers/index.mjs +32 -0
- package/src/lib/providers/parallel.mjs +270 -0
- package/src/lib/providers/tavily.mjs +245 -0
- package/src/lib/setup.mjs +111 -0
- package/src/lib/state.mjs +197 -0
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# Parallel AI — endpoint reference
|
|
2
|
+
|
|
3
|
+
Base URL: `https://api.parallel.ai`.
|
|
4
|
+
|
|
5
|
+
**Auth**: header `x-api-key: <PARALLEL_API_KEY>` (**not** `Authorization: Bearer`).
|
|
6
|
+
|
|
7
|
+
Get a key at <https://platform.parallel.ai>. Docs at <https://docs.parallel.ai>.
|
|
8
|
+
|
|
9
|
+
## POST /v1/search
|
|
10
|
+
|
|
11
|
+
Web search.
|
|
12
|
+
|
|
13
|
+
| Field | Type | Notes |
|
|
14
|
+
|---|---|---|
|
|
15
|
+
| `objective` | string | required — natural language description of what you need |
|
|
16
|
+
| `search_queries` | string[] | required — search queries to issue |
|
|
17
|
+
| `processor` | `lite` \| `base` | optional; default `lite`. `base` ≈ Tavily's `advanced` depth |
|
|
18
|
+
| `max_results` | int | optional |
|
|
19
|
+
| `source_policy.include_domains` | string[] | optional |
|
|
20
|
+
| `source_policy.exclude_domains` | string[] | optional |
|
|
21
|
+
|
|
22
|
+
Response:
|
|
23
|
+
|
|
24
|
+
```json
|
|
25
|
+
{
|
|
26
|
+
"search_id": "...",
|
|
27
|
+
"results": [
|
|
28
|
+
{
|
|
29
|
+
"url": "...",
|
|
30
|
+
"title": "...",
|
|
31
|
+
"excerpts": ["...", "..."],
|
|
32
|
+
"publish_date": "2026-01-01"
|
|
33
|
+
}
|
|
34
|
+
],
|
|
35
|
+
"warnings": [],
|
|
36
|
+
"usage": {}
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Doc: <https://docs.parallel.ai/search/search-quickstart>
|
|
41
|
+
|
|
42
|
+
## POST /v1beta/extract (beta)
|
|
43
|
+
|
|
44
|
+
Pull content from known URLs.
|
|
45
|
+
|
|
46
|
+
| Field | Type | Notes |
|
|
47
|
+
|---|---|---|
|
|
48
|
+
| `urls` | string[] | required |
|
|
49
|
+
| `objective` | string | optional — focuses extraction on specific information |
|
|
50
|
+
| `excerpts` | bool | optional — return objective-focused excerpts |
|
|
51
|
+
| `full_content` | bool | optional — return full Markdown of each page |
|
|
52
|
+
|
|
53
|
+
Response:
|
|
54
|
+
|
|
55
|
+
```json
|
|
56
|
+
{
|
|
57
|
+
"extract_id": "...",
|
|
58
|
+
"results": [
|
|
59
|
+
{
|
|
60
|
+
"url": "...",
|
|
61
|
+
"title": "...",
|
|
62
|
+
"publish_date": "...",
|
|
63
|
+
"excerpts": ["..."],
|
|
64
|
+
"full_content": "..."
|
|
65
|
+
}
|
|
66
|
+
],
|
|
67
|
+
"errors": []
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Doc: <https://docs.parallel.ai/extract/extract-quickstart>
|
|
72
|
+
|
|
73
|
+
## POST /v1/tasks/runs → GET /v1/tasks/runs/{run_id}/result
|
|
74
|
+
|
|
75
|
+
Async Task API — Parallel's deep-research equivalent.
|
|
76
|
+
|
|
77
|
+
`POST /v1/tasks/runs` body:
|
|
78
|
+
|
|
79
|
+
| Field | Type | Notes |
|
|
80
|
+
|---|---|---|
|
|
81
|
+
| `input` | string \| object | required |
|
|
82
|
+
| `processor` | `lite` \| `base` \| `core` \| `pro` \| `ultra` \| `ultra8x` | required |
|
|
83
|
+
| `task_spec.output_schema` | JSON Schema / text / auto | optional — structured output |
|
|
84
|
+
| `metadata` | object | optional |
|
|
85
|
+
|
|
86
|
+
Returns **HTTP 202** with `{ run_id, status: "queued" \| "running", is_active, processor }`.
|
|
87
|
+
|
|
88
|
+
`GET /v1/tasks/runs/{run_id}` returns status only.
|
|
89
|
+
|
|
90
|
+
`GET /v1/tasks/runs/{run_id}/result` returns the result **only after `status: "completed"`**:
|
|
91
|
+
|
|
92
|
+
```json
|
|
93
|
+
{
|
|
94
|
+
"run_id": "...",
|
|
95
|
+
"status": "completed",
|
|
96
|
+
"output": {
|
|
97
|
+
"content": "...",
|
|
98
|
+
"basis": [
|
|
99
|
+
{ "url": "...", "title": "..." }
|
|
100
|
+
]
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Processor mapping used by `surf-skill research`:
|
|
106
|
+
|
|
107
|
+
| `--model` | Parallel processor |
|
|
108
|
+
|---|---|
|
|
109
|
+
| `mini` | `lite` |
|
|
110
|
+
| `auto` | `base` |
|
|
111
|
+
| `pro` | `pro` |
|
|
112
|
+
| `ultra` | `ultra` |
|
|
113
|
+
|
|
114
|
+
Doc: <https://docs.parallel.ai/task-api/task-quickstart>,
|
|
115
|
+
<https://docs.parallel.ai/task-api/guides/choose-a-processor>
|
|
116
|
+
|
|
117
|
+
## Capabilities not provided by Parallel
|
|
118
|
+
|
|
119
|
+
- **No crawl endpoint** (no recursive site walk).
|
|
120
|
+
- **No URL-map endpoint** (no sitemap discovery without fetching content).
|
|
121
|
+
- **No public `/usage` endpoint** at the time of writing.
|
|
122
|
+
|
|
123
|
+
`surf` routes `crawl` and `map` to Tavily only.
|
|
124
|
+
|
|
125
|
+
## Error format
|
|
126
|
+
|
|
127
|
+
Non-2xx responses use:
|
|
128
|
+
|
|
129
|
+
```json
|
|
130
|
+
{
|
|
131
|
+
"type": "error",
|
|
132
|
+
"error": {
|
|
133
|
+
"ref_id": "...",
|
|
134
|
+
"message": "human-readable message",
|
|
135
|
+
"detail": {}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Surf classifies them as:
|
|
141
|
+
|
|
142
|
+
- `401` → `auth` (burn key, try next)
|
|
143
|
+
- `402` insufficient credits → `auth` (burn key, try next)
|
|
144
|
+
- `403` → `auth` **unless** body says "processor" → `caller_4xx`
|
|
145
|
+
- `429` → `rate_limit_429` (retry with backoff)
|
|
146
|
+
- `5xx` → `server_5xx` (retry; after 3 attempts, burn key)
|
|
147
|
+
- other 4xx → `caller_4xx` (no fallback, throw)
|
|
148
|
+
|
|
149
|
+
## Notes
|
|
150
|
+
|
|
151
|
+
- Free tier / RPS / `Retry-After` header are **not publicly documented**.
|
|
152
|
+
- The `parallel-web` Node SDK exists (v0.4.1) but requires Node 20+; surf
|
|
153
|
+
uses `fetch` directly to stay zero-deps and Node 18+ compatible.
|
|
154
|
+
- Extract is in `v1beta`; the path may change. The adapter is in
|
|
155
|
+
`lib/providers/parallel.mjs` if you need to update it.
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# Tavily API — endpoint reference
|
|
2
|
+
|
|
3
|
+
Base: `https://api.tavily.com`. Auth: `Authorization: Bearer tvly-…`.
|
|
4
|
+
|
|
5
|
+
## POST /search
|
|
6
|
+
Search the web.
|
|
7
|
+
|
|
8
|
+
| Field | Type | Notes |
|
|
9
|
+
|---|---|---|
|
|
10
|
+
| `query` | string | required |
|
|
11
|
+
| `search_depth` | `basic` \| `advanced` \| `fast` \| `ultra-fast` | default `basic` |
|
|
12
|
+
| `max_results` | int | ≤20 |
|
|
13
|
+
| `topic` | `general` \| `news` \| `finance` | |
|
|
14
|
+
| `time_range` | `day` \| `week` \| `month` \| `year` | |
|
|
15
|
+
| `start_date`, `end_date` | ISO date | |
|
|
16
|
+
| `include_domains` | string[] | ≤300 |
|
|
17
|
+
| `exclude_domains` | string[] | ≤150 |
|
|
18
|
+
| `include_answer` | `false` \| `basic` \| `advanced` | |
|
|
19
|
+
| `include_raw_content` | `false` \| `markdown` \| `text` | |
|
|
20
|
+
| `include_images`, `include_image_descriptions`, `include_favicon` | bool | |
|
|
21
|
+
| `country` | string | only with `topic=general` |
|
|
22
|
+
| `auto_parameters` | bool | always 2 credits |
|
|
23
|
+
| `exact_match` | bool | |
|
|
24
|
+
| `include_usage` | bool | include credit usage in response |
|
|
25
|
+
|
|
26
|
+
## POST /extract
|
|
27
|
+
Pull content from known URLs.
|
|
28
|
+
|
|
29
|
+
| Field | Type | Notes |
|
|
30
|
+
|---|---|---|
|
|
31
|
+
| `urls` | string[] | up to 20 |
|
|
32
|
+
| `extract_depth` | `basic` \| `advanced` | |
|
|
33
|
+
| `format` | `markdown` \| `text` | |
|
|
34
|
+
| `include_images`, `include_favicon` | bool | |
|
|
35
|
+
| `query` | string | filter relevance |
|
|
36
|
+
| `chunks_per_source` | 1–5 | |
|
|
37
|
+
| `timeout` | float | 1.0–60.0 s |
|
|
38
|
+
|
|
39
|
+
## POST /crawl
|
|
40
|
+
Mapping + extraction starting from a URL.
|
|
41
|
+
|
|
42
|
+
| Field | Type | Notes |
|
|
43
|
+
|---|---|---|
|
|
44
|
+
| `url` | string | required |
|
|
45
|
+
| `max_depth`, `max_breadth`, `limit` | int | |
|
|
46
|
+
| `instructions` | string | natural language guidance |
|
|
47
|
+
| `select_paths`, `select_domains` | string[] | regex |
|
|
48
|
+
| `exclude_paths`, `exclude_domains` | string[] | |
|
|
49
|
+
| `allow_external` | bool | |
|
|
50
|
+
| `include_images` | bool | |
|
|
51
|
+
| `categories` | string[] | |
|
|
52
|
+
| `extract_depth` | `basic` \| `advanced` | |
|
|
53
|
+
| `format` | `markdown` \| `text` | |
|
|
54
|
+
| `query` | string | optional focus filter |
|
|
55
|
+
| `chunks_per_source` | 1–5 | |
|
|
56
|
+
| `timeout` | float | custom server-side timeout |
|
|
57
|
+
| `include_usage` | bool | include credit usage in response |
|
|
58
|
+
|
|
59
|
+
## POST /map
|
|
60
|
+
Discover URLs without extracting (cheaper).
|
|
61
|
+
|
|
62
|
+
Same selection fields as crawl, no `extract_depth`. API also supports `timeout` and `include_usage`.
|
|
63
|
+
|
|
64
|
+
## POST /research
|
|
65
|
+
Start an async research job.
|
|
66
|
+
|
|
67
|
+
| Field | Type | Notes |
|
|
68
|
+
|---|---|---|
|
|
69
|
+
| `input` | string | required |
|
|
70
|
+
| `model` | `mini` \| `pro` \| `auto` | |
|
|
71
|
+
| `stream` | bool | SSE |
|
|
72
|
+
| `output_schema` | JSON Schema | |
|
|
73
|
+
| `citation_format` | `numbered` \| `mla` \| `apa` \| `chicago` | |
|
|
74
|
+
|
|
75
|
+
Returns `{ request_id, status: "pending", model }` quickly.
|
|
76
|
+
|
|
77
|
+
## GET /research/{request_id}
|
|
78
|
+
Poll a research job. Free.
|
|
79
|
+
|
|
80
|
+
Returns `{ status: "pending" | "completed" | "failed", content, sources }`.
|
|
81
|
+
|
|
82
|
+
## GET /usage
|
|
83
|
+
Account usage. Free.
|
|
84
|
+
|
|
85
|
+
## Status codes
|
|
86
|
+
|
|
87
|
+
- `200` ok
|
|
88
|
+
- `401` bad/missing key
|
|
89
|
+
- `429` rate limit (retry with backoff)
|
|
90
|
+
- `432`, `433` plan/quota limits — escalate to user
|
package/src/env.mjs
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
// Key discovery for library mode.
|
|
2
|
+
// Priority (each level can contribute; results merged + deduped):
|
|
3
|
+
// 1. Explicit opts (opts.tavilyKey / opts.tavilyKeys / parallel*)
|
|
4
|
+
// 2. process.env (TAVILY_API_KEYS comma-separated + TAVILY_API_KEY)
|
|
5
|
+
// 3. .env file at process.cwd() (lightweight regex parser, no dotenv dep)
|
|
6
|
+
// 4. ~/.config/surf/keys.json (CLI persistent store, fallback only)
|
|
7
|
+
|
|
8
|
+
import { existsSync, promises as fs } from 'node:fs';
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
import { loadState } from './lib/state.mjs';
|
|
11
|
+
|
|
12
|
+
const ENV_FILE_CACHE = new Map();
|
|
13
|
+
|
|
14
|
+
async function loadDotenv(dir) {
|
|
15
|
+
if (ENV_FILE_CACHE.has(dir)) return ENV_FILE_CACHE.get(dir);
|
|
16
|
+
const p = path.join(dir, '.env');
|
|
17
|
+
const out = {};
|
|
18
|
+
if (existsSync(p)) {
|
|
19
|
+
try {
|
|
20
|
+
const txt = await fs.readFile(p, 'utf8');
|
|
21
|
+
for (const line of txt.split('\n')) {
|
|
22
|
+
const m = line.match(/^\s*([A-Z][A-Z0-9_]*)\s*=\s*"?([^"#]*?)"?\s*(?:#.*)?$/);
|
|
23
|
+
if (m) out[m[1]] = m[2].trim();
|
|
24
|
+
}
|
|
25
|
+
} catch {}
|
|
26
|
+
}
|
|
27
|
+
ENV_FILE_CACHE.set(dir, out);
|
|
28
|
+
return out;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function splitCsv(s) {
|
|
32
|
+
return typeof s === 'string'
|
|
33
|
+
? s.split(',').map(x => x.trim()).filter(Boolean)
|
|
34
|
+
: [];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function arrayify(v) {
|
|
38
|
+
if (!v) return [];
|
|
39
|
+
return Array.isArray(v) ? v.filter(Boolean) : [v].filter(Boolean);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Resolve API keys for both providers using the discovery hierarchy.
|
|
44
|
+
*
|
|
45
|
+
* @param {object} opts
|
|
46
|
+
* @param {string|string[]} [opts.tavilyKey] - single key or array
|
|
47
|
+
* @param {string[]} [opts.tavilyKeys] - array (alias)
|
|
48
|
+
* @param {string|string[]} [opts.parallelKey] - single or array
|
|
49
|
+
* @param {string[]} [opts.parallelKeys] - array (alias)
|
|
50
|
+
* @param {boolean} [opts.skipDotenv=false] - skip .env scanning
|
|
51
|
+
* @param {boolean} [opts.skipConfigFile=false] - skip ~/.config/surf/keys.json
|
|
52
|
+
* @param {string} [opts.cwd=process.cwd()]
|
|
53
|
+
* @returns {Promise<{tavily: string[], parallel: string[]}>}
|
|
54
|
+
*/
|
|
55
|
+
export async function discoverKeys(opts = {}) {
|
|
56
|
+
const cwd = opts.cwd || process.cwd();
|
|
57
|
+
|
|
58
|
+
// Level 1: explicit
|
|
59
|
+
const explicitTavily = [
|
|
60
|
+
...arrayify(opts.tavilyKey),
|
|
61
|
+
...arrayify(opts.tavilyKeys),
|
|
62
|
+
];
|
|
63
|
+
const explicitParallel = [
|
|
64
|
+
...arrayify(opts.parallelKey),
|
|
65
|
+
...arrayify(opts.parallelKeys),
|
|
66
|
+
];
|
|
67
|
+
|
|
68
|
+
// Level 2: process.env
|
|
69
|
+
const envTavily = [
|
|
70
|
+
...splitCsv(process.env.TAVILY_API_KEYS),
|
|
71
|
+
process.env.TAVILY_API_KEY,
|
|
72
|
+
].filter(Boolean);
|
|
73
|
+
const envParallel = [
|
|
74
|
+
...splitCsv(process.env.PARALLEL_API_KEYS),
|
|
75
|
+
process.env.PARALLEL_API_KEY,
|
|
76
|
+
].filter(Boolean);
|
|
77
|
+
|
|
78
|
+
// Level 3: .env file
|
|
79
|
+
let dotenvTavily = [];
|
|
80
|
+
let dotenvParallel = [];
|
|
81
|
+
if (!opts.skipDotenv) {
|
|
82
|
+
const env = await loadDotenv(cwd);
|
|
83
|
+
dotenvTavily = [
|
|
84
|
+
...splitCsv(env.TAVILY_API_KEYS),
|
|
85
|
+
env.TAVILY_API_KEY,
|
|
86
|
+
].filter(Boolean);
|
|
87
|
+
dotenvParallel = [
|
|
88
|
+
...splitCsv(env.PARALLEL_API_KEYS),
|
|
89
|
+
env.PARALLEL_API_KEY,
|
|
90
|
+
].filter(Boolean);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Level 4: ~/.config/surf/keys.json (only if nothing yet from 1-3)
|
|
94
|
+
let cfgTavily = [];
|
|
95
|
+
let cfgParallel = [];
|
|
96
|
+
const noneSoFarTavily = !explicitTavily.length && !envTavily.length && !dotenvTavily.length;
|
|
97
|
+
const noneSoFarParallel = !explicitParallel.length && !envParallel.length && !dotenvParallel.length;
|
|
98
|
+
if (!opts.skipConfigFile && (noneSoFarTavily || noneSoFarParallel)) {
|
|
99
|
+
try {
|
|
100
|
+
const state = await loadState();
|
|
101
|
+
if (noneSoFarTavily) cfgTavily = state.tavily.keys || [];
|
|
102
|
+
if (noneSoFarParallel) cfgParallel = state.parallel.keys || [];
|
|
103
|
+
} catch {}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
tavily: [...new Set([...explicitTavily, ...envTavily, ...dotenvTavily, ...cfgTavily])],
|
|
108
|
+
parallel: [...new Set([...explicitParallel, ...envParallel, ...dotenvParallel, ...cfgParallel])],
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Build an in-memory state object that the dispatch layer can use directly
|
|
114
|
+
* without touching ~/.config/surf/keys.json.
|
|
115
|
+
*/
|
|
116
|
+
export async function buildInMemoryState(opts = {}) {
|
|
117
|
+
const { tavily, parallel } = await discoverKeys(opts);
|
|
118
|
+
return {
|
|
119
|
+
schema_version: 1,
|
|
120
|
+
tavily: { keys: tavily, current: 0, burned: [] },
|
|
121
|
+
parallel: { keys: parallel, current: 0, burned: [] },
|
|
122
|
+
last_ok_provider: null,
|
|
123
|
+
_inMemory: true,
|
|
124
|
+
};
|
|
125
|
+
}
|
package/src/index.mjs
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// surf-skill — library entry point.
|
|
2
|
+
// Named exports for each operation. CLI is at bin/surf-skill.mjs.
|
|
3
|
+
//
|
|
4
|
+
// Usage:
|
|
5
|
+
// import { search, extract, research } from 'surf-skill';
|
|
6
|
+
// const r = await search('claude api', { max: 3 });
|
|
7
|
+
//
|
|
8
|
+
// Keys are auto-discovered (opts > process.env > .env > ~/.config/surf/keys.json).
|
|
9
|
+
// Pass `tavilyKeys: [...]` / `parallelKeys: [...]` to override.
|
|
10
|
+
|
|
11
|
+
export { search } from './lib/api/search.mjs';
|
|
12
|
+
export { extract } from './lib/api/extract.mjs';
|
|
13
|
+
export { crawl } from './lib/api/crawl.mjs';
|
|
14
|
+
export { map } from './lib/api/map.mjs';
|
|
15
|
+
export {
|
|
16
|
+
research,
|
|
17
|
+
researchStart,
|
|
18
|
+
researchPoll,
|
|
19
|
+
} from './lib/api/research.mjs';
|
|
20
|
+
|
|
21
|
+
export { discoverKeys, buildInMemoryState } from './env.mjs';
|
|
22
|
+
export { setSilent } from './lib/progress.mjs';
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Runs after `npm install`. Idempotent. Must never fail npm install.
|
|
3
|
+
// - Detects global vs local install.
|
|
4
|
+
// - Global: symlinks/copies package into the 4 supported harness skill dirs,
|
|
5
|
+
// creates ~/.config/surf/keys.json skeleton, cleans legacy 'tavily'/'surf'/'tvly'
|
|
6
|
+
// symlinks from prior versions.
|
|
7
|
+
// - Local: just prints "installed as library" and exits.
|
|
8
|
+
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
import { fileURLToPath } from 'node:url';
|
|
11
|
+
import {
|
|
12
|
+
installSkill,
|
|
13
|
+
cleanupLegacy,
|
|
14
|
+
ensureKeysSkeleton,
|
|
15
|
+
} from '../lib/harness-install.mjs';
|
|
16
|
+
|
|
17
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
18
|
+
const pkgRoot = path.resolve(__dirname, '..', '..');
|
|
19
|
+
|
|
20
|
+
const isGlobal = process.env.npm_config_global === 'true';
|
|
21
|
+
// Some CI environments don't set npm_config_global; also treat the case where
|
|
22
|
+
// __dirname is under a global node_modules path as "global enough".
|
|
23
|
+
const looksGlobal = /node_modules\/surf-skill\/src\/install$/.test(__dirname) ||
|
|
24
|
+
/node_modules\\surf-skill\\src\\install$/.test(__dirname);
|
|
25
|
+
|
|
26
|
+
async function main() {
|
|
27
|
+
if (!isGlobal && !looksGlobal) {
|
|
28
|
+
// Local install: don't touch user system. Library mode.
|
|
29
|
+
process.stdout.write('surf-skill installed as a library (npm i surf-skill).\n');
|
|
30
|
+
process.stdout.write(' → For the global CLI: npm i -g surf-skill\n');
|
|
31
|
+
process.stdout.write(' → To import: import { search } from "surf-skill"\n');
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Cleanup legacy symlinks from earlier versions BEFORE creating new ones.
|
|
36
|
+
const legacy = await cleanupLegacy();
|
|
37
|
+
for (const r of legacy) {
|
|
38
|
+
process.stdout.write(`✓ removed legacy ${r.removed}\n`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Install symlinks into each harness skill dir.
|
|
42
|
+
const installed = await installSkill(pkgRoot);
|
|
43
|
+
for (const r of installed) {
|
|
44
|
+
if (r.action === 'error') {
|
|
45
|
+
process.stdout.write(`⚠ ${r.dir}: ${r.error}\n`);
|
|
46
|
+
} else {
|
|
47
|
+
const verb = {
|
|
48
|
+
symlinked: '✓ symlinked',
|
|
49
|
+
copied: '✓ copied (no symlink permission)',
|
|
50
|
+
'kept-symlink': '✓ already linked',
|
|
51
|
+
'preserved-existing': 'ℹ preserved your existing copy at',
|
|
52
|
+
}[r.action] || r.action;
|
|
53
|
+
process.stdout.write(`${verb} ${r.dir}\n`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Create state dir + skeleton keys.json.
|
|
58
|
+
const skel = await ensureKeysSkeleton();
|
|
59
|
+
if (skel.created) process.stdout.write(`✓ created ${skel.created} (chmod 600)\n`);
|
|
60
|
+
|
|
61
|
+
process.stdout.write('\n');
|
|
62
|
+
process.stdout.write('✓ surf-skill 2.0.0 installed globally\n');
|
|
63
|
+
process.stdout.write(' → Run `surf-skill setup` to add Tavily/Parallel keys\n');
|
|
64
|
+
process.stdout.write(' (or just run any command — wizard auto-launches in TTY)\n');
|
|
65
|
+
process.stdout.write(' → `surf-skill --help` for the full command list\n');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
main().catch(e => {
|
|
69
|
+
// NEVER fail npm install. Print warning + exit 0.
|
|
70
|
+
process.stderr.write(`surf-skill postinstall warning: ${e.message}\n`);
|
|
71
|
+
process.stderr.write(' (skill is installed; harness symlinks may need manual setup)\n');
|
|
72
|
+
process.exit(0);
|
|
73
|
+
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Runs before `npm rm -g surf-skill`. Removes our symlinks; leaves user state.
|
|
3
|
+
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import { uninstallSkill } from '../lib/harness-install.mjs';
|
|
7
|
+
|
|
8
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
9
|
+
const pkgRoot = path.resolve(__dirname, '..', '..');
|
|
10
|
+
|
|
11
|
+
async function main() {
|
|
12
|
+
const results = await uninstallSkill(pkgRoot);
|
|
13
|
+
for (const r of results) {
|
|
14
|
+
if (r.removed) process.stdout.write(`✓ removed ${r.dir}\n`);
|
|
15
|
+
else if (r.error) process.stdout.write(`⚠ ${r.dir}: ${r.error}\n`);
|
|
16
|
+
}
|
|
17
|
+
process.stdout.write('\nsurf-skill uninstalled.\n');
|
|
18
|
+
process.stdout.write(' → Your keys at ~/.config/surf/keys.json are preserved.\n');
|
|
19
|
+
process.stdout.write(' → To wipe: rm -rf ~/.config/surf ~/.cache/surf\n');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
main().catch(e => {
|
|
23
|
+
process.stderr.write(`surf-skill preuninstall warning: ${e.message}\n`);
|
|
24
|
+
process.exit(0);
|
|
25
|
+
});
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// Library wrapper for `crawl` (Tavily only).
|
|
2
|
+
|
|
3
|
+
import { dispatch } from '../dispatch.mjs';
|
|
4
|
+
import { buildInMemoryState } from '../../env.mjs';
|
|
5
|
+
import { setSilent } from '../progress.mjs';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Recursive site crawl. Tavily-only.
|
|
9
|
+
*
|
|
10
|
+
* @param {string} url - root URL
|
|
11
|
+
* @param {object} [opts]
|
|
12
|
+
* @param {number} [opts.maxDepth=1]
|
|
13
|
+
* @param {number} [opts.maxBreadth=20]
|
|
14
|
+
* @param {number} [opts.limit=50]
|
|
15
|
+
* @param {string} [opts.instructions]
|
|
16
|
+
* @param {string|string[]} [opts.selectPaths]
|
|
17
|
+
* @param {string|string[]} [opts.excludePaths]
|
|
18
|
+
* @param {string} [opts.tavilyKey]
|
|
19
|
+
* @returns {Promise<object>} normalized envelope
|
|
20
|
+
*/
|
|
21
|
+
export async function crawl(url, opts = {}) {
|
|
22
|
+
if (opts.quiet !== false) setSilent(true);
|
|
23
|
+
if (!url || typeof url !== 'string') throw new Error('crawl: url required');
|
|
24
|
+
|
|
25
|
+
const state = await buildInMemoryState(opts);
|
|
26
|
+
return dispatch(
|
|
27
|
+
'crawl',
|
|
28
|
+
{
|
|
29
|
+
url,
|
|
30
|
+
maxDepth: opts.maxDepth,
|
|
31
|
+
maxBreadth: opts.maxBreadth,
|
|
32
|
+
limit: opts.limit,
|
|
33
|
+
instructions: opts.instructions,
|
|
34
|
+
selectPaths: opts.selectPaths,
|
|
35
|
+
selectDomains: opts.selectDomains,
|
|
36
|
+
excludePaths: opts.excludePaths,
|
|
37
|
+
excludeDomains: opts.excludeDomains,
|
|
38
|
+
allowExternal: opts.allowExternal,
|
|
39
|
+
images: opts.images,
|
|
40
|
+
categories: opts.categories,
|
|
41
|
+
extractDepth: opts.extractDepth || 'basic',
|
|
42
|
+
format: opts.format || 'markdown',
|
|
43
|
+
query: opts.query,
|
|
44
|
+
chunks: opts.chunks,
|
|
45
|
+
timeout: opts.timeout,
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
provider: opts.provider,
|
|
49
|
+
'no-fallback': opts.noFallback,
|
|
50
|
+
'no-cache': opts.noCache,
|
|
51
|
+
'confirm-expensive': true,
|
|
52
|
+
},
|
|
53
|
+
{ state }
|
|
54
|
+
);
|
|
55
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// Library wrapper for `extract`.
|
|
2
|
+
|
|
3
|
+
import { dispatch } from '../dispatch.mjs';
|
|
4
|
+
import { buildInMemoryState } from '../../env.mjs';
|
|
5
|
+
import { setSilent } from '../progress.mjs';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Extract clean content from URLs.
|
|
9
|
+
*
|
|
10
|
+
* @param {string|string[]} urls - one or more URLs (max 20)
|
|
11
|
+
* @param {object} [opts]
|
|
12
|
+
* @param {string|string[]} [opts.tavilyKey|opts.tavilyKeys]
|
|
13
|
+
* @param {string|string[]} [opts.parallelKey|opts.parallelKeys]
|
|
14
|
+
* @param {'tavily'|'parallel'} [opts.provider]
|
|
15
|
+
* @param {'basic'|'advanced'} [opts.depth='basic']
|
|
16
|
+
* @param {string} [opts.query] - focus extraction on this topic
|
|
17
|
+
* @param {boolean} [opts.quiet=true]
|
|
18
|
+
* @returns {Promise<object>} normalized envelope
|
|
19
|
+
*/
|
|
20
|
+
export async function extract(urls, opts = {}) {
|
|
21
|
+
if (opts.quiet !== false) setSilent(true);
|
|
22
|
+
const urlList = Array.isArray(urls) ? urls : [urls];
|
|
23
|
+
if (!urlList.length) throw new Error('extract: at least 1 URL required');
|
|
24
|
+
if (urlList.length > 20) throw new Error('extract: max 20 URLs per call');
|
|
25
|
+
|
|
26
|
+
const state = await buildInMemoryState(opts);
|
|
27
|
+
return dispatch(
|
|
28
|
+
'extract',
|
|
29
|
+
{
|
|
30
|
+
urls: urlList,
|
|
31
|
+
depth: opts.depth || 'basic',
|
|
32
|
+
format: opts.format || 'markdown',
|
|
33
|
+
query: opts.query,
|
|
34
|
+
chunks: opts.chunks,
|
|
35
|
+
images: opts.images,
|
|
36
|
+
extractTimeout: opts.extractTimeout,
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
provider: opts.provider,
|
|
40
|
+
'no-fallback': opts.noFallback,
|
|
41
|
+
'no-cache': opts.noCache,
|
|
42
|
+
'confirm-expensive': true,
|
|
43
|
+
},
|
|
44
|
+
{ state }
|
|
45
|
+
);
|
|
46
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// Library wrapper for `map` (URL discovery; Tavily only).
|
|
2
|
+
|
|
3
|
+
import { dispatch } from '../dispatch.mjs';
|
|
4
|
+
import { buildInMemoryState } from '../../env.mjs';
|
|
5
|
+
import { setSilent } from '../progress.mjs';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Discover URLs on a site without fetching content. Tavily-only.
|
|
9
|
+
*
|
|
10
|
+
* @param {string} url - root URL
|
|
11
|
+
* @param {object} [opts]
|
|
12
|
+
* @returns {Promise<object>} normalized envelope with { base_url, urls[] }
|
|
13
|
+
*/
|
|
14
|
+
export async function map(url, opts = {}) {
|
|
15
|
+
if (opts.quiet !== false) setSilent(true);
|
|
16
|
+
if (!url || typeof url !== 'string') throw new Error('map: url required');
|
|
17
|
+
|
|
18
|
+
const state = await buildInMemoryState(opts);
|
|
19
|
+
return dispatch(
|
|
20
|
+
'map',
|
|
21
|
+
{
|
|
22
|
+
url,
|
|
23
|
+
maxDepth: opts.maxDepth,
|
|
24
|
+
maxBreadth: opts.maxBreadth,
|
|
25
|
+
limit: opts.limit,
|
|
26
|
+
instructions: opts.instructions,
|
|
27
|
+
selectPaths: opts.selectPaths,
|
|
28
|
+
selectDomains: opts.selectDomains,
|
|
29
|
+
excludePaths: opts.excludePaths,
|
|
30
|
+
excludeDomains: opts.excludeDomains,
|
|
31
|
+
allowExternal: opts.allowExternal,
|
|
32
|
+
categories: opts.categories,
|
|
33
|
+
timeout: opts.timeout,
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
provider: opts.provider,
|
|
37
|
+
'no-fallback': opts.noFallback,
|
|
38
|
+
'no-cache': opts.noCache,
|
|
39
|
+
'confirm-expensive': true,
|
|
40
|
+
},
|
|
41
|
+
{ state }
|
|
42
|
+
);
|
|
43
|
+
}
|