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.
- package/README.md +109 -2
- package/dist/cli/index.cjs +439 -6
- package/dist/cli/index.mjs +439 -6
- package/dist/index.cjs +2 -1
- package/dist/index.d.cts +56 -9
- package/dist/index.d.mts +56 -9
- package/dist/index.mjs +2 -2
- package/dist/{tools-DBF9nCIs.mjs → tools-1ZllUYBx.mjs} +269 -5
- package/dist/{tools-Ci1EDrCT.cjs → tools-Brr43TiA.cjs} +274 -4
- package/package.json +15 -15
package/dist/cli/index.mjs
CHANGED
|
@@ -1,10 +1,122 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { a as coerceGeminiBackend, c as runSearch, d as formatError,
|
|
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
|
|
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-
|
|
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 {
|
|
223
|
+
export type { Citation, CreateWebSearchToolParams, Env, EstimateCostParams, GeminiBackend, NormalizedSearchResult, ProviderName, RunSearchParams, SearchCost, SearchParams, SearchProvider, SearchResultItem, SearchUsage, ToolResult, WebSearchArgs, WebSearchTool, WebseekErrorCode, WebseekErrorOptions };
|