@kolisachint/hoocode-agent 0.5.52 → 0.5.53

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (114) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/dist/cli/args.d.ts +2 -2
  3. package/dist/cli/args.d.ts.map +1 -1
  4. package/dist/cli/args.js +9 -8
  5. package/dist/cli/args.js.map +1 -1
  6. package/dist/core/agent-frontmatter.d.ts +8 -2
  7. package/dist/core/agent-frontmatter.d.ts.map +1 -1
  8. package/dist/core/agent-frontmatter.js +13 -7
  9. package/dist/core/agent-frontmatter.js.map +1 -1
  10. package/dist/core/agent-session-services.d.ts +1 -1
  11. package/dist/core/agent-session-services.d.ts.map +1 -1
  12. package/dist/core/agent-session-services.js +1 -1
  13. package/dist/core/agent-session-services.js.map +1 -1
  14. package/dist/core/agent-session.d.ts.map +1 -1
  15. package/dist/core/agent-session.js +2 -2
  16. package/dist/core/agent-session.js.map +1 -1
  17. package/dist/core/embsearch/embsearch-service.d.ts +1 -1
  18. package/dist/core/embsearch/embsearch-service.d.ts.map +1 -1
  19. package/dist/core/embsearch/embsearch-service.js +1 -1
  20. package/dist/core/embsearch/embsearch-service.js.map +1 -1
  21. package/dist/core/extensions/plugins/authoring.d.ts.map +1 -1
  22. package/dist/core/extensions/plugins/authoring.js +1 -1
  23. package/dist/core/extensions/plugins/authoring.js.map +1 -1
  24. package/dist/core/extensions/plugins/formats/shared.d.ts +1 -1
  25. package/dist/core/extensions/plugins/formats/shared.d.ts.map +1 -1
  26. package/dist/core/extensions/plugins/formats/shared.js +1 -1
  27. package/dist/core/extensions/plugins/formats/shared.js.map +1 -1
  28. package/dist/core/extensions/plugins/formats/types.d.ts +1 -1
  29. package/dist/core/extensions/plugins/formats/types.d.ts.map +1 -1
  30. package/dist/core/extensions/plugins/formats/types.js.map +1 -1
  31. package/dist/core/external-tools.d.ts +1 -1
  32. package/dist/core/external-tools.d.ts.map +1 -1
  33. package/dist/core/external-tools.js +2 -2
  34. package/dist/core/external-tools.js.map +1 -1
  35. package/dist/core/sdk.d.ts +2 -2
  36. package/dist/core/sdk.d.ts.map +1 -1
  37. package/dist/core/sdk.js +3 -3
  38. package/dist/core/sdk.js.map +1 -1
  39. package/dist/core/self-docs.d.ts +1 -1
  40. package/dist/core/self-docs.d.ts.map +1 -1
  41. package/dist/core/self-docs.js +1 -1
  42. package/dist/core/self-docs.js.map +1 -1
  43. package/dist/core/settings-defaults.d.ts +1 -1
  44. package/dist/core/settings-defaults.d.ts.map +1 -1
  45. package/dist/core/settings-defaults.js +1 -1
  46. package/dist/core/settings-defaults.js.map +1 -1
  47. package/dist/core/settings-manager.d.ts +2 -2
  48. package/dist/core/settings-manager.d.ts.map +1 -1
  49. package/dist/core/settings-manager.js +13 -5
  50. package/dist/core/settings-manager.js.map +1 -1
  51. package/dist/core/settings-types.d.ts +1 -1
  52. package/dist/core/settings-types.d.ts.map +1 -1
  53. package/dist/core/settings-types.js.map +1 -1
  54. package/dist/core/system-prompt.d.ts.map +1 -1
  55. package/dist/core/system-prompt.js +5 -5
  56. package/dist/core/system-prompt.js.map +1 -1
  57. package/dist/core/tools/index.d.ts +2 -2
  58. package/dist/core/tools/index.d.ts.map +1 -1
  59. package/dist/core/tools/index.js +3 -3
  60. package/dist/core/tools/index.js.map +1 -1
  61. package/dist/core/tools/plugins.d.ts.map +1 -1
  62. package/dist/core/tools/plugins.js +2 -2
  63. package/dist/core/tools/plugins.js.map +1 -1
  64. package/dist/core/tools/propose-plugin.d.ts.map +1 -1
  65. package/dist/core/tools/propose-plugin.js +1 -1
  66. package/dist/core/tools/propose-plugin.js.map +1 -1
  67. package/dist/core/tools/search.d.ts +1 -1
  68. package/dist/core/tools/search.d.ts.map +1 -1
  69. package/dist/core/tools/search.js +5 -5
  70. package/dist/core/tools/search.js.map +1 -1
  71. package/dist/init-templates.generated.d.ts.map +1 -1
  72. package/dist/init-templates.generated.js +9 -9
  73. package/dist/init-templates.generated.js.map +1 -1
  74. package/dist/main.d.ts.map +1 -1
  75. package/dist/main.js +7 -7
  76. package/dist/main.js.map +1 -1
  77. package/dist/modes/interactive/components/tool-chain-summary.d.ts.map +1 -1
  78. package/dist/modes/interactive/components/tool-chain-summary.js +1 -1
  79. package/dist/modes/interactive/components/tool-chain-summary.js.map +1 -1
  80. package/dist/modes/interactive/components/tool-signal.d.ts.map +1 -1
  81. package/dist/modes/interactive/components/tool-signal.js +7 -7
  82. package/dist/modes/interactive/components/tool-signal.js.map +1 -1
  83. package/dist/modes/interactive/components/tree-selector.d.ts.map +1 -1
  84. package/dist/modes/interactive/components/tree-selector.js +2 -2
  85. package/dist/modes/interactive/components/tree-selector.js.map +1 -1
  86. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  87. package/dist/modes/interactive/interactive-mode.js +2 -2
  88. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  89. package/docs/extensions.md +1 -1
  90. package/docs/modes.md +1 -1
  91. package/docs/quickstart.md +1 -1
  92. package/docs/sdk.md +2 -2
  93. package/docs/settings.md +3 -3
  94. package/docs/usage.md +4 -4
  95. package/examples/agents/fork-reviewer.md +1 -1
  96. package/examples/agents/orchestrator.md +1 -1
  97. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  98. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  99. package/examples/extensions/plan-mode/index.ts +1 -1
  100. package/examples/extensions/preset.ts +1 -1
  101. package/examples/extensions/sandbox/package.json +1 -1
  102. package/examples/extensions/truncated-tool.ts +1 -1
  103. package/examples/extensions/with-deps/package.json +1 -1
  104. package/examples/sdk/05-tools.ts +3 -3
  105. package/package.json +4 -4
  106. package/templates/agents/code-review.md +1 -1
  107. package/templates/agents/explore.md +2 -2
  108. package/templates/agents/general-purpose.md +2 -2
  109. package/templates/agents/plan.md +2 -2
  110. package/templates/agents/security-review.md +1 -1
  111. package/templates/modes/debug/system.md +1 -1
  112. package/templates/skills/artifact-design/SKILL.md +1 -1
  113. package/templates/skills/canvas-design/SKILL.md +1 -1
  114. package/templates/skills/plugin-authoring/SKILL.md +1 -1
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * The external (Rust) binaries hoocode can use, and what each one is worth.
3
3
  *
4
- * hoocode is self-sufficient without any of them: `search` and @-file
4
+ * hoocode is self-sufficient without any of them: `SearchCodebase` and @-file
5
5
  * autocomplete fall back to pure-JS implementations, and the features that have
6
6
  * no fallback (web, voice) are off or inert rather than broken. These binaries are an
7
7
  * *expansion* layer, which is exactly why they were invisible - nothing failed
@@ -51,7 +51,7 @@ export const EXTERNAL_TOOLS = [
51
51
  acquisition: "on-demand",
52
52
  env: ["HOOCODE_EMBSEARCH_BINARY"],
53
53
  dependentRows: ["group:embsearch"],
54
- settingsKeys: ["enableEmbsearchTools", "embsearchBinaryPath", "embsearchThresholdBytes"],
54
+ settingsKeys: ["enableSemanticIndex", "embsearchBinaryPath", "embsearchThresholdBytes"],
55
55
  },
56
56
  {
57
57
  tool: "webtools",
@@ -1 +1 @@
1
- {"version":3,"file":"external-tools.js","sourceRoot":"","sources":["../../src/core/external-tools.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAE,aAAa,EAAE,aAAa,EAA4C,MAAM,2BAA2B,CAAC;AAkCnH;;;GAGG;AACH,MAAM,CAAC,MAAM,cAAc,GAA+B;IACzD;QACC,IAAI,EAAE,IAAI;QACV,KAAK,EAAE,cAAc;QACrB,OAAO,EAAE,+BAA+B;QACxC,OAAO,EAAE,CAAC,8DAA8D,CAAC;QACzE,QAAQ,EACP,+JAA+J;QAChK,WAAW,EAAE,SAAS;QACtB,GAAG,EAAE,CAAC,mBAAmB,EAAE,oEAAoE,CAAC;QAChG,aAAa,EAAE,EAAE;QACjB,YAAY,EAAE,EAAE;KAChB;IACD;QACC,IAAI,EAAE,IAAI;QACV,KAAK,EAAE,IAAI;QACX,OAAO,EAAE,gCAAgC;QACzC,OAAO,EAAE,CAAC,4EAA4E,CAAC;QACvF,QAAQ,EACP,wIAAwI;QACzI,WAAW,EAAE,SAAS;QACtB,GAAG,EAAE,CAAC,mBAAmB,EAAE,oEAAoE,CAAC;QAChG,aAAa,EAAE,EAAE;QACjB,YAAY,EAAE,EAAE;KAChB;IACD;QACC,IAAI,EAAE,WAAW;QACjB,KAAK,EAAE,4BAA4B;QACnC,OAAO,EAAE,wEAAwE;QACjF,OAAO,EAAE;YACR,kDAAkD;YAClD,oEAAoE;SACpE;QACD,QAAQ,EACP,yQAAyQ;QAC1Q,WAAW,EAAE,WAAW;QACxB,GAAG,EAAE,CAAC,0BAA0B,CAAC;QACjC,aAAa,EAAE,CAAC,iBAAiB,CAAC;QAClC,YAAY,EAAE,CAAC,sBAAsB,EAAE,qBAAqB,EAAE,yBAAyB,CAAC;KACxF;IACD;QACC,IAAI,EAAE,UAAU;QAChB,KAAK,EAAE,+BAA+B;QACtC,OAAO,EAAE,yEAAyE;QAClF,OAAO,EAAE,CAAC,mBAAmB,EAAE,oBAAoB,CAAC;QACpD,QAAQ,EACP,6IAA6I;QAC9I,WAAW,EAAE,WAAW;QACxB,GAAG,EAAE,CAAC,yBAAyB,EAAE,0BAA0B,CAAC;QAC5D,aAAa,EAAE,CAAC,WAAW,EAAE,uBAAuB,CAAC;QACrD,YAAY,EAAE,CAAC,gBAAgB,EAAE,sBAAsB,CAAC;KACxD;IACD;QACC,IAAI,EAAE,YAAY;QAClB,KAAK,EAAE,0BAA0B;QACjC,OAAO,EAAE,mDAAmD;QAC5D,OAAO,EAAE,CAAC,wCAAwC,CAAC;QACnD,QAAQ,EAAE,wEAAwE;QAClF,WAAW,EAAE,WAAW;QACxB,GAAG,EAAE,CAAC,gBAAgB,EAAE,2BAA2B,EAAE,uBAAuB,CAAC;QAC7E,aAAa,EAAE,CAAC,kBAAkB,CAAC;QACnC,YAAY,EAAE,CAAC,iBAAiB,CAAC;KACjC;CACD,CAAC;AAWF,sEAAsE;AACtE,MAAM,UAAU,WAAW,CAAC,MAA0B,EAAU;IAC/D,IAAI,CAAC,MAAM,CAAC,SAAS;QAAE,OAAO,MAAM,CAAC,YAAY,CAAC,CAAC,CAAC,eAAe,CAAC,CAAC,CAAC,aAAa,CAAC;IACpF,QAAQ,MAAM,CAAC,MAAM,EAAE,CAAC;QACvB,KAAK,UAAU;YACd,OAAO,cAAc,CAAC;QACvB,KAAK,SAAS;YACb,OAAO,WAAW,CAAC;QACpB,KAAK,MAAM;YACV,OAAO,QAAQ,CAAC;QACjB;YACC,OAAO,WAAW,CAAC;IACrB,CAAC;AAAA,CACD;AAED;;;GAGG;AACH,MAAM,UAAU,qBAAqB,GAAyB;IAC7D,MAAM,OAAO,GAAG,aAAa,EAAE,CAAC;IAChC,MAAM,OAAO,GAAG,OAAO,CAAC,QAAQ,KAAK,SAAS,CAAC;IAC/C,OAAO,cAAc,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC;QAClC,MAAM,MAAM,GAAG,aAAa,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QACvC,OAAO;YACN,GAAG,GAAG;YACN,GAAG,MAAM;YACT,SAAS,EAAE,MAAM,CAAC,IAAI,KAAK,IAAI;YAC/B,YAAY,EAAE,CAAC,OAAO,IAAI,CAAC,OAAO,IAAI,GAAG,CAAC,WAAW,KAAK,QAAQ;SAClE,CAAC;IAAA,CACF,CAAC,CAAC;AAAA,CACH;AAED,4EAA4E;AAC5E,MAAM,UAAU,aAAa,CAAC,QAAuC,EAAmC;IACvG,MAAM,KAAK,GAAG,IAAI,GAAG,EAA8B,CAAC;IACpD,KAAK,MAAM,MAAM,IAAI,QAAQ,EAAE,CAAC;QAC/B,KAAK,MAAM,KAAK,IAAI,MAAM,CAAC,aAAa;YAAE,KAAK,CAAC,GAAG,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC;IACpE,CAAC;IACD,OAAO,KAAK,CAAC;AAAA,CACb","sourcesContent":["/**\n * The external (Rust) binaries hoocode can use, and what each one is worth.\n *\n * hoocode is self-sufficient without any of them: `search` and @-file\n * autocomplete fall back to pure-JS implementations, and the features that have\n * no fallback (web, voice) are off or inert rather than broken. These binaries are an\n * *expansion* layer, which is exactly why they were invisible - nothing failed\n * loudly enough to tell anyone they existed.\n *\n * This module is the single description of that layer. The `/settings` pane\n * renders it; the same table says which settings rows are gated on a binary, so\n * a row that cannot do anything today can say so instead of lying.\n */\n\nimport { getToolStatus, isOfflineMode, type ManagedTool, type ManagedToolStatus } from \"../utils/tools-manager.js\";\n\n/** How hoocode gets a binary it does not already have. */\nexport type Acquisition =\n\t/** Fetched in the background at startup. */\n\t| \"startup\"\n\t/** Fetched the first time the feature is actually used. */\n\t| \"on-demand\"\n\t/** Never fetched implicitly; install it yourself or point the env var at it. */\n\t| \"manual\";\n\nexport interface ExternalToolDoc {\n\ttool: ManagedTool;\n\t/** Row label in the pane. */\n\tlabel: string;\n\t/** What the binary does for hoocode, in one line. */\n\tsummary: string;\n\t/** What it turns on, feature by feature. */\n\tenables: string[];\n\t/** What hoocode does instead when it is missing. Never \"nothing works\". */\n\tfallback: string;\n\tacquisition: Acquisition;\n\t/** Env vars that change how this binary is resolved or driven. */\n\tenv: string[];\n\t/**\n\t * `/settings` row ids whose effect depends on this binary. A row listed here\n\t * is annotated (not hidden) while the binary is missing: hiding it would\n\t * recreate the discoverability hole this whole surface exists to close.\n\t */\n\tdependentRows: string[];\n\t/** settings.json keys this binary gates, for docs and search. */\n\tsettingsKeys: string[];\n}\n\n/**\n * Ordered so the two that silently make everything faster come first, then the\n * three that add capability hoocode does not otherwise have.\n */\nexport const EXTERNAL_TOOLS: readonly ExternalToolDoc[] = [\n\t{\n\t\ttool: \"rg\",\n\t\tlabel: \"ripgrep (rg)\",\n\t\tsummary: \"Fast path for content search.\",\n\t\tenables: [\"the lexical half of search runs rg instead of the JS scanner\"],\n\t\tfallback:\n\t\t\t\"A pure-JS scanner produces the same match shape, so results are identical - it is materially slower on large trees and respects fewer ignore-file edge cases.\",\n\t\tacquisition: \"startup\",\n\t\tenv: [\"HOOCODE_RG_BINARY\", \"HOOCODE_NATIVE_SEARCH=1 forces the JS path even when rg is present\"],\n\t\tdependentRows: [],\n\t\tsettingsKeys: [],\n\t},\n\t{\n\t\ttool: \"fd\",\n\t\tlabel: \"fd\",\n\t\tsummary: \"Fast path for filename search.\",\n\t\tenables: [\"@-file autocomplete lists paths with fd instead of the JS directory walker\"],\n\t\tfallback:\n\t\t\t\"A JS walker produces the same result shape - slower on large trees, and glob/ignore handling is the JS approximation rather than fd's.\",\n\t\tacquisition: \"startup\",\n\t\tenv: [\"HOOCODE_FD_BINARY\", \"HOOCODE_NATIVE_SEARCH=1 forces the JS path even when fd is present\"],\n\t\tdependentRows: [],\n\t\tsettingsKeys: [],\n\t},\n\t{\n\t\ttool: \"embsearch\",\n\t\tlabel: \"embsearch (semantic index)\",\n\t\tsummary: \"Local embedding index. The only source of semantic ranking in hoocode.\",\n\t\tenables: [\n\t\t\t\"search fuses semantic hits with its lexical hits\",\n\t\t\t\"MCP/capability deferral ranks tools by meaning rather than keyword\",\n\t\t],\n\t\tfallback:\n\t\t\t\"search is lexical-only and capability lookup ranks lexically. Nothing errors; queries phrased by intent rather than by token simply rank worse. Requires the ONNX build - the mock build is rejected on purpose, because it would rank at random while looking healthy.\",\n\t\tacquisition: \"on-demand\",\n\t\tenv: [\"HOOCODE_EMBSEARCH_BINARY\"],\n\t\tdependentRows: [\"group:embsearch\"],\n\t\tsettingsKeys: [\"enableEmbsearchTools\", \"embsearchBinaryPath\", \"embsearchThresholdBytes\"],\n\t},\n\t{\n\t\ttool: \"webtools\",\n\t\tlabel: \"webtools (webfetch/websearch)\",\n\t\tsummary: \"The network layer. Without it hoocode has no way to reach the internet.\",\n\t\tenables: [\"the webfetch tool\", \"the websearch tool\"],\n\t\tfallback:\n\t\t\t\"Both tools return an error when called. The web tool group is off by default, so a missing binary is invisible until you turn the group on.\",\n\t\tacquisition: \"on-demand\",\n\t\tenv: [\"HOOCODE_WEBTOOLS_BINARY\", \"HOOCODE_WEBTOOLS_TIMEOUT\"],\n\t\tdependentRows: [\"group:web\", \"webtools-timeout-secs\"],\n\t\tsettingsKeys: [\"enableWebTools\", \"webtools.timeoutSecs\"],\n\t},\n\t{\n\t\ttool: \"voicetools\",\n\t\tlabel: \"voicetools (voice input)\",\n\t\tsummary: \"Microphone capture and transcription for the TUI.\",\n\t\tenables: [\"push-to-talk voice input in the editor\"],\n\t\tfallback: \"Voice capture reports an error and never starts. Typing is unaffected.\",\n\t\tacquisition: \"on-demand\",\n\t\tenv: [\"VOICETOOLS_BIN\", \"HOOCODE_VOICETOOLS_BINARY\", \"VOICETOOLS_SILENCE_MS\"],\n\t\tdependentRows: [\"voice-silence-ms\"],\n\t\tsettingsKeys: [\"voice.silenceMs\"],\n\t},\n];\n\nexport interface ExternalToolStatus extends ExternalToolDoc, ManagedToolStatus {\n\tinstalled: boolean;\n\t/**\n\t * Whether hoocode would fetch it if the feature were used right now. False in\n\t * offline mode, and on Android where the published Linux builds do not run.\n\t */\n\tdownloadable: boolean;\n}\n\n/** Short, fixed-width-ish status word for the pane's value column. */\nexport function statusLabel(status: ExternalToolStatus): string {\n\tif (!status.installed) return status.downloadable ? \"not installed\" : \"unavailable\";\n\tswitch (status.source) {\n\t\tcase \"override\":\n\t\t\treturn \"env override\";\n\t\tcase \"managed\":\n\t\t\treturn \"installed\";\n\t\tcase \"path\":\n\t\t\treturn \"system\";\n\t\tdefault:\n\t\t\treturn \"installed\";\n\t}\n}\n\n/**\n * Resolve every external tool. Never downloads: this is what the pane shows\n * *before* the user opts into anything.\n */\nexport function describeExternalTools(): ExternalToolStatus[] {\n\tconst offline = isOfflineMode();\n\tconst android = process.platform === \"android\";\n\treturn EXTERNAL_TOOLS.map((doc) => {\n\t\tconst status = getToolStatus(doc.tool);\n\t\treturn {\n\t\t\t...doc,\n\t\t\t...status,\n\t\t\tinstalled: status.path !== null,\n\t\t\tdownloadable: !offline && !android && doc.acquisition !== \"manual\",\n\t\t};\n\t});\n}\n\n/** Index of pane-row id -> the tool it needs, for annotating gated rows. */\nexport function buildRowGates(statuses: readonly ExternalToolStatus[]): Map<string, ExternalToolStatus> {\n\tconst gates = new Map<string, ExternalToolStatus>();\n\tfor (const status of statuses) {\n\t\tfor (const rowId of status.dependentRows) gates.set(rowId, status);\n\t}\n\treturn gates;\n}\n"]}
1
+ {"version":3,"file":"external-tools.js","sourceRoot":"","sources":["../../src/core/external-tools.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The external (Rust) binaries hoocode can use, and what each one is worth.\n *\n * hoocode is self-sufficient without any of them: `SearchCodebase` and @-file\n * autocomplete fall back to pure-JS implementations, and the features that have\n * no fallback (web, voice) are off or inert rather than broken. These binaries are an\n * *expansion* layer, which is exactly why they were invisible - nothing failed\n * loudly enough to tell anyone they existed.\n *\n * This module is the single description of that layer. The `/settings` pane\n * renders it; the same table says which settings rows are gated on a binary, so\n * a row that cannot do anything today can say so instead of lying.\n */\n\nimport { getToolStatus, isOfflineMode, type ManagedTool, type ManagedToolStatus } from \"../utils/tools-manager.js\";\n\n/** How hoocode gets a binary it does not already have. */\nexport type Acquisition =\n\t/** Fetched in the background at startup. */\n\t| \"startup\"\n\t/** Fetched the first time the feature is actually used. */\n\t| \"on-demand\"\n\t/** Never fetched implicitly; install it yourself or point the env var at it. */\n\t| \"manual\";\n\nexport interface ExternalToolDoc {\n\ttool: ManagedTool;\n\t/** Row label in the pane. */\n\tlabel: string;\n\t/** What the binary does for hoocode, in one line. */\n\tsummary: string;\n\t/** What it turns on, feature by feature. */\n\tenables: string[];\n\t/** What hoocode does instead when it is missing. Never \"nothing works\". */\n\tfallback: string;\n\tacquisition: Acquisition;\n\t/** Env vars that change how this binary is resolved or driven. */\n\tenv: string[];\n\t/**\n\t * `/settings` row ids whose effect depends on this binary. A row listed here\n\t * is annotated (not hidden) while the binary is missing: hiding it would\n\t * recreate the discoverability hole this whole surface exists to close.\n\t */\n\tdependentRows: string[];\n\t/** settings.json keys this binary gates, for docs and search. */\n\tsettingsKeys: string[];\n}\n\n/**\n * Ordered so the two that silently make everything faster come first, then the\n * three that add capability hoocode does not otherwise have.\n */\nexport const EXTERNAL_TOOLS: readonly ExternalToolDoc[] = [\n\t{\n\t\ttool: \"rg\",\n\t\tlabel: \"ripgrep (rg)\",\n\t\tsummary: \"Fast path for content search.\",\n\t\tenables: [\"the lexical half of search runs rg instead of the JS scanner\"],\n\t\tfallback:\n\t\t\t\"A pure-JS scanner produces the same match shape, so results are identical - it is materially slower on large trees and respects fewer ignore-file edge cases.\",\n\t\tacquisition: \"startup\",\n\t\tenv: [\"HOOCODE_RG_BINARY\", \"HOOCODE_NATIVE_SEARCH=1 forces the JS path even when rg is present\"],\n\t\tdependentRows: [],\n\t\tsettingsKeys: [],\n\t},\n\t{\n\t\ttool: \"fd\",\n\t\tlabel: \"fd\",\n\t\tsummary: \"Fast path for filename search.\",\n\t\tenables: [\"@-file autocomplete lists paths with fd instead of the JS directory walker\"],\n\t\tfallback:\n\t\t\t\"A JS walker produces the same result shape - slower on large trees, and glob/ignore handling is the JS approximation rather than fd's.\",\n\t\tacquisition: \"startup\",\n\t\tenv: [\"HOOCODE_FD_BINARY\", \"HOOCODE_NATIVE_SEARCH=1 forces the JS path even when fd is present\"],\n\t\tdependentRows: [],\n\t\tsettingsKeys: [],\n\t},\n\t{\n\t\ttool: \"embsearch\",\n\t\tlabel: \"embsearch (semantic index)\",\n\t\tsummary: \"Local embedding index. The only source of semantic ranking in hoocode.\",\n\t\tenables: [\n\t\t\t\"search fuses semantic hits with its lexical hits\",\n\t\t\t\"MCP/capability deferral ranks tools by meaning rather than keyword\",\n\t\t],\n\t\tfallback:\n\t\t\t\"search is lexical-only and capability lookup ranks lexically. Nothing errors; queries phrased by intent rather than by token simply rank worse. Requires the ONNX build - the mock build is rejected on purpose, because it would rank at random while looking healthy.\",\n\t\tacquisition: \"on-demand\",\n\t\tenv: [\"HOOCODE_EMBSEARCH_BINARY\"],\n\t\tdependentRows: [\"group:embsearch\"],\n\t\tsettingsKeys: [\"enableSemanticIndex\", \"embsearchBinaryPath\", \"embsearchThresholdBytes\"],\n\t},\n\t{\n\t\ttool: \"webtools\",\n\t\tlabel: \"webtools (webfetch/websearch)\",\n\t\tsummary: \"The network layer. Without it hoocode has no way to reach the internet.\",\n\t\tenables: [\"the webfetch tool\", \"the websearch tool\"],\n\t\tfallback:\n\t\t\t\"Both tools return an error when called. The web tool group is off by default, so a missing binary is invisible until you turn the group on.\",\n\t\tacquisition: \"on-demand\",\n\t\tenv: [\"HOOCODE_WEBTOOLS_BINARY\", \"HOOCODE_WEBTOOLS_TIMEOUT\"],\n\t\tdependentRows: [\"group:web\", \"webtools-timeout-secs\"],\n\t\tsettingsKeys: [\"enableWebTools\", \"webtools.timeoutSecs\"],\n\t},\n\t{\n\t\ttool: \"voicetools\",\n\t\tlabel: \"voicetools (voice input)\",\n\t\tsummary: \"Microphone capture and transcription for the TUI.\",\n\t\tenables: [\"push-to-talk voice input in the editor\"],\n\t\tfallback: \"Voice capture reports an error and never starts. Typing is unaffected.\",\n\t\tacquisition: \"on-demand\",\n\t\tenv: [\"VOICETOOLS_BIN\", \"HOOCODE_VOICETOOLS_BINARY\", \"VOICETOOLS_SILENCE_MS\"],\n\t\tdependentRows: [\"voice-silence-ms\"],\n\t\tsettingsKeys: [\"voice.silenceMs\"],\n\t},\n];\n\nexport interface ExternalToolStatus extends ExternalToolDoc, ManagedToolStatus {\n\tinstalled: boolean;\n\t/**\n\t * Whether hoocode would fetch it if the feature were used right now. False in\n\t * offline mode, and on Android where the published Linux builds do not run.\n\t */\n\tdownloadable: boolean;\n}\n\n/** Short, fixed-width-ish status word for the pane's value column. */\nexport function statusLabel(status: ExternalToolStatus): string {\n\tif (!status.installed) return status.downloadable ? \"not installed\" : \"unavailable\";\n\tswitch (status.source) {\n\t\tcase \"override\":\n\t\t\treturn \"env override\";\n\t\tcase \"managed\":\n\t\t\treturn \"installed\";\n\t\tcase \"path\":\n\t\t\treturn \"system\";\n\t\tdefault:\n\t\t\treturn \"installed\";\n\t}\n}\n\n/**\n * Resolve every external tool. Never downloads: this is what the pane shows\n * *before* the user opts into anything.\n */\nexport function describeExternalTools(): ExternalToolStatus[] {\n\tconst offline = isOfflineMode();\n\tconst android = process.platform === \"android\";\n\treturn EXTERNAL_TOOLS.map((doc) => {\n\t\tconst status = getToolStatus(doc.tool);\n\t\treturn {\n\t\t\t...doc,\n\t\t\t...status,\n\t\t\tinstalled: status.path !== null,\n\t\t\tdownloadable: !offline && !android && doc.acquisition !== \"manual\",\n\t\t};\n\t});\n}\n\n/** Index of pane-row id -> the tool it needs, for annotating gated rows. */\nexport function buildRowGates(statuses: readonly ExternalToolStatus[]): Map<string, ExternalToolStatus> {\n\tconst gates = new Map<string, ExternalToolStatus>();\n\tfor (const status of statuses) {\n\t\tfor (const rowId of status.dependentRows) gates.set(rowId, status);\n\t}\n\treturn gates;\n}\n"]}
@@ -55,13 +55,13 @@ export interface CreateAgentSessionOptions {
55
55
  */
56
56
  enableWebTools?: boolean;
57
57
  /**
58
- * Enable the semantic index layer for the built-in `search` tool (ranked
58
+ * Enable the semantic index layer for the built-in `SearchCodebase` tool (ranked
59
59
  * lexical + semantic retrieval, rank-fused). The search tool is active by
60
60
  * default; this flag only toggles the optional embedding index. When false,
61
61
  * search degrades to lexical-only. Ignored when an explicit `tools` allowlist
62
62
  * is provided (list it there instead).
63
63
  */
64
- enableEmbsearchTools?: boolean;
64
+ enableSemanticIndex?: boolean;
65
65
  /** Custom tools to register (in addition to built-in tools). */
66
66
  customTools?: ToolDefinition[];
67
67
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"sdk.d.ts","sourceRoot":"","sources":["../../src/core/sdk.ts"],"names":[],"mappings":"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","sourcesContent":["import { join } from \"node:path\";\nimport {\n\tAgent,\n\ttype AgentMessage,\n\ttype AgentTool,\n\tconvertToLlm,\n\tcreateBackgroundPlaceholderText,\n\tcreateBackgroundTaskMessage,\n\testimateContextTokens,\n\ttype ThinkingLevel,\n} from \"@kolisachint/hoocode-agent-core\";\nimport { clampThinkingLevel, type Message, type Model, streamSimple } from \"@kolisachint/hoocode-ai\";\nimport { getAgentDir } from \"../config.js\";\nimport { readConfig as readHooConfig } from \"../extensions/core/config.js\";\nimport { AgentSession } from \"./agent-session.js\";\nimport { formatNoModelsAvailableMessage } from \"./auth-guidance.js\";\nimport { AuthStorage } from \"./auth-storage.js\";\nimport { evictSupersededReads } from \"./context-gc.js\";\nimport { DEFAULT_THINKING_LEVEL } from \"./defaults.js\";\nimport type { ExtensionRunner, LoadExtensionsResult, SessionStartEvent, ToolDefinition } from \"./extensions/index.js\";\nimport { ModelRegistry } from \"./model-registry.js\";\nimport { defaultModelPerProvider, findInitialModel } from \"./model-resolver.js\";\nimport type { ResourceLoader } from \"./resource-loader.js\";\nimport { DefaultResourceLoader } from \"./resource-loader.js\";\nimport { getDefaultSessionDir, SessionManager } from \"./session-manager.js\";\nimport { SettingsManager } from \"./settings-manager.js\";\nimport { peekSubagentPool } from \"./subagent-pool-instance.js\";\nimport { isInstallTelemetryEnabled } from \"./telemetry.js\";\nimport { time } from \"./timings.js\";\nimport {\n\tcreateBashTool,\n\tcreateCodingTools,\n\tcreateEditTool,\n\tcreateReadOnlyTools,\n\tcreateReadTool,\n\tcreateWriteTool,\n\ttype ToolName,\n} from \"./tools/index.js\";\n\nexport interface CreateAgentSessionOptions {\n\t/** Working directory for project-local discovery. Default: process.cwd() */\n\tcwd?: string;\n\t/** Global config directory. Default: ~/.hoocode/agent */\n\tagentDir?: string;\n\n\t/** Auth storage for credentials. Default: AuthStorage.create(agentDir/auth.json) */\n\tauthStorage?: AuthStorage;\n\t/** Model registry. Default: ModelRegistry.create(authStorage, agentDir/models.json) */\n\tmodelRegistry?: ModelRegistry;\n\n\t/** Model to use. Default: from settings, else first available */\n\tmodel?: Model<any>;\n\t/** Thinking level. Default: from settings, else 'medium' (clamped to model capabilities) */\n\tthinkingLevel?: ThinkingLevel;\n\t/** Models available for cycling (Ctrl+P in interactive mode) */\n\tscopedModels?: Array<{ model: Model<any>; thinkingLevel?: ThinkingLevel }>;\n\n\t/**\n\t * Optional default tool suppression mode when no explicit allowlist is provided.\n\t *\n\t * - \"all\": start with no tools enabled\n\t * - \"builtin\": disable the default built-in tools (read, bash, edit, write)\n\t * but keep extension/custom tools enabled\n\t */\n\tnoTools?: \"all\" | \"builtin\";\n\t/**\n\t * Optional allowlist of tool names.\n\t *\n\t * When omitted, hoocode enables the default built-in tools (read, bash, edit, write)\n\t * and leaves extension/custom tools enabled unless `noTools` changes that default.\n\t * When provided, only the listed tool names are enabled.\n\t */\n\ttools?: string[];\n\t/**\n\t * Optional denylist of tool names, subtracted from whatever set is otherwise\n\t * enabled (allowlist or default). Applied to built-in, extension, and custom tools.\n\t */\n\tdisallowedTools?: string[];\n\t/**\n\t * Enable the built-in `webfetch` + `websearch` tools, which are defined but\n\t * inactive by default. Ignored when an explicit `tools` allowlist is provided\n\t * (list `webfetch`/`websearch` there instead). Network access is still gated\n\t * per call and filtered by `.webtoolsignore`.\n\t */\n\tenableWebTools?: boolean;\n\t/**\n\t * Enable the semantic index layer for the built-in `search` tool (ranked\n\t * lexical + semantic retrieval, rank-fused). The search tool is active by\n\t * default; this flag only toggles the optional embedding index. When false,\n\t * search degrades to lexical-only. Ignored when an explicit `tools` allowlist\n\t * is provided (list it there instead).\n\t */\n\tenableEmbsearchTools?: boolean;\n\t/** Custom tools to register (in addition to built-in tools). */\n\tcustomTools?: ToolDefinition[];\n\t/**\n\t * Replace the built-in base tools entirely (the light preset uses this to\n\t * swap in short-schema read/write/edit/bash variants). Keys become the\n\t * default active tool set when no explicit `tools` allowlist is provided.\n\t */\n\tbaseToolsOverride?: Record<string, AgentTool>;\n\n\t/** Resource loader. When omitted, DefaultResourceLoader is used. */\n\tresourceLoader?: ResourceLoader;\n\n\t/** Session manager. Default: SessionManager.create(cwd) */\n\tsessionManager?: SessionManager;\n\n\t/** Settings manager. Default: SettingsManager.create(cwd, agentDir) */\n\tsettingsManager?: SettingsManager;\n\t/** Session start event metadata for extension runtime startup. */\n\tsessionStartEvent?: SessionStartEvent;\n}\n\n/** Result from createAgentSession */\nexport interface CreateAgentSessionResult {\n\t/** The created session */\n\tsession: AgentSession;\n\t/** Extensions result (for UI context setup in interactive mode) */\n\textensionsResult: LoadExtensionsResult;\n\t/** Warning if session was restored with a different model than saved */\n\tmodelFallbackMessage?: string;\n}\n\n// Re-exports\n\nexport * from \"./agent-session-runtime.js\";\nexport type { ExtensionFactory } from \"./extensions/index.js\";\nexport type { PromptTemplate } from \"./prompt-templates.js\";\n\nexport {\n\t// Tool factories (for custom cwd)\n\tcreateCodingTools,\n\tcreateReadOnlyTools,\n\tcreateReadTool,\n\tcreateBashTool,\n\tcreateEditTool,\n\tcreateWriteTool,\n};\n\n// Helper Functions\n\nfunction getDefaultAgentDir(): string {\n\treturn getAgentDir();\n}\n\nfunction getAttributionHeaders(\n\tmodel: Model<any>,\n\tsettingsManager: SettingsManager,\n): Record<string, string> | undefined {\n\tif (!isInstallTelemetryEnabled(settingsManager)) {\n\t\treturn undefined;\n\t}\n\n\tif (model.provider === \"openrouter\" || model.baseUrl.includes(\"openrouter.ai\")) {\n\t\treturn {\n\t\t\t\"HTTP-Referer\": \"https://github.com/kolisachint/hoocode\",\n\t\t\t\"X-OpenRouter-Title\": \"hoocode\",\n\t\t\t\"X-OpenRouter-Categories\": \"cli-agent\",\n\t\t};\n\t}\n\n\treturn undefined;\n}\n\n/**\n * Create an AgentSession with the specified options.\n *\n * @example\n * ```typescript\n * // Minimal - uses defaults\n * const { session } = await createAgentSession();\n *\n * // With explicit model\n * import { getModel } from '@kolisachint/hoocode-ai';\n * const { session } = await createAgentSession({\n * model: getModel('anthropic', 'claude-opus-4-5'),\n * thinkingLevel: 'high',\n * });\n *\n * // Continue previous session\n * const { session, modelFallbackMessage } = await createAgentSession({\n * continueSession: true,\n * });\n *\n * // Full control\n * const loader = new DefaultResourceLoader({\n * cwd: process.cwd(),\n * agentDir: getAgentDir(),\n * settingsManager: SettingsManager.create(),\n * });\n * await loader.reload();\n * const { session } = await createAgentSession({\n * model: myModel,\n * tools: [readTool, bashTool],\n * resourceLoader: loader,\n * sessionManager: SessionManager.inMemory(),\n * });\n * ```\n */\nexport async function createAgentSession(options: CreateAgentSessionOptions = {}): Promise<CreateAgentSessionResult> {\n\tconst cwd = options.cwd ?? options.sessionManager?.getCwd() ?? process.cwd();\n\tconst agentDir = options.agentDir ?? getDefaultAgentDir();\n\tlet resourceLoader = options.resourceLoader;\n\n\t// Use provided or create AuthStorage and ModelRegistry\n\tconst authPath = options.agentDir ? join(agentDir, \"auth.json\") : undefined;\n\tconst modelsPath = options.agentDir ? join(agentDir, \"models.json\") : undefined;\n\tconst authStorage = options.authStorage ?? AuthStorage.create(authPath);\n\tconst modelRegistry = options.modelRegistry ?? ModelRegistry.create(authStorage, modelsPath);\n\n\tconst settingsManager = options.settingsManager ?? SettingsManager.create(cwd, agentDir);\n\tconst sessionManager = options.sessionManager ?? SessionManager.create(cwd, getDefaultSessionDir(cwd, agentDir));\n\n\tif (!resourceLoader) {\n\t\tresourceLoader = new DefaultResourceLoader({ cwd, agentDir, settingsManager });\n\t\tawait resourceLoader.reload();\n\t\ttime(\"resourceLoader.reload\");\n\t}\n\n\t// Check if session has existing data to restore\n\tconst existingSession = sessionManager.buildSessionContext();\n\tconst hasExistingSession = existingSession.messages.length > 0;\n\tconst hasThinkingEntry = sessionManager.getBranch().some((entry) => entry.type === \"thinking_level_change\");\n\n\tlet model = options.model;\n\tlet modelFallbackMessage: string | undefined;\n\n\t// If session has data, try to restore model from it\n\tif (!model && hasExistingSession && existingSession.model) {\n\t\tconst restoredModel = modelRegistry.find(existingSession.model.provider, existingSession.model.modelId);\n\t\tif (restoredModel && modelRegistry.hasConfiguredAuth(restoredModel)) {\n\t\t\tmodel = restoredModel;\n\t\t}\n\t\tif (!model) {\n\t\t\tmodelFallbackMessage = `Could not restore model ${existingSession.model.provider}/${existingSession.model.modelId}`;\n\t\t}\n\t}\n\n\t// If still no model, use findInitialModel (checks settings default, then provider defaults)\n\tif (!model) {\n\t\t// The pi-layer settings.json default wins; when it is unset, fall back to the\n\t\t// hoo-config.json `llm.default_provider` so the seeded/user default actually\n\t\t// takes effect. findInitialModel only honours it if that provider has auth.\n\t\tconst hooLlm = readHooConfig().llm;\n\t\tconst defaultProvider = settingsManager.getDefaultProvider() ?? hooLlm?.default_provider;\n\t\tconst defaultModelId =\n\t\t\tsettingsManager.getDefaultModel() ??\n\t\t\t(settingsManager.getDefaultProvider()\n\t\t\t\t? undefined\n\t\t\t\t: (hooLlm?.default_model ??\n\t\t\t\t\t(defaultProvider && defaultProvider in defaultModelPerProvider\n\t\t\t\t\t\t? defaultModelPerProvider[defaultProvider as keyof typeof defaultModelPerProvider]\n\t\t\t\t\t\t: undefined)));\n\t\tconst result = await findInitialModel({\n\t\t\tscopedModels: [],\n\t\t\tisContinuing: hasExistingSession,\n\t\t\tdefaultProvider,\n\t\t\tdefaultModelId,\n\t\t\tdefaultThinkingLevel: settingsManager.getDefaultThinkingLevel(),\n\t\t\tmodelRegistry,\n\t\t});\n\t\tmodel = result.model;\n\t\tif (!model) {\n\t\t\tmodelFallbackMessage = formatNoModelsAvailableMessage();\n\t\t} else if (modelFallbackMessage) {\n\t\t\tmodelFallbackMessage += `. Using ${model.provider}/${model.id}`;\n\t\t}\n\t}\n\n\tlet thinkingLevel = options.thinkingLevel;\n\n\t// If session has data, restore thinking level from it\n\tif (thinkingLevel === undefined && hasExistingSession) {\n\t\tthinkingLevel = hasThinkingEntry\n\t\t\t? (existingSession.thinkingLevel as ThinkingLevel)\n\t\t\t: (settingsManager.getDefaultThinkingLevel() ?? DEFAULT_THINKING_LEVEL);\n\t}\n\n\t// Fall back to settings default\n\tif (thinkingLevel === undefined) {\n\t\tthinkingLevel = settingsManager.getDefaultThinkingLevel() ?? DEFAULT_THINKING_LEVEL;\n\t}\n\n\t// Clamp to model capabilities\n\tif (!model) {\n\t\tthinkingLevel = \"off\";\n\t} else {\n\t\tthinkingLevel = clampThinkingLevel(model, thinkingLevel) as ThinkingLevel;\n\t}\n\n\t// `search` is always active: it answers \"find where X lives\" with ranked\n\t// results and degrades to exact-text lexical retrieval when no semantic\n\t// index is present, so it needs no binary to be useful. The\n\t// `enableEmbsearchTools` flag only controls whether the semantic index is\n\t// built and fused in (see main.ts) — not whether the tool exists.\n\tconst defaultActiveToolNames: ToolName[] = [\"read\", \"bash\", \"edit\", \"write\", \"search\"];\n\t// Web tools are registered as base tools but inactive by default; opt-in adds\n\t// them to the default active set. An explicit allowlist (`tools`) takes over\n\t// fully, so callers must list them there to enable in that mode.\n\tconst optInActiveToolNames: ToolName[] = [\n\t\t...(options.enableWebTools ? [\"webfetch\", \"websearch\"] : []),\n\t] as ToolName[];\n\tconst allowedToolNames = options.tools ?? (options.noTools === \"all\" ? [] : undefined);\n\tconst initialActiveToolNames: string[] = options.tools\n\t\t? [...options.tools]\n\t\t: options.noTools\n\t\t\t? []\n\t\t\t: [...defaultActiveToolNames, ...optInActiveToolNames];\n\n\tlet agent: Agent;\n\n\t// Create convertToLlm wrapper that filters images if blockImages is enabled (defense-in-depth)\n\tconst convertToLlmWithBlockImages = (messages: AgentMessage[]): Message[] => {\n\t\tconst converted = convertToLlm(messages);\n\t\t// Check setting dynamically so mid-session changes take effect\n\t\tif (!settingsManager.getBlockImages()) {\n\t\t\treturn converted;\n\t\t}\n\t\t// Filter out ImageContent from all messages, replacing with text placeholder\n\t\treturn converted.map((msg) => {\n\t\t\tif (msg.role === \"user\" || msg.role === \"toolResult\") {\n\t\t\t\tconst content = msg.content;\n\t\t\t\tif (Array.isArray(content)) {\n\t\t\t\t\tconst hasImages = content.some((c) => c.type === \"image\");\n\t\t\t\t\tif (hasImages) {\n\t\t\t\t\t\tconst filteredContent = content\n\t\t\t\t\t\t\t.map((c) =>\n\t\t\t\t\t\t\t\tc.type === \"image\" ? { type: \"text\" as const, text: \"Image reading is disabled.\" } : c,\n\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\t.filter(\n\t\t\t\t\t\t\t\t(c, i, arr) =>\n\t\t\t\t\t\t\t\t\t// Dedupe consecutive \"Image reading is disabled.\" texts\n\t\t\t\t\t\t\t\t\t!(\n\t\t\t\t\t\t\t\t\t\tc.type === \"text\" &&\n\t\t\t\t\t\t\t\t\t\tc.text === \"Image reading is disabled.\" &&\n\t\t\t\t\t\t\t\t\t\ti > 0 &&\n\t\t\t\t\t\t\t\t\t\tarr[i - 1].type === \"text\" &&\n\t\t\t\t\t\t\t\t\t\t(arr[i - 1] as { type: \"text\"; text: string }).text === \"Image reading is disabled.\"\n\t\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\treturn { ...msg, content: filteredContent };\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn msg;\n\t\t});\n\t};\n\n\tconst extensionRunnerRef: { current?: ExtensionRunner } = {};\n\n\t// Token-budget pressure for context GC: the fraction of the active model's\n\t// context window in use, measured from the real usage on the outgoing\n\t// message copy. It latches to a high-water mark so that our own evictions —\n\t// which shrink the next turn's measured usage — cannot oscillate a message\n\t// in and out of the transcript and thrash the provider's prefix cache. A\n\t// large drop (compaction, fork) resets the latch to the new, smaller size.\n\tlet budgetPressureHighWater = 0;\n\tconst getBudgetPressure = (contextMessages: AgentMessage[]): number => {\n\t\tconst contextWindow = agent.state.model?.contextWindow ?? 0;\n\t\tif (contextWindow <= 0) return 0;\n\t\tconst gauge = Math.min(estimateContextTokens(contextMessages).tokens / contextWindow, 1);\n\t\tif (gauge > budgetPressureHighWater) {\n\t\t\tbudgetPressureHighWater = gauge; // rising usage — track it\n\t\t} else if (gauge < budgetPressureHighWater - 0.15) {\n\t\t\tbudgetPressureHighWater = gauge; // context collapsed — reset the latch\n\t\t}\n\t\treturn budgetPressureHighWater;\n\t};\n\n\tagent = new Agent({\n\t\tinitialState: {\n\t\t\tsystemPrompt: \"\",\n\t\t\tmodel,\n\t\t\tthinkingLevel,\n\t\t\ttools: [],\n\t\t},\n\t\tconvertToLlm: convertToLlmWithBlockImages,\n\t\tcreateBackgroundResultMessage: createBackgroundTaskMessage,\n\t\tcreateBackgroundPlaceholder: (toolCall) => createBackgroundPlaceholderText(toolCall),\n\t\t// Report in-process background tool load (e.g. background MCP tools) to the\n\t\t// subagent lifeguard so it widens its heartbeat/timeout tolerance for\n\t\t// concurrently-monitored subagents. Peek (don't create) the pool: background\n\t\t// tools can run before any subagent is ever dispatched.\n\t\tonBackgroundTaskCountChange: (count) => peekSubagentPool()?.setExternalLoad(count),\n\t\tstreamFn: async (model, context, options) => {\n\t\t\tconst auth = await modelRegistry.getApiKeyAndHeaders(model);\n\t\t\tif (!auth.ok) {\n\t\t\t\tthrow new Error(auth.error);\n\t\t\t}\n\t\t\tconst providerRetrySettings = settingsManager.getProviderRetrySettings();\n\t\t\tconst attributionHeaders = getAttributionHeaders(model, settingsManager);\n\t\t\treturn streamSimple(model, context, {\n\t\t\t\t...options,\n\t\t\t\tapiKey: auth.apiKey,\n\t\t\t\ttimeoutMs: options?.timeoutMs ?? providerRetrySettings.timeoutMs,\n\t\t\t\tmaxRetries: options?.maxRetries ?? providerRetrySettings.maxRetries,\n\t\t\t\tmaxRetryDelayMs: options?.maxRetryDelayMs ?? providerRetrySettings.maxRetryDelayMs,\n\t\t\t\theaders:\n\t\t\t\t\tattributionHeaders || auth.headers || options?.headers\n\t\t\t\t\t\t? { ...attributionHeaders, ...auth.headers, ...options?.headers }\n\t\t\t\t\t\t: undefined,\n\t\t\t});\n\t\t},\n\t\tonPayload: async (payload, _model) => {\n\t\t\tconst runner = extensionRunnerRef.current;\n\t\t\tif (!runner?.hasHandlers(\"before_provider_request\")) {\n\t\t\t\treturn payload;\n\t\t\t}\n\t\t\treturn runner.emitBeforeProviderRequest(payload);\n\t\t},\n\t\tonResponse: async (response, _model) => {\n\t\t\tconst runner = extensionRunnerRef.current;\n\t\t\tif (!runner?.hasHandlers(\"after_provider_response\")) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tawait runner.emit({\n\t\t\t\ttype: \"after_provider_response\",\n\t\t\t\tstatus: response.status,\n\t\t\t\theaders: response.headers,\n\t\t\t});\n\t\t},\n\t\tsessionId: sessionManager.getSessionId(),\n\t\ttransformContext: async (messages) => {\n\t\t\tconst runner = extensionRunnerRef.current;\n\t\t\tconst transformed = runner ? await runner.emitContext(messages) : messages;\n\t\t\tif (!settingsManager.getContextGcEnabled()) return transformed;\n\t\t\treturn evictSupersededReads(transformed, { cwd, budgetPressure: getBudgetPressure(transformed) });\n\t\t},\n\t\tsteeringMode: settingsManager.getSteeringMode(),\n\t\tfollowUpMode: settingsManager.getFollowUpMode(),\n\t\ttransport: settingsManager.getTransport(),\n\t\tthinkingBudgets: settingsManager.getThinkingBudgets(),\n\t\tthinkingDisplay: settingsManager.getThinkingDisplay(),\n\t\tmaxRetryDelayMs: settingsManager.getProviderRetrySettings().maxRetryDelayMs,\n\t});\n\n\t// Restore messages if session has existing data\n\tif (hasExistingSession) {\n\t\tagent.state.messages = existingSession.messages;\n\t\tif (!hasThinkingEntry) {\n\t\t\tsessionManager.appendThinkingLevelChange(thinkingLevel);\n\t\t}\n\t} else {\n\t\t// Save initial model and thinking level for new sessions so they can be restored on resume\n\t\tif (model) {\n\t\t\tsessionManager.appendModelChange(model.provider, model.id);\n\t\t}\n\t\tsessionManager.appendThinkingLevelChange(thinkingLevel);\n\t}\n\n\tconst session = new AgentSession({\n\t\tagent,\n\t\tsessionManager,\n\t\tsettingsManager,\n\t\tcwd,\n\t\tscopedModels: options.scopedModels,\n\t\tresourceLoader,\n\t\tcustomTools: options.customTools,\n\t\tbaseToolsOverride: options.baseToolsOverride,\n\t\tmodelRegistry,\n\t\tinitialActiveToolNames,\n\t\tallowedToolNames,\n\t\tdisallowedToolNames: options.disallowedTools,\n\t\textensionRunnerRef,\n\t\tsessionStartEvent: options.sessionStartEvent,\n\t});\n\tconst extensionsResult = resourceLoader.getExtensions();\n\n\treturn {\n\t\tsession,\n\t\textensionsResult,\n\t\tmodelFallbackMessage,\n\t};\n}\n"]}
1
+ {"version":3,"file":"sdk.d.ts","sourceRoot":"","sources":["../../src/core/sdk.ts"],"names":[],"mappings":"AACA,OAAO,EAGN,KAAK,SAAS,EAKd,KAAK,aAAa,EAClB,MAAM,iCAAiC,CAAC;AACzC,OAAO,EAAoC,KAAK,KAAK,EAAgB,MAAM,yBAAyB,CAAC;AAGrG,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAElD,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAGhD,OAAO,KAAK,EAAmB,oBAAoB,EAAE,iBAAiB,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AACtH,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAEpD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAE3D,OAAO,EAAwB,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAC5E,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAIxD,OAAO,EACN,cAAc,EACd,iBAAiB,EACjB,cAAc,EACd,mBAAmB,EACnB,cAAc,EACd,eAAe,EAEf,MAAM,kBAAkB,CAAC;AAE1B,MAAM,WAAW,yBAAyB;IACzC,4EAA4E;IAC5E,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,yDAAyD;IACzD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAElB,oFAAoF;IACpF,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B,uFAAuF;IACvF,aAAa,CAAC,EAAE,aAAa,CAAC;IAE9B,iEAAiE;IACjE,KAAK,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC;IACnB,4FAA4F;IAC5F,aAAa,CAAC,EAAE,aAAa,CAAC;IAC9B,gEAAgE;IAChE,YAAY,CAAC,EAAE,KAAK,CAAC;QAAE,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC;QAAC,aAAa,CAAC,EAAE,aAAa,CAAA;KAAE,CAAC,CAAC;IAE3E;;;;;;OAMG;IACH,OAAO,CAAC,EAAE,KAAK,GAAG,SAAS,CAAC;IAC5B;;;;;;OAMG;IACH,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;IACjB;;;OAGG;IACH,eAAe,CAAC,EAAE,MAAM,EAAE,CAAC;IAC3B;;;;;OAKG;IACH,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB;;;;;;OAMG;IACH,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAC9B,gEAAgE;IAChE,WAAW,CAAC,EAAE,cAAc,EAAE,CAAC;IAC/B;;;;OAIG;IACH,iBAAiB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;IAE9C,oEAAoE;IACpE,cAAc,CAAC,EAAE,cAAc,CAAC;IAEhC,2DAA2D;IAC3D,cAAc,CAAC,EAAE,cAAc,CAAC;IAEhC,uEAAuE;IACvE,eAAe,CAAC,EAAE,eAAe,CAAC;IAClC,kEAAkE;IAClE,iBAAiB,CAAC,EAAE,iBAAiB,CAAC;CACtC;AAED,qCAAqC;AACrC,MAAM,WAAW,wBAAwB;IACxC,0BAA0B;IAC1B,OAAO,EAAE,YAAY,CAAC;IACtB,mEAAmE;IACnE,gBAAgB,EAAE,oBAAoB,CAAC;IACvC,wEAAwE;IACxE,oBAAoB,CAAC,EAAE,MAAM,CAAC;CAC9B;AAID,cAAc,4BAA4B,CAAC;AAC3C,YAAY,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAC9D,YAAY,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAE5D,OAAO,EAEN,iBAAiB,EACjB,mBAAmB,EACnB,cAAc,EACd,cAAc,EACd,cAAc,EACd,eAAe,GACf,CAAC;AA2BF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AACH,wBAAsB,kBAAkB,CAAC,OAAO,GAAE,yBAA8B,GAAG,OAAO,CAAC,wBAAwB,CAAC,CAkRnH","sourcesContent":["import { join } from \"node:path\";\nimport {\n\tAgent,\n\ttype AgentMessage,\n\ttype AgentTool,\n\tconvertToLlm,\n\tcreateBackgroundPlaceholderText,\n\tcreateBackgroundTaskMessage,\n\testimateContextTokens,\n\ttype ThinkingLevel,\n} from \"@kolisachint/hoocode-agent-core\";\nimport { clampThinkingLevel, type Message, type Model, streamSimple } from \"@kolisachint/hoocode-ai\";\nimport { getAgentDir } from \"../config.js\";\nimport { readConfig as readHooConfig } from \"../extensions/core/config.js\";\nimport { AgentSession } from \"./agent-session.js\";\nimport { formatNoModelsAvailableMessage } from \"./auth-guidance.js\";\nimport { AuthStorage } from \"./auth-storage.js\";\nimport { evictSupersededReads } from \"./context-gc.js\";\nimport { DEFAULT_THINKING_LEVEL } from \"./defaults.js\";\nimport type { ExtensionRunner, LoadExtensionsResult, SessionStartEvent, ToolDefinition } from \"./extensions/index.js\";\nimport { ModelRegistry } from \"./model-registry.js\";\nimport { defaultModelPerProvider, findInitialModel } from \"./model-resolver.js\";\nimport type { ResourceLoader } from \"./resource-loader.js\";\nimport { DefaultResourceLoader } from \"./resource-loader.js\";\nimport { getDefaultSessionDir, SessionManager } from \"./session-manager.js\";\nimport { SettingsManager } from \"./settings-manager.js\";\nimport { peekSubagentPool } from \"./subagent-pool-instance.js\";\nimport { isInstallTelemetryEnabled } from \"./telemetry.js\";\nimport { time } from \"./timings.js\";\nimport {\n\tcreateBashTool,\n\tcreateCodingTools,\n\tcreateEditTool,\n\tcreateReadOnlyTools,\n\tcreateReadTool,\n\tcreateWriteTool,\n\ttype ToolName,\n} from \"./tools/index.js\";\n\nexport interface CreateAgentSessionOptions {\n\t/** Working directory for project-local discovery. Default: process.cwd() */\n\tcwd?: string;\n\t/** Global config directory. Default: ~/.hoocode/agent */\n\tagentDir?: string;\n\n\t/** Auth storage for credentials. Default: AuthStorage.create(agentDir/auth.json) */\n\tauthStorage?: AuthStorage;\n\t/** Model registry. Default: ModelRegistry.create(authStorage, agentDir/models.json) */\n\tmodelRegistry?: ModelRegistry;\n\n\t/** Model to use. Default: from settings, else first available */\n\tmodel?: Model<any>;\n\t/** Thinking level. Default: from settings, else 'medium' (clamped to model capabilities) */\n\tthinkingLevel?: ThinkingLevel;\n\t/** Models available for cycling (Ctrl+P in interactive mode) */\n\tscopedModels?: Array<{ model: Model<any>; thinkingLevel?: ThinkingLevel }>;\n\n\t/**\n\t * Optional default tool suppression mode when no explicit allowlist is provided.\n\t *\n\t * - \"all\": start with no tools enabled\n\t * - \"builtin\": disable the default built-in tools (read, bash, edit, write)\n\t * but keep extension/custom tools enabled\n\t */\n\tnoTools?: \"all\" | \"builtin\";\n\t/**\n\t * Optional allowlist of tool names.\n\t *\n\t * When omitted, hoocode enables the default built-in tools (read, bash, edit, write)\n\t * and leaves extension/custom tools enabled unless `noTools` changes that default.\n\t * When provided, only the listed tool names are enabled.\n\t */\n\ttools?: string[];\n\t/**\n\t * Optional denylist of tool names, subtracted from whatever set is otherwise\n\t * enabled (allowlist or default). Applied to built-in, extension, and custom tools.\n\t */\n\tdisallowedTools?: string[];\n\t/**\n\t * Enable the built-in `webfetch` + `websearch` tools, which are defined but\n\t * inactive by default. Ignored when an explicit `tools` allowlist is provided\n\t * (list `webfetch`/`websearch` there instead). Network access is still gated\n\t * per call and filtered by `.webtoolsignore`.\n\t */\n\tenableWebTools?: boolean;\n\t/**\n\t * Enable the semantic index layer for the built-in `SearchCodebase` tool (ranked\n\t * lexical + semantic retrieval, rank-fused). The search tool is active by\n\t * default; this flag only toggles the optional embedding index. When false,\n\t * search degrades to lexical-only. Ignored when an explicit `tools` allowlist\n\t * is provided (list it there instead).\n\t */\n\tenableSemanticIndex?: boolean;\n\t/** Custom tools to register (in addition to built-in tools). */\n\tcustomTools?: ToolDefinition[];\n\t/**\n\t * Replace the built-in base tools entirely (the light preset uses this to\n\t * swap in short-schema read/write/edit/bash variants). Keys become the\n\t * default active tool set when no explicit `tools` allowlist is provided.\n\t */\n\tbaseToolsOverride?: Record<string, AgentTool>;\n\n\t/** Resource loader. When omitted, DefaultResourceLoader is used. */\n\tresourceLoader?: ResourceLoader;\n\n\t/** Session manager. Default: SessionManager.create(cwd) */\n\tsessionManager?: SessionManager;\n\n\t/** Settings manager. Default: SettingsManager.create(cwd, agentDir) */\n\tsettingsManager?: SettingsManager;\n\t/** Session start event metadata for extension runtime startup. */\n\tsessionStartEvent?: SessionStartEvent;\n}\n\n/** Result from createAgentSession */\nexport interface CreateAgentSessionResult {\n\t/** The created session */\n\tsession: AgentSession;\n\t/** Extensions result (for UI context setup in interactive mode) */\n\textensionsResult: LoadExtensionsResult;\n\t/** Warning if session was restored with a different model than saved */\n\tmodelFallbackMessage?: string;\n}\n\n// Re-exports\n\nexport * from \"./agent-session-runtime.js\";\nexport type { ExtensionFactory } from \"./extensions/index.js\";\nexport type { PromptTemplate } from \"./prompt-templates.js\";\n\nexport {\n\t// Tool factories (for custom cwd)\n\tcreateCodingTools,\n\tcreateReadOnlyTools,\n\tcreateReadTool,\n\tcreateBashTool,\n\tcreateEditTool,\n\tcreateWriteTool,\n};\n\n// Helper Functions\n\nfunction getDefaultAgentDir(): string {\n\treturn getAgentDir();\n}\n\nfunction getAttributionHeaders(\n\tmodel: Model<any>,\n\tsettingsManager: SettingsManager,\n): Record<string, string> | undefined {\n\tif (!isInstallTelemetryEnabled(settingsManager)) {\n\t\treturn undefined;\n\t}\n\n\tif (model.provider === \"openrouter\" || model.baseUrl.includes(\"openrouter.ai\")) {\n\t\treturn {\n\t\t\t\"HTTP-Referer\": \"https://github.com/kolisachint/hoocode\",\n\t\t\t\"X-OpenRouter-Title\": \"hoocode\",\n\t\t\t\"X-OpenRouter-Categories\": \"cli-agent\",\n\t\t};\n\t}\n\n\treturn undefined;\n}\n\n/**\n * Create an AgentSession with the specified options.\n *\n * @example\n * ```typescript\n * // Minimal - uses defaults\n * const { session } = await createAgentSession();\n *\n * // With explicit model\n * import { getModel } from '@kolisachint/hoocode-ai';\n * const { session } = await createAgentSession({\n * model: getModel('anthropic', 'claude-opus-4-5'),\n * thinkingLevel: 'high',\n * });\n *\n * // Continue previous session\n * const { session, modelFallbackMessage } = await createAgentSession({\n * continueSession: true,\n * });\n *\n * // Full control\n * const loader = new DefaultResourceLoader({\n * cwd: process.cwd(),\n * agentDir: getAgentDir(),\n * settingsManager: SettingsManager.create(),\n * });\n * await loader.reload();\n * const { session } = await createAgentSession({\n * model: myModel,\n * tools: [readTool, bashTool],\n * resourceLoader: loader,\n * sessionManager: SessionManager.inMemory(),\n * });\n * ```\n */\nexport async function createAgentSession(options: CreateAgentSessionOptions = {}): Promise<CreateAgentSessionResult> {\n\tconst cwd = options.cwd ?? options.sessionManager?.getCwd() ?? process.cwd();\n\tconst agentDir = options.agentDir ?? getDefaultAgentDir();\n\tlet resourceLoader = options.resourceLoader;\n\n\t// Use provided or create AuthStorage and ModelRegistry\n\tconst authPath = options.agentDir ? join(agentDir, \"auth.json\") : undefined;\n\tconst modelsPath = options.agentDir ? join(agentDir, \"models.json\") : undefined;\n\tconst authStorage = options.authStorage ?? AuthStorage.create(authPath);\n\tconst modelRegistry = options.modelRegistry ?? ModelRegistry.create(authStorage, modelsPath);\n\n\tconst settingsManager = options.settingsManager ?? SettingsManager.create(cwd, agentDir);\n\tconst sessionManager = options.sessionManager ?? SessionManager.create(cwd, getDefaultSessionDir(cwd, agentDir));\n\n\tif (!resourceLoader) {\n\t\tresourceLoader = new DefaultResourceLoader({ cwd, agentDir, settingsManager });\n\t\tawait resourceLoader.reload();\n\t\ttime(\"resourceLoader.reload\");\n\t}\n\n\t// Check if session has existing data to restore\n\tconst existingSession = sessionManager.buildSessionContext();\n\tconst hasExistingSession = existingSession.messages.length > 0;\n\tconst hasThinkingEntry = sessionManager.getBranch().some((entry) => entry.type === \"thinking_level_change\");\n\n\tlet model = options.model;\n\tlet modelFallbackMessage: string | undefined;\n\n\t// If session has data, try to restore model from it\n\tif (!model && hasExistingSession && existingSession.model) {\n\t\tconst restoredModel = modelRegistry.find(existingSession.model.provider, existingSession.model.modelId);\n\t\tif (restoredModel && modelRegistry.hasConfiguredAuth(restoredModel)) {\n\t\t\tmodel = restoredModel;\n\t\t}\n\t\tif (!model) {\n\t\t\tmodelFallbackMessage = `Could not restore model ${existingSession.model.provider}/${existingSession.model.modelId}`;\n\t\t}\n\t}\n\n\t// If still no model, use findInitialModel (checks settings default, then provider defaults)\n\tif (!model) {\n\t\t// The pi-layer settings.json default wins; when it is unset, fall back to the\n\t\t// hoo-config.json `llm.default_provider` so the seeded/user default actually\n\t\t// takes effect. findInitialModel only honours it if that provider has auth.\n\t\tconst hooLlm = readHooConfig().llm;\n\t\tconst defaultProvider = settingsManager.getDefaultProvider() ?? hooLlm?.default_provider;\n\t\tconst defaultModelId =\n\t\t\tsettingsManager.getDefaultModel() ??\n\t\t\t(settingsManager.getDefaultProvider()\n\t\t\t\t? undefined\n\t\t\t\t: (hooLlm?.default_model ??\n\t\t\t\t\t(defaultProvider && defaultProvider in defaultModelPerProvider\n\t\t\t\t\t\t? defaultModelPerProvider[defaultProvider as keyof typeof defaultModelPerProvider]\n\t\t\t\t\t\t: undefined)));\n\t\tconst result = await findInitialModel({\n\t\t\tscopedModels: [],\n\t\t\tisContinuing: hasExistingSession,\n\t\t\tdefaultProvider,\n\t\t\tdefaultModelId,\n\t\t\tdefaultThinkingLevel: settingsManager.getDefaultThinkingLevel(),\n\t\t\tmodelRegistry,\n\t\t});\n\t\tmodel = result.model;\n\t\tif (!model) {\n\t\t\tmodelFallbackMessage = formatNoModelsAvailableMessage();\n\t\t} else if (modelFallbackMessage) {\n\t\t\tmodelFallbackMessage += `. Using ${model.provider}/${model.id}`;\n\t\t}\n\t}\n\n\tlet thinkingLevel = options.thinkingLevel;\n\n\t// If session has data, restore thinking level from it\n\tif (thinkingLevel === undefined && hasExistingSession) {\n\t\tthinkingLevel = hasThinkingEntry\n\t\t\t? (existingSession.thinkingLevel as ThinkingLevel)\n\t\t\t: (settingsManager.getDefaultThinkingLevel() ?? DEFAULT_THINKING_LEVEL);\n\t}\n\n\t// Fall back to settings default\n\tif (thinkingLevel === undefined) {\n\t\tthinkingLevel = settingsManager.getDefaultThinkingLevel() ?? DEFAULT_THINKING_LEVEL;\n\t}\n\n\t// Clamp to model capabilities\n\tif (!model) {\n\t\tthinkingLevel = \"off\";\n\t} else {\n\t\tthinkingLevel = clampThinkingLevel(model, thinkingLevel) as ThinkingLevel;\n\t}\n\n\t// `SearchCodebase` is always active: it answers \"find where X lives\" with ranked\n\t// results and degrades to exact-text lexical retrieval when no semantic\n\t// index is present, so it needs no binary to be useful. The\n\t// `enableSemanticIndex` flag only controls whether the semantic index is\n\t// built and fused in (see main.ts) — not whether the tool exists.\n\tconst defaultActiveToolNames: ToolName[] = [\"read\", \"bash\", \"edit\", \"write\", \"SearchCodebase\"];\n\t// Web tools are registered as base tools but inactive by default; opt-in adds\n\t// them to the default active set. An explicit allowlist (`tools`) takes over\n\t// fully, so callers must list them there to enable in that mode.\n\tconst optInActiveToolNames: ToolName[] = [\n\t\t...(options.enableWebTools ? [\"webfetch\", \"websearch\"] : []),\n\t] as ToolName[];\n\tconst allowedToolNames = options.tools ?? (options.noTools === \"all\" ? [] : undefined);\n\tconst initialActiveToolNames: string[] = options.tools\n\t\t? [...options.tools]\n\t\t: options.noTools\n\t\t\t? []\n\t\t\t: [...defaultActiveToolNames, ...optInActiveToolNames];\n\n\tlet agent: Agent;\n\n\t// Create convertToLlm wrapper that filters images if blockImages is enabled (defense-in-depth)\n\tconst convertToLlmWithBlockImages = (messages: AgentMessage[]): Message[] => {\n\t\tconst converted = convertToLlm(messages);\n\t\t// Check setting dynamically so mid-session changes take effect\n\t\tif (!settingsManager.getBlockImages()) {\n\t\t\treturn converted;\n\t\t}\n\t\t// Filter out ImageContent from all messages, replacing with text placeholder\n\t\treturn converted.map((msg) => {\n\t\t\tif (msg.role === \"user\" || msg.role === \"toolResult\") {\n\t\t\t\tconst content = msg.content;\n\t\t\t\tif (Array.isArray(content)) {\n\t\t\t\t\tconst hasImages = content.some((c) => c.type === \"image\");\n\t\t\t\t\tif (hasImages) {\n\t\t\t\t\t\tconst filteredContent = content\n\t\t\t\t\t\t\t.map((c) =>\n\t\t\t\t\t\t\t\tc.type === \"image\" ? { type: \"text\" as const, text: \"Image reading is disabled.\" } : c,\n\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\t.filter(\n\t\t\t\t\t\t\t\t(c, i, arr) =>\n\t\t\t\t\t\t\t\t\t// Dedupe consecutive \"Image reading is disabled.\" texts\n\t\t\t\t\t\t\t\t\t!(\n\t\t\t\t\t\t\t\t\t\tc.type === \"text\" &&\n\t\t\t\t\t\t\t\t\t\tc.text === \"Image reading is disabled.\" &&\n\t\t\t\t\t\t\t\t\t\ti > 0 &&\n\t\t\t\t\t\t\t\t\t\tarr[i - 1].type === \"text\" &&\n\t\t\t\t\t\t\t\t\t\t(arr[i - 1] as { type: \"text\"; text: string }).text === \"Image reading is disabled.\"\n\t\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\treturn { ...msg, content: filteredContent };\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn msg;\n\t\t});\n\t};\n\n\tconst extensionRunnerRef: { current?: ExtensionRunner } = {};\n\n\t// Token-budget pressure for context GC: the fraction of the active model's\n\t// context window in use, measured from the real usage on the outgoing\n\t// message copy. It latches to a high-water mark so that our own evictions —\n\t// which shrink the next turn's measured usage — cannot oscillate a message\n\t// in and out of the transcript and thrash the provider's prefix cache. A\n\t// large drop (compaction, fork) resets the latch to the new, smaller size.\n\tlet budgetPressureHighWater = 0;\n\tconst getBudgetPressure = (contextMessages: AgentMessage[]): number => {\n\t\tconst contextWindow = agent.state.model?.contextWindow ?? 0;\n\t\tif (contextWindow <= 0) return 0;\n\t\tconst gauge = Math.min(estimateContextTokens(contextMessages).tokens / contextWindow, 1);\n\t\tif (gauge > budgetPressureHighWater) {\n\t\t\tbudgetPressureHighWater = gauge; // rising usage — track it\n\t\t} else if (gauge < budgetPressureHighWater - 0.15) {\n\t\t\tbudgetPressureHighWater = gauge; // context collapsed — reset the latch\n\t\t}\n\t\treturn budgetPressureHighWater;\n\t};\n\n\tagent = new Agent({\n\t\tinitialState: {\n\t\t\tsystemPrompt: \"\",\n\t\t\tmodel,\n\t\t\tthinkingLevel,\n\t\t\ttools: [],\n\t\t},\n\t\tconvertToLlm: convertToLlmWithBlockImages,\n\t\tcreateBackgroundResultMessage: createBackgroundTaskMessage,\n\t\tcreateBackgroundPlaceholder: (toolCall) => createBackgroundPlaceholderText(toolCall),\n\t\t// Report in-process background tool load (e.g. background MCP tools) to the\n\t\t// subagent lifeguard so it widens its heartbeat/timeout tolerance for\n\t\t// concurrently-monitored subagents. Peek (don't create) the pool: background\n\t\t// tools can run before any subagent is ever dispatched.\n\t\tonBackgroundTaskCountChange: (count) => peekSubagentPool()?.setExternalLoad(count),\n\t\tstreamFn: async (model, context, options) => {\n\t\t\tconst auth = await modelRegistry.getApiKeyAndHeaders(model);\n\t\t\tif (!auth.ok) {\n\t\t\t\tthrow new Error(auth.error);\n\t\t\t}\n\t\t\tconst providerRetrySettings = settingsManager.getProviderRetrySettings();\n\t\t\tconst attributionHeaders = getAttributionHeaders(model, settingsManager);\n\t\t\treturn streamSimple(model, context, {\n\t\t\t\t...options,\n\t\t\t\tapiKey: auth.apiKey,\n\t\t\t\ttimeoutMs: options?.timeoutMs ?? providerRetrySettings.timeoutMs,\n\t\t\t\tmaxRetries: options?.maxRetries ?? providerRetrySettings.maxRetries,\n\t\t\t\tmaxRetryDelayMs: options?.maxRetryDelayMs ?? providerRetrySettings.maxRetryDelayMs,\n\t\t\t\theaders:\n\t\t\t\t\tattributionHeaders || auth.headers || options?.headers\n\t\t\t\t\t\t? { ...attributionHeaders, ...auth.headers, ...options?.headers }\n\t\t\t\t\t\t: undefined,\n\t\t\t});\n\t\t},\n\t\tonPayload: async (payload, _model) => {\n\t\t\tconst runner = extensionRunnerRef.current;\n\t\t\tif (!runner?.hasHandlers(\"before_provider_request\")) {\n\t\t\t\treturn payload;\n\t\t\t}\n\t\t\treturn runner.emitBeforeProviderRequest(payload);\n\t\t},\n\t\tonResponse: async (response, _model) => {\n\t\t\tconst runner = extensionRunnerRef.current;\n\t\t\tif (!runner?.hasHandlers(\"after_provider_response\")) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tawait runner.emit({\n\t\t\t\ttype: \"after_provider_response\",\n\t\t\t\tstatus: response.status,\n\t\t\t\theaders: response.headers,\n\t\t\t});\n\t\t},\n\t\tsessionId: sessionManager.getSessionId(),\n\t\ttransformContext: async (messages) => {\n\t\t\tconst runner = extensionRunnerRef.current;\n\t\t\tconst transformed = runner ? await runner.emitContext(messages) : messages;\n\t\t\tif (!settingsManager.getContextGcEnabled()) return transformed;\n\t\t\treturn evictSupersededReads(transformed, { cwd, budgetPressure: getBudgetPressure(transformed) });\n\t\t},\n\t\tsteeringMode: settingsManager.getSteeringMode(),\n\t\tfollowUpMode: settingsManager.getFollowUpMode(),\n\t\ttransport: settingsManager.getTransport(),\n\t\tthinkingBudgets: settingsManager.getThinkingBudgets(),\n\t\tthinkingDisplay: settingsManager.getThinkingDisplay(),\n\t\tmaxRetryDelayMs: settingsManager.getProviderRetrySettings().maxRetryDelayMs,\n\t});\n\n\t// Restore messages if session has existing data\n\tif (hasExistingSession) {\n\t\tagent.state.messages = existingSession.messages;\n\t\tif (!hasThinkingEntry) {\n\t\t\tsessionManager.appendThinkingLevelChange(thinkingLevel);\n\t\t}\n\t} else {\n\t\t// Save initial model and thinking level for new sessions so they can be restored on resume\n\t\tif (model) {\n\t\t\tsessionManager.appendModelChange(model.provider, model.id);\n\t\t}\n\t\tsessionManager.appendThinkingLevelChange(thinkingLevel);\n\t}\n\n\tconst session = new AgentSession({\n\t\tagent,\n\t\tsessionManager,\n\t\tsettingsManager,\n\t\tcwd,\n\t\tscopedModels: options.scopedModels,\n\t\tresourceLoader,\n\t\tcustomTools: options.customTools,\n\t\tbaseToolsOverride: options.baseToolsOverride,\n\t\tmodelRegistry,\n\t\tinitialActiveToolNames,\n\t\tallowedToolNames,\n\t\tdisallowedToolNames: options.disallowedTools,\n\t\textensionRunnerRef,\n\t\tsessionStartEvent: options.sessionStartEvent,\n\t});\n\tconst extensionsResult = resourceLoader.getExtensions();\n\n\treturn {\n\t\tsession,\n\t\textensionsResult,\n\t\tmodelFallbackMessage,\n\t};\n}\n"]}
package/dist/core/sdk.js CHANGED
@@ -154,12 +154,12 @@ export async function createAgentSession(options = {}) {
154
154
  else {
155
155
  thinkingLevel = clampThinkingLevel(model, thinkingLevel);
156
156
  }
157
- // `search` is always active: it answers "find where X lives" with ranked
157
+ // `SearchCodebase` is always active: it answers "find where X lives" with ranked
158
158
  // results and degrades to exact-text lexical retrieval when no semantic
159
159
  // index is present, so it needs no binary to be useful. The
160
- // `enableEmbsearchTools` flag only controls whether the semantic index is
160
+ // `enableSemanticIndex` flag only controls whether the semantic index is
161
161
  // built and fused in (see main.ts) — not whether the tool exists.
162
- const defaultActiveToolNames = ["read", "bash", "edit", "write", "search"];
162
+ const defaultActiveToolNames = ["read", "bash", "edit", "write", "SearchCodebase"];
163
163
  // Web tools are registered as base tools but inactive by default; opt-in adds
164
164
  // them to the default active set. An explicit allowlist (`tools`) takes over
165
165
  // fully, so callers must list them there to enable in that mode.
@@ -1 +1 @@
1
- {"version":3,"file":"sdk.js","sourceRoot":"","sources":["../../src/core/sdk.ts"],"names":[],"mappings":"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","sourcesContent":["import { join } from \"node:path\";\nimport {\n\tAgent,\n\ttype AgentMessage,\n\ttype AgentTool,\n\tconvertToLlm,\n\tcreateBackgroundPlaceholderText,\n\tcreateBackgroundTaskMessage,\n\testimateContextTokens,\n\ttype ThinkingLevel,\n} from \"@kolisachint/hoocode-agent-core\";\nimport { clampThinkingLevel, type Message, type Model, streamSimple } from \"@kolisachint/hoocode-ai\";\nimport { getAgentDir } from \"../config.js\";\nimport { readConfig as readHooConfig } from \"../extensions/core/config.js\";\nimport { AgentSession } from \"./agent-session.js\";\nimport { formatNoModelsAvailableMessage } from \"./auth-guidance.js\";\nimport { AuthStorage } from \"./auth-storage.js\";\nimport { evictSupersededReads } from \"./context-gc.js\";\nimport { DEFAULT_THINKING_LEVEL } from \"./defaults.js\";\nimport type { ExtensionRunner, LoadExtensionsResult, SessionStartEvent, ToolDefinition } from \"./extensions/index.js\";\nimport { ModelRegistry } from \"./model-registry.js\";\nimport { defaultModelPerProvider, findInitialModel } from \"./model-resolver.js\";\nimport type { ResourceLoader } from \"./resource-loader.js\";\nimport { DefaultResourceLoader } from \"./resource-loader.js\";\nimport { getDefaultSessionDir, SessionManager } from \"./session-manager.js\";\nimport { SettingsManager } from \"./settings-manager.js\";\nimport { peekSubagentPool } from \"./subagent-pool-instance.js\";\nimport { isInstallTelemetryEnabled } from \"./telemetry.js\";\nimport { time } from \"./timings.js\";\nimport {\n\tcreateBashTool,\n\tcreateCodingTools,\n\tcreateEditTool,\n\tcreateReadOnlyTools,\n\tcreateReadTool,\n\tcreateWriteTool,\n\ttype ToolName,\n} from \"./tools/index.js\";\n\nexport interface CreateAgentSessionOptions {\n\t/** Working directory for project-local discovery. Default: process.cwd() */\n\tcwd?: string;\n\t/** Global config directory. Default: ~/.hoocode/agent */\n\tagentDir?: string;\n\n\t/** Auth storage for credentials. Default: AuthStorage.create(agentDir/auth.json) */\n\tauthStorage?: AuthStorage;\n\t/** Model registry. Default: ModelRegistry.create(authStorage, agentDir/models.json) */\n\tmodelRegistry?: ModelRegistry;\n\n\t/** Model to use. Default: from settings, else first available */\n\tmodel?: Model<any>;\n\t/** Thinking level. Default: from settings, else 'medium' (clamped to model capabilities) */\n\tthinkingLevel?: ThinkingLevel;\n\t/** Models available for cycling (Ctrl+P in interactive mode) */\n\tscopedModels?: Array<{ model: Model<any>; thinkingLevel?: ThinkingLevel }>;\n\n\t/**\n\t * Optional default tool suppression mode when no explicit allowlist is provided.\n\t *\n\t * - \"all\": start with no tools enabled\n\t * - \"builtin\": disable the default built-in tools (read, bash, edit, write)\n\t * but keep extension/custom tools enabled\n\t */\n\tnoTools?: \"all\" | \"builtin\";\n\t/**\n\t * Optional allowlist of tool names.\n\t *\n\t * When omitted, hoocode enables the default built-in tools (read, bash, edit, write)\n\t * and leaves extension/custom tools enabled unless `noTools` changes that default.\n\t * When provided, only the listed tool names are enabled.\n\t */\n\ttools?: string[];\n\t/**\n\t * Optional denylist of tool names, subtracted from whatever set is otherwise\n\t * enabled (allowlist or default). Applied to built-in, extension, and custom tools.\n\t */\n\tdisallowedTools?: string[];\n\t/**\n\t * Enable the built-in `webfetch` + `websearch` tools, which are defined but\n\t * inactive by default. Ignored when an explicit `tools` allowlist is provided\n\t * (list `webfetch`/`websearch` there instead). Network access is still gated\n\t * per call and filtered by `.webtoolsignore`.\n\t */\n\tenableWebTools?: boolean;\n\t/**\n\t * Enable the semantic index layer for the built-in `search` tool (ranked\n\t * lexical + semantic retrieval, rank-fused). The search tool is active by\n\t * default; this flag only toggles the optional embedding index. When false,\n\t * search degrades to lexical-only. Ignored when an explicit `tools` allowlist\n\t * is provided (list it there instead).\n\t */\n\tenableEmbsearchTools?: boolean;\n\t/** Custom tools to register (in addition to built-in tools). */\n\tcustomTools?: ToolDefinition[];\n\t/**\n\t * Replace the built-in base tools entirely (the light preset uses this to\n\t * swap in short-schema read/write/edit/bash variants). Keys become the\n\t * default active tool set when no explicit `tools` allowlist is provided.\n\t */\n\tbaseToolsOverride?: Record<string, AgentTool>;\n\n\t/** Resource loader. When omitted, DefaultResourceLoader is used. */\n\tresourceLoader?: ResourceLoader;\n\n\t/** Session manager. Default: SessionManager.create(cwd) */\n\tsessionManager?: SessionManager;\n\n\t/** Settings manager. Default: SettingsManager.create(cwd, agentDir) */\n\tsettingsManager?: SettingsManager;\n\t/** Session start event metadata for extension runtime startup. */\n\tsessionStartEvent?: SessionStartEvent;\n}\n\n/** Result from createAgentSession */\nexport interface CreateAgentSessionResult {\n\t/** The created session */\n\tsession: AgentSession;\n\t/** Extensions result (for UI context setup in interactive mode) */\n\textensionsResult: LoadExtensionsResult;\n\t/** Warning if session was restored with a different model than saved */\n\tmodelFallbackMessage?: string;\n}\n\n// Re-exports\n\nexport * from \"./agent-session-runtime.js\";\nexport type { ExtensionFactory } from \"./extensions/index.js\";\nexport type { PromptTemplate } from \"./prompt-templates.js\";\n\nexport {\n\t// Tool factories (for custom cwd)\n\tcreateCodingTools,\n\tcreateReadOnlyTools,\n\tcreateReadTool,\n\tcreateBashTool,\n\tcreateEditTool,\n\tcreateWriteTool,\n};\n\n// Helper Functions\n\nfunction getDefaultAgentDir(): string {\n\treturn getAgentDir();\n}\n\nfunction getAttributionHeaders(\n\tmodel: Model<any>,\n\tsettingsManager: SettingsManager,\n): Record<string, string> | undefined {\n\tif (!isInstallTelemetryEnabled(settingsManager)) {\n\t\treturn undefined;\n\t}\n\n\tif (model.provider === \"openrouter\" || model.baseUrl.includes(\"openrouter.ai\")) {\n\t\treturn {\n\t\t\t\"HTTP-Referer\": \"https://github.com/kolisachint/hoocode\",\n\t\t\t\"X-OpenRouter-Title\": \"hoocode\",\n\t\t\t\"X-OpenRouter-Categories\": \"cli-agent\",\n\t\t};\n\t}\n\n\treturn undefined;\n}\n\n/**\n * Create an AgentSession with the specified options.\n *\n * @example\n * ```typescript\n * // Minimal - uses defaults\n * const { session } = await createAgentSession();\n *\n * // With explicit model\n * import { getModel } from '@kolisachint/hoocode-ai';\n * const { session } = await createAgentSession({\n * model: getModel('anthropic', 'claude-opus-4-5'),\n * thinkingLevel: 'high',\n * });\n *\n * // Continue previous session\n * const { session, modelFallbackMessage } = await createAgentSession({\n * continueSession: true,\n * });\n *\n * // Full control\n * const loader = new DefaultResourceLoader({\n * cwd: process.cwd(),\n * agentDir: getAgentDir(),\n * settingsManager: SettingsManager.create(),\n * });\n * await loader.reload();\n * const { session } = await createAgentSession({\n * model: myModel,\n * tools: [readTool, bashTool],\n * resourceLoader: loader,\n * sessionManager: SessionManager.inMemory(),\n * });\n * ```\n */\nexport async function createAgentSession(options: CreateAgentSessionOptions = {}): Promise<CreateAgentSessionResult> {\n\tconst cwd = options.cwd ?? options.sessionManager?.getCwd() ?? process.cwd();\n\tconst agentDir = options.agentDir ?? getDefaultAgentDir();\n\tlet resourceLoader = options.resourceLoader;\n\n\t// Use provided or create AuthStorage and ModelRegistry\n\tconst authPath = options.agentDir ? join(agentDir, \"auth.json\") : undefined;\n\tconst modelsPath = options.agentDir ? join(agentDir, \"models.json\") : undefined;\n\tconst authStorage = options.authStorage ?? AuthStorage.create(authPath);\n\tconst modelRegistry = options.modelRegistry ?? ModelRegistry.create(authStorage, modelsPath);\n\n\tconst settingsManager = options.settingsManager ?? SettingsManager.create(cwd, agentDir);\n\tconst sessionManager = options.sessionManager ?? SessionManager.create(cwd, getDefaultSessionDir(cwd, agentDir));\n\n\tif (!resourceLoader) {\n\t\tresourceLoader = new DefaultResourceLoader({ cwd, agentDir, settingsManager });\n\t\tawait resourceLoader.reload();\n\t\ttime(\"resourceLoader.reload\");\n\t}\n\n\t// Check if session has existing data to restore\n\tconst existingSession = sessionManager.buildSessionContext();\n\tconst hasExistingSession = existingSession.messages.length > 0;\n\tconst hasThinkingEntry = sessionManager.getBranch().some((entry) => entry.type === \"thinking_level_change\");\n\n\tlet model = options.model;\n\tlet modelFallbackMessage: string | undefined;\n\n\t// If session has data, try to restore model from it\n\tif (!model && hasExistingSession && existingSession.model) {\n\t\tconst restoredModel = modelRegistry.find(existingSession.model.provider, existingSession.model.modelId);\n\t\tif (restoredModel && modelRegistry.hasConfiguredAuth(restoredModel)) {\n\t\t\tmodel = restoredModel;\n\t\t}\n\t\tif (!model) {\n\t\t\tmodelFallbackMessage = `Could not restore model ${existingSession.model.provider}/${existingSession.model.modelId}`;\n\t\t}\n\t}\n\n\t// If still no model, use findInitialModel (checks settings default, then provider defaults)\n\tif (!model) {\n\t\t// The pi-layer settings.json default wins; when it is unset, fall back to the\n\t\t// hoo-config.json `llm.default_provider` so the seeded/user default actually\n\t\t// takes effect. findInitialModel only honours it if that provider has auth.\n\t\tconst hooLlm = readHooConfig().llm;\n\t\tconst defaultProvider = settingsManager.getDefaultProvider() ?? hooLlm?.default_provider;\n\t\tconst defaultModelId =\n\t\t\tsettingsManager.getDefaultModel() ??\n\t\t\t(settingsManager.getDefaultProvider()\n\t\t\t\t? undefined\n\t\t\t\t: (hooLlm?.default_model ??\n\t\t\t\t\t(defaultProvider && defaultProvider in defaultModelPerProvider\n\t\t\t\t\t\t? defaultModelPerProvider[defaultProvider as keyof typeof defaultModelPerProvider]\n\t\t\t\t\t\t: undefined)));\n\t\tconst result = await findInitialModel({\n\t\t\tscopedModels: [],\n\t\t\tisContinuing: hasExistingSession,\n\t\t\tdefaultProvider,\n\t\t\tdefaultModelId,\n\t\t\tdefaultThinkingLevel: settingsManager.getDefaultThinkingLevel(),\n\t\t\tmodelRegistry,\n\t\t});\n\t\tmodel = result.model;\n\t\tif (!model) {\n\t\t\tmodelFallbackMessage = formatNoModelsAvailableMessage();\n\t\t} else if (modelFallbackMessage) {\n\t\t\tmodelFallbackMessage += `. Using ${model.provider}/${model.id}`;\n\t\t}\n\t}\n\n\tlet thinkingLevel = options.thinkingLevel;\n\n\t// If session has data, restore thinking level from it\n\tif (thinkingLevel === undefined && hasExistingSession) {\n\t\tthinkingLevel = hasThinkingEntry\n\t\t\t? (existingSession.thinkingLevel as ThinkingLevel)\n\t\t\t: (settingsManager.getDefaultThinkingLevel() ?? DEFAULT_THINKING_LEVEL);\n\t}\n\n\t// Fall back to settings default\n\tif (thinkingLevel === undefined) {\n\t\tthinkingLevel = settingsManager.getDefaultThinkingLevel() ?? DEFAULT_THINKING_LEVEL;\n\t}\n\n\t// Clamp to model capabilities\n\tif (!model) {\n\t\tthinkingLevel = \"off\";\n\t} else {\n\t\tthinkingLevel = clampThinkingLevel(model, thinkingLevel) as ThinkingLevel;\n\t}\n\n\t// `search` is always active: it answers \"find where X lives\" with ranked\n\t// results and degrades to exact-text lexical retrieval when no semantic\n\t// index is present, so it needs no binary to be useful. The\n\t// `enableEmbsearchTools` flag only controls whether the semantic index is\n\t// built and fused in (see main.ts) — not whether the tool exists.\n\tconst defaultActiveToolNames: ToolName[] = [\"read\", \"bash\", \"edit\", \"write\", \"search\"];\n\t// Web tools are registered as base tools but inactive by default; opt-in adds\n\t// them to the default active set. An explicit allowlist (`tools`) takes over\n\t// fully, so callers must list them there to enable in that mode.\n\tconst optInActiveToolNames: ToolName[] = [\n\t\t...(options.enableWebTools ? [\"webfetch\", \"websearch\"] : []),\n\t] as ToolName[];\n\tconst allowedToolNames = options.tools ?? (options.noTools === \"all\" ? [] : undefined);\n\tconst initialActiveToolNames: string[] = options.tools\n\t\t? [...options.tools]\n\t\t: options.noTools\n\t\t\t? []\n\t\t\t: [...defaultActiveToolNames, ...optInActiveToolNames];\n\n\tlet agent: Agent;\n\n\t// Create convertToLlm wrapper that filters images if blockImages is enabled (defense-in-depth)\n\tconst convertToLlmWithBlockImages = (messages: AgentMessage[]): Message[] => {\n\t\tconst converted = convertToLlm(messages);\n\t\t// Check setting dynamically so mid-session changes take effect\n\t\tif (!settingsManager.getBlockImages()) {\n\t\t\treturn converted;\n\t\t}\n\t\t// Filter out ImageContent from all messages, replacing with text placeholder\n\t\treturn converted.map((msg) => {\n\t\t\tif (msg.role === \"user\" || msg.role === \"toolResult\") {\n\t\t\t\tconst content = msg.content;\n\t\t\t\tif (Array.isArray(content)) {\n\t\t\t\t\tconst hasImages = content.some((c) => c.type === \"image\");\n\t\t\t\t\tif (hasImages) {\n\t\t\t\t\t\tconst filteredContent = content\n\t\t\t\t\t\t\t.map((c) =>\n\t\t\t\t\t\t\t\tc.type === \"image\" ? { type: \"text\" as const, text: \"Image reading is disabled.\" } : c,\n\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\t.filter(\n\t\t\t\t\t\t\t\t(c, i, arr) =>\n\t\t\t\t\t\t\t\t\t// Dedupe consecutive \"Image reading is disabled.\" texts\n\t\t\t\t\t\t\t\t\t!(\n\t\t\t\t\t\t\t\t\t\tc.type === \"text\" &&\n\t\t\t\t\t\t\t\t\t\tc.text === \"Image reading is disabled.\" &&\n\t\t\t\t\t\t\t\t\t\ti > 0 &&\n\t\t\t\t\t\t\t\t\t\tarr[i - 1].type === \"text\" &&\n\t\t\t\t\t\t\t\t\t\t(arr[i - 1] as { type: \"text\"; text: string }).text === \"Image reading is disabled.\"\n\t\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\treturn { ...msg, content: filteredContent };\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn msg;\n\t\t});\n\t};\n\n\tconst extensionRunnerRef: { current?: ExtensionRunner } = {};\n\n\t// Token-budget pressure for context GC: the fraction of the active model's\n\t// context window in use, measured from the real usage on the outgoing\n\t// message copy. It latches to a high-water mark so that our own evictions —\n\t// which shrink the next turn's measured usage — cannot oscillate a message\n\t// in and out of the transcript and thrash the provider's prefix cache. A\n\t// large drop (compaction, fork) resets the latch to the new, smaller size.\n\tlet budgetPressureHighWater = 0;\n\tconst getBudgetPressure = (contextMessages: AgentMessage[]): number => {\n\t\tconst contextWindow = agent.state.model?.contextWindow ?? 0;\n\t\tif (contextWindow <= 0) return 0;\n\t\tconst gauge = Math.min(estimateContextTokens(contextMessages).tokens / contextWindow, 1);\n\t\tif (gauge > budgetPressureHighWater) {\n\t\t\tbudgetPressureHighWater = gauge; // rising usage — track it\n\t\t} else if (gauge < budgetPressureHighWater - 0.15) {\n\t\t\tbudgetPressureHighWater = gauge; // context collapsed — reset the latch\n\t\t}\n\t\treturn budgetPressureHighWater;\n\t};\n\n\tagent = new Agent({\n\t\tinitialState: {\n\t\t\tsystemPrompt: \"\",\n\t\t\tmodel,\n\t\t\tthinkingLevel,\n\t\t\ttools: [],\n\t\t},\n\t\tconvertToLlm: convertToLlmWithBlockImages,\n\t\tcreateBackgroundResultMessage: createBackgroundTaskMessage,\n\t\tcreateBackgroundPlaceholder: (toolCall) => createBackgroundPlaceholderText(toolCall),\n\t\t// Report in-process background tool load (e.g. background MCP tools) to the\n\t\t// subagent lifeguard so it widens its heartbeat/timeout tolerance for\n\t\t// concurrently-monitored subagents. Peek (don't create) the pool: background\n\t\t// tools can run before any subagent is ever dispatched.\n\t\tonBackgroundTaskCountChange: (count) => peekSubagentPool()?.setExternalLoad(count),\n\t\tstreamFn: async (model, context, options) => {\n\t\t\tconst auth = await modelRegistry.getApiKeyAndHeaders(model);\n\t\t\tif (!auth.ok) {\n\t\t\t\tthrow new Error(auth.error);\n\t\t\t}\n\t\t\tconst providerRetrySettings = settingsManager.getProviderRetrySettings();\n\t\t\tconst attributionHeaders = getAttributionHeaders(model, settingsManager);\n\t\t\treturn streamSimple(model, context, {\n\t\t\t\t...options,\n\t\t\t\tapiKey: auth.apiKey,\n\t\t\t\ttimeoutMs: options?.timeoutMs ?? providerRetrySettings.timeoutMs,\n\t\t\t\tmaxRetries: options?.maxRetries ?? providerRetrySettings.maxRetries,\n\t\t\t\tmaxRetryDelayMs: options?.maxRetryDelayMs ?? providerRetrySettings.maxRetryDelayMs,\n\t\t\t\theaders:\n\t\t\t\t\tattributionHeaders || auth.headers || options?.headers\n\t\t\t\t\t\t? { ...attributionHeaders, ...auth.headers, ...options?.headers }\n\t\t\t\t\t\t: undefined,\n\t\t\t});\n\t\t},\n\t\tonPayload: async (payload, _model) => {\n\t\t\tconst runner = extensionRunnerRef.current;\n\t\t\tif (!runner?.hasHandlers(\"before_provider_request\")) {\n\t\t\t\treturn payload;\n\t\t\t}\n\t\t\treturn runner.emitBeforeProviderRequest(payload);\n\t\t},\n\t\tonResponse: async (response, _model) => {\n\t\t\tconst runner = extensionRunnerRef.current;\n\t\t\tif (!runner?.hasHandlers(\"after_provider_response\")) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tawait runner.emit({\n\t\t\t\ttype: \"after_provider_response\",\n\t\t\t\tstatus: response.status,\n\t\t\t\theaders: response.headers,\n\t\t\t});\n\t\t},\n\t\tsessionId: sessionManager.getSessionId(),\n\t\ttransformContext: async (messages) => {\n\t\t\tconst runner = extensionRunnerRef.current;\n\t\t\tconst transformed = runner ? await runner.emitContext(messages) : messages;\n\t\t\tif (!settingsManager.getContextGcEnabled()) return transformed;\n\t\t\treturn evictSupersededReads(transformed, { cwd, budgetPressure: getBudgetPressure(transformed) });\n\t\t},\n\t\tsteeringMode: settingsManager.getSteeringMode(),\n\t\tfollowUpMode: settingsManager.getFollowUpMode(),\n\t\ttransport: settingsManager.getTransport(),\n\t\tthinkingBudgets: settingsManager.getThinkingBudgets(),\n\t\tthinkingDisplay: settingsManager.getThinkingDisplay(),\n\t\tmaxRetryDelayMs: settingsManager.getProviderRetrySettings().maxRetryDelayMs,\n\t});\n\n\t// Restore messages if session has existing data\n\tif (hasExistingSession) {\n\t\tagent.state.messages = existingSession.messages;\n\t\tif (!hasThinkingEntry) {\n\t\t\tsessionManager.appendThinkingLevelChange(thinkingLevel);\n\t\t}\n\t} else {\n\t\t// Save initial model and thinking level for new sessions so they can be restored on resume\n\t\tif (model) {\n\t\t\tsessionManager.appendModelChange(model.provider, model.id);\n\t\t}\n\t\tsessionManager.appendThinkingLevelChange(thinkingLevel);\n\t}\n\n\tconst session = new AgentSession({\n\t\tagent,\n\t\tsessionManager,\n\t\tsettingsManager,\n\t\tcwd,\n\t\tscopedModels: options.scopedModels,\n\t\tresourceLoader,\n\t\tcustomTools: options.customTools,\n\t\tbaseToolsOverride: options.baseToolsOverride,\n\t\tmodelRegistry,\n\t\tinitialActiveToolNames,\n\t\tallowedToolNames,\n\t\tdisallowedToolNames: options.disallowedTools,\n\t\textensionRunnerRef,\n\t\tsessionStartEvent: options.sessionStartEvent,\n\t});\n\tconst extensionsResult = resourceLoader.getExtensions();\n\n\treturn {\n\t\tsession,\n\t\textensionsResult,\n\t\tmodelFallbackMessage,\n\t};\n}\n"]}
1
+ {"version":3,"file":"sdk.js","sourceRoot":"","sources":["../../src/core/sdk.ts"],"names":[],"mappings":"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","sourcesContent":["import { join } from \"node:path\";\nimport {\n\tAgent,\n\ttype AgentMessage,\n\ttype AgentTool,\n\tconvertToLlm,\n\tcreateBackgroundPlaceholderText,\n\tcreateBackgroundTaskMessage,\n\testimateContextTokens,\n\ttype ThinkingLevel,\n} from \"@kolisachint/hoocode-agent-core\";\nimport { clampThinkingLevel, type Message, type Model, streamSimple } from \"@kolisachint/hoocode-ai\";\nimport { getAgentDir } from \"../config.js\";\nimport { readConfig as readHooConfig } from \"../extensions/core/config.js\";\nimport { AgentSession } from \"./agent-session.js\";\nimport { formatNoModelsAvailableMessage } from \"./auth-guidance.js\";\nimport { AuthStorage } from \"./auth-storage.js\";\nimport { evictSupersededReads } from \"./context-gc.js\";\nimport { DEFAULT_THINKING_LEVEL } from \"./defaults.js\";\nimport type { ExtensionRunner, LoadExtensionsResult, SessionStartEvent, ToolDefinition } from \"./extensions/index.js\";\nimport { ModelRegistry } from \"./model-registry.js\";\nimport { defaultModelPerProvider, findInitialModel } from \"./model-resolver.js\";\nimport type { ResourceLoader } from \"./resource-loader.js\";\nimport { DefaultResourceLoader } from \"./resource-loader.js\";\nimport { getDefaultSessionDir, SessionManager } from \"./session-manager.js\";\nimport { SettingsManager } from \"./settings-manager.js\";\nimport { peekSubagentPool } from \"./subagent-pool-instance.js\";\nimport { isInstallTelemetryEnabled } from \"./telemetry.js\";\nimport { time } from \"./timings.js\";\nimport {\n\tcreateBashTool,\n\tcreateCodingTools,\n\tcreateEditTool,\n\tcreateReadOnlyTools,\n\tcreateReadTool,\n\tcreateWriteTool,\n\ttype ToolName,\n} from \"./tools/index.js\";\n\nexport interface CreateAgentSessionOptions {\n\t/** Working directory for project-local discovery. Default: process.cwd() */\n\tcwd?: string;\n\t/** Global config directory. Default: ~/.hoocode/agent */\n\tagentDir?: string;\n\n\t/** Auth storage for credentials. Default: AuthStorage.create(agentDir/auth.json) */\n\tauthStorage?: AuthStorage;\n\t/** Model registry. Default: ModelRegistry.create(authStorage, agentDir/models.json) */\n\tmodelRegistry?: ModelRegistry;\n\n\t/** Model to use. Default: from settings, else first available */\n\tmodel?: Model<any>;\n\t/** Thinking level. Default: from settings, else 'medium' (clamped to model capabilities) */\n\tthinkingLevel?: ThinkingLevel;\n\t/** Models available for cycling (Ctrl+P in interactive mode) */\n\tscopedModels?: Array<{ model: Model<any>; thinkingLevel?: ThinkingLevel }>;\n\n\t/**\n\t * Optional default tool suppression mode when no explicit allowlist is provided.\n\t *\n\t * - \"all\": start with no tools enabled\n\t * - \"builtin\": disable the default built-in tools (read, bash, edit, write)\n\t * but keep extension/custom tools enabled\n\t */\n\tnoTools?: \"all\" | \"builtin\";\n\t/**\n\t * Optional allowlist of tool names.\n\t *\n\t * When omitted, hoocode enables the default built-in tools (read, bash, edit, write)\n\t * and leaves extension/custom tools enabled unless `noTools` changes that default.\n\t * When provided, only the listed tool names are enabled.\n\t */\n\ttools?: string[];\n\t/**\n\t * Optional denylist of tool names, subtracted from whatever set is otherwise\n\t * enabled (allowlist or default). Applied to built-in, extension, and custom tools.\n\t */\n\tdisallowedTools?: string[];\n\t/**\n\t * Enable the built-in `webfetch` + `websearch` tools, which are defined but\n\t * inactive by default. Ignored when an explicit `tools` allowlist is provided\n\t * (list `webfetch`/`websearch` there instead). Network access is still gated\n\t * per call and filtered by `.webtoolsignore`.\n\t */\n\tenableWebTools?: boolean;\n\t/**\n\t * Enable the semantic index layer for the built-in `SearchCodebase` tool (ranked\n\t * lexical + semantic retrieval, rank-fused). The search tool is active by\n\t * default; this flag only toggles the optional embedding index. When false,\n\t * search degrades to lexical-only. Ignored when an explicit `tools` allowlist\n\t * is provided (list it there instead).\n\t */\n\tenableSemanticIndex?: boolean;\n\t/** Custom tools to register (in addition to built-in tools). */\n\tcustomTools?: ToolDefinition[];\n\t/**\n\t * Replace the built-in base tools entirely (the light preset uses this to\n\t * swap in short-schema read/write/edit/bash variants). Keys become the\n\t * default active tool set when no explicit `tools` allowlist is provided.\n\t */\n\tbaseToolsOverride?: Record<string, AgentTool>;\n\n\t/** Resource loader. When omitted, DefaultResourceLoader is used. */\n\tresourceLoader?: ResourceLoader;\n\n\t/** Session manager. Default: SessionManager.create(cwd) */\n\tsessionManager?: SessionManager;\n\n\t/** Settings manager. Default: SettingsManager.create(cwd, agentDir) */\n\tsettingsManager?: SettingsManager;\n\t/** Session start event metadata for extension runtime startup. */\n\tsessionStartEvent?: SessionStartEvent;\n}\n\n/** Result from createAgentSession */\nexport interface CreateAgentSessionResult {\n\t/** The created session */\n\tsession: AgentSession;\n\t/** Extensions result (for UI context setup in interactive mode) */\n\textensionsResult: LoadExtensionsResult;\n\t/** Warning if session was restored with a different model than saved */\n\tmodelFallbackMessage?: string;\n}\n\n// Re-exports\n\nexport * from \"./agent-session-runtime.js\";\nexport type { ExtensionFactory } from \"./extensions/index.js\";\nexport type { PromptTemplate } from \"./prompt-templates.js\";\n\nexport {\n\t// Tool factories (for custom cwd)\n\tcreateCodingTools,\n\tcreateReadOnlyTools,\n\tcreateReadTool,\n\tcreateBashTool,\n\tcreateEditTool,\n\tcreateWriteTool,\n};\n\n// Helper Functions\n\nfunction getDefaultAgentDir(): string {\n\treturn getAgentDir();\n}\n\nfunction getAttributionHeaders(\n\tmodel: Model<any>,\n\tsettingsManager: SettingsManager,\n): Record<string, string> | undefined {\n\tif (!isInstallTelemetryEnabled(settingsManager)) {\n\t\treturn undefined;\n\t}\n\n\tif (model.provider === \"openrouter\" || model.baseUrl.includes(\"openrouter.ai\")) {\n\t\treturn {\n\t\t\t\"HTTP-Referer\": \"https://github.com/kolisachint/hoocode\",\n\t\t\t\"X-OpenRouter-Title\": \"hoocode\",\n\t\t\t\"X-OpenRouter-Categories\": \"cli-agent\",\n\t\t};\n\t}\n\n\treturn undefined;\n}\n\n/**\n * Create an AgentSession with the specified options.\n *\n * @example\n * ```typescript\n * // Minimal - uses defaults\n * const { session } = await createAgentSession();\n *\n * // With explicit model\n * import { getModel } from '@kolisachint/hoocode-ai';\n * const { session } = await createAgentSession({\n * model: getModel('anthropic', 'claude-opus-4-5'),\n * thinkingLevel: 'high',\n * });\n *\n * // Continue previous session\n * const { session, modelFallbackMessage } = await createAgentSession({\n * continueSession: true,\n * });\n *\n * // Full control\n * const loader = new DefaultResourceLoader({\n * cwd: process.cwd(),\n * agentDir: getAgentDir(),\n * settingsManager: SettingsManager.create(),\n * });\n * await loader.reload();\n * const { session } = await createAgentSession({\n * model: myModel,\n * tools: [readTool, bashTool],\n * resourceLoader: loader,\n * sessionManager: SessionManager.inMemory(),\n * });\n * ```\n */\nexport async function createAgentSession(options: CreateAgentSessionOptions = {}): Promise<CreateAgentSessionResult> {\n\tconst cwd = options.cwd ?? options.sessionManager?.getCwd() ?? process.cwd();\n\tconst agentDir = options.agentDir ?? getDefaultAgentDir();\n\tlet resourceLoader = options.resourceLoader;\n\n\t// Use provided or create AuthStorage and ModelRegistry\n\tconst authPath = options.agentDir ? join(agentDir, \"auth.json\") : undefined;\n\tconst modelsPath = options.agentDir ? join(agentDir, \"models.json\") : undefined;\n\tconst authStorage = options.authStorage ?? AuthStorage.create(authPath);\n\tconst modelRegistry = options.modelRegistry ?? ModelRegistry.create(authStorage, modelsPath);\n\n\tconst settingsManager = options.settingsManager ?? SettingsManager.create(cwd, agentDir);\n\tconst sessionManager = options.sessionManager ?? SessionManager.create(cwd, getDefaultSessionDir(cwd, agentDir));\n\n\tif (!resourceLoader) {\n\t\tresourceLoader = new DefaultResourceLoader({ cwd, agentDir, settingsManager });\n\t\tawait resourceLoader.reload();\n\t\ttime(\"resourceLoader.reload\");\n\t}\n\n\t// Check if session has existing data to restore\n\tconst existingSession = sessionManager.buildSessionContext();\n\tconst hasExistingSession = existingSession.messages.length > 0;\n\tconst hasThinkingEntry = sessionManager.getBranch().some((entry) => entry.type === \"thinking_level_change\");\n\n\tlet model = options.model;\n\tlet modelFallbackMessage: string | undefined;\n\n\t// If session has data, try to restore model from it\n\tif (!model && hasExistingSession && existingSession.model) {\n\t\tconst restoredModel = modelRegistry.find(existingSession.model.provider, existingSession.model.modelId);\n\t\tif (restoredModel && modelRegistry.hasConfiguredAuth(restoredModel)) {\n\t\t\tmodel = restoredModel;\n\t\t}\n\t\tif (!model) {\n\t\t\tmodelFallbackMessage = `Could not restore model ${existingSession.model.provider}/${existingSession.model.modelId}`;\n\t\t}\n\t}\n\n\t// If still no model, use findInitialModel (checks settings default, then provider defaults)\n\tif (!model) {\n\t\t// The pi-layer settings.json default wins; when it is unset, fall back to the\n\t\t// hoo-config.json `llm.default_provider` so the seeded/user default actually\n\t\t// takes effect. findInitialModel only honours it if that provider has auth.\n\t\tconst hooLlm = readHooConfig().llm;\n\t\tconst defaultProvider = settingsManager.getDefaultProvider() ?? hooLlm?.default_provider;\n\t\tconst defaultModelId =\n\t\t\tsettingsManager.getDefaultModel() ??\n\t\t\t(settingsManager.getDefaultProvider()\n\t\t\t\t? undefined\n\t\t\t\t: (hooLlm?.default_model ??\n\t\t\t\t\t(defaultProvider && defaultProvider in defaultModelPerProvider\n\t\t\t\t\t\t? defaultModelPerProvider[defaultProvider as keyof typeof defaultModelPerProvider]\n\t\t\t\t\t\t: undefined)));\n\t\tconst result = await findInitialModel({\n\t\t\tscopedModels: [],\n\t\t\tisContinuing: hasExistingSession,\n\t\t\tdefaultProvider,\n\t\t\tdefaultModelId,\n\t\t\tdefaultThinkingLevel: settingsManager.getDefaultThinkingLevel(),\n\t\t\tmodelRegistry,\n\t\t});\n\t\tmodel = result.model;\n\t\tif (!model) {\n\t\t\tmodelFallbackMessage = formatNoModelsAvailableMessage();\n\t\t} else if (modelFallbackMessage) {\n\t\t\tmodelFallbackMessage += `. Using ${model.provider}/${model.id}`;\n\t\t}\n\t}\n\n\tlet thinkingLevel = options.thinkingLevel;\n\n\t// If session has data, restore thinking level from it\n\tif (thinkingLevel === undefined && hasExistingSession) {\n\t\tthinkingLevel = hasThinkingEntry\n\t\t\t? (existingSession.thinkingLevel as ThinkingLevel)\n\t\t\t: (settingsManager.getDefaultThinkingLevel() ?? DEFAULT_THINKING_LEVEL);\n\t}\n\n\t// Fall back to settings default\n\tif (thinkingLevel === undefined) {\n\t\tthinkingLevel = settingsManager.getDefaultThinkingLevel() ?? DEFAULT_THINKING_LEVEL;\n\t}\n\n\t// Clamp to model capabilities\n\tif (!model) {\n\t\tthinkingLevel = \"off\";\n\t} else {\n\t\tthinkingLevel = clampThinkingLevel(model, thinkingLevel) as ThinkingLevel;\n\t}\n\n\t// `SearchCodebase` is always active: it answers \"find where X lives\" with ranked\n\t// results and degrades to exact-text lexical retrieval when no semantic\n\t// index is present, so it needs no binary to be useful. The\n\t// `enableSemanticIndex` flag only controls whether the semantic index is\n\t// built and fused in (see main.ts) — not whether the tool exists.\n\tconst defaultActiveToolNames: ToolName[] = [\"read\", \"bash\", \"edit\", \"write\", \"SearchCodebase\"];\n\t// Web tools are registered as base tools but inactive by default; opt-in adds\n\t// them to the default active set. An explicit allowlist (`tools`) takes over\n\t// fully, so callers must list them there to enable in that mode.\n\tconst optInActiveToolNames: ToolName[] = [\n\t\t...(options.enableWebTools ? [\"webfetch\", \"websearch\"] : []),\n\t] as ToolName[];\n\tconst allowedToolNames = options.tools ?? (options.noTools === \"all\" ? [] : undefined);\n\tconst initialActiveToolNames: string[] = options.tools\n\t\t? [...options.tools]\n\t\t: options.noTools\n\t\t\t? []\n\t\t\t: [...defaultActiveToolNames, ...optInActiveToolNames];\n\n\tlet agent: Agent;\n\n\t// Create convertToLlm wrapper that filters images if blockImages is enabled (defense-in-depth)\n\tconst convertToLlmWithBlockImages = (messages: AgentMessage[]): Message[] => {\n\t\tconst converted = convertToLlm(messages);\n\t\t// Check setting dynamically so mid-session changes take effect\n\t\tif (!settingsManager.getBlockImages()) {\n\t\t\treturn converted;\n\t\t}\n\t\t// Filter out ImageContent from all messages, replacing with text placeholder\n\t\treturn converted.map((msg) => {\n\t\t\tif (msg.role === \"user\" || msg.role === \"toolResult\") {\n\t\t\t\tconst content = msg.content;\n\t\t\t\tif (Array.isArray(content)) {\n\t\t\t\t\tconst hasImages = content.some((c) => c.type === \"image\");\n\t\t\t\t\tif (hasImages) {\n\t\t\t\t\t\tconst filteredContent = content\n\t\t\t\t\t\t\t.map((c) =>\n\t\t\t\t\t\t\t\tc.type === \"image\" ? { type: \"text\" as const, text: \"Image reading is disabled.\" } : c,\n\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\t.filter(\n\t\t\t\t\t\t\t\t(c, i, arr) =>\n\t\t\t\t\t\t\t\t\t// Dedupe consecutive \"Image reading is disabled.\" texts\n\t\t\t\t\t\t\t\t\t!(\n\t\t\t\t\t\t\t\t\t\tc.type === \"text\" &&\n\t\t\t\t\t\t\t\t\t\tc.text === \"Image reading is disabled.\" &&\n\t\t\t\t\t\t\t\t\t\ti > 0 &&\n\t\t\t\t\t\t\t\t\t\tarr[i - 1].type === \"text\" &&\n\t\t\t\t\t\t\t\t\t\t(arr[i - 1] as { type: \"text\"; text: string }).text === \"Image reading is disabled.\"\n\t\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\treturn { ...msg, content: filteredContent };\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn msg;\n\t\t});\n\t};\n\n\tconst extensionRunnerRef: { current?: ExtensionRunner } = {};\n\n\t// Token-budget pressure for context GC: the fraction of the active model's\n\t// context window in use, measured from the real usage on the outgoing\n\t// message copy. It latches to a high-water mark so that our own evictions —\n\t// which shrink the next turn's measured usage — cannot oscillate a message\n\t// in and out of the transcript and thrash the provider's prefix cache. A\n\t// large drop (compaction, fork) resets the latch to the new, smaller size.\n\tlet budgetPressureHighWater = 0;\n\tconst getBudgetPressure = (contextMessages: AgentMessage[]): number => {\n\t\tconst contextWindow = agent.state.model?.contextWindow ?? 0;\n\t\tif (contextWindow <= 0) return 0;\n\t\tconst gauge = Math.min(estimateContextTokens(contextMessages).tokens / contextWindow, 1);\n\t\tif (gauge > budgetPressureHighWater) {\n\t\t\tbudgetPressureHighWater = gauge; // rising usage — track it\n\t\t} else if (gauge < budgetPressureHighWater - 0.15) {\n\t\t\tbudgetPressureHighWater = gauge; // context collapsed — reset the latch\n\t\t}\n\t\treturn budgetPressureHighWater;\n\t};\n\n\tagent = new Agent({\n\t\tinitialState: {\n\t\t\tsystemPrompt: \"\",\n\t\t\tmodel,\n\t\t\tthinkingLevel,\n\t\t\ttools: [],\n\t\t},\n\t\tconvertToLlm: convertToLlmWithBlockImages,\n\t\tcreateBackgroundResultMessage: createBackgroundTaskMessage,\n\t\tcreateBackgroundPlaceholder: (toolCall) => createBackgroundPlaceholderText(toolCall),\n\t\t// Report in-process background tool load (e.g. background MCP tools) to the\n\t\t// subagent lifeguard so it widens its heartbeat/timeout tolerance for\n\t\t// concurrently-monitored subagents. Peek (don't create) the pool: background\n\t\t// tools can run before any subagent is ever dispatched.\n\t\tonBackgroundTaskCountChange: (count) => peekSubagentPool()?.setExternalLoad(count),\n\t\tstreamFn: async (model, context, options) => {\n\t\t\tconst auth = await modelRegistry.getApiKeyAndHeaders(model);\n\t\t\tif (!auth.ok) {\n\t\t\t\tthrow new Error(auth.error);\n\t\t\t}\n\t\t\tconst providerRetrySettings = settingsManager.getProviderRetrySettings();\n\t\t\tconst attributionHeaders = getAttributionHeaders(model, settingsManager);\n\t\t\treturn streamSimple(model, context, {\n\t\t\t\t...options,\n\t\t\t\tapiKey: auth.apiKey,\n\t\t\t\ttimeoutMs: options?.timeoutMs ?? providerRetrySettings.timeoutMs,\n\t\t\t\tmaxRetries: options?.maxRetries ?? providerRetrySettings.maxRetries,\n\t\t\t\tmaxRetryDelayMs: options?.maxRetryDelayMs ?? providerRetrySettings.maxRetryDelayMs,\n\t\t\t\theaders:\n\t\t\t\t\tattributionHeaders || auth.headers || options?.headers\n\t\t\t\t\t\t? { ...attributionHeaders, ...auth.headers, ...options?.headers }\n\t\t\t\t\t\t: undefined,\n\t\t\t});\n\t\t},\n\t\tonPayload: async (payload, _model) => {\n\t\t\tconst runner = extensionRunnerRef.current;\n\t\t\tif (!runner?.hasHandlers(\"before_provider_request\")) {\n\t\t\t\treturn payload;\n\t\t\t}\n\t\t\treturn runner.emitBeforeProviderRequest(payload);\n\t\t},\n\t\tonResponse: async (response, _model) => {\n\t\t\tconst runner = extensionRunnerRef.current;\n\t\t\tif (!runner?.hasHandlers(\"after_provider_response\")) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tawait runner.emit({\n\t\t\t\ttype: \"after_provider_response\",\n\t\t\t\tstatus: response.status,\n\t\t\t\theaders: response.headers,\n\t\t\t});\n\t\t},\n\t\tsessionId: sessionManager.getSessionId(),\n\t\ttransformContext: async (messages) => {\n\t\t\tconst runner = extensionRunnerRef.current;\n\t\t\tconst transformed = runner ? await runner.emitContext(messages) : messages;\n\t\t\tif (!settingsManager.getContextGcEnabled()) return transformed;\n\t\t\treturn evictSupersededReads(transformed, { cwd, budgetPressure: getBudgetPressure(transformed) });\n\t\t},\n\t\tsteeringMode: settingsManager.getSteeringMode(),\n\t\tfollowUpMode: settingsManager.getFollowUpMode(),\n\t\ttransport: settingsManager.getTransport(),\n\t\tthinkingBudgets: settingsManager.getThinkingBudgets(),\n\t\tthinkingDisplay: settingsManager.getThinkingDisplay(),\n\t\tmaxRetryDelayMs: settingsManager.getProviderRetrySettings().maxRetryDelayMs,\n\t});\n\n\t// Restore messages if session has existing data\n\tif (hasExistingSession) {\n\t\tagent.state.messages = existingSession.messages;\n\t\tif (!hasThinkingEntry) {\n\t\t\tsessionManager.appendThinkingLevelChange(thinkingLevel);\n\t\t}\n\t} else {\n\t\t// Save initial model and thinking level for new sessions so they can be restored on resume\n\t\tif (model) {\n\t\t\tsessionManager.appendModelChange(model.provider, model.id);\n\t\t}\n\t\tsessionManager.appendThinkingLevelChange(thinkingLevel);\n\t}\n\n\tconst session = new AgentSession({\n\t\tagent,\n\t\tsessionManager,\n\t\tsettingsManager,\n\t\tcwd,\n\t\tscopedModels: options.scopedModels,\n\t\tresourceLoader,\n\t\tcustomTools: options.customTools,\n\t\tbaseToolsOverride: options.baseToolsOverride,\n\t\tmodelRegistry,\n\t\tinitialActiveToolNames,\n\t\tallowedToolNames,\n\t\tdisallowedToolNames: options.disallowedTools,\n\t\textensionRunnerRef,\n\t\tsessionStartEvent: options.sessionStartEvent,\n\t});\n\tconst extensionsResult = resourceLoader.getExtensions();\n\n\treturn {\n\t\tsession,\n\t\textensionsResult,\n\t\tmodelFallbackMessage,\n\t};\n}\n"]}
@@ -10,7 +10,7 @@
10
10
  * docs path into model context.
11
11
  *
12
12
  * That gap is not one the model can close by itself. Its cwd is the user's
13
- * project, so `search` there discovers the user's docs, never hoocode's,
13
+ * project, so `SearchCodebase` there discovers the user's docs, never hoocode's,
14
14
  * which live in an install directory whose path it cannot derive.
15
15
  *
16
16
  * Descriptions come from `docs/index.md` rather than being duplicated here.
@@ -1 +1 @@
1
- {"version":3,"file":"self-docs.d.ts","sourceRoot":"","sources":["../../src/core/self-docs.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAMH,MAAM,WAAW,OAAO;IACvB,kFAAkF;IAClF,EAAE,EAAE,MAAM,CAAC;IACX,8DAA8D;IAC9D,IAAI,EAAE,MAAM,CAAC;IACb,kCAAkC;IAClC,KAAK,EAAE,MAAM,CAAC;IACd,iFAAiF;IACjF,WAAW,EAAE,MAAM,CAAC;CACpB;AA8GD,oFAAoF;AACpF,wBAAgB,aAAa,IAAI,IAAI,CAGpC;AAED;;;;;;GAMG;AACH,wBAAgB,YAAY,IAAI,OAAO,EAAE,CA6CxC;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,uBAAuB,CAAC,IAAI,GAAE,SAAS,OAAO,EAAmB,GAAG,MAAM,CAsBzF;AAMD;;;;;;;;;;GAUG;AACH,MAAM,WAAW,cAAc;IAC9B,iDAAiD;IACjD,EAAE,EAAE,MAAM,CAAC;IACX,sCAAsC;IACtC,IAAI,EAAE,MAAM,CAAC;IACb,iCAAiC;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,yFAAyF;IACzF,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,wEAAwE;IACxE,IAAI,EAAE,MAAM,CAAC;IACb,gFAAgF;IAChF,OAAO,EAAE,MAAM,CAAC;CAChB;AAuBD,gGAA2F;AAC3F,wBAAgB,YAAY,CAAC,OAAO,EAAE,cAAc,GAAG,MAAM,CAE5D;AAED;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,cAAc,EAAE,CAmDhG;AAsBD,0FAA0F;AAC1F,wBAAgB,oBAAoB,IAAI,IAAI,CAE3C;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,IAAI,cAAc,EAAE,CAiBtD","sourcesContent":["/**\n * The agent's index of hoocode's *own* documentation.\n *\n * The startup banner promises \"hoocode can explain its own features and look up\n * its docs\", and the docs really do ship with the install (`package.json`\n * `files` includes `docs`, and `copy-binary-assets` copies them into `dist/` for\n * the pkg binaries). What was missing is the only part that makes the promise\n * true: telling the model they exist. `getDocsPath()` had exactly one consumer —\n * `auth-guidance.ts`, which prints paths to the *human* — so nothing ever put a\n * docs path into model context.\n *\n * That gap is not one the model can close by itself. Its cwd is the user's\n * project, so `search` there discovers the user's docs, never hoocode's,\n * which live in an install directory whose path it cannot derive.\n *\n * Descriptions come from `docs/index.md` rather than being duplicated here.\n * That file is a curated, human-maintained table of contents, and a second\n * hand-written list is how an index goes stale the first week nobody updates\n * it. The directory listing stays the source of truth for *what exists*, so a\n * new doc still shows up (described from its own first paragraph) on the day it\n * lands, with or without an index entry.\n */\n\nimport { existsSync, readdirSync, readFileSync, statSync } from \"node:fs\";\nimport { basename, dirname, join } from \"node:path\";\nimport { getChangelogPath, getDocsPath, getReadmePath } from \"../config.js\";\n\nexport interface SelfDoc {\n\t/** Stable id: the filename, e.g. `skills.md`. Also how the model refers to it. */\n\tid: string;\n\t/** Absolute path, ready to hand to the read tool verbatim. */\n\tpath: string;\n\t/** Human title, e.g. \"Skills\". */\n\ttitle: string;\n\t/** One line on what the doc covers. May be empty if nothing could be derived. */\n\tdescription: string;\n}\n\n/** How much of a doc to read when deriving a fallback description. */\nconst HEAD_BYTES = 2048;\n\n/** Cap on a derived description, so one run-on opening line cannot bloat the prompt. */\nconst MAX_DESCRIPTION = 110;\n\nfunction truncate(text: string, max = MAX_DESCRIPTION): string {\n\tconst clean = text.replace(/\\s+/g, \" \").trim();\n\tif (clean.length <= max) return clean;\n\treturn `${clean.slice(0, max - 1).trimEnd()}…`;\n}\n\n/** Strip inline markdown that adds noise but no meaning in a prompt listing. */\nfunction stripInlineMarkdown(text: string): string {\n\treturn text\n\t\t.replace(/\\[([^\\]]+)\\]\\([^)]*\\)/g, \"$1\") // links → their text\n\t\t.replace(/[`*_]/g, \"\")\n\t\t.trim();\n}\n\nfunction readHead(path: string): string {\n\ttry {\n\t\t// Whole-file read: these are small, and slicing bytes off a UTF-8 file can\n\t\t// split a multi-byte character. Truncate after decoding instead.\n\t\treturn readFileSync(path, \"utf-8\").slice(0, HEAD_BYTES);\n\t} catch {\n\t\treturn \"\";\n\t}\n}\n\n/** First `# ` heading, or undefined. */\nfunction firstHeading(markdown: string): string | undefined {\n\tfor (const line of markdown.split(/\\r?\\n/)) {\n\t\tconst match = /^#\\s+(.+)$/.exec(line.trim());\n\t\tif (match?.[1]) return stripInlineMarkdown(match[1]);\n\t}\n\treturn undefined;\n}\n\n/**\n * First real prose line: not a heading, blockquote, list item, fence, or table\n * row. Used only for docs the curated index does not describe.\n */\nfunction firstParagraph(markdown: string): string | undefined {\n\tlet inFence = false;\n\tfor (const raw of markdown.split(/\\r?\\n/)) {\n\t\tconst line = raw.trim();\n\t\tif (line.startsWith(\"```\")) {\n\t\t\tinFence = !inFence;\n\t\t\tcontinue;\n\t\t}\n\t\tif (inFence || line === \"\") continue;\n\t\tif (/^[#>|-]/.test(line) || /^\\d+\\./.test(line)) continue;\n\t\treturn stripInlineMarkdown(line);\n\t}\n\treturn undefined;\n}\n\n/**\n * Titles and descriptions the docs maintain about themselves, keyed by filename.\n *\n * Matches list entries of the form `- [Title](file.md) - description`, which is\n * how every section of `index.md` is written. Anything that does not match is\n * skipped rather than guessed at.\n */\nfunction parseCuratedIndex(docsRoot: string): Map<string, { title: string; description: string }> {\n\tconst curated = new Map<string, { title: string; description: string }>();\n\tconst indexPath = join(docsRoot, \"index.md\");\n\tif (!existsSync(indexPath)) return curated;\n\n\tlet content: string;\n\ttry {\n\t\tcontent = readFileSync(indexPath, \"utf-8\");\n\t} catch {\n\t\treturn curated;\n\t}\n\n\t// `[Title](file.md)` followed by a dash of any width and the description.\n\tconst entry = /^\\s*[-*]\\s*\\[([^\\]]+)\\]\\(([^)#]+\\.md)\\)\\s*[-–—:]\\s*(.+?)\\s*$/;\n\tfor (const line of content.split(/\\r?\\n/)) {\n\t\tconst match = entry.exec(line);\n\t\tif (!match) continue;\n\t\tconst [, title, target, description] = match;\n\t\tconst file = basename(target);\n\t\tif (curated.has(file)) continue; // first mention wins\n\t\tcurated.set(file, { title: stripInlineMarkdown(title), description: truncate(stripInlineMarkdown(description)) });\n\t}\n\treturn curated;\n}\n\nfunction describe(path: string, file: string, curated: Map<string, { title: string; description: string }>): SelfDoc {\n\tconst fromIndex = curated.get(file);\n\tif (fromIndex) {\n\t\treturn { id: file, path, title: fromIndex.title, description: fromIndex.description };\n\t}\n\t// Not in the curated index — derive from the doc itself so new files are\n\t// still usable the day they land.\n\tconst head = readHead(path);\n\treturn {\n\t\tid: file,\n\t\tpath,\n\t\ttitle: firstHeading(head) ?? file.replace(/\\.md$/, \"\"),\n\t\tdescription: truncate(firstParagraph(head) ?? \"\"),\n\t};\n}\n\nlet cached: SelfDoc[] | undefined;\n\n/** Drop the cached listing. Tests, and anything that relocates the package root. */\nexport function resetSelfDocs(): void {\n\tcached = undefined;\n\tcachedSections = undefined;\n}\n\n/**\n * Every shipped doc, sorted with the overview first and the rest alphabetical.\n *\n * Returns `[]` when the docs directory is absent rather than throwing: a source\n * checkout, an odd packaging, or a trimmed container should degrade to \"no docs\n * section in the prompt\", never to a failed session start.\n */\nexport function listSelfDocs(): SelfDoc[] {\n\tif (cached) return cached;\n\n\tconst docsRoot = getDocsPath();\n\tconst docs: SelfDoc[] = [];\n\n\tif (existsSync(docsRoot)) {\n\t\tconst curated = parseCuratedIndex(docsRoot);\n\t\tlet files: string[];\n\t\ttry {\n\t\t\tfiles = readdirSync(docsRoot).filter((f) => f.endsWith(\".md\"));\n\t\t} catch {\n\t\t\tfiles = [];\n\t\t}\n\t\t// Overview first: it is the doc that explains the others.\n\t\tfiles.sort((a, b) => (a === \"index.md\" ? -1 : b === \"index.md\" ? 1 : a.localeCompare(b)));\n\t\tfor (const file of files) {\n\t\t\tconst path = join(docsRoot, file);\n\t\t\ttry {\n\t\t\t\tif (!statSync(path).isFile()) continue;\n\t\t\t} catch {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tdocs.push(describe(path, file, curated));\n\t\t}\n\t}\n\n\t// README and CHANGELOG sit beside the docs directory, not inside it, but the\n\t// model needs them for the two questions the docs do not answer: what\n\t// hoocode is, and what changed in this version.\n\tconst extras: Array<{ path: string; title: string; description: string }> = [\n\t\t{ path: getReadmePath(), title: \"README\", description: \"What hoocode is, install, and a feature overview.\" },\n\t\t{\n\t\t\tpath: getChangelogPath(),\n\t\t\ttitle: \"Changelog\",\n\t\t\tdescription: \"Released versions and what changed in each.\",\n\t\t},\n\t];\n\tfor (const extra of extras) {\n\t\tif (!existsSync(extra.path)) continue;\n\t\tdocs.push({ id: basename(extra.path), path: extra.path, title: extra.title, description: extra.description });\n\t}\n\n\tcached = docs;\n\treturn docs;\n}\n\n/**\n * The system-prompt section, or `\"\"` when there is nothing to point at.\n *\n * Deliberately just filenames. An earlier version carried a one-line summary\n * per doc and cost ~860 tokens on every single turn, which is a poor trade for\n * something most turns never use — and it stopped being necessary once\n * SearchHooCode could retrieve at the heading level. Filenames alone still let\n * the model go straight to `themes.md` or `keybindings.md` for the obvious\n * cases, and anything less obvious is one search away. That is ~180 tokens.\n *\n * Directories are printed once rather than repeated per entry, for the same\n * reason: the path was the single largest term on every line.\n */\nexport function formatSelfDocsForPrompt(docs: readonly SelfDoc[] = listSelfDocs()): string {\n\tif (docs.length === 0) return \"\";\n\n\t// Insertion order is already meaningful (overview first, then alphabetical,\n\t// then README/CHANGELOG), so group without re-sorting.\n\tconst groups = new Map<string, string[]>();\n\tfor (const doc of docs) {\n\t\tconst root = dirname(doc.path);\n\t\tconst bucket = groups.get(root);\n\t\tif (bucket) bucket.push(doc.id);\n\t\telse groups.set(root, [doc.id]);\n\t}\n\n\tconst sections = [...groups].map(([root, files]) => `${root}/: ${files.join(\", \")}`);\n\n\treturn `\n\n# About hoocode itself\n\nYou are running inside hoocode. Its own docs ship with the install, listed below; hoocode is actively developed, so answer questions about it from these files rather than from memory. They sit outside the working directory, so searching the project will not find them. Use SearchHooCode to locate a specific heading, or read a file directly.\n\n${sections.join(\"\\n\")}`;\n}\n\n// ---------------------------------------------------------------------------\n// Section index\n// ---------------------------------------------------------------------------\n\n/**\n * A single heading's worth of a doc.\n *\n * Doc-level retrieval would add nothing the prompt listing above does not\n * already give: thirty files with a summary each are cheap enough to list in\n * full, so a search that answers \"read extensions.md\" is a round trip for\n * information the model already had. The questions that actually need\n * retrieval are the ones inside a 1,100-line file — \"how do I register a\n * tool?\" should land on `extensions.md § Custom tools` with a line number, not\n * on the file.\n */\nexport interface SelfDocSection {\n\t/** `<file>#<slug>`, unique across the corpus. */\n\tid: string;\n\t/** Filename, e.g. `extensions.md`. */\n\tfile: string;\n\t/** Absolute path to the file. */\n\tpath: string;\n\t/** Heading trail from the document title down, e.g. `[\"Extensions\", \"Custom tools\"]`. */\n\theadings: string[];\n\t/** 1-based line of the heading, so a reader can jump straight to it. */\n\tline: number;\n\t/** Start of the section body, for ranking and for showing why a hit matched. */\n\texcerpt: string;\n}\n\n/**\n * How much section body to keep.\n *\n * Every character past this is invisible to retrieval, so the cap is a recall\n * limit, not just a size one: at 240 a question about `/grill` missed the\n * section that documents it, because the term sat in the fourth sentence. 400\n * covers the opening of essentially every section here for about 95KB more\n * index across the corpus, which buys back that class of miss.\n */\nconst MAX_EXCERPT = 400;\n\n/** `Custom tools` → `custom-tools`, so ids stay stable and readable. */\nfunction slugify(heading: string): string {\n\treturn (\n\t\theading\n\t\t\t.toLowerCase()\n\t\t\t.replace(/[^a-z0-9]+/g, \"-\")\n\t\t\t.replace(/^-+|-+$/g, \"\") || \"section\"\n\t);\n}\n\n/** `extensions.md § Extensions › Custom tools` — what a search result is labelled with. */\nexport function sectionLabel(section: SelfDocSection): string {\n\treturn section.headings.length > 0 ? `${section.file} § ${section.headings.join(\" › \")}` : section.file;\n}\n\n/**\n * Split one markdown file into sections at its headings.\n *\n * Fenced code is tracked so a `#` comment inside a bash block cannot be\n * mistaken for a heading — which would otherwise split docs at every shell\n * comment. Code *content* still lands in the excerpt: the exact identifiers\n * someone searches for (`pi.registerTool`) usually live in the examples, and\n * dropping them would blind the lexical leg to the best terms in the file.\n */\nexport function splitIntoSections(markdown: string, file: string, path: string): SelfDocSection[] {\n\tconst lines = markdown.split(/\\r?\\n/);\n\tconst sections: SelfDocSection[] = [];\n\tconst trail: Array<{ depth: number; text: string }> = [];\n\tconst usedIds = new Set<string>();\n\n\tlet current: SelfDocSection | undefined;\n\tlet body: string[] = [];\n\tlet inFence = false;\n\n\tconst flush = (): void => {\n\t\tif (!current) return;\n\t\tcurrent.excerpt = truncate(stripInlineMarkdown(body.join(\" \")), MAX_EXCERPT);\n\t\tsections.push(current);\n\t\tbody = [];\n\t};\n\n\tfor (let i = 0; i < lines.length; i++) {\n\t\tconst raw = lines[i] ?? \"\";\n\t\tif (raw.trimStart().startsWith(\"```\")) {\n\t\t\tinFence = !inFence;\n\t\t\tcontinue;\n\t\t}\n\t\tconst heading = inFence ? null : /^(#{1,6})\\s+(.+?)\\s*$/.exec(raw);\n\t\tif (!heading) {\n\t\t\tif (raw.trim() !== \"\") body.push(raw.trim());\n\t\t\tcontinue;\n\t\t}\n\n\t\tflush();\n\n\t\tconst depth = heading[1]?.length ?? 1;\n\t\tconst text = stripInlineMarkdown(heading[2] ?? \"\");\n\t\twhile (trail.length > 0 && (trail[trail.length - 1]?.depth ?? 0) >= depth) trail.pop();\n\t\ttrail.push({ depth, text });\n\n\t\t// Disambiguate repeated headings (\"Example\" appears eleven times in\n\t\t// extensions.md) so ids stay unique and the registry does not collapse them.\n\t\tlet id = `${file}#${slugify(trail.map((t) => t.text).join(\"-\"))}`;\n\t\tif (usedIds.has(id)) {\n\t\t\tlet n = 2;\n\t\t\twhile (usedIds.has(`${id}-${n}`)) n++;\n\t\t\tid = `${id}-${n}`;\n\t\t}\n\t\tusedIds.add(id);\n\n\t\tcurrent = { id, file, path, headings: trail.map((t) => t.text), line: i + 1, excerpt: \"\" };\n\t}\n\tflush();\n\n\treturn sections;\n}\n\n/**\n * Files kept out of the section index.\n *\n * The changelog is 40% of the corpus by section count and none of it answers\n * \"how does X work\": it is hundreds of near-identical `Added`/`Fixed`/`Changed`\n * headings under version numbers, which crowd real documentation out of the\n * ranking while matching almost any query about a feature by name.\n *\n * `index.md` is excluded for the mirror-image reason: it is a table of contents,\n * so its \"sections\" are lists of links whose text is every other doc's title and\n * summary. That makes it match any query those docs would match, while carrying\n * none of the content — a guaranteed false attractor that displaces the page it\n * is pointing at.\n *\n * Both stay in the prompt's filename listing, one read away.\n */\nconst SECTION_INDEX_EXCLUDED = new Set([\"CHANGELOG.md\", \"index.md\"]);\n\nlet cachedSections: SelfDocSection[] | undefined;\n\n/** Drop the cached section index. Tests, and anything that relocates the package root. */\nexport function resetSelfDocSections(): void {\n\tcachedSections = undefined;\n}\n\n/**\n * Every section of every shipped doc.\n *\n * Reads each file once per session and caches; the docs are read-only install\n * content, so there is nothing to invalidate on.\n */\nexport function listSelfDocSections(): SelfDocSection[] {\n\tif (cachedSections) return cachedSections;\n\n\tconst sections: SelfDocSection[] = [];\n\tfor (const doc of listSelfDocs()) {\n\t\tif (SECTION_INDEX_EXCLUDED.has(doc.id)) continue;\n\t\tlet content: string;\n\t\ttry {\n\t\t\tcontent = readFileSync(doc.path, \"utf-8\");\n\t\t} catch {\n\t\t\tcontinue;\n\t\t}\n\t\tsections.push(...splitIntoSections(content, doc.id, doc.path));\n\t}\n\n\tcachedSections = sections;\n\treturn sections;\n}\n"]}
1
+ {"version":3,"file":"self-docs.d.ts","sourceRoot":"","sources":["../../src/core/self-docs.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAMH,MAAM,WAAW,OAAO;IACvB,kFAAkF;IAClF,EAAE,EAAE,MAAM,CAAC;IACX,8DAA8D;IAC9D,IAAI,EAAE,MAAM,CAAC;IACb,kCAAkC;IAClC,KAAK,EAAE,MAAM,CAAC;IACd,iFAAiF;IACjF,WAAW,EAAE,MAAM,CAAC;CACpB;AA8GD,oFAAoF;AACpF,wBAAgB,aAAa,IAAI,IAAI,CAGpC;AAED;;;;;;GAMG;AACH,wBAAgB,YAAY,IAAI,OAAO,EAAE,CA6CxC;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,uBAAuB,CAAC,IAAI,GAAE,SAAS,OAAO,EAAmB,GAAG,MAAM,CAsBzF;AAMD;;;;;;;;;;GAUG;AACH,MAAM,WAAW,cAAc;IAC9B,iDAAiD;IACjD,EAAE,EAAE,MAAM,CAAC;IACX,sCAAsC;IACtC,IAAI,EAAE,MAAM,CAAC;IACb,iCAAiC;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,yFAAyF;IACzF,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,wEAAwE;IACxE,IAAI,EAAE,MAAM,CAAC;IACb,gFAAgF;IAChF,OAAO,EAAE,MAAM,CAAC;CAChB;AAuBD,gGAA2F;AAC3F,wBAAgB,YAAY,CAAC,OAAO,EAAE,cAAc,GAAG,MAAM,CAE5D;AAED;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,cAAc,EAAE,CAmDhG;AAsBD,0FAA0F;AAC1F,wBAAgB,oBAAoB,IAAI,IAAI,CAE3C;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,IAAI,cAAc,EAAE,CAiBtD","sourcesContent":["/**\n * The agent's index of hoocode's *own* documentation.\n *\n * The startup banner promises \"hoocode can explain its own features and look up\n * its docs\", and the docs really do ship with the install (`package.json`\n * `files` includes `docs`, and `copy-binary-assets` copies them into `dist/` for\n * the pkg binaries). What was missing is the only part that makes the promise\n * true: telling the model they exist. `getDocsPath()` had exactly one consumer —\n * `auth-guidance.ts`, which prints paths to the *human* — so nothing ever put a\n * docs path into model context.\n *\n * That gap is not one the model can close by itself. Its cwd is the user's\n * project, so `SearchCodebase` there discovers the user's docs, never hoocode's,\n * which live in an install directory whose path it cannot derive.\n *\n * Descriptions come from `docs/index.md` rather than being duplicated here.\n * That file is a curated, human-maintained table of contents, and a second\n * hand-written list is how an index goes stale the first week nobody updates\n * it. The directory listing stays the source of truth for *what exists*, so a\n * new doc still shows up (described from its own first paragraph) on the day it\n * lands, with or without an index entry.\n */\n\nimport { existsSync, readdirSync, readFileSync, statSync } from \"node:fs\";\nimport { basename, dirname, join } from \"node:path\";\nimport { getChangelogPath, getDocsPath, getReadmePath } from \"../config.js\";\n\nexport interface SelfDoc {\n\t/** Stable id: the filename, e.g. `skills.md`. Also how the model refers to it. */\n\tid: string;\n\t/** Absolute path, ready to hand to the read tool verbatim. */\n\tpath: string;\n\t/** Human title, e.g. \"Skills\". */\n\ttitle: string;\n\t/** One line on what the doc covers. May be empty if nothing could be derived. */\n\tdescription: string;\n}\n\n/** How much of a doc to read when deriving a fallback description. */\nconst HEAD_BYTES = 2048;\n\n/** Cap on a derived description, so one run-on opening line cannot bloat the prompt. */\nconst MAX_DESCRIPTION = 110;\n\nfunction truncate(text: string, max = MAX_DESCRIPTION): string {\n\tconst clean = text.replace(/\\s+/g, \" \").trim();\n\tif (clean.length <= max) return clean;\n\treturn `${clean.slice(0, max - 1).trimEnd()}…`;\n}\n\n/** Strip inline markdown that adds noise but no meaning in a prompt listing. */\nfunction stripInlineMarkdown(text: string): string {\n\treturn text\n\t\t.replace(/\\[([^\\]]+)\\]\\([^)]*\\)/g, \"$1\") // links → their text\n\t\t.replace(/[`*_]/g, \"\")\n\t\t.trim();\n}\n\nfunction readHead(path: string): string {\n\ttry {\n\t\t// Whole-file read: these are small, and slicing bytes off a UTF-8 file can\n\t\t// split a multi-byte character. Truncate after decoding instead.\n\t\treturn readFileSync(path, \"utf-8\").slice(0, HEAD_BYTES);\n\t} catch {\n\t\treturn \"\";\n\t}\n}\n\n/** First `# ` heading, or undefined. */\nfunction firstHeading(markdown: string): string | undefined {\n\tfor (const line of markdown.split(/\\r?\\n/)) {\n\t\tconst match = /^#\\s+(.+)$/.exec(line.trim());\n\t\tif (match?.[1]) return stripInlineMarkdown(match[1]);\n\t}\n\treturn undefined;\n}\n\n/**\n * First real prose line: not a heading, blockquote, list item, fence, or table\n * row. Used only for docs the curated index does not describe.\n */\nfunction firstParagraph(markdown: string): string | undefined {\n\tlet inFence = false;\n\tfor (const raw of markdown.split(/\\r?\\n/)) {\n\t\tconst line = raw.trim();\n\t\tif (line.startsWith(\"```\")) {\n\t\t\tinFence = !inFence;\n\t\t\tcontinue;\n\t\t}\n\t\tif (inFence || line === \"\") continue;\n\t\tif (/^[#>|-]/.test(line) || /^\\d+\\./.test(line)) continue;\n\t\treturn stripInlineMarkdown(line);\n\t}\n\treturn undefined;\n}\n\n/**\n * Titles and descriptions the docs maintain about themselves, keyed by filename.\n *\n * Matches list entries of the form `- [Title](file.md) - description`, which is\n * how every section of `index.md` is written. Anything that does not match is\n * skipped rather than guessed at.\n */\nfunction parseCuratedIndex(docsRoot: string): Map<string, { title: string; description: string }> {\n\tconst curated = new Map<string, { title: string; description: string }>();\n\tconst indexPath = join(docsRoot, \"index.md\");\n\tif (!existsSync(indexPath)) return curated;\n\n\tlet content: string;\n\ttry {\n\t\tcontent = readFileSync(indexPath, \"utf-8\");\n\t} catch {\n\t\treturn curated;\n\t}\n\n\t// `[Title](file.md)` followed by a dash of any width and the description.\n\tconst entry = /^\\s*[-*]\\s*\\[([^\\]]+)\\]\\(([^)#]+\\.md)\\)\\s*[-–—:]\\s*(.+?)\\s*$/;\n\tfor (const line of content.split(/\\r?\\n/)) {\n\t\tconst match = entry.exec(line);\n\t\tif (!match) continue;\n\t\tconst [, title, target, description] = match;\n\t\tconst file = basename(target);\n\t\tif (curated.has(file)) continue; // first mention wins\n\t\tcurated.set(file, { title: stripInlineMarkdown(title), description: truncate(stripInlineMarkdown(description)) });\n\t}\n\treturn curated;\n}\n\nfunction describe(path: string, file: string, curated: Map<string, { title: string; description: string }>): SelfDoc {\n\tconst fromIndex = curated.get(file);\n\tif (fromIndex) {\n\t\treturn { id: file, path, title: fromIndex.title, description: fromIndex.description };\n\t}\n\t// Not in the curated index — derive from the doc itself so new files are\n\t// still usable the day they land.\n\tconst head = readHead(path);\n\treturn {\n\t\tid: file,\n\t\tpath,\n\t\ttitle: firstHeading(head) ?? file.replace(/\\.md$/, \"\"),\n\t\tdescription: truncate(firstParagraph(head) ?? \"\"),\n\t};\n}\n\nlet cached: SelfDoc[] | undefined;\n\n/** Drop the cached listing. Tests, and anything that relocates the package root. */\nexport function resetSelfDocs(): void {\n\tcached = undefined;\n\tcachedSections = undefined;\n}\n\n/**\n * Every shipped doc, sorted with the overview first and the rest alphabetical.\n *\n * Returns `[]` when the docs directory is absent rather than throwing: a source\n * checkout, an odd packaging, or a trimmed container should degrade to \"no docs\n * section in the prompt\", never to a failed session start.\n */\nexport function listSelfDocs(): SelfDoc[] {\n\tif (cached) return cached;\n\n\tconst docsRoot = getDocsPath();\n\tconst docs: SelfDoc[] = [];\n\n\tif (existsSync(docsRoot)) {\n\t\tconst curated = parseCuratedIndex(docsRoot);\n\t\tlet files: string[];\n\t\ttry {\n\t\t\tfiles = readdirSync(docsRoot).filter((f) => f.endsWith(\".md\"));\n\t\t} catch {\n\t\t\tfiles = [];\n\t\t}\n\t\t// Overview first: it is the doc that explains the others.\n\t\tfiles.sort((a, b) => (a === \"index.md\" ? -1 : b === \"index.md\" ? 1 : a.localeCompare(b)));\n\t\tfor (const file of files) {\n\t\t\tconst path = join(docsRoot, file);\n\t\t\ttry {\n\t\t\t\tif (!statSync(path).isFile()) continue;\n\t\t\t} catch {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tdocs.push(describe(path, file, curated));\n\t\t}\n\t}\n\n\t// README and CHANGELOG sit beside the docs directory, not inside it, but the\n\t// model needs them for the two questions the docs do not answer: what\n\t// hoocode is, and what changed in this version.\n\tconst extras: Array<{ path: string; title: string; description: string }> = [\n\t\t{ path: getReadmePath(), title: \"README\", description: \"What hoocode is, install, and a feature overview.\" },\n\t\t{\n\t\t\tpath: getChangelogPath(),\n\t\t\ttitle: \"Changelog\",\n\t\t\tdescription: \"Released versions and what changed in each.\",\n\t\t},\n\t];\n\tfor (const extra of extras) {\n\t\tif (!existsSync(extra.path)) continue;\n\t\tdocs.push({ id: basename(extra.path), path: extra.path, title: extra.title, description: extra.description });\n\t}\n\n\tcached = docs;\n\treturn docs;\n}\n\n/**\n * The system-prompt section, or `\"\"` when there is nothing to point at.\n *\n * Deliberately just filenames. An earlier version carried a one-line summary\n * per doc and cost ~860 tokens on every single turn, which is a poor trade for\n * something most turns never use — and it stopped being necessary once\n * SearchHooCode could retrieve at the heading level. Filenames alone still let\n * the model go straight to `themes.md` or `keybindings.md` for the obvious\n * cases, and anything less obvious is one search away. That is ~180 tokens.\n *\n * Directories are printed once rather than repeated per entry, for the same\n * reason: the path was the single largest term on every line.\n */\nexport function formatSelfDocsForPrompt(docs: readonly SelfDoc[] = listSelfDocs()): string {\n\tif (docs.length === 0) return \"\";\n\n\t// Insertion order is already meaningful (overview first, then alphabetical,\n\t// then README/CHANGELOG), so group without re-sorting.\n\tconst groups = new Map<string, string[]>();\n\tfor (const doc of docs) {\n\t\tconst root = dirname(doc.path);\n\t\tconst bucket = groups.get(root);\n\t\tif (bucket) bucket.push(doc.id);\n\t\telse groups.set(root, [doc.id]);\n\t}\n\n\tconst sections = [...groups].map(([root, files]) => `${root}/: ${files.join(\", \")}`);\n\n\treturn `\n\n# About hoocode itself\n\nYou are running inside hoocode. Its own docs ship with the install, listed below; hoocode is actively developed, so answer questions about it from these files rather than from memory. They sit outside the working directory, so searching the project will not find them. Use SearchHooCode to locate a specific heading, or read a file directly.\n\n${sections.join(\"\\n\")}`;\n}\n\n// ---------------------------------------------------------------------------\n// Section index\n// ---------------------------------------------------------------------------\n\n/**\n * A single heading's worth of a doc.\n *\n * Doc-level retrieval would add nothing the prompt listing above does not\n * already give: thirty files with a summary each are cheap enough to list in\n * full, so a search that answers \"read extensions.md\" is a round trip for\n * information the model already had. The questions that actually need\n * retrieval are the ones inside a 1,100-line file — \"how do I register a\n * tool?\" should land on `extensions.md § Custom tools` with a line number, not\n * on the file.\n */\nexport interface SelfDocSection {\n\t/** `<file>#<slug>`, unique across the corpus. */\n\tid: string;\n\t/** Filename, e.g. `extensions.md`. */\n\tfile: string;\n\t/** Absolute path to the file. */\n\tpath: string;\n\t/** Heading trail from the document title down, e.g. `[\"Extensions\", \"Custom tools\"]`. */\n\theadings: string[];\n\t/** 1-based line of the heading, so a reader can jump straight to it. */\n\tline: number;\n\t/** Start of the section body, for ranking and for showing why a hit matched. */\n\texcerpt: string;\n}\n\n/**\n * How much section body to keep.\n *\n * Every character past this is invisible to retrieval, so the cap is a recall\n * limit, not just a size one: at 240 a question about `/grill` missed the\n * section that documents it, because the term sat in the fourth sentence. 400\n * covers the opening of essentially every section here for about 95KB more\n * index across the corpus, which buys back that class of miss.\n */\nconst MAX_EXCERPT = 400;\n\n/** `Custom tools` → `custom-tools`, so ids stay stable and readable. */\nfunction slugify(heading: string): string {\n\treturn (\n\t\theading\n\t\t\t.toLowerCase()\n\t\t\t.replace(/[^a-z0-9]+/g, \"-\")\n\t\t\t.replace(/^-+|-+$/g, \"\") || \"section\"\n\t);\n}\n\n/** `extensions.md § Extensions › Custom tools` — what a search result is labelled with. */\nexport function sectionLabel(section: SelfDocSection): string {\n\treturn section.headings.length > 0 ? `${section.file} § ${section.headings.join(\" › \")}` : section.file;\n}\n\n/**\n * Split one markdown file into sections at its headings.\n *\n * Fenced code is tracked so a `#` comment inside a bash block cannot be\n * mistaken for a heading — which would otherwise split docs at every shell\n * comment. Code *content* still lands in the excerpt: the exact identifiers\n * someone searches for (`pi.registerTool`) usually live in the examples, and\n * dropping them would blind the lexical leg to the best terms in the file.\n */\nexport function splitIntoSections(markdown: string, file: string, path: string): SelfDocSection[] {\n\tconst lines = markdown.split(/\\r?\\n/);\n\tconst sections: SelfDocSection[] = [];\n\tconst trail: Array<{ depth: number; text: string }> = [];\n\tconst usedIds = new Set<string>();\n\n\tlet current: SelfDocSection | undefined;\n\tlet body: string[] = [];\n\tlet inFence = false;\n\n\tconst flush = (): void => {\n\t\tif (!current) return;\n\t\tcurrent.excerpt = truncate(stripInlineMarkdown(body.join(\" \")), MAX_EXCERPT);\n\t\tsections.push(current);\n\t\tbody = [];\n\t};\n\n\tfor (let i = 0; i < lines.length; i++) {\n\t\tconst raw = lines[i] ?? \"\";\n\t\tif (raw.trimStart().startsWith(\"```\")) {\n\t\t\tinFence = !inFence;\n\t\t\tcontinue;\n\t\t}\n\t\tconst heading = inFence ? null : /^(#{1,6})\\s+(.+?)\\s*$/.exec(raw);\n\t\tif (!heading) {\n\t\t\tif (raw.trim() !== \"\") body.push(raw.trim());\n\t\t\tcontinue;\n\t\t}\n\n\t\tflush();\n\n\t\tconst depth = heading[1]?.length ?? 1;\n\t\tconst text = stripInlineMarkdown(heading[2] ?? \"\");\n\t\twhile (trail.length > 0 && (trail[trail.length - 1]?.depth ?? 0) >= depth) trail.pop();\n\t\ttrail.push({ depth, text });\n\n\t\t// Disambiguate repeated headings (\"Example\" appears eleven times in\n\t\t// extensions.md) so ids stay unique and the registry does not collapse them.\n\t\tlet id = `${file}#${slugify(trail.map((t) => t.text).join(\"-\"))}`;\n\t\tif (usedIds.has(id)) {\n\t\t\tlet n = 2;\n\t\t\twhile (usedIds.has(`${id}-${n}`)) n++;\n\t\t\tid = `${id}-${n}`;\n\t\t}\n\t\tusedIds.add(id);\n\n\t\tcurrent = { id, file, path, headings: trail.map((t) => t.text), line: i + 1, excerpt: \"\" };\n\t}\n\tflush();\n\n\treturn sections;\n}\n\n/**\n * Files kept out of the section index.\n *\n * The changelog is 40% of the corpus by section count and none of it answers\n * \"how does X work\": it is hundreds of near-identical `Added`/`Fixed`/`Changed`\n * headings under version numbers, which crowd real documentation out of the\n * ranking while matching almost any query about a feature by name.\n *\n * `index.md` is excluded for the mirror-image reason: it is a table of contents,\n * so its \"sections\" are lists of links whose text is every other doc's title and\n * summary. That makes it match any query those docs would match, while carrying\n * none of the content — a guaranteed false attractor that displaces the page it\n * is pointing at.\n *\n * Both stay in the prompt's filename listing, one read away.\n */\nconst SECTION_INDEX_EXCLUDED = new Set([\"CHANGELOG.md\", \"index.md\"]);\n\nlet cachedSections: SelfDocSection[] | undefined;\n\n/** Drop the cached section index. Tests, and anything that relocates the package root. */\nexport function resetSelfDocSections(): void {\n\tcachedSections = undefined;\n}\n\n/**\n * Every section of every shipped doc.\n *\n * Reads each file once per session and caches; the docs are read-only install\n * content, so there is nothing to invalidate on.\n */\nexport function listSelfDocSections(): SelfDocSection[] {\n\tif (cachedSections) return cachedSections;\n\n\tconst sections: SelfDocSection[] = [];\n\tfor (const doc of listSelfDocs()) {\n\t\tif (SECTION_INDEX_EXCLUDED.has(doc.id)) continue;\n\t\tlet content: string;\n\t\ttry {\n\t\t\tcontent = readFileSync(doc.path, \"utf-8\");\n\t\t} catch {\n\t\t\tcontinue;\n\t\t}\n\t\tsections.push(...splitIntoSections(content, doc.id, doc.path));\n\t}\n\n\tcachedSections = sections;\n\treturn sections;\n}\n"]}
@@ -10,7 +10,7 @@
10
10
  * docs path into model context.
11
11
  *
12
12
  * That gap is not one the model can close by itself. Its cwd is the user's
13
- * project, so `search` there discovers the user's docs, never hoocode's,
13
+ * project, so `SearchCodebase` there discovers the user's docs, never hoocode's,
14
14
  * which live in an install directory whose path it cannot derive.
15
15
  *
16
16
  * Descriptions come from `docs/index.md` rather than being duplicated here.
@@ -1 +1 @@
1
- {"version":3,"file":"self-docs.js","sourceRoot":"","sources":["../../src/core/self-docs.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The agent's index of hoocode's *own* documentation.\n *\n * The startup banner promises \"hoocode can explain its own features and look up\n * its docs\", and the docs really do ship with the install (`package.json`\n * `files` includes `docs`, and `copy-binary-assets` copies them into `dist/` for\n * the pkg binaries). What was missing is the only part that makes the promise\n * true: telling the model they exist. `getDocsPath()` had exactly one consumer —\n * `auth-guidance.ts`, which prints paths to the *human* — so nothing ever put a\n * docs path into model context.\n *\n * That gap is not one the model can close by itself. Its cwd is the user's\n * project, so `search` there discovers the user's docs, never hoocode's,\n * which live in an install directory whose path it cannot derive.\n *\n * Descriptions come from `docs/index.md` rather than being duplicated here.\n * That file is a curated, human-maintained table of contents, and a second\n * hand-written list is how an index goes stale the first week nobody updates\n * it. The directory listing stays the source of truth for *what exists*, so a\n * new doc still shows up (described from its own first paragraph) on the day it\n * lands, with or without an index entry.\n */\n\nimport { existsSync, readdirSync, readFileSync, statSync } from \"node:fs\";\nimport { basename, dirname, join } from \"node:path\";\nimport { getChangelogPath, getDocsPath, getReadmePath } from \"../config.js\";\n\nexport interface SelfDoc {\n\t/** Stable id: the filename, e.g. `skills.md`. Also how the model refers to it. */\n\tid: string;\n\t/** Absolute path, ready to hand to the read tool verbatim. */\n\tpath: string;\n\t/** Human title, e.g. \"Skills\". */\n\ttitle: string;\n\t/** One line on what the doc covers. May be empty if nothing could be derived. */\n\tdescription: string;\n}\n\n/** How much of a doc to read when deriving a fallback description. */\nconst HEAD_BYTES = 2048;\n\n/** Cap on a derived description, so one run-on opening line cannot bloat the prompt. */\nconst MAX_DESCRIPTION = 110;\n\nfunction truncate(text: string, max = MAX_DESCRIPTION): string {\n\tconst clean = text.replace(/\\s+/g, \" \").trim();\n\tif (clean.length <= max) return clean;\n\treturn `${clean.slice(0, max - 1).trimEnd()}…`;\n}\n\n/** Strip inline markdown that adds noise but no meaning in a prompt listing. */\nfunction stripInlineMarkdown(text: string): string {\n\treturn text\n\t\t.replace(/\\[([^\\]]+)\\]\\([^)]*\\)/g, \"$1\") // links → their text\n\t\t.replace(/[`*_]/g, \"\")\n\t\t.trim();\n}\n\nfunction readHead(path: string): string {\n\ttry {\n\t\t// Whole-file read: these are small, and slicing bytes off a UTF-8 file can\n\t\t// split a multi-byte character. Truncate after decoding instead.\n\t\treturn readFileSync(path, \"utf-8\").slice(0, HEAD_BYTES);\n\t} catch {\n\t\treturn \"\";\n\t}\n}\n\n/** First `# ` heading, or undefined. */\nfunction firstHeading(markdown: string): string | undefined {\n\tfor (const line of markdown.split(/\\r?\\n/)) {\n\t\tconst match = /^#\\s+(.+)$/.exec(line.trim());\n\t\tif (match?.[1]) return stripInlineMarkdown(match[1]);\n\t}\n\treturn undefined;\n}\n\n/**\n * First real prose line: not a heading, blockquote, list item, fence, or table\n * row. Used only for docs the curated index does not describe.\n */\nfunction firstParagraph(markdown: string): string | undefined {\n\tlet inFence = false;\n\tfor (const raw of markdown.split(/\\r?\\n/)) {\n\t\tconst line = raw.trim();\n\t\tif (line.startsWith(\"```\")) {\n\t\t\tinFence = !inFence;\n\t\t\tcontinue;\n\t\t}\n\t\tif (inFence || line === \"\") continue;\n\t\tif (/^[#>|-]/.test(line) || /^\\d+\\./.test(line)) continue;\n\t\treturn stripInlineMarkdown(line);\n\t}\n\treturn undefined;\n}\n\n/**\n * Titles and descriptions the docs maintain about themselves, keyed by filename.\n *\n * Matches list entries of the form `- [Title](file.md) - description`, which is\n * how every section of `index.md` is written. Anything that does not match is\n * skipped rather than guessed at.\n */\nfunction parseCuratedIndex(docsRoot: string): Map<string, { title: string; description: string }> {\n\tconst curated = new Map<string, { title: string; description: string }>();\n\tconst indexPath = join(docsRoot, \"index.md\");\n\tif (!existsSync(indexPath)) return curated;\n\n\tlet content: string;\n\ttry {\n\t\tcontent = readFileSync(indexPath, \"utf-8\");\n\t} catch {\n\t\treturn curated;\n\t}\n\n\t// `[Title](file.md)` followed by a dash of any width and the description.\n\tconst entry = /^\\s*[-*]\\s*\\[([^\\]]+)\\]\\(([^)#]+\\.md)\\)\\s*[-–—:]\\s*(.+?)\\s*$/;\n\tfor (const line of content.split(/\\r?\\n/)) {\n\t\tconst match = entry.exec(line);\n\t\tif (!match) continue;\n\t\tconst [, title, target, description] = match;\n\t\tconst file = basename(target);\n\t\tif (curated.has(file)) continue; // first mention wins\n\t\tcurated.set(file, { title: stripInlineMarkdown(title), description: truncate(stripInlineMarkdown(description)) });\n\t}\n\treturn curated;\n}\n\nfunction describe(path: string, file: string, curated: Map<string, { title: string; description: string }>): SelfDoc {\n\tconst fromIndex = curated.get(file);\n\tif (fromIndex) {\n\t\treturn { id: file, path, title: fromIndex.title, description: fromIndex.description };\n\t}\n\t// Not in the curated index — derive from the doc itself so new files are\n\t// still usable the day they land.\n\tconst head = readHead(path);\n\treturn {\n\t\tid: file,\n\t\tpath,\n\t\ttitle: firstHeading(head) ?? file.replace(/\\.md$/, \"\"),\n\t\tdescription: truncate(firstParagraph(head) ?? \"\"),\n\t};\n}\n\nlet cached: SelfDoc[] | undefined;\n\n/** Drop the cached listing. Tests, and anything that relocates the package root. */\nexport function resetSelfDocs(): void {\n\tcached = undefined;\n\tcachedSections = undefined;\n}\n\n/**\n * Every shipped doc, sorted with the overview first and the rest alphabetical.\n *\n * Returns `[]` when the docs directory is absent rather than throwing: a source\n * checkout, an odd packaging, or a trimmed container should degrade to \"no docs\n * section in the prompt\", never to a failed session start.\n */\nexport function listSelfDocs(): SelfDoc[] {\n\tif (cached) return cached;\n\n\tconst docsRoot = getDocsPath();\n\tconst docs: SelfDoc[] = [];\n\n\tif (existsSync(docsRoot)) {\n\t\tconst curated = parseCuratedIndex(docsRoot);\n\t\tlet files: string[];\n\t\ttry {\n\t\t\tfiles = readdirSync(docsRoot).filter((f) => f.endsWith(\".md\"));\n\t\t} catch {\n\t\t\tfiles = [];\n\t\t}\n\t\t// Overview first: it is the doc that explains the others.\n\t\tfiles.sort((a, b) => (a === \"index.md\" ? -1 : b === \"index.md\" ? 1 : a.localeCompare(b)));\n\t\tfor (const file of files) {\n\t\t\tconst path = join(docsRoot, file);\n\t\t\ttry {\n\t\t\t\tif (!statSync(path).isFile()) continue;\n\t\t\t} catch {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tdocs.push(describe(path, file, curated));\n\t\t}\n\t}\n\n\t// README and CHANGELOG sit beside the docs directory, not inside it, but the\n\t// model needs them for the two questions the docs do not answer: what\n\t// hoocode is, and what changed in this version.\n\tconst extras: Array<{ path: string; title: string; description: string }> = [\n\t\t{ path: getReadmePath(), title: \"README\", description: \"What hoocode is, install, and a feature overview.\" },\n\t\t{\n\t\t\tpath: getChangelogPath(),\n\t\t\ttitle: \"Changelog\",\n\t\t\tdescription: \"Released versions and what changed in each.\",\n\t\t},\n\t];\n\tfor (const extra of extras) {\n\t\tif (!existsSync(extra.path)) continue;\n\t\tdocs.push({ id: basename(extra.path), path: extra.path, title: extra.title, description: extra.description });\n\t}\n\n\tcached = docs;\n\treturn docs;\n}\n\n/**\n * The system-prompt section, or `\"\"` when there is nothing to point at.\n *\n * Deliberately just filenames. An earlier version carried a one-line summary\n * per doc and cost ~860 tokens on every single turn, which is a poor trade for\n * something most turns never use — and it stopped being necessary once\n * SearchHooCode could retrieve at the heading level. Filenames alone still let\n * the model go straight to `themes.md` or `keybindings.md` for the obvious\n * cases, and anything less obvious is one search away. That is ~180 tokens.\n *\n * Directories are printed once rather than repeated per entry, for the same\n * reason: the path was the single largest term on every line.\n */\nexport function formatSelfDocsForPrompt(docs: readonly SelfDoc[] = listSelfDocs()): string {\n\tif (docs.length === 0) return \"\";\n\n\t// Insertion order is already meaningful (overview first, then alphabetical,\n\t// then README/CHANGELOG), so group without re-sorting.\n\tconst groups = new Map<string, string[]>();\n\tfor (const doc of docs) {\n\t\tconst root = dirname(doc.path);\n\t\tconst bucket = groups.get(root);\n\t\tif (bucket) bucket.push(doc.id);\n\t\telse groups.set(root, [doc.id]);\n\t}\n\n\tconst sections = [...groups].map(([root, files]) => `${root}/: ${files.join(\", \")}`);\n\n\treturn `\n\n# About hoocode itself\n\nYou are running inside hoocode. Its own docs ship with the install, listed below; hoocode is actively developed, so answer questions about it from these files rather than from memory. They sit outside the working directory, so searching the project will not find them. Use SearchHooCode to locate a specific heading, or read a file directly.\n\n${sections.join(\"\\n\")}`;\n}\n\n// ---------------------------------------------------------------------------\n// Section index\n// ---------------------------------------------------------------------------\n\n/**\n * A single heading's worth of a doc.\n *\n * Doc-level retrieval would add nothing the prompt listing above does not\n * already give: thirty files with a summary each are cheap enough to list in\n * full, so a search that answers \"read extensions.md\" is a round trip for\n * information the model already had. The questions that actually need\n * retrieval are the ones inside a 1,100-line file — \"how do I register a\n * tool?\" should land on `extensions.md § Custom tools` with a line number, not\n * on the file.\n */\nexport interface SelfDocSection {\n\t/** `<file>#<slug>`, unique across the corpus. */\n\tid: string;\n\t/** Filename, e.g. `extensions.md`. */\n\tfile: string;\n\t/** Absolute path to the file. */\n\tpath: string;\n\t/** Heading trail from the document title down, e.g. `[\"Extensions\", \"Custom tools\"]`. */\n\theadings: string[];\n\t/** 1-based line of the heading, so a reader can jump straight to it. */\n\tline: number;\n\t/** Start of the section body, for ranking and for showing why a hit matched. */\n\texcerpt: string;\n}\n\n/**\n * How much section body to keep.\n *\n * Every character past this is invisible to retrieval, so the cap is a recall\n * limit, not just a size one: at 240 a question about `/grill` missed the\n * section that documents it, because the term sat in the fourth sentence. 400\n * covers the opening of essentially every section here for about 95KB more\n * index across the corpus, which buys back that class of miss.\n */\nconst MAX_EXCERPT = 400;\n\n/** `Custom tools` → `custom-tools`, so ids stay stable and readable. */\nfunction slugify(heading: string): string {\n\treturn (\n\t\theading\n\t\t\t.toLowerCase()\n\t\t\t.replace(/[^a-z0-9]+/g, \"-\")\n\t\t\t.replace(/^-+|-+$/g, \"\") || \"section\"\n\t);\n}\n\n/** `extensions.md § Extensions › Custom tools` — what a search result is labelled with. */\nexport function sectionLabel(section: SelfDocSection): string {\n\treturn section.headings.length > 0 ? `${section.file} § ${section.headings.join(\" › \")}` : section.file;\n}\n\n/**\n * Split one markdown file into sections at its headings.\n *\n * Fenced code is tracked so a `#` comment inside a bash block cannot be\n * mistaken for a heading — which would otherwise split docs at every shell\n * comment. Code *content* still lands in the excerpt: the exact identifiers\n * someone searches for (`pi.registerTool`) usually live in the examples, and\n * dropping them would blind the lexical leg to the best terms in the file.\n */\nexport function splitIntoSections(markdown: string, file: string, path: string): SelfDocSection[] {\n\tconst lines = markdown.split(/\\r?\\n/);\n\tconst sections: SelfDocSection[] = [];\n\tconst trail: Array<{ depth: number; text: string }> = [];\n\tconst usedIds = new Set<string>();\n\n\tlet current: SelfDocSection | undefined;\n\tlet body: string[] = [];\n\tlet inFence = false;\n\n\tconst flush = (): void => {\n\t\tif (!current) return;\n\t\tcurrent.excerpt = truncate(stripInlineMarkdown(body.join(\" \")), MAX_EXCERPT);\n\t\tsections.push(current);\n\t\tbody = [];\n\t};\n\n\tfor (let i = 0; i < lines.length; i++) {\n\t\tconst raw = lines[i] ?? \"\";\n\t\tif (raw.trimStart().startsWith(\"```\")) {\n\t\t\tinFence = !inFence;\n\t\t\tcontinue;\n\t\t}\n\t\tconst heading = inFence ? null : /^(#{1,6})\\s+(.+?)\\s*$/.exec(raw);\n\t\tif (!heading) {\n\t\t\tif (raw.trim() !== \"\") body.push(raw.trim());\n\t\t\tcontinue;\n\t\t}\n\n\t\tflush();\n\n\t\tconst depth = heading[1]?.length ?? 1;\n\t\tconst text = stripInlineMarkdown(heading[2] ?? \"\");\n\t\twhile (trail.length > 0 && (trail[trail.length - 1]?.depth ?? 0) >= depth) trail.pop();\n\t\ttrail.push({ depth, text });\n\n\t\t// Disambiguate repeated headings (\"Example\" appears eleven times in\n\t\t// extensions.md) so ids stay unique and the registry does not collapse them.\n\t\tlet id = `${file}#${slugify(trail.map((t) => t.text).join(\"-\"))}`;\n\t\tif (usedIds.has(id)) {\n\t\t\tlet n = 2;\n\t\t\twhile (usedIds.has(`${id}-${n}`)) n++;\n\t\t\tid = `${id}-${n}`;\n\t\t}\n\t\tusedIds.add(id);\n\n\t\tcurrent = { id, file, path, headings: trail.map((t) => t.text), line: i + 1, excerpt: \"\" };\n\t}\n\tflush();\n\n\treturn sections;\n}\n\n/**\n * Files kept out of the section index.\n *\n * The changelog is 40% of the corpus by section count and none of it answers\n * \"how does X work\": it is hundreds of near-identical `Added`/`Fixed`/`Changed`\n * headings under version numbers, which crowd real documentation out of the\n * ranking while matching almost any query about a feature by name.\n *\n * `index.md` is excluded for the mirror-image reason: it is a table of contents,\n * so its \"sections\" are lists of links whose text is every other doc's title and\n * summary. That makes it match any query those docs would match, while carrying\n * none of the content — a guaranteed false attractor that displaces the page it\n * is pointing at.\n *\n * Both stay in the prompt's filename listing, one read away.\n */\nconst SECTION_INDEX_EXCLUDED = new Set([\"CHANGELOG.md\", \"index.md\"]);\n\nlet cachedSections: SelfDocSection[] | undefined;\n\n/** Drop the cached section index. Tests, and anything that relocates the package root. */\nexport function resetSelfDocSections(): void {\n\tcachedSections = undefined;\n}\n\n/**\n * Every section of every shipped doc.\n *\n * Reads each file once per session and caches; the docs are read-only install\n * content, so there is nothing to invalidate on.\n */\nexport function listSelfDocSections(): SelfDocSection[] {\n\tif (cachedSections) return cachedSections;\n\n\tconst sections: SelfDocSection[] = [];\n\tfor (const doc of listSelfDocs()) {\n\t\tif (SECTION_INDEX_EXCLUDED.has(doc.id)) continue;\n\t\tlet content: string;\n\t\ttry {\n\t\t\tcontent = readFileSync(doc.path, \"utf-8\");\n\t\t} catch {\n\t\t\tcontinue;\n\t\t}\n\t\tsections.push(...splitIntoSections(content, doc.id, doc.path));\n\t}\n\n\tcachedSections = sections;\n\treturn sections;\n}\n"]}
1
+ {"version":3,"file":"self-docs.js","sourceRoot":"","sources":["../../src/core/self-docs.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The agent's index of hoocode's *own* documentation.\n *\n * The startup banner promises \"hoocode can explain its own features and look up\n * its docs\", and the docs really do ship with the install (`package.json`\n * `files` includes `docs`, and `copy-binary-assets` copies them into `dist/` for\n * the pkg binaries). What was missing is the only part that makes the promise\n * true: telling the model they exist. `getDocsPath()` had exactly one consumer —\n * `auth-guidance.ts`, which prints paths to the *human* — so nothing ever put a\n * docs path into model context.\n *\n * That gap is not one the model can close by itself. Its cwd is the user's\n * project, so `SearchCodebase` there discovers the user's docs, never hoocode's,\n * which live in an install directory whose path it cannot derive.\n *\n * Descriptions come from `docs/index.md` rather than being duplicated here.\n * That file is a curated, human-maintained table of contents, and a second\n * hand-written list is how an index goes stale the first week nobody updates\n * it. The directory listing stays the source of truth for *what exists*, so a\n * new doc still shows up (described from its own first paragraph) on the day it\n * lands, with or without an index entry.\n */\n\nimport { existsSync, readdirSync, readFileSync, statSync } from \"node:fs\";\nimport { basename, dirname, join } from \"node:path\";\nimport { getChangelogPath, getDocsPath, getReadmePath } from \"../config.js\";\n\nexport interface SelfDoc {\n\t/** Stable id: the filename, e.g. `skills.md`. Also how the model refers to it. */\n\tid: string;\n\t/** Absolute path, ready to hand to the read tool verbatim. */\n\tpath: string;\n\t/** Human title, e.g. \"Skills\". */\n\ttitle: string;\n\t/** One line on what the doc covers. May be empty if nothing could be derived. */\n\tdescription: string;\n}\n\n/** How much of a doc to read when deriving a fallback description. */\nconst HEAD_BYTES = 2048;\n\n/** Cap on a derived description, so one run-on opening line cannot bloat the prompt. */\nconst MAX_DESCRIPTION = 110;\n\nfunction truncate(text: string, max = MAX_DESCRIPTION): string {\n\tconst clean = text.replace(/\\s+/g, \" \").trim();\n\tif (clean.length <= max) return clean;\n\treturn `${clean.slice(0, max - 1).trimEnd()}…`;\n}\n\n/** Strip inline markdown that adds noise but no meaning in a prompt listing. */\nfunction stripInlineMarkdown(text: string): string {\n\treturn text\n\t\t.replace(/\\[([^\\]]+)\\]\\([^)]*\\)/g, \"$1\") // links → their text\n\t\t.replace(/[`*_]/g, \"\")\n\t\t.trim();\n}\n\nfunction readHead(path: string): string {\n\ttry {\n\t\t// Whole-file read: these are small, and slicing bytes off a UTF-8 file can\n\t\t// split a multi-byte character. Truncate after decoding instead.\n\t\treturn readFileSync(path, \"utf-8\").slice(0, HEAD_BYTES);\n\t} catch {\n\t\treturn \"\";\n\t}\n}\n\n/** First `# ` heading, or undefined. */\nfunction firstHeading(markdown: string): string | undefined {\n\tfor (const line of markdown.split(/\\r?\\n/)) {\n\t\tconst match = /^#\\s+(.+)$/.exec(line.trim());\n\t\tif (match?.[1]) return stripInlineMarkdown(match[1]);\n\t}\n\treturn undefined;\n}\n\n/**\n * First real prose line: not a heading, blockquote, list item, fence, or table\n * row. Used only for docs the curated index does not describe.\n */\nfunction firstParagraph(markdown: string): string | undefined {\n\tlet inFence = false;\n\tfor (const raw of markdown.split(/\\r?\\n/)) {\n\t\tconst line = raw.trim();\n\t\tif (line.startsWith(\"```\")) {\n\t\t\tinFence = !inFence;\n\t\t\tcontinue;\n\t\t}\n\t\tif (inFence || line === \"\") continue;\n\t\tif (/^[#>|-]/.test(line) || /^\\d+\\./.test(line)) continue;\n\t\treturn stripInlineMarkdown(line);\n\t}\n\treturn undefined;\n}\n\n/**\n * Titles and descriptions the docs maintain about themselves, keyed by filename.\n *\n * Matches list entries of the form `- [Title](file.md) - description`, which is\n * how every section of `index.md` is written. Anything that does not match is\n * skipped rather than guessed at.\n */\nfunction parseCuratedIndex(docsRoot: string): Map<string, { title: string; description: string }> {\n\tconst curated = new Map<string, { title: string; description: string }>();\n\tconst indexPath = join(docsRoot, \"index.md\");\n\tif (!existsSync(indexPath)) return curated;\n\n\tlet content: string;\n\ttry {\n\t\tcontent = readFileSync(indexPath, \"utf-8\");\n\t} catch {\n\t\treturn curated;\n\t}\n\n\t// `[Title](file.md)` followed by a dash of any width and the description.\n\tconst entry = /^\\s*[-*]\\s*\\[([^\\]]+)\\]\\(([^)#]+\\.md)\\)\\s*[-–—:]\\s*(.+?)\\s*$/;\n\tfor (const line of content.split(/\\r?\\n/)) {\n\t\tconst match = entry.exec(line);\n\t\tif (!match) continue;\n\t\tconst [, title, target, description] = match;\n\t\tconst file = basename(target);\n\t\tif (curated.has(file)) continue; // first mention wins\n\t\tcurated.set(file, { title: stripInlineMarkdown(title), description: truncate(stripInlineMarkdown(description)) });\n\t}\n\treturn curated;\n}\n\nfunction describe(path: string, file: string, curated: Map<string, { title: string; description: string }>): SelfDoc {\n\tconst fromIndex = curated.get(file);\n\tif (fromIndex) {\n\t\treturn { id: file, path, title: fromIndex.title, description: fromIndex.description };\n\t}\n\t// Not in the curated index — derive from the doc itself so new files are\n\t// still usable the day they land.\n\tconst head = readHead(path);\n\treturn {\n\t\tid: file,\n\t\tpath,\n\t\ttitle: firstHeading(head) ?? file.replace(/\\.md$/, \"\"),\n\t\tdescription: truncate(firstParagraph(head) ?? \"\"),\n\t};\n}\n\nlet cached: SelfDoc[] | undefined;\n\n/** Drop the cached listing. Tests, and anything that relocates the package root. */\nexport function resetSelfDocs(): void {\n\tcached = undefined;\n\tcachedSections = undefined;\n}\n\n/**\n * Every shipped doc, sorted with the overview first and the rest alphabetical.\n *\n * Returns `[]` when the docs directory is absent rather than throwing: a source\n * checkout, an odd packaging, or a trimmed container should degrade to \"no docs\n * section in the prompt\", never to a failed session start.\n */\nexport function listSelfDocs(): SelfDoc[] {\n\tif (cached) return cached;\n\n\tconst docsRoot = getDocsPath();\n\tconst docs: SelfDoc[] = [];\n\n\tif (existsSync(docsRoot)) {\n\t\tconst curated = parseCuratedIndex(docsRoot);\n\t\tlet files: string[];\n\t\ttry {\n\t\t\tfiles = readdirSync(docsRoot).filter((f) => f.endsWith(\".md\"));\n\t\t} catch {\n\t\t\tfiles = [];\n\t\t}\n\t\t// Overview first: it is the doc that explains the others.\n\t\tfiles.sort((a, b) => (a === \"index.md\" ? -1 : b === \"index.md\" ? 1 : a.localeCompare(b)));\n\t\tfor (const file of files) {\n\t\t\tconst path = join(docsRoot, file);\n\t\t\ttry {\n\t\t\t\tif (!statSync(path).isFile()) continue;\n\t\t\t} catch {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tdocs.push(describe(path, file, curated));\n\t\t}\n\t}\n\n\t// README and CHANGELOG sit beside the docs directory, not inside it, but the\n\t// model needs them for the two questions the docs do not answer: what\n\t// hoocode is, and what changed in this version.\n\tconst extras: Array<{ path: string; title: string; description: string }> = [\n\t\t{ path: getReadmePath(), title: \"README\", description: \"What hoocode is, install, and a feature overview.\" },\n\t\t{\n\t\t\tpath: getChangelogPath(),\n\t\t\ttitle: \"Changelog\",\n\t\t\tdescription: \"Released versions and what changed in each.\",\n\t\t},\n\t];\n\tfor (const extra of extras) {\n\t\tif (!existsSync(extra.path)) continue;\n\t\tdocs.push({ id: basename(extra.path), path: extra.path, title: extra.title, description: extra.description });\n\t}\n\n\tcached = docs;\n\treturn docs;\n}\n\n/**\n * The system-prompt section, or `\"\"` when there is nothing to point at.\n *\n * Deliberately just filenames. An earlier version carried a one-line summary\n * per doc and cost ~860 tokens on every single turn, which is a poor trade for\n * something most turns never use — and it stopped being necessary once\n * SearchHooCode could retrieve at the heading level. Filenames alone still let\n * the model go straight to `themes.md` or `keybindings.md` for the obvious\n * cases, and anything less obvious is one search away. That is ~180 tokens.\n *\n * Directories are printed once rather than repeated per entry, for the same\n * reason: the path was the single largest term on every line.\n */\nexport function formatSelfDocsForPrompt(docs: readonly SelfDoc[] = listSelfDocs()): string {\n\tif (docs.length === 0) return \"\";\n\n\t// Insertion order is already meaningful (overview first, then alphabetical,\n\t// then README/CHANGELOG), so group without re-sorting.\n\tconst groups = new Map<string, string[]>();\n\tfor (const doc of docs) {\n\t\tconst root = dirname(doc.path);\n\t\tconst bucket = groups.get(root);\n\t\tif (bucket) bucket.push(doc.id);\n\t\telse groups.set(root, [doc.id]);\n\t}\n\n\tconst sections = [...groups].map(([root, files]) => `${root}/: ${files.join(\", \")}`);\n\n\treturn `\n\n# About hoocode itself\n\nYou are running inside hoocode. Its own docs ship with the install, listed below; hoocode is actively developed, so answer questions about it from these files rather than from memory. They sit outside the working directory, so searching the project will not find them. Use SearchHooCode to locate a specific heading, or read a file directly.\n\n${sections.join(\"\\n\")}`;\n}\n\n// ---------------------------------------------------------------------------\n// Section index\n// ---------------------------------------------------------------------------\n\n/**\n * A single heading's worth of a doc.\n *\n * Doc-level retrieval would add nothing the prompt listing above does not\n * already give: thirty files with a summary each are cheap enough to list in\n * full, so a search that answers \"read extensions.md\" is a round trip for\n * information the model already had. The questions that actually need\n * retrieval are the ones inside a 1,100-line file — \"how do I register a\n * tool?\" should land on `extensions.md § Custom tools` with a line number, not\n * on the file.\n */\nexport interface SelfDocSection {\n\t/** `<file>#<slug>`, unique across the corpus. */\n\tid: string;\n\t/** Filename, e.g. `extensions.md`. */\n\tfile: string;\n\t/** Absolute path to the file. */\n\tpath: string;\n\t/** Heading trail from the document title down, e.g. `[\"Extensions\", \"Custom tools\"]`. */\n\theadings: string[];\n\t/** 1-based line of the heading, so a reader can jump straight to it. */\n\tline: number;\n\t/** Start of the section body, for ranking and for showing why a hit matched. */\n\texcerpt: string;\n}\n\n/**\n * How much section body to keep.\n *\n * Every character past this is invisible to retrieval, so the cap is a recall\n * limit, not just a size one: at 240 a question about `/grill` missed the\n * section that documents it, because the term sat in the fourth sentence. 400\n * covers the opening of essentially every section here for about 95KB more\n * index across the corpus, which buys back that class of miss.\n */\nconst MAX_EXCERPT = 400;\n\n/** `Custom tools` → `custom-tools`, so ids stay stable and readable. */\nfunction slugify(heading: string): string {\n\treturn (\n\t\theading\n\t\t\t.toLowerCase()\n\t\t\t.replace(/[^a-z0-9]+/g, \"-\")\n\t\t\t.replace(/^-+|-+$/g, \"\") || \"section\"\n\t);\n}\n\n/** `extensions.md § Extensions › Custom tools` — what a search result is labelled with. */\nexport function sectionLabel(section: SelfDocSection): string {\n\treturn section.headings.length > 0 ? `${section.file} § ${section.headings.join(\" › \")}` : section.file;\n}\n\n/**\n * Split one markdown file into sections at its headings.\n *\n * Fenced code is tracked so a `#` comment inside a bash block cannot be\n * mistaken for a heading — which would otherwise split docs at every shell\n * comment. Code *content* still lands in the excerpt: the exact identifiers\n * someone searches for (`pi.registerTool`) usually live in the examples, and\n * dropping them would blind the lexical leg to the best terms in the file.\n */\nexport function splitIntoSections(markdown: string, file: string, path: string): SelfDocSection[] {\n\tconst lines = markdown.split(/\\r?\\n/);\n\tconst sections: SelfDocSection[] = [];\n\tconst trail: Array<{ depth: number; text: string }> = [];\n\tconst usedIds = new Set<string>();\n\n\tlet current: SelfDocSection | undefined;\n\tlet body: string[] = [];\n\tlet inFence = false;\n\n\tconst flush = (): void => {\n\t\tif (!current) return;\n\t\tcurrent.excerpt = truncate(stripInlineMarkdown(body.join(\" \")), MAX_EXCERPT);\n\t\tsections.push(current);\n\t\tbody = [];\n\t};\n\n\tfor (let i = 0; i < lines.length; i++) {\n\t\tconst raw = lines[i] ?? \"\";\n\t\tif (raw.trimStart().startsWith(\"```\")) {\n\t\t\tinFence = !inFence;\n\t\t\tcontinue;\n\t\t}\n\t\tconst heading = inFence ? null : /^(#{1,6})\\s+(.+?)\\s*$/.exec(raw);\n\t\tif (!heading) {\n\t\t\tif (raw.trim() !== \"\") body.push(raw.trim());\n\t\t\tcontinue;\n\t\t}\n\n\t\tflush();\n\n\t\tconst depth = heading[1]?.length ?? 1;\n\t\tconst text = stripInlineMarkdown(heading[2] ?? \"\");\n\t\twhile (trail.length > 0 && (trail[trail.length - 1]?.depth ?? 0) >= depth) trail.pop();\n\t\ttrail.push({ depth, text });\n\n\t\t// Disambiguate repeated headings (\"Example\" appears eleven times in\n\t\t// extensions.md) so ids stay unique and the registry does not collapse them.\n\t\tlet id = `${file}#${slugify(trail.map((t) => t.text).join(\"-\"))}`;\n\t\tif (usedIds.has(id)) {\n\t\t\tlet n = 2;\n\t\t\twhile (usedIds.has(`${id}-${n}`)) n++;\n\t\t\tid = `${id}-${n}`;\n\t\t}\n\t\tusedIds.add(id);\n\n\t\tcurrent = { id, file, path, headings: trail.map((t) => t.text), line: i + 1, excerpt: \"\" };\n\t}\n\tflush();\n\n\treturn sections;\n}\n\n/**\n * Files kept out of the section index.\n *\n * The changelog is 40% of the corpus by section count and none of it answers\n * \"how does X work\": it is hundreds of near-identical `Added`/`Fixed`/`Changed`\n * headings under version numbers, which crowd real documentation out of the\n * ranking while matching almost any query about a feature by name.\n *\n * `index.md` is excluded for the mirror-image reason: it is a table of contents,\n * so its \"sections\" are lists of links whose text is every other doc's title and\n * summary. That makes it match any query those docs would match, while carrying\n * none of the content — a guaranteed false attractor that displaces the page it\n * is pointing at.\n *\n * Both stay in the prompt's filename listing, one read away.\n */\nconst SECTION_INDEX_EXCLUDED = new Set([\"CHANGELOG.md\", \"index.md\"]);\n\nlet cachedSections: SelfDocSection[] | undefined;\n\n/** Drop the cached section index. Tests, and anything that relocates the package root. */\nexport function resetSelfDocSections(): void {\n\tcachedSections = undefined;\n}\n\n/**\n * Every section of every shipped doc.\n *\n * Reads each file once per session and caches; the docs are read-only install\n * content, so there is nothing to invalidate on.\n */\nexport function listSelfDocSections(): SelfDocSection[] {\n\tif (cachedSections) return cachedSections;\n\n\tconst sections: SelfDocSection[] = [];\n\tfor (const doc of listSelfDocs()) {\n\t\tif (SECTION_INDEX_EXCLUDED.has(doc.id)) continue;\n\t\tlet content: string;\n\t\ttry {\n\t\t\tcontent = readFileSync(doc.path, \"utf-8\");\n\t\t} catch {\n\t\t\tcontinue;\n\t\t}\n\t\tsections.push(...splitIntoSections(content, doc.id, doc.path));\n\t}\n\n\tcachedSections = sections;\n\treturn sections;\n}\n"]}