crw-sdk 0.23.0 → 0.24.1

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 CHANGED
@@ -58,7 +58,7 @@ 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 SearXNG URL configured on the engine.
61
+ ¹ Local search needs a search backend configured on the engine.
62
62
 
63
63
  ```ts
64
64
  // Structured extraction (async job → per-URL results array):
@@ -95,7 +95,7 @@ class CrwClient {
95
95
  return result.links ?? [];
96
96
  }
97
97
  /**
98
- * Works in both modes; local mode needs a SearXNG URL configured on the engine.
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
- return this.httpPost("/v1/search", args);
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
  /**
@@ -178,12 +206,14 @@ class CrwClient {
178
206
  async extract(opts) {
179
207
  if (!this.apiUrl)
180
208
  throw new errors_js_1.CrwError(httpOnlyHint("extract", "LLM extract job endpoint"));
181
- const { urls, prompt, schema, llmApiKey, llmProvider, llmModel, pollInterval = 2, timeout = 120 } = opts;
209
+ const { urls, prompt, schema, basis, llmApiKey, llmProvider, llmModel, pollInterval = 2, timeout = 120 } = opts;
182
210
  const body = { urls: [...urls] };
183
211
  if (prompt !== undefined)
184
212
  body.prompt = prompt;
185
213
  if (schema !== undefined)
186
214
  body.schema = schema;
215
+ if (basis !== undefined)
216
+ body.basis = basis;
187
217
  if (llmApiKey !== undefined)
188
218
  body.llmApiKey = llmApiKey;
189
219
  if (llmProvider !== undefined)
@@ -192,8 +222,14 @@ class CrwClient {
192
222
  body.llmModel = llmModel;
193
223
  const start = await this.httpRequest("POST", "/v1/extract", body, { raw: true });
194
224
  const jobId = start.id;
195
- if (!jobId)
196
- throw new errors_js_1.CrwError(`extract did not return job ID: ${JSON.stringify(start)}`);
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
+ }
197
233
  const deadline = Date.now() + timeout * 1000;
198
234
  for (;;) {
199
235
  if (Date.now() > deadline)
@@ -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 SearXNG URL configured on the engine.
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
  /**
@@ -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 SearXNG URL configured on the engine.
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
- return this.httpPost("/v1/search", args);
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
  /**
@@ -175,12 +203,14 @@ export class CrwClient {
175
203
  async extract(opts) {
176
204
  if (!this.apiUrl)
177
205
  throw new CrwError(httpOnlyHint("extract", "LLM extract job endpoint"));
178
- const { urls, prompt, schema, llmApiKey, llmProvider, llmModel, pollInterval = 2, timeout = 120 } = opts;
206
+ const { urls, prompt, schema, basis, llmApiKey, llmProvider, llmModel, pollInterval = 2, timeout = 120 } = opts;
179
207
  const body = { urls: [...urls] };
180
208
  if (prompt !== undefined)
181
209
  body.prompt = prompt;
182
210
  if (schema !== undefined)
183
211
  body.schema = schema;
212
+ if (basis !== undefined)
213
+ body.basis = basis;
184
214
  if (llmApiKey !== undefined)
185
215
  body.llmApiKey = llmApiKey;
186
216
  if (llmProvider !== undefined)
@@ -189,8 +219,14 @@ export class CrwClient {
189
219
  body.llmModel = llmModel;
190
220
  const start = await this.httpRequest("POST", "/v1/extract", body, { raw: true });
191
221
  const jobId = start.id;
192
- if (!jobId)
193
- throw new CrwError(`extract did not return job ID: ${JSON.stringify(start)}`);
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
+ }
194
230
  const deadline = Date.now() + timeout * 1000;
195
231
  for (;;) {
196
232
  if (Date.now() > deadline)
@@ -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,6 +104,11 @@ export interface ExtractOptions {
60
104
  urls: string[];
61
105
  prompt?: string;
62
106
  schema?: Json;
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;
63
112
  /** BYOK: use your own LLM key/provider/model instead of the server's. */
64
113
  llmApiKey?: string;
65
114
  llmProvider?: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "crw-sdk",
3
- "version": "0.23.0",
3
+ "version": "0.24.1",
4
4
  "description": "TypeScript/JavaScript SDK for CRW — scrape, crawl, map, search, parse, and extract any website",
5
5
  "license": "MIT",
6
6
  "homepage": "https://github.com/us/crw",