crw-sdk 0.22.0 → 0.24.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/README.md +5 -3
- package/dist/cjs/client.js +56 -12
- package/dist/esm/client.d.ts +6 -2
- package/dist/esm/client.js +56 -12
- package/dist/esm/types.d.ts +55 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -58,14 +58,16 @@ CRW_LOCAL=1 node app.js
|
|
|
58
58
|
| `changeTrackingDiff(cur, prev?)` | Diff vs a prior snapshot | HTTP |
|
|
59
59
|
| `close()` | Shut down the local subprocess | — |
|
|
60
60
|
|
|
61
|
-
¹ Local search needs a
|
|
61
|
+
¹ Local search needs a search backend configured on the engine.
|
|
62
62
|
|
|
63
63
|
```ts
|
|
64
|
-
// Structured extraction:
|
|
65
|
-
const
|
|
64
|
+
// Structured extraction (async job → per-URL results array):
|
|
65
|
+
const results = await crw.extract({
|
|
66
66
|
urls: ["https://example.com"],
|
|
67
67
|
schema: { type: "object", properties: { title: { type: "string" } } },
|
|
68
68
|
});
|
|
69
|
+
// results: [{ url, status, data, error, llmUsage }]
|
|
70
|
+
for (const r of results) if (r.status === "completed") console.log(r.url, r.data);
|
|
69
71
|
|
|
70
72
|
// Parse a PDF:
|
|
71
73
|
import { readFileSync } from "node:fs";
|
package/dist/cjs/client.js
CHANGED
|
@@ -95,7 +95,7 @@ class CrwClient {
|
|
|
95
95
|
return result.links ?? [];
|
|
96
96
|
}
|
|
97
97
|
/**
|
|
98
|
-
* Works in both modes; local mode needs a
|
|
98
|
+
* Works in both modes; local mode needs a search backend configured on the engine.
|
|
99
99
|
*/
|
|
100
100
|
async search(query, opts = {}) {
|
|
101
101
|
const { limit = 5, lang, tbs, sources, categories, scrapeOptions, ...rest } = opts;
|
|
@@ -111,8 +111,36 @@ class CrwClient {
|
|
|
111
111
|
if (scrapeOptions)
|
|
112
112
|
args.scrapeOptions = scrapeOptions;
|
|
113
113
|
Object.assign(args, rest);
|
|
114
|
-
if (this.apiUrl)
|
|
115
|
-
|
|
114
|
+
if (this.apiUrl) {
|
|
115
|
+
// `data` IS the results, in whatever shape the caller asked for: a flat
|
|
116
|
+
// array, a `{ web, news }` object when `sources` is set, or the engine's
|
|
117
|
+
// `{ results, answer, … }` when you point the client at a self-hosted
|
|
118
|
+
// server. Return it untouched — reshaping it here silently emptied both the
|
|
119
|
+
// grouped and the self-hosted cases.
|
|
120
|
+
//
|
|
121
|
+
// `answer` / `citations` / `llmUsage` ride BESIDE `data` on the managed API,
|
|
122
|
+
// and the old `data` unwrap dropped them: you could pay for an AI answer and
|
|
123
|
+
// have no way to read it. Attach them non-enumerably, so `results.answer`
|
|
124
|
+
// works while `for…of`, spread and JSON.stringify keep seeing exactly the
|
|
125
|
+
// array they saw before.
|
|
126
|
+
const raw = await this.httpRequest("POST", "/v1/search", args, {
|
|
127
|
+
raw: true,
|
|
128
|
+
});
|
|
129
|
+
const data = (raw.data ?? []);
|
|
130
|
+
if (data !== null && typeof data === "object") {
|
|
131
|
+
for (const key of ["answer", "citations", "llmUsage"]) {
|
|
132
|
+
if (raw[key] !== undefined) {
|
|
133
|
+
Object.defineProperty(data, key, {
|
|
134
|
+
value: raw[key],
|
|
135
|
+
enumerable: false,
|
|
136
|
+
configurable: true,
|
|
137
|
+
writable: true,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return data;
|
|
143
|
+
}
|
|
116
144
|
return this.localTransport().toolCall("crw_search", args);
|
|
117
145
|
}
|
|
118
146
|
/**
|
|
@@ -170,29 +198,45 @@ class CrwClient {
|
|
|
170
198
|
Object.assign(args, rest);
|
|
171
199
|
return this.localTransport().toolCall("crw_parse_file", args);
|
|
172
200
|
}
|
|
173
|
-
/**
|
|
201
|
+
/**
|
|
202
|
+
* Native multi-URL structured extraction (HTTP mode only). Starts an async
|
|
203
|
+
* `/v1/extract` job and polls until complete, returning a per-URL results
|
|
204
|
+
* array (`[{ url, status, data, error, llmUsage }]`) in request order.
|
|
205
|
+
*/
|
|
174
206
|
async extract(opts) {
|
|
175
207
|
if (!this.apiUrl)
|
|
176
208
|
throw new errors_js_1.CrwError(httpOnlyHint("extract", "LLM extract job endpoint"));
|
|
177
|
-
const { urls, prompt, schema,
|
|
209
|
+
const { urls, prompt, schema, basis, llmApiKey, llmProvider, llmModel, pollInterval = 2, timeout = 120 } = opts;
|
|
178
210
|
const body = { urls: [...urls] };
|
|
179
211
|
if (prompt !== undefined)
|
|
180
212
|
body.prompt = prompt;
|
|
181
213
|
if (schema !== undefined)
|
|
182
214
|
body.schema = schema;
|
|
183
|
-
if (
|
|
184
|
-
body.
|
|
185
|
-
|
|
215
|
+
if (basis !== undefined)
|
|
216
|
+
body.basis = basis;
|
|
217
|
+
if (llmApiKey !== undefined)
|
|
218
|
+
body.llmApiKey = llmApiKey;
|
|
219
|
+
if (llmProvider !== undefined)
|
|
220
|
+
body.llmProvider = llmProvider;
|
|
221
|
+
if (llmModel !== undefined)
|
|
222
|
+
body.llmModel = llmModel;
|
|
223
|
+
const start = await this.httpRequest("POST", "/v1/extract", body, { raw: true });
|
|
186
224
|
const jobId = start.id;
|
|
187
|
-
|
|
188
|
-
|
|
225
|
+
// The managed API answers synchronously: the extraction is already finished and
|
|
226
|
+
// the payload is in this first response, with no job to poll. Only the async
|
|
227
|
+
// path hands back an `id`. Demanding one made every managed extract() throw.
|
|
228
|
+
if (!jobId) {
|
|
229
|
+
if (Array.isArray(start.results))
|
|
230
|
+
return start.results;
|
|
231
|
+
throw new errors_js_1.CrwError(`extract returned neither a job id nor results: ${JSON.stringify(start)}`);
|
|
232
|
+
}
|
|
189
233
|
const deadline = Date.now() + timeout * 1000;
|
|
190
234
|
for (;;) {
|
|
191
235
|
if (Date.now() > deadline)
|
|
192
236
|
throw new errors_js_1.CrwTimeoutError(`Extract ${jobId} timed out after ${timeout}s`);
|
|
193
|
-
const status = await this.httpRequest("GET", `/
|
|
237
|
+
const status = await this.httpRequest("GET", `/v1/extract/${jobId}`, undefined, { raw: true, checkSuccess: false });
|
|
194
238
|
if (status.status === "completed")
|
|
195
|
-
return status.
|
|
239
|
+
return status.results ?? [];
|
|
196
240
|
if (status.status === "failed")
|
|
197
241
|
throw new errors_js_1.CrwError(`Extract failed: ${status.error ?? "unknown"}`);
|
|
198
242
|
await sleep(pollInterval * 1000);
|
package/dist/esm/client.d.ts
CHANGED
|
@@ -17,7 +17,7 @@ export declare class CrwClient {
|
|
|
17
17
|
crawl(url: string, opts?: CrawlOptions): Promise<CrawlResult>;
|
|
18
18
|
map(url: string, opts?: MapOptions): Promise<string[]>;
|
|
19
19
|
/**
|
|
20
|
-
* Works in both modes; local mode needs a
|
|
20
|
+
* Works in both modes; local mode needs a search backend configured on the engine.
|
|
21
21
|
*/
|
|
22
22
|
search(query: string, opts?: SearchOptions): Promise<SearchResult>;
|
|
23
23
|
/**
|
|
@@ -37,7 +37,11 @@ export declare class CrwClient {
|
|
|
37
37
|
* Parse a document (PDF) into markdown / structured JSON. Works in both modes.
|
|
38
38
|
*/
|
|
39
39
|
parseFile(content: Uint8Array, opts?: ParseFileOptions): Promise<ParseResult>;
|
|
40
|
-
/**
|
|
40
|
+
/**
|
|
41
|
+
* Native multi-URL structured extraction (HTTP mode only). Starts an async
|
|
42
|
+
* `/v1/extract` job and polls until complete, returning a per-URL results
|
|
43
|
+
* array (`[{ url, status, data, error, llmUsage }]`) in request order.
|
|
44
|
+
*/
|
|
41
45
|
extract(opts: ExtractOptions): Promise<ExtractResult>;
|
|
42
46
|
/** Scrape many URLs in one async batch job (HTTP mode only). */
|
|
43
47
|
batchScrape(urls: string[], opts?: BatchScrapeOptions): Promise<BatchResult>;
|
package/dist/esm/client.js
CHANGED
|
@@ -92,7 +92,7 @@ export class CrwClient {
|
|
|
92
92
|
return result.links ?? [];
|
|
93
93
|
}
|
|
94
94
|
/**
|
|
95
|
-
* Works in both modes; local mode needs a
|
|
95
|
+
* Works in both modes; local mode needs a search backend configured on the engine.
|
|
96
96
|
*/
|
|
97
97
|
async search(query, opts = {}) {
|
|
98
98
|
const { limit = 5, lang, tbs, sources, categories, scrapeOptions, ...rest } = opts;
|
|
@@ -108,8 +108,36 @@ export class CrwClient {
|
|
|
108
108
|
if (scrapeOptions)
|
|
109
109
|
args.scrapeOptions = scrapeOptions;
|
|
110
110
|
Object.assign(args, rest);
|
|
111
|
-
if (this.apiUrl)
|
|
112
|
-
|
|
111
|
+
if (this.apiUrl) {
|
|
112
|
+
// `data` IS the results, in whatever shape the caller asked for: a flat
|
|
113
|
+
// array, a `{ web, news }` object when `sources` is set, or the engine's
|
|
114
|
+
// `{ results, answer, … }` when you point the client at a self-hosted
|
|
115
|
+
// server. Return it untouched — reshaping it here silently emptied both the
|
|
116
|
+
// grouped and the self-hosted cases.
|
|
117
|
+
//
|
|
118
|
+
// `answer` / `citations` / `llmUsage` ride BESIDE `data` on the managed API,
|
|
119
|
+
// and the old `data` unwrap dropped them: you could pay for an AI answer and
|
|
120
|
+
// have no way to read it. Attach them non-enumerably, so `results.answer`
|
|
121
|
+
// works while `for…of`, spread and JSON.stringify keep seeing exactly the
|
|
122
|
+
// array they saw before.
|
|
123
|
+
const raw = await this.httpRequest("POST", "/v1/search", args, {
|
|
124
|
+
raw: true,
|
|
125
|
+
});
|
|
126
|
+
const data = (raw.data ?? []);
|
|
127
|
+
if (data !== null && typeof data === "object") {
|
|
128
|
+
for (const key of ["answer", "citations", "llmUsage"]) {
|
|
129
|
+
if (raw[key] !== undefined) {
|
|
130
|
+
Object.defineProperty(data, key, {
|
|
131
|
+
value: raw[key],
|
|
132
|
+
enumerable: false,
|
|
133
|
+
configurable: true,
|
|
134
|
+
writable: true,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return data;
|
|
140
|
+
}
|
|
113
141
|
return this.localTransport().toolCall("crw_search", args);
|
|
114
142
|
}
|
|
115
143
|
/**
|
|
@@ -167,29 +195,45 @@ export class CrwClient {
|
|
|
167
195
|
Object.assign(args, rest);
|
|
168
196
|
return this.localTransport().toolCall("crw_parse_file", args);
|
|
169
197
|
}
|
|
170
|
-
/**
|
|
198
|
+
/**
|
|
199
|
+
* Native multi-URL structured extraction (HTTP mode only). Starts an async
|
|
200
|
+
* `/v1/extract` job and polls until complete, returning a per-URL results
|
|
201
|
+
* array (`[{ url, status, data, error, llmUsage }]`) in request order.
|
|
202
|
+
*/
|
|
171
203
|
async extract(opts) {
|
|
172
204
|
if (!this.apiUrl)
|
|
173
205
|
throw new CrwError(httpOnlyHint("extract", "LLM extract job endpoint"));
|
|
174
|
-
const { urls, prompt, schema,
|
|
206
|
+
const { urls, prompt, schema, basis, llmApiKey, llmProvider, llmModel, pollInterval = 2, timeout = 120 } = opts;
|
|
175
207
|
const body = { urls: [...urls] };
|
|
176
208
|
if (prompt !== undefined)
|
|
177
209
|
body.prompt = prompt;
|
|
178
210
|
if (schema !== undefined)
|
|
179
211
|
body.schema = schema;
|
|
180
|
-
if (
|
|
181
|
-
body.
|
|
182
|
-
|
|
212
|
+
if (basis !== undefined)
|
|
213
|
+
body.basis = basis;
|
|
214
|
+
if (llmApiKey !== undefined)
|
|
215
|
+
body.llmApiKey = llmApiKey;
|
|
216
|
+
if (llmProvider !== undefined)
|
|
217
|
+
body.llmProvider = llmProvider;
|
|
218
|
+
if (llmModel !== undefined)
|
|
219
|
+
body.llmModel = llmModel;
|
|
220
|
+
const start = await this.httpRequest("POST", "/v1/extract", body, { raw: true });
|
|
183
221
|
const jobId = start.id;
|
|
184
|
-
|
|
185
|
-
|
|
222
|
+
// The managed API answers synchronously: the extraction is already finished and
|
|
223
|
+
// the payload is in this first response, with no job to poll. Only the async
|
|
224
|
+
// path hands back an `id`. Demanding one made every managed extract() throw.
|
|
225
|
+
if (!jobId) {
|
|
226
|
+
if (Array.isArray(start.results))
|
|
227
|
+
return start.results;
|
|
228
|
+
throw new CrwError(`extract returned neither a job id nor results: ${JSON.stringify(start)}`);
|
|
229
|
+
}
|
|
186
230
|
const deadline = Date.now() + timeout * 1000;
|
|
187
231
|
for (;;) {
|
|
188
232
|
if (Date.now() > deadline)
|
|
189
233
|
throw new CrwTimeoutError(`Extract ${jobId} timed out after ${timeout}s`);
|
|
190
|
-
const status = await this.httpRequest("GET", `/
|
|
234
|
+
const status = await this.httpRequest("GET", `/v1/extract/${jobId}`, undefined, { raw: true, checkSuccess: false });
|
|
191
235
|
if (status.status === "completed")
|
|
192
|
-
return status.
|
|
236
|
+
return status.results ?? [];
|
|
193
237
|
if (status.status === "failed")
|
|
194
238
|
throw new CrwError(`Extract failed: ${status.error ?? "unknown"}`);
|
|
195
239
|
await sleep(pollInterval * 1000);
|
package/dist/esm/types.d.ts
CHANGED
|
@@ -25,9 +25,53 @@ export interface ScrapeOptions {
|
|
|
25
25
|
waitFor?: number;
|
|
26
26
|
/** JSON Schema for structured LLM extraction (auto-adds the `json` format). */
|
|
27
27
|
jsonSchema?: Json;
|
|
28
|
+
/**
|
|
29
|
+
* Return per-field evidence alongside `json`. Every top-level scalar property
|
|
30
|
+
* of `jsonSchema` comes back as a {@link Basis}. Requires `jsonSchema`.
|
|
31
|
+
*/
|
|
32
|
+
basis?: boolean;
|
|
28
33
|
/** Any other engine scrape option, passed through verbatim. */
|
|
29
34
|
[key: string]: unknown;
|
|
30
35
|
}
|
|
36
|
+
/** How well an extracted field is attributed to its source. */
|
|
37
|
+
export type FieldStatus = "supported" | "unverified" | "unsupported" | "notFound";
|
|
38
|
+
/** The citation backing an extracted value. Every field is server-produced. */
|
|
39
|
+
export interface EvidenceCitation {
|
|
40
|
+
/** The document the engine fetched. Never a model-supplied url. */
|
|
41
|
+
url: string;
|
|
42
|
+
title?: string;
|
|
43
|
+
/**
|
|
44
|
+
* Verbatim span from the canonical source text. Absent when the span could
|
|
45
|
+
* not be established (`status: "unverified"`).
|
|
46
|
+
*/
|
|
47
|
+
excerpt?: string;
|
|
48
|
+
/** `sha256:<hex>` of the exact text sent to the model. */
|
|
49
|
+
sourceHash: string;
|
|
50
|
+
/** Which text `sourceHash` covers. `"llmInput"` for extraction basis. */
|
|
51
|
+
sourceTextKind: string;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Per-field extraction evidence. Emitted only for top-level scalar schema
|
|
55
|
+
* properties, and only when the request set `basis: true`.
|
|
56
|
+
*
|
|
57
|
+
* `status` is honest: a field whose attribution could not be verified is marked
|
|
58
|
+
* `unverified`/`unsupported` rather than given a fabricated citation.
|
|
59
|
+
* `citations` is empty exactly when `status` is `unsupported` or `notFound`;
|
|
60
|
+
* `value` is `null` exactly when `status` is `notFound`.
|
|
61
|
+
*/
|
|
62
|
+
export interface Basis {
|
|
63
|
+
basisVersion: number;
|
|
64
|
+
field: string;
|
|
65
|
+
value: unknown | null;
|
|
66
|
+
status: FieldStatus;
|
|
67
|
+
confidence?: "low" | "medium" | "high";
|
|
68
|
+
citations: EvidenceCitation[];
|
|
69
|
+
}
|
|
70
|
+
/** A coded reason a field's attribution was downgraded. Never upstream text. */
|
|
71
|
+
export interface BasisWarning {
|
|
72
|
+
field: string;
|
|
73
|
+
code: string;
|
|
74
|
+
}
|
|
31
75
|
export interface CrawlOptions {
|
|
32
76
|
maxDepth?: number;
|
|
33
77
|
maxPages?: number;
|
|
@@ -60,7 +104,15 @@ export interface ExtractOptions {
|
|
|
60
104
|
urls: string[];
|
|
61
105
|
prompt?: string;
|
|
62
106
|
schema?: Json;
|
|
63
|
-
|
|
107
|
+
/**
|
|
108
|
+
* Return per-field evidence: each result carries a `basis` array, one
|
|
109
|
+
* {@link Basis} per top-level scalar schema property. Requires `schema`.
|
|
110
|
+
*/
|
|
111
|
+
basis?: boolean;
|
|
112
|
+
/** BYOK: use your own LLM key/provider/model instead of the server's. */
|
|
113
|
+
llmApiKey?: string;
|
|
114
|
+
llmProvider?: string;
|
|
115
|
+
llmModel?: string;
|
|
64
116
|
pollInterval?: number;
|
|
65
117
|
timeout?: number;
|
|
66
118
|
}
|
|
@@ -80,7 +132,8 @@ export type ScrapeResult = Json;
|
|
|
80
132
|
export type CrawlResult = Json[];
|
|
81
133
|
export type SearchResult = Json | Json[];
|
|
82
134
|
export type ParseResult = Json;
|
|
83
|
-
|
|
135
|
+
/** Native `/v1/extract` returns one result object per URL, in request order. */
|
|
136
|
+
export type ExtractResult = Json[];
|
|
84
137
|
export type BatchResult = Json[];
|
|
85
138
|
export type Capabilities = Json;
|
|
86
139
|
export type DiffResult = Json;
|
package/package.json
CHANGED