{"version":3,"file":"self-knowledge.d.ts","sourceRoot":"","sources":["../../../src/extensions/core/self-knowledge.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAOH,OAAO,KAAK,EAAyB,YAAY,EAAkB,MAAM,gCAAgC,CAAC;AAyI1G,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,YAAY,GAAG,IAAI,CAsE1D;AAED,uEAAuE;AACvE,wBAAgB,kBAAkB,IAAI,IAAI,CAGzC","sourcesContent":["/**\n * SearchHooCode — retrieval over hoocode's own docs and the capabilities this\n * session actually has.\n *\n * The system prompt already lists every shipped doc with a one-line summary\n * (see `core/self-docs.ts`), which answers \"which file covers extensions?\".\n * What it cannot answer is anything *inside* a file: `extensions.md` alone is\n * over a thousand lines, and reading it whole to find one heading costs more\n * context than the rest of the prompt put together. This indexes docs at the\n * heading level so a question lands on a section and a line number.\n *\n * It also registers the capability kinds the registry has always declared but\n * nobody ever filled — `skill`, `command`, `agent`, `plugin-installed`\n * (registry.ts notes §6.4 left them eager). Those are eager in the prompt, so\n * indexing them is not about reachability; it is about \"what can you do?\"\n * having one place that answers it rather than the model reciting whichever\n * list happens to be in front of it.\n *\n * `mcp-tool` is deliberately out of scope: ResolveMcpTools owns that kind\n * because finding an MCP tool and making it callable are the same action\n * there, and a second searcher that returns un-resolvable names would be a\n * worse answer, not an extra one.\n */\n\nimport type { Static } from \"typebox\";\nimport { Type } from \"typebox\";\nimport { ensureDenseIndex } from \"../../core/capabilities/dense.js\";\nimport { type CapabilityDoc, getCapabilities, registerCapabilities } from \"../../core/capabilities/registry.js\";\nimport { resetCapabilitySearch, searchCapabilities } from \"../../core/capabilities/search.js\";\nimport type { BeforeAgentStartEvent, ExtensionAPI, ToolDefinition } from \"../../core/extensions/types.js\";\nimport { listSelfDocSections, sectionLabel } from \"../../core/self-docs.js\";\n\nconst SEARCH_HOOCODE_TOOL_NAME = \"SearchHooCode\";\n\n/** Kinds this tool searches. `mcp-tool` belongs to ResolveMcpTools. */\nconst SEARCHABLE = [\"doc\", \"skill\", \"command\", \"agent\", \"plugin-installed\"] as const;\n\nconst DEFAULT_LIMIT = 8;\nconst MAX_LIMIT = 25;\n\n/** Registered once per process; the docs are read-only install content. */\nlet docsRegistered = false;\n\n/**\n * Index the shipped docs, the first time anyone asks.\n *\n * Lazy because it reads thirty files and produces roughly a thousand sections.\n * A session that never asks hoocode about itself should not pay for that at\n * startup, and the cost is invisible once paid — `listSelfDocSections` caches.\n */\nfunction ensureDocsRegistered(): void {\n\tif (docsRegistered) return;\n\tdocsRegistered = true;\n\n\tconst sections = listSelfDocSections();\n\tif (sections.length === 0) return;\n\n\tregisterCapabilities(\n\t\t\"doc\",\n\t\tsections.map(\n\t\t\t(section): CapabilityDoc => ({\n\t\t\t\tid: `doc:${section.id}`,\n\t\t\t\tkind: \"doc\",\n\t\t\t\tname: sectionLabel(section),\n\t\t\t\tdescription: section.excerpt,\n\t\t\t\tsource: section.path,\n\t\t\t\t// The body is withheld until the model reads the file, which is\n\t\t\t\t// exactly the condition this flag records.\n\t\t\t\tdeferred: true,\n\t\t\t}),\n\t\t),\n\t);\n\tresetCapabilitySearch();\n}\n\n/** Line-number lookup for rendering a hit, keyed by the id we registered under. */\nfunction sectionIndex(): Map<string, { path: string; line: number }> {\n\tconst index = new Map<string, { path: string; line: number }>();\n\tfor (const section of listSelfDocSections()) {\n\t\tindex.set(`doc:${section.id}`, { path: section.path, line: section.line });\n\t}\n\treturn index;\n}\n\n/**\n * Mirror the session's skills, commands, and subagents into the registry.\n *\n * Driven from `before_agent_start` because that is where the assembled\n * `systemPromptOptions` carries the skills and agents the session actually\n * loaded — re-discovering them from disk here would risk disagreeing with what\n * the model was told. Guarded by a signature so a steady session registers once\n * instead of dropping the lexical index on every turn.\n */\nlet lastSignature = \"\";\n\nfunction registerSessionCapabilities(event: BeforeAgentStartEvent, hoo: ExtensionAPI): void {\n\tconst skills = event.systemPromptOptions.skills ?? [];\n\tconst agents = event.systemPromptOptions.agents ?? [];\n\tlet commands: Array<{ name: string; description?: string }> = [];\n\ttry {\n\t\tcommands = hoo.getCommands();\n\t} catch {\n\t\t// Commands are an interactive-mode concern; a headless session may not\n\t\t// have them. Missing commands is not a reason to skip skills and agents.\n\t\tcommands = [];\n\t}\n\n\tconst signature = [\n\t\tskills.map((s) => s.name).join(\",\"),\n\t\tagents.map((a) => a.name).join(\",\"),\n\t\tcommands.map((c) => c.name).join(\",\"),\n\t].join(\"|\");\n\tif (signature === lastSignature) return;\n\tlastSignature = signature;\n\n\tregisterCapabilities(\n\t\t\"skill\",\n\t\tskills.map(\n\t\t\t(skill): CapabilityDoc => ({\n\t\t\t\tid: `skill:${skill.name}`,\n\t\t\t\tkind: \"skill\",\n\t\t\t\tname: skill.name,\n\t\t\t\tdescription: skill.description ?? \"\",\n\t\t\t\tsource: skill.filePath,\n\t\t\t\t// Eager: the prompt already lists these, so a hit is a convenience.\n\t\t\t\tdeferred: false,\n\t\t\t}),\n\t\t),\n\t);\n\tregisterCapabilities(\n\t\t\"agent\",\n\t\tagents.map(\n\t\t\t(agent): CapabilityDoc => ({\n\t\t\t\tid: `agent:${agent.name}`,\n\t\t\t\tkind: \"agent\",\n\t\t\t\tname: agent.name,\n\t\t\t\tdescription: agent.description ?? \"\",\n\t\t\t\tdeferred: false,\n\t\t\t}),\n\t\t),\n\t);\n\tregisterCapabilities(\n\t\t\"command\",\n\t\tcommands.map(\n\t\t\t(command): CapabilityDoc => ({\n\t\t\t\tid: `command:${command.name}`,\n\t\t\t\tkind: \"command\",\n\t\t\t\tname: `/${command.name}`,\n\t\t\t\tdescription: command.description ?? \"\",\n\t\t\t\tdeferred: false,\n\t\t\t}),\n\t\t),\n\t);\n\tresetCapabilitySearch();\n}\n\nfunction describeHit(doc: CapabilityDoc, sections: Map<string, { path: string; line: number }>): string {\n\tif (doc.kind === \"doc\") {\n\t\tconst where = sections.get(doc.id);\n\t\tconst location = where ? `${where.path}:${where.line}` : (doc.source ?? \"\");\n\t\treturn `- [doc] ${doc.name}\\n  read ${location}\\n  ${doc.description}`;\n\t}\n\tconst summary = doc.description ? ` — ${doc.description}` : \"\";\n\treturn `- [${doc.kind}] ${doc.name}${summary}`;\n}\n\nexport function setupSelfKnowledge(hoo: ExtensionAPI): void {\n\thoo.on(\"before_agent_start\", (event: BeforeAgentStartEvent) => {\n\t\tregisterSessionCapabilities(event, hoo);\n\t});\n\n\tconst params = Type.Object(\n\t\t{\n\t\t\tquery: Type.String({\n\t\t\t\tdescription:\n\t\t\t\t\t\"What you want to know about hoocode, in your own words — 'how do I write an extension', \" +\n\t\t\t\t\t\"'where are sessions stored', 'can it run subagents'.\",\n\t\t\t}),\n\t\t\tlimit: Type.Optional(Type.Number({ description: `Maximum results. Default ${DEFAULT_LIMIT}.` })),\n\t\t},\n\t\t{ additionalProperties: false },\n\t);\n\n\thoo.registerTool({\n\t\tname: SEARCH_HOOCODE_TOOL_NAME,\n\t\tlabel: SEARCH_HOOCODE_TOOL_NAME,\n\t\tdescription:\n\t\t\t\"Search hoocode's own documentation and the capabilities loaded in this session (skills, slash \" +\n\t\t\t\"commands, subagents, installed plugins). Use it when the user asks what hoocode can do, how one of \" +\n\t\t\t\"its features works, or how to configure or extend it. Doc results come back as a file path and line \" +\n\t\t\t\"number — read that range for the answer rather than replying from the excerpt alone. \" +\n\t\t\t\"MCP tools are not covered here; find those with ResolveMcpTools.\",\n\t\tpromptSnippet: \"Search hoocode's own docs and this session's capabilities by describing what you need.\",\n\t\tpromptGuidelines: [\n\t\t\t\"For questions about hoocode itself — its features, configuration, or how to extend it — use SearchHooCode and read the section it points at instead of answering from memory.\",\n\t\t],\n\t\tparameters: params,\n\t\texecutionMode: \"parallel\",\n\t\tasync execute(_toolCallId: string, args: Static<typeof params>) {\n\t\t\tconst query = args.query?.trim();\n\t\t\tif (!query) {\n\t\t\t\treturn { content: [{ type: \"text\" as const, text: \"Provide a query.\" }], details: undefined };\n\t\t\t}\n\n\t\t\tensureDocsRegistered();\n\t\t\tconst limit = Math.min(Math.max(1, Math.trunc(args.limit ?? DEFAULT_LIMIT)), MAX_LIMIT);\n\n\t\t\t// Fire-and-forget over everything registered, not just the docs: the\n\t\t\t// dense store is shared, so indexing a subset here would evict whatever\n\t\t\t// the MCP resolver indexed. The lexical leg answers this call either way.\n\t\t\tvoid ensureDenseIndex(getCapabilities()).catch(() => {});\n\n\t\t\tconst { hits, legs } = await searchCapabilities(query, { kinds: [...SEARCHABLE], limit });\n\t\t\tif (hits.length === 0) {\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\ttype: \"text\" as const,\n\t\t\t\t\t\t\ttext: `Nothing matched \"${query}\". The docs are listed in the system prompt under \"About hoocode itself\" — read the most likely file directly.`,\n\t\t\t\t\t\t},\n\t\t\t\t\t],\n\t\t\t\t\tdetails: undefined,\n\t\t\t\t};\n\t\t\t}\n\n\t\t\tconst sections = sectionIndex();\n\t\t\tconst body = hits.map((hit) => describeHit(hit.doc, sections)).join(\"\\n\");\n\t\t\t// Say which legs answered: lexical-only on a conceptual query is a\n\t\t\t// weaker result, and the caller should be able to see that.\n\t\t\tconst note = legs.includes(\"dense\") ? \"\" : \"\\n(Lexical match only — try naming the feature if this missed.)\";\n\t\t\treturn {\n\t\t\t\tcontent: [{ type: \"text\" as const, text: `Results for \"${query}\":\\n${body}${note}` }],\n\t\t\t\tdetails: undefined,\n\t\t\t};\n\t\t},\n\t} as ToolDefinition);\n}\n\n/** Tests, and anything that relocates the package root mid-process. */\nexport function resetSelfKnowledge(): void {\n\tdocsRegistered = false;\n\tlastSignature = \"\";\n}\n"]}