Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 10 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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`,
Expand All @@ -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:
Expand Down
5 changes: 5 additions & 0 deletions packages/pi-web/src/tool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
normalizeDocsToolInput,
type DocsToolInput,
type LinksResult,
type SearchProvider,
type WebCredentials,
type WebOperations,
type SearchResponse,
Expand Down Expand Up @@ -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 = [
Expand Down Expand Up @@ -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",
Expand All @@ -367,6 +371,7 @@ export function webSearchTool(dependencies: WebToolDependencies = {}) {
queries.map((query) =>
operations.search({
query,
...(provider === undefined ? {} : { provider }),
credentials: credentials(),
signal,
}),
Expand Down
22 changes: 22 additions & 0 deletions packages/pi-web/test/extension.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = <T>(signal: AbortSignal | undefined) =>
new Promise<T>((_resolve, reject) => {
Expand Down
3 changes: 1 addition & 2 deletions packages/web-core/test/search.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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" }] },
});
Expand Down
7 changes: 5 additions & 2 deletions packages/web/src/mcp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@ const sgraphInputSchema = schema<SGraphToolInput>({
const searchOutputSchema = schema({
type: "object",
properties: {
provider: { type: "string", enum: ["Exa", "Brave"] },
provider: { type: "string", enum: ["Exa", "Brave", "Kepos Bridge"] },
results: {
type: "array",
items: {
Expand Down Expand Up @@ -432,7 +432,10 @@ export function createMcpCommand(
): Command {
return new Command("mcp")
.description("Serve typed web tools over stdio MCP")
.option("--provider <provider>", "Search provider: exa or brave")
.option(
"--provider <provider>",
"Search provider: exa, brave, or kepos-bridge",
)
.action((options: { provider?: string }) => {
const provider = options.provider;
serveStdio(() => createMcpServer({ ...dependencies, provider }), {
Expand Down
5 changes: 4 additions & 1 deletion packages/web/src/program.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,10 @@ function createSearchCommand(dependencies: ProgramDependencies): Command {
.description("Search the web")
.argument("<query>", "One search query")
.option("--json", "Output the structured result as JSON")
.option("--provider <provider>", "Search provider: exa or brave")
.option(
"--provider <provider>",
"Search provider: exa, brave, or kepos-bridge",
)
.action(
async (query: string, options: { json?: boolean; provider?: string }) => {
const result = await dependencies.operations.search({
Expand Down
14 changes: 11 additions & 3 deletions packages/web/test/mcp.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,10 @@ describe("web stdio MCP adapter", () => {
.properties! as Record<string, unknown>;
const sgraphProperties = byName.source_search!.inputSchema
.properties! as Record<string, unknown>;
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"],
Expand All @@ -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 () => {
Expand Down Expand Up @@ -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" }),
);
});
});
Expand Down
Loading