@nimble-way/ai-sdk 0.1.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,14 +1,15 @@
1
1
  # @nimble-way/ai-sdk
2
2
 
3
- Nimble Web Search as a ready-made [Vercel AI SDK](https://ai-sdk.dev) tool. Give any AI SDK agent the ability to search the web with [Nimble](https://nimbleway.com) in a few lines.
3
+ Nimble Web Search and Extract as ready-made [Vercel AI SDK](https://ai-sdk.dev) tools. Give any AI SDK agent the ability to search the web and read pages with [Nimble](https://nimbleway.com) in a few lines.
4
4
 
5
5
  ## Features
6
6
 
7
7
  - **Web Search** — a `nimbleSearch()` tool the model can call to retrieve ranked, real-time web results and ground its answers in them.
8
- - **Model- and gateway-agnostic** — an app-side `tool()`; works the same with the Vercel AI Gateway or a direct provider.
8
+ - **Extract** — a `nimbleExtract()` tool that fetches a URL and returns clean markdown (or HTML) for the model to read, quote, or summarize.
9
+ - **Model- and gateway-agnostic** — app-side `tool()`s; work the same with the Vercel AI Gateway or a direct provider.
9
10
  - **Typed** — typed config and normalized output; an injectable client for testing.
10
11
 
11
- > Extract, Map, and Crawl tools are planned follow-ups.
12
+ > Map and Crawl tools are planned follow-ups.
12
13
 
13
14
  ## Install
14
15
 
@@ -77,6 +78,32 @@ export async function POST(req: Request) {
77
78
 
78
79
  The tool runs **in your app** (an app-side `tool()`, not a provider/server-executed search). It is therefore **gateway-agnostic**: it behaves identically whether you route your model through the [Vercel AI Gateway](https://vercel.com/docs/ai-gateway) (plain-string model IDs like `'openai/gpt-4o-mini'`) or call a provider SDK directly. The gateway, if present, only routes the *model* call.
79
80
 
81
+ ## Extract
82
+
83
+ Give the model a URL and get back clean content to read, quote, or summarize:
84
+
85
+ ```ts
86
+ import { generateText, stepCountIs } from 'ai';
87
+ import { nimbleExtract } from '@nimble-way/ai-sdk';
88
+
89
+ const { text } = await generateText({
90
+ model: 'openai/gpt-4o-mini',
91
+ prompt: 'Summarize https://en.wikipedia.org/wiki/Web_scraping',
92
+ tools: { extract: nimbleExtract({ format: 'markdown' }) },
93
+ // Allow a step after the tool call so the model can summarize the page.
94
+ stopWhen: stepCountIs(2),
95
+ });
96
+ ```
97
+
98
+ Register both tools together so the model can search, then read the best result:
99
+
100
+ ```ts
101
+ tools: {
102
+ webSearch: nimbleSearch(),
103
+ extract: nimbleExtract(),
104
+ }
105
+ ```
106
+
80
107
  ## Options
81
108
 
82
109
  `nimbleSearch(config)` — all fields optional:
@@ -94,8 +121,22 @@ The tool runs **in your app** (an app-side `tool()`, not a provider/server-execu
94
121
 
95
122
  The **model-facing input** is just `{ query: string, maxResults?: number }` — all policy above is developer-controlled, not model-controlled.
96
123
 
124
+ `nimbleExtract(config)` — all fields optional:
125
+
126
+ | Option | Type | Default | Notes |
127
+ |---|---|---|---|
128
+ | `apiKey` | `string` | `process.env.NIMBLE_API_KEY` | Nimble API key. |
129
+ | `client` | `NimbleExtractClient` | — | Inject a pre-built/mock client. |
130
+ | `format` | `'markdown' \| 'html'` | `'markdown'` | Content format returned to the model. |
131
+ | `country` | `string` | — | ISO country for geolocation / proxy. |
132
+ | `maxContentLength` | `number` | `50_000` | Truncate the extracted content. |
133
+
134
+ The **model-facing input** is just `{ url: string }`.
135
+
97
136
  ## Output shape
98
137
 
138
+ `nimbleSearch`:
139
+
99
140
  ```ts
100
141
  {
101
142
  query: string;
@@ -112,9 +153,22 @@ The **model-facing input** is just `{ query: string, maxResults?: number }` —
112
153
  }
113
154
  ```
114
155
 
156
+ `nimbleExtract`:
157
+
158
+ ```ts
159
+ {
160
+ url: string;
161
+ status: string; // e.g. 'success'
162
+ statusCode?: number;
163
+ format: 'markdown' | 'html';
164
+ content: string; // truncated to maxContentLength
165
+ links?: string[];
166
+ }
167
+ ```
168
+
115
169
  ## Limitations
116
170
 
117
- - **Search only.** Extract / Map / Crawl / Agents are follow-ups.
171
+ - **Search + Extract.** Map / Crawl / Agents are follow-ups.
118
172
  - **No answer generation.** `include_answer` is intentionally not exposed.
119
173
  - **`searchDepth: 'fast'` is not available** (enterprise-gated).
120
174
  - **Runtime:** targets the **Node.js runtime** (Node ≥ 18). Edge/serverless is expected to work but not yet verified — prefer the Node runtime.
@@ -124,6 +178,7 @@ The **model-facing input** is just `{ query: string, maxResults?: number }` —
124
178
  | Symptom | Fix |
125
179
  |---|---|
126
180
  | `NimbleConfigError: missing API key` | Set `NIMBLE_API_KEY` or pass `apiKey`. |
181
+ | `NimbleExtractError` with a status | The Nimble Extract API returned an error; the HTTP status is on `err.status`. |
127
182
  | `NimbleSearchError` with a status | The Nimble API returned an error; the HTTP status is on `err.status`. |
128
183
  | Tool never called | Ensure your prompt invites tool use and `stopWhen` allows multiple steps. |
129
184
 
package/dist/index.cjs CHANGED
@@ -9,6 +9,9 @@ var nimbleSearchInputSchema = zod.z.object({
9
9
  query: zod.z.string().min(1).describe("The web search query."),
10
10
  maxResults: zod.z.number().int().positive().optional().describe("How many results to return (clamped to the developer-configured cap).")
11
11
  });
12
+ var nimbleExtractInputSchema = zod.z.object({
13
+ url: zod.z.string().url().describe("The URL of the web page to extract clean content from.")
14
+ });
12
15
 
13
16
  // src/normalize.ts
14
17
  function isSerpMetadata(metadata) {
@@ -44,6 +47,23 @@ function normalizeSearchResponse(response, options) {
44
47
  results
45
48
  };
46
49
  }
50
+ function normalizeExtractResponse(response, options) {
51
+ const data = response.data;
52
+ const otherFormat = options.format === "html" ? "markdown" : "html";
53
+ const primary = options.format === "html" ? data.html : data.markdown;
54
+ const fallback = options.format === "html" ? data.markdown : data.html;
55
+ const usedFormat = primary && primary.length > 0 ? options.format : fallback && fallback.length > 0 ? otherFormat : options.format;
56
+ const content = truncate(primary || fallback || "", options.maxContentLength);
57
+ const out = {
58
+ url: response.url,
59
+ status: response.status,
60
+ format: usedFormat,
61
+ content
62
+ };
63
+ if (typeof response.status_code === "number") out.statusCode = response.status_code;
64
+ if (data.links && data.links.length > 0) out.links = data.links;
65
+ return out;
66
+ }
47
67
 
48
68
  // src/errors.ts
49
69
  var NimbleConfigError = class extends Error {
@@ -60,6 +80,14 @@ var NimbleSearchError = class extends Error {
60
80
  this.status = options?.status;
61
81
  }
62
82
  };
83
+ var NimbleExtractError = class extends Error {
84
+ status;
85
+ constructor(message, options) {
86
+ super(message, options?.cause !== void 0 ? { cause: options.cause } : void 0);
87
+ this.name = "NimbleExtractError";
88
+ this.status = options?.status;
89
+ }
90
+ };
63
91
 
64
92
  // src/nimble-search.ts
65
93
  var NIMBLE_SEARCH_DEFAULTS = {
@@ -133,12 +161,71 @@ function nimbleSearch(config = {}) {
133
161
  }
134
162
  });
135
163
  }
164
+ var NIMBLE_EXTRACT_DEFAULTS = {
165
+ format: "markdown",
166
+ maxContentLength: 5e4
167
+ };
168
+ function readStatus2(err) {
169
+ if (typeof err === "object" && err !== null && "status" in err) {
170
+ const status = err.status;
171
+ return typeof status === "number" ? status : void 0;
172
+ }
173
+ return void 0;
174
+ }
175
+ function toExtractError(err) {
176
+ if (err instanceof NimbleExtractError) return err;
177
+ const message = err instanceof Error ? err.message : String(err);
178
+ return new NimbleExtractError(`Nimble extract failed: ${message}`, {
179
+ status: readStatus2(err),
180
+ cause: err
181
+ });
182
+ }
183
+ function resolveClient2(config) {
184
+ if (config.client) return config.client;
185
+ const apiKey = config.apiKey ?? process.env.NIMBLE_API_KEY;
186
+ if (!apiKey) {
187
+ throw new NimbleConfigError(
188
+ "Missing Nimble API key: set NIMBLE_API_KEY or pass { apiKey } to nimbleExtract()."
189
+ );
190
+ }
191
+ return new nimbleJs.Nimble({ apiKey });
192
+ }
193
+ function nimbleExtract(config = {}) {
194
+ const format = config.format ?? NIMBLE_EXTRACT_DEFAULTS.format;
195
+ const maxContentLength = config.maxContentLength ?? NIMBLE_EXTRACT_DEFAULTS.maxContentLength;
196
+ const country = config.country;
197
+ return ai.tool({
198
+ description: "Fetch a web page by URL with Nimble and return its clean, readable content (markdown or HTML) \u2014 use this to read, quote, or summarize a specific page.",
199
+ inputSchema: nimbleExtractInputSchema,
200
+ execute: async (input) => {
201
+ const client = resolveClient2(config);
202
+ const params = {
203
+ url: input.url,
204
+ formats: format === "html" ? ["html", "markdown", "links"] : ["markdown", "html", "links"]
205
+ };
206
+ if (country) params.country = country;
207
+ if (format === "markdown") params.markdown_backend = "main_content";
208
+ let raw;
209
+ try {
210
+ raw = await client.extract(params);
211
+ } catch (err) {
212
+ throw toExtractError(err);
213
+ }
214
+ return normalizeExtractResponse(raw, { format, maxContentLength });
215
+ }
216
+ });
217
+ }
136
218
 
219
+ exports.NIMBLE_EXTRACT_DEFAULTS = NIMBLE_EXTRACT_DEFAULTS;
137
220
  exports.NIMBLE_SEARCH_DEFAULTS = NIMBLE_SEARCH_DEFAULTS;
138
221
  exports.NimbleConfigError = NimbleConfigError;
222
+ exports.NimbleExtractError = NimbleExtractError;
139
223
  exports.NimbleSearchError = NimbleSearchError;
224
+ exports.nimbleExtract = nimbleExtract;
225
+ exports.nimbleExtractInputSchema = nimbleExtractInputSchema;
140
226
  exports.nimbleSearch = nimbleSearch;
141
227
  exports.nimbleSearchInputSchema = nimbleSearchInputSchema;
228
+ exports.normalizeExtractResponse = normalizeExtractResponse;
142
229
  exports.normalizeSearchResponse = normalizeSearchResponse;
143
230
  //# sourceMappingURL=index.cjs.map
144
231
  //# sourceMappingURL=index.cjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/schemas.ts","../src/normalize.ts","../src/errors.ts","../src/nimble-search.ts"],"names":["z","Nimble","tool"],"mappings":"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","file":"index.cjs","sourcesContent":["import { z } from 'zod';\n\n/**\n * The tool input the model fills in. Kept deliberately small: the model only\n * chooses the query and (optionally) how many results it wants. All policy\n * (depth, focus, region, caps) is fixed by the developer via the factory\n * config, not by the model.\n */\nexport const nimbleSearchInputSchema = z.object({\n query: z.string().min(1).describe('The web search query.'),\n maxResults: z\n .number()\n .int()\n .positive()\n .optional()\n .describe('How many results to return (clamped to the developer-configured cap).'),\n});\n\nexport type NimbleSearchInput = z.infer<typeof nimbleSearchInputSchema>;\n\n/**\n * v1 exposes only the two non-enterprise depths. `fast` is enterprise-gated and\n * intentionally not offered here.\n */\nexport type SearchDepth = 'lite' | 'deep';\n\n/**\n * Developer-facing factory config. `focus` is fixed to `general` in v1 and is\n * not exposed. `include_answer` and `search_depth: 'fast'` are intentionally\n * absent (enterprise / unverified-entitlement surface).\n */\nexport interface NimbleSearchToolConfig {\n /** Nimble API key. Defaults to `process.env.NIMBLE_API_KEY`. */\n apiKey?: string;\n /** Inject a pre-built / mock Nimble client (tests, advanced users). */\n client?: NimbleSearchClient;\n /** Default number of results when the model doesn't specify. Default 5. */\n maxResults?: number;\n /** Hard upper bound on results, regardless of model request. Default 10. */\n maxResultsCap?: number;\n /** Search depth. Default `lite`. */\n searchDepth?: SearchDepth;\n /** ISO country for result localization. Default `US`. */\n country?: string;\n /** Locale for result localization. Default `en`. */\n locale?: string;\n /** Truncate each result's body to this many characters. Default 10_000. */\n maxContentLength?: number;\n}\n\n/**\n * Structural surface of `@nimble-way/nimble-js`'s `client.search()` that this\n * package relies on. Declared structurally so the scaffold typechecks without\n * pinning to the SDK's generated type names, and so tests can inject a mock.\n *\n * Phase B: reconcile field casing/names against\n * `sdks/nimble-js/checkout/src/` after `./tools/sync-sdk.sh nimble-js`.\n */\nexport interface NimbleSearchParams {\n query: string;\n max_results?: number;\n search_depth?: SearchDepth;\n focus?: string;\n country?: string;\n locale?: string;\n}\n\n/** Metadata for SERP-based results (general/news/location focus). */\nexport interface NimbleSerpMetadata {\n country: string;\n entity_type: string;\n locale: string;\n position: number;\n driver?: string | null;\n}\n\n/** Metadata for WSA-based results (shopping/social/geo focus). */\nexport interface NimbleWsaMetadata {\n agent_name: string;\n}\n\nexport interface NimbleRawSearchResult {\n /** Full page text in `deep`; may be empty in `lite`. */\n content: string;\n description: string;\n title: string;\n url: string;\n /** SERP focus (v1 `general`) yields {@link NimbleSerpMetadata}. */\n metadata: NimbleSerpMetadata | NimbleWsaMetadata;\n /** Platform-specific extras (price, publish_date, …); omitted when none. */\n additional_data?: Record<string, unknown> | null;\n}\n\nexport interface NimbleRawSearchResponse {\n request_id: string;\n results: NimbleRawSearchResult[];\n total_results: number;\n /** Intentionally never surfaced in v1 (include_answer is off). */\n answer?: string | null;\n}\n\nexport interface NimbleSearchClient {\n search(params: NimbleSearchParams): Promise<NimbleRawSearchResponse>;\n}\n\n/** A single normalized result item returned to the model. */\nexport interface NimbleSearchResultItem {\n title: string;\n url: string;\n description?: string;\n content?: string;\n position?: number;\n entityType?: string;\n}\n\n/** The normalized tool output. `answer` is intentionally omitted in v1. */\nexport interface NimbleSearchOutput {\n query: string;\n requestId?: string;\n totalResults?: number;\n results: NimbleSearchResultItem[];\n}\n","import type {\n NimbleRawSearchResponse,\n NimbleRawSearchResult,\n NimbleSearchOutput,\n NimbleSearchResultItem,\n NimbleSerpMetadata,\n} from './schemas';\n\nexport interface NormalizeOptions {\n query: string;\n maxContentLength: number;\n}\n\n/** SERP focus (v1 `general`) carries position + entity_type; WSA does not. */\nfunction isSerpMetadata(\n metadata: NimbleRawSearchResult['metadata'],\n): metadata is NimbleSerpMetadata {\n return 'position' in metadata;\n}\n\nfunction truncate(text: string, max: number): string {\n return text.length > max ? text.slice(0, max) : text;\n}\n\n/**\n * Map a raw `/v1/search` response into the package's normalized output shape.\n *\n * - `description` is the snippet (always present when the API returns one).\n * - `content` is the full page text, present only in `deep` depth (empty in\n * `lite`), truncated to `maxContentLength`.\n * - `position` / `entityType` come from SERP metadata; for WSA results\n * `position` falls back to the array index and `entityType` is omitted.\n * - Results without a URL are dropped (defensive).\n * - `response.answer` is never surfaced in v1.\n */\nexport function normalizeSearchResponse(\n response: NimbleRawSearchResponse,\n options: NormalizeOptions,\n): NimbleSearchOutput {\n const results: NimbleSearchResultItem[] = [];\n\n response.results.forEach((raw, index) => {\n if (!raw.url) return;\n\n const item: NimbleSearchResultItem = {\n title: raw.title ?? '',\n url: raw.url,\n };\n if (raw.description) item.description = raw.description;\n if (raw.content && raw.content.trim().length > 0) {\n item.content = truncate(raw.content, options.maxContentLength);\n }\n\n if (isSerpMetadata(raw.metadata)) {\n item.position = raw.metadata.position;\n item.entityType = raw.metadata.entity_type;\n } else {\n item.position = index + 1;\n }\n\n results.push(item);\n });\n\n return {\n query: options.query,\n requestId: response.request_id,\n totalResults: response.total_results,\n results,\n };\n}\n","/**\n * Thrown when the tool is invoked without a resolvable API key (no `apiKey`\n * config and no `NIMBLE_API_KEY` in the environment) and no injected client.\n * Raised at execute time, not at factory-construction time, so the tool can be\n * constructed in environments without a key (e.g. unit tests, type-checking).\n */\nexport class NimbleConfigError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'NimbleConfigError';\n }\n}\n\n/**\n * Wraps an error surfaced by the Nimble client / API during a search call,\n * preserving the HTTP status when available. The AI SDK surfaces a thrown\n * tool error back to the model as a tool-call failure.\n */\nexport class NimbleSearchError extends Error {\n readonly status?: number;\n\n constructor(message: string, options?: { status?: number; cause?: unknown }) {\n super(message, options?.cause !== undefined ? { cause: options.cause } : undefined);\n this.name = 'NimbleSearchError';\n this.status = options?.status;\n }\n}\n","import { tool } from 'ai';\nimport { Nimble } from '@nimble-way/nimble-js';\nimport { nimbleSearchInputSchema } from './schemas';\nimport type {\n NimbleSearchClient,\n NimbleSearchOutput,\n NimbleSearchParams,\n NimbleSearchToolConfig,\n} from './schemas';\nimport { normalizeSearchResponse } from './normalize';\nimport { NimbleConfigError, NimbleSearchError } from './errors';\n\n/** v1 factory defaults. `focus` is fixed to `general` and not user-exposed. */\nexport const NIMBLE_SEARCH_DEFAULTS = {\n maxResults: 5,\n maxResultsCap: 10,\n searchDepth: 'lite',\n country: 'US',\n locale: 'en',\n maxContentLength: 10_000,\n focus: 'general',\n} as const;\n\nfunction clamp(value: number, min: number, max: number): number {\n return Math.min(Math.max(value, min), max);\n}\n\nfunction readStatus(err: unknown): number | undefined {\n if (typeof err === 'object' && err !== null && 'status' in err) {\n const status = (err as { status?: unknown }).status;\n return typeof status === 'number' ? status : undefined;\n }\n return undefined;\n}\n\nfunction toSearchError(err: unknown): NimbleSearchError {\n if (err instanceof NimbleSearchError) return err;\n const message = err instanceof Error ? err.message : String(err);\n return new NimbleSearchError(`Nimble search failed: ${message}`, {\n status: readStatus(err),\n cause: err,\n });\n}\n\nfunction resolveClient(config: NimbleSearchToolConfig): NimbleSearchClient {\n if (config.client) return config.client;\n const apiKey = config.apiKey ?? process.env.NIMBLE_API_KEY;\n if (!apiKey) {\n throw new NimbleConfigError(\n 'Missing Nimble API key: set NIMBLE_API_KEY or pass { apiKey } to nimbleSearch().',\n );\n }\n return new Nimble({ apiKey }) as unknown as NimbleSearchClient;\n}\n\n/**\n * Create a ready-made Vercel AI SDK web-search tool backed by Nimble Search\n * (`@nimble-way/nimble-js` → `POST /v1/search`).\n *\n * The model only chooses `{ query, maxResults? }`; all policy (depth, focus,\n * region, caps) is fixed by `config` — mirroring the Exa `webSearch()` shape.\n *\n * @example\n * ```ts\n * import { generateText } from 'ai';\n * import { nimbleSearch } from '@nimble-way/ai-sdk';\n *\n * const { text } = await generateText({\n * model: 'anthropic/claude-sonnet-4.6',\n * prompt: 'What are the latest Nimble release notes?',\n * tools: { webSearch: nimbleSearch({ searchDepth: 'lite', maxResults: 5 }) },\n * });\n * ```\n */\nexport function nimbleSearch(config: NimbleSearchToolConfig = {}) {\n const maxResults = config.maxResults ?? NIMBLE_SEARCH_DEFAULTS.maxResults;\n const maxResultsCap = config.maxResultsCap ?? NIMBLE_SEARCH_DEFAULTS.maxResultsCap;\n const searchDepth = config.searchDepth ?? NIMBLE_SEARCH_DEFAULTS.searchDepth;\n const country = config.country ?? NIMBLE_SEARCH_DEFAULTS.country;\n const locale = config.locale ?? NIMBLE_SEARCH_DEFAULTS.locale;\n const maxContentLength = config.maxContentLength ?? NIMBLE_SEARCH_DEFAULTS.maxContentLength;\n\n return tool({\n description:\n 'Search the web with Nimble and return ranked results (title, url, ' +\n 'snippet, and page content) for answering questions about current or ' +\n 'factual information.',\n inputSchema: nimbleSearchInputSchema,\n execute: async (input): Promise<NimbleSearchOutput> => {\n const client = resolveClient(config);\n\n const params: NimbleSearchParams = {\n query: input.query,\n max_results: clamp(input.maxResults ?? maxResults, 1, maxResultsCap),\n search_depth: searchDepth,\n focus: NIMBLE_SEARCH_DEFAULTS.focus, // fixed 'general' in v1\n country,\n locale,\n };\n\n let raw;\n try {\n raw = await client.search(params);\n } catch (err) {\n throw toSearchError(err);\n }\n\n return normalizeSearchResponse(raw, {\n query: input.query,\n maxContentLength,\n });\n },\n });\n}\n"]}
1
+ {"version":3,"sources":["../src/schemas.ts","../src/normalize.ts","../src/errors.ts","../src/nimble-search.ts","../src/nimble-extract.ts"],"names":["z","Nimble","tool","readStatus","resolveClient"],"mappings":"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","file":"index.cjs","sourcesContent":["import { z } from 'zod';\n\n/**\n * The tool input the model fills in. Kept deliberately small: the model only\n * chooses the query and (optionally) how many results it wants. All policy\n * (depth, focus, region, caps) is fixed by the developer via the factory\n * config, not by the model.\n */\nexport const nimbleSearchInputSchema = z.object({\n query: z.string().min(1).describe('The web search query.'),\n maxResults: z\n .number()\n .int()\n .positive()\n .optional()\n .describe('How many results to return (clamped to the developer-configured cap).'),\n});\n\nexport type NimbleSearchInput = z.infer<typeof nimbleSearchInputSchema>;\n\n/**\n * v1 exposes only the two non-enterprise depths. `fast` is enterprise-gated and\n * intentionally not offered here.\n */\nexport type SearchDepth = 'lite' | 'deep';\n\n/**\n * Developer-facing factory config. `focus` is fixed to `general` in v1 and is\n * not exposed. `include_answer` and `search_depth: 'fast'` are intentionally\n * absent (enterprise / unverified-entitlement surface).\n */\nexport interface NimbleSearchToolConfig {\n /** Nimble API key. Defaults to `process.env.NIMBLE_API_KEY`. */\n apiKey?: string;\n /** Inject a pre-built / mock Nimble client (tests, advanced users). */\n client?: NimbleSearchClient;\n /** Default number of results when the model doesn't specify. Default 5. */\n maxResults?: number;\n /** Hard upper bound on results, regardless of model request. Default 10. */\n maxResultsCap?: number;\n /** Search depth. Default `lite`. */\n searchDepth?: SearchDepth;\n /** ISO country for result localization. Default `US`. */\n country?: string;\n /** Locale for result localization. Default `en`. */\n locale?: string;\n /** Truncate each result's body to this many characters. Default 10_000. */\n maxContentLength?: number;\n}\n\n/**\n * Structural surface of `@nimble-way/nimble-js`'s `client.search()` that this\n * package relies on. Declared structurally so the scaffold typechecks without\n * pinning to the SDK's generated type names, and so tests can inject a mock.\n *\n * Phase B: reconcile field casing/names against\n * `sdks/nimble-js/checkout/src/` after `./tools/sync-sdk.sh nimble-js`.\n */\nexport interface NimbleSearchParams {\n query: string;\n max_results?: number;\n search_depth?: SearchDepth;\n focus?: string;\n country?: string;\n locale?: string;\n}\n\n/** Metadata for SERP-based results (general/news/location focus). */\nexport interface NimbleSerpMetadata {\n country: string;\n entity_type: string;\n locale: string;\n position: number;\n driver?: string | null;\n}\n\n/** Metadata for WSA-based results (shopping/social/geo focus). */\nexport interface NimbleWsaMetadata {\n agent_name: string;\n}\n\nexport interface NimbleRawSearchResult {\n /** Full page text in `deep`; may be empty in `lite`. */\n content: string;\n description: string;\n title: string;\n url: string;\n /** SERP focus (v1 `general`) yields {@link NimbleSerpMetadata}. */\n metadata: NimbleSerpMetadata | NimbleWsaMetadata;\n /** Platform-specific extras (price, publish_date, …); omitted when none. */\n additional_data?: Record<string, unknown> | null;\n}\n\nexport interface NimbleRawSearchResponse {\n request_id: string;\n results: NimbleRawSearchResult[];\n total_results: number;\n /** Intentionally never surfaced in v1 (include_answer is off). */\n answer?: string | null;\n}\n\nexport interface NimbleSearchClient {\n search(params: NimbleSearchParams): Promise<NimbleRawSearchResponse>;\n}\n\n/** A single normalized result item returned to the model. */\nexport interface NimbleSearchResultItem {\n title: string;\n url: string;\n description?: string;\n content?: string;\n position?: number;\n entityType?: string;\n}\n\n/** The normalized tool output. `answer` is intentionally omitted in v1. */\nexport interface NimbleSearchOutput {\n query: string;\n requestId?: string;\n totalResults?: number;\n results: NimbleSearchResultItem[];\n}\n\n// ── Extract ────────────────────────────────────────────────────────────────\n\n/** Output format for extracted page content. */\nexport type ExtractFormat = 'markdown' | 'html';\n\n/**\n * The extract tool input the model fills in: just the URL to read. All policy\n * (format, region, length cap) is fixed by the developer via the factory config.\n */\nexport const nimbleExtractInputSchema = z.object({\n url: z.string().url().describe('The URL of the web page to extract clean content from.'),\n});\n\nexport type NimbleExtractInput = z.infer<typeof nimbleExtractInputSchema>;\n\n/** Developer-facing factory config for the extract tool. */\nexport interface NimbleExtractToolConfig {\n /** Nimble API key. Defaults to `process.env.NIMBLE_API_KEY`. */\n apiKey?: string;\n /** Inject a pre-built / mock Nimble client (tests, advanced users). */\n client?: NimbleExtractClient;\n /** Content format. Default `markdown`. */\n format?: ExtractFormat;\n /** ISO country for geolocation / proxy selection. */\n country?: string;\n /** Truncate the extracted content to this many characters. Default 50_000. */\n maxContentLength?: number;\n}\n\n/** Params this package sends to the SDK's `client.extract()`. */\nexport interface NimbleExtractParams {\n url: string;\n country?: string;\n /** Which renderings to request; `data.<format>` is populated per entry. */\n formats?: Array<'html' | 'markdown' | 'links'>;\n /** Refines Markdown extraction; `main_content` yields the cleaned article. */\n markdown_backend?: 'full_page' | 'main_content';\n}\n\n/** Structural surface of the SDK extract response data this package consumes. */\nexport interface NimbleRawExtractData {\n /** Markdown rendering of the page (default). */\n markdown?: string;\n /** Raw HTML of the page. */\n html?: string;\n /** Unique URLs found on the page. */\n links?: string[];\n}\n\nexport interface NimbleRawExtractResponse {\n url: string;\n status: string;\n status_code?: number;\n task_id: string;\n data: NimbleRawExtractData;\n warnings?: string[];\n}\n\nexport interface NimbleExtractClient {\n extract(params: NimbleExtractParams): Promise<NimbleRawExtractResponse>;\n}\n\n/** The normalized extract output returned to the model. */\nexport interface NimbleExtractOutput {\n /** The final URL (after redirects). */\n url: string;\n /** Task status reported by Nimble (e.g. `success`). */\n status: string;\n statusCode?: number;\n format: ExtractFormat;\n /** The extracted page content in the requested format, truncated. */\n content: string;\n /** Unique links found on the page, when available. */\n links?: string[];\n}\n","import type {\n ExtractFormat,\n NimbleExtractOutput,\n NimbleRawExtractResponse,\n NimbleRawSearchResponse,\n NimbleRawSearchResult,\n NimbleSearchOutput,\n NimbleSearchResultItem,\n NimbleSerpMetadata,\n} from './schemas';\n\nexport interface NormalizeOptions {\n query: string;\n maxContentLength: number;\n}\n\n/** SERP focus (v1 `general`) carries position + entity_type; WSA does not. */\nfunction isSerpMetadata(\n metadata: NimbleRawSearchResult['metadata'],\n): metadata is NimbleSerpMetadata {\n return 'position' in metadata;\n}\n\nfunction truncate(text: string, max: number): string {\n return text.length > max ? text.slice(0, max) : text;\n}\n\n/**\n * Map a raw `/v1/search` response into the package's normalized output shape.\n *\n * - `description` is the snippet (always present when the API returns one).\n * - `content` is the full page text, present only in `deep` depth (empty in\n * `lite`), truncated to `maxContentLength`.\n * - `position` / `entityType` come from SERP metadata; for WSA results\n * `position` falls back to the array index and `entityType` is omitted.\n * - Results without a URL are dropped (defensive).\n * - `response.answer` is never surfaced in v1.\n */\nexport function normalizeSearchResponse(\n response: NimbleRawSearchResponse,\n options: NormalizeOptions,\n): NimbleSearchOutput {\n const results: NimbleSearchResultItem[] = [];\n\n response.results.forEach((raw, index) => {\n if (!raw.url) return;\n\n const item: NimbleSearchResultItem = {\n title: raw.title ?? '',\n url: raw.url,\n };\n if (raw.description) item.description = raw.description;\n if (raw.content && raw.content.trim().length > 0) {\n item.content = truncate(raw.content, options.maxContentLength);\n }\n\n if (isSerpMetadata(raw.metadata)) {\n item.position = raw.metadata.position;\n item.entityType = raw.metadata.entity_type;\n } else {\n item.position = index + 1;\n }\n\n results.push(item);\n });\n\n return {\n query: options.query,\n requestId: response.request_id,\n totalResults: response.total_results,\n results,\n };\n}\n\nexport interface NormalizeExtractOptions {\n format: ExtractFormat;\n maxContentLength: number;\n}\n\n/**\n * Map a raw `/v1/extract` response into the package's normalized extract shape.\n *\n * - `content` is `data.markdown` (default) or `data.html`, falling back to the\n * other when the requested one is empty, truncated to `maxContentLength`.\n * - `format` reflects the rendering actually returned, which may differ from\n * the requested format when the fallback is used.\n * - `links` is surfaced when present; everything else (browser actions, network\n * captures, screenshots) is intentionally dropped.\n */\nexport function normalizeExtractResponse(\n response: NimbleRawExtractResponse,\n options: NormalizeExtractOptions,\n): NimbleExtractOutput {\n const data = response.data;\n const otherFormat: ExtractFormat = options.format === 'html' ? 'markdown' : 'html';\n const primary = options.format === 'html' ? data.html : data.markdown;\n const fallback = options.format === 'html' ? data.markdown : data.html;\n\n // Report the format that actually populated `content`. We request one\n // rendering, but if the API returns it empty we fall back to the other — and\n // the model must be told which format it received, not the one we asked for.\n const usedFormat: ExtractFormat =\n primary && primary.length > 0\n ? options.format\n : fallback && fallback.length > 0\n ? otherFormat\n : options.format;\n const content = truncate(primary || fallback || '', options.maxContentLength);\n\n const out: NimbleExtractOutput = {\n url: response.url,\n status: response.status,\n format: usedFormat,\n content,\n };\n if (typeof response.status_code === 'number') out.statusCode = response.status_code;\n if (data.links && data.links.length > 0) out.links = data.links;\n return out;\n}\n","/**\n * Thrown when the tool is invoked without a resolvable API key (no `apiKey`\n * config and no `NIMBLE_API_KEY` in the environment) and no injected client.\n * Raised at execute time, not at factory-construction time, so the tool can be\n * constructed in environments without a key (e.g. unit tests, type-checking).\n */\nexport class NimbleConfigError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'NimbleConfigError';\n }\n}\n\n/**\n * Wraps an error surfaced by the Nimble client / API during a search call,\n * preserving the HTTP status when available. The AI SDK surfaces a thrown\n * tool error back to the model as a tool-call failure.\n */\nexport class NimbleSearchError extends Error {\n readonly status?: number;\n\n constructor(message: string, options?: { status?: number; cause?: unknown }) {\n super(message, options?.cause !== undefined ? { cause: options.cause } : undefined);\n this.name = 'NimbleSearchError';\n this.status = options?.status;\n }\n}\n\n/**\n * Wraps an error surfaced by the Nimble client / API during an extract call,\n * preserving the HTTP status when available.\n */\nexport class NimbleExtractError extends Error {\n readonly status?: number;\n\n constructor(message: string, options?: { status?: number; cause?: unknown }) {\n super(message, options?.cause !== undefined ? { cause: options.cause } : undefined);\n this.name = 'NimbleExtractError';\n this.status = options?.status;\n }\n}\n","import { tool } from 'ai';\nimport { Nimble } from '@nimble-way/nimble-js';\nimport { nimbleSearchInputSchema } from './schemas';\nimport type {\n NimbleSearchClient,\n NimbleSearchOutput,\n NimbleSearchParams,\n NimbleSearchToolConfig,\n} from './schemas';\nimport { normalizeSearchResponse } from './normalize';\nimport { NimbleConfigError, NimbleSearchError } from './errors';\n\n/** v1 factory defaults. `focus` is fixed to `general` and not user-exposed. */\nexport const NIMBLE_SEARCH_DEFAULTS = {\n maxResults: 5,\n maxResultsCap: 10,\n searchDepth: 'lite',\n country: 'US',\n locale: 'en',\n maxContentLength: 10_000,\n focus: 'general',\n} as const;\n\nfunction clamp(value: number, min: number, max: number): number {\n return Math.min(Math.max(value, min), max);\n}\n\nfunction readStatus(err: unknown): number | undefined {\n if (typeof err === 'object' && err !== null && 'status' in err) {\n const status = (err as { status?: unknown }).status;\n return typeof status === 'number' ? status : undefined;\n }\n return undefined;\n}\n\nfunction toSearchError(err: unknown): NimbleSearchError {\n if (err instanceof NimbleSearchError) return err;\n const message = err instanceof Error ? err.message : String(err);\n return new NimbleSearchError(`Nimble search failed: ${message}`, {\n status: readStatus(err),\n cause: err,\n });\n}\n\nfunction resolveClient(config: NimbleSearchToolConfig): NimbleSearchClient {\n if (config.client) return config.client;\n const apiKey = config.apiKey ?? process.env.NIMBLE_API_KEY;\n if (!apiKey) {\n throw new NimbleConfigError(\n 'Missing Nimble API key: set NIMBLE_API_KEY or pass { apiKey } to nimbleSearch().',\n );\n }\n return new Nimble({ apiKey }) as unknown as NimbleSearchClient;\n}\n\n/**\n * Create a ready-made Vercel AI SDK web-search tool backed by Nimble Search\n * (`@nimble-way/nimble-js` → `POST /v1/search`).\n *\n * The model only chooses `{ query, maxResults? }`; all policy (depth, focus,\n * region, caps) is fixed by `config` — mirroring the Exa `webSearch()` shape.\n *\n * @example\n * ```ts\n * import { generateText } from 'ai';\n * import { nimbleSearch } from '@nimble-way/ai-sdk';\n *\n * const { text } = await generateText({\n * model: 'anthropic/claude-sonnet-4.6',\n * prompt: 'What are the latest Nimble release notes?',\n * tools: { webSearch: nimbleSearch({ searchDepth: 'lite', maxResults: 5 }) },\n * });\n * ```\n */\nexport function nimbleSearch(config: NimbleSearchToolConfig = {}) {\n const maxResults = config.maxResults ?? NIMBLE_SEARCH_DEFAULTS.maxResults;\n const maxResultsCap = config.maxResultsCap ?? NIMBLE_SEARCH_DEFAULTS.maxResultsCap;\n const searchDepth = config.searchDepth ?? NIMBLE_SEARCH_DEFAULTS.searchDepth;\n const country = config.country ?? NIMBLE_SEARCH_DEFAULTS.country;\n const locale = config.locale ?? NIMBLE_SEARCH_DEFAULTS.locale;\n const maxContentLength = config.maxContentLength ?? NIMBLE_SEARCH_DEFAULTS.maxContentLength;\n\n return tool({\n description:\n 'Search the web with Nimble and return ranked results (title, url, ' +\n 'snippet, and page content) for answering questions about current or ' +\n 'factual information.',\n inputSchema: nimbleSearchInputSchema,\n execute: async (input): Promise<NimbleSearchOutput> => {\n const client = resolveClient(config);\n\n const params: NimbleSearchParams = {\n query: input.query,\n max_results: clamp(input.maxResults ?? maxResults, 1, maxResultsCap),\n search_depth: searchDepth,\n focus: NIMBLE_SEARCH_DEFAULTS.focus, // fixed 'general' in v1\n country,\n locale,\n };\n\n let raw;\n try {\n raw = await client.search(params);\n } catch (err) {\n throw toSearchError(err);\n }\n\n return normalizeSearchResponse(raw, {\n query: input.query,\n maxContentLength,\n });\n },\n });\n}\n","import { tool } from 'ai';\nimport { Nimble } from '@nimble-way/nimble-js';\nimport { nimbleExtractInputSchema } from './schemas';\nimport type {\n ExtractFormat,\n NimbleExtractClient,\n NimbleExtractOutput,\n NimbleExtractParams,\n NimbleExtractToolConfig,\n} from './schemas';\nimport { normalizeExtractResponse } from './normalize';\nimport { NimbleConfigError, NimbleExtractError } from './errors';\n\n/** v1 extract defaults. */\nexport const NIMBLE_EXTRACT_DEFAULTS: { format: ExtractFormat; maxContentLength: number } = {\n format: 'markdown',\n maxContentLength: 50_000,\n};\n\nfunction readStatus(err: unknown): number | undefined {\n if (typeof err === 'object' && err !== null && 'status' in err) {\n const status = (err as { status?: unknown }).status;\n return typeof status === 'number' ? status : undefined;\n }\n return undefined;\n}\n\nfunction toExtractError(err: unknown): NimbleExtractError {\n if (err instanceof NimbleExtractError) return err;\n const message = err instanceof Error ? err.message : String(err);\n return new NimbleExtractError(`Nimble extract failed: ${message}`, {\n status: readStatus(err),\n cause: err,\n });\n}\n\nfunction resolveClient(config: NimbleExtractToolConfig): NimbleExtractClient {\n if (config.client) return config.client;\n const apiKey = config.apiKey ?? process.env.NIMBLE_API_KEY;\n if (!apiKey) {\n throw new NimbleConfigError(\n 'Missing Nimble API key: set NIMBLE_API_KEY or pass { apiKey } to nimbleExtract().',\n );\n }\n return new Nimble({ apiKey }) as unknown as NimbleExtractClient;\n}\n\n/**\n * Create a Vercel AI SDK tool that extracts clean, readable content from a web\n * page with Nimble (`@nimble-way/nimble-js` → `POST /v1/extract`).\n *\n * The model only chooses `{ url }`; format, region, and length cap are fixed by\n * `config`. Mirrors {@link nimbleSearch}'s ergonomics.\n *\n * @example\n * ```ts\n * import { generateText } from 'ai';\n * import { nimbleExtract } from '@nimble-way/ai-sdk';\n *\n * const { text } = await generateText({\n * model: 'openai/gpt-4o-mini',\n * prompt: 'Summarize https://nimbleway.com',\n * tools: { extract: nimbleExtract() },\n * });\n * ```\n */\nexport function nimbleExtract(config: NimbleExtractToolConfig = {}) {\n const format = config.format ?? NIMBLE_EXTRACT_DEFAULTS.format;\n const maxContentLength = config.maxContentLength ?? NIMBLE_EXTRACT_DEFAULTS.maxContentLength;\n const country = config.country;\n\n return tool({\n description:\n 'Fetch a web page by URL with Nimble and return its clean, readable content ' +\n '(markdown or HTML) — use this to read, quote, or summarize a specific page.',\n inputSchema: nimbleExtractInputSchema,\n execute: async (input): Promise<NimbleExtractOutput> => {\n const client = resolveClient(config);\n\n // Request both renderings (preferred format first) plus links. A rendering\n // is only produced when requested via `formats`, so asking for both lets an\n // empty primary fall back to the other; normalizeExtractResponse reports\n // whichever rendering actually populated the content.\n const params: NimbleExtractParams = {\n url: input.url,\n formats:\n format === 'html' ? ['html', 'markdown', 'links'] : ['markdown', 'html', 'links'],\n };\n if (country) params.country = country;\n // `main_content` returns the cleaned article body rather than the full page.\n if (format === 'markdown') params.markdown_backend = 'main_content';\n\n let raw;\n try {\n raw = await client.extract(params);\n } catch (err) {\n throw toExtractError(err);\n }\n\n return normalizeExtractResponse(raw, { format, maxContentLength });\n },\n });\n}\n"]}
package/dist/index.d.cts CHANGED
@@ -111,6 +111,75 @@ interface NimbleSearchOutput {
111
111
  totalResults?: number;
112
112
  results: NimbleSearchResultItem[];
113
113
  }
114
+ /** Output format for extracted page content. */
115
+ type ExtractFormat = 'markdown' | 'html';
116
+ /**
117
+ * The extract tool input the model fills in: just the URL to read. All policy
118
+ * (format, region, length cap) is fixed by the developer via the factory config.
119
+ */
120
+ declare const nimbleExtractInputSchema: z.ZodObject<{
121
+ url: z.ZodString;
122
+ }, "strip", z.ZodTypeAny, {
123
+ url: string;
124
+ }, {
125
+ url: string;
126
+ }>;
127
+ type NimbleExtractInput = z.infer<typeof nimbleExtractInputSchema>;
128
+ /** Developer-facing factory config for the extract tool. */
129
+ interface NimbleExtractToolConfig {
130
+ /** Nimble API key. Defaults to `process.env.NIMBLE_API_KEY`. */
131
+ apiKey?: string;
132
+ /** Inject a pre-built / mock Nimble client (tests, advanced users). */
133
+ client?: NimbleExtractClient;
134
+ /** Content format. Default `markdown`. */
135
+ format?: ExtractFormat;
136
+ /** ISO country for geolocation / proxy selection. */
137
+ country?: string;
138
+ /** Truncate the extracted content to this many characters. Default 50_000. */
139
+ maxContentLength?: number;
140
+ }
141
+ /** Params this package sends to the SDK's `client.extract()`. */
142
+ interface NimbleExtractParams {
143
+ url: string;
144
+ country?: string;
145
+ /** Which renderings to request; `data.<format>` is populated per entry. */
146
+ formats?: Array<'html' | 'markdown' | 'links'>;
147
+ /** Refines Markdown extraction; `main_content` yields the cleaned article. */
148
+ markdown_backend?: 'full_page' | 'main_content';
149
+ }
150
+ /** Structural surface of the SDK extract response data this package consumes. */
151
+ interface NimbleRawExtractData {
152
+ /** Markdown rendering of the page (default). */
153
+ markdown?: string;
154
+ /** Raw HTML of the page. */
155
+ html?: string;
156
+ /** Unique URLs found on the page. */
157
+ links?: string[];
158
+ }
159
+ interface NimbleRawExtractResponse {
160
+ url: string;
161
+ status: string;
162
+ status_code?: number;
163
+ task_id: string;
164
+ data: NimbleRawExtractData;
165
+ warnings?: string[];
166
+ }
167
+ interface NimbleExtractClient {
168
+ extract(params: NimbleExtractParams): Promise<NimbleRawExtractResponse>;
169
+ }
170
+ /** The normalized extract output returned to the model. */
171
+ interface NimbleExtractOutput {
172
+ /** The final URL (after redirects). */
173
+ url: string;
174
+ /** Task status reported by Nimble (e.g. `success`). */
175
+ status: string;
176
+ statusCode?: number;
177
+ format: ExtractFormat;
178
+ /** The extracted page content in the requested format, truncated. */
179
+ content: string;
180
+ /** Unique links found on the page, when available. */
181
+ links?: string[];
182
+ }
114
183
 
115
184
  /** v1 factory defaults. `focus` is fixed to `general` and not user-exposed. */
116
185
  declare const NIMBLE_SEARCH_DEFAULTS: {
@@ -146,6 +215,34 @@ declare function nimbleSearch(config?: NimbleSearchToolConfig): ai.Tool<{
146
215
  maxResults?: number | undefined;
147
216
  }, NimbleSearchOutput>;
148
217
 
218
+ /** v1 extract defaults. */
219
+ declare const NIMBLE_EXTRACT_DEFAULTS: {
220
+ format: ExtractFormat;
221
+ maxContentLength: number;
222
+ };
223
+ /**
224
+ * Create a Vercel AI SDK tool that extracts clean, readable content from a web
225
+ * page with Nimble (`@nimble-way/nimble-js` → `POST /v1/extract`).
226
+ *
227
+ * The model only chooses `{ url }`; format, region, and length cap are fixed by
228
+ * `config`. Mirrors {@link nimbleSearch}'s ergonomics.
229
+ *
230
+ * @example
231
+ * ```ts
232
+ * import { generateText } from 'ai';
233
+ * import { nimbleExtract } from '@nimble-way/ai-sdk';
234
+ *
235
+ * const { text } = await generateText({
236
+ * model: 'openai/gpt-4o-mini',
237
+ * prompt: 'Summarize https://nimbleway.com',
238
+ * tools: { extract: nimbleExtract() },
239
+ * });
240
+ * ```
241
+ */
242
+ declare function nimbleExtract(config?: NimbleExtractToolConfig): ai.Tool<{
243
+ url: string;
244
+ }, NimbleExtractOutput>;
245
+
149
246
  interface NormalizeOptions {
150
247
  query: string;
151
248
  maxContentLength: number;
@@ -162,6 +259,21 @@ interface NormalizeOptions {
162
259
  * - `response.answer` is never surfaced in v1.
163
260
  */
164
261
  declare function normalizeSearchResponse(response: NimbleRawSearchResponse, options: NormalizeOptions): NimbleSearchOutput;
262
+ interface NormalizeExtractOptions {
263
+ format: ExtractFormat;
264
+ maxContentLength: number;
265
+ }
266
+ /**
267
+ * Map a raw `/v1/extract` response into the package's normalized extract shape.
268
+ *
269
+ * - `content` is `data.markdown` (default) or `data.html`, falling back to the
270
+ * other when the requested one is empty, truncated to `maxContentLength`.
271
+ * - `format` reflects the rendering actually returned, which may differ from
272
+ * the requested format when the fallback is used.
273
+ * - `links` is surfaced when present; everything else (browser actions, network
274
+ * captures, screenshots) is intentionally dropped.
275
+ */
276
+ declare function normalizeExtractResponse(response: NimbleRawExtractResponse, options: NormalizeExtractOptions): NimbleExtractOutput;
165
277
 
166
278
  /**
167
279
  * Thrown when the tool is invoked without a resolvable API key (no `apiKey`
@@ -184,5 +296,16 @@ declare class NimbleSearchError extends Error {
184
296
  cause?: unknown;
185
297
  });
186
298
  }
299
+ /**
300
+ * Wraps an error surfaced by the Nimble client / API during an extract call,
301
+ * preserving the HTTP status when available.
302
+ */
303
+ declare class NimbleExtractError extends Error {
304
+ readonly status?: number;
305
+ constructor(message: string, options?: {
306
+ status?: number;
307
+ cause?: unknown;
308
+ });
309
+ }
187
310
 
188
- export { NIMBLE_SEARCH_DEFAULTS, NimbleConfigError, type NimbleRawSearchResponse, type NimbleRawSearchResult, type NimbleSearchClient, NimbleSearchError, type NimbleSearchInput, type NimbleSearchOutput, type NimbleSearchParams, type NimbleSearchResultItem, type NimbleSearchToolConfig, type NimbleSerpMetadata, type NimbleWsaMetadata, type NormalizeOptions, type SearchDepth, nimbleSearch, nimbleSearchInputSchema, normalizeSearchResponse };
311
+ export { type ExtractFormat, NIMBLE_EXTRACT_DEFAULTS, NIMBLE_SEARCH_DEFAULTS, NimbleConfigError, type NimbleExtractClient, NimbleExtractError, type NimbleExtractInput, type NimbleExtractOutput, type NimbleExtractParams, type NimbleExtractToolConfig, type NimbleRawExtractData, type NimbleRawExtractResponse, type NimbleRawSearchResponse, type NimbleRawSearchResult, type NimbleSearchClient, NimbleSearchError, type NimbleSearchInput, type NimbleSearchOutput, type NimbleSearchParams, type NimbleSearchResultItem, type NimbleSearchToolConfig, type NimbleSerpMetadata, type NimbleWsaMetadata, type NormalizeExtractOptions, type NormalizeOptions, type SearchDepth, nimbleExtract, nimbleExtractInputSchema, nimbleSearch, nimbleSearchInputSchema, normalizeExtractResponse, normalizeSearchResponse };
package/dist/index.d.ts CHANGED
@@ -111,6 +111,75 @@ interface NimbleSearchOutput {
111
111
  totalResults?: number;
112
112
  results: NimbleSearchResultItem[];
113
113
  }
114
+ /** Output format for extracted page content. */
115
+ type ExtractFormat = 'markdown' | 'html';
116
+ /**
117
+ * The extract tool input the model fills in: just the URL to read. All policy
118
+ * (format, region, length cap) is fixed by the developer via the factory config.
119
+ */
120
+ declare const nimbleExtractInputSchema: z.ZodObject<{
121
+ url: z.ZodString;
122
+ }, "strip", z.ZodTypeAny, {
123
+ url: string;
124
+ }, {
125
+ url: string;
126
+ }>;
127
+ type NimbleExtractInput = z.infer<typeof nimbleExtractInputSchema>;
128
+ /** Developer-facing factory config for the extract tool. */
129
+ interface NimbleExtractToolConfig {
130
+ /** Nimble API key. Defaults to `process.env.NIMBLE_API_KEY`. */
131
+ apiKey?: string;
132
+ /** Inject a pre-built / mock Nimble client (tests, advanced users). */
133
+ client?: NimbleExtractClient;
134
+ /** Content format. Default `markdown`. */
135
+ format?: ExtractFormat;
136
+ /** ISO country for geolocation / proxy selection. */
137
+ country?: string;
138
+ /** Truncate the extracted content to this many characters. Default 50_000. */
139
+ maxContentLength?: number;
140
+ }
141
+ /** Params this package sends to the SDK's `client.extract()`. */
142
+ interface NimbleExtractParams {
143
+ url: string;
144
+ country?: string;
145
+ /** Which renderings to request; `data.<format>` is populated per entry. */
146
+ formats?: Array<'html' | 'markdown' | 'links'>;
147
+ /** Refines Markdown extraction; `main_content` yields the cleaned article. */
148
+ markdown_backend?: 'full_page' | 'main_content';
149
+ }
150
+ /** Structural surface of the SDK extract response data this package consumes. */
151
+ interface NimbleRawExtractData {
152
+ /** Markdown rendering of the page (default). */
153
+ markdown?: string;
154
+ /** Raw HTML of the page. */
155
+ html?: string;
156
+ /** Unique URLs found on the page. */
157
+ links?: string[];
158
+ }
159
+ interface NimbleRawExtractResponse {
160
+ url: string;
161
+ status: string;
162
+ status_code?: number;
163
+ task_id: string;
164
+ data: NimbleRawExtractData;
165
+ warnings?: string[];
166
+ }
167
+ interface NimbleExtractClient {
168
+ extract(params: NimbleExtractParams): Promise<NimbleRawExtractResponse>;
169
+ }
170
+ /** The normalized extract output returned to the model. */
171
+ interface NimbleExtractOutput {
172
+ /** The final URL (after redirects). */
173
+ url: string;
174
+ /** Task status reported by Nimble (e.g. `success`). */
175
+ status: string;
176
+ statusCode?: number;
177
+ format: ExtractFormat;
178
+ /** The extracted page content in the requested format, truncated. */
179
+ content: string;
180
+ /** Unique links found on the page, when available. */
181
+ links?: string[];
182
+ }
114
183
 
115
184
  /** v1 factory defaults. `focus` is fixed to `general` and not user-exposed. */
116
185
  declare const NIMBLE_SEARCH_DEFAULTS: {
@@ -146,6 +215,34 @@ declare function nimbleSearch(config?: NimbleSearchToolConfig): ai.Tool<{
146
215
  maxResults?: number | undefined;
147
216
  }, NimbleSearchOutput>;
148
217
 
218
+ /** v1 extract defaults. */
219
+ declare const NIMBLE_EXTRACT_DEFAULTS: {
220
+ format: ExtractFormat;
221
+ maxContentLength: number;
222
+ };
223
+ /**
224
+ * Create a Vercel AI SDK tool that extracts clean, readable content from a web
225
+ * page with Nimble (`@nimble-way/nimble-js` → `POST /v1/extract`).
226
+ *
227
+ * The model only chooses `{ url }`; format, region, and length cap are fixed by
228
+ * `config`. Mirrors {@link nimbleSearch}'s ergonomics.
229
+ *
230
+ * @example
231
+ * ```ts
232
+ * import { generateText } from 'ai';
233
+ * import { nimbleExtract } from '@nimble-way/ai-sdk';
234
+ *
235
+ * const { text } = await generateText({
236
+ * model: 'openai/gpt-4o-mini',
237
+ * prompt: 'Summarize https://nimbleway.com',
238
+ * tools: { extract: nimbleExtract() },
239
+ * });
240
+ * ```
241
+ */
242
+ declare function nimbleExtract(config?: NimbleExtractToolConfig): ai.Tool<{
243
+ url: string;
244
+ }, NimbleExtractOutput>;
245
+
149
246
  interface NormalizeOptions {
150
247
  query: string;
151
248
  maxContentLength: number;
@@ -162,6 +259,21 @@ interface NormalizeOptions {
162
259
  * - `response.answer` is never surfaced in v1.
163
260
  */
164
261
  declare function normalizeSearchResponse(response: NimbleRawSearchResponse, options: NormalizeOptions): NimbleSearchOutput;
262
+ interface NormalizeExtractOptions {
263
+ format: ExtractFormat;
264
+ maxContentLength: number;
265
+ }
266
+ /**
267
+ * Map a raw `/v1/extract` response into the package's normalized extract shape.
268
+ *
269
+ * - `content` is `data.markdown` (default) or `data.html`, falling back to the
270
+ * other when the requested one is empty, truncated to `maxContentLength`.
271
+ * - `format` reflects the rendering actually returned, which may differ from
272
+ * the requested format when the fallback is used.
273
+ * - `links` is surfaced when present; everything else (browser actions, network
274
+ * captures, screenshots) is intentionally dropped.
275
+ */
276
+ declare function normalizeExtractResponse(response: NimbleRawExtractResponse, options: NormalizeExtractOptions): NimbleExtractOutput;
165
277
 
166
278
  /**
167
279
  * Thrown when the tool is invoked without a resolvable API key (no `apiKey`
@@ -184,5 +296,16 @@ declare class NimbleSearchError extends Error {
184
296
  cause?: unknown;
185
297
  });
186
298
  }
299
+ /**
300
+ * Wraps an error surfaced by the Nimble client / API during an extract call,
301
+ * preserving the HTTP status when available.
302
+ */
303
+ declare class NimbleExtractError extends Error {
304
+ readonly status?: number;
305
+ constructor(message: string, options?: {
306
+ status?: number;
307
+ cause?: unknown;
308
+ });
309
+ }
187
310
 
188
- export { NIMBLE_SEARCH_DEFAULTS, NimbleConfigError, type NimbleRawSearchResponse, type NimbleRawSearchResult, type NimbleSearchClient, NimbleSearchError, type NimbleSearchInput, type NimbleSearchOutput, type NimbleSearchParams, type NimbleSearchResultItem, type NimbleSearchToolConfig, type NimbleSerpMetadata, type NimbleWsaMetadata, type NormalizeOptions, type SearchDepth, nimbleSearch, nimbleSearchInputSchema, normalizeSearchResponse };
311
+ export { type ExtractFormat, NIMBLE_EXTRACT_DEFAULTS, NIMBLE_SEARCH_DEFAULTS, NimbleConfigError, type NimbleExtractClient, NimbleExtractError, type NimbleExtractInput, type NimbleExtractOutput, type NimbleExtractParams, type NimbleExtractToolConfig, type NimbleRawExtractData, type NimbleRawExtractResponse, type NimbleRawSearchResponse, type NimbleRawSearchResult, type NimbleSearchClient, NimbleSearchError, type NimbleSearchInput, type NimbleSearchOutput, type NimbleSearchParams, type NimbleSearchResultItem, type NimbleSearchToolConfig, type NimbleSerpMetadata, type NimbleWsaMetadata, type NormalizeExtractOptions, type NormalizeOptions, type SearchDepth, nimbleExtract, nimbleExtractInputSchema, nimbleSearch, nimbleSearchInputSchema, normalizeExtractResponse, normalizeSearchResponse };
package/dist/index.js CHANGED
@@ -7,6 +7,9 @@ var nimbleSearchInputSchema = z.object({
7
7
  query: z.string().min(1).describe("The web search query."),
8
8
  maxResults: z.number().int().positive().optional().describe("How many results to return (clamped to the developer-configured cap).")
9
9
  });
10
+ var nimbleExtractInputSchema = z.object({
11
+ url: z.string().url().describe("The URL of the web page to extract clean content from.")
12
+ });
10
13
 
11
14
  // src/normalize.ts
12
15
  function isSerpMetadata(metadata) {
@@ -42,6 +45,23 @@ function normalizeSearchResponse(response, options) {
42
45
  results
43
46
  };
44
47
  }
48
+ function normalizeExtractResponse(response, options) {
49
+ const data = response.data;
50
+ const otherFormat = options.format === "html" ? "markdown" : "html";
51
+ const primary = options.format === "html" ? data.html : data.markdown;
52
+ const fallback = options.format === "html" ? data.markdown : data.html;
53
+ const usedFormat = primary && primary.length > 0 ? options.format : fallback && fallback.length > 0 ? otherFormat : options.format;
54
+ const content = truncate(primary || fallback || "", options.maxContentLength);
55
+ const out = {
56
+ url: response.url,
57
+ status: response.status,
58
+ format: usedFormat,
59
+ content
60
+ };
61
+ if (typeof response.status_code === "number") out.statusCode = response.status_code;
62
+ if (data.links && data.links.length > 0) out.links = data.links;
63
+ return out;
64
+ }
45
65
 
46
66
  // src/errors.ts
47
67
  var NimbleConfigError = class extends Error {
@@ -58,6 +78,14 @@ var NimbleSearchError = class extends Error {
58
78
  this.status = options?.status;
59
79
  }
60
80
  };
81
+ var NimbleExtractError = class extends Error {
82
+ status;
83
+ constructor(message, options) {
84
+ super(message, options?.cause !== void 0 ? { cause: options.cause } : void 0);
85
+ this.name = "NimbleExtractError";
86
+ this.status = options?.status;
87
+ }
88
+ };
61
89
 
62
90
  // src/nimble-search.ts
63
91
  var NIMBLE_SEARCH_DEFAULTS = {
@@ -131,7 +159,61 @@ function nimbleSearch(config = {}) {
131
159
  }
132
160
  });
133
161
  }
162
+ var NIMBLE_EXTRACT_DEFAULTS = {
163
+ format: "markdown",
164
+ maxContentLength: 5e4
165
+ };
166
+ function readStatus2(err) {
167
+ if (typeof err === "object" && err !== null && "status" in err) {
168
+ const status = err.status;
169
+ return typeof status === "number" ? status : void 0;
170
+ }
171
+ return void 0;
172
+ }
173
+ function toExtractError(err) {
174
+ if (err instanceof NimbleExtractError) return err;
175
+ const message = err instanceof Error ? err.message : String(err);
176
+ return new NimbleExtractError(`Nimble extract failed: ${message}`, {
177
+ status: readStatus2(err),
178
+ cause: err
179
+ });
180
+ }
181
+ function resolveClient2(config) {
182
+ if (config.client) return config.client;
183
+ const apiKey = config.apiKey ?? process.env.NIMBLE_API_KEY;
184
+ if (!apiKey) {
185
+ throw new NimbleConfigError(
186
+ "Missing Nimble API key: set NIMBLE_API_KEY or pass { apiKey } to nimbleExtract()."
187
+ );
188
+ }
189
+ return new Nimble({ apiKey });
190
+ }
191
+ function nimbleExtract(config = {}) {
192
+ const format = config.format ?? NIMBLE_EXTRACT_DEFAULTS.format;
193
+ const maxContentLength = config.maxContentLength ?? NIMBLE_EXTRACT_DEFAULTS.maxContentLength;
194
+ const country = config.country;
195
+ return tool({
196
+ description: "Fetch a web page by URL with Nimble and return its clean, readable content (markdown or HTML) \u2014 use this to read, quote, or summarize a specific page.",
197
+ inputSchema: nimbleExtractInputSchema,
198
+ execute: async (input) => {
199
+ const client = resolveClient2(config);
200
+ const params = {
201
+ url: input.url,
202
+ formats: format === "html" ? ["html", "markdown", "links"] : ["markdown", "html", "links"]
203
+ };
204
+ if (country) params.country = country;
205
+ if (format === "markdown") params.markdown_backend = "main_content";
206
+ let raw;
207
+ try {
208
+ raw = await client.extract(params);
209
+ } catch (err) {
210
+ throw toExtractError(err);
211
+ }
212
+ return normalizeExtractResponse(raw, { format, maxContentLength });
213
+ }
214
+ });
215
+ }
134
216
 
135
- export { NIMBLE_SEARCH_DEFAULTS, NimbleConfigError, NimbleSearchError, nimbleSearch, nimbleSearchInputSchema, normalizeSearchResponse };
217
+ export { NIMBLE_EXTRACT_DEFAULTS, NIMBLE_SEARCH_DEFAULTS, NimbleConfigError, NimbleExtractError, NimbleSearchError, nimbleExtract, nimbleExtractInputSchema, nimbleSearch, nimbleSearchInputSchema, normalizeExtractResponse, normalizeSearchResponse };
136
218
  //# sourceMappingURL=index.js.map
137
219
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/schemas.ts","../src/normalize.ts","../src/errors.ts","../src/nimble-search.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["import { z } from 'zod';\n\n/**\n * The tool input the model fills in. Kept deliberately small: the model only\n * chooses the query and (optionally) how many results it wants. All policy\n * (depth, focus, region, caps) is fixed by the developer via the factory\n * config, not by the model.\n */\nexport const nimbleSearchInputSchema = z.object({\n query: z.string().min(1).describe('The web search query.'),\n maxResults: z\n .number()\n .int()\n .positive()\n .optional()\n .describe('How many results to return (clamped to the developer-configured cap).'),\n});\n\nexport type NimbleSearchInput = z.infer<typeof nimbleSearchInputSchema>;\n\n/**\n * v1 exposes only the two non-enterprise depths. `fast` is enterprise-gated and\n * intentionally not offered here.\n */\nexport type SearchDepth = 'lite' | 'deep';\n\n/**\n * Developer-facing factory config. `focus` is fixed to `general` in v1 and is\n * not exposed. `include_answer` and `search_depth: 'fast'` are intentionally\n * absent (enterprise / unverified-entitlement surface).\n */\nexport interface NimbleSearchToolConfig {\n /** Nimble API key. Defaults to `process.env.NIMBLE_API_KEY`. */\n apiKey?: string;\n /** Inject a pre-built / mock Nimble client (tests, advanced users). */\n client?: NimbleSearchClient;\n /** Default number of results when the model doesn't specify. Default 5. */\n maxResults?: number;\n /** Hard upper bound on results, regardless of model request. Default 10. */\n maxResultsCap?: number;\n /** Search depth. Default `lite`. */\n searchDepth?: SearchDepth;\n /** ISO country for result localization. Default `US`. */\n country?: string;\n /** Locale for result localization. Default `en`. */\n locale?: string;\n /** Truncate each result's body to this many characters. Default 10_000. */\n maxContentLength?: number;\n}\n\n/**\n * Structural surface of `@nimble-way/nimble-js`'s `client.search()` that this\n * package relies on. Declared structurally so the scaffold typechecks without\n * pinning to the SDK's generated type names, and so tests can inject a mock.\n *\n * Phase B: reconcile field casing/names against\n * `sdks/nimble-js/checkout/src/` after `./tools/sync-sdk.sh nimble-js`.\n */\nexport interface NimbleSearchParams {\n query: string;\n max_results?: number;\n search_depth?: SearchDepth;\n focus?: string;\n country?: string;\n locale?: string;\n}\n\n/** Metadata for SERP-based results (general/news/location focus). */\nexport interface NimbleSerpMetadata {\n country: string;\n entity_type: string;\n locale: string;\n position: number;\n driver?: string | null;\n}\n\n/** Metadata for WSA-based results (shopping/social/geo focus). */\nexport interface NimbleWsaMetadata {\n agent_name: string;\n}\n\nexport interface NimbleRawSearchResult {\n /** Full page text in `deep`; may be empty in `lite`. */\n content: string;\n description: string;\n title: string;\n url: string;\n /** SERP focus (v1 `general`) yields {@link NimbleSerpMetadata}. */\n metadata: NimbleSerpMetadata | NimbleWsaMetadata;\n /** Platform-specific extras (price, publish_date, …); omitted when none. */\n additional_data?: Record<string, unknown> | null;\n}\n\nexport interface NimbleRawSearchResponse {\n request_id: string;\n results: NimbleRawSearchResult[];\n total_results: number;\n /** Intentionally never surfaced in v1 (include_answer is off). */\n answer?: string | null;\n}\n\nexport interface NimbleSearchClient {\n search(params: NimbleSearchParams): Promise<NimbleRawSearchResponse>;\n}\n\n/** A single normalized result item returned to the model. */\nexport interface NimbleSearchResultItem {\n title: string;\n url: string;\n description?: string;\n content?: string;\n position?: number;\n entityType?: string;\n}\n\n/** The normalized tool output. `answer` is intentionally omitted in v1. */\nexport interface NimbleSearchOutput {\n query: string;\n requestId?: string;\n totalResults?: number;\n results: NimbleSearchResultItem[];\n}\n","import type {\n NimbleRawSearchResponse,\n NimbleRawSearchResult,\n NimbleSearchOutput,\n NimbleSearchResultItem,\n NimbleSerpMetadata,\n} from './schemas';\n\nexport interface NormalizeOptions {\n query: string;\n maxContentLength: number;\n}\n\n/** SERP focus (v1 `general`) carries position + entity_type; WSA does not. */\nfunction isSerpMetadata(\n metadata: NimbleRawSearchResult['metadata'],\n): metadata is NimbleSerpMetadata {\n return 'position' in metadata;\n}\n\nfunction truncate(text: string, max: number): string {\n return text.length > max ? text.slice(0, max) : text;\n}\n\n/**\n * Map a raw `/v1/search` response into the package's normalized output shape.\n *\n * - `description` is the snippet (always present when the API returns one).\n * - `content` is the full page text, present only in `deep` depth (empty in\n * `lite`), truncated to `maxContentLength`.\n * - `position` / `entityType` come from SERP metadata; for WSA results\n * `position` falls back to the array index and `entityType` is omitted.\n * - Results without a URL are dropped (defensive).\n * - `response.answer` is never surfaced in v1.\n */\nexport function normalizeSearchResponse(\n response: NimbleRawSearchResponse,\n options: NormalizeOptions,\n): NimbleSearchOutput {\n const results: NimbleSearchResultItem[] = [];\n\n response.results.forEach((raw, index) => {\n if (!raw.url) return;\n\n const item: NimbleSearchResultItem = {\n title: raw.title ?? '',\n url: raw.url,\n };\n if (raw.description) item.description = raw.description;\n if (raw.content && raw.content.trim().length > 0) {\n item.content = truncate(raw.content, options.maxContentLength);\n }\n\n if (isSerpMetadata(raw.metadata)) {\n item.position = raw.metadata.position;\n item.entityType = raw.metadata.entity_type;\n } else {\n item.position = index + 1;\n }\n\n results.push(item);\n });\n\n return {\n query: options.query,\n requestId: response.request_id,\n totalResults: response.total_results,\n results,\n };\n}\n","/**\n * Thrown when the tool is invoked without a resolvable API key (no `apiKey`\n * config and no `NIMBLE_API_KEY` in the environment) and no injected client.\n * Raised at execute time, not at factory-construction time, so the tool can be\n * constructed in environments without a key (e.g. unit tests, type-checking).\n */\nexport class NimbleConfigError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'NimbleConfigError';\n }\n}\n\n/**\n * Wraps an error surfaced by the Nimble client / API during a search call,\n * preserving the HTTP status when available. The AI SDK surfaces a thrown\n * tool error back to the model as a tool-call failure.\n */\nexport class NimbleSearchError extends Error {\n readonly status?: number;\n\n constructor(message: string, options?: { status?: number; cause?: unknown }) {\n super(message, options?.cause !== undefined ? { cause: options.cause } : undefined);\n this.name = 'NimbleSearchError';\n this.status = options?.status;\n }\n}\n","import { tool } from 'ai';\nimport { Nimble } from '@nimble-way/nimble-js';\nimport { nimbleSearchInputSchema } from './schemas';\nimport type {\n NimbleSearchClient,\n NimbleSearchOutput,\n NimbleSearchParams,\n NimbleSearchToolConfig,\n} from './schemas';\nimport { normalizeSearchResponse } from './normalize';\nimport { NimbleConfigError, NimbleSearchError } from './errors';\n\n/** v1 factory defaults. `focus` is fixed to `general` and not user-exposed. */\nexport const NIMBLE_SEARCH_DEFAULTS = {\n maxResults: 5,\n maxResultsCap: 10,\n searchDepth: 'lite',\n country: 'US',\n locale: 'en',\n maxContentLength: 10_000,\n focus: 'general',\n} as const;\n\nfunction clamp(value: number, min: number, max: number): number {\n return Math.min(Math.max(value, min), max);\n}\n\nfunction readStatus(err: unknown): number | undefined {\n if (typeof err === 'object' && err !== null && 'status' in err) {\n const status = (err as { status?: unknown }).status;\n return typeof status === 'number' ? status : undefined;\n }\n return undefined;\n}\n\nfunction toSearchError(err: unknown): NimbleSearchError {\n if (err instanceof NimbleSearchError) return err;\n const message = err instanceof Error ? err.message : String(err);\n return new NimbleSearchError(`Nimble search failed: ${message}`, {\n status: readStatus(err),\n cause: err,\n });\n}\n\nfunction resolveClient(config: NimbleSearchToolConfig): NimbleSearchClient {\n if (config.client) return config.client;\n const apiKey = config.apiKey ?? process.env.NIMBLE_API_KEY;\n if (!apiKey) {\n throw new NimbleConfigError(\n 'Missing Nimble API key: set NIMBLE_API_KEY or pass { apiKey } to nimbleSearch().',\n );\n }\n return new Nimble({ apiKey }) as unknown as NimbleSearchClient;\n}\n\n/**\n * Create a ready-made Vercel AI SDK web-search tool backed by Nimble Search\n * (`@nimble-way/nimble-js` → `POST /v1/search`).\n *\n * The model only chooses `{ query, maxResults? }`; all policy (depth, focus,\n * region, caps) is fixed by `config` — mirroring the Exa `webSearch()` shape.\n *\n * @example\n * ```ts\n * import { generateText } from 'ai';\n * import { nimbleSearch } from '@nimble-way/ai-sdk';\n *\n * const { text } = await generateText({\n * model: 'anthropic/claude-sonnet-4.6',\n * prompt: 'What are the latest Nimble release notes?',\n * tools: { webSearch: nimbleSearch({ searchDepth: 'lite', maxResults: 5 }) },\n * });\n * ```\n */\nexport function nimbleSearch(config: NimbleSearchToolConfig = {}) {\n const maxResults = config.maxResults ?? NIMBLE_SEARCH_DEFAULTS.maxResults;\n const maxResultsCap = config.maxResultsCap ?? NIMBLE_SEARCH_DEFAULTS.maxResultsCap;\n const searchDepth = config.searchDepth ?? NIMBLE_SEARCH_DEFAULTS.searchDepth;\n const country = config.country ?? NIMBLE_SEARCH_DEFAULTS.country;\n const locale = config.locale ?? NIMBLE_SEARCH_DEFAULTS.locale;\n const maxContentLength = config.maxContentLength ?? NIMBLE_SEARCH_DEFAULTS.maxContentLength;\n\n return tool({\n description:\n 'Search the web with Nimble and return ranked results (title, url, ' +\n 'snippet, and page content) for answering questions about current or ' +\n 'factual information.',\n inputSchema: nimbleSearchInputSchema,\n execute: async (input): Promise<NimbleSearchOutput> => {\n const client = resolveClient(config);\n\n const params: NimbleSearchParams = {\n query: input.query,\n max_results: clamp(input.maxResults ?? maxResults, 1, maxResultsCap),\n search_depth: searchDepth,\n focus: NIMBLE_SEARCH_DEFAULTS.focus, // fixed 'general' in v1\n country,\n locale,\n };\n\n let raw;\n try {\n raw = await client.search(params);\n } catch (err) {\n throw toSearchError(err);\n }\n\n return normalizeSearchResponse(raw, {\n query: input.query,\n maxContentLength,\n });\n },\n });\n}\n"]}
1
+ {"version":3,"sources":["../src/schemas.ts","../src/normalize.ts","../src/errors.ts","../src/nimble-search.ts","../src/nimble-extract.ts"],"names":["readStatus","resolveClient","Nimble","tool"],"mappings":"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","file":"index.js","sourcesContent":["import { z } from 'zod';\n\n/**\n * The tool input the model fills in. Kept deliberately small: the model only\n * chooses the query and (optionally) how many results it wants. All policy\n * (depth, focus, region, caps) is fixed by the developer via the factory\n * config, not by the model.\n */\nexport const nimbleSearchInputSchema = z.object({\n query: z.string().min(1).describe('The web search query.'),\n maxResults: z\n .number()\n .int()\n .positive()\n .optional()\n .describe('How many results to return (clamped to the developer-configured cap).'),\n});\n\nexport type NimbleSearchInput = z.infer<typeof nimbleSearchInputSchema>;\n\n/**\n * v1 exposes only the two non-enterprise depths. `fast` is enterprise-gated and\n * intentionally not offered here.\n */\nexport type SearchDepth = 'lite' | 'deep';\n\n/**\n * Developer-facing factory config. `focus` is fixed to `general` in v1 and is\n * not exposed. `include_answer` and `search_depth: 'fast'` are intentionally\n * absent (enterprise / unverified-entitlement surface).\n */\nexport interface NimbleSearchToolConfig {\n /** Nimble API key. Defaults to `process.env.NIMBLE_API_KEY`. */\n apiKey?: string;\n /** Inject a pre-built / mock Nimble client (tests, advanced users). */\n client?: NimbleSearchClient;\n /** Default number of results when the model doesn't specify. Default 5. */\n maxResults?: number;\n /** Hard upper bound on results, regardless of model request. Default 10. */\n maxResultsCap?: number;\n /** Search depth. Default `lite`. */\n searchDepth?: SearchDepth;\n /** ISO country for result localization. Default `US`. */\n country?: string;\n /** Locale for result localization. Default `en`. */\n locale?: string;\n /** Truncate each result's body to this many characters. Default 10_000. */\n maxContentLength?: number;\n}\n\n/**\n * Structural surface of `@nimble-way/nimble-js`'s `client.search()` that this\n * package relies on. Declared structurally so the scaffold typechecks without\n * pinning to the SDK's generated type names, and so tests can inject a mock.\n *\n * Phase B: reconcile field casing/names against\n * `sdks/nimble-js/checkout/src/` after `./tools/sync-sdk.sh nimble-js`.\n */\nexport interface NimbleSearchParams {\n query: string;\n max_results?: number;\n search_depth?: SearchDepth;\n focus?: string;\n country?: string;\n locale?: string;\n}\n\n/** Metadata for SERP-based results (general/news/location focus). */\nexport interface NimbleSerpMetadata {\n country: string;\n entity_type: string;\n locale: string;\n position: number;\n driver?: string | null;\n}\n\n/** Metadata for WSA-based results (shopping/social/geo focus). */\nexport interface NimbleWsaMetadata {\n agent_name: string;\n}\n\nexport interface NimbleRawSearchResult {\n /** Full page text in `deep`; may be empty in `lite`. */\n content: string;\n description: string;\n title: string;\n url: string;\n /** SERP focus (v1 `general`) yields {@link NimbleSerpMetadata}. */\n metadata: NimbleSerpMetadata | NimbleWsaMetadata;\n /** Platform-specific extras (price, publish_date, …); omitted when none. */\n additional_data?: Record<string, unknown> | null;\n}\n\nexport interface NimbleRawSearchResponse {\n request_id: string;\n results: NimbleRawSearchResult[];\n total_results: number;\n /** Intentionally never surfaced in v1 (include_answer is off). */\n answer?: string | null;\n}\n\nexport interface NimbleSearchClient {\n search(params: NimbleSearchParams): Promise<NimbleRawSearchResponse>;\n}\n\n/** A single normalized result item returned to the model. */\nexport interface NimbleSearchResultItem {\n title: string;\n url: string;\n description?: string;\n content?: string;\n position?: number;\n entityType?: string;\n}\n\n/** The normalized tool output. `answer` is intentionally omitted in v1. */\nexport interface NimbleSearchOutput {\n query: string;\n requestId?: string;\n totalResults?: number;\n results: NimbleSearchResultItem[];\n}\n\n// ── Extract ────────────────────────────────────────────────────────────────\n\n/** Output format for extracted page content. */\nexport type ExtractFormat = 'markdown' | 'html';\n\n/**\n * The extract tool input the model fills in: just the URL to read. All policy\n * (format, region, length cap) is fixed by the developer via the factory config.\n */\nexport const nimbleExtractInputSchema = z.object({\n url: z.string().url().describe('The URL of the web page to extract clean content from.'),\n});\n\nexport type NimbleExtractInput = z.infer<typeof nimbleExtractInputSchema>;\n\n/** Developer-facing factory config for the extract tool. */\nexport interface NimbleExtractToolConfig {\n /** Nimble API key. Defaults to `process.env.NIMBLE_API_KEY`. */\n apiKey?: string;\n /** Inject a pre-built / mock Nimble client (tests, advanced users). */\n client?: NimbleExtractClient;\n /** Content format. Default `markdown`. */\n format?: ExtractFormat;\n /** ISO country for geolocation / proxy selection. */\n country?: string;\n /** Truncate the extracted content to this many characters. Default 50_000. */\n maxContentLength?: number;\n}\n\n/** Params this package sends to the SDK's `client.extract()`. */\nexport interface NimbleExtractParams {\n url: string;\n country?: string;\n /** Which renderings to request; `data.<format>` is populated per entry. */\n formats?: Array<'html' | 'markdown' | 'links'>;\n /** Refines Markdown extraction; `main_content` yields the cleaned article. */\n markdown_backend?: 'full_page' | 'main_content';\n}\n\n/** Structural surface of the SDK extract response data this package consumes. */\nexport interface NimbleRawExtractData {\n /** Markdown rendering of the page (default). */\n markdown?: string;\n /** Raw HTML of the page. */\n html?: string;\n /** Unique URLs found on the page. */\n links?: string[];\n}\n\nexport interface NimbleRawExtractResponse {\n url: string;\n status: string;\n status_code?: number;\n task_id: string;\n data: NimbleRawExtractData;\n warnings?: string[];\n}\n\nexport interface NimbleExtractClient {\n extract(params: NimbleExtractParams): Promise<NimbleRawExtractResponse>;\n}\n\n/** The normalized extract output returned to the model. */\nexport interface NimbleExtractOutput {\n /** The final URL (after redirects). */\n url: string;\n /** Task status reported by Nimble (e.g. `success`). */\n status: string;\n statusCode?: number;\n format: ExtractFormat;\n /** The extracted page content in the requested format, truncated. */\n content: string;\n /** Unique links found on the page, when available. */\n links?: string[];\n}\n","import type {\n ExtractFormat,\n NimbleExtractOutput,\n NimbleRawExtractResponse,\n NimbleRawSearchResponse,\n NimbleRawSearchResult,\n NimbleSearchOutput,\n NimbleSearchResultItem,\n NimbleSerpMetadata,\n} from './schemas';\n\nexport interface NormalizeOptions {\n query: string;\n maxContentLength: number;\n}\n\n/** SERP focus (v1 `general`) carries position + entity_type; WSA does not. */\nfunction isSerpMetadata(\n metadata: NimbleRawSearchResult['metadata'],\n): metadata is NimbleSerpMetadata {\n return 'position' in metadata;\n}\n\nfunction truncate(text: string, max: number): string {\n return text.length > max ? text.slice(0, max) : text;\n}\n\n/**\n * Map a raw `/v1/search` response into the package's normalized output shape.\n *\n * - `description` is the snippet (always present when the API returns one).\n * - `content` is the full page text, present only in `deep` depth (empty in\n * `lite`), truncated to `maxContentLength`.\n * - `position` / `entityType` come from SERP metadata; for WSA results\n * `position` falls back to the array index and `entityType` is omitted.\n * - Results without a URL are dropped (defensive).\n * - `response.answer` is never surfaced in v1.\n */\nexport function normalizeSearchResponse(\n response: NimbleRawSearchResponse,\n options: NormalizeOptions,\n): NimbleSearchOutput {\n const results: NimbleSearchResultItem[] = [];\n\n response.results.forEach((raw, index) => {\n if (!raw.url) return;\n\n const item: NimbleSearchResultItem = {\n title: raw.title ?? '',\n url: raw.url,\n };\n if (raw.description) item.description = raw.description;\n if (raw.content && raw.content.trim().length > 0) {\n item.content = truncate(raw.content, options.maxContentLength);\n }\n\n if (isSerpMetadata(raw.metadata)) {\n item.position = raw.metadata.position;\n item.entityType = raw.metadata.entity_type;\n } else {\n item.position = index + 1;\n }\n\n results.push(item);\n });\n\n return {\n query: options.query,\n requestId: response.request_id,\n totalResults: response.total_results,\n results,\n };\n}\n\nexport interface NormalizeExtractOptions {\n format: ExtractFormat;\n maxContentLength: number;\n}\n\n/**\n * Map a raw `/v1/extract` response into the package's normalized extract shape.\n *\n * - `content` is `data.markdown` (default) or `data.html`, falling back to the\n * other when the requested one is empty, truncated to `maxContentLength`.\n * - `format` reflects the rendering actually returned, which may differ from\n * the requested format when the fallback is used.\n * - `links` is surfaced when present; everything else (browser actions, network\n * captures, screenshots) is intentionally dropped.\n */\nexport function normalizeExtractResponse(\n response: NimbleRawExtractResponse,\n options: NormalizeExtractOptions,\n): NimbleExtractOutput {\n const data = response.data;\n const otherFormat: ExtractFormat = options.format === 'html' ? 'markdown' : 'html';\n const primary = options.format === 'html' ? data.html : data.markdown;\n const fallback = options.format === 'html' ? data.markdown : data.html;\n\n // Report the format that actually populated `content`. We request one\n // rendering, but if the API returns it empty we fall back to the other — and\n // the model must be told which format it received, not the one we asked for.\n const usedFormat: ExtractFormat =\n primary && primary.length > 0\n ? options.format\n : fallback && fallback.length > 0\n ? otherFormat\n : options.format;\n const content = truncate(primary || fallback || '', options.maxContentLength);\n\n const out: NimbleExtractOutput = {\n url: response.url,\n status: response.status,\n format: usedFormat,\n content,\n };\n if (typeof response.status_code === 'number') out.statusCode = response.status_code;\n if (data.links && data.links.length > 0) out.links = data.links;\n return out;\n}\n","/**\n * Thrown when the tool is invoked without a resolvable API key (no `apiKey`\n * config and no `NIMBLE_API_KEY` in the environment) and no injected client.\n * Raised at execute time, not at factory-construction time, so the tool can be\n * constructed in environments without a key (e.g. unit tests, type-checking).\n */\nexport class NimbleConfigError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'NimbleConfigError';\n }\n}\n\n/**\n * Wraps an error surfaced by the Nimble client / API during a search call,\n * preserving the HTTP status when available. The AI SDK surfaces a thrown\n * tool error back to the model as a tool-call failure.\n */\nexport class NimbleSearchError extends Error {\n readonly status?: number;\n\n constructor(message: string, options?: { status?: number; cause?: unknown }) {\n super(message, options?.cause !== undefined ? { cause: options.cause } : undefined);\n this.name = 'NimbleSearchError';\n this.status = options?.status;\n }\n}\n\n/**\n * Wraps an error surfaced by the Nimble client / API during an extract call,\n * preserving the HTTP status when available.\n */\nexport class NimbleExtractError extends Error {\n readonly status?: number;\n\n constructor(message: string, options?: { status?: number; cause?: unknown }) {\n super(message, options?.cause !== undefined ? { cause: options.cause } : undefined);\n this.name = 'NimbleExtractError';\n this.status = options?.status;\n }\n}\n","import { tool } from 'ai';\nimport { Nimble } from '@nimble-way/nimble-js';\nimport { nimbleSearchInputSchema } from './schemas';\nimport type {\n NimbleSearchClient,\n NimbleSearchOutput,\n NimbleSearchParams,\n NimbleSearchToolConfig,\n} from './schemas';\nimport { normalizeSearchResponse } from './normalize';\nimport { NimbleConfigError, NimbleSearchError } from './errors';\n\n/** v1 factory defaults. `focus` is fixed to `general` and not user-exposed. */\nexport const NIMBLE_SEARCH_DEFAULTS = {\n maxResults: 5,\n maxResultsCap: 10,\n searchDepth: 'lite',\n country: 'US',\n locale: 'en',\n maxContentLength: 10_000,\n focus: 'general',\n} as const;\n\nfunction clamp(value: number, min: number, max: number): number {\n return Math.min(Math.max(value, min), max);\n}\n\nfunction readStatus(err: unknown): number | undefined {\n if (typeof err === 'object' && err !== null && 'status' in err) {\n const status = (err as { status?: unknown }).status;\n return typeof status === 'number' ? status : undefined;\n }\n return undefined;\n}\n\nfunction toSearchError(err: unknown): NimbleSearchError {\n if (err instanceof NimbleSearchError) return err;\n const message = err instanceof Error ? err.message : String(err);\n return new NimbleSearchError(`Nimble search failed: ${message}`, {\n status: readStatus(err),\n cause: err,\n });\n}\n\nfunction resolveClient(config: NimbleSearchToolConfig): NimbleSearchClient {\n if (config.client) return config.client;\n const apiKey = config.apiKey ?? process.env.NIMBLE_API_KEY;\n if (!apiKey) {\n throw new NimbleConfigError(\n 'Missing Nimble API key: set NIMBLE_API_KEY or pass { apiKey } to nimbleSearch().',\n );\n }\n return new Nimble({ apiKey }) as unknown as NimbleSearchClient;\n}\n\n/**\n * Create a ready-made Vercel AI SDK web-search tool backed by Nimble Search\n * (`@nimble-way/nimble-js` → `POST /v1/search`).\n *\n * The model only chooses `{ query, maxResults? }`; all policy (depth, focus,\n * region, caps) is fixed by `config` — mirroring the Exa `webSearch()` shape.\n *\n * @example\n * ```ts\n * import { generateText } from 'ai';\n * import { nimbleSearch } from '@nimble-way/ai-sdk';\n *\n * const { text } = await generateText({\n * model: 'anthropic/claude-sonnet-4.6',\n * prompt: 'What are the latest Nimble release notes?',\n * tools: { webSearch: nimbleSearch({ searchDepth: 'lite', maxResults: 5 }) },\n * });\n * ```\n */\nexport function nimbleSearch(config: NimbleSearchToolConfig = {}) {\n const maxResults = config.maxResults ?? NIMBLE_SEARCH_DEFAULTS.maxResults;\n const maxResultsCap = config.maxResultsCap ?? NIMBLE_SEARCH_DEFAULTS.maxResultsCap;\n const searchDepth = config.searchDepth ?? NIMBLE_SEARCH_DEFAULTS.searchDepth;\n const country = config.country ?? NIMBLE_SEARCH_DEFAULTS.country;\n const locale = config.locale ?? NIMBLE_SEARCH_DEFAULTS.locale;\n const maxContentLength = config.maxContentLength ?? NIMBLE_SEARCH_DEFAULTS.maxContentLength;\n\n return tool({\n description:\n 'Search the web with Nimble and return ranked results (title, url, ' +\n 'snippet, and page content) for answering questions about current or ' +\n 'factual information.',\n inputSchema: nimbleSearchInputSchema,\n execute: async (input): Promise<NimbleSearchOutput> => {\n const client = resolveClient(config);\n\n const params: NimbleSearchParams = {\n query: input.query,\n max_results: clamp(input.maxResults ?? maxResults, 1, maxResultsCap),\n search_depth: searchDepth,\n focus: NIMBLE_SEARCH_DEFAULTS.focus, // fixed 'general' in v1\n country,\n locale,\n };\n\n let raw;\n try {\n raw = await client.search(params);\n } catch (err) {\n throw toSearchError(err);\n }\n\n return normalizeSearchResponse(raw, {\n query: input.query,\n maxContentLength,\n });\n },\n });\n}\n","import { tool } from 'ai';\nimport { Nimble } from '@nimble-way/nimble-js';\nimport { nimbleExtractInputSchema } from './schemas';\nimport type {\n ExtractFormat,\n NimbleExtractClient,\n NimbleExtractOutput,\n NimbleExtractParams,\n NimbleExtractToolConfig,\n} from './schemas';\nimport { normalizeExtractResponse } from './normalize';\nimport { NimbleConfigError, NimbleExtractError } from './errors';\n\n/** v1 extract defaults. */\nexport const NIMBLE_EXTRACT_DEFAULTS: { format: ExtractFormat; maxContentLength: number } = {\n format: 'markdown',\n maxContentLength: 50_000,\n};\n\nfunction readStatus(err: unknown): number | undefined {\n if (typeof err === 'object' && err !== null && 'status' in err) {\n const status = (err as { status?: unknown }).status;\n return typeof status === 'number' ? status : undefined;\n }\n return undefined;\n}\n\nfunction toExtractError(err: unknown): NimbleExtractError {\n if (err instanceof NimbleExtractError) return err;\n const message = err instanceof Error ? err.message : String(err);\n return new NimbleExtractError(`Nimble extract failed: ${message}`, {\n status: readStatus(err),\n cause: err,\n });\n}\n\nfunction resolveClient(config: NimbleExtractToolConfig): NimbleExtractClient {\n if (config.client) return config.client;\n const apiKey = config.apiKey ?? process.env.NIMBLE_API_KEY;\n if (!apiKey) {\n throw new NimbleConfigError(\n 'Missing Nimble API key: set NIMBLE_API_KEY or pass { apiKey } to nimbleExtract().',\n );\n }\n return new Nimble({ apiKey }) as unknown as NimbleExtractClient;\n}\n\n/**\n * Create a Vercel AI SDK tool that extracts clean, readable content from a web\n * page with Nimble (`@nimble-way/nimble-js` → `POST /v1/extract`).\n *\n * The model only chooses `{ url }`; format, region, and length cap are fixed by\n * `config`. Mirrors {@link nimbleSearch}'s ergonomics.\n *\n * @example\n * ```ts\n * import { generateText } from 'ai';\n * import { nimbleExtract } from '@nimble-way/ai-sdk';\n *\n * const { text } = await generateText({\n * model: 'openai/gpt-4o-mini',\n * prompt: 'Summarize https://nimbleway.com',\n * tools: { extract: nimbleExtract() },\n * });\n * ```\n */\nexport function nimbleExtract(config: NimbleExtractToolConfig = {}) {\n const format = config.format ?? NIMBLE_EXTRACT_DEFAULTS.format;\n const maxContentLength = config.maxContentLength ?? NIMBLE_EXTRACT_DEFAULTS.maxContentLength;\n const country = config.country;\n\n return tool({\n description:\n 'Fetch a web page by URL with Nimble and return its clean, readable content ' +\n '(markdown or HTML) — use this to read, quote, or summarize a specific page.',\n inputSchema: nimbleExtractInputSchema,\n execute: async (input): Promise<NimbleExtractOutput> => {\n const client = resolveClient(config);\n\n // Request both renderings (preferred format first) plus links. A rendering\n // is only produced when requested via `formats`, so asking for both lets an\n // empty primary fall back to the other; normalizeExtractResponse reports\n // whichever rendering actually populated the content.\n const params: NimbleExtractParams = {\n url: input.url,\n formats:\n format === 'html' ? ['html', 'markdown', 'links'] : ['markdown', 'html', 'links'],\n };\n if (country) params.country = country;\n // `main_content` returns the cleaned article body rather than the full page.\n if (format === 'markdown') params.markdown_backend = 'main_content';\n\n let raw;\n try {\n raw = await client.extract(params);\n } catch (err) {\n throw toExtractError(err);\n }\n\n return normalizeExtractResponse(raw, { format, maxContentLength });\n },\n });\n}\n"]}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@nimble-way/ai-sdk",
3
- "version": "0.1.0",
4
- "description": "Nimble Web Search as a ready-made tool for the Vercel AI SDK.",
3
+ "version": "0.2.1",
4
+ "description": "Nimble Web Search and Extract as ready-made tools for the Vercel AI SDK.",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
7
7
  "author": "Nimble",
@@ -20,6 +20,7 @@
20
20
  "ai",
21
21
  "web-search",
22
22
  "search",
23
+ "extract",
23
24
  "tool",
24
25
  "llm",
25
26
  "agent"