mirror of
https://bitbucket.org/siakitem/my-pi.git
synced 2026-08-28 08:35:57 +00:00
feat: vendor grouped tool search
This commit is contained in:
@@ -0,0 +1,111 @@
|
||||
import { mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
||||
import { dirname, join } from "node:path";
|
||||
|
||||
export type ToolSearchDefaultResult = "updated" | "unchanged" | "skipped-invalid";
|
||||
|
||||
export const BUNDLE_TOOL_SEARCH_DEFAULTS = {
|
||||
alwaysEnabled: ["codegraph_explore", "lsp_diagnostics"],
|
||||
showToolSearchFooterStatus: false,
|
||||
maxActiveGroups: 3,
|
||||
maxToolsPerGroup: 8,
|
||||
maxDynamicTools: 20,
|
||||
groupOverrides: {},
|
||||
} as const;
|
||||
|
||||
export interface ToolSearchConfig {
|
||||
alwaysEnabled: string[];
|
||||
showToolSearchFooterStatus: boolean;
|
||||
maxActiveGroups: number;
|
||||
maxToolsPerGroup: number;
|
||||
maxDynamicTools: number;
|
||||
groupOverrides: Record<string, string[]>;
|
||||
}
|
||||
|
||||
function isObject(value: unknown): value is Record<string, unknown> {
|
||||
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<string, string[]> {
|
||||
if (!isObject(value)) return {};
|
||||
const overrides: Record<string, string[]> = {};
|
||||
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: {},
|
||||
};
|
||||
}
|
||||
|
||||
export function readToolSearchConfig(agentDir: string): ToolSearchConfig {
|
||||
const fallback = bundleFallbackConfig();
|
||||
try {
|
||||
const parsed: unknown = JSON.parse(readFileSync(join(agentDir, "settings.json"), "utf8"));
|
||||
if (!isObject(parsed) || !isObject(parsed.toolSearch)) return fallback;
|
||||
const config = parsed.toolSearch;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/** Add bundle defaults without replacing explicit user choices. */
|
||||
export function ensureToolSearchDefaults(agentDir: string): ToolSearchDefaultResult {
|
||||
const targetPath = join(agentDir, "settings.json");
|
||||
let settings: Record<string, unknown> = {};
|
||||
|
||||
try {
|
||||
const parsed: unknown = JSON.parse(readFileSync(targetPath, "utf8"));
|
||||
if (!isObject(parsed)) return "skipped-invalid";
|
||||
settings = parsed;
|
||||
} catch (error) {
|
||||
if ((error as NodeJS.ErrnoException).code !== "ENOENT") return "skipped-invalid";
|
||||
}
|
||||
|
||||
const existing = settings.toolSearch;
|
||||
if (existing !== undefined && !isObject(existing)) return "skipped-invalid";
|
||||
|
||||
const toolSearch = { ...(existing ?? {}) };
|
||||
let changed = false;
|
||||
for (const [key, value] of Object.entries(BUNDLE_TOOL_SEARCH_DEFAULTS)) {
|
||||
if (Object.hasOwn(toolSearch, key)) continue;
|
||||
toolSearch[key] = Array.isArray(value) ? [...value] : isObject(value) ? { ...value } : value;
|
||||
changed = true;
|
||||
}
|
||||
if (!changed) return "unchanged";
|
||||
|
||||
settings.toolSearch = toolSearch;
|
||||
mkdirSync(dirname(targetPath), { recursive: true });
|
||||
const temporaryPath = `${targetPath}.my-pi.tmp`;
|
||||
writeFileSync(temporaryPath, `${JSON.stringify(settings, null, 2)}\n`, "utf8");
|
||||
renameSync(temporaryPath, targetPath);
|
||||
return "updated";
|
||||
}
|
||||
Reference in New Issue
Block a user