webseek 0.3.0 → 0.5.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,10 +1,122 @@
1
1
  #!/usr/bin/env node
2
- import { a as coerceGeminiBackend, c as runSearch, d as formatError, l as WebseekError, o as coerceMaxResults, s as coerceProvider, t as createWebSearchTool, u as errorExitCode } from "../tools-DBF9nCIs.mjs";
2
+ import { a as coerceGeminiBackend, c as runSearch, d as errorExitCode, f as formatError, i as PROVIDER_NAMES, o as coerceMaxResults, s as coerceProvider, t as createWebSearchTool, u as WebseekError } from "../tools-1ZllUYBx.mjs";
3
+ import { z } from "zod";
3
4
  import { readFileSync } from "node:fs";
4
5
  import { dirname, join } from "node:path";
5
6
  import { Command } from "commander";
6
7
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
7
8
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
9
+ import { appendFile, mkdir, readFile } from "node:fs/promises";
10
+ import { homedir } from "node:os";
11
+ //#region src/stats/usage-log.ts
12
+ /**
13
+ * The local usage log: one JSON line per successful search, appended by the
14
+ * CLI and the MCP server and read back by `webseek stats`.
15
+ *
16
+ * Records hold what a search consumed and cost — never the query text — so the
17
+ * log is safe to keep around. The library entry point never writes to it.
18
+ */
19
+ const USAGE_LOG_FILE = "usage.jsonl";
20
+ const RECORD_VERSION = 1;
21
+ /**
22
+ * The directory the usage log lives in: `WEBSEEK_DATA_DIR`, else
23
+ * `$XDG_DATA_HOME/webseek`, else `~/.local/share/webseek`.
24
+ */
25
+ function resolveDataDir(params = {}) {
26
+ const env = params.env ?? process.env;
27
+ if (env.WEBSEEK_DATA_DIR) return env.WEBSEEK_DATA_DIR;
28
+ if (env.XDG_DATA_HOME) return join(env.XDG_DATA_HOME, "webseek");
29
+ return join(homedir(), ".local", "share", "webseek");
30
+ }
31
+ function resolveUsageLogPath(params = {}) {
32
+ return join(resolveDataDir(params), USAGE_LOG_FILE);
33
+ }
34
+ /** Logging is on unless `WEBSEEK_USAGE_LOG` is `0`, `false` or `off`. */
35
+ function isUsageLogEnabled(params = {}) {
36
+ const value = (params.env ?? process.env).WEBSEEK_USAGE_LOG?.trim().toLowerCase();
37
+ return value !== "0" && value !== "false" && value !== "off";
38
+ }
39
+ function toUsageRecord(params) {
40
+ const { result } = params;
41
+ return {
42
+ v: RECORD_VERSION,
43
+ at: params.now ?? Date.now(),
44
+ provider: result.provider,
45
+ model: result.model,
46
+ usage: result.usage,
47
+ cost: result.cost
48
+ };
49
+ }
50
+ /**
51
+ * Append a search to the usage log. Never throws: a failed write is reported
52
+ * through `onError` so it cannot fail (or corrupt the output of) the search.
53
+ */
54
+ async function appendUsageRecord(params) {
55
+ if (!isUsageLogEnabled({ env: params.env })) return;
56
+ try {
57
+ const dir = resolveDataDir({ env: params.env });
58
+ await mkdir(dir, {
59
+ recursive: true,
60
+ mode: 448
61
+ });
62
+ const record = toUsageRecord({
63
+ result: params.result,
64
+ now: params.now
65
+ });
66
+ await appendFile(join(dir, USAGE_LOG_FILE), `${JSON.stringify(record)}\n`, {
67
+ encoding: "utf8",
68
+ mode: 384
69
+ });
70
+ } catch (error) {
71
+ params.onError?.(error);
72
+ }
73
+ }
74
+ /** Read every valid record from the usage log; a missing log reads as empty. */
75
+ async function readUsageRecords(params = {}) {
76
+ let text;
77
+ try {
78
+ text = await readFile(resolveUsageLogPath(params), "utf8");
79
+ } catch (error) {
80
+ if (error.code === "ENOENT") return [];
81
+ throw error;
82
+ }
83
+ return parseUsageRecords(text);
84
+ }
85
+ const usageRecordSchema = z.looseObject({
86
+ v: z.literal(RECORD_VERSION),
87
+ at: z.number(),
88
+ provider: z.enum(PROVIDER_NAMES),
89
+ model: z.string().optional(),
90
+ usage: z.looseObject({
91
+ inputTokens: z.number().optional(),
92
+ cachedInputTokens: z.number().optional(),
93
+ outputTokens: z.number().optional(),
94
+ totalTokens: z.number().optional(),
95
+ searchCalls: z.number()
96
+ }).optional(),
97
+ cost: z.looseObject({
98
+ totalUsd: z.number(),
99
+ tokensUsd: z.number(),
100
+ searchUsd: z.number()
101
+ }).optional()
102
+ });
103
+ /** Parse JSONL, skipping blank, malformed and unknown-version lines. */
104
+ function parseUsageRecords(text) {
105
+ const records = [];
106
+ for (const line of text.split("\n")) {
107
+ if (!line.trim()) continue;
108
+ let json;
109
+ try {
110
+ json = JSON.parse(line);
111
+ } catch {
112
+ continue;
113
+ }
114
+ const parsed = usageRecordSchema.safeParse(json);
115
+ if (parsed.success) records.push(parsed.data);
116
+ }
117
+ return records;
118
+ }
119
+ //#endregion
8
120
  //#region src/mcp/server.ts
9
121
  /**
10
122
  * MCP server mode: exposes the `web_search` tool over the stdio transport so
@@ -17,7 +129,10 @@ async function startMcpServer(params) {
17
129
  name: "webseek",
18
130
  version: params.version
19
131
  });
20
- const tool = createWebSearchTool();
132
+ const tool = createWebSearchTool({ onResult: (result) => appendUsageRecord({
133
+ result,
134
+ onError: (error) => params.logger.warn(`could not record usage: ${formatError(error)}`)
135
+ }) });
21
136
  server.registerTool(tool.name, tool.config, tool.handler);
22
137
  const transport = new StdioServerTransport();
23
138
  await server.connect(transport);
@@ -102,7 +217,10 @@ function formatJson(result) {
102
217
  results: result.results,
103
218
  answer: result.answer,
104
219
  citations: result.citations,
105
- searchQueries: result.searchQueries
220
+ searchQueries: result.searchQueries,
221
+ model: result.model,
222
+ usage: result.usage,
223
+ cost: result.cost
106
224
  };
107
225
  if (result.raw !== void 0) payload.raw = result.raw;
108
226
  return JSON.stringify(payload, null, 2);
@@ -128,8 +246,29 @@ function formatText(result) {
128
246
  lines.push("");
129
247
  }
130
248
  if (result.searchQueries.length > 0) lines.push(`Searches: ${result.searchQueries.join(" | ")}`);
249
+ if (result.usage) lines.push(formatUsage({
250
+ usage: result.usage,
251
+ cost: result.cost
252
+ }));
131
253
  return lines.join("\n").trimEnd();
132
254
  }
255
+ function formatUsage(params) {
256
+ const { usage, cost } = params;
257
+ const parts = [];
258
+ if (usage.inputTokens !== void 0 || usage.outputTokens !== void 0) {
259
+ const input = usage.inputTokens ?? 0;
260
+ const output = usage.outputTokens ?? 0;
261
+ const total = usage.totalTokens ?? input + output;
262
+ parts.push(`${total.toLocaleString("en-US")} tokens (${input.toLocaleString("en-US")} in, ${output.toLocaleString("en-US")} out)`);
263
+ }
264
+ parts.push(`${usage.searchCalls} search${usage.searchCalls === 1 ? "" : "es"}`);
265
+ parts.push(cost === void 0 ? "cost unknown" : `~${formatUsd(cost.totalUsd)}`);
266
+ return `Usage: ${parts.join(", ")}`;
267
+ }
268
+ function formatUsd(value) {
269
+ if (value > 0 && value < 5e-5) return "<$0.0001";
270
+ return `$${value.toFixed(4)}`;
271
+ }
133
272
  //#endregion
134
273
  //#region src/cli/commands/search.ts
135
274
  /**
@@ -152,10 +291,15 @@ function toSearchRequest(params) {
152
291
  };
153
292
  }
154
293
  async function runSearchCommand(params) {
155
- const result = await runSearch(toSearchRequest({
294
+ const request = toSearchRequest({
156
295
  queryParts: params.queryParts,
157
296
  options: params.options
158
- }));
297
+ });
298
+ const result = await runSearch(request);
299
+ await appendUsageRecord({
300
+ result,
301
+ onError: (error) => params.logger.warn(`could not record usage: ${formatError(error)}`)
302
+ });
159
303
  params.logger.result(formatResult({
160
304
  result,
161
305
  json: Boolean(params.options.json)
@@ -175,6 +319,293 @@ function registerSearchCommand(program) {
175
319
  }));
176
320
  }
177
321
  //#endregion
322
+ //#region src/stats/aggregate.ts
323
+ /**
324
+ * `--all` → all time; `--days N` → since local midnight N-1 days ago (0 or 1
325
+ * mean today); `--year Y` → that calendar year; default → this year so far.
326
+ */
327
+ function resolveStatsRange(params = {}) {
328
+ const now = params.now ?? /* @__PURE__ */ new Date();
329
+ const to = now.getTime() + 1;
330
+ if (params.all) return {
331
+ from: void 0,
332
+ to,
333
+ label: "all time"
334
+ };
335
+ if (params.days !== void 0) {
336
+ const from = new Date(now.getFullYear(), now.getMonth(), now.getDate());
337
+ from.setDate(from.getDate() - Math.max(0, params.days - 1));
338
+ return {
339
+ from: from.getTime(),
340
+ to,
341
+ label: params.days <= 1 ? "today" : `last ${params.days} days`
342
+ };
343
+ }
344
+ const year = params.year ?? now.getFullYear();
345
+ const isCurrentYear = year === now.getFullYear();
346
+ return {
347
+ from: new Date(year, 0, 1).getTime(),
348
+ to: isCurrentYear ? to : new Date(year + 1, 0, 1).getTime(),
349
+ label: isCurrentYear ? `${year} so far` : String(year)
350
+ };
351
+ }
352
+ function aggregateUsage(params) {
353
+ const { range } = params;
354
+ const stats = {
355
+ range: {
356
+ from: range.from,
357
+ to: range.to
358
+ },
359
+ searches: 0,
360
+ searchCalls: 0,
361
+ tokens: {
362
+ input: 0,
363
+ cachedInput: 0,
364
+ output: 0,
365
+ total: 0
366
+ },
367
+ cost: {
368
+ totalUsd: 0,
369
+ tokensUsd: 0,
370
+ searchUsd: 0
371
+ },
372
+ unpricedSearches: 0,
373
+ activeDays: 0,
374
+ models: []
375
+ };
376
+ const days = /* @__PURE__ */ new Set();
377
+ const models = /* @__PURE__ */ new Map();
378
+ for (const record of params.records) {
379
+ if (!((range.from === void 0 || record.at >= range.from) && record.at < range.to) || params.provider !== void 0 && record.provider !== params.provider) continue;
380
+ addRecord({
381
+ stats,
382
+ models,
383
+ record
384
+ });
385
+ const at = new Date(record.at);
386
+ days.add(`${at.getFullYear()}-${at.getMonth()}-${at.getDate()}`);
387
+ }
388
+ stats.activeDays = days.size;
389
+ stats.models = [...models.values()].toSorted((a, b) => b.costUsd - a.costUsd || b.searches - a.searches);
390
+ return stats;
391
+ }
392
+ function addRecord(params) {
393
+ const { stats, record } = params;
394
+ const usage = record.usage;
395
+ const input = usage?.inputTokens ?? 0;
396
+ const output = usage?.outputTokens ?? 0;
397
+ const total = usage?.totalTokens ?? input + output;
398
+ const searchCalls = usage?.searchCalls ?? 0;
399
+ stats.searches += 1;
400
+ stats.searchCalls += searchCalls;
401
+ stats.tokens.input += input;
402
+ stats.tokens.cachedInput += usage?.cachedInputTokens ?? 0;
403
+ stats.tokens.output += output;
404
+ stats.tokens.total += total;
405
+ if (record.cost) {
406
+ stats.cost.totalUsd += record.cost.totalUsd;
407
+ stats.cost.tokensUsd += record.cost.tokensUsd;
408
+ stats.cost.searchUsd += record.cost.searchUsd;
409
+ } else stats.unpricedSearches += 1;
410
+ const key = `${record.provider}/${record.model ?? ""}`;
411
+ const model = params.models.get(key) ?? {
412
+ provider: record.provider,
413
+ model: record.model,
414
+ searches: 0,
415
+ searchCalls: 0,
416
+ tokens: 0,
417
+ costUsd: 0,
418
+ unpricedSearches: 0
419
+ };
420
+ model.searches += 1;
421
+ model.searchCalls += searchCalls;
422
+ model.tokens += total;
423
+ model.costUsd += record.cost?.totalUsd ?? 0;
424
+ if (!record.cost) model.unpricedSearches += 1;
425
+ params.models.set(key, model);
426
+ }
427
+ //#endregion
428
+ //#region src/stats/render.ts
429
+ /**
430
+ * Renders aggregated usage as the `webseek stats` text report, laid out after
431
+ * `opencode stats`: a summary by default, plus COST & TOKENS and MODELS
432
+ * sections on request.
433
+ */
434
+ function renderStats(params) {
435
+ const { stats } = params;
436
+ const detailed = params.cost || params.models;
437
+ const header = `webseek stats · ${params.label} · ${params.scope}`;
438
+ const lines = [];
439
+ if (detailed) lines.push(`${params.label} · ${params.scope}`);
440
+ else if (stats.searches === 0) lines.push(header, "", "no searches in this range");
441
+ else {
442
+ lines.push(header, "", `${plural({
443
+ value: stats.searches,
444
+ singular: "search",
445
+ pluralForm: "searches"
446
+ })} · ${plural({
447
+ value: stats.searchCalls,
448
+ singular: "search call"
449
+ })} · ${plural({
450
+ value: stats.tokens.total,
451
+ singular: "token"
452
+ })}`, `~${formatUsd(stats.cost.totalUsd)} estimated · ${plural({
453
+ value: stats.activeDays,
454
+ singular: "active day"
455
+ })}`);
456
+ if (stats.unpricedSearches > 0) lines.push(`${plural({
457
+ value: stats.unpricedSearches,
458
+ singular: "search",
459
+ pluralForm: "searches"
460
+ })} without a price estimate`);
461
+ }
462
+ if (params.cost) lines.push("", ...renderCost(stats));
463
+ if (params.models) lines.push("", ...renderModels({
464
+ models: stats.models,
465
+ limit: params.limit
466
+ }));
467
+ return lines.join("\n");
468
+ }
469
+ function renderCost(stats) {
470
+ const rows = [
471
+ ["cost", `~${formatUsd(stats.cost.totalUsd)}`],
472
+ [" tokens", formatUsd(stats.cost.tokensUsd)],
473
+ [" search fees", formatUsd(stats.cost.searchUsd)],
474
+ ["input", compact(stats.tokens.input)],
475
+ ["cached input", compact(stats.tokens.cachedInput)],
476
+ ["output", compact(stats.tokens.output)],
477
+ ["search calls", compact(stats.searchCalls)]
478
+ ];
479
+ if (stats.unpricedSearches > 0) rows.push(["unpriced searches", compact(stats.unpricedSearches)]);
480
+ return ["COST & TOKENS", ...rows.map(([label, value]) => ` ${label.padEnd(20)}${value}`)];
481
+ }
482
+ function renderModels(params) {
483
+ const { models, limit } = params;
484
+ if (models.length === 0) return ["MODELS", " no searches"];
485
+ const shown = limit === void 0 ? models : models.slice(0, limit);
486
+ const rest = models.length - shown.length;
487
+ return [
488
+ "MODELS",
489
+ tableRow({
490
+ name: "model",
491
+ searches: "searches",
492
+ tokens: "tokens",
493
+ cost: "cost"
494
+ }),
495
+ ...shown.map((model) => tableRow({
496
+ name: modelName(model),
497
+ searches: compact(model.searches),
498
+ tokens: compact(model.tokens),
499
+ cost: model.unpricedSearches === model.searches ? "-" : formatUsd(model.costUsd)
500
+ })),
501
+ ...rest > 0 ? ["", `+${rest.toLocaleString("en-US")} more model${rest === 1 ? "" : "s"}`] : []
502
+ ];
503
+ }
504
+ function modelName(model) {
505
+ const name = model.model?.replace(/\p{Cc}/gu, "");
506
+ return name ? `${model.provider}/${name}` : model.provider;
507
+ }
508
+ function tableRow(cells) {
509
+ return `${(cells.name.length <= 34 ? cells.name : `${cells.name.slice(0, 33)}…`).padEnd(34)}${cells.searches.padStart(10)}${cells.tokens.padStart(10)}${cells.cost.padStart(12)}`;
510
+ }
511
+ /** 1234 → "1.2k", 2_000_000 → "2m". */
512
+ function compact(value) {
513
+ if (value >= 1e9) return `${oneDecimal(value / 1e9)}b`;
514
+ if (value >= 1e6) return `${oneDecimal(value / 1e6)}m`;
515
+ if (value >= 1e3) return `${oneDecimal(value / 1e3)}k`;
516
+ return Math.round(value).toLocaleString("en-US");
517
+ }
518
+ function oneDecimal(value) {
519
+ return value.toFixed(1).replace(/\.0$/, "");
520
+ }
521
+ function plural(params) {
522
+ const noun = params.value === 1 ? params.singular : params.pluralForm ?? `${params.singular}s`;
523
+ return `${compact(params.value)} ${noun}`;
524
+ }
525
+ //#endregion
526
+ //#region src/cli/commands/stats.ts
527
+ /** Validate CLI options into a stats request. Pure apart from reading `now`. */
528
+ function toStatsRequest(params) {
529
+ const { options } = params;
530
+ if ([
531
+ options.days !== void 0,
532
+ options.year !== void 0,
533
+ options.all
534
+ ].filter(Boolean).length > 1) throw new WebseekError({
535
+ code: "invalid_usage",
536
+ message: "--days, --year, and --all cannot be combined."
537
+ });
538
+ return {
539
+ range: resolveStatsRange({
540
+ days: options.days === void 0 ? void 0 : parseInteger({
541
+ name: "days",
542
+ value: options.days,
543
+ min: 0,
544
+ max: 36500
545
+ }),
546
+ year: options.year === void 0 ? void 0 : parseInteger({
547
+ name: "year",
548
+ value: options.year,
549
+ min: 1970,
550
+ max: 9999
551
+ }),
552
+ all: options.all,
553
+ now: params.now
554
+ }),
555
+ provider: options.provider === void 0 ? void 0 : coerceProvider(options.provider),
556
+ cost: Boolean(options.cost || options.full),
557
+ models: Boolean(options.models || options.full),
558
+ limit: options.limit === void 0 ? void 0 : parseInteger({
559
+ name: "limit",
560
+ value: options.limit,
561
+ min: 1,
562
+ max: Number.MAX_SAFE_INTEGER
563
+ }),
564
+ json: Boolean(options.json)
565
+ };
566
+ }
567
+ function parseInteger(params) {
568
+ const { name, value, min, max } = params;
569
+ const parsed = Number(value);
570
+ if (!/^\d+$/.test(value.trim()) || !Number.isSafeInteger(parsed) || parsed < min || parsed > max) throw new WebseekError({
571
+ code: "invalid_usage",
572
+ message: `--${name} must be an integer between ${min} and ${max}.`
573
+ });
574
+ return parsed;
575
+ }
576
+ async function runStatsCommand(params) {
577
+ const request = toStatsRequest({ options: params.options });
578
+ const stats = aggregateUsage({
579
+ records: await readUsageRecords({ env: params.env }),
580
+ range: request.range,
581
+ provider: request.provider
582
+ });
583
+ if (request.json) {
584
+ const models = request.limit === void 0 ? stats.models : stats.models.slice(0, request.limit);
585
+ params.logger.result(JSON.stringify({
586
+ ...stats,
587
+ models
588
+ }, null, 2));
589
+ return;
590
+ }
591
+ params.logger.result(renderStats({
592
+ stats,
593
+ label: request.range.label,
594
+ scope: request.provider ?? "all providers",
595
+ cost: request.cost,
596
+ models: request.models,
597
+ limit: request.limit
598
+ }));
599
+ }
600
+ function registerStatsCommand(program) {
601
+ program.command("stats").description("Show usage and estimated cost of the searches recorded locally").option("--days <n>", "show the last N days; 0 means today").option("--year <year>", "show a calendar year").option("--all", "show lifetime statistics").option("-p, --provider <name>", "only count one provider: openai | google | gemini").option("--cost", "show cost and token details").option("--models", "show usage per model").option("--full", "show every detailed section").option("--limit <n>", "number of rows in the models section").option("--json", "output statistics as JSON").action(wrapCommand(async ({ logger }, options) => {
602
+ await runStatsCommand({
603
+ logger,
604
+ options
605
+ });
606
+ }));
607
+ }
608
+ //#endregion
178
609
  //#region src/cli/index.ts
179
610
  /**
180
611
  * webseek CLI entry point.
@@ -182,6 +613,7 @@ function registerSearchCommand(program) {
182
613
  * Two modes:
183
614
  * webseek <query> --provider <name> run a one-off web search (root command)
184
615
  * webseek mcp start the MCP server (stdio)
616
+ * webseek stats summarize the locally recorded usage
185
617
  */
186
618
  function readPackageVersion(dir) {
187
619
  try {
@@ -203,12 +635,13 @@ function getVersion() {
203
635
  function buildProgram() {
204
636
  const program = new Command();
205
637
  const version = getVersion();
206
- program.name("webseek").description("Unified multi-provider web search (CLI + MCP server)").version(version, "-v, --version", "Show version");
638
+ program.name("webseek").description("Unified multi-provider web search (CLI + MCP server)").version(version, "-v, --version", "Show version").enablePositionalOptions();
207
639
  registerSearchCommand(program);
208
640
  registerMcpCommand({
209
641
  program,
210
642
  version
211
643
  });
644
+ registerStatsCommand(program);
212
645
  return program;
213
646
  }
214
647
  async function main() {
package/dist/index.cjs CHANGED
@@ -1,10 +1,11 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_tools = require("./tools-Ci1EDrCT.cjs");
2
+ const require_tools = require("./tools-Brr43TiA.cjs");
3
3
  exports.GEMINI_BACKENDS = require_tools.GEMINI_BACKENDS;
4
4
  exports.PROVIDER_NAMES = require_tools.PROVIDER_NAMES;
5
5
  exports.WebseekError = require_tools.WebseekError;
6
6
  exports.createWebSearchTool = require_tools.createWebSearchTool;
7
7
  exports.errorExitCode = require_tools.errorExitCode;
8
+ exports.estimateCost = require_tools.estimateCost;
8
9
  exports.formatError = require_tools.formatError;
9
10
  exports.runSearch = require_tools.runSearch;
10
11
  exports.webSearchInputShape = require_tools.webSearchInputShape;
package/dist/index.d.cts CHANGED
@@ -41,6 +41,24 @@ interface Citation {
41
41
  startIndex?: number;
42
42
  endIndex?: number;
43
43
  }
44
+ /** What a search consumed. Token counts are absent for SERP providers. */
45
+ interface SearchUsage {
46
+ /** Prompt tokens, including cached ones and fetched search content. */
47
+ inputTokens?: number;
48
+ /** The part of `inputTokens` served from the prompt cache. */
49
+ cachedInputTokens?: number;
50
+ /** Generated tokens, including reasoning/thinking tokens. */
51
+ outputTokens?: number;
52
+ totalTokens?: number;
53
+ /** Searches the provider ran (web search tool calls, grounding queries, or SERP requests). */
54
+ searchCalls: number;
55
+ }
56
+ /** Estimated list-price cost of a search in USD (free tiers not applied). */
57
+ interface SearchCost {
58
+ totalUsd: number;
59
+ tokensUsd: number;
60
+ searchUsd: number;
61
+ }
44
62
  /** The normalized result shape returned by every provider. */
45
63
  interface NormalizedSearchResult {
46
64
  provider: ProviderName;
@@ -53,6 +71,12 @@ interface NormalizedSearchResult {
53
71
  citations: Citation[];
54
72
  /** Queries the provider actually ran (grounded providers). */
55
73
  searchQueries: string[];
74
+ /** The model that served the search (grounded providers). */
75
+ model?: string;
76
+ /** Tokens and searches consumed (always set by the built-in providers). */
77
+ usage?: SearchUsage;
78
+ /** Estimated cost; absent when the model's price is unknown. */
79
+ cost?: SearchCost;
56
80
  /** The provider's raw response, included only when requested. */
57
81
  raw?: unknown;
58
82
  }
@@ -75,8 +99,8 @@ interface SearchProvider {
75
99
  }
76
100
  //#endregion
77
101
  //#region src/lib/search.d.ts
78
- declare const PROVIDER_NAMES: readonly ["openai", "google", "gemini"];
79
- declare const GEMINI_BACKENDS: readonly ["gemini-api", "vertex-express"];
102
+ export declare const PROVIDER_NAMES: readonly ["openai", "google", "gemini"];
103
+ export declare const GEMINI_BACKENDS: readonly ["gemini-api", "vertex-express"];
80
104
  interface RunSearchParams {
81
105
  provider: ProviderName;
82
106
  query: string;
@@ -87,7 +111,28 @@ interface RunSearchParams {
87
111
  env?: Env;
88
112
  fetchImpl?: typeof fetch;
89
113
  }
90
- declare function runSearch(params: RunSearchParams): Promise<NormalizedSearchResult>;
114
+ export declare function runSearch(params: RunSearchParams): Promise<NormalizedSearchResult>;
115
+ //#endregion
116
+ //#region src/pricing/pricing.d.ts
117
+ interface EstimateCostParams {
118
+ provider: ProviderName;
119
+ /** The model that served the search (ignored for SERP providers). */
120
+ model?: string;
121
+ /**
122
+ * Priced instead when `model` is not in the table — typically the requested
123
+ * model, since the served id may be a snapshot or variant the table lacks.
124
+ */
125
+ fallbackModel?: string;
126
+ usage: SearchUsage;
127
+ /** Price as of this moment (rates can change on a set date); defaults to now. */
128
+ now?: Date;
129
+ }
130
+ /**
131
+ * Estimate what a search cost at list price, or `undefined` when it cannot be
132
+ * priced: the model is not in this table (e.g. newer than it), or an LLM-backed
133
+ * provider reported no token counts (a search-fee-only figure would understate).
134
+ */
135
+ export declare function estimateCost(params: EstimateCostParams): SearchCost | undefined;
91
136
  //#endregion
92
137
  //#region src/utils/error.d.ts
93
138
  /**
@@ -104,7 +149,7 @@ interface WebseekErrorOptions {
104
149
  message: string;
105
150
  cause?: unknown;
106
151
  }
107
- declare class WebseekError extends Error {
152
+ export declare class WebseekError extends Error {
108
153
  readonly code: WebseekErrorCode;
109
154
  constructor(options: WebseekErrorOptions);
110
155
  }
@@ -112,12 +157,12 @@ declare class WebseekError extends Error {
112
157
  * Map a thrown value to a process exit code: `2` for usage mistakes, `1` for
113
158
  * any other failure.
114
159
  */
115
- declare function errorExitCode(error: unknown): number;
160
+ export declare function errorExitCode(error: unknown): number;
116
161
  /** Render any thrown value into a single-line, user-facing message. */
117
- declare function formatError(error: unknown): string;
162
+ export declare function formatError(error: unknown): string;
118
163
  //#endregion
119
164
  //#region src/mcp/tools.d.ts
120
- declare const webSearchInputShape: {
165
+ export declare const webSearchInputShape: {
121
166
  query: z.ZodString;
122
167
  provider: z.ZodEnum<{
123
168
  gemini: "gemini";
@@ -161,6 +206,8 @@ interface CreateWebSearchToolParams {
161
206
  env?: Env;
162
207
  /** Injectable for tests; defaults to the global fetch. */
163
208
  fetchImpl?: typeof fetch;
209
+ /** Called with every successful result before it is returned (e.g. to log usage). */
210
+ onResult?: (result: NormalizedSearchResult) => Promise<void> | void;
164
211
  }
165
212
  interface WebSearchTool {
166
213
  name: string;
@@ -171,6 +218,6 @@ interface WebSearchTool {
171
218
  };
172
219
  handler: (args: WebSearchArgs) => Promise<ToolResult>;
173
220
  }
174
- declare function createWebSearchTool(params?: CreateWebSearchToolParams): WebSearchTool;
221
+ export declare function createWebSearchTool(params?: CreateWebSearchToolParams): WebSearchTool;
175
222
  //#endregion
176
- export { type Citation, type CreateWebSearchToolParams, type Env, GEMINI_BACKENDS, type GeminiBackend, type NormalizedSearchResult, PROVIDER_NAMES, type ProviderName, type RunSearchParams, type SearchParams, type SearchProvider, type SearchResultItem, type ToolResult, type WebSearchArgs, type WebSearchTool, WebseekError, type WebseekErrorCode, type WebseekErrorOptions, createWebSearchTool, errorExitCode, formatError, runSearch, webSearchInputShape };
223
+ export type { Citation, CreateWebSearchToolParams, Env, EstimateCostParams, GeminiBackend, NormalizedSearchResult, ProviderName, RunSearchParams, SearchCost, SearchParams, SearchProvider, SearchResultItem, SearchUsage, ToolResult, WebSearchArgs, WebSearchTool, WebseekErrorCode, WebseekErrorOptions };