@warlock.js/ai-tools 5.19.0 → 5.20.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.
@@ -1,129 +1,129 @@
1
- ---
2
- name: use-web-and-http-tools
3
- description: "Use the web + HTTP tool belt in @warlock.js/ai-tools; use when you need to use web and http tools."
4
- ---
5
-
6
- # Use the web + HTTP tool belt
7
-
8
- `@warlock.js/ai-tools` ships five ready-made tools. Import the package once for its side effect and they register on the shared `ai` object under `ai.tools.*` — each is a factory returning a `ToolContract` you spread into `ai.agent({ tools: [...] })`.
9
-
10
- ```ts
11
- import "@warlock.js/ai-tools";
12
- import { ai } from "@warlock.js/ai";
13
- import { OpenAISDK } from "@warlock.js/ai-openai";
14
-
15
- const openai = new OpenAISDK({ apiKey: process.env.OPENAI_API_KEY! });
16
-
17
- const agent = ai.agent({
18
- model: openai.model({ name: "gpt-4o" }),
19
- systemPrompt: "Research, then call the API. Read tool errors and adjust.",
20
- tools: [
21
- ai.tools.webSearch({ provider: "tavily" }),
22
- ai.tools.fetchUrl({ extract: "text", allowHosts: ["docs.stripe.com"] }),
23
- ai.tools.http({ baseUrl: "https://api.stripe.com", allowHosts: ["api.stripe.com"] }),
24
- ai.tools.calculator(),
25
- ai.tools.dateTime({ defaultTimeZone: "UTC" }),
26
- ],
27
- maxTrips: 15,
28
- });
29
- ```
30
-
31
- The construction `options` are the **rails** — they bound what the model may do; the model supplies the per-call inputs *within* those rails. Least-privilege is just array composition: hand an agent only the tools (and only the methods/hosts) it needs.
32
-
33
- ## The five tools
34
-
35
- | Factory | Name | Model input | Returns |
36
- |---|---|---|---|
37
- | `ai.tools.webSearch(options)` | `web_search` | `{ query, maxResults? }` | `{ results: { title, url, snippet, score? }[] }` |
38
- | `ai.tools.fetchUrl(options?)` | `fetch_url` | `{ url }` | `{ url, status, content, truncated }` |
39
- | `ai.tools.http(options?)` | `http_request` | `{ method?, url, headers?, body? }` | `{ status, headers, body, truncated }` |
40
- | `ai.tools.calculator(options?)` | `calculator` | `{ expression }` | `{ result }` |
41
- | `ai.tools.dateTime(options?)` | `date_time` | `{ op, iso?, from?, to?, amount?, unit?, timeZone?, format? }` | `{ value }` |
42
-
43
- ## `web_search` — pick a provider, supply a key
44
-
45
- ```ts
46
- ai.tools.webSearch({ provider: "tavily", apiKey: process.env.TAVILY_API_KEY, maxResults: 8 });
47
- ```
48
-
49
- - `provider`: `"tavily"` (LLM-ready snippets + a relevance `score`), `"brave"`, or `"serpapi"`. Each is called over the global `fetch` — **no provider SDK is required**.
50
- - `apiKey` falls back to a per-provider env var: `TAVILY_API_KEY` / `BRAVE_API_KEY` / `SERPAPI_API_KEY`. A missing key is a `WebToolError` of type `"missing-key"`.
51
- - The model's `maxResults` is clamped into `[1, options.maxResults]` (default `5`).
52
-
53
- ## `fetch_url` — read a page you have the URL for
54
-
55
- ```ts
56
- ai.tools.fetchUrl({ extract: "text", allowHosts: ["docs.stripe.com"], maxBytes: 500_000, timeoutMs: 10_000 });
57
- ```
58
-
59
- - `extract`: `"text"` (default — readability-extracted main text), `"html"` (raw body), or `"markdown"`. **`"text"`/`"markdown"` lazily import the `@mozilla/readability` + `jsdom` optional peers** — a missing peer is a `WebToolError` of type `"missing-peer"` carrying the `npm install @mozilla/readability jsdom` string. `"html"` needs nothing.
60
- - `allowHosts` (when set) rejects any other host **before the fetch** (an SSRF guardrail) → `"denied-host"`, and redirect targets are held to the same allowlist.
61
- - **Private-network deny by default.** Every request — and every redirect hop — now routes through `@warlock.js/ai`'s hardened `guardedFetch` outbound policy instead of a local host check. It refuses private / loopback / link-local / CGNAT / cloud-metadata addresses (`169.254.169.254`, RFC1918, `localhost`, …) by resolving the hostname via DNS and checking every returned address, failing closed on resolution failure — and re-validates every redirect `Location` (scheme, allowlist, private-IP deny) before following it. This applies even with no `allowHosts` set, so a bare `ai.tools.fetchUrl()` is no longer an SSRF primitive; blocks surface as `WebToolError` `type: "denied-host"`.
62
- - `allowPrivateNetwork` (default `false`) — set `true` to deliberately permit a private/internal target, e.g. a tool that must call a local dev server. Pair with `allowHosts` to scope it rather than opening the whole private range.
63
- - `maxBytes` caps the body and flags `truncated`; `timeoutMs` aborts via `AbortSignal.timeout`.
64
-
65
- ## `http_request` — a guarded REST client
66
-
67
- ```ts
68
- ai.tools.http({
69
- baseUrl: "https://api.stripe.com",
70
- allowHosts: ["api.stripe.com"],
71
- allowMethods: ["GET", "POST"], // default ["GET"] — mutating methods are opt-in
72
- headers: { authorization: `Bearer ${process.env.STRIPE_KEY}` },
73
- timeoutMs: 15_000,
74
- maxBytes: 1_000_000,
75
- // allowPrivateNetwork: true, // only for a deliberate internal/dev target
76
- });
77
- ```
78
-
79
- All guardrails are enforced **before the network call** and surface as a typed `HttpPolicyError`:
80
- - a method outside `allowMethods` → `"method-not-allowed"`;
81
- - a host outside `allowHosts` → `"host-not-allowed"` (SSRF guardrail);
82
- - an unresolvable URL → `"invalid-url"`.
83
- - **Private-network deny by default** — like `fetch_url`, every request and redirect hop routes through `guardedFetch`, refusing private/loopback/link-local/CGNAT/cloud-metadata addresses (DNS-resolved and re-checked per redirect hop) even when `allowHosts` isn't set → `"host-not-allowed"`. Set `allowPrivateNetwork: true` (default `false`) to permit a deliberate internal target.
84
-
85
- When `baseUrl` is set the model passes a **path** joined against it; otherwise it must pass an absolute `http(s)` URL. Static `headers` merge **under** the per-call headers (per-call wins). An object `body` is JSON-serialized (with a default `content-type: application/json`); a string `body` is sent verbatim; `body` is dropped for `GET`. The response body is JSON-parsed when the `content-type` is JSON, else returned as text.
86
-
87
- ## `calculator` — SAFE arithmetic (never `eval`)
88
-
89
- ```ts
90
- const { data } = await ai.tools.calculator().invoke({ expression: "(3 + 4) * 2 ^ 3" });
91
- data?.result; // 56
92
- ```
93
-
94
- Supports `+ - * / % ^`, unary signs, parentheses, and decimal/scientific literals, with the usual precedence (`^` highest and right-associative). The expression is tokenized and evaluated with a shunting-yard pass — there is no path to an identifier, function call, or property access, so nothing code-like can be smuggled in. Failures are a `CalculatorError`: `"syntax"`, `"divide-by-zero"`, or `"overflow"`.
95
-
96
- ## `date_time` — clock + calendar
97
-
98
- ```ts
99
- await ai.tools.dateTime().invoke({ op: "now", format: "iso" });
100
- await ai.tools.dateTime().invoke({ op: "add", iso: "2026-06-22T00:00:00Z", amount: 3, unit: "days" });
101
- await ai.tools.dateTime().invoke({ op: "diff", iso: "2026-06-22T00:00:00Z", to: "2026-06-25T00:00:00Z", unit: "days" }); // "3"
102
- await ai.tools.dateTime({ defaultTimeZone: "Africa/Cairo" }).invoke({ op: "format", iso: "2026-06-22T12:00:00Z", format: "datetime" });
103
- ```
104
-
105
- - `op`: `"now"` / `"add"` / `"diff"` / `"format"`. `format`: `"iso"` (default), `"date"`, `"time"`, `"datetime"`.
106
- - For `diff`, pass the start instant as `iso` **or** `from` (an alias models reach for naturally with `from`/`to`); `iso` wins when both are set. The end instant is always `to`.
107
- - Units are millisecond-based: `milliseconds`, `seconds`, `minutes`, `hours`, `days`, `weeks` (plus singular/short aliases like `day`/`d`). Calendar-relative `month`/`year` are intentionally **unsupported** — they have no fixed length. An unknown unit → `DateTimeError` `"invalid-unit"`; an unknown IANA zone → `"invalid-time-zone"`.
108
- - For deterministic tests, inject a clock by calling the factory directly: `import { dateTimeTool } from "@warlock.js/ai-tools"; dateTimeTool({}, { clock: () => Date.parse("2026-06-22T00:00:00Z") })`.
109
-
110
- ## Errors flow as data, not throws
111
-
112
- Every failure above is thrown inside the tool handler, caught by the framework's `tool()` wrapper, and surfaced in the returned `{ error }` field — `invoke()` never throws, so the agent reads the failure as a `role: "tool"` message and self-corrects on the next trip. Each error class extends the `@warlock.js/ai` `AIError` base; branch on `.type`:
113
-
114
- ```ts
115
- import { WebToolError } from "@warlock.js/ai-tools";
116
-
117
- const { error } = await ai.tools.fetchUrl({ allowHosts: ["docs.stripe.com"] }).invoke({ url: "http://evil.test" });
118
- if (error instanceof WebToolError && error.type === "denied-host") {
119
- // rejected before any network call
120
- }
121
- ```
122
-
123
- ## See also
124
-
125
- - [`@warlock.js/ai-tools/connect-mcp-server/SKILL.md`](@warlock.js/ai-tools/connect-mcp-server/SKILL.md) — adapt an external MCP server's tools as agent tools.
126
- - [`@warlock.js/ai-tools/expose-as-mcp-server/SKILL.md`](@warlock.js/ai-tools/expose-as-mcp-server/SKILL.md) — publish these tools AS an MCP server.
127
- - [`@warlock.js/ai-workspace/use-a-workspace/SKILL.md`](@warlock.js/ai-workspace/use-a-workspace/SKILL.md) — the filesystem + shell tool belt (jailed `read_file` / `edit_file` / `run_shell` / …).
128
- - [`@warlock.js/ai/define-ai-tool/SKILL.md`](@warlock.js/ai/define-ai-tool/SKILL.md) — the `tool()` / `ToolContract` seam every tool here is built on.
129
- - [`@warlock.js/ai/run-ai-agent/SKILL.md`](@warlock.js/ai/run-ai-agent/SKILL.md) — running the agent that consumes these tools.
1
+ ---
2
+ name: use-web-and-http-tools
3
+ description: 'Wire the @warlock.js/ai-tools belt — web search, fetch/scrape, HTTP/REST, calculator, date-time — into an agent under their guardrails. Triggers: `ai.tools.webSearch`, `ai.tools.fetchUrl`, `ai.tools.http`, `ai.tools.calculator`, `ai.tools.dateTime`, `webSearchTool`, `fetchUrlTool`, `httpRequestTool`, `calculatorTool`, `dateTimeTool`, `web_search`, `fetch_url`, `http_request`, `calculator`, `date_time`, `WebToolError`, `HttpPolicyError`, `CalculatorError`, `DateTimeError`, `allowHosts`, `allowMethods`, `allowPrivateNetwork`, `baseUrl`, `maxBytes`, `guardedFetch`, `SearchProvider`, `TAVILY_API_KEY`; ''give an agent web search'', ''let an agent fetch/scrape a page'', ''let an agent call a REST API'', ''restrict which hosts/methods an agent can hit'', ''add a calculator/date tool to an agent'', ''SSRF guardrail for an agent tool'', ''let a tool reach an internal/dev service''; typical import `import "@warlock.js/ai-tools"; import { ai } from "@warlock.js/ai"`. Skip: connecting to an external MCP server — `@warlock.js/ai-tools/connect-mcp-server/SKILL.md`; exposing tools AS an MCP server — `@warlock.js/ai-tools/expose-as-mcp-server/SKILL.md`; filesystem/shell tools — `@warlock.js/ai-workspace/use-a-workspace/SKILL.md`; defining your own custom tool — `@warlock.js/ai/define-ai-tool/SKILL.md`.'
4
+ ---
5
+
6
+ # Use the web + HTTP tool belt
7
+
8
+ `@warlock.js/ai-tools` ships five ready-made tools. Import the package once for its side effect and they register on the shared `ai` object under `ai.tools.*` — each is a factory returning a `ToolContract` you spread into `ai.agent({ tools: [...] })`.
9
+
10
+ ```ts
11
+ import "@warlock.js/ai-tools";
12
+ import { ai } from "@warlock.js/ai";
13
+ import { OpenAISDK } from "@warlock.js/ai-openai";
14
+
15
+ const openai = new OpenAISDK({ apiKey: process.env.OPENAI_API_KEY! });
16
+
17
+ const agent = ai.agent({
18
+ model: openai.model({ name: "gpt-4o" }),
19
+ systemPrompt: "Research, then call the API. Read tool errors and adjust.",
20
+ tools: [
21
+ ai.tools.webSearch({ provider: "tavily" }),
22
+ ai.tools.fetchUrl({ extract: "text", allowHosts: ["docs.stripe.com"] }),
23
+ ai.tools.http({ baseUrl: "https://api.stripe.com", allowHosts: ["api.stripe.com"] }),
24
+ ai.tools.calculator(),
25
+ ai.tools.dateTime({ defaultTimeZone: "UTC" }),
26
+ ],
27
+ maxTrips: 15,
28
+ });
29
+ ```
30
+
31
+ The construction `options` are the **rails** — they bound what the model may do; the model supplies the per-call inputs *within* those rails. Least-privilege is just array composition: hand an agent only the tools (and only the methods/hosts) it needs.
32
+
33
+ ## The five tools
34
+
35
+ | Factory | Name | Model input | Returns |
36
+ |---|---|---|---|
37
+ | `ai.tools.webSearch(options)` | `web_search` | `{ query, maxResults? }` | `{ results: { title, url, snippet, score? }[] }` |
38
+ | `ai.tools.fetchUrl(options?)` | `fetch_url` | `{ url }` | `{ url, status, content, truncated }` |
39
+ | `ai.tools.http(options?)` | `http_request` | `{ method?, url, headers?, body? }` | `{ status, headers, body, truncated }` |
40
+ | `ai.tools.calculator(options?)` | `calculator` | `{ expression }` | `{ result }` |
41
+ | `ai.tools.dateTime(options?)` | `date_time` | `{ op, iso?, from?, to?, amount?, unit?, timeZone?, format? }` | `{ value }` |
42
+
43
+ ## `web_search` — pick a provider, supply a key
44
+
45
+ ```ts
46
+ ai.tools.webSearch({ provider: "tavily", apiKey: process.env.TAVILY_API_KEY, maxResults: 8 });
47
+ ```
48
+
49
+ - `provider`: `"tavily"` (LLM-ready snippets + a relevance `score`), `"brave"`, or `"serpapi"`. Each is called over the global `fetch` — **no provider SDK is required**.
50
+ - `apiKey` falls back to a per-provider env var: `TAVILY_API_KEY` / `BRAVE_API_KEY` / `SERPAPI_API_KEY`. A missing key is a `WebToolError` of type `"missing-key"`.
51
+ - The model's `maxResults` is clamped into `[1, options.maxResults]` (default `5`).
52
+
53
+ ## `fetch_url` — read a page you have the URL for
54
+
55
+ ```ts
56
+ ai.tools.fetchUrl({ extract: "text", allowHosts: ["docs.stripe.com"], maxBytes: 500_000, timeoutMs: 10_000 });
57
+ ```
58
+
59
+ - `extract`: `"text"` (default — readability-extracted main text), `"html"` (raw body), or `"markdown"`. **`"text"`/`"markdown"` lazily import the `@mozilla/readability` + `jsdom` optional peers** — a missing peer is a `WebToolError` of type `"missing-peer"` carrying the `npm install @mozilla/readability jsdom` string. `"html"` needs nothing.
60
+ - `allowHosts` (when set) rejects any other host **before the fetch** (an SSRF guardrail) → `"denied-host"`, and redirect targets are held to the same allowlist.
61
+ - **Private-network deny by default.** Every request — and every redirect hop — now routes through `@warlock.js/ai`'s hardened `guardedFetch` outbound policy instead of a local host check. It refuses private / loopback / link-local / CGNAT / cloud-metadata addresses (`169.254.169.254`, RFC1918, `localhost`, …) by resolving the hostname via DNS and checking every returned address, failing closed on resolution failure — and re-validates every redirect `Location` (scheme, allowlist, private-IP deny) before following it. This applies even with no `allowHosts` set, so a bare `ai.tools.fetchUrl()` is no longer an SSRF primitive; blocks surface as `WebToolError` `type: "denied-host"`.
62
+ - `allowPrivateNetwork` (default `false`) — set `true` to deliberately permit a private/internal target, e.g. a tool that must call a local dev server. Pair with `allowHosts` to scope it rather than opening the whole private range.
63
+ - `maxBytes` caps the body and flags `truncated`; `timeoutMs` aborts via `AbortSignal.timeout`.
64
+
65
+ ## `http_request` — a guarded REST client
66
+
67
+ ```ts
68
+ ai.tools.http({
69
+ baseUrl: "https://api.stripe.com",
70
+ allowHosts: ["api.stripe.com"],
71
+ allowMethods: ["GET", "POST"], // default ["GET"] — mutating methods are opt-in
72
+ headers: { authorization: `Bearer ${process.env.STRIPE_KEY}` },
73
+ timeoutMs: 15_000,
74
+ maxBytes: 1_000_000,
75
+ // allowPrivateNetwork: true, // only for a deliberate internal/dev target
76
+ });
77
+ ```
78
+
79
+ All guardrails are enforced **before the network call** and surface as a typed `HttpPolicyError`:
80
+ - a method outside `allowMethods` → `"method-not-allowed"`;
81
+ - a host outside `allowHosts` → `"host-not-allowed"` (SSRF guardrail);
82
+ - an unresolvable URL → `"invalid-url"`.
83
+ - **Private-network deny by default** — like `fetch_url`, every request and redirect hop routes through `guardedFetch`, refusing private/loopback/link-local/CGNAT/cloud-metadata addresses (DNS-resolved and re-checked per redirect hop) even when `allowHosts` isn't set → `"host-not-allowed"`. Set `allowPrivateNetwork: true` (default `false`) to permit a deliberate internal target.
84
+
85
+ When `baseUrl` is set the model passes a **path** joined against it; otherwise it must pass an absolute `http(s)` URL. Static `headers` merge **under** the per-call headers (per-call wins). An object `body` is JSON-serialized (with a default `content-type: application/json`); a string `body` is sent verbatim; `body` is dropped for `GET`. The response body is JSON-parsed when the `content-type` is JSON, else returned as text.
86
+
87
+ ## `calculator` — SAFE arithmetic (never `eval`)
88
+
89
+ ```ts
90
+ const { data } = await ai.tools.calculator().invoke({ expression: "(3 + 4) * 2 ^ 3" });
91
+ data?.result; // 56
92
+ ```
93
+
94
+ Supports `+ - * / % ^`, unary signs, parentheses, and decimal/scientific literals, with the usual precedence (`^` highest and right-associative). The expression is tokenized and evaluated with a shunting-yard pass — there is no path to an identifier, function call, or property access, so nothing code-like can be smuggled in. Failures are a `CalculatorError`: `"syntax"`, `"divide-by-zero"`, or `"overflow"`.
95
+
96
+ ## `date_time` — clock + calendar
97
+
98
+ ```ts
99
+ await ai.tools.dateTime().invoke({ op: "now", format: "iso" });
100
+ await ai.tools.dateTime().invoke({ op: "add", iso: "2026-06-22T00:00:00Z", amount: 3, unit: "days" });
101
+ await ai.tools.dateTime().invoke({ op: "diff", iso: "2026-06-22T00:00:00Z", to: "2026-06-25T00:00:00Z", unit: "days" }); // "3"
102
+ await ai.tools.dateTime({ defaultTimeZone: "Africa/Cairo" }).invoke({ op: "format", iso: "2026-06-22T12:00:00Z", format: "datetime" });
103
+ ```
104
+
105
+ - `op`: `"now"` / `"add"` / `"diff"` / `"format"`. `format`: `"iso"` (default), `"date"`, `"time"`, `"datetime"`.
106
+ - For `diff`, pass the start instant as `iso` **or** `from` (an alias models reach for naturally with `from`/`to`); `iso` wins when both are set. The end instant is always `to`.
107
+ - Units are millisecond-based: `milliseconds`, `seconds`, `minutes`, `hours`, `days`, `weeks` (plus singular/short aliases like `day`/`d`). Calendar-relative `month`/`year` are intentionally **unsupported** — they have no fixed length. An unknown unit → `DateTimeError` `"invalid-unit"`; an unknown IANA zone → `"invalid-time-zone"`.
108
+ - For deterministic tests, inject a clock by calling the factory directly: `import { dateTimeTool } from "@warlock.js/ai-tools"; dateTimeTool({}, { clock: () => Date.parse("2026-06-22T00:00:00Z") })`.
109
+
110
+ ## Errors flow as data, not throws
111
+
112
+ Every failure above is thrown inside the tool handler, caught by the framework's `tool()` wrapper, and surfaced in the returned `{ error }` field — `invoke()` never throws, so the agent reads the failure as a `role: "tool"` message and self-corrects on the next trip. Each error class extends the `@warlock.js/ai` `AIError` base; branch on `.type`:
113
+
114
+ ```ts
115
+ import { WebToolError } from "@warlock.js/ai-tools";
116
+
117
+ const { error } = await ai.tools.fetchUrl({ allowHosts: ["docs.stripe.com"] }).invoke({ url: "http://evil.test" });
118
+ if (error instanceof WebToolError && error.type === "denied-host") {
119
+ // rejected before any network call
120
+ }
121
+ ```
122
+
123
+ ## See also
124
+
125
+ - [`@warlock.js/ai-tools/connect-mcp-server/SKILL.md`](@warlock.js/ai-tools/connect-mcp-server/SKILL.md) — adapt an external MCP server's tools as agent tools.
126
+ - [`@warlock.js/ai-tools/expose-as-mcp-server/SKILL.md`](@warlock.js/ai-tools/expose-as-mcp-server/SKILL.md) — publish these tools AS an MCP server.
127
+ - [`@warlock.js/ai-workspace/use-a-workspace/SKILL.md`](@warlock.js/ai-workspace/use-a-workspace/SKILL.md) — the filesystem + shell tool belt (jailed `read_file` / `edit_file` / `run_shell` / …).
128
+ - [`@warlock.js/ai/define-ai-tool/SKILL.md`](@warlock.js/ai/define-ai-tool/SKILL.md) — the `tool()` / `ToolContract` seam every tool here is built on.
129
+ - [`@warlock.js/ai/run-ai-agent/SKILL.md`](@warlock.js/ai/run-ai-agent/SKILL.md) — running the agent that consumes these tools.