diff --git a/README.md b/README.md index e877755..a34fec1 100644 --- a/README.md +++ b/README.md @@ -21,9 +21,10 @@ npx @guionai/web --help ``` Search needs one provider credential for Exa or Brave. If both are present, Exa -is selected by default; select a provider explicitly with `--provider exa` or -`--provider brave`. The DSH Web integration also supports the credential-free -`kepos-bridge` provider through its live settings card. +is selected by default; select a provider explicitly with `--provider exa`, +`--provider brave`, or `--provider kepos-bridge`. Kepos Bridge uses the bundled +default route unless it runs in DSH, whose live settings card can override the +route. Context7 works anonymously when its key is absent. ```bash @@ -45,6 +46,7 @@ document on stdout, which is useful for automation. ```bash web search --provider exa -- "Node AbortSignal" +web search --provider kepos-bridge -- "Node AbortSignal" web fetch https://example.com/article --tree web fetch https://example.com/article --section introduction web links https://example.com/article --limit 50 @@ -66,6 +68,7 @@ Run the stdio server with the same credential environment: web mcp # Pin search selection for the lifetime of this MCP process: web mcp --provider brave +web mcp --provider kepos-bridge ``` The server exposes six read-only tools: `search`, `fetch`, `links`, `docs_resolve`, @@ -90,6 +93,10 @@ an integer `waitMs` when its host provides that optional executable. `web_links` uses the same explicit rendering contract and lists HTTP(S) anchors from the original page DOM. +Set `WEB_SEARCH_PROVIDER=kepos-bridge` before starting Pi to select the +credential-free Kepos Bridge provider. Pi uses the bundled default Bridge route; +only DSH exposes a route setting. + ## DSH Install the DSH bundle in the existing Web profile: diff --git a/packages/pi-web/src/tool.ts b/packages/pi-web/src/tool.ts index f7a43e1..e0a1690 100644 --- a/packages/pi-web/src/tool.ts +++ b/packages/pi-web/src/tool.ts @@ -7,6 +7,7 @@ import { normalizeDocsToolInput, type DocsToolInput, type LinksResult, + type SearchProvider, type WebCredentials, type WebOperations, type SearchResponse, @@ -187,6 +188,8 @@ type SearchResult = SearchResponse & { export type WebToolDependencies = { operations?: WebOperations; credentials?: () => WebCredentials; + /** Pins the provider used by every Pi web search. */ + provider?: SearchProvider; }; const SEARCH_PROMPT_GUIDELINES = [ @@ -353,6 +356,7 @@ function normalizeDocs(input: unknown): DocsToolInput { export function webSearchTool(dependencies: WebToolDependencies = {}) { const operations = dependencies.operations ?? createWebOperations(); const credentials = dependencies.credentials ?? environmentCredentials; + const provider = dependencies.provider ?? process.env.WEB_SEARCH_PROVIDER; return makeTool({ name: "web_search", label: "Web search", @@ -367,6 +371,7 @@ export function webSearchTool(dependencies: WebToolDependencies = {}) { queries.map((query) => operations.search({ query, + ...(provider === undefined ? {} : { provider }), credentials: credentials(), signal, }), diff --git a/packages/pi-web/test/extension.test.ts b/packages/pi-web/test/extension.test.ts index f86e1b5..1c42dd3 100644 --- a/packages/pi-web/test/extension.test.ts +++ b/packages/pi-web/test/extension.test.ts @@ -354,6 +354,28 @@ describe("pi-web extension", () => { ); }); + it("pins an explicitly configured Kepos provider for every search", async () => { + const search = vi.fn(async () => ({ + provider: "Kepos Bridge" as const, + results: [], + })); + const tool = webSearchTool({ + operations: operations({ search }), + provider: "kepos-bridge", + }); + + await call(tool, { queries: ["one", "two"] }); + + expect(search).toHaveBeenNthCalledWith( + 1, + expect.objectContaining({ provider: "kepos-bridge", query: "one" }), + ); + expect(search).toHaveBeenNthCalledWith( + 2, + expect.objectContaining({ provider: "kepos-bridge", query: "two" }), + ); + }); + it("delegates every capability in-process and propagates caller cancellation", async () => { const abortable = (signal: AbortSignal | undefined) => new Promise((_resolve, reject) => { diff --git a/packages/web-core/test/search.test.ts b/packages/web-core/test/search.test.ts index a0461c1..3abbe29 100644 --- a/packages/web-core/test/search.test.ts +++ b/packages/web-core/test/search.test.ts @@ -136,7 +136,6 @@ describe("search providers migrated from Organon fixtures", () => { query: "managed search", provider: "kepos-bridge", maxResults: 2, - keposBridgeEndpoint: "http://fixture.test/codex/web-search", credentials: {}, fetch: async (receivedURL, init) => { url = String(receivedURL); @@ -159,7 +158,7 @@ describe("search providers migrated from Organon fixtures", () => { }); }, }); - expect(url).toBe("http://fixture.test/codex/web-search"); + expect(url).toBe(DEFAULT_KEPOS_BRIDGE_ENDPOINT); expect(body).toEqual({ commands: { search_query: [{ q: "managed search" }] }, }); diff --git a/packages/web/src/mcp.ts b/packages/web/src/mcp.ts index aed1e13..36e5c7b 100644 --- a/packages/web/src/mcp.ts +++ b/packages/web/src/mcp.ts @@ -192,7 +192,7 @@ const sgraphInputSchema = schema({ const searchOutputSchema = schema({ type: "object", properties: { - provider: { type: "string", enum: ["Exa", "Brave"] }, + provider: { type: "string", enum: ["Exa", "Brave", "Kepos Bridge"] }, results: { type: "array", items: { @@ -432,7 +432,10 @@ export function createMcpCommand( ): Command { return new Command("mcp") .description("Serve typed web tools over stdio MCP") - .option("--provider ", "Search provider: exa or brave") + .option( + "--provider ", + "Search provider: exa, brave, or kepos-bridge", + ) .action((options: { provider?: string }) => { const provider = options.provider; serveStdio(() => createMcpServer({ ...dependencies, provider }), { diff --git a/packages/web/src/program.ts b/packages/web/src/program.ts index 653ae0a..aa8ca79 100644 --- a/packages/web/src/program.ts +++ b/packages/web/src/program.ts @@ -39,7 +39,10 @@ function createSearchCommand(dependencies: ProgramDependencies): Command { .description("Search the web") .argument("", "One search query") .option("--json", "Output the structured result as JSON") - .option("--provider ", "Search provider: exa or brave") + .option( + "--provider ", + "Search provider: exa, brave, or kepos-bridge", + ) .action( async (query: string, options: { json?: boolean; provider?: string }) => { const result = await dependencies.operations.search({ diff --git a/packages/web/test/mcp.test.ts b/packages/web/test/mcp.test.ts index a4f4a38..561ccfd 100644 --- a/packages/web/test/mcp.test.ts +++ b/packages/web/test/mcp.test.ts @@ -246,6 +246,10 @@ describe("web stdio MCP adapter", () => { .properties! as Record; const sgraphProperties = byName.source_search!.inputSchema .properties! as Record; + const searchProperties = byName.search!.outputSchema!.properties! as Record< + string, + unknown + >; expect(fetchProperties.tree_threshold).toMatchObject({ default: 5000 }); expect(fetchProperties.render).toMatchObject({ enum: ["fetch", "agent-browser"], @@ -272,6 +276,10 @@ describe("web stdio MCP adapter", () => { context: { default: 10 }, timeout: { default: 0 }, }); + expect(searchProperties.provider).toEqual({ + type: "string", + enum: ["Exa", "Brave", "Kepos Bridge"], + }); }); it("maps every input to the shared core and returns structured results", async () => { @@ -555,18 +563,18 @@ describe("web stdio MCP adapter", () => { }); it("pins the selected provider for every search over a server lifetime", async () => { - const { client, operations } = await connect(webService(), "exa"); + const { client, operations } = await connect(webService(), "kepos-bridge"); await client.callTool({ name: "search", arguments: { query: "first" } }); await client.callTool({ name: "search", arguments: { query: "second" } }); expect(operations.search).toHaveBeenNthCalledWith( 1, - expect.objectContaining({ provider: "exa" }), + expect.objectContaining({ provider: "kepos-bridge" }), ); expect(operations.search).toHaveBeenNthCalledWith( 2, - expect.objectContaining({ provider: "exa" }), + expect.objectContaining({ provider: "kepos-bridge" }), ); }); });