@arnilo/prism-tools 0.0.0-stage → 0.13.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/CHANGELOG.md +418 -0
- package/LICENSE +21 -0
- package/README.md +44 -2
- package/browser/README.md +98 -0
- package/browser/skills/browser-automation/SKILL.md +14 -0
- package/browser/src/actions/cdp.ts +131 -0
- package/browser/src/actions/dialog.ts +25 -0
- package/browser/src/actions/download_release.ts +22 -0
- package/browser/src/actions/index.ts +37 -0
- package/browser/src/actions/locator.ts +70 -0
- package/browser/src/actions/navigate.ts +23 -0
- package/browser/src/actions/screenshot.ts +27 -0
- package/browser/src/actions/scroll.ts +24 -0
- package/browser/src/actions/select_page.ts +21 -0
- package/browser/src/actions/types.ts +114 -0
- package/browser/src/actions/upload.ts +44 -0
- package/browser/src/actions/wait.ts +45 -0
- package/browser/src/browser.ts +197 -0
- package/browser/src/cdp.ts +178 -0
- package/browser/src/checkpoint.ts +189 -0
- package/browser/src/config.ts +72 -0
- package/browser/src/downloads.ts +253 -0
- package/browser/src/errors.ts +31 -0
- package/browser/src/evaluate.ts +101 -0
- package/browser/src/index.ts +129 -0
- package/browser/src/limits.ts +271 -0
- package/browser/src/manager.ts +730 -0
- package/browser/src/network.ts +263 -0
- package/browser/src/observe.ts +178 -0
- package/browser/src/policy.ts +133 -0
- package/browser/src/screenshot.ts +107 -0
- package/browser/src/shared-sandbox.ts +53 -0
- package/browser/src/snapshot.ts +99 -0
- package/browser/src/targets.ts +165 -0
- package/browser/src/tools.ts +480 -0
- package/browser/src/types.ts +316 -0
- package/browser/src/uploads.ts +174 -0
- package/coding/README.md +337 -0
- package/coding/src/agent/acp-operations.ts +146 -0
- package/coding/src/agent/artifacts.ts +38 -0
- package/coding/src/agent/ask-user-decision.ts +623 -0
- package/coding/src/agent/atomic-write.ts +23 -0
- package/coding/src/agent/bounded-file.ts +21 -0
- package/coding/src/agent/checks.ts +301 -0
- package/coding/src/agent/coding-checkpoint.ts +828 -0
- package/coding/src/agent/delete.ts +237 -0
- package/coding/src/agent/diagnostics-tool.ts +163 -0
- package/coding/src/agent/diagnostics.ts +262 -0
- package/coding/src/agent/edit-diff.ts +382 -0
- package/coding/src/agent/edit.ts +322 -0
- package/coding/src/agent/effects.ts +102 -0
- package/coding/src/agent/env.ts +28 -0
- package/coding/src/agent/execution-policy.ts +89 -0
- package/coding/src/agent/file-mutation-queue.ts +72 -0
- package/coding/src/agent/forge/github.ts +603 -0
- package/coding/src/agent/forge/index.ts +16 -0
- package/coding/src/agent/forge/types.ts +185 -0
- package/coding/src/agent/git-aware-repository.ts +305 -0
- package/coding/src/agent/git-exec.ts +326 -0
- package/coding/src/agent/git-status.ts +177 -0
- package/coding/src/agent/git-tools.ts +360 -0
- package/coding/src/agent/git.ts +1074 -0
- package/coding/src/agent/glob-match.ts +146 -0
- package/coding/src/agent/glob.ts +182 -0
- package/coding/src/agent/goal-verify.ts +385 -0
- package/coding/src/agent/grep-worker.ts +74 -0
- package/coding/src/agent/grep.ts +459 -0
- package/coding/src/agent/identity.ts +48 -0
- package/coding/src/agent/index.ts +676 -0
- package/coding/src/agent/language/client.ts +429 -0
- package/coding/src/agent/language/framing.ts +190 -0
- package/coding/src/agent/language/index.ts +23 -0
- package/coding/src/agent/language/intelligence.ts +682 -0
- package/coding/src/agent/language/types.ts +187 -0
- package/coding/src/agent/lifecycle-events.ts +121 -0
- package/coding/src/agent/lifecycle.ts +166 -0
- package/coding/src/agent/limits.ts +214 -0
- package/coding/src/agent/move.ts +178 -0
- package/coding/src/agent/mutation-path.ts +52 -0
- package/coding/src/agent/output-accumulator.ts +304 -0
- package/coding/src/agent/path-utils.ts +120 -0
- package/coding/src/agent/process/index.ts +58 -0
- package/coding/src/agent/process/recovery.ts +508 -0
- package/coding/src/agent/process/sessions-host.ts +222 -0
- package/coding/src/agent/process/sessions-monitor.ts +141 -0
- package/coding/src/agent/process/sessions-recovery.ts +241 -0
- package/coding/src/agent/process/sessions-spawn.ts +613 -0
- package/coding/src/agent/process/sessions-teardown.ts +281 -0
- package/coding/src/agent/process/sessions.ts +30 -0
- package/coding/src/agent/process/types.ts +358 -0
- package/coding/src/agent/read-path-set.ts +126 -0
- package/coding/src/agent/read.ts +713 -0
- package/coding/src/agent/repository/glob.ts +147 -0
- package/coding/src/agent/repository/indexed-search.ts +480 -0
- package/coding/src/agent/repository/list.ts +131 -0
- package/coding/src/agent/repository/operations.ts +22 -0
- package/coding/src/agent/repository/path.ts +98 -0
- package/coding/src/agent/repository/search.ts +277 -0
- package/coding/src/agent/repository/types.ts +243 -0
- package/coding/src/agent/repository/walk.ts +126 -0
- package/coding/src/agent/repository.ts +12 -0
- package/coding/src/agent/shell-background.ts +264 -0
- package/coding/src/agent/shell-output.ts +75 -0
- package/coding/src/agent/shell.ts +526 -0
- package/coding/src/agent/todo.ts +399 -0
- package/coding/src/agent/truncate.ts +251 -0
- package/coding/src/agent/write.ts +170 -0
- package/coding/src/ask-user-host.ts +48 -0
- package/coding/src/config.ts +278 -0
- package/coding/src/hard-deny.ts +97 -0
- package/coding/src/index.ts +318 -0
- package/coding/src/principal-guard.ts +84 -0
- package/coding/src/sandbox.ts +92 -0
- package/coding/src/security/approval.ts +144 -0
- package/coding/src/security/command-rules.ts +57 -0
- package/coding/src/security/docker-cli.ts +222 -0
- package/coding/src/security/docker-sandbox.ts +1205 -0
- package/coding/src/security/e2b-sandbox.ts +853 -0
- package/coding/src/security/egress/dns-pin.ts +88 -0
- package/coding/src/security/egress/index.ts +32 -0
- package/coding/src/security/egress/limits.ts +57 -0
- package/coding/src/security/egress/policy.ts +137 -0
- package/coding/src/security/egress/proxy.ts +599 -0
- package/coding/src/security/egress/types.ts +30 -0
- package/coding/src/security/index.ts +199 -0
- package/coding/src/security/native-sandbox.ts +639 -0
- package/coding/src/security/path-containment.ts +55 -0
- package/coding/src/security/sandbox-coding-operations.ts +392 -0
- package/coding/src/security/sandbox-fs-operations.ts +644 -0
- package/coding/src/security/sandbox-limits.ts +147 -0
- package/coding/src/security/sandbox-tar.ts +308 -0
- package/coding/src/security/sandbox.ts +179 -0
- package/coding/src/security/semaphore.ts +55 -0
- package/coding/src/targets.ts +110 -0
- package/coding/src/workspace/diff.ts +78 -0
- package/coding/src/workspace/errors.ts +29 -0
- package/coding/src/workspace/index.ts +5 -0
- package/coding/src/workspace/merge.ts +151 -0
- package/coding/src/workspace/routing.ts +76 -0
- package/coding/src/workspace/service.ts +584 -0
- package/computer-use/README.md +105 -0
- package/computer-use/skills/computer-use-linux/SKILL.md +17 -0
- package/computer-use/src/classify.ts +53 -0
- package/computer-use/src/command.ts +29 -0
- package/computer-use/src/config.ts +69 -0
- package/computer-use/src/create.ts +170 -0
- package/computer-use/src/devices.ts +247 -0
- package/computer-use/src/env.ts +28 -0
- package/computer-use/src/index.ts +157 -0
- package/computer-use/src/skill.ts +21 -0
- package/openapi/README.md +97 -0
- package/openapi/src/compile.ts +386 -0
- package/openapi/src/config.ts +73 -0
- package/openapi/src/errors.ts +16 -0
- package/openapi/src/execute.ts +241 -0
- package/openapi/src/index.ts +124 -0
- package/openapi/src/limits.ts +63 -0
- package/openapi/src/specs.ts +86 -0
- package/openapi/src/tools.ts +60 -0
- package/openapi/src/types.ts +61 -0
- package/package.json +171 -4
- package/research/README.md +72 -0
- package/research/skills/research-report/SKILL.md +12 -0
- package/research/skills/research-scoping/SKILL.md +10 -0
- package/research/skills/research-synthesis/SKILL.md +10 -0
- package/research/skills/source-triage/SKILL.md +10 -0
- package/research/src/check.ts +71 -0
- package/research/src/config.ts +41 -0
- package/research/src/index.ts +147 -0
- package/research/src/ledger.ts +157 -0
- package/research/src/urls.ts +21 -0
- package/web/README.md +287 -0
- package/web/skills/web-research/SKILL.md +17 -0
- package/web/src/brave.ts +66 -0
- package/web/src/cache.ts +151 -0
- package/web/src/config.ts +210 -0
- package/web/src/controls.ts +128 -0
- package/web/src/domains.ts +114 -0
- package/web/src/exa.ts +60 -0
- package/web/src/firecrawl.ts +160 -0
- package/web/src/index.ts +387 -0
- package/web/src/kagi.ts +73 -0
- package/web/src/limits.ts +55 -0
- package/web/src/native/convert.ts +84 -0
- package/web/src/native/fetch.ts +226 -0
- package/web/src/normalize.ts +131 -0
- package/web/src/obscura/cdp.ts +201 -0
- package/web/src/obscura/classify.ts +35 -0
- package/web/src/obscura/cli.ts +254 -0
- package/web/src/obscura/errors.ts +8 -0
- package/web/src/obscura/index.ts +59 -0
- package/web/src/obscura/limits.ts +92 -0
- package/web/src/obscura/mcp.ts +104 -0
- package/web/src/obscura/playwright.ts +133 -0
- package/web/src/obscura/process.ts +235 -0
- package/web/src/obscura/search-profile.ts +73 -0
- package/web/src/obscura/types.ts +48 -0
- package/web/src/obscura/web.ts +419 -0
- package/web/src/prompt-fetch.ts +88 -0
- package/web/src/render.ts +75 -0
- package/web/src/searxng.ts +78 -0
- package/web/src/slice.ts +139 -0
- package/web/src/tavily.ts +77 -0
- package/web/src/tools.ts +182 -0
- package/web/src/transport.ts +310 -0
- package/web/src/types.ts +199 -0
- package/work/README.md +77 -0
- package/work/THIRD_PARTY_NOTICES +24 -0
- package/work/docling/ocr.py +150 -0
- package/work/sandbox/Dockerfile +36 -0
- package/work/sandbox/soffice.sh +12 -0
- package/work/src/command.ts +29 -0
- package/work/src/config.ts +138 -0
- package/work/src/connectors/cli.ts +343 -0
- package/work/src/connectors/drafts.ts +647 -0
- package/work/src/connectors/errors.ts +8 -0
- package/work/src/connectors/file-bytes.ts +149 -0
- package/work/src/connectors/google-workspace-http.ts +399 -0
- package/work/src/connectors/google-workspace.ts +465 -0
- package/work/src/connectors/gws-tools.ts +378 -0
- package/work/src/connectors/http.ts +123 -0
- package/work/src/connectors/idempotency.ts +235 -0
- package/work/src/connectors/identity.ts +34 -0
- package/work/src/connectors/index.ts +93 -0
- package/work/src/connectors/limits.ts +50 -0
- package/work/src/connectors/m365-tools.ts +308 -0
- package/work/src/connectors/microsoft365-http.ts +428 -0
- package/work/src/connectors/microsoft365.ts +402 -0
- package/work/src/connectors/normalize.ts +120 -0
- package/work/src/connectors/tool-helpers.ts +398 -0
- package/work/src/connectors/tools.ts +42 -0
- package/work/src/connectors/types.ts +399 -0
- package/work/src/diagrams/canonicalize.ts +166 -0
- package/work/src/diagrams/caps.ts +52 -0
- package/work/src/diagrams/embed.ts +340 -0
- package/work/src/diagrams/errors.ts +76 -0
- package/work/src/diagrams/index.ts +81 -0
- package/work/src/diagrams/messages.ts +150 -0
- package/work/src/diagrams/origin.ts +52 -0
- package/work/src/diagrams/telemetry.ts +34 -0
- package/work/src/diagrams/xml.ts +267 -0
- package/work/src/document-extraction/index.ts +563 -0
- package/work/src/document-reader/ambient.d.ts +2 -0
- package/work/src/document-reader/errors.ts +7 -0
- package/work/src/document-reader/index.ts +181 -0
- package/work/src/document-reader/mistral-ocr.ts +318 -0
- package/work/src/document-reader/parsers.ts +171 -0
- package/work/src/documents/caps.ts +135 -0
- package/work/src/documents/diff.ts +269 -0
- package/work/src/documents/errors.ts +44 -0
- package/work/src/documents/fidelity.ts +98 -0
- package/work/src/documents/generate.ts +96 -0
- package/work/src/documents/hash.ts +8 -0
- package/work/src/documents/index.ts +14 -0
- package/work/src/documents/model-schema.ts +639 -0
- package/work/src/documents/parse.ts +252 -0
- package/work/src/documents/patch-history.ts +81 -0
- package/work/src/documents/patch.ts +476 -0
- package/work/src/documents/preview-html.ts +245 -0
- package/work/src/documents/preview.ts +227 -0
- package/work/src/documents/telemetry.ts +36 -0
- package/work/src/documents/translate/docx.ts +292 -0
- package/work/src/documents/translate/pptx.ts +304 -0
- package/work/src/documents/translate/xlsx.ts +137 -0
- package/work/src/documents/types.ts +188 -0
- package/work/src/index.ts +311 -0
- package/work/src/runtime/worker-pool-entry.ts +102 -0
- package/work/src/runtime/worker-pool.ts +267 -0
- package/work/src/sandbox/composition.ts +212 -0
- package/work/src/sandbox/image.ts +6 -0
- package/work/src/sandbox/index.ts +14 -0
- package/work/src/sheets/caps.ts +68 -0
- package/work/src/sheets/csv.ts +351 -0
- package/work/src/sheets/decimal.ts +388 -0
- package/work/src/sheets/errors.ts +52 -0
- package/work/src/sheets/index.ts +62 -0
- package/work/src/sheets/inference.ts +378 -0
- package/work/src/sheets/telemetry.ts +34 -0
- package/work/src/sheets/types.ts +162 -0
- package/work/src/sheets/xlsx.ts +388 -0
- package/work/src/skills/index.ts +8 -0
- package/work/src/skills/load.ts +43 -0
- package/work/src/tools/filesystem.ts +21 -0
- package/work/src/tools/index.ts +1 -0
- package/work/src/tools/local-filesystem.ts +33 -0
- package/work/src/tools/office.ts +311 -0
- package/work/vendor/hermes-agent/LICENSE +21 -0
- package/work/vendor/hermes-agent/skills/productivity/docx/LICENSE +21 -0
- package/work/vendor/hermes-agent/skills/productivity/docx/SKILL.md +196 -0
- package/work/vendor/hermes-agent/skills/productivity/docx/scripts/docx_comments.py +289 -0
- package/work/vendor/hermes-agent/skills/productivity/docx/scripts/docx_common.py +94 -0
- package/work/vendor/hermes-agent/skills/productivity/docx/scripts/docx_create.py +177 -0
- package/work/vendor/hermes-agent/skills/productivity/docx/scripts/docx_edit.py +250 -0
- package/work/vendor/hermes-agent/skills/productivity/docx/scripts/docx_read.py +149 -0
- package/work/vendor/hermes-agent/skills/productivity/docx/scripts/docx_revisions.py +147 -0
- package/work/vendor/hermes-agent/skills/productivity/docx/scripts/docx_template.py +70 -0
- package/work/vendor/hermes-agent/skills/productivity/docx/scripts/docx_validate.py +156 -0
- package/work/vendor/hermes-agent/skills/productivity/pdf/LICENSE +21 -0
- package/work/vendor/hermes-agent/skills/productivity/pdf/SKILL.md +125 -0
- package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/_raster.py +76 -0
- package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/extract_marker.py +88 -0
- package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/extract_pymupdf.py +101 -0
- package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_create.py +130 -0
- package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_fill_form.py +97 -0
- package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_form_layout.py +168 -0
- package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_make_form.py +145 -0
- package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_merge.py +50 -0
- package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_meta.py +115 -0
- package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_page_image.py +99 -0
- package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_read.py +153 -0
- package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_secure.py +71 -0
- package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_split.py +84 -0
- package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_stamp.py +143 -0
- package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_watermark.py +51 -0
- package/work/vendor/hermes-agent/skills/productivity/powerpoint/LICENSE +21 -0
- package/work/vendor/hermes-agent/skills/productivity/powerpoint/SKILL.md +220 -0
- package/work/vendor/hermes-agent/skills/productivity/powerpoint/scripts/pptx_create.py +214 -0
- package/work/vendor/hermes-agent/skills/productivity/powerpoint/scripts/pptx_edit.py +436 -0
- package/work/vendor/hermes-agent/skills/productivity/powerpoint/scripts/pptx_from_template.py +88 -0
- package/work/vendor/hermes-agent/skills/productivity/powerpoint/scripts/pptx_read.py +131 -0
- package/work/vendor/hermes-agent/skills/productivity/powerpoint/scripts/pptx_render.py +93 -0
- package/work/vendor/hermes-agent/skills/productivity/xlsx/LICENSE +21 -0
- package/work/vendor/hermes-agent/skills/productivity/xlsx/SKILL.md +196 -0
- package/work/vendor/hermes-agent/skills/productivity/xlsx/scripts/csv_to_xlsx.py +104 -0
- package/work/vendor/hermes-agent/skills/productivity/xlsx/scripts/xlsx_create.py +259 -0
- package/work/vendor/hermes-agent/skills/productivity/xlsx/scripts/xlsx_edit.py +263 -0
- package/work/vendor/hermes-agent/skills/productivity/xlsx/scripts/xlsx_read.py +160 -0
- package/work/vendor/hermes-agent/skills/productivity/xlsx/scripts/xlsx_recalc.py +111 -0
- package/work/vendor/hermes-agent/skills/productivity/xlsx/scripts/xlsx_restructure.py +337 -0
- package/work/vendor/hermes-agent/skills/productivity/xlsx/scripts/xlsx_to_csv.py +69 -0
- package/work/vendor-lock.json +11 -0
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { assertSsrfAllowedUrl, type JsonObject, type JsonValue } from "@arnilo/prism";
|
|
2
|
+
import { citation, object, providerFacts, text } from "./normalize.js";
|
|
3
|
+
import { truncateUtf8Bytes } from "./slice.js";
|
|
4
|
+
import { assertBoundedJson, boundedJson, createWebTransport, WebToolError } from "./transport.js";
|
|
5
|
+
import type {
|
|
6
|
+
FirecrawlExtractOptions,
|
|
7
|
+
FirecrawlFetchOptions,
|
|
8
|
+
FirecrawlTargetPolicy,
|
|
9
|
+
WebDocument,
|
|
10
|
+
WebExtractAdapter,
|
|
11
|
+
WebExtraction,
|
|
12
|
+
WebFetchAdapter,
|
|
13
|
+
} from "./types.js";
|
|
14
|
+
|
|
15
|
+
const ORIGIN = "https://api.firecrawl.dev";
|
|
16
|
+
export function createFirecrawlFetch(options: FirecrawlFetchOptions): WebFetchAdapter {
|
|
17
|
+
const transport = createWebTransport("firecrawl", ORIGIN, options);
|
|
18
|
+
return {
|
|
19
|
+
provider: "firecrawl",
|
|
20
|
+
async fetch(input, call = {}) {
|
|
21
|
+
return transport.run(call.signal, async (signal) => {
|
|
22
|
+
const url = await target(input, options, signal);
|
|
23
|
+
const response = await transport.json(
|
|
24
|
+
"firecrawl",
|
|
25
|
+
"api_key",
|
|
26
|
+
new URL("/v2/scrape", ORIGIN),
|
|
27
|
+
{ method: "POST", body: JSON.stringify({ url: url.toString(), formats: ["markdown"] }) },
|
|
28
|
+
signal,
|
|
29
|
+
// A scrape is billed per page, but a 429 means it was refused, not billed: retry only that.
|
|
30
|
+
{ retry: "safe-post" },
|
|
31
|
+
);
|
|
32
|
+
const root = object(response.value),
|
|
33
|
+
data = object(root.data),
|
|
34
|
+
markdown = text(data.markdown);
|
|
35
|
+
if (root.success !== true || markdown === undefined)
|
|
36
|
+
throw new WebToolError("ERR_PRISM_WEB_RESPONSE", "Firecrawl scrape returned no Markdown");
|
|
37
|
+
// A page over the fetch limit is kept up to it and flagged, never failed: the caller pages
|
|
38
|
+
// through what was kept. `maxBytes` never lifts the limit, it only lowers it.
|
|
39
|
+
const limit = Math.min(call.maxBytes ?? transport.limits.maxMarkdownBytes, transport.limits.maxMarkdownBytes);
|
|
40
|
+
const sourceTruncated = Buffer.byteLength(markdown, "utf8") > limit;
|
|
41
|
+
const providerMetadata = providerFacts(response.value, response.metadata),
|
|
42
|
+
metadata = typeof data.metadata === "object" && data.metadata ? (data.metadata as Record<string, unknown>) : {};
|
|
43
|
+
const sourceUrl = text(metadata.sourceURL) ?? url.toString();
|
|
44
|
+
return boundedJson<WebDocument>(
|
|
45
|
+
{
|
|
46
|
+
...citation("firecrawl", sourceUrl),
|
|
47
|
+
title: text(metadata.title),
|
|
48
|
+
markdown: sourceTruncated ? truncateUtf8Bytes(markdown, limit) : markdown,
|
|
49
|
+
...(sourceTruncated ? { sourceTruncated: true } : {}),
|
|
50
|
+
retrievedAt: new Date().toISOString(),
|
|
51
|
+
metadata: providerMetadata,
|
|
52
|
+
untrusted: true,
|
|
53
|
+
},
|
|
54
|
+
transport.limits.maxJsonDepth,
|
|
55
|
+
transport.limits.maxJsonProperties,
|
|
56
|
+
transport.limits.maxAggregateBytes,
|
|
57
|
+
);
|
|
58
|
+
});
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function createFirecrawlExtractor(options: FirecrawlExtractOptions): WebExtractAdapter {
|
|
64
|
+
const transport = createWebTransport("firecrawl", ORIGIN, options);
|
|
65
|
+
assertSchema(options.schema, transport.limits.maxSchemaBytes, transport.limits.maxJsonDepth, transport.limits.maxJsonProperties);
|
|
66
|
+
return {
|
|
67
|
+
provider: "firecrawl",
|
|
68
|
+
async extract(inputs, call = {}) {
|
|
69
|
+
return transport.run(call.signal, async (signal) => {
|
|
70
|
+
if (!inputs.length || inputs.length > transport.limits.maxUrls)
|
|
71
|
+
throw new WebToolError("ERR_PRISM_WEB_INPUT", "Firecrawl URL count is invalid");
|
|
72
|
+
const urls: string[] = [];
|
|
73
|
+
for (const input of inputs) urls.push((await target(input, options, signal)).toString());
|
|
74
|
+
let response = await transport.json(
|
|
75
|
+
"firecrawl",
|
|
76
|
+
"api_key",
|
|
77
|
+
new URL("/v2/extract", ORIGIN),
|
|
78
|
+
{ method: "POST", body: JSON.stringify({ urls, schema: options.schema }) },
|
|
79
|
+
signal,
|
|
80
|
+
// Creating an extract job is billed work: never submit it twice.
|
|
81
|
+
{ retry: "never" },
|
|
82
|
+
);
|
|
83
|
+
let root = object(response.value),
|
|
84
|
+
metadata = providerFacts(response.value, response.metadata);
|
|
85
|
+
if (root.data === undefined) {
|
|
86
|
+
const id = text(root.id);
|
|
87
|
+
if (!id || !/^[A-Za-z0-9_-]{1,256}$/u.test(id))
|
|
88
|
+
throw new WebToolError("ERR_PRISM_WEB_RESPONSE", "Firecrawl extract returned no data or job id");
|
|
89
|
+
for (let poll = 0; poll < transport.limits.maxPollingAttempts; poll++) {
|
|
90
|
+
if (transport.limits.pollingDelayMs) await (options.sleep ?? defaultSleep)(transport.limits.pollingDelayMs, signal);
|
|
91
|
+
// Polling a job by id is a GET: safe to retry.
|
|
92
|
+
response = await transport.json("firecrawl", "api_key", new URL(`/v2/extract/${id}`, ORIGIN), { method: "GET" }, signal, {
|
|
93
|
+
retry: "idempotent",
|
|
94
|
+
});
|
|
95
|
+
root = object(response.value);
|
|
96
|
+
metadata = providerFacts(response.value, response.metadata) ?? metadata;
|
|
97
|
+
if (root.status === "failed") throw new WebToolError("ERR_PRISM_WEB_RESPONSE", "Firecrawl extraction failed");
|
|
98
|
+
if (root.data !== undefined || root.status === "completed") break;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
if (root.data === undefined) throw new WebToolError("ERR_PRISM_WEB_LIMIT", "Firecrawl extraction polling limit reached");
|
|
102
|
+
assertBoundedJson(root.data, transport.limits.maxJsonDepth, transport.limits.maxJsonProperties, transport.limits.maxExtractBytes);
|
|
103
|
+
let checked: ReturnType<typeof options.validator.validate>;
|
|
104
|
+
try {
|
|
105
|
+
checked = options.validator.validate(options.schema, root.data);
|
|
106
|
+
} catch {
|
|
107
|
+
throw new WebToolError("ERR_PRISM_WEB_SCHEMA", "Firecrawl extraction validation failed");
|
|
108
|
+
}
|
|
109
|
+
if (!checked.ok) throw new WebToolError("ERR_PRISM_WEB_SCHEMA", "Firecrawl extraction did not match host schema");
|
|
110
|
+
return boundedJson<WebExtraction>(
|
|
111
|
+
{ provider: "firecrawl", urls, data: root.data as JsonValue, retrievedAt: new Date().toISOString(), metadata, untrusted: true },
|
|
112
|
+
transport.limits.maxJsonDepth,
|
|
113
|
+
transport.limits.maxJsonProperties,
|
|
114
|
+
transport.limits.maxAggregateBytes,
|
|
115
|
+
);
|
|
116
|
+
});
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async function target(input: string, policy: FirecrawlTargetPolicy, signal: AbortSignal): Promise<URL> {
|
|
122
|
+
assertSsrfAllowedUrl(input, policy.ssrf);
|
|
123
|
+
const url = new URL(input);
|
|
124
|
+
await policy.validateUrl?.(url, signal);
|
|
125
|
+
return url;
|
|
126
|
+
}
|
|
127
|
+
function assertSchema(schema: JsonObject, bytes: number, depth: number, properties: number): void {
|
|
128
|
+
assertBoundedJson(schema, depth, properties, bytes);
|
|
129
|
+
const stack: unknown[] = [schema];
|
|
130
|
+
while (stack.length) {
|
|
131
|
+
const value = stack.pop();
|
|
132
|
+
if (!value || typeof value !== "object") continue;
|
|
133
|
+
for (const [key, child] of Object.entries(value)) {
|
|
134
|
+
if (key === "$ref" && (typeof child !== "string" || !child.startsWith("#")))
|
|
135
|
+
throw new WebToolError("ERR_PRISM_WEB_SCHEMA", "Remote JSON Schema references are not allowed");
|
|
136
|
+
stack.push(child);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
function defaultSleep(ms: number, signal: AbortSignal): Promise<void> {
|
|
141
|
+
return new Promise((resolve, reject) => {
|
|
142
|
+
try {
|
|
143
|
+
signal.throwIfAborted();
|
|
144
|
+
} catch (error) {
|
|
145
|
+
reject(error);
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
const cleanup = () => signal.removeEventListener("abort", abort);
|
|
149
|
+
const timer = setTimeout(() => {
|
|
150
|
+
cleanup();
|
|
151
|
+
resolve();
|
|
152
|
+
}, ms);
|
|
153
|
+
const abort = () => {
|
|
154
|
+
clearTimeout(timer);
|
|
155
|
+
cleanup();
|
|
156
|
+
reject(signal.reason);
|
|
157
|
+
};
|
|
158
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
159
|
+
});
|
|
160
|
+
}
|
package/web/src/index.ts
ADDED
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@arnilo/prism-tools/web` — the first-party web search and fetch tools as a package.
|
|
3
|
+
*
|
|
4
|
+
* The default export registers `web_search`, `web_fetch`, and (through the named `createWebTools`
|
|
5
|
+
* factory) `web_extract`, each declaring `effects: ["network"]`. The backend is selected by config:
|
|
6
|
+
*
|
|
7
|
+
* - `brave` / `exa`: a search key (ref-only) plus a fetch backend (`native` by default, or Firecrawl).
|
|
8
|
+
* - `firecrawl`: fetch and extract only.
|
|
9
|
+
* - `native`: keyless fetch only, on the kernel's DNS-pinned request with a host-side redirect loop.
|
|
10
|
+
* - `obscura`: a host-installed binary; no key.
|
|
11
|
+
*
|
|
12
|
+
* A missing key ref or a missing Obscura binary registers nothing and writes a `warn` status note.
|
|
13
|
+
* When a fetch backend is configured the package also provides the `web.fetch` service.
|
|
14
|
+
*
|
|
15
|
+
* The 0.12 factory names (`createBraveSearch`, `createExaSearch`, `createFirecrawlFetch`,
|
|
16
|
+
* `createFirecrawlExtractor`, `createWebTools`, `createObscuraWebTools`, `connectObscuraCdp`,
|
|
17
|
+
* `createObscuraMcpTools`) stay exported for hosts that build tool lists by hand.
|
|
18
|
+
*
|
|
19
|
+
* ```ts
|
|
20
|
+
* import toolsWeb from "@arnilo/prism-tools/web";
|
|
21
|
+
* import { createAgent } from "@arnilo/prism";
|
|
22
|
+
*
|
|
23
|
+
* const agent = await createAgent({
|
|
24
|
+
* model,
|
|
25
|
+
* packages: [toolsWeb({ backend: "brave", apiKey: "env:BRAVE_API_KEY", fetch: { backend: "firecrawl", apiKey: "env:FIRECRAWL_API_KEY" } })],
|
|
26
|
+
* });
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
import { fileURLToPath } from "node:url";
|
|
30
|
+
import {
|
|
31
|
+
assertSsrfAllowedUrl,
|
|
32
|
+
definePackage,
|
|
33
|
+
type JsonObject,
|
|
34
|
+
type JsonSchema,
|
|
35
|
+
type PackageInstance,
|
|
36
|
+
type Secret,
|
|
37
|
+
type ToolArgumentValidator,
|
|
38
|
+
type ToolDefinition,
|
|
39
|
+
validateJsonSchema,
|
|
40
|
+
} from "@arnilo/prism";
|
|
41
|
+
import manifest from "../../package.json" with { type: "json" };
|
|
42
|
+
|
|
43
|
+
import { createBraveSearch } from "./brave.js";
|
|
44
|
+
import { createFetchCache, resolveFetchCache, withFetchCache } from "./cache.js";
|
|
45
|
+
import { type ToolsWebConfig, toolsWebConfigSchema } from "./config.js";
|
|
46
|
+
import { createDomainPolicy, type DomainPolicy } from "./domains.js";
|
|
47
|
+
import { createExaSearch } from "./exa.js";
|
|
48
|
+
import { createFirecrawlExtractor, createFirecrawlFetch } from "./firecrawl.js";
|
|
49
|
+
import { createKagiSearch } from "./kagi.js";
|
|
50
|
+
import { resolveWebLimits } from "./limits.js";
|
|
51
|
+
import { createNativeFetch } from "./native/fetch.js";
|
|
52
|
+
import { OBSCURA_GUARDED_VERSION, probeObscuraVersion } from "./obscura/cli.js";
|
|
53
|
+
import { resolveObscuraWebLimits } from "./obscura/limits.js";
|
|
54
|
+
import { resolveObscuraCommand } from "./obscura/process.js";
|
|
55
|
+
import { createObscuraPageFetcher, createObscuraWebTools } from "./obscura/web.js";
|
|
56
|
+
import { createPromptFetch, DEFAULT_PROMPT_INPUT_CHARS, DEFAULT_PROMPT_OUTPUT_TOKENS, withOutputBudget } from "./prompt-fetch.js";
|
|
57
|
+
import { createSearxngSearch } from "./searxng.js";
|
|
58
|
+
import { createTavilySearch } from "./tavily.js";
|
|
59
|
+
import { createWebTools } from "./tools.js";
|
|
60
|
+
import type { PromptFetch, WebCredentialSource, WebFetchAdapter, WebFetchService, WebSearchAdapter } from "./types.js";
|
|
61
|
+
|
|
62
|
+
// The moved 0.12 surface keeps its names, now behind one entry point.
|
|
63
|
+
export * from "./brave.js";
|
|
64
|
+
export * from "./cache.js";
|
|
65
|
+
export type {
|
|
66
|
+
ToolsWebBackend,
|
|
67
|
+
ToolsWebCacheConfig,
|
|
68
|
+
ToolsWebConfig,
|
|
69
|
+
ToolsWebDomainsConfig,
|
|
70
|
+
ToolsWebFetchConfig,
|
|
71
|
+
ToolsWebObscuraConfig,
|
|
72
|
+
ToolsWebPromptConfig,
|
|
73
|
+
ToolsWebSearxngConfig,
|
|
74
|
+
} from "./config.js";
|
|
75
|
+
export { toolsWebConfigSchema } from "./config.js";
|
|
76
|
+
export * from "./domains.js";
|
|
77
|
+
export * from "./exa.js";
|
|
78
|
+
export * from "./firecrawl.js";
|
|
79
|
+
export * from "./kagi.js";
|
|
80
|
+
export * from "./limits.js";
|
|
81
|
+
export * from "./native/fetch.js";
|
|
82
|
+
export * from "./normalize.js";
|
|
83
|
+
export * from "./obscura/index.js";
|
|
84
|
+
export * from "./prompt-fetch.js";
|
|
85
|
+
export * from "./searxng.js";
|
|
86
|
+
export * from "./tavily.js";
|
|
87
|
+
export * from "./tools.js";
|
|
88
|
+
export * from "./transport.js";
|
|
89
|
+
export * from "./types.js";
|
|
90
|
+
|
|
91
|
+
import type { ToolsWebBackend } from "./config.js";
|
|
92
|
+
|
|
93
|
+
const BACKENDS: readonly ToolsWebConfig["backend"][] = ["brave", "exa", "firecrawl", "native", "obscura", "searxng", "tavily", "kagi"];
|
|
94
|
+
/** Identifies the package on every native fetch; config `fetch.userAgent` replaces it. */
|
|
95
|
+
const DEFAULT_USER_AGENT = `Prism-Web-Fetch/${manifest.version} (+https://github.com/ashiqrniloy/prism)`;
|
|
96
|
+
|
|
97
|
+
function errorMessage(error: unknown): string {
|
|
98
|
+
return error instanceof Error ? error.message : String(error);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** One validator, no second implementation: the package validates extraction against the kernel's subset. */
|
|
102
|
+
const schemaValidator: ToolArgumentValidator = {
|
|
103
|
+
validate: (schema, value) => validateJsonSchema(schema as JsonSchema, value),
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
/** Compiles the schema now, so an unsupported subset fails setup instead of the first extraction. */
|
|
107
|
+
function checkExtractSchema(schema: JsonObject): JsonObject {
|
|
108
|
+
try {
|
|
109
|
+
validateJsonSchema(schema as JsonSchema, {});
|
|
110
|
+
return schema;
|
|
111
|
+
} catch (error) {
|
|
112
|
+
throw new Error(`prism-tools-web: extract.schema is not supported: ${errorMessage(error)}`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** `web_extract` over the configured schema, or nothing when the config asks for no extraction. */
|
|
117
|
+
function extractAdapter(
|
|
118
|
+
config: ToolsWebConfig["extract"],
|
|
119
|
+
credentials: WebCredentialSource,
|
|
120
|
+
limits: ReturnType<typeof resolveWebLimits>,
|
|
121
|
+
validateUrl: (url: URL) => void,
|
|
122
|
+
) {
|
|
123
|
+
if (config === undefined) return undefined;
|
|
124
|
+
const schema = checkExtractSchema(config.schema);
|
|
125
|
+
try {
|
|
126
|
+
return createFirecrawlExtractor({ credentials, limits, validateUrl, schema, validator: schemaValidator });
|
|
127
|
+
} catch (error) {
|
|
128
|
+
throw new Error(`prism-tools-web: extract.schema is not supported: ${errorMessage(error)}`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** `fetch.userAgent` over the package's own name, keyless: no credential source is involved. */
|
|
133
|
+
function nativeFetch(config: ToolsWebConfig, limits: ReturnType<typeof resolveWebLimits>, domains?: DomainPolicy): WebFetchAdapter {
|
|
134
|
+
return createNativeFetch({
|
|
135
|
+
userAgent: config.fetch?.userAgent ?? DEFAULT_USER_AGENT,
|
|
136
|
+
limits,
|
|
137
|
+
...(domains === undefined ? {} : { domains }),
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** The `web.fetch` service, with the host domain policy enforced before the adapter runs. */
|
|
142
|
+
function fetchService(fetch: WebFetchAdapter, domains: DomainPolicy | undefined): WebFetchService {
|
|
143
|
+
return guardService({ fetch: (url, options) => fetch.fetch(url, options) }, domains);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Same guard for a service that is not built from a single adapter (Obscura's page fetcher). */
|
|
147
|
+
function guardService(service: WebFetchService, domains: DomainPolicy | undefined): WebFetchService {
|
|
148
|
+
if (domains === undefined) return service;
|
|
149
|
+
return {
|
|
150
|
+
fetch: (url, options) => {
|
|
151
|
+
domains.assert(url);
|
|
152
|
+
return service.fetch(url, options);
|
|
153
|
+
},
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** A credential source over a secret the package already resolved from its ref. */
|
|
158
|
+
function secretSource(secret: Secret): WebCredentialSource {
|
|
159
|
+
return async () => secret.value;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const definition = definePackage<ToolsWebConfig>({
|
|
163
|
+
name: "@arnilo/prism-tools/web",
|
|
164
|
+
version: manifest.version,
|
|
165
|
+
description: "Web search and fetch tools with declared network effects, ref-only keys, and a web.fetch service.",
|
|
166
|
+
requires: { api: 1 },
|
|
167
|
+
// `models` is declared statically (capabilities are not per-config); `models.client` is only called
|
|
168
|
+
// when `fetch.prompt` is configured.
|
|
169
|
+
capabilities: ["credentials", "models"],
|
|
170
|
+
config: toolsWebConfigSchema,
|
|
171
|
+
setup: async (prism) => {
|
|
172
|
+
// Registered first: `requiresTools` hides the skill whenever this config leaves `web_fetch` unregistered.
|
|
173
|
+
prism.skills.addDir(fileURLToPath(new URL("../skills", import.meta.url)));
|
|
174
|
+
const backend = prism.config.backend;
|
|
175
|
+
if (backend === undefined || !BACKENDS.includes(backend)) {
|
|
176
|
+
throw new Error("prism-tools-web: config needs backend (one of brave, exa, firecrawl, native, obscura)");
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
let limits: ReturnType<typeof resolveWebLimits>;
|
|
180
|
+
try {
|
|
181
|
+
limits = resolveWebLimits(prism.config.limits);
|
|
182
|
+
} catch (error) {
|
|
183
|
+
throw new Error(`prism-tools-web: invalid limits config: ${errorMessage(error)}`);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
let domains: DomainPolicy | undefined;
|
|
187
|
+
if (prism.config.domains !== undefined) {
|
|
188
|
+
try {
|
|
189
|
+
domains = createDomainPolicy(prism.config.domains);
|
|
190
|
+
} catch (error) {
|
|
191
|
+
throw new Error(`prism-tools-web: invalid domains config: ${errorMessage(error)}`);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// One cache for the whole package: `prism.state.agent` keeps it in process memory for the
|
|
196
|
+
// agent's lifetime, and the session hook drops a closed session's pages.
|
|
197
|
+
let cacheConfig: ReturnType<typeof resolveFetchCache>;
|
|
198
|
+
try {
|
|
199
|
+
cacheConfig = resolveFetchCache(prism.config.cache);
|
|
200
|
+
} catch (error) {
|
|
201
|
+
throw new Error(`prism-tools-web: invalid cache config: ${errorMessage(error)}`);
|
|
202
|
+
}
|
|
203
|
+
const cache = prism.state.agent("fetchCache", createFetchCache(cacheConfig)).get();
|
|
204
|
+
prism.hooks.on("session.closed", (event) => cache.dropSession(event.sessionId));
|
|
205
|
+
|
|
206
|
+
// Opt-in question answering: the host picks the model, so a bad ref is a warning, not a failed load.
|
|
207
|
+
let prompt: PromptFetch | undefined;
|
|
208
|
+
const promptConfig = prism.config.fetch?.prompt;
|
|
209
|
+
if (promptConfig !== undefined) {
|
|
210
|
+
const maxInputChars = promptConfig.maxInputChars ?? DEFAULT_PROMPT_INPUT_CHARS;
|
|
211
|
+
const maxOutputTokens = promptConfig.maxOutputTokens ?? DEFAULT_PROMPT_OUTPUT_TOKENS;
|
|
212
|
+
try {
|
|
213
|
+
const resolved = prism.models.client(promptConfig.model).model;
|
|
214
|
+
prompt = createPromptFetch(prism.models.client(withOutputBudget(resolved, maxOutputTokens)), { maxInputChars });
|
|
215
|
+
} catch (error) {
|
|
216
|
+
prism.status.note("warn", `fetch.prompt: model could not be resolved (${errorMessage(error)}); web_fetch has no prompt argument`);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const tools: ToolDefinition[] = [];
|
|
221
|
+
let service: WebFetchService | undefined;
|
|
222
|
+
const toolOptions = {
|
|
223
|
+
...(domains === undefined ? {} : { domains }),
|
|
224
|
+
cache,
|
|
225
|
+
...(prompt === undefined ? {} : { prompt }),
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
const validateUrl = (url: URL): void => assertSsrfAllowedUrl(url.toString());
|
|
229
|
+
/** Every search backend shares one fetch/extract wiring; only the search adapter differs. */
|
|
230
|
+
const registerSearch = async (name: ToolsWebBackend, search: WebSearchAdapter): Promise<void> => {
|
|
231
|
+
const fetchBackend = prism.config.fetch?.backend ?? "native";
|
|
232
|
+
let fetchAdapter: WebFetchAdapter | undefined;
|
|
233
|
+
let extract: ReturnType<typeof createFirecrawlExtractor> | undefined;
|
|
234
|
+
if (fetchBackend === "native") {
|
|
235
|
+
fetchAdapter = nativeFetch(prism.config, limits, domains);
|
|
236
|
+
// Extraction is a Firecrawl endpoint: without its key the rest of the package still works.
|
|
237
|
+
if (prism.config.extract !== undefined)
|
|
238
|
+
prism.status.note("warn", `${name}: extract needs fetch backend firecrawl with an apiKey ref; skipping web_extract`);
|
|
239
|
+
} else if (fetchBackend === "firecrawl") {
|
|
240
|
+
const fetchRef = prism.config.fetch?.apiKey;
|
|
241
|
+
const fetchSecret = typeof fetchRef === "string" && fetchRef.length > 0 ? await prism.credentials.resolve(fetchRef) : undefined;
|
|
242
|
+
if (fetchSecret === undefined) {
|
|
243
|
+
prism.status.note("warn", `${name}: fetch backend firecrawl needs an apiKey ref; registering web_search only`);
|
|
244
|
+
if (prism.config.extract !== undefined)
|
|
245
|
+
prism.status.note("warn", `${name}: extract needs the firecrawl fetch apiKey ref; skipping web_extract`);
|
|
246
|
+
} else {
|
|
247
|
+
fetchAdapter = createFirecrawlFetch({ credentials: secretSource(fetchSecret), limits, validateUrl });
|
|
248
|
+
extract = extractAdapter(prism.config.extract, secretSource(fetchSecret), limits, validateUrl);
|
|
249
|
+
}
|
|
250
|
+
} else if (prism.config.extract !== undefined) {
|
|
251
|
+
prism.status.note("warn", `${name}: extract needs fetch backend firecrawl with an apiKey ref; skipping web_extract`);
|
|
252
|
+
}
|
|
253
|
+
tools.push(
|
|
254
|
+
...createWebTools({
|
|
255
|
+
search,
|
|
256
|
+
...(fetchAdapter === undefined ? {} : { fetch: fetchAdapter }),
|
|
257
|
+
...(extract === undefined ? {} : { extract }),
|
|
258
|
+
...toolOptions,
|
|
259
|
+
}),
|
|
260
|
+
);
|
|
261
|
+
if (fetchAdapter !== undefined) service = fetchService(fetchAdapter, domains);
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
if (backend === "native") {
|
|
265
|
+
const fetch = nativeFetch(prism.config, limits, domains);
|
|
266
|
+
tools.push(...createWebTools({ fetch, ...toolOptions }));
|
|
267
|
+
service = fetchService(fetch, domains);
|
|
268
|
+
} else if (backend === "obscura") {
|
|
269
|
+
const binary = prism.config.obscura?.binary ?? "obscura";
|
|
270
|
+
const command = resolveObscuraCommand(binary);
|
|
271
|
+
if (command === undefined) {
|
|
272
|
+
prism.status.note("warn", `obscura: binary ${JSON.stringify(binary)} is not an executable file`);
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
let obscuraLimits: ReturnType<typeof resolveObscuraWebLimits> | undefined;
|
|
276
|
+
try {
|
|
277
|
+
obscuraLimits = prism.config.obscura?.limits === undefined ? undefined : resolveObscuraWebLimits(prism.config.obscura.limits);
|
|
278
|
+
} catch (error) {
|
|
279
|
+
throw new Error(`prism-tools-web: invalid limits config: ${errorMessage(error)}`);
|
|
280
|
+
}
|
|
281
|
+
const bounds = obscuraLimits === undefined ? {} : { limits: obscuraLimits };
|
|
282
|
+
const nativeTools = prism.config.obscura?.nativeTools ?? true;
|
|
283
|
+
// Older Obscura clients validate literal hosts per hop but resolve DNS separately, so a redirect
|
|
284
|
+
// to a public-looking name that resolves private slips through. Gate the page path on the first
|
|
285
|
+
// release that re-resolves every hop, and fail closed when the version is unknown.
|
|
286
|
+
const probe = await probeObscuraVersion({ command });
|
|
287
|
+
const fetchTools = probe.guarded;
|
|
288
|
+
if (!fetchTools) {
|
|
289
|
+
prism.status.note(
|
|
290
|
+
"warn",
|
|
291
|
+
`obscura: version ${probe.version ?? "unverified"} lacks the per-hop private-network guard (needs ${OBSCURA_GUARDED_VERSION}+); registering web_search only and serving web_fetch from the native backend`,
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
tools.push(
|
|
295
|
+
...createObscuraWebTools({
|
|
296
|
+
command,
|
|
297
|
+
nativeTools,
|
|
298
|
+
fetchTools,
|
|
299
|
+
...bounds,
|
|
300
|
+
cache,
|
|
301
|
+
...(prompt === undefined ? {} : { prompt }),
|
|
302
|
+
...(domains === undefined ? {} : { domains }),
|
|
303
|
+
}).tools,
|
|
304
|
+
);
|
|
305
|
+
if (fetchTools) {
|
|
306
|
+
// The same page path the web_fetch tool uses, so wiki and RAG URL ingestion work with Obscura.
|
|
307
|
+
service = guardService(createObscuraPageFetcher({ command, ...bounds }), domains);
|
|
308
|
+
} else {
|
|
309
|
+
const fetch = nativeFetch(prism.config, limits, domains);
|
|
310
|
+
tools.push(...createWebTools({ fetch, ...toolOptions }));
|
|
311
|
+
service = fetchService(fetch, domains);
|
|
312
|
+
}
|
|
313
|
+
} else if (backend === "searxng") {
|
|
314
|
+
const origin = prism.config.searxng?.origin;
|
|
315
|
+
if (typeof origin !== "string" || origin.length === 0) {
|
|
316
|
+
prism.status.note("warn", "searxng: searxng.origin is required");
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
// The key is optional; an instance behind an authenticating proxy may need one.
|
|
320
|
+
let credentials: WebCredentialSource | undefined;
|
|
321
|
+
const keyRef = prism.config.apiKey;
|
|
322
|
+
if (typeof keyRef === "string" && keyRef.length > 0) {
|
|
323
|
+
const secret = await prism.credentials.resolve(keyRef);
|
|
324
|
+
if (secret === undefined) {
|
|
325
|
+
prism.status.note("warn", `searxng: apiKey ref ${JSON.stringify(keyRef)} did not resolve`);
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
credentials = secretSource(secret);
|
|
329
|
+
}
|
|
330
|
+
const search = createSearxngSearch({
|
|
331
|
+
origin,
|
|
332
|
+
allowLoopback: prism.config.searxng?.allowLoopback === true,
|
|
333
|
+
...(credentials === undefined ? {} : { credentials }),
|
|
334
|
+
limits,
|
|
335
|
+
});
|
|
336
|
+
await registerSearch(backend, search);
|
|
337
|
+
} else if (backend !== "firecrawl") {
|
|
338
|
+
const apiKeyRef = prism.config.apiKey;
|
|
339
|
+
if (typeof apiKeyRef !== "string" || apiKeyRef.length === 0) {
|
|
340
|
+
prism.status.note("warn", `${backend}: apiKey ref is required`);
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
const secret = await prism.credentials.resolve(apiKeyRef);
|
|
344
|
+
if (secret === undefined) {
|
|
345
|
+
prism.status.note("warn", `${backend}: apiKey ref ${JSON.stringify(apiKeyRef)} did not resolve`);
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
const credentials = secretSource(secret);
|
|
349
|
+
const search =
|
|
350
|
+
backend === "brave"
|
|
351
|
+
? createBraveSearch({ credentials, limits })
|
|
352
|
+
: backend === "exa"
|
|
353
|
+
? createExaSearch({ credentials, limits })
|
|
354
|
+
: backend === "tavily"
|
|
355
|
+
? createTavilySearch({ credentials, limits })
|
|
356
|
+
: createKagiSearch({ credentials, limits });
|
|
357
|
+
await registerSearch(backend, search);
|
|
358
|
+
} else {
|
|
359
|
+
const apiKeyRef = prism.config.apiKey;
|
|
360
|
+
if (typeof apiKeyRef !== "string" || apiKeyRef.length === 0) {
|
|
361
|
+
prism.status.note("warn", `${backend}: apiKey ref is required`);
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
const secret = await prism.credentials.resolve(apiKeyRef);
|
|
365
|
+
if (secret === undefined) {
|
|
366
|
+
prism.status.note("warn", `${backend}: apiKey ref ${JSON.stringify(apiKeyRef)} did not resolve`);
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
const credentials = secretSource(secret);
|
|
370
|
+
const fetch = createFirecrawlFetch({ credentials, limits, validateUrl });
|
|
371
|
+
const extract = extractAdapter(prism.config.extract, credentials, limits, validateUrl);
|
|
372
|
+
tools.push(...createWebTools({ fetch, ...(extract === undefined ? {} : { extract }), ...toolOptions }));
|
|
373
|
+
service = fetchService(fetch, domains);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
for (const tool of tools) prism.tools.register(tool);
|
|
377
|
+
if (service !== undefined) prism.services.provide("web.fetch", withFetchCache(guardService(service, domains), cache));
|
|
378
|
+
},
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
/** The package definition. Pass config through `packageConfig` or use {@link toolsWeb}. */
|
|
382
|
+
export default definition;
|
|
383
|
+
|
|
384
|
+
/** The package with config baked in, for `packages: [toolsWeb({ backend })]`. */
|
|
385
|
+
export function toolsWeb(config: ToolsWebConfig): PackageInstance {
|
|
386
|
+
return definition(config);
|
|
387
|
+
}
|
package/web/src/kagi.ts
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { searchControls } from "./controls.js";
|
|
2
|
+
import { normalizeSearchResults, providerFacts } from "./normalize.js";
|
|
3
|
+
import { boundedJson, createWebTransport, WebToolError } from "./transport.js";
|
|
4
|
+
import type { SearchControl, WebAdapterOptions, WebSearchAdapter, WebSearchControlValues } from "./types.js";
|
|
5
|
+
|
|
6
|
+
/** Kagi API v1: `GET /api/v0/search` is deprecated and deliberately not implemented. */
|
|
7
|
+
const ORIGIN = "https://kagi.com";
|
|
8
|
+
/** Kagi has no language control; `year` is a date filter because `timeRelative` has no year value. */
|
|
9
|
+
const SUPPORTS: ReadonlySet<SearchControl> = new Set(["freshness", "includeDomains", "excludeDomains", "country", "offset"]);
|
|
10
|
+
const VALUES: WebSearchControlValues = { freshness: ["day", "week", "month", "year"] };
|
|
11
|
+
const TIME_RELATIVE: Readonly<Record<string, string>> = { day: "day", week: "week", month: "month" };
|
|
12
|
+
const MAX_PAGE = 10;
|
|
13
|
+
const HARD_OFFSET = MAX_PAGE - 1;
|
|
14
|
+
export function createKagiSearch(options: WebAdapterOptions): WebSearchAdapter {
|
|
15
|
+
// Kagi's API key is an `Authorization: Bot <token>` credential, not a bearer token.
|
|
16
|
+
const transport = createWebTransport("kagi", ORIGIN, options, { auth: "bot" });
|
|
17
|
+
return {
|
|
18
|
+
provider: "kagi",
|
|
19
|
+
limits: transport.limits,
|
|
20
|
+
supports: SUPPORTS,
|
|
21
|
+
controlValues: VALUES,
|
|
22
|
+
async search(query, call = {}) {
|
|
23
|
+
return transport.run(call.signal, async (signal) => {
|
|
24
|
+
// A `language` request fails here, naming the argument Kagi cannot honor.
|
|
25
|
+
const controls = searchControls(SUPPORTS, call, VALUES);
|
|
26
|
+
const normalized = query.trim();
|
|
27
|
+
if (!normalized || Buffer.byteLength(normalized) > transport.limits.maxQueryBytes)
|
|
28
|
+
throw new WebToolError("ERR_PRISM_WEB_INPUT", "Kagi query is empty or exceeds byte limit");
|
|
29
|
+
const count = call.count ?? transport.limits.maxResults;
|
|
30
|
+
if (!Number.isSafeInteger(count) || count < 1 || count > transport.limits.maxResults)
|
|
31
|
+
throw new WebToolError("ERR_PRISM_WEB_INPUT", "Kagi result count is invalid");
|
|
32
|
+
if (controls.offset !== undefined && controls.offset > HARD_OFFSET)
|
|
33
|
+
throw new WebToolError("ERR_PRISM_WEB_INPUT", `offset must be 0..${HARD_OFFSET}`);
|
|
34
|
+
const filters = {
|
|
35
|
+
...(controls.country === undefined ? {} : { region: controls.country.toUpperCase() }),
|
|
36
|
+
...(controls.freshness === "year" ? { after: new Date(Date.now() - 365 * 86_400_000).toISOString().slice(0, 10) } : {}),
|
|
37
|
+
};
|
|
38
|
+
const lens = {
|
|
39
|
+
...(controls.includeDomains === undefined ? {} : { sitesIncluded: [...controls.includeDomains] }),
|
|
40
|
+
...(controls.excludeDomains === undefined ? {} : { sitesExcluded: [...controls.excludeDomains] }),
|
|
41
|
+
...(controls.freshness === undefined || controls.freshness === "year"
|
|
42
|
+
? {}
|
|
43
|
+
: { timeRelative: TIME_RELATIVE[controls.freshness]! }),
|
|
44
|
+
};
|
|
45
|
+
const body = JSON.stringify({
|
|
46
|
+
query: normalized,
|
|
47
|
+
workflow: "search",
|
|
48
|
+
page: (controls.offset ?? 0) + 1,
|
|
49
|
+
limit: count,
|
|
50
|
+
...(Object.keys(filters).length === 0 ? {} : { filters }),
|
|
51
|
+
...(Object.keys(lens).length === 0 ? {} : { lens }),
|
|
52
|
+
});
|
|
53
|
+
// Search is billed per request; a 429 was refused, so retrying it cannot double-bill.
|
|
54
|
+
const response = await transport.json("kagi", "api_key", new URL("/api/v1/search", ORIGIN), { method: "POST", body }, signal, {
|
|
55
|
+
retry: "safe-post",
|
|
56
|
+
});
|
|
57
|
+
const metadata = providerFacts(response.value, response.metadata);
|
|
58
|
+
return boundedJson(
|
|
59
|
+
{
|
|
60
|
+
provider: "kagi" as const,
|
|
61
|
+
query: normalized,
|
|
62
|
+
results: normalizeSearchResults("kagi", normalized, response.value, count),
|
|
63
|
+
metadata,
|
|
64
|
+
untrusted: true as const,
|
|
65
|
+
},
|
|
66
|
+
transport.limits.maxJsonDepth,
|
|
67
|
+
transport.limits.maxJsonProperties,
|
|
68
|
+
transport.limits.maxAggregateBytes,
|
|
69
|
+
);
|
|
70
|
+
});
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { ResolvedWebLimits, WebLimits } from "./types.js";
|
|
2
|
+
|
|
3
|
+
export const DEFAULT_WEB_LIMITS: ResolvedWebLimits = {
|
|
4
|
+
maxQueryBytes: 4 * 1024,
|
|
5
|
+
maxResults: 10,
|
|
6
|
+
maxUrls: 5,
|
|
7
|
+
maxRequestBytes: 256 * 1024,
|
|
8
|
+
maxResponseBytes: 2 * 1024 * 1024,
|
|
9
|
+
maxMarkdownBytes: 1024 * 1024,
|
|
10
|
+
maxExtractBytes: 256 * 1024,
|
|
11
|
+
maxSchemaBytes: 64 * 1024,
|
|
12
|
+
maxAggregateBytes: 2 * 1024 * 1024,
|
|
13
|
+
maxJsonDepth: 64,
|
|
14
|
+
maxJsonProperties: 10_000,
|
|
15
|
+
maxRetries: 2,
|
|
16
|
+
maxRateLimitDelayMs: 5_000,
|
|
17
|
+
maxConcurrency: 4,
|
|
18
|
+
maxQueued: 64,
|
|
19
|
+
maxPollingAttempts: 20,
|
|
20
|
+
pollingDelayMs: 1_000,
|
|
21
|
+
timeoutMs: 60_000,
|
|
22
|
+
};
|
|
23
|
+
export const HARD_WEB_LIMITS: ResolvedWebLimits = {
|
|
24
|
+
maxQueryBytes: 16 * 1024,
|
|
25
|
+
maxResults: 20,
|
|
26
|
+
maxUrls: 20,
|
|
27
|
+
maxRequestBytes: 1024 * 1024,
|
|
28
|
+
maxResponseBytes: 16 * 1024 * 1024,
|
|
29
|
+
maxMarkdownBytes: 8 * 1024 * 1024,
|
|
30
|
+
maxExtractBytes: 1024 * 1024,
|
|
31
|
+
maxSchemaBytes: 256 * 1024,
|
|
32
|
+
maxAggregateBytes: 16 * 1024 * 1024,
|
|
33
|
+
maxJsonDepth: 128,
|
|
34
|
+
maxJsonProperties: 100_000,
|
|
35
|
+
maxRetries: 4,
|
|
36
|
+
maxRateLimitDelayMs: 60_000,
|
|
37
|
+
maxConcurrency: 16,
|
|
38
|
+
maxQueued: 256,
|
|
39
|
+
maxPollingAttempts: 100,
|
|
40
|
+
pollingDelayMs: 60_000,
|
|
41
|
+
timeoutMs: 30 * 60_000,
|
|
42
|
+
};
|
|
43
|
+
export function resolveWebLimits(input: WebLimits = {}): ResolvedWebLimits {
|
|
44
|
+
const out = {} as Record<keyof ResolvedWebLimits, number>;
|
|
45
|
+
for (const key of Object.keys(DEFAULT_WEB_LIMITS) as (keyof ResolvedWebLimits)[]) {
|
|
46
|
+
const value = input[key] ?? DEFAULT_WEB_LIMITS[key];
|
|
47
|
+
const permitsZero = key === "maxRetries" || key === "pollingDelayMs";
|
|
48
|
+
if (!Number.isSafeInteger(value) || value < (permitsZero ? 0 : 1) || value > HARD_WEB_LIMITS[key])
|
|
49
|
+
throw new RangeError(
|
|
50
|
+
`${key} must be ${permitsZero ? "a non-negative" : "a positive"} safe integer no greater than ${HARD_WEB_LIMITS[key]}`,
|
|
51
|
+
);
|
|
52
|
+
out[key] = value;
|
|
53
|
+
}
|
|
54
|
+
return out as unknown as ResolvedWebLimits;
|
|
55
|
+
}
|