mirror of
https://bitbucket.org/siakitem/my-pi.git
synced 2026-08-28 08:35:57 +00:00
feat: add search API key configuration
This commit is contained in:
+30
-27
@@ -1,33 +1,36 @@
|
||||
import type { McpConfig } from "pi-mcp-adapter";
|
||||
|
||||
export const EXA_TOOLS = ["web_search_exa", "web_fetch_exa", "web_search_advanced_exa"];
|
||||
const EXA_MCP_URL = `https://mcp.exa.ai/mcp?tools=${EXA_TOOLS.join(",")}`;
|
||||
|
||||
export const MCP_CONFIG: McpConfig = {
|
||||
settings: {
|
||||
disableProxyTool: true,
|
||||
scriptMode: false,
|
||||
},
|
||||
mcpServers: {
|
||||
exa: {
|
||||
url: EXA_MCP_URL,
|
||||
protocolVersion: "auto",
|
||||
lifecycle: "eager",
|
||||
directTools: EXA_TOOLS,
|
||||
includeTools: EXA_TOOLS,
|
||||
toolPrefix: "none",
|
||||
approveTools: false,
|
||||
exposeResources: false,
|
||||
export function createMcpConfig(env: Record<string, string | undefined>) {
|
||||
const hasExaApiKey = Boolean(env.EXA_API_KEY?.trim());
|
||||
return {
|
||||
settings: {
|
||||
disableProxyTool: true,
|
||||
scriptMode: false,
|
||||
},
|
||||
codegraph: {
|
||||
command: "codegraph",
|
||||
args: ["serve", "--mcp"],
|
||||
lifecycle: "keep-alive",
|
||||
directTools: ["codegraph_explore"],
|
||||
includeTools: ["codegraph_explore"],
|
||||
toolPrefix: "none",
|
||||
approveTools: false,
|
||||
exposeResources: false,
|
||||
mcpServers: {
|
||||
exa: {
|
||||
url: EXA_MCP_URL,
|
||||
...(hasExaApiKey ? { headers: { "x-api-key": "$env:EXA_API_KEY" } } : {}),
|
||||
protocolVersion: "auto" as const,
|
||||
lifecycle: "eager" as const,
|
||||
directTools: EXA_TOOLS,
|
||||
includeTools: EXA_TOOLS,
|
||||
toolPrefix: "none" as const,
|
||||
approveTools: false,
|
||||
exposeResources: false,
|
||||
},
|
||||
codegraph: {
|
||||
command: "codegraph",
|
||||
args: ["serve", "--mcp"],
|
||||
lifecycle: "keep-alive" as const,
|
||||
directTools: ["codegraph_explore"],
|
||||
includeTools: ["codegraph_explore"],
|
||||
toolPrefix: "none" as const,
|
||||
approveTools: false,
|
||||
exposeResources: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
+4
-5
@@ -1,13 +1,12 @@
|
||||
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
||||
import { createMcpAdapter } from "pi-mcp-adapter";
|
||||
import { createExaToolApiOverride } from "./exa-tool-prefix.ts";
|
||||
import { MCP_CONFIG } from "./mcp-config.ts";
|
||||
|
||||
const mcpAdapter = createMcpAdapter({
|
||||
config: MCP_CONFIG,
|
||||
});
|
||||
import { createMcpConfig } from "./mcp-config.ts";
|
||||
import { loadSearchConfigEnv } from "./search-config.ts";
|
||||
|
||||
/** Connect Pi to Exa and CodeGraph through one shared MCP adapter instance. */
|
||||
export default function mcpExtension(pi: ExtensionAPI): void {
|
||||
const searchEnvironment = loadSearchConfigEnv();
|
||||
const mcpAdapter = createMcpAdapter({ config: createMcpConfig(searchEnvironment) });
|
||||
mcpAdapter(createExaToolApiOverride(pi));
|
||||
}
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { homedir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
|
||||
export const SEARCH_API_KEY_NAMES = ["TAVILY_API_KEY", "EXA_API_KEY", "KEENABLE_API_KEY"] as const;
|
||||
|
||||
type SearchApiKeyName = (typeof SEARCH_API_KEY_NAMES)[number];
|
||||
type SearchEnvironment = Record<string, string | undefined>;
|
||||
|
||||
const SAFE_VALUE = /^[A-Za-z0-9._-]+$/;
|
||||
|
||||
export function getSearchConfigPath(env: SearchEnvironment = process.env): string {
|
||||
if (env.MY_PI_SEARCH_CONFIG?.trim()) {
|
||||
return env.MY_PI_SEARCH_CONFIG.trim();
|
||||
}
|
||||
const configHome = env.XDG_CONFIG_HOME?.trim() || join(env.HOME?.trim() || homedir(), ".config");
|
||||
return join(configHome, "my-pi", "search.env");
|
||||
}
|
||||
|
||||
export function parseSearchConfig(text: string): Partial<Record<SearchApiKeyName, string>> {
|
||||
const parsed: Partial<Record<SearchApiKeyName, string>> = {};
|
||||
for (const rawLine of text.split(/\r?\n/u)) {
|
||||
const line = rawLine.trim().replace(/^export[ \t]+/u, "");
|
||||
if (!line || line.startsWith("#")) continue;
|
||||
const separator = line.indexOf("=");
|
||||
if (separator < 1) continue;
|
||||
const name = line.slice(0, separator).trim() as SearchApiKeyName;
|
||||
const value = line.slice(separator + 1).trim();
|
||||
if (!SEARCH_API_KEY_NAMES.includes(name) || !SAFE_VALUE.test(value)) continue;
|
||||
parsed[name] = value;
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
|
||||
/** Load package-managed search credentials without overriding explicit process environment values. */
|
||||
export function loadSearchConfigEnv(
|
||||
env: SearchEnvironment = process.env,
|
||||
configPath = getSearchConfigPath(env),
|
||||
): Partial<Record<SearchApiKeyName, string>> {
|
||||
let parsed: Partial<Record<SearchApiKeyName, string>> = {};
|
||||
try {
|
||||
parsed = parseSearchConfig(readFileSync(configPath, "utf8"));
|
||||
} catch {
|
||||
// Missing or unreadable config is equivalent to an empty package-managed config.
|
||||
}
|
||||
const effective: Partial<Record<SearchApiKeyName, string>> = {};
|
||||
for (const name of SEARCH_API_KEY_NAMES) {
|
||||
if (!env[name]?.trim() && parsed[name]) env[name] = parsed[name];
|
||||
if (env[name]?.trim()) effective[name] = env[name]?.trim();
|
||||
}
|
||||
return effective;
|
||||
}
|
||||
|
||||
/** Load search API keys before provider extensions initialize. */
|
||||
export default function searchConfigExtension(): void {
|
||||
loadSearchConfigEnv();
|
||||
}
|
||||
Reference in New Issue
Block a user