import { readFileSync } from "node:fs"; import { join } from "node:path"; const TOOL_SEARCH_DIR = join("my-pi", "tool-search"); /** * Path of the bundle-managed Tool Search config inside the agent directory. * `scripts/rebuild-bundle-config.mjs` deploys `config/tool-search.json` here; * this extension never writes or migrates it. */ export function getToolSearchConfigPath(agentDir: string): string { return join(agentDir, TOOL_SEARCH_DIR, "config.json"); } /** Catalog cache path under the same bundle-owned `my-pi/tool-search/` tree. */ export function getToolSearchCatalogPath(agentDir: string): string { return join(agentDir, TOOL_SEARCH_DIR, "catalog-v1.json"); } export const BUNDLE_TOOL_SEARCH_DEFAULTS = { alwaysEnabled: [ "codegraph_explore", "lsp_diagnostics", "ctx_execute", "ctx_execute_file", "ctx_batch_execute", "memory_search", "session_search", "memory_add", "memory_replace", "memory_remove", "skill_manage", "context_tree_query", ], showToolSearchFooterStatus: false, maxActiveGroups: 5, maxToolsPerGroup: 8, maxDynamicTools: 28, groupOverrides: {}, } as const; export interface ToolSearchConfig { alwaysEnabled: string[]; showToolSearchFooterStatus: boolean; maxActiveGroups: number; maxToolsPerGroup: number; maxDynamicTools: number; groupOverrides: Record; } function isObject(value: unknown): value is Record { return value !== null && typeof value === "object" && !Array.isArray(value); } function positiveInteger(value: unknown, fallback: number): number { return typeof value === "number" && Number.isInteger(value) && value > 0 ? value : fallback; } function stringList(value: unknown): string[] { if (!Array.isArray(value)) return []; return [...new Set(value.filter((item): item is string => typeof item === "string" && item.length > 0))]; } function readGroupOverrides(value: unknown): Record { if (!isObject(value)) return {}; const overrides: Record = {}; for (const [groupId, names] of Object.entries(value)) { const tools = stringList(names); if (groupId.trim() && tools.length > 0) overrides[groupId] = tools; } return overrides; } export function bundleFallbackConfig(): ToolSearchConfig { return { alwaysEnabled: [...BUNDLE_TOOL_SEARCH_DEFAULTS.alwaysEnabled], showToolSearchFooterStatus: BUNDLE_TOOL_SEARCH_DEFAULTS.showToolSearchFooterStatus, maxActiveGroups: BUNDLE_TOOL_SEARCH_DEFAULTS.maxActiveGroups, maxToolsPerGroup: BUNDLE_TOOL_SEARCH_DEFAULTS.maxToolsPerGroup, maxDynamicTools: BUNDLE_TOOL_SEARCH_DEFAULTS.maxDynamicTools, groupOverrides: {}, }; } /** * Read the Tool Search config from `/my-pi/tool-search/config.json`. * A missing, unreadable, or non-object file falls back to the bundle defaults. */ export function readToolSearchConfig(agentDir: string): ToolSearchConfig { const fallback = bundleFallbackConfig(); try { const parsed: unknown = JSON.parse(readFileSync(getToolSearchConfigPath(agentDir), "utf8")); if (!isObject(parsed)) return fallback; const config = parsed; return { alwaysEnabled: Array.isArray(config.alwaysEnabled) ? stringList(config.alwaysEnabled) : fallback.alwaysEnabled, showToolSearchFooterStatus: typeof config.showToolSearchFooterStatus === "boolean" ? config.showToolSearchFooterStatus : fallback.showToolSearchFooterStatus, maxActiveGroups: positiveInteger(config.maxActiveGroups, fallback.maxActiveGroups), maxToolsPerGroup: positiveInteger(config.maxToolsPerGroup, fallback.maxToolsPerGroup), maxDynamicTools: positiveInteger(config.maxDynamicTools, fallback.maxDynamicTools), groupOverrides: readGroupOverrides(config.groupOverrides), }; } catch { return fallback; } }