import type { ToolInfo } from "@earendil-works/pi-coding-agent"; import { CATALOG_VERSION, applyCatalogOverrides, createFallbackCatalog, type CatalogConstraints, type GroupCard, type ToolCard, type ToolCatalog, } from "./catalog.ts"; interface BundleGroupDefinition { id: string; title: string; summary: string; useWhen: string[]; avoidWhen: string[]; keywords: string[]; tools: string[]; } /** Curated groups for every non-core tool exposed by the my-pi bundle. */ export const BUNDLE_GROUP_DEFINITIONS: BundleGroupDefinition[] = [ { id: "filesystem-navigation", title: "Filesystem navigation", summary: "Inspect directory layouts and run bounded multi-pattern literal searches.", useWhen: ["You need directory entries or several literal searches after narrowing a path"], avoidWhen: ["Code relationships or types are the real question"], keywords: ["files", "directory", "list", "multi grep", "目录", "文件", "多模式搜索"], tools: ["ls", "multi_grep"], }, { id: "ssh-connection", title: "SSH connection", summary: "Connect sequentially to an explicitly imported SSH host through the reviewed agent tool flow.", useWhen: ["The user names a remote server as part of a concrete task and no matching SSH2 connection is active; call ssh_connect separately and wait for success before dependent remote tools"], avoidWhen: ["The task is local, the server was not explicitly named, or the host has not been imported"], keywords: ["ssh", "connect", "server", "host", "remote", "连接服务器", "远程主机", "SSH"], tools: ["ssh_connect"], }, { id: "ssh-remote-files", title: "SSH remote files", summary: "Read, write, and edit files in the currently connected remote SSH workspace.", useWhen: ["You need explicit file operations on an active SSH target rather than the local workspace"], avoidWhen: ["A remote shell command is required, or the files are local"], keywords: ["ssh", "remote", "read", "write", "edit", "远程", "文件", "SSH"], tools: ["ssh_read", "ssh_write", "ssh_edit"], }, { id: "ssh-remote-search", title: "SSH remote search", summary: "Run bounded adaptive filename and content searches on the active SSH target.", useWhen: ["You need to locate remote files or literals before reading or operating on them"], avoidWhen: ["The search belongs to the local workspace, or a known remote path can be read directly"], keywords: ["ssh", "remote", "find", "grep", "search", "远程搜索", "文件查找", "SSH"], tools: ["ssh_find", "ssh_grep"], }, { id: "ssh-remote-shell", title: "SSH remote workspace shell", summary: "Change the active remote cwd as a separate reviewed step, or run a reviewed shell command in that workspace.", useWhen: ["You need to change the active remote workspace or run builds, tests, Git, or other shell operations there; call ssh_cd separately and wait for success before dependent remote calls"], avoidWhen: ["Structured remote file tools are sufficient, or the operation should run locally"], keywords: ["ssh", "remote", "bash", "cd", "cwd", "workspace", "build", "test", "git", "远程命令", "工作目录", "SSH"], tools: ["ssh_cd", "ssh_bash"], }, { id: "code-intelligence", title: "Code intelligence", summary: "Explore code relationships and query language-server definitions, references, types, symbols, and diagnostics.", useWhen: ["Understanding architecture, call paths, symbol identity, types, references, or compiler diagnostics"], avoidWhen: ["Only an exact literal text match is required"], keywords: ["codegraph", "lsp", "definition", "references", "diagnostics", "代码结构", "定义", "引用", "诊断"], tools: ["codegraph_explore", "lsp_definition", "lsp_references", "lsp_hover", "lsp_symbols", "lsp_diagnostics"], }, { id: "web-search", title: "Web search and fetch", summary: "Search the web for current or citable information and read selected pages as clean markdown.", useWhen: ["Current events, external facts, documentation, papers, or page content are needed"], avoidWhen: ["The answer is already in the workspace or local code tools suffice"], keywords: ["web", "search", "fetch", "news", "docs", "网页", "搜索", "新闻", "文档"], tools: ["web_search", "web_fetch"], }, { id: "context-execution", title: "Context-isolated execution", summary: "Run commands or analyze large files in a sandbox while returning only compact derived output.", useWhen: ["Logs, tests, builds, generated data, commands, or workspace files may produce large output"], avoidWhen: ["Exact source lines are needed for an anchored edit"], keywords: ["execute", "large file", "logs", "tests", "build", "大文件", "日志", "测试", "构建"], tools: ["ctx_execute", "ctx_execute_file", "ctx_batch_execute"], }, { id: "context-knowledge", title: "Context knowledge base", summary: "Index local or web documentation and retrieve focused passages from the persistent context-mode knowledge base.", useWhen: ["Documentation or session knowledge should be stored and queried without rereading raw content"], avoidWhen: ["A one-shot small source can be read directly"], keywords: ["index", "search", "knowledge base", "fetch docs", "索引", "知识库", "文档检索"], tools: ["ctx_index", "ctx_search", "ctx_fetch_and_index"], }, { id: "context-observability", title: "Context observability", summary: "Inspect context-mode health, savings statistics, and the hosted Insight dashboard.", useWhen: ["Diagnosing context-mode or reviewing context savings and analytics"], avoidWhen: ["The task is normal code or data processing"], keywords: ["stats", "doctor", "insight", "health", "统计", "诊断", "上下文节省"], tools: ["ctx_stats", "ctx_doctor", "ctx_insight"], }, { id: "context-administration", title: "Context administration", summary: "Upgrade context-mode or destructively purge indexed context data.", useWhen: ["The user explicitly asks to upgrade context-mode or purge a named scope"], avoidWhen: ["Routine searching, indexing, or performance inspection"], keywords: ["upgrade", "purge", "delete", "升级", "清除", "删除知识库"], tools: ["ctx_upgrade", "ctx_purge"], }, { id: "memory-recall", title: "Persistent memory recall", summary: "Search durable user, global, project, and failure memories, search indexed past conversations, or recover pruned tool-call output.", useWhen: ["The task may depend on durable context from previous sessions: preferences, project conventions, prior decisions, known failures, corrections, or condensed outputs"], avoidWhen: ["The required evidence is already in the current visible context, or the question is a generic one-off that durable memory would not help"], keywords: ["memory", "recall", "search", "session", "history", "recover", "记忆", "召回", "检索", "历史会话", "恢复输出"], tools: ["memory_search", "session_search", "context_tree_query"], }, { id: "memory-management", title: "Persistent memory management", summary: "Save, update, or remove durable memories, and create or maintain reusable procedural skills.", useWhen: ["A durable fact, preference, convention, correction, or non-obvious reusable procedure should persist across sessions"], avoidWhen: ["The information is temporary task progress, derivable from the repository, or a one-off detail that would add noise"], keywords: ["remember", "save", "replace", "remove", "skill", "procedure", "记住", "保存", "修改记忆", "技能", "流程"], tools: ["memory_add", "memory_replace", "memory_remove", "skill_manage"], }, { id: "subagents", title: "Subagent orchestration", summary: "Launch isolated subagents, collect background results, and steer running delegated work.", useWhen: ["A bounded task benefits from an isolated specialist, background execution, or delegated follow-up"], avoidWhen: ["The current agent can complete a small direct task without delegation overhead"], keywords: ["subagent", "agent", "delegate", "background", "steer", "子代理", "委派", "后台"], tools: ["Agent", "get_subagent_result", "steer_subagent"], }, { id: "user-interaction", title: "Interactive user questions", summary: "Pause the agent and collect one or more structured decisions from the user in the TUI.", useWhen: ["A missing requirement, preference, confirmation, or choice must come directly from the user before continuing"], avoidWhen: ["The user's direct messages already provide the answer, or the task can proceed without another decision"], keywords: ["ask", "question", "clarify", "choice", "preference", "提问", "澄清", "选择", "确认"], tools: ["ask_user_question"], }, { id: "chrome-navigation", title: "Chrome navigation and inspection", summary: "Connect to the authorized Chrome bridge, manage tabs, navigate, inspect page structure, wait for state, and capture screenshots.", useWhen: ["You need to inspect or navigate the user's existing signed-in Chrome profile after explicit /chrome authorization"], avoidWhen: ["A normal web search or HTTP page fetch is sufficient, or Chrome has not been explicitly authorized"], keywords: ["chrome", "browser", "tab", "navigate", "snapshot", "inspect", "screenshot", "浏览器", "标签页", "网页操作"], tools: ["chrome_launch", "chrome_tab", "chrome_navigate", "chrome_snapshot", "chrome_find", "chrome_inspect", "chrome_wait_for", "chrome_screenshot"], }, { id: "chrome-interaction", title: "Chrome page interaction", summary: "Interact with an authorized Chrome page using real pointer, keyboard, touch, form, drag, and scroll input.", useWhen: ["You must click, type, fill, press keys, hover, drag, tap, or scroll in the user's authorized Chrome session"], avoidWhen: ["Page observation is enough, or the action is irreversible and the user has not approved it"], keywords: ["chrome", "browser", "click", "type", "fill", "drag", "scroll", "点击", "输入", "滚动", "浏览器交互"], tools: ["chrome_click", "chrome_type", "chrome_fill", "chrome_key", "chrome_hover", "chrome_drag", "chrome_tap", "chrome_scroll"], }, { id: "chrome-debugging", title: "Chrome diagnostics and upload", summary: "Evaluate page JavaScript, inspect captured console/network activity, retrieve response bodies, or attach local files.", useWhen: ["Debugging the authorized Chrome page, examining fetch/XHR behavior, evaluating page state, or uploading an explicitly named local file"], avoidWhen: ["Navigation, DOM inspection, or ordinary page interaction tools are sufficient"], keywords: ["chrome", "browser", "console", "network", "javascript", "upload", "调试", "网络请求", "文件上传"], tools: ["chrome_evaluate", "chrome_list_console_messages", "chrome_list_network_requests", "chrome_get_network_request", "chrome_upload_file"], }, { id: "mcp-management", title: "MCP management", summary: "Inspect and manage MCP server connections exposed by Pi's shared adapter.", useWhen: ["The user explicitly asks about MCP server status or operations"], avoidWhen: ["A mapped first-class MCP tool already handles the task"], keywords: ["mcp", "server", "connection", "MCP 服务", "连接"], tools: ["mcp"], }, ]; export interface BundleCatalogResult { catalog: ToolCatalog; coveredNames: Set; unknownTools: ToolInfo[]; } function compactDescription(tool: ToolInfo): string { return tool.description.replace(/\s+/gu, " ").trim().slice(0, 240) || `Use the ${tool.name} tool.`; } export function createBundleCatalog( tools: ToolInfo[], manifestHash: string, constraints: CatalogConstraints, ): BundleCatalogResult { const available = new Map(tools.map((tool) => [tool.name, tool])); const coveredNames = new Set(); const groups: GroupCard[] = []; const cards: ToolCard[] = []; for (const definition of BUNDLE_GROUP_DEFINITIONS) { const members = definition.tools.flatMap((name) => { const tool = available.get(name); return tool ? [tool] : []; }); if (members.length === 0) continue; for (const tool of members) { coveredNames.add(tool.name); cards.push({ name: tool.name, summary: compactDescription(tool), useWhen: [...definition.useWhen], avoidWhen: [...definition.avoidWhen], keywords: [...new Set([...definition.keywords, ...tool.name.split(/[_-]/u)])], primaryGroup: definition.id, }); } groups.push({ id: definition.id, title: definition.title, summary: definition.summary, useWhen: [...definition.useWhen], avoidWhen: [...definition.avoidWhen], tools: members.map((tool) => tool.name), }); } const unknownTools = tools.filter((tool) => !coveredNames.has(tool.name)); const fallback = createFallbackCatalog(unknownTools, manifestHash, { maxToolsPerGroup: constraints.maxToolsPerGroup, groupOverrides: {}, }); const reservedIds = new Set(groups.map((group) => group.id)); const renamedGroups = new Map(); for (const group of fallback.groups) { let id = group.id; while (reservedIds.has(id)) id = `custom-${id}`; reservedIds.add(id); renamedGroups.set(group.id, id); groups.push({ ...group, id, title: id.replace(/-/gu, " ") }); } for (const card of fallback.tools) { cards.push({ ...card, primaryGroup: renamedGroups.get(card.primaryGroup) ?? card.primaryGroup }); } const overridden = applyCatalogOverrides(groups, cards, constraints.groupOverrides); return { catalog: { version: CATALOG_VERSION, manifestHash, generatedAt: new Date().toISOString(), generatedBy: "my-pi bundle seed", groups: overridden.groups, tools: overridden.tools, }, coveredNames, unknownTools, }; } export function catalogPreservesBundleAssignments( candidate: ToolCatalog, bundleCatalog: ToolCatalog, coveredNames: Set, ): boolean { const expected = new Map( bundleCatalog.tools.filter((tool) => coveredNames.has(tool.name)).map((tool) => [tool.name, tool.primaryGroup]), ); const actual = new Map(candidate.tools.map((tool) => [tool.name, tool.primaryGroup])); return [...expected].every(([name, group]) => actual.get(name) === group); }