import { join } from "node:path"; import { BUILTIN_COMMANDS } from "../constants.ts"; import { canonicalName, compactSourceRef, displayName, extractPackageNameFromPath, makeItemKey } from "../identity.ts"; import type { HelpState, IndexedItem, McpServerEntry, PackageEntry, ResourceIndex, ResourceScope } from "../types.ts"; import { upsertItem } from "../state/usage.ts"; import { mergePackageEntries, readPackagesFromSettings } from "./settings.ts"; import { readMcpServersFromConfig } from "./mcp.ts"; export interface CommandLike { name: string; description?: string; sourceInfo: { source?: string; scope?: ResourceScope; origin?: string }; } export interface ToolLike { name: string; description?: string; sourceInfo: { source?: string; scope?: ResourceScope; origin?: string }; } export interface ResourceRuntimeInput { commands: CommandLike[]; tools: ToolLike[]; packages: PackageEntry[]; mcpServers: McpServerEntry[]; state: HelpState; now?: number; } function sourceLabelForPackage(pkg: string): string { if (pkg.startsWith("npm:")) return pkg; if (pkg.startsWith("git:")) return "git"; if (pkg.startsWith("http://") || pkg.startsWith("https://")) return "git"; if (pkg.startsWith("./") || pkg.startsWith("../") || pkg.startsWith("/")) return "本地包"; return `npm:${pkg}`; } function packageLabelFromSource(source: string): string { const pkg = extractPackageNameFromPath(source) ?? (source.startsWith("npm:") ? source.slice("npm:".length) : undefined); return pkg ? `npm:${pkg}` : sourceLabelForPackage(source); } function sourceLabelForCommand(sourceInfo: { source?: string; scope?: ResourceScope; origin?: string }): string { const source = sourceInfo.source?.replace(/\\/g, "/"); if (!source) return "未知"; if (source === "builtin") return "内置"; if (source === "sdk") return "SDK"; if (source === "local") return sourceInfo.scope === "project" ? "项目本地" : "全局本地"; if (sourceInfo.origin === "package") return packageLabelFromSource(source); if (source === "extension") return "扩展"; if (source === "prompt") return "模板"; if (source === "skill") return "技能"; if (source.startsWith("npm:") || source.startsWith("git:") || source.startsWith("http://") || source.startsWith("https://")) return source; if (source.startsWith("./") || source.startsWith("../")) return sourceInfo.scope === "project" ? "项目本地" : "本地包"; if (source.startsWith("/")) { const pkg = extractPackageNameFromPath(source); if (pkg) return `npm:${pkg}`; return sourceInfo.scope === "project" ? "项目本地" : "全局本地"; } return source; } function sourceLabelForTool(sourceInfo: { source?: string; scope?: ResourceScope; origin?: string }): string { const source = sourceInfo.source?.replace(/\\/g, "/"); if (!source) return "未知工具"; if (source === "builtin") return "内置工具"; if (source === "sdk") return "SDK 工具"; if (source === "local") return sourceInfo.scope === "project" ? "项目扩展" : "全局扩展"; if (sourceInfo.origin === "package") return packageLabelFromSource(source); if (source.startsWith("npm:") || source.startsWith("git:") || source.startsWith("http://") || source.startsWith("https://")) return source; if (source.startsWith("/")) { const pkg = extractPackageNameFromPath(source); if (pkg) return `npm:${pkg}`; return sourceInfo.scope === "project" ? "项目扩展" : "全局扩展"; } return source; } function sourceLabelForMcp(server: McpServerEntry): string { const bits = [server.mode === "http" ? "HTTP" : "stdio"]; if (server.lifecycle) bits.push(server.lifecycle); return bits.join(" · "); } function itemBase(kind: IndexedItem["kind"], name: string, sourceRef: string): Pick { const canonical = canonicalName(kind, name); return { key: makeItemKey(kind, canonical, sourceRef), kind, name: canonical, displayName: displayName(kind, canonical), sourceRef, useCount: 0, }; } export function buildResourceIndex(input: ResourceRuntimeInput): ResourceIndex { const { commands, tools, state } = input; const packages = mergePackageEntries(input.packages); const now = input.now ?? Date.now(); const mergedMcp = new Map(); for (const server of input.mcpServers) mergedMcp.set(server.name, server); const commandItems: IndexedItem[] = []; const seenCommandKeys = new Set(); for (const command of commands) { const sourceRef = compactSourceRef(command.sourceInfo.source ?? command.sourceInfo.origin ?? command.sourceInfo.scope); const base = itemBase("command", command.name, sourceRef); const item = upsertItem(state, { ...base, description: command.description, sourceLabel: sourceLabelForCommand(command.sourceInfo), scope: command.sourceInfo.scope, packageName: command.sourceInfo.source, lastSeenAt: now, }); commandItems.push(item); seenCommandKeys.add(item.key); } for (const builtin of BUILTIN_COMMANDS) { const sourceRef = "builtin"; const base = itemBase("command", builtin.name, sourceRef); if (seenCommandKeys.has(base.key)) continue; commandItems.push(upsertItem(state, { ...base, description: builtin.description, sourceLabel: "内置", scope: "builtin", lastSeenAt: now, })); } const toolItems: IndexedItem[] = []; for (const tool of tools) { if (tool.name === "mcp") continue; const sourceRef = compactSourceRef(tool.sourceInfo.source ?? tool.sourceInfo.origin ?? tool.sourceInfo.scope); const base = itemBase("tool", tool.name, sourceRef); toolItems.push(upsertItem(state, { ...base, description: tool.description, sourceLabel: sourceLabelForTool(tool.sourceInfo), scope: tool.sourceInfo.scope, packageName: tool.sourceInfo.source, lastSeenAt: now, })); } const packageItems: IndexedItem[] = []; for (const pkg of packages) { const sourceRef = compactSourceRef(pkg.name); const base = itemBase("package", pkg.name, sourceRef); packageItems.push(upsertItem(state, { ...base, description: "已安装包", sourceLabel: sourceLabelForPackage(pkg.name), scope: pkg.scope, packageName: pkg.name, lastSeenAt: now, })); } const mcpItems: IndexedItem[] = []; const gatewayBase = itemBase("mcp", "mcp", "mcp:gateway"); mcpItems.push(upsertItem(state, { ...gatewayBase, description: "显示 MCP 服务器状态", sourceLabel: "内置网关", scope: "builtin", lastSeenAt: now, })); for (const server of mergedMcp.values()) { const sourceRef = `mcp:${server.name}`; const base = itemBase("mcp", server.name, sourceRef); mcpItems.push(upsertItem(state, { ...base, description: server.mode === "http" ? "HTTP 服务器" : "stdio 服务器", sourceLabel: sourceLabelForMcp(server), scope: "user", packageName: undefined, lastSeenAt: now, })); } state.lastRefresh = now; return { commands: commandItems, tools: toolItems, mcp: mcpItems, packages: packageItems }; } export function readRuntimePackages(agentDir: string, cwd: string): PackageEntry[] { return [ ...readPackagesFromSettings(join(agentDir, "settings.json"), "user"), ...readPackagesFromSettings(join(cwd, ".pi", "settings.json"), "project"), ]; } export function readRuntimeMcpServers(agentDir: string, homeDir: string, cwd: string): McpServerEntry[] { return [ ...readMcpServersFromConfig(join(homeDir, ".config", "mcp", "mcp.json")), ...readMcpServersFromConfig(join(agentDir, "mcp.json")), ...readMcpServersFromConfig(join(cwd, ".mcp.json")), ...readMcpServersFromConfig(join(cwd, ".pi", "mcp.json")), ]; }