import { readFileSync } from "node:fs"; import { join } from "node:path"; import { fileURLToPath } from "node:url"; const TOOL_SEARCH_DIR = join("my-pi", "tool-search"); /** * Bundle-level `config/` is the single source of truth: the extension reads * `config/tool-search.json` from inside the installed bundle (resolved * relative to this source file), never deployed to or migrated from the * agent directory. */ export function getToolSearchConfigPath(): string { return fileURLToPath(new URL("../../config/tool-search.json", import.meta.url)); } /** Disposable catalog cache under the agent dir's `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", "memory_search", "session_search", "memory_add", "memory_replace", "memory_remove", "skill_manage", "context_tree_query", "overflow_query", "codemode", ], 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. Defaults to the bundle-level * `config/tool-search.json`; tests inject an explicit temp path. A missing, * unreadable, or non-object file falls back to the bundle defaults. */ export function readToolSearchConfig( configPath: string = getToolSearchConfigPath(), ): ToolSearchConfig { const fallback = bundleFallbackConfig(); try { const parsed: unknown = JSON.parse(readFileSync(configPath, "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; } }