webseek 0.1.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/LICENSE +21 -0
- package/README.md +153 -0
- package/dist/cli/index.cjs +217 -0
- package/dist/cli/index.d.cts +1 -0
- package/dist/cli/index.d.mts +1 -0
- package/dist/cli/index.mjs +218 -0
- package/dist/index.cjs +10 -0
- package/dist/index.d.cts +177 -0
- package/dist/index.d.mts +177 -0
- package/dist/index.mjs +2 -0
- package/dist/tools-CCqL3Phx.mjs +550 -0
- package/dist/tools-Cd93c2_h.cjs +615 -0
- package/package.json +98 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 dyoshikawa
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
# webseek
|
|
2
|
+
|
|
3
|
+
Web search using the API keys of multiple providers behind a single, unified
|
|
4
|
+
interface. Bring whichever provider key you already have and search the web —
|
|
5
|
+
either from your terminal (**CLI mode**) or from an MCP client such as an editor
|
|
6
|
+
or agent (**MCP server mode**). Both modes share the same core search logic.
|
|
7
|
+
|
|
8
|
+
## Providers
|
|
9
|
+
|
|
10
|
+
`webseek` normalizes two fundamentally different kinds of "web search":
|
|
11
|
+
|
|
12
|
+
| Provider | Kind | Output |
|
|
13
|
+
| -------- | ------------------------------------------- | -------------------------------- |
|
|
14
|
+
| `google` | SERP-style (Google Custom Search) | a ranked list of links |
|
|
15
|
+
| `openai` | LLM-grounded (Responses API `web_search`) | a synthesized answer + citations |
|
|
16
|
+
| `gemini` | LLM-grounded (Grounding with Google Search) | a synthesized answer + citations |
|
|
17
|
+
|
|
18
|
+
The Gemini provider supports two backends that share the same request/response
|
|
19
|
+
shape: the **Gemini Developer API** (`gemini-api`, default) and **Vertex AI
|
|
20
|
+
express mode** (`vertex-express`).
|
|
21
|
+
|
|
22
|
+
## Install
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npm install -g webseek # install the CLI globally
|
|
26
|
+
npx webseek search "..." -p google # or run without installing
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### Build from source
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
pnpm install
|
|
33
|
+
pnpm build # dual-format (ESM + CJS) build via tsdown; exposes the `webseek` bin
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
During development you can run without building via `pnpm dev -- <args>` (runs
|
|
37
|
+
the TypeScript source through `tsx`).
|
|
38
|
+
|
|
39
|
+
## CLI mode
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
webseek search <query...> --provider <openai|google|gemini> [options]
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Options for `search`:
|
|
46
|
+
|
|
47
|
+
| Flag | Description |
|
|
48
|
+
| ----------------------- | ------------------------------------------- |
|
|
49
|
+
| `-p, --provider <name>` | `openai` \| `google` \| `gemini` (required) |
|
|
50
|
+
| `-n, --max-results <n>` | Desired number of results (SERP providers) |
|
|
51
|
+
| `-m, --model <name>` | Model override (`openai`, `gemini`) |
|
|
52
|
+
| `--gemini-backend <b>` | `gemini-api` (default) \| `vertex-express` |
|
|
53
|
+
| `--json` | Emit normalized JSON instead of text |
|
|
54
|
+
| `--raw` | Include the provider's raw response |
|
|
55
|
+
|
|
56
|
+
Examples:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
webseek search "best static site generators 2026" -p google -n 5
|
|
60
|
+
webseek search "summarize the latest TypeScript release" -p openai
|
|
61
|
+
webseek search "who won euro 2024" -p gemini --gemini-backend vertex-express --json
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## MCP server mode
|
|
65
|
+
|
|
66
|
+
Start a [Model Context Protocol](https://modelcontextprotocol.io) server over
|
|
67
|
+
stdio that exposes a single `web_search` tool:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
webseek mcp
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Register it with an MCP client, e.g.:
|
|
74
|
+
|
|
75
|
+
```jsonc
|
|
76
|
+
{
|
|
77
|
+
"mcpServers": {
|
|
78
|
+
"webseek": {
|
|
79
|
+
"command": "webseek",
|
|
80
|
+
"args": ["mcp"],
|
|
81
|
+
"env": { "GEMINI_API_KEY": "..." },
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
The `web_search` tool accepts `{ query, provider, maxResults?, model?, geminiBackend?, includeRaw? }`
|
|
88
|
+
and returns the normalized result as JSON.
|
|
89
|
+
|
|
90
|
+
## Library (programmatic API)
|
|
91
|
+
|
|
92
|
+
`webseek` is published as a dual-format package (ESM + CJS), so it can be
|
|
93
|
+
imported as a library in addition to running as a CLI/MCP server:
|
|
94
|
+
|
|
95
|
+
```ts
|
|
96
|
+
import { runSearch, WebseekError } from "webseek";
|
|
97
|
+
|
|
98
|
+
const result = await runSearch({ provider: "google", query: "best static site generators 2026" });
|
|
99
|
+
console.log(result.results);
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
CommonJS consumers can `require` it the same way:
|
|
103
|
+
|
|
104
|
+
```js
|
|
105
|
+
const { runSearch } = require("webseek");
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
To embed the `web_search` tool into your own MCP server, use the
|
|
109
|
+
`createWebSearchTool` factory exported from the same entry point.
|
|
110
|
+
|
|
111
|
+
## Authentication
|
|
112
|
+
|
|
113
|
+
API keys are read from **environment variables only** (never from flags), so
|
|
114
|
+
they don't leak into shell history or process listings.
|
|
115
|
+
|
|
116
|
+
| Provider | Environment variables |
|
|
117
|
+
| ------------------------- | ----------------------------------------------------------------- |
|
|
118
|
+
| `openai` | `OPENAI_API_KEY` |
|
|
119
|
+
| `google` | `GOOGLE_API_KEY`, `GOOGLE_CSE_CX` (Programmable Search Engine ID) |
|
|
120
|
+
| `gemini` (gemini-api) | `GEMINI_API_KEY` (falls back to `GOOGLE_API_KEY`) |
|
|
121
|
+
| `gemini` (vertex-express) | `VERTEX_API_KEY` |
|
|
122
|
+
|
|
123
|
+
## Caveats
|
|
124
|
+
|
|
125
|
+
- **Google Custom Search is closed to new customers.** Existing customers must
|
|
126
|
+
migrate before 2027-01-01. Treat this provider as the most at-risk.
|
|
127
|
+
- **Google Custom Search returns at most 100 results** (10 per request); large
|
|
128
|
+
`--max-results` values paginate and consume more quota.
|
|
129
|
+
- **Gemini grounding requires displaying Google Search Suggestions** per its
|
|
130
|
+
terms. As a CLI we surface the queries the model ran on the `Searches:` line;
|
|
131
|
+
the source URIs Gemini returns are temporary redirect links.
|
|
132
|
+
|
|
133
|
+
## Architecture
|
|
134
|
+
|
|
135
|
+
```
|
|
136
|
+
src/
|
|
137
|
+
index.ts public library entry (runSearch, types, createWebSearchTool)
|
|
138
|
+
cli/ commander program: `search` and `mcp` commands
|
|
139
|
+
mcp/ MCP server + the web_search tool
|
|
140
|
+
lib/ runSearch — the shared core called by both CLI and MCP
|
|
141
|
+
providers/ per-provider implementations (openai, google-cse, gemini)
|
|
142
|
+
config/ credential + base-URL resolution from env
|
|
143
|
+
output/ text / JSON formatting
|
|
144
|
+
utils/ logger, error formatter
|
|
145
|
+
e2e/ end-to-end tests (spawn the CLI / drive the MCP server)
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
## Development
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
pnpm cicheck # format check, lint, typecheck, unit tests, spelling, secrets
|
|
152
|
+
pnpm test:e2e # end-to-end tests (CLI subprocess + MCP stdio client)
|
|
153
|
+
```
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
const require_tools = require("../tools-Cd93c2_h.cjs");
|
|
3
|
+
let node_fs = require("node:fs");
|
|
4
|
+
let node_path = require("node:path");
|
|
5
|
+
let commander = require("commander");
|
|
6
|
+
let _modelcontextprotocol_sdk_server_mcp_js = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
7
|
+
let _modelcontextprotocol_sdk_server_stdio_js = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
8
|
+
//#region src/mcp/server.ts
|
|
9
|
+
/**
|
|
10
|
+
* MCP server mode: exposes the `web_search` tool over the stdio transport so
|
|
11
|
+
* MCP clients (editors, agents) can search the web through this tool.
|
|
12
|
+
*
|
|
13
|
+
* Diagnostics go to stderr only — stdout is reserved for the JSON-RPC stream.
|
|
14
|
+
*/
|
|
15
|
+
async function startMcpServer(params) {
|
|
16
|
+
const server = new _modelcontextprotocol_sdk_server_mcp_js.McpServer({
|
|
17
|
+
name: "webseek",
|
|
18
|
+
version: params.version
|
|
19
|
+
});
|
|
20
|
+
const tool = require_tools.createWebSearchTool();
|
|
21
|
+
server.registerTool(tool.name, tool.config, tool.handler);
|
|
22
|
+
const transport = new _modelcontextprotocol_sdk_server_stdio_js.StdioServerTransport();
|
|
23
|
+
await server.connect(transport);
|
|
24
|
+
params.logger.info("webseek MCP server started (stdio)");
|
|
25
|
+
}
|
|
26
|
+
//#endregion
|
|
27
|
+
//#region src/utils/logger.ts
|
|
28
|
+
function writeErr(message) {
|
|
29
|
+
process.stderr.write(`${message}\n`);
|
|
30
|
+
}
|
|
31
|
+
function createConsoleLogger(params = {}) {
|
|
32
|
+
const silent = params.silent ?? process.env.NODE_ENV === "test";
|
|
33
|
+
const diagnosticsOnly = params.diagnosticsOnly ?? false;
|
|
34
|
+
return {
|
|
35
|
+
info: (message) => {
|
|
36
|
+
if (!silent) writeErr(message);
|
|
37
|
+
},
|
|
38
|
+
warn: (message) => {
|
|
39
|
+
if (!silent) writeErr(`warning: ${message}`);
|
|
40
|
+
},
|
|
41
|
+
error: (message) => {
|
|
42
|
+
writeErr(message);
|
|
43
|
+
},
|
|
44
|
+
result: (text) => {
|
|
45
|
+
if (diagnosticsOnly) {
|
|
46
|
+
writeErr(text);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
process.stdout.write(`${text}\n`);
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
//#endregion
|
|
54
|
+
//#region src/cli/wrap-command.ts
|
|
55
|
+
/**
|
|
56
|
+
* Wraps a commander action so every command shares the same error handling:
|
|
57
|
+
* a logger is created, the handler runs, and any thrown value is formatted to
|
|
58
|
+
* stderr and mapped to a process exit code.
|
|
59
|
+
*/
|
|
60
|
+
/**
|
|
61
|
+
* Returns a commander-compatible action. Commander invokes actions with the
|
|
62
|
+
* command's positionals/options followed by the Command instance, all of which
|
|
63
|
+
* are forwarded to the handler.
|
|
64
|
+
*/
|
|
65
|
+
function wrapCommand(handler) {
|
|
66
|
+
return async (...args) => {
|
|
67
|
+
const logger = createConsoleLogger();
|
|
68
|
+
try {
|
|
69
|
+
await handler({ logger }, ...args);
|
|
70
|
+
} catch (error) {
|
|
71
|
+
logger.error(require_tools.formatError(error));
|
|
72
|
+
process.exitCode = require_tools.errorExitCode(error);
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
//#endregion
|
|
77
|
+
//#region src/cli/commands/mcp.ts
|
|
78
|
+
async function runMcpCommand(params) {
|
|
79
|
+
await startMcpServer({
|
|
80
|
+
version: params.version,
|
|
81
|
+
logger: params.logger
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
function registerMcpCommand(params) {
|
|
85
|
+
params.program.command("mcp").description("Start the MCP server (stdio) exposing the web_search tool").action(wrapCommand(async ({ logger }) => {
|
|
86
|
+
await runMcpCommand({
|
|
87
|
+
logger,
|
|
88
|
+
version: params.version
|
|
89
|
+
});
|
|
90
|
+
}));
|
|
91
|
+
}
|
|
92
|
+
//#endregion
|
|
93
|
+
//#region src/output/format.ts
|
|
94
|
+
function formatResult(params) {
|
|
95
|
+
if (params.json) return formatJson(params.result);
|
|
96
|
+
return formatText(params.result);
|
|
97
|
+
}
|
|
98
|
+
function formatJson(result) {
|
|
99
|
+
const payload = {
|
|
100
|
+
provider: result.provider,
|
|
101
|
+
query: result.query,
|
|
102
|
+
results: result.results,
|
|
103
|
+
answer: result.answer,
|
|
104
|
+
citations: result.citations,
|
|
105
|
+
searchQueries: result.searchQueries
|
|
106
|
+
};
|
|
107
|
+
if (result.raw !== void 0) payload.raw = result.raw;
|
|
108
|
+
return JSON.stringify(payload, null, 2);
|
|
109
|
+
}
|
|
110
|
+
function formatText(result) {
|
|
111
|
+
const lines = [];
|
|
112
|
+
if (result.answer) lines.push(result.answer.trim(), "");
|
|
113
|
+
if (result.results.length > 0) {
|
|
114
|
+
lines.push("Results:");
|
|
115
|
+
result.results.forEach((item, index) => {
|
|
116
|
+
lines.push(`${index + 1}. ${item.title || item.url}`);
|
|
117
|
+
lines.push(` ${item.url}`);
|
|
118
|
+
if (item.snippet) lines.push(` ${item.snippet}`);
|
|
119
|
+
});
|
|
120
|
+
lines.push("");
|
|
121
|
+
}
|
|
122
|
+
if (result.citations.length > 0) {
|
|
123
|
+
lines.push("Sources:");
|
|
124
|
+
result.citations.forEach((citation, index) => {
|
|
125
|
+
const label = citation.title ? `${citation.title} — ${citation.url}` : citation.url;
|
|
126
|
+
lines.push(` [${index + 1}] ${label}`);
|
|
127
|
+
});
|
|
128
|
+
lines.push("");
|
|
129
|
+
}
|
|
130
|
+
if (result.searchQueries.length > 0) lines.push(`Searches: ${result.searchQueries.join(" | ")}`);
|
|
131
|
+
return lines.join("\n").trimEnd();
|
|
132
|
+
}
|
|
133
|
+
//#endregion
|
|
134
|
+
//#region src/cli/commands/search.ts
|
|
135
|
+
/**
|
|
136
|
+
* Translate raw CLI args/options into validated `runSearch` parameters. Kept
|
|
137
|
+
* pure (no I/O) so it can be unit-tested directly.
|
|
138
|
+
*/
|
|
139
|
+
function toSearchRequest(params) {
|
|
140
|
+
const query = params.queryParts.join(" ").trim();
|
|
141
|
+
if (!query) throw new require_tools.WebseekError({
|
|
142
|
+
code: "invalid_usage",
|
|
143
|
+
message: "Missing search query."
|
|
144
|
+
});
|
|
145
|
+
return {
|
|
146
|
+
provider: require_tools.coerceProvider(params.options.provider),
|
|
147
|
+
query,
|
|
148
|
+
maxResults: params.options.maxResults === void 0 ? void 0 : require_tools.coerceMaxResults(params.options.maxResults),
|
|
149
|
+
model: params.options.model,
|
|
150
|
+
includeRaw: Boolean(params.options.raw),
|
|
151
|
+
geminiBackend: params.options.geminiBackend === void 0 ? void 0 : require_tools.coerceGeminiBackend(params.options.geminiBackend)
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
async function runSearchCommand(params) {
|
|
155
|
+
const result = await require_tools.runSearch(toSearchRequest({
|
|
156
|
+
queryParts: params.queryParts,
|
|
157
|
+
options: params.options
|
|
158
|
+
}));
|
|
159
|
+
params.logger.result(formatResult({
|
|
160
|
+
result,
|
|
161
|
+
json: Boolean(params.options.json)
|
|
162
|
+
}));
|
|
163
|
+
}
|
|
164
|
+
function registerSearchCommand(program) {
|
|
165
|
+
program.command("search").description("Search the web via a provider").argument("<query...>", "the search query").requiredOption("-p, --provider <name>", "provider: openai | google | gemini").option("-n, --max-results <number>", "desired number of results (SERP providers)").option("-m, --model <name>", "model override (openai, gemini)").option("--gemini-backend <backend>", "gemini backend: gemini-api | vertex-express").option("--json", "emit normalized JSON instead of text").option("--raw", "include the provider's raw response").action(wrapCommand(async ({ logger }, queryParts, options) => {
|
|
166
|
+
await runSearchCommand({
|
|
167
|
+
logger,
|
|
168
|
+
queryParts,
|
|
169
|
+
options
|
|
170
|
+
});
|
|
171
|
+
}));
|
|
172
|
+
}
|
|
173
|
+
//#endregion
|
|
174
|
+
//#region src/cli/index.ts
|
|
175
|
+
/**
|
|
176
|
+
* webseek CLI entry point.
|
|
177
|
+
*
|
|
178
|
+
* Two modes:
|
|
179
|
+
* webseek search <query> --provider <name> run a one-off web search
|
|
180
|
+
* webseek mcp start the MCP server (stdio)
|
|
181
|
+
*/
|
|
182
|
+
function readPackageVersion(dir) {
|
|
183
|
+
try {
|
|
184
|
+
const pkg = JSON.parse((0, node_fs.readFileSync)((0, node_path.join)(dir, "package.json"), "utf8"));
|
|
185
|
+
return pkg.name === "webseek" ? pkg.version : void 0;
|
|
186
|
+
} catch {
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
function getVersion() {
|
|
191
|
+
let dir = __dirname;
|
|
192
|
+
for (let depth = 0; depth < 6; depth += 1) {
|
|
193
|
+
const version = readPackageVersion(dir);
|
|
194
|
+
if (version) return version;
|
|
195
|
+
dir = (0, node_path.dirname)(dir);
|
|
196
|
+
}
|
|
197
|
+
return "0.0.0";
|
|
198
|
+
}
|
|
199
|
+
function buildProgram() {
|
|
200
|
+
const program = new commander.Command();
|
|
201
|
+
const version = getVersion();
|
|
202
|
+
program.name("webseek").description("Unified multi-provider web search (CLI + MCP server)").version(version, "-v, --version", "Show version");
|
|
203
|
+
registerSearchCommand(program);
|
|
204
|
+
registerMcpCommand({
|
|
205
|
+
program,
|
|
206
|
+
version
|
|
207
|
+
});
|
|
208
|
+
return program;
|
|
209
|
+
}
|
|
210
|
+
async function main() {
|
|
211
|
+
await buildProgram().parseAsync(process.argv);
|
|
212
|
+
}
|
|
213
|
+
main().catch((error) => {
|
|
214
|
+
process.stderr.write(`${require_tools.formatError(error)}\n`);
|
|
215
|
+
process.exitCode = 1;
|
|
216
|
+
});
|
|
217
|
+
//#endregion
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { };
|
|
@@ -0,0 +1,218 @@
|
|
|
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-CCqL3Phx.mjs";
|
|
3
|
+
import { readFileSync } from "node:fs";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
import { Command } from "commander";
|
|
6
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
7
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
8
|
+
//#region src/mcp/server.ts
|
|
9
|
+
/**
|
|
10
|
+
* MCP server mode: exposes the `web_search` tool over the stdio transport so
|
|
11
|
+
* MCP clients (editors, agents) can search the web through this tool.
|
|
12
|
+
*
|
|
13
|
+
* Diagnostics go to stderr only — stdout is reserved for the JSON-RPC stream.
|
|
14
|
+
*/
|
|
15
|
+
async function startMcpServer(params) {
|
|
16
|
+
const server = new McpServer({
|
|
17
|
+
name: "webseek",
|
|
18
|
+
version: params.version
|
|
19
|
+
});
|
|
20
|
+
const tool = createWebSearchTool();
|
|
21
|
+
server.registerTool(tool.name, tool.config, tool.handler);
|
|
22
|
+
const transport = new StdioServerTransport();
|
|
23
|
+
await server.connect(transport);
|
|
24
|
+
params.logger.info("webseek MCP server started (stdio)");
|
|
25
|
+
}
|
|
26
|
+
//#endregion
|
|
27
|
+
//#region src/utils/logger.ts
|
|
28
|
+
function writeErr(message) {
|
|
29
|
+
process.stderr.write(`${message}\n`);
|
|
30
|
+
}
|
|
31
|
+
function createConsoleLogger(params = {}) {
|
|
32
|
+
const silent = params.silent ?? process.env.NODE_ENV === "test";
|
|
33
|
+
const diagnosticsOnly = params.diagnosticsOnly ?? false;
|
|
34
|
+
return {
|
|
35
|
+
info: (message) => {
|
|
36
|
+
if (!silent) writeErr(message);
|
|
37
|
+
},
|
|
38
|
+
warn: (message) => {
|
|
39
|
+
if (!silent) writeErr(`warning: ${message}`);
|
|
40
|
+
},
|
|
41
|
+
error: (message) => {
|
|
42
|
+
writeErr(message);
|
|
43
|
+
},
|
|
44
|
+
result: (text) => {
|
|
45
|
+
if (diagnosticsOnly) {
|
|
46
|
+
writeErr(text);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
process.stdout.write(`${text}\n`);
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
//#endregion
|
|
54
|
+
//#region src/cli/wrap-command.ts
|
|
55
|
+
/**
|
|
56
|
+
* Wraps a commander action so every command shares the same error handling:
|
|
57
|
+
* a logger is created, the handler runs, and any thrown value is formatted to
|
|
58
|
+
* stderr and mapped to a process exit code.
|
|
59
|
+
*/
|
|
60
|
+
/**
|
|
61
|
+
* Returns a commander-compatible action. Commander invokes actions with the
|
|
62
|
+
* command's positionals/options followed by the Command instance, all of which
|
|
63
|
+
* are forwarded to the handler.
|
|
64
|
+
*/
|
|
65
|
+
function wrapCommand(handler) {
|
|
66
|
+
return async (...args) => {
|
|
67
|
+
const logger = createConsoleLogger();
|
|
68
|
+
try {
|
|
69
|
+
await handler({ logger }, ...args);
|
|
70
|
+
} catch (error) {
|
|
71
|
+
logger.error(formatError(error));
|
|
72
|
+
process.exitCode = errorExitCode(error);
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
//#endregion
|
|
77
|
+
//#region src/cli/commands/mcp.ts
|
|
78
|
+
async function runMcpCommand(params) {
|
|
79
|
+
await startMcpServer({
|
|
80
|
+
version: params.version,
|
|
81
|
+
logger: params.logger
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
function registerMcpCommand(params) {
|
|
85
|
+
params.program.command("mcp").description("Start the MCP server (stdio) exposing the web_search tool").action(wrapCommand(async ({ logger }) => {
|
|
86
|
+
await runMcpCommand({
|
|
87
|
+
logger,
|
|
88
|
+
version: params.version
|
|
89
|
+
});
|
|
90
|
+
}));
|
|
91
|
+
}
|
|
92
|
+
//#endregion
|
|
93
|
+
//#region src/output/format.ts
|
|
94
|
+
function formatResult(params) {
|
|
95
|
+
if (params.json) return formatJson(params.result);
|
|
96
|
+
return formatText(params.result);
|
|
97
|
+
}
|
|
98
|
+
function formatJson(result) {
|
|
99
|
+
const payload = {
|
|
100
|
+
provider: result.provider,
|
|
101
|
+
query: result.query,
|
|
102
|
+
results: result.results,
|
|
103
|
+
answer: result.answer,
|
|
104
|
+
citations: result.citations,
|
|
105
|
+
searchQueries: result.searchQueries
|
|
106
|
+
};
|
|
107
|
+
if (result.raw !== void 0) payload.raw = result.raw;
|
|
108
|
+
return JSON.stringify(payload, null, 2);
|
|
109
|
+
}
|
|
110
|
+
function formatText(result) {
|
|
111
|
+
const lines = [];
|
|
112
|
+
if (result.answer) lines.push(result.answer.trim(), "");
|
|
113
|
+
if (result.results.length > 0) {
|
|
114
|
+
lines.push("Results:");
|
|
115
|
+
result.results.forEach((item, index) => {
|
|
116
|
+
lines.push(`${index + 1}. ${item.title || item.url}`);
|
|
117
|
+
lines.push(` ${item.url}`);
|
|
118
|
+
if (item.snippet) lines.push(` ${item.snippet}`);
|
|
119
|
+
});
|
|
120
|
+
lines.push("");
|
|
121
|
+
}
|
|
122
|
+
if (result.citations.length > 0) {
|
|
123
|
+
lines.push("Sources:");
|
|
124
|
+
result.citations.forEach((citation, index) => {
|
|
125
|
+
const label = citation.title ? `${citation.title} — ${citation.url}` : citation.url;
|
|
126
|
+
lines.push(` [${index + 1}] ${label}`);
|
|
127
|
+
});
|
|
128
|
+
lines.push("");
|
|
129
|
+
}
|
|
130
|
+
if (result.searchQueries.length > 0) lines.push(`Searches: ${result.searchQueries.join(" | ")}`);
|
|
131
|
+
return lines.join("\n").trimEnd();
|
|
132
|
+
}
|
|
133
|
+
//#endregion
|
|
134
|
+
//#region src/cli/commands/search.ts
|
|
135
|
+
/**
|
|
136
|
+
* Translate raw CLI args/options into validated `runSearch` parameters. Kept
|
|
137
|
+
* pure (no I/O) so it can be unit-tested directly.
|
|
138
|
+
*/
|
|
139
|
+
function toSearchRequest(params) {
|
|
140
|
+
const query = params.queryParts.join(" ").trim();
|
|
141
|
+
if (!query) throw new WebseekError({
|
|
142
|
+
code: "invalid_usage",
|
|
143
|
+
message: "Missing search query."
|
|
144
|
+
});
|
|
145
|
+
return {
|
|
146
|
+
provider: coerceProvider(params.options.provider),
|
|
147
|
+
query,
|
|
148
|
+
maxResults: params.options.maxResults === void 0 ? void 0 : coerceMaxResults(params.options.maxResults),
|
|
149
|
+
model: params.options.model,
|
|
150
|
+
includeRaw: Boolean(params.options.raw),
|
|
151
|
+
geminiBackend: params.options.geminiBackend === void 0 ? void 0 : coerceGeminiBackend(params.options.geminiBackend)
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
async function runSearchCommand(params) {
|
|
155
|
+
const result = await runSearch(toSearchRequest({
|
|
156
|
+
queryParts: params.queryParts,
|
|
157
|
+
options: params.options
|
|
158
|
+
}));
|
|
159
|
+
params.logger.result(formatResult({
|
|
160
|
+
result,
|
|
161
|
+
json: Boolean(params.options.json)
|
|
162
|
+
}));
|
|
163
|
+
}
|
|
164
|
+
function registerSearchCommand(program) {
|
|
165
|
+
program.command("search").description("Search the web via a provider").argument("<query...>", "the search query").requiredOption("-p, --provider <name>", "provider: openai | google | gemini").option("-n, --max-results <number>", "desired number of results (SERP providers)").option("-m, --model <name>", "model override (openai, gemini)").option("--gemini-backend <backend>", "gemini backend: gemini-api | vertex-express").option("--json", "emit normalized JSON instead of text").option("--raw", "include the provider's raw response").action(wrapCommand(async ({ logger }, queryParts, options) => {
|
|
166
|
+
await runSearchCommand({
|
|
167
|
+
logger,
|
|
168
|
+
queryParts,
|
|
169
|
+
options
|
|
170
|
+
});
|
|
171
|
+
}));
|
|
172
|
+
}
|
|
173
|
+
//#endregion
|
|
174
|
+
//#region src/cli/index.ts
|
|
175
|
+
/**
|
|
176
|
+
* webseek CLI entry point.
|
|
177
|
+
*
|
|
178
|
+
* Two modes:
|
|
179
|
+
* webseek search <query> --provider <name> run a one-off web search
|
|
180
|
+
* webseek mcp start the MCP server (stdio)
|
|
181
|
+
*/
|
|
182
|
+
function readPackageVersion(dir) {
|
|
183
|
+
try {
|
|
184
|
+
const pkg = JSON.parse(readFileSync(join(dir, "package.json"), "utf8"));
|
|
185
|
+
return pkg.name === "webseek" ? pkg.version : void 0;
|
|
186
|
+
} catch {
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
function getVersion() {
|
|
191
|
+
let dir = import.meta.dirname;
|
|
192
|
+
for (let depth = 0; depth < 6; depth += 1) {
|
|
193
|
+
const version = readPackageVersion(dir);
|
|
194
|
+
if (version) return version;
|
|
195
|
+
dir = dirname(dir);
|
|
196
|
+
}
|
|
197
|
+
return "0.0.0";
|
|
198
|
+
}
|
|
199
|
+
function buildProgram() {
|
|
200
|
+
const program = new Command();
|
|
201
|
+
const version = getVersion();
|
|
202
|
+
program.name("webseek").description("Unified multi-provider web search (CLI + MCP server)").version(version, "-v, --version", "Show version");
|
|
203
|
+
registerSearchCommand(program);
|
|
204
|
+
registerMcpCommand({
|
|
205
|
+
program,
|
|
206
|
+
version
|
|
207
|
+
});
|
|
208
|
+
return program;
|
|
209
|
+
}
|
|
210
|
+
async function main() {
|
|
211
|
+
await buildProgram().parseAsync(process.argv);
|
|
212
|
+
}
|
|
213
|
+
main().catch((error) => {
|
|
214
|
+
process.stderr.write(`${formatError(error)}\n`);
|
|
215
|
+
process.exitCode = 1;
|
|
216
|
+
});
|
|
217
|
+
//#endregion
|
|
218
|
+
export {};
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
const require_tools = require("./tools-Cd93c2_h.cjs");
|
|
3
|
+
exports.GEMINI_BACKENDS = require_tools.GEMINI_BACKENDS;
|
|
4
|
+
exports.PROVIDER_NAMES = require_tools.PROVIDER_NAMES;
|
|
5
|
+
exports.WebseekError = require_tools.WebseekError;
|
|
6
|
+
exports.createWebSearchTool = require_tools.createWebSearchTool;
|
|
7
|
+
exports.errorExitCode = require_tools.errorExitCode;
|
|
8
|
+
exports.formatError = require_tools.formatError;
|
|
9
|
+
exports.runSearch = require_tools.runSearch;
|
|
10
|
+
exports.webSearchInputShape = require_tools.webSearchInputShape;
|