@mjasnikovs/pi-task 0.38.15 → 0.38.17
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/dist/config/config.d.ts +26 -0
- package/dist/config/config.js +68 -17
- package/dist/shared/child-process.js +9 -16
- package/dist/task/accept-debt.d.ts +7 -5
- package/dist/task/accept-debt.js +18 -14
- package/dist/task/artifact-closure.js +18 -63
- package/dist/task/auto-orchestrator.js +211 -218
- package/dist/task/autofix-ledger.d.ts +113 -0
- package/dist/task/autofix-ledger.js +152 -0
- package/dist/task/boot-probe.d.ts +109 -1
- package/dist/task/boot-probe.js +139 -23
- package/dist/task/child-runner.d.ts +50 -6
- package/dist/task/child-runner.js +48 -69
- package/dist/task/command-run.d.ts +49 -6
- package/dist/task/command-run.js +154 -18
- package/dist/task/coverage-loop.d.ts +11 -0
- package/dist/task/coverage-loop.js +16 -0
- package/dist/task/external-context.d.ts +9 -12
- package/dist/task/external-context.js +5 -5
- package/dist/task/failure-classifier.d.ts +9 -1
- package/dist/task/failure-classifier.js +9 -0
- package/dist/task/final-gate-fix.d.ts +22 -26
- package/dist/task/final-gate-fix.js +16 -31
- package/dist/task/final-gate.d.ts +10 -2
- package/dist/task/final-gate.js +55 -89
- package/dist/task/fix-child.d.ts +64 -0
- package/dist/task/fix-child.js +66 -0
- package/dist/task/gate-deps.js +20 -13
- package/dist/task/lint-fix.d.ts +7 -0
- package/dist/task/lint-fix.js +45 -9
- package/dist/task/orchestrator.d.ts +33 -24
- package/dist/task/orchestrator.js +75 -46
- package/dist/task/phases.d.ts +120 -34
- package/dist/task/phases.js +221 -134
- package/dist/task/plan-orchestrator.js +2 -2
- package/dist/task/plan-rounds.d.ts +86 -0
- package/dist/task/plan-rounds.js +105 -0
- package/dist/task/plan-session.d.ts +31 -21
- package/dist/task/plan-session.js +97 -120
- package/dist/task/qa-transcript.d.ts +100 -0
- package/dist/task/qa-transcript.js +99 -0
- package/dist/task/question-source.d.ts +117 -0
- package/dist/task/question-source.js +174 -0
- package/dist/task/repo-health-check.d.ts +21 -21
- package/dist/task/repo-health-check.js +43 -112
- package/dist/task/run-end.d.ts +77 -0
- package/dist/task/run-end.js +37 -0
- package/dist/task/run-final-gate.js +71 -79
- package/dist/task/serve-entry.js +6 -57
- package/dist/task/shipped-source.d.ts +67 -0
- package/dist/task/shipped-source.js +144 -0
- package/dist/task/task-gates.d.ts +9 -1
- package/dist/task/task-gates.js +27 -6
- package/dist/task/terminal-outcome.d.ts +1 -1
- package/dist/task/terminal-outcome.js +12 -0
- package/dist/task/verify-work.d.ts +46 -0
- package/dist/task/verify-work.js +51 -3
- package/dist/workers/brave-search.d.ts +7 -0
- package/dist/workers/brave-search.js +36 -55
- package/dist/workers/ddg-search.d.ts +1 -1
- package/dist/workers/ddg-search.js +27 -47
- package/dist/workers/docs-core.d.ts +71 -1
- package/dist/workers/docs-core.js +131 -71
- package/dist/workers/exa-search.d.ts +2 -2
- package/dist/workers/exa-search.js +53 -68
- package/dist/workers/html-clean.js +67 -88
- package/dist/workers/http-request.d.ts +74 -0
- package/dist/workers/http-request.js +103 -0
- package/dist/workers/npm-version.js +37 -42
- package/dist/workers/pi-worker-core.d.ts +13 -2
- package/dist/workers/pi-worker-core.js +35 -25
- package/dist/workers/pi-worker-docs.d.ts +1 -1
- package/dist/workers/pi-worker-docs.js +49 -68
- package/dist/workers/pi-worker-fetch.d.ts +1 -1
- package/dist/workers/pi-worker-fetch.js +20 -21
- package/dist/workers/pi-worker-search.js +6 -4
- package/dist/workers/pi-worker.js +5 -4
- package/dist/workers/search-core.d.ts +1 -1
- package/dist/workers/search-core.js +36 -42
- package/dist/workers/search-types.d.ts +13 -0
- package/dist/workers/search-types.js +27 -0
- package/dist/workers/shared.d.ts +51 -11
- package/dist/workers/shared.js +0 -0
- package/dist/workers/worker-channels.d.ts +60 -0
- package/dist/workers/worker-channels.js +98 -0
- package/package.json +1 -1
|
@@ -176,7 +176,8 @@ export function ensureDocsModulesDir(dir) {
|
|
|
176
176
|
fs.writeFileSync(pkgPath, '{"name":"pi-worker-docs-modules","private":true}\n', 'utf8');
|
|
177
177
|
}
|
|
178
178
|
}
|
|
179
|
-
export async function runAutoInstall(spawn, packageName,
|
|
179
|
+
export async function runAutoInstall(spawn, packageName, opts = {}) {
|
|
180
|
+
const { signal, versionRange } = opts;
|
|
180
181
|
const installDir = getDocsModulesDir();
|
|
181
182
|
ensureDocsModulesDir(installDir);
|
|
182
183
|
// `shell: false` in runChild, so a `^`/`~`/space in the range stays a single
|
|
@@ -203,31 +204,97 @@ export async function runAutoInstall(spawn, packageName, signal, versionRange) {
|
|
|
203
204
|
}, installDir, signal, { mode: 'text', discardStdout: true });
|
|
204
205
|
return { success: result.exitCode === 0 && !result.aborted, installDir, stderr: result.stderr };
|
|
205
206
|
}
|
|
206
|
-
/**
|
|
207
|
-
*
|
|
208
|
-
|
|
207
|
+
/**
|
|
208
|
+
* Get a package onto disk and resolved: resolve from `cwd`, and on
|
|
209
|
+
* `not_installed` install it — at the range the PROJECT declares when it declares
|
|
210
|
+
* one — then resolve again from the install dir.
|
|
211
|
+
*
|
|
212
|
+
* One statement of the ladder, for both callers. It was written twice: inline in
|
|
213
|
+
* `docsRaw` for the requested package, and in `tryResolveOrInstall` for each hop
|
|
214
|
+
* of the type-redirect chain — and the copies had drifted on all three things
|
|
215
|
+
* that matter.
|
|
216
|
+
*
|
|
217
|
+
* - The abort signal. The hop copy passed it; the primary copy passed a bare
|
|
218
|
+
* `undefined` placeholder to reach the fourth positional, while
|
|
219
|
+
* `DocsRawInput.signal` was honoured on either side of that call. So a user
|
|
220
|
+
* cancel during the MAIN `npm install` of a model-chosen package was not
|
|
221
|
+
* delivered. `runAutoInstall` takes an options object now, so a hole like that
|
|
222
|
+
* cannot be typed.
|
|
223
|
+
* - The version pin. `findDeclaredRange` had exactly one call site — the primary
|
|
224
|
+
* copy. The hop copy installed `latest` unconditionally, on the hop most likely
|
|
225
|
+
* to be the declared one: `declarationChain` exists precisely because "a
|
|
226
|
+
* project that uses Bun declares `@types/bun`, not `bun`".
|
|
227
|
+
* - The provenance. `autoInstalled`/`autoInstallPin` were locals of `docsRaw`, so
|
|
228
|
+
* a package acquired only through a hop reported neither and got no version
|
|
229
|
+
* banner.
|
|
230
|
+
*
|
|
231
|
+
* CONTEXT.md records the redirect WALK as unified (`resolveTypeSource`) and its
|
|
232
|
+
* `resolveHop` seam as "the one thing its two call sites disagree about". They
|
|
233
|
+
* should disagree only about WHETHER to install, never about HOW.
|
|
234
|
+
*/
|
|
235
|
+
export async function acquirePackage(input) {
|
|
236
|
+
const { name, cwd, spawn, resolvePackage, signal } = input;
|
|
209
237
|
try {
|
|
210
|
-
return resolvePackage(name, cwd);
|
|
238
|
+
return { ok: true, pkg: resolvePackage(name, cwd), autoInstalled: false };
|
|
211
239
|
}
|
|
212
|
-
catch (
|
|
213
|
-
if (!(
|
|
214
|
-
return
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
240
|
+
catch (firstErr) {
|
|
241
|
+
if (!(firstErr instanceof ResolveError) || firstErr.kind !== 'not_installed') {
|
|
242
|
+
return { ok: false, stage: 'resolve', err: firstErr };
|
|
243
|
+
}
|
|
244
|
+
// Auto-install — but FIRST honour the version the project intends. If the
|
|
245
|
+
// dep is declared in the project's package.json, install that range so a
|
|
246
|
+
// scaffolding answer is grounded in the project's major, not whatever npm
|
|
247
|
+
// currently tags `latest`.
|
|
248
|
+
const asked = extractParentPackage(name);
|
|
249
|
+
const declaredRange = findDeclaredRange(asked, cwd);
|
|
250
|
+
const pin = declaredRange ?
|
|
251
|
+
{ source: 'declared-range', range: declaredRange, asked }
|
|
252
|
+
: { source: 'npm-latest', asked };
|
|
253
|
+
const install = await runAutoInstall(spawn, asked, {
|
|
254
|
+
signal,
|
|
255
|
+
versionRange: declaredRange ?? undefined
|
|
256
|
+
});
|
|
257
|
+
if (!install.success) {
|
|
258
|
+
return { ok: false, stage: 'install', stderr: install.stderr, pin, asked };
|
|
259
|
+
}
|
|
218
260
|
try {
|
|
219
|
-
return
|
|
261
|
+
return {
|
|
262
|
+
ok: true,
|
|
263
|
+
pkg: resolvePackage(name, install.installDir),
|
|
264
|
+
autoInstalled: true,
|
|
265
|
+
pin
|
|
266
|
+
};
|
|
220
267
|
}
|
|
221
|
-
catch {
|
|
222
|
-
return
|
|
268
|
+
catch (retryErr) {
|
|
269
|
+
return { ok: false, stage: 'reresolve', err: retryErr, pin };
|
|
223
270
|
}
|
|
224
271
|
}
|
|
225
272
|
}
|
|
273
|
+
/** Resolve `name` from cwd; on `not_installed`, auto-install it and resolve from
|
|
274
|
+
* the install dir. Returns null on any failure (caller keeps its fallback). */
|
|
275
|
+
async function tryResolveOrInstall(name, cwd, spawn, resolvePackage, signal, onAcquired) {
|
|
276
|
+
const got = await acquirePackage({ name, cwd, spawn, resolvePackage, signal });
|
|
277
|
+
if (!got.ok)
|
|
278
|
+
return null;
|
|
279
|
+
if (got.autoInstalled)
|
|
280
|
+
onAcquired?.(got.pin);
|
|
281
|
+
return got.pkg;
|
|
282
|
+
}
|
|
226
283
|
/** The docs pipeline's adapter over the shared redirect walk (docs-resolve.ts):
|
|
227
284
|
* hops resolve through the auto-installing lookup, so a declaration package that is
|
|
228
285
|
* declared but not yet on disk is fetched rather than abandoned. */
|
|
229
|
-
function resolveTypeSourceForDocs(pkg, requested, cwd, spawn, resolvePackage, signal) {
|
|
230
|
-
|
|
286
|
+
async function resolveTypeSourceForDocs(pkg, requested, cwd, spawn, resolvePackage, signal) {
|
|
287
|
+
// A package acquired ONLY through a hop used to report neither `autoInstalled`
|
|
288
|
+
// nor a pin, so `pi-worker-docs` emitted no version banner for it — while the
|
|
289
|
+
// banner's own text ("only its types are, as `@types/bun` `^1.2`") claims a
|
|
290
|
+
// range as provenance. Report the last hop that actually installed.
|
|
291
|
+
let installed = false;
|
|
292
|
+
let pin;
|
|
293
|
+
const out = await resolveTypeSource(pkg, extractParentPackage(requested), next => tryResolveOrInstall(next, cwd, spawn, resolvePackage, signal, hopPin => {
|
|
294
|
+
installed = true;
|
|
295
|
+
pin = hopPin;
|
|
296
|
+
}));
|
|
297
|
+
return pin ? { pkg: out, installed, pin } : { pkg: out, installed };
|
|
231
298
|
}
|
|
232
299
|
export async function docsRaw(input) {
|
|
233
300
|
const resolvePackage = input.resolvePackage ?? defaultResolvePackage;
|
|
@@ -247,83 +314,76 @@ export async function docsRaw(input) {
|
|
|
247
314
|
const npmVersionPromise = npmVersionLookup(extractParentPackage(requested), {
|
|
248
315
|
signal: input.signal
|
|
249
316
|
}).catch(() => null);
|
|
250
|
-
// Step 1: resolve
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
resolveError: 'not_installed',
|
|
275
|
-
installError: installResult.stderr,
|
|
276
|
-
autoInstallPin,
|
|
277
|
-
npmVersion: await npmVersionPromise
|
|
278
|
-
};
|
|
279
|
-
}
|
|
280
|
-
autoInstalled = true;
|
|
281
|
-
try {
|
|
282
|
-
pkg = resolvePackage(requested, installResult.installDir);
|
|
283
|
-
}
|
|
284
|
-
catch (retryErr) {
|
|
285
|
-
if (retryErr instanceof ResolveError) {
|
|
286
|
-
return {
|
|
287
|
-
kind: 'error',
|
|
288
|
-
message: retryErr.message,
|
|
289
|
-
resolveError: retryErr.kind,
|
|
290
|
-
autoInstalled,
|
|
291
|
-
autoInstallPin,
|
|
292
|
-
npmVersion: await npmVersionPromise
|
|
293
|
-
};
|
|
294
|
-
}
|
|
317
|
+
// Step 1: acquire the package — resolve, or install-at-the-declared-range and
|
|
318
|
+
// resolve again. The ladder is `acquirePackage`; this maps its stages onto the
|
|
319
|
+
// rich error results the docs tool reports. `input.signal` now reaches the
|
|
320
|
+
// install, which it never did while the range was a fourth positional.
|
|
321
|
+
const got = await acquirePackage({
|
|
322
|
+
name: requested,
|
|
323
|
+
cwd: input.cwd,
|
|
324
|
+
spawn,
|
|
325
|
+
resolvePackage,
|
|
326
|
+
signal: input.signal
|
|
327
|
+
});
|
|
328
|
+
if (!got.ok) {
|
|
329
|
+
if (got.stage === 'install') {
|
|
330
|
+
return {
|
|
331
|
+
kind: 'error',
|
|
332
|
+
message: `Package "${got.asked}" is not installed and auto-install failed.\n${got.stderr}`,
|
|
333
|
+
resolveError: 'not_installed',
|
|
334
|
+
installError: got.stderr,
|
|
335
|
+
autoInstallPin: got.pin,
|
|
336
|
+
npmVersion: await npmVersionPromise
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
if (got.stage === 'reresolve') {
|
|
340
|
+
if (got.err instanceof ResolveError) {
|
|
295
341
|
return {
|
|
296
342
|
kind: 'error',
|
|
297
|
-
message:
|
|
298
|
-
|
|
299
|
-
|
|
343
|
+
message: got.err.message,
|
|
344
|
+
resolveError: got.err.kind,
|
|
345
|
+
autoInstalled: true,
|
|
346
|
+
autoInstallPin: got.pin,
|
|
300
347
|
npmVersion: await npmVersionPromise
|
|
301
348
|
};
|
|
302
349
|
}
|
|
303
|
-
}
|
|
304
|
-
else if (firstErr instanceof ResolveError) {
|
|
305
350
|
return {
|
|
306
351
|
kind: 'error',
|
|
307
|
-
message:
|
|
308
|
-
|
|
352
|
+
message: `Could not resolve "${input.pkg}" after install: ${got.err instanceof Error ? got.err.message : String(got.err)}`,
|
|
353
|
+
autoInstalled: true,
|
|
354
|
+
autoInstallPin: got.pin,
|
|
309
355
|
npmVersion: await npmVersionPromise
|
|
310
356
|
};
|
|
311
357
|
}
|
|
312
|
-
|
|
358
|
+
if (got.err instanceof ResolveError) {
|
|
313
359
|
return {
|
|
314
360
|
kind: 'error',
|
|
315
|
-
message:
|
|
361
|
+
message: got.err.message,
|
|
362
|
+
resolveError: got.err.kind,
|
|
316
363
|
npmVersion: await npmVersionPromise
|
|
317
364
|
};
|
|
318
365
|
}
|
|
366
|
+
return {
|
|
367
|
+
kind: 'error',
|
|
368
|
+
message: `Could not resolve "${input.pkg}": ${got.err instanceof Error ? got.err.message : String(got.err)}`,
|
|
369
|
+
npmVersion: await npmVersionPromise
|
|
370
|
+
};
|
|
319
371
|
}
|
|
372
|
+
let pkg = got.pkg;
|
|
373
|
+
let autoInstalled = got.autoInstalled;
|
|
374
|
+
let autoInstallPin = got.pin;
|
|
320
375
|
// Step 1b: if the resolved package ships no usable type declarations (e.g.
|
|
321
376
|
// the `bun` runtime launcher, which is just a binary + install README), or is
|
|
322
377
|
// a pure `@types/<name>` redirect stub, follow the conventional
|
|
323
378
|
// @types/<name> + triple-slash `<reference types>` chain to the package that
|
|
324
379
|
// actually holds the declarations (e.g. bun -> @types/bun -> bun-types).
|
|
325
380
|
// Best-effort: any failure leaves the original resolution untouched.
|
|
326
|
-
|
|
381
|
+
const viaTypes = await resolveTypeSourceForDocs(pkg, requested, input.cwd, spawn, resolvePackage, input.signal);
|
|
382
|
+
pkg = viaTypes.pkg;
|
|
383
|
+
if (viaTypes.installed) {
|
|
384
|
+
autoInstalled = true;
|
|
385
|
+
autoInstallPin ??= viaTypes.pin;
|
|
386
|
+
}
|
|
327
387
|
// Step 2: open cache
|
|
328
388
|
let cache = null;
|
|
329
389
|
let cacheError;
|
|
@@ -7,9 +7,9 @@
|
|
|
7
7
|
* and the result payload is one text blob of `Title:`/`URL:`/`Text:` blocks
|
|
8
8
|
* separated by `---`, which we parse back into structured results.
|
|
9
9
|
*/
|
|
10
|
+
import { type FetchLike } from './http-request.js';
|
|
10
11
|
import type { SearchResult } from './search-types.js';
|
|
11
|
-
|
|
12
|
-
export type FetchLike = (url: string, init?: RequestInit) => Promise<Response>;
|
|
12
|
+
export type { FetchLike };
|
|
13
13
|
export interface ExaSearchOpts {
|
|
14
14
|
count?: number;
|
|
15
15
|
timeoutMs?: number;
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* and the result payload is one text blob of `Title:`/`URL:`/`Text:` blocks
|
|
8
8
|
* separated by `---`, which we parse back into structured results.
|
|
9
9
|
*/
|
|
10
|
+
import { httpRequest, HttpRequestError } from './http-request.js';
|
|
10
11
|
const EXA_MCP_ENDPOINT = 'https://mcp.exa.ai/mcp';
|
|
11
12
|
const DEFAULT_COUNT = 10;
|
|
12
13
|
const MAX_COUNT = 20;
|
|
@@ -24,72 +25,61 @@ export class ExaSearchError extends Error {
|
|
|
24
25
|
}
|
|
25
26
|
export async function exaSearch(query, opts = {}) {
|
|
26
27
|
const count = Math.max(1, Math.min(MAX_COUNT, opts.count ?? DEFAULT_COUNT));
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
};
|
|
36
|
-
if (opts.signal) {
|
|
37
|
-
if (opts.signal.aborted)
|
|
38
|
-
onUserAbort();
|
|
39
|
-
else
|
|
40
|
-
opts.signal.addEventListener('abort', onUserAbort, { once: true });
|
|
41
|
-
}
|
|
28
|
+
return await request({
|
|
29
|
+
timeoutMs: opts.timeoutMs ?? DEFAULT_TIMEOUT_MS,
|
|
30
|
+
...(opts.signal === undefined ? {} : { signal: opts.signal }),
|
|
31
|
+
...(opts.fetchImpl === undefined ? {} : { fetchImpl: opts.fetchImpl })
|
|
32
|
+
}, count, query);
|
|
33
|
+
}
|
|
34
|
+
/** The Exa-specific half: its argv, its status policy, its error type. */
|
|
35
|
+
async function request(bounds, count, query) {
|
|
42
36
|
try {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
contextMaxCharacters: 3000
|
|
63
|
-
}
|
|
37
|
+
return await httpRequest(EXA_MCP_ENDPOINT, {
|
|
38
|
+
...bounds,
|
|
39
|
+
method: 'POST',
|
|
40
|
+
headers: {
|
|
41
|
+
'content-type': 'application/json',
|
|
42
|
+
accept: 'application/json, text/event-stream'
|
|
43
|
+
},
|
|
44
|
+
body: JSON.stringify({
|
|
45
|
+
jsonrpc: '2.0',
|
|
46
|
+
id: 1,
|
|
47
|
+
method: 'tools/call',
|
|
48
|
+
params: {
|
|
49
|
+
name: 'web_search_exa',
|
|
50
|
+
arguments: {
|
|
51
|
+
query,
|
|
52
|
+
numResults: count,
|
|
53
|
+
type: 'auto',
|
|
54
|
+
livecrawl: 'fallback',
|
|
55
|
+
contextMaxCharacters: 3000
|
|
64
56
|
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
throw new ExaSearchError(text?.trim() || 'Exa MCP returned an error result.', 'protocol');
|
|
84
|
-
}
|
|
85
|
-
if (!text)
|
|
86
|
-
throw new ExaSearchError('Exa MCP returned no text content.', 'protocol');
|
|
87
|
-
return parseResultBlocks(text).slice(0, count);
|
|
57
|
+
}
|
|
58
|
+
})
|
|
59
|
+
}, async (response) => {
|
|
60
|
+
if (!response.ok) {
|
|
61
|
+
throw new ExaSearchError(`Exa search HTTP ${response.status} ${response.statusText}`, 'http', response.status);
|
|
62
|
+
}
|
|
63
|
+
const rpc = parseRpcBody(await response.text());
|
|
64
|
+
if (rpc.error) {
|
|
65
|
+
throw new ExaSearchError(`Exa MCP error${rpc.error.code !== undefined ? ` ${rpc.error.code}` : ''}: ${rpc.error.message ?? 'unknown error'}`, 'protocol');
|
|
66
|
+
}
|
|
67
|
+
const text = rpc.result?.content?.find(c => c.type === 'text' && typeof c.text === 'string' && c.text.trim().length > 0)?.text;
|
|
68
|
+
if (rpc.result?.isError) {
|
|
69
|
+
throw new ExaSearchError(text?.trim() || 'Exa MCP returned an error result.', 'protocol');
|
|
70
|
+
}
|
|
71
|
+
if (!text)
|
|
72
|
+
throw new ExaSearchError('Exa MCP returned no text content.', 'protocol');
|
|
73
|
+
return parseResultBlocks(text).slice(0, count);
|
|
74
|
+
});
|
|
88
75
|
}
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
76
|
+
catch (err) {
|
|
77
|
+
if (err instanceof HttpRequestError) {
|
|
78
|
+
throw err.kind === 'aborted' ?
|
|
79
|
+
new ExaSearchError('Search aborted.', 'aborted')
|
|
80
|
+
: new ExaSearchError(`Exa search request failed: ${err.detail}`, 'network');
|
|
81
|
+
}
|
|
82
|
+
throw err;
|
|
93
83
|
}
|
|
94
84
|
}
|
|
95
85
|
/**
|
|
@@ -157,8 +147,3 @@ function parseResultBlocks(text) {
|
|
|
157
147
|
}
|
|
158
148
|
return results;
|
|
159
149
|
}
|
|
160
|
-
function describeError(err) {
|
|
161
|
-
if (err instanceof Error)
|
|
162
|
-
return err.message;
|
|
163
|
-
return String(err);
|
|
164
|
-
}
|
|
@@ -2,6 +2,7 @@ import { parseHTML } from 'linkedom';
|
|
|
2
2
|
import { Readability } from '@mozilla/readability';
|
|
3
3
|
import TurndownService from 'turndown';
|
|
4
4
|
import { readPkgVersion } from '../shared/pkg-version.js';
|
|
5
|
+
import { httpRequest, HttpRequestError, describeError } from './http-request.js';
|
|
5
6
|
const turndown = new TurndownService({
|
|
6
7
|
codeBlockStyle: 'fenced',
|
|
7
8
|
headingStyle: 'atx',
|
|
@@ -86,100 +87,83 @@ export class FetchAndCleanError extends Error {
|
|
|
86
87
|
}
|
|
87
88
|
}
|
|
88
89
|
export async function fetchAndClean(url, opts = {}) {
|
|
89
|
-
const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
90
90
|
const maxBytes = opts.maxBytes ?? DEFAULT_MAX_BYTES;
|
|
91
|
-
const internalController = new AbortController();
|
|
92
91
|
let sizeExceeded = false;
|
|
93
|
-
let userAborted = false;
|
|
94
|
-
const timeoutHandle = setTimeout(() => internalController.abort(), timeoutMs);
|
|
95
|
-
const onUserAbort = () => {
|
|
96
|
-
userAborted = true;
|
|
97
|
-
internalController.abort();
|
|
98
|
-
};
|
|
99
|
-
if (opts.signal) {
|
|
100
|
-
if (opts.signal.aborted)
|
|
101
|
-
onUserAbort();
|
|
102
|
-
else
|
|
103
|
-
opts.signal.addEventListener('abort', onUserAbort, { once: true });
|
|
104
|
-
}
|
|
105
92
|
try {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
catch (err) {
|
|
115
|
-
if (userAborted) {
|
|
116
|
-
throw new FetchAndCleanError('Fetch aborted.', 'aborted', err);
|
|
93
|
+
return await httpRequest(url, {
|
|
94
|
+
timeoutMs: opts.timeoutMs ?? DEFAULT_TIMEOUT_MS,
|
|
95
|
+
...(opts.signal === undefined ? {} : { signal: opts.signal }),
|
|
96
|
+
headers: { 'user-agent': USER_AGENT },
|
|
97
|
+
redirect: 'follow'
|
|
98
|
+
}, async (response, ctl) => {
|
|
99
|
+
if (!response.ok) {
|
|
100
|
+
throw new FetchAndCleanError(`Fetch failed: HTTP ${response.status} ${response.statusText} for ${url}`, 'http-error');
|
|
117
101
|
}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
while (true) {
|
|
137
|
-
// response.body's stream type doesn't resolve here, so the chunk
|
|
138
|
-
// surfaces as `any`; pin it to the Uint8Array the reader yields.
|
|
139
|
-
const { value, done } = (await reader.read());
|
|
140
|
-
if (done)
|
|
141
|
-
break;
|
|
142
|
-
if (value) {
|
|
143
|
-
bytesRead += value.byteLength;
|
|
144
|
-
if (bytesRead > maxBytes) {
|
|
145
|
-
sizeExceeded = true;
|
|
146
|
-
internalController.abort();
|
|
102
|
+
const contentType = response.headers.get('content-type') ?? '';
|
|
103
|
+
const kind = classifyContentType(contentType);
|
|
104
|
+
if (kind === 'reject') {
|
|
105
|
+
throw new FetchAndCleanError(`${url} is ${contentType || 'unknown content type'}, not a text or HTML page that pi-worker-fetch can read.`, 'not-html');
|
|
106
|
+
}
|
|
107
|
+
const reader = response.body?.getReader();
|
|
108
|
+
if (!reader) {
|
|
109
|
+
throw new FetchAndCleanError(`Could not fetch ${url}: empty response body`, 'network');
|
|
110
|
+
}
|
|
111
|
+
const decoder = decoderFor(contentType);
|
|
112
|
+
let text = '';
|
|
113
|
+
let bytesRead = 0;
|
|
114
|
+
try {
|
|
115
|
+
while (true) {
|
|
116
|
+
// response.body's stream type doesn't resolve here, so the chunk
|
|
117
|
+
// surfaces as `any`; pin it to the Uint8Array the reader yields.
|
|
118
|
+
const { value, done } = (await reader.read());
|
|
119
|
+
if (done)
|
|
147
120
|
break;
|
|
121
|
+
if (value) {
|
|
122
|
+
bytesRead += value.byteLength;
|
|
123
|
+
if (bytesRead > maxBytes) {
|
|
124
|
+
// OUR abort, told apart from the user's and the clock's
|
|
125
|
+
// by the seam — all three abort the same signal.
|
|
126
|
+
sizeExceeded = true;
|
|
127
|
+
ctl.abort();
|
|
128
|
+
break;
|
|
129
|
+
}
|
|
130
|
+
text += decoder.decode(value, { stream: true });
|
|
148
131
|
}
|
|
149
|
-
text += decoder.decode(value, { stream: true });
|
|
150
132
|
}
|
|
133
|
+
text += decoder.decode();
|
|
151
134
|
}
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
135
|
+
catch (err) {
|
|
136
|
+
if (sizeExceeded) {
|
|
137
|
+
// fall through to throw outside the catch
|
|
138
|
+
}
|
|
139
|
+
else if (ctl.userAborted()) {
|
|
140
|
+
throw new FetchAndCleanError('Fetch aborted.', 'aborted', err);
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
throw new FetchAndCleanError(`Could not fetch ${url}: ${describeError(err)}`, 'network', err);
|
|
144
|
+
}
|
|
160
145
|
}
|
|
161
|
-
|
|
162
|
-
throw new FetchAndCleanError(
|
|
146
|
+
if (sizeExceeded) {
|
|
147
|
+
throw new FetchAndCleanError(`${url} exceeds ${formatBytes(maxBytes)} size cap. Try a more specific URL.`, 'too-large');
|
|
163
148
|
}
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
title: hostnameOf(finalUrl),
|
|
175
|
-
markdown: text.trim(),
|
|
176
|
-
finalUrl
|
|
177
|
-
};
|
|
149
|
+
const finalUrl = response.url || url;
|
|
150
|
+
if (kind === 'html')
|
|
151
|
+
return cleanHtml(text, finalUrl);
|
|
152
|
+
// text-ish formats are already clean — return them verbatim.
|
|
153
|
+
return {
|
|
154
|
+
title: hostnameOf(finalUrl),
|
|
155
|
+
markdown: text.trim(),
|
|
156
|
+
finalUrl
|
|
157
|
+
};
|
|
158
|
+
});
|
|
178
159
|
}
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
160
|
+
catch (err) {
|
|
161
|
+
if (err instanceof HttpRequestError) {
|
|
162
|
+
throw err.kind === 'aborted' ?
|
|
163
|
+
new FetchAndCleanError('Fetch aborted.', 'aborted', err.cause)
|
|
164
|
+
: new FetchAndCleanError(`Could not fetch ${url}: ${err.detail}`, 'network', err.cause);
|
|
165
|
+
}
|
|
166
|
+
throw err;
|
|
183
167
|
}
|
|
184
168
|
}
|
|
185
169
|
function hostnameOf(url) {
|
|
@@ -190,11 +174,6 @@ function hostnameOf(url) {
|
|
|
190
174
|
return url;
|
|
191
175
|
}
|
|
192
176
|
}
|
|
193
|
-
function describeError(err) {
|
|
194
|
-
if (err instanceof Error)
|
|
195
|
-
return err.message;
|
|
196
|
-
return String(err);
|
|
197
|
-
}
|
|
198
177
|
function formatBytes(n) {
|
|
199
178
|
if (n >= 1024 * 1024)
|
|
200
179
|
return `${(n / 1024 / 1024).toFixed(1)} MB`;
|