@kolisachint/hoocode-agent 0.5.3 → 0.5.5

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 (178) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/dist/cli/args.d.ts +1 -1
  3. package/dist/cli/args.d.ts.map +1 -1
  4. package/dist/cli/args.js +10 -7
  5. package/dist/cli/args.js.map +1 -1
  6. package/dist/core/agent-session.d.ts.map +1 -1
  7. package/dist/core/agent-session.js +10 -2
  8. package/dist/core/agent-session.js.map +1 -1
  9. package/dist/core/capabilities/deferral.d.ts +78 -0
  10. package/dist/core/capabilities/deferral.d.ts.map +1 -0
  11. package/dist/core/capabilities/deferral.js +99 -0
  12. package/dist/core/capabilities/deferral.js.map +1 -0
  13. package/dist/core/capabilities/dense.d.ts +51 -0
  14. package/dist/core/capabilities/dense.d.ts.map +1 -0
  15. package/dist/core/capabilities/dense.js +121 -0
  16. package/dist/core/capabilities/dense.js.map +1 -0
  17. package/dist/core/capabilities/lexical.d.ts +43 -0
  18. package/dist/core/capabilities/lexical.d.ts.map +1 -0
  19. package/dist/core/capabilities/lexical.js +104 -0
  20. package/dist/core/capabilities/lexical.js.map +1 -0
  21. package/dist/core/capabilities/registry.d.ts +56 -0
  22. package/dist/core/capabilities/registry.d.ts.map +1 -0
  23. package/dist/core/capabilities/registry.js +0 -0
  24. package/dist/core/capabilities/registry.js.map +1 -0
  25. package/dist/core/capabilities/search.d.ts +47 -0
  26. package/dist/core/capabilities/search.d.ts.map +1 -0
  27. package/dist/core/capabilities/search.js +82 -0
  28. package/dist/core/capabilities/search.js.map +1 -0
  29. package/dist/core/extensions/loader.d.ts +27 -3
  30. package/dist/core/extensions/loader.d.ts.map +1 -1
  31. package/dist/core/extensions/loader.js +61 -7
  32. package/dist/core/extensions/loader.js.map +1 -1
  33. package/dist/core/extensions/plugins/authoring.d.ts +56 -9
  34. package/dist/core/extensions/plugins/authoring.d.ts.map +1 -1
  35. package/dist/core/extensions/plugins/authoring.js +0 -0
  36. package/dist/core/extensions/plugins/authoring.js.map +1 -1
  37. package/dist/core/extensions/plugins/drift.d.ts +75 -0
  38. package/dist/core/extensions/plugins/drift.d.ts.map +1 -0
  39. package/dist/core/extensions/plugins/drift.js +194 -0
  40. package/dist/core/extensions/plugins/drift.js.map +1 -0
  41. package/dist/core/extensions/plugins/formats/claude.d.ts.map +1 -1
  42. package/dist/core/extensions/plugins/formats/claude.js +5 -0
  43. package/dist/core/extensions/plugins/formats/claude.js.map +1 -1
  44. package/dist/core/extensions/plugins/formats/copilot.d.ts +6 -0
  45. package/dist/core/extensions/plugins/formats/copilot.d.ts.map +1 -1
  46. package/dist/core/extensions/plugins/formats/copilot.js +57 -16
  47. package/dist/core/extensions/plugins/formats/copilot.js.map +1 -1
  48. package/dist/core/extensions/plugins/formats/index.d.ts +39 -1
  49. package/dist/core/extensions/plugins/formats/index.d.ts.map +1 -1
  50. package/dist/core/extensions/plugins/formats/index.js +90 -13
  51. package/dist/core/extensions/plugins/formats/index.js.map +1 -1
  52. package/dist/core/extensions/plugins/formats/jsonManifest.d.ts +14 -0
  53. package/dist/core/extensions/plugins/formats/jsonManifest.d.ts.map +1 -1
  54. package/dist/core/extensions/plugins/formats/jsonManifest.js +37 -4
  55. package/dist/core/extensions/plugins/formats/jsonManifest.js.map +1 -1
  56. package/dist/core/extensions/plugins/formats/platform-targets.d.ts +48 -24
  57. package/dist/core/extensions/plugins/formats/platform-targets.d.ts.map +1 -1
  58. package/dist/core/extensions/plugins/formats/platform-targets.js +62 -30
  59. package/dist/core/extensions/plugins/formats/platform-targets.js.map +1 -1
  60. package/dist/core/extensions/plugins/formats/shared.d.ts +22 -0
  61. package/dist/core/extensions/plugins/formats/shared.d.ts.map +1 -1
  62. package/dist/core/extensions/plugins/formats/shared.js +81 -4
  63. package/dist/core/extensions/plugins/formats/shared.js.map +1 -1
  64. package/dist/core/extensions/plugins/formats/types.d.ts +10 -1
  65. package/dist/core/extensions/plugins/formats/types.d.ts.map +1 -1
  66. package/dist/core/extensions/plugins/formats/types.js.map +1 -1
  67. package/dist/core/extensions/plugins/gates.d.ts +64 -0
  68. package/dist/core/extensions/plugins/gates.d.ts.map +1 -0
  69. package/dist/core/extensions/plugins/gates.js +297 -0
  70. package/dist/core/extensions/plugins/gates.js.map +1 -0
  71. package/dist/core/extensions/plugins/hooks-bridge.d.ts +1 -1
  72. package/dist/core/extensions/plugins/hooks-bridge.d.ts.map +1 -1
  73. package/dist/core/extensions/plugins/hooks-bridge.js +12 -8
  74. package/dist/core/extensions/plugins/hooks-bridge.js.map +1 -1
  75. package/dist/core/extensions/plugins/index.d.ts +36 -5
  76. package/dist/core/extensions/plugins/index.d.ts.map +1 -1
  77. package/dist/core/extensions/plugins/index.js +66 -15
  78. package/dist/core/extensions/plugins/index.js.map +1 -1
  79. package/dist/core/extensions/plugins/install.d.ts +43 -24
  80. package/dist/core/extensions/plugins/install.d.ts.map +1 -1
  81. package/dist/core/extensions/plugins/install.js +167 -54
  82. package/dist/core/extensions/plugins/install.js.map +1 -1
  83. package/dist/core/extensions/plugins/locations.d.ts +84 -0
  84. package/dist/core/extensions/plugins/locations.d.ts.map +1 -0
  85. package/dist/core/extensions/plugins/locations.js +137 -0
  86. package/dist/core/extensions/plugins/locations.js.map +1 -0
  87. package/dist/core/extensions/plugins/manifest.d.ts +21 -1
  88. package/dist/core/extensions/plugins/manifest.d.ts.map +1 -1
  89. package/dist/core/extensions/plugins/manifest.js +2 -2
  90. package/dist/core/extensions/plugins/manifest.js.map +1 -1
  91. package/dist/core/extensions/plugins/packaging.d.ts +115 -0
  92. package/dist/core/extensions/plugins/packaging.d.ts.map +1 -0
  93. package/dist/core/extensions/plugins/packaging.js +362 -0
  94. package/dist/core/extensions/plugins/packaging.js.map +1 -0
  95. package/dist/core/extensions/plugins/smoke.d.ts +33 -0
  96. package/dist/core/extensions/plugins/smoke.d.ts.map +1 -0
  97. package/dist/core/extensions/plugins/smoke.js +231 -0
  98. package/dist/core/extensions/plugins/smoke.js.map +1 -0
  99. package/dist/core/extensions/plugins/trigger-eval.d.ts +136 -0
  100. package/dist/core/extensions/plugins/trigger-eval.d.ts.map +1 -0
  101. package/dist/core/extensions/plugins/trigger-eval.js +0 -0
  102. package/dist/core/extensions/plugins/trigger-eval.js.map +1 -0
  103. package/dist/core/package-manager.d.ts +6 -0
  104. package/dist/core/package-manager.d.ts.map +1 -1
  105. package/dist/core/package-manager.js.map +1 -1
  106. package/dist/core/resource-loader.d.ts +2 -0
  107. package/dist/core/resource-loader.d.ts.map +1 -1
  108. package/dist/core/resource-loader.js +15 -0
  109. package/dist/core/resource-loader.js.map +1 -1
  110. package/dist/core/settings-manager.d.ts +2 -2
  111. package/dist/core/settings-manager.d.ts.map +1 -1
  112. package/dist/core/settings-manager.js +3 -3
  113. package/dist/core/settings-manager.js.map +1 -1
  114. package/dist/core/settings-types.d.ts +1 -1
  115. package/dist/core/settings-types.d.ts.map +1 -1
  116. package/dist/core/settings-types.js.map +1 -1
  117. package/dist/core/skills.d.ts +9 -0
  118. package/dist/core/skills.d.ts.map +1 -1
  119. package/dist/core/skills.js +23 -3
  120. package/dist/core/skills.js.map +1 -1
  121. package/dist/core/tools/package-plugin.d.ts +19 -0
  122. package/dist/core/tools/package-plugin.d.ts.map +1 -0
  123. package/dist/core/tools/package-plugin.js +81 -0
  124. package/dist/core/tools/package-plugin.js.map +1 -0
  125. package/dist/core/tools/plugin-tool-names.d.ts +1 -0
  126. package/dist/core/tools/plugin-tool-names.d.ts.map +1 -1
  127. package/dist/core/tools/plugin-tool-names.js +6 -0
  128. package/dist/core/tools/plugin-tool-names.js.map +1 -1
  129. package/dist/core/tools/plugins.d.ts.map +1 -1
  130. package/dist/core/tools/plugins.js +48 -7
  131. package/dist/core/tools/plugins.js.map +1 -1
  132. package/dist/core/tools/propose-plugin.d.ts.map +1 -1
  133. package/dist/core/tools/propose-plugin.js +49 -21
  134. package/dist/core/tools/propose-plugin.js.map +1 -1
  135. package/dist/extensions/core/marketplace.d.ts +14 -3
  136. package/dist/extensions/core/marketplace.d.ts.map +1 -1
  137. package/dist/extensions/core/marketplace.js +77 -18
  138. package/dist/extensions/core/marketplace.js.map +1 -1
  139. package/dist/extensions/core/mcp-deferred.d.ts +39 -2
  140. package/dist/extensions/core/mcp-deferred.d.ts.map +1 -1
  141. package/dist/extensions/core/mcp-deferred.js +74 -10
  142. package/dist/extensions/core/mcp-deferred.js.map +1 -1
  143. package/dist/extensions/core/mcp-loader.d.ts.map +1 -1
  144. package/dist/extensions/core/mcp-loader.js +52 -9
  145. package/dist/extensions/core/mcp-loader.js.map +1 -1
  146. package/dist/extensions/core/scaffold.d.ts +3 -3
  147. package/dist/extensions/core/scaffold.d.ts.map +1 -1
  148. package/dist/extensions/core/scaffold.js +9 -9
  149. package/dist/extensions/core/scaffold.js.map +1 -1
  150. package/dist/main.d.ts.map +1 -1
  151. package/dist/main.js +25 -7
  152. package/dist/main.js.map +1 -1
  153. package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
  154. package/dist/modes/interactive/components/settings-selector.js +2 -1
  155. package/dist/modes/interactive/components/settings-selector.js.map +1 -1
  156. package/dist/modes/interactive/theme/colorsafe-dark.json +96 -0
  157. package/dist/modes/interactive/theme/colorsafe-light.json +96 -0
  158. package/dist/modes/interactive/theme/dark.json +1 -0
  159. package/dist/modes/interactive/theme/high-contrast-dark.json +100 -0
  160. package/dist/modes/interactive/theme/high-contrast-light.json +97 -0
  161. package/dist/modes/interactive/theme/light.json +1 -0
  162. package/dist/modes/interactive/theme/theme-schema.json +4 -0
  163. package/dist/modes/interactive/theme/theme.d.ts +6 -0
  164. package/dist/modes/interactive/theme/theme.d.ts.map +1 -1
  165. package/dist/modes/interactive/theme/theme.js +70 -8
  166. package/dist/modes/interactive/theme/theme.js.map +1 -1
  167. package/dist/modes/interactive/theme/warm-dark.json +98 -0
  168. package/dist/modes/interactive/theme/warm-light.json +96 -0
  169. package/dist/utils/tools-manager.d.ts +8 -0
  170. package/dist/utils/tools-manager.d.ts.map +1 -1
  171. package/dist/utils/tools-manager.js +8 -2
  172. package/dist/utils/tools-manager.js.map +1 -1
  173. package/docs/themes.md +57 -2
  174. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  175. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  176. package/examples/extensions/sandbox/package.json +1 -1
  177. package/examples/extensions/with-deps/package.json +1 -1
  178. package/package.json +5 -4
@@ -1 +1 @@
1
- {"version":3,"file":"tools-manager.d.ts","sourceRoot":"","sources":["../../src/utils/tools-manager.ts"],"names":[],"mappings":"AA+BA,gFAAgF;AAChF,MAAM,MAAM,WAAW,GAAG,IAAI,GAAG,IAAI,GAAG,UAAU,GAAG,YAAY,GAAG,WAAW,CAAC;AAmOhF;;;;GAIG;AACH,MAAM,MAAM,gBAAgB,GAAG,CAAC,aAAa,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAC;AAM1F,wBAAsB,YAAY,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CA4C1G;AA6ID,wBAAsB,UAAU,CAC/B,IAAI,EAAE,WAAW,EACjB,MAAM,GAAE,OAAe,EACvB,UAAU,CAAC,EAAE,gBAAgB,GAC3B,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CA2C7B","sourcesContent":["import chalk from \"chalk\";\nimport { spawnSync } from \"child_process\";\nimport { createHash, randomBytes } from \"crypto\";\nimport extractZip from \"extract-zip\";\nimport {\n\tchmodSync,\n\tcreateWriteStream,\n\texistsSync,\n\tmkdirSync,\n\treaddirSync,\n\treadFileSync,\n\trenameSync,\n\trmSync,\n\tstatSync,\n} from \"fs\";\nimport { arch, platform } from \"os\";\nimport { join } from \"path\";\nimport { Readable } from \"stream\";\nimport { pipeline } from \"stream/promises\";\nimport { APP_NAME, getBinDir } from \"../config.js\";\n\nconst TOOLS_DIR = getBinDir();\nconst NETWORK_TIMEOUT_MS = 10_000;\nconst DOWNLOAD_TIMEOUT_MS = 120_000;\n\nfunction isOfflineModeEnabled(): boolean {\n\tconst value = process.env.HOOCODE_OFFLINE;\n\tif (!value) return false;\n\treturn value === \"1\" || value.toLowerCase() === \"true\" || value.toLowerCase() === \"yes\";\n}\n\n/** Tools whose binaries hoocode can resolve from PATH or download on demand. */\nexport type ManagedTool = \"fd\" | \"rg\" | \"webtools\" | \"voicetools\" | \"embsearch\";\n\ninterface ToolConfig {\n\tname: string;\n\trepo: string; // GitHub repo (e.g., \"sharkdp/fd\")\n\tbinaryName: string; // Name of the binary inside the archive\n\tsystemBinaryNames?: string[]; // Alternative system command names to try before downloading\n\ttagPrefix: string; // Prefix for tags (e.g., \"v\" for v1.0.0, \"\" for 1.0.0)\n\tgetAssetName: (version: string, plat: string, architecture: string) => string | null;\n}\n\nconst TOOLS: Record<string, ToolConfig> = {\n\tfd: {\n\t\tname: \"fd\",\n\t\trepo: \"sharkdp/fd\",\n\t\tbinaryName: \"fd\",\n\t\tsystemBinaryNames: [\"fd\", \"fdfind\"],\n\t\ttagPrefix: \"v\",\n\t\tgetAssetName: (version, plat, architecture) => {\n\t\t\tif (plat === \"darwin\") {\n\t\t\t\tconst archStr = architecture === \"arm64\" ? \"aarch64\" : \"x86_64\";\n\t\t\t\treturn `fd-v${version}-${archStr}-apple-darwin.tar.gz`;\n\t\t\t} else if (plat === \"linux\") {\n\t\t\t\tconst archStr = architecture === \"arm64\" ? \"aarch64\" : \"x86_64\";\n\t\t\t\treturn `fd-v${version}-${archStr}-unknown-linux-gnu.tar.gz`;\n\t\t\t} else if (plat === \"win32\") {\n\t\t\t\tconst archStr = architecture === \"arm64\" ? \"aarch64\" : \"x86_64\";\n\t\t\t\treturn `fd-v${version}-${archStr}-pc-windows-msvc.zip`;\n\t\t\t}\n\t\t\treturn null;\n\t\t},\n\t},\n\trg: {\n\t\tname: \"ripgrep\",\n\t\trepo: \"BurntSushi/ripgrep\",\n\t\tbinaryName: \"rg\",\n\t\ttagPrefix: \"\",\n\t\tgetAssetName: (version, plat, architecture) => {\n\t\t\tif (plat === \"darwin\") {\n\t\t\t\tconst archStr = architecture === \"arm64\" ? \"aarch64\" : \"x86_64\";\n\t\t\t\treturn `ripgrep-${version}-${archStr}-apple-darwin.tar.gz`;\n\t\t\t} else if (plat === \"linux\") {\n\t\t\t\tif (architecture === \"arm64\") {\n\t\t\t\t\treturn `ripgrep-${version}-aarch64-unknown-linux-gnu.tar.gz`;\n\t\t\t\t}\n\t\t\t\treturn `ripgrep-${version}-x86_64-unknown-linux-musl.tar.gz`;\n\t\t\t} else if (plat === \"win32\") {\n\t\t\t\tconst archStr = architecture === \"arm64\" ? \"aarch64\" : \"x86_64\";\n\t\t\t\treturn `ripgrep-${version}-${archStr}-pc-windows-msvc.zip`;\n\t\t\t}\n\t\t\treturn null;\n\t\t},\n\t},\n\twebtools: {\n\t\tname: \"webtools\",\n\t\trepo: \"kolisachint/webtools\",\n\t\tbinaryName: \"webtools\",\n\t\ttagPrefix: \"v\",\n\t\t// Release assets follow Rust target triples: webtools-<arch>-<target>.<ext>.\n\t\t// Some platforms may not be published yet; a missing asset 404s and ensureTool\n\t\t// degrades gracefully (returns undefined, tools fall back to an error message).\n\t\tgetAssetName: (_version, plat, architecture) => {\n\t\t\tconst archStr = architecture === \"arm64\" ? \"aarch64\" : \"x86_64\";\n\t\t\tif (plat === \"darwin\") {\n\t\t\t\treturn `webtools-${archStr}-apple-darwin.tar.gz`;\n\t\t\t} else if (plat === \"linux\") {\n\t\t\t\t// The upstream release matrix publishes aarch64 Linux as a musl\n\t\t\t\t// build and x86_64 as gnu; asking for aarch64-gnu only ever 404s.\n\t\t\t\tconst libc = architecture === \"arm64\" ? \"musl\" : \"gnu\";\n\t\t\t\treturn `webtools-${archStr}-unknown-linux-${libc}.tar.gz`;\n\t\t\t} else if (plat === \"win32\") {\n\t\t\t\treturn `webtools-${archStr}-pc-windows-msvc.zip`;\n\t\t\t}\n\t\t\treturn null;\n\t\t},\n\t},\n\tvoicetools: {\n\t\tname: \"voicetools\",\n\t\trepo: \"kolisachint/voicetools\",\n\t\tbinaryName: \"voicetools\",\n\t\ttagPrefix: \"v\",\n\t\t// Release archives follow Rust target triples: voicetools-<arch>-<target>.<ext>.\n\t\t// A missing platform asset 404s and ensureTool degrades gracefully (returns\n\t\t// undefined; the voice-transcribe caller then surfaces an error message).\n\t\tgetAssetName: (_version, plat, architecture) => {\n\t\t\tconst archStr = architecture === \"arm64\" ? \"aarch64\" : \"x86_64\";\n\t\t\tif (plat === \"darwin\") {\n\t\t\t\treturn `voicetools-${archStr}-apple-darwin.tar.gz`;\n\t\t\t} else if (plat === \"linux\") {\n\t\t\t\treturn `voicetools-${archStr}-unknown-linux-gnu.tar.gz`;\n\t\t\t} else if (plat === \"win32\") {\n\t\t\t\treturn `voicetools-${archStr}-pc-windows-msvc.zip`;\n\t\t\t}\n\t\t\treturn null;\n\t\t},\n\t},\n\tembsearch: {\n\t\tname: \"embsearch\",\n\t\trepo: \"kolisachint/embeddingsearchtools\",\n\t\tbinaryName: \"embsearch\",\n\t\ttagPrefix: \"v\",\n\t\t// Release archives follow Rust target triples: embsearch-<arch>-<target>.<ext>.\n\t\t// Must be the ONNX build (model bundled); the mock build is rejected by the\n\t\t// EmbsearchService. A missing platform asset 404s and ensureTool degrades\n\t\t// gracefully (returns undefined; the search tool stays lexical-only).\n\t\tgetAssetName: (_version, plat, architecture) => {\n\t\t\tconst archStr = architecture === \"arm64\" ? \"aarch64\" : \"x86_64\";\n\t\t\tif (plat === \"darwin\") {\n\t\t\t\treturn `embsearch-${archStr}-apple-darwin.tar.gz`;\n\t\t\t} else if (plat === \"linux\") {\n\t\t\t\treturn `embsearch-${archStr}-unknown-linux-gnu.tar.gz`;\n\t\t\t} else if (plat === \"win32\") {\n\t\t\t\treturn `embsearch-${archStr}-pc-windows-msvc.zip`;\n\t\t\t}\n\t\t\treturn null;\n\t\t},\n\t},\n};\n\n// Check if a command exists in PATH by trying to run it\nfunction commandExists(cmd: string): boolean {\n\ttry {\n\t\tconst result = spawnSync(cmd, [\"--version\"], { stdio: \"pipe\" });\n\t\t// Check for ENOENT error (command not found)\n\t\treturn result.error === undefined || result.error === null;\n\t} catch {\n\t\treturn false;\n\t}\n}\n\n// Resolved tool paths are stable for the life of the process. Cache the first\n// successful resolution so we never re-run the synchronous spawnSync probe in\n// commandExists() on every grep/find/glob invocation, which blocks the event loop.\nconst resolvedToolPathCache = new Map<ManagedTool, string>();\n\n// Get the path to a tool (system-wide or in our tools dir)\nfunction getToolPath(tool: ManagedTool): string | null {\n\tconst config = TOOLS[tool];\n\tif (!config) return null;\n\n\t// Explicit binary override (per-tool env var, e.g. HOOCODE_WEBTOOLS_BINARY).\n\t// Lets a developer point at a locally built binary that predates a release,\n\t// bypassing the tools-dir/PATH resolution and download. Authoritative when set\n\t// and the path exists.\n\tconst overrideEnv = `HOOCODE_${tool.toUpperCase()}_BINARY`;\n\tconst override = process.env[overrideEnv]?.trim();\n\tif (override && existsSync(override)) {\n\t\treturn override;\n\t}\n\n\t// Reuse a previously resolved path. A bare command name resolves via PATH;\n\t// an absolute path must still exist (revalidate cheaply with existsSync).\n\tconst cached = resolvedToolPathCache.get(tool);\n\tif (cached !== undefined) {\n\t\tconst isAbsolutePath = cached.includes(\"/\") || cached.includes(\"\\\\\");\n\t\tif (!isAbsolutePath || existsSync(cached)) {\n\t\t\treturn cached;\n\t\t}\n\t\tresolvedToolPathCache.delete(tool);\n\t}\n\n\t// Check our tools directory first\n\tconst localPath = join(TOOLS_DIR, config.binaryName + (platform() === \"win32\" ? \".exe\" : \"\"));\n\tif (existsSync(localPath)) {\n\t\tresolvedToolPathCache.set(tool, localPath);\n\t\treturn localPath;\n\t}\n\n\t// Check system PATH - if found, just return the command name (it's in PATH)\n\tconst systemBinaryNames = config.systemBinaryNames ?? [config.binaryName];\n\tfor (const systemBinaryName of systemBinaryNames) {\n\t\tif (commandExists(systemBinaryName)) {\n\t\t\tresolvedToolPathCache.set(tool, systemBinaryName);\n\t\t\treturn systemBinaryName;\n\t\t}\n\t}\n\n\treturn null;\n}\n\n// Fetch latest release version from GitHub\nasync function getLatestVersion(repo: string): Promise<string> {\n\tconst response = await fetch(`https://api.github.com/repos/${repo}/releases/latest`, {\n\t\theaders: { \"User-Agent\": `${APP_NAME}-coding-agent` },\n\t\tsignal: AbortSignal.timeout(NETWORK_TIMEOUT_MS),\n\t});\n\n\tif (!response.ok) {\n\t\tthrow new Error(`GitHub API error: ${response.status}`);\n\t}\n\n\tconst data = (await response.json()) as { tag_name: string };\n\treturn data.tag_name.replace(/^v/, \"\");\n}\n\n// Best-effort SHA-256 verification: fetch \"<downloadUrl>.sha256\" and, when it is\n// served (HTTP 200), verify the downloaded file against it. A 404 (or any other\n// non-200 / network error) means no published checksum, so verification is\n// skipped rather than treated as a failure. A genuine mismatch throws.\nasync function verifyChecksum(downloadUrl: string, filePath: string): Promise<void> {\n\tlet checksumResponse: Awaited<ReturnType<typeof fetch>>;\n\ttry {\n\t\tchecksumResponse = await fetch(`${downloadUrl}.sha256`, {\n\t\t\tsignal: AbortSignal.timeout(NETWORK_TIMEOUT_MS),\n\t\t});\n\t} catch {\n\t\t// Network error fetching the checksum is non-fatal for best-effort verification.\n\t\treturn;\n\t}\n\n\tif (checksumResponse.status !== 200) {\n\t\treturn;\n\t}\n\n\t// sha256 files are commonly \"<hex> <filename>\"; take the leading token.\n\tconst expectedHash = (await checksumResponse.text()).trim().split(/\\s+/)[0]?.toLowerCase();\n\tif (!expectedHash || !/^[0-9a-f]{64}$/.test(expectedHash)) {\n\t\t// Unusable checksum body: skip rather than fail (still best-effort).\n\t\treturn;\n\t}\n\n\tconst actualHash = createHash(\"sha256\").update(readFileSync(filePath)).digest(\"hex\");\n\tif (actualHash !== expectedHash) {\n\t\tthrow new Error(`Checksum mismatch for ${downloadUrl}: expected ${expectedHash}, got ${actualHash}`);\n\t}\n}\n\n/**\n * Live download progress: bytes received so far and the total from\n * Content-Length (null when the server doesn't send one). Callers use it to\n * drive a progress bar instead of an indefinite spinner.\n */\nexport type DownloadProgress = (receivedBytes: number, totalBytes: number | null) => void;\n\n// Download a file from URL into `dest`, validating integrity. Throws (and removes\n// the partial file) on a truncated transfer (bytes written != Content-Length when\n// the header is present) or a SHA-256 mismatch, so a corrupt artifact is never\n// left behind. Exported for tests.\nexport async function downloadFile(url: string, dest: string, onProgress?: DownloadProgress): Promise<void> {\n\ttry {\n\t\tconst response = await fetch(url, {\n\t\t\tsignal: AbortSignal.timeout(DOWNLOAD_TIMEOUT_MS),\n\t\t});\n\n\t\tif (!response.ok) {\n\t\t\tthrow new Error(`Failed to download: ${response.status}`);\n\t\t}\n\n\t\tif (!response.body) {\n\t\t\tthrow new Error(\"No response body\");\n\t\t}\n\n\t\tconst contentLengthHeader = response.headers.get(\"content-length\");\n\t\tconst expectedBytes =\n\t\t\tcontentLengthHeader !== null && contentLengthHeader.trim() !== \"\" ? Number(contentLengthHeader) : null;\n\n\t\tconst fileStream = createWriteStream(dest);\n\t\tconst source = Readable.fromWeb(response.body as Parameters<typeof Readable.fromWeb>[0]);\n\t\tif (onProgress) {\n\t\t\tlet received = 0;\n\t\t\tsource.on(\"data\", (chunk: Buffer) => {\n\t\t\t\treceived += chunk.length;\n\t\t\t\tonProgress(received, expectedBytes);\n\t\t\t});\n\t\t}\n\t\tawait pipeline(source, fileStream);\n\n\t\tif (expectedBytes !== null && Number.isFinite(expectedBytes)) {\n\t\t\tconst bytesWritten = statSync(dest).size;\n\t\t\tif (bytesWritten !== expectedBytes) {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t`Truncated download from ${url}: expected ${expectedBytes} bytes, received ${bytesWritten}`,\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\n\t\tawait verifyChecksum(url, dest);\n\t} catch (e) {\n\t\t// Never leave a partial/corrupt file behind on any failure.\n\t\trmSync(dest, { force: true });\n\t\tthrow e;\n\t}\n}\n\nfunction findBinaryRecursively(rootDir: string, binaryFileName: string): string | null {\n\tconst stack: string[] = [rootDir];\n\n\twhile (stack.length > 0) {\n\t\tconst currentDir = stack.pop();\n\t\tif (!currentDir) continue;\n\n\t\tconst entries = readdirSync(currentDir, { withFileTypes: true });\n\t\tfor (const entry of entries) {\n\t\t\tconst fullPath = join(currentDir, entry.name);\n\t\t\tif (entry.isFile() && entry.name === binaryFileName) {\n\t\t\t\treturn fullPath;\n\t\t\t}\n\t\t\tif (entry.isDirectory()) {\n\t\t\t\tstack.push(fullPath);\n\t\t\t}\n\t\t}\n\t}\n\n\treturn null;\n}\n\n// Download and install a tool\nasync function downloadTool(tool: ManagedTool, onProgress?: DownloadProgress): Promise<string> {\n\tconst config = TOOLS[tool];\n\tif (!config) throw new Error(`Unknown tool: ${tool}`);\n\n\tconst plat = platform();\n\tconst architecture = arch();\n\n\t// Get latest version\n\tconst version = await getLatestVersion(config.repo);\n\n\t// Get asset name for this platform\n\tconst assetName = config.getAssetName(version, plat, architecture);\n\tif (!assetName) {\n\t\tthrow new Error(`Unsupported platform: ${plat}/${architecture}`);\n\t}\n\n\t// Create tools directory\n\tmkdirSync(TOOLS_DIR, { recursive: true });\n\n\tconst downloadUrl = `https://github.com/${config.repo}/releases/download/${config.tagPrefix}${version}/${assetName}`;\n\tconst archivePath = join(TOOLS_DIR, assetName);\n\tconst binaryExt = plat === \"win32\" ? \".exe\" : \"\";\n\tconst binaryPath = join(TOOLS_DIR, config.binaryName + binaryExt);\n\n\t// Download to a unique temp path, validate, then atomically rename into place.\n\t// Writing the shared archive path directly would leave a corrupt partial behind\n\t// if the transfer fails or is truncated. fd/rg/webtools can also download\n\t// concurrently at startup, so the per-attempt temp name must be unique.\n\tconst tempArchivePath = `${archivePath}.${process.pid}.${randomBytes(6).toString(\"hex\")}.part`;\n\n\t// Extract into a unique temp directory. fd and rg downloads can run concurrently\n\t// during startup, so sharing a fixed directory causes races.\n\tconst extractDir = join(\n\t\tTOOLS_DIR,\n\t\t`extract_tmp_${config.binaryName}_${process.pid}_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`,\n\t);\n\n\ttry {\n\t\t// One retry (2 attempts total) around download + integrity verification.\n\t\t// downloadFile removes its own partial on failure, so each attempt is clean.\n\t\tlet lastError: unknown;\n\t\tlet downloaded = false;\n\t\tfor (let attempt = 1; attempt <= 2 && !downloaded; attempt++) {\n\t\t\ttry {\n\t\t\t\tawait downloadFile(downloadUrl, tempArchivePath, onProgress);\n\t\t\t\tdownloaded = true;\n\t\t\t} catch (e) {\n\t\t\t\tlastError = e;\n\t\t\t\trmSync(tempArchivePath, { force: true });\n\t\t\t}\n\t\t}\n\t\tif (!downloaded) {\n\t\t\tthrow lastError instanceof Error ? lastError : new Error(String(lastError));\n\t\t}\n\n\t\t// Atomic publish of the verified archive, then extract.\n\t\trenameSync(tempArchivePath, archivePath);\n\t\tmkdirSync(extractDir, { recursive: true });\n\n\t\tif (assetName.endsWith(\".tar.gz\")) {\n\t\t\tconst extractResult = spawnSync(\"tar\", [\"xzf\", archivePath, \"-C\", extractDir], { stdio: \"pipe\" });\n\t\t\tif (extractResult.error || extractResult.status !== 0) {\n\t\t\t\tconst errMsg = extractResult.error?.message ?? extractResult.stderr?.toString().trim() ?? \"unknown error\";\n\t\t\t\tthrow new Error(`Failed to extract ${assetName}: ${errMsg}`);\n\t\t\t}\n\t\t} else if (assetName.endsWith(\".zip\")) {\n\t\t\tawait extractZip(archivePath, { dir: extractDir });\n\t\t} else {\n\t\t\tthrow new Error(`Unsupported archive format: ${assetName}`);\n\t\t}\n\n\t\t// Find the binary in extracted files. Some archives contain files directly\n\t\t// at root, others nest under a versioned subdirectory.\n\t\tconst binaryFileName = config.binaryName + binaryExt;\n\t\tconst extractedDir = join(extractDir, assetName.replace(/\\.(tar\\.gz|zip)$/, \"\"));\n\t\tconst extractedBinaryCandidates = [join(extractedDir, binaryFileName), join(extractDir, binaryFileName)];\n\t\tlet extractedBinary = extractedBinaryCandidates.find((candidate) => existsSync(candidate));\n\n\t\tif (!extractedBinary) {\n\t\t\textractedBinary = findBinaryRecursively(extractDir, binaryFileName) ?? undefined;\n\t\t}\n\n\t\tif (extractedBinary) {\n\t\t\trenameSync(extractedBinary, binaryPath);\n\t\t} else {\n\t\t\tthrow new Error(`Binary not found in archive: expected ${binaryFileName} under ${extractDir}`);\n\t\t}\n\n\t\t// Make executable (Unix only)\n\t\tif (plat !== \"win32\") {\n\t\t\tchmodSync(binaryPath, 0o755);\n\t\t}\n\t} finally {\n\t\t// Guaranteed cleanup of every transient artifact on ANY outcome: the temp\n\t\t// download (if a failure left it before the rename), the published archive,\n\t\t// and the temp extract dir.\n\t\trmSync(tempArchivePath, { force: true });\n\t\trmSync(archivePath, { force: true });\n\t\trmSync(extractDir, { recursive: true, force: true });\n\t}\n\n\treturn binaryPath;\n}\n\n// Termux package names for tools\nconst TERMUX_PACKAGES: Record<string, string> = {\n\tfd: \"fd\",\n\trg: \"ripgrep\",\n\twebtools: \"webtools\",\n\tvoicetools: \"voicetools\",\n\tembsearch: \"embsearch\",\n};\n\n// Ensure a tool is available, downloading if necessary\n// Returns the path to the tool, or null if unavailable\n// onProgress receives live byte counts while the release archive downloads.\nexport async function ensureTool(\n\ttool: ManagedTool,\n\tsilent: boolean = false,\n\tonProgress?: DownloadProgress,\n): Promise<string | undefined> {\n\tconst existingPath = getToolPath(tool);\n\tif (existingPath) {\n\t\treturn existingPath;\n\t}\n\n\tconst config = TOOLS[tool];\n\tif (!config) return undefined;\n\n\tif (isOfflineModeEnabled()) {\n\t\tif (!silent) {\n\t\t\tconsole.log(chalk.yellow(`${config.name} not found. Offline mode enabled, skipping download.`));\n\t\t}\n\t\treturn undefined;\n\t}\n\n\t// On Android/Termux, Linux binaries don't work due to Bionic libc incompatibility.\n\t// Users must install via pkg.\n\tif (platform() === \"android\") {\n\t\tconst pkgName = TERMUX_PACKAGES[tool] ?? tool;\n\t\tif (!silent) {\n\t\t\tconsole.log(chalk.yellow(`${config.name} not found. Install with: pkg install ${pkgName}`));\n\t\t}\n\t\treturn undefined;\n\t}\n\n\t// Tool not found - download it\n\tif (!silent) {\n\t\tconsole.log(chalk.dim(`${config.name} not found. Downloading...`));\n\t}\n\n\ttry {\n\t\tconst path = await downloadTool(tool, onProgress);\n\t\tif (!silent) {\n\t\t\tconsole.log(chalk.dim(`${config.name} installed to ${path}`));\n\t\t}\n\t\treturn path;\n\t} catch (e) {\n\t\tif (!silent) {\n\t\t\tconsole.log(chalk.yellow(`Failed to download ${config.name}: ${e instanceof Error ? e.message : e}`));\n\t\t}\n\t\treturn undefined;\n\t}\n}\n"]}
1
+ {"version":3,"file":"tools-manager.d.ts","sourceRoot":"","sources":["../../src/utils/tools-manager.ts"],"names":[],"mappings":"AA+BA,gFAAgF;AAChF,MAAM,MAAM,WAAW,GAAG,IAAI,GAAG,IAAI,GAAG,UAAU,GAAG,YAAY,GAAG,WAAW,CAAC;AAuIhF;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,WAAW,GAAG,MAAM,GAAG,IAAI,CA0C5D;AAiDD;;;;GAIG;AACH,MAAM,MAAM,gBAAgB,GAAG,CAAC,aAAa,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAC;AAM1F,wBAAsB,YAAY,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CA4C1G;AA6ID,wBAAsB,UAAU,CAC/B,IAAI,EAAE,WAAW,EACjB,MAAM,GAAE,OAAe,EACvB,UAAU,CAAC,EAAE,gBAAgB,GAC3B,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CA2C7B","sourcesContent":["import chalk from \"chalk\";\nimport { spawnSync } from \"child_process\";\nimport { createHash, randomBytes } from \"crypto\";\nimport extractZip from \"extract-zip\";\nimport {\n\tchmodSync,\n\tcreateWriteStream,\n\texistsSync,\n\tmkdirSync,\n\treaddirSync,\n\treadFileSync,\n\trenameSync,\n\trmSync,\n\tstatSync,\n} from \"fs\";\nimport { arch, platform } from \"os\";\nimport { join } from \"path\";\nimport { Readable } from \"stream\";\nimport { pipeline } from \"stream/promises\";\nimport { APP_NAME, getBinDir } from \"../config.js\";\n\nconst TOOLS_DIR = getBinDir();\nconst NETWORK_TIMEOUT_MS = 10_000;\nconst DOWNLOAD_TIMEOUT_MS = 120_000;\n\nfunction isOfflineModeEnabled(): boolean {\n\tconst value = process.env.HOOCODE_OFFLINE;\n\tif (!value) return false;\n\treturn value === \"1\" || value.toLowerCase() === \"true\" || value.toLowerCase() === \"yes\";\n}\n\n/** Tools whose binaries hoocode can resolve from PATH or download on demand. */\nexport type ManagedTool = \"fd\" | \"rg\" | \"webtools\" | \"voicetools\" | \"embsearch\";\n\ninterface ToolConfig {\n\tname: string;\n\trepo: string; // GitHub repo (e.g., \"sharkdp/fd\")\n\tbinaryName: string; // Name of the binary inside the archive\n\tsystemBinaryNames?: string[]; // Alternative system command names to try before downloading\n\ttagPrefix: string; // Prefix for tags (e.g., \"v\" for v1.0.0, \"\" for 1.0.0)\n\tgetAssetName: (version: string, plat: string, architecture: string) => string | null;\n}\n\nconst TOOLS: Record<string, ToolConfig> = {\n\tfd: {\n\t\tname: \"fd\",\n\t\trepo: \"sharkdp/fd\",\n\t\tbinaryName: \"fd\",\n\t\tsystemBinaryNames: [\"fd\", \"fdfind\"],\n\t\ttagPrefix: \"v\",\n\t\tgetAssetName: (version, plat, architecture) => {\n\t\t\tif (plat === \"darwin\") {\n\t\t\t\tconst archStr = architecture === \"arm64\" ? \"aarch64\" : \"x86_64\";\n\t\t\t\treturn `fd-v${version}-${archStr}-apple-darwin.tar.gz`;\n\t\t\t} else if (plat === \"linux\") {\n\t\t\t\tconst archStr = architecture === \"arm64\" ? \"aarch64\" : \"x86_64\";\n\t\t\t\treturn `fd-v${version}-${archStr}-unknown-linux-gnu.tar.gz`;\n\t\t\t} else if (plat === \"win32\") {\n\t\t\t\tconst archStr = architecture === \"arm64\" ? \"aarch64\" : \"x86_64\";\n\t\t\t\treturn `fd-v${version}-${archStr}-pc-windows-msvc.zip`;\n\t\t\t}\n\t\t\treturn null;\n\t\t},\n\t},\n\trg: {\n\t\tname: \"ripgrep\",\n\t\trepo: \"BurntSushi/ripgrep\",\n\t\tbinaryName: \"rg\",\n\t\ttagPrefix: \"\",\n\t\tgetAssetName: (version, plat, architecture) => {\n\t\t\tif (plat === \"darwin\") {\n\t\t\t\tconst archStr = architecture === \"arm64\" ? \"aarch64\" : \"x86_64\";\n\t\t\t\treturn `ripgrep-${version}-${archStr}-apple-darwin.tar.gz`;\n\t\t\t} else if (plat === \"linux\") {\n\t\t\t\tif (architecture === \"arm64\") {\n\t\t\t\t\treturn `ripgrep-${version}-aarch64-unknown-linux-gnu.tar.gz`;\n\t\t\t\t}\n\t\t\t\treturn `ripgrep-${version}-x86_64-unknown-linux-musl.tar.gz`;\n\t\t\t} else if (plat === \"win32\") {\n\t\t\t\tconst archStr = architecture === \"arm64\" ? \"aarch64\" : \"x86_64\";\n\t\t\t\treturn `ripgrep-${version}-${archStr}-pc-windows-msvc.zip`;\n\t\t\t}\n\t\t\treturn null;\n\t\t},\n\t},\n\twebtools: {\n\t\tname: \"webtools\",\n\t\trepo: \"kolisachint/webtools\",\n\t\tbinaryName: \"webtools\",\n\t\ttagPrefix: \"v\",\n\t\t// Release assets follow Rust target triples: webtools-<arch>-<target>.<ext>.\n\t\t// Some platforms may not be published yet; a missing asset 404s and ensureTool\n\t\t// degrades gracefully (returns undefined, tools fall back to an error message).\n\t\tgetAssetName: (_version, plat, architecture) => {\n\t\t\tconst archStr = architecture === \"arm64\" ? \"aarch64\" : \"x86_64\";\n\t\t\tif (plat === \"darwin\") {\n\t\t\t\treturn `webtools-${archStr}-apple-darwin.tar.gz`;\n\t\t\t} else if (plat === \"linux\") {\n\t\t\t\t// The upstream release matrix publishes aarch64 Linux as a musl\n\t\t\t\t// build and x86_64 as gnu; asking for aarch64-gnu only ever 404s.\n\t\t\t\tconst libc = architecture === \"arm64\" ? \"musl\" : \"gnu\";\n\t\t\t\treturn `webtools-${archStr}-unknown-linux-${libc}.tar.gz`;\n\t\t\t} else if (plat === \"win32\") {\n\t\t\t\treturn `webtools-${archStr}-pc-windows-msvc.zip`;\n\t\t\t}\n\t\t\treturn null;\n\t\t},\n\t},\n\tvoicetools: {\n\t\tname: \"voicetools\",\n\t\trepo: \"kolisachint/voicetools\",\n\t\tbinaryName: \"voicetools\",\n\t\ttagPrefix: \"v\",\n\t\t// Release archives follow Rust target triples: voicetools-<arch>-<target>.<ext>.\n\t\t// A missing platform asset 404s and ensureTool degrades gracefully (returns\n\t\t// undefined; the voice-transcribe caller then surfaces an error message).\n\t\tgetAssetName: (_version, plat, architecture) => {\n\t\t\tconst archStr = architecture === \"arm64\" ? \"aarch64\" : \"x86_64\";\n\t\t\tif (plat === \"darwin\") {\n\t\t\t\treturn `voicetools-${archStr}-apple-darwin.tar.gz`;\n\t\t\t} else if (plat === \"linux\") {\n\t\t\t\treturn `voicetools-${archStr}-unknown-linux-gnu.tar.gz`;\n\t\t\t} else if (plat === \"win32\") {\n\t\t\t\treturn `voicetools-${archStr}-pc-windows-msvc.zip`;\n\t\t\t}\n\t\t\treturn null;\n\t\t},\n\t},\n\tembsearch: {\n\t\tname: \"embsearch\",\n\t\trepo: \"kolisachint/embeddingsearchtools\",\n\t\tbinaryName: \"embsearch\",\n\t\ttagPrefix: \"v\",\n\t\t// Release archives follow Rust target triples: embsearch-<arch>-<target>.<ext>.\n\t\t// Must be the ONNX build (model bundled); the mock build is rejected by the\n\t\t// EmbsearchService. A missing platform asset 404s and ensureTool degrades\n\t\t// gracefully (returns undefined; the search tool stays lexical-only).\n\t\tgetAssetName: (_version, plat, architecture) => {\n\t\t\tconst archStr = architecture === \"arm64\" ? \"aarch64\" : \"x86_64\";\n\t\t\tif (plat === \"darwin\") {\n\t\t\t\treturn `embsearch-${archStr}-apple-darwin.tar.gz`;\n\t\t\t} else if (plat === \"linux\") {\n\t\t\t\treturn `embsearch-${archStr}-unknown-linux-gnu.tar.gz`;\n\t\t\t} else if (plat === \"win32\") {\n\t\t\t\treturn `embsearch-${archStr}-pc-windows-msvc.zip`;\n\t\t\t}\n\t\t\treturn null;\n\t\t},\n\t},\n};\n\n// Check if a command exists in PATH by trying to run it\nfunction commandExists(cmd: string): boolean {\n\ttry {\n\t\tconst result = spawnSync(cmd, [\"--version\"], { stdio: \"pipe\" });\n\t\t// Check for ENOENT error (command not found)\n\t\treturn result.error === undefined || result.error === null;\n\t} catch {\n\t\treturn false;\n\t}\n}\n\n// Resolved tool paths are stable for the life of the process. Cache the first\n// successful resolution so we never re-run the synchronous spawnSync probe in\n// commandExists() on every grep/find/glob invocation, which blocks the event loop.\nconst resolvedToolPathCache = new Map<ManagedTool, string>();\n\n/**\n * Path to an already-available tool (system-wide or in our tools dir), or null.\n *\n * Exported as the never-downloads counterpart to {@link ensureTool}: a caller\n * that wants a capability *if it happens to be installed* — and must not spend a\n * download to get it — asks here and degrades when the answer is null.\n */\nexport function getToolPath(tool: ManagedTool): string | null {\n\tconst config = TOOLS[tool];\n\tif (!config) return null;\n\n\t// Explicit binary override (per-tool env var, e.g. HOOCODE_WEBTOOLS_BINARY).\n\t// Lets a developer point at a locally built binary that predates a release,\n\t// bypassing the tools-dir/PATH resolution and download. Authoritative when set\n\t// and the path exists.\n\tconst overrideEnv = `HOOCODE_${tool.toUpperCase()}_BINARY`;\n\tconst override = process.env[overrideEnv]?.trim();\n\tif (override && existsSync(override)) {\n\t\treturn override;\n\t}\n\n\t// Reuse a previously resolved path. A bare command name resolves via PATH;\n\t// an absolute path must still exist (revalidate cheaply with existsSync).\n\tconst cached = resolvedToolPathCache.get(tool);\n\tif (cached !== undefined) {\n\t\tconst isAbsolutePath = cached.includes(\"/\") || cached.includes(\"\\\\\");\n\t\tif (!isAbsolutePath || existsSync(cached)) {\n\t\t\treturn cached;\n\t\t}\n\t\tresolvedToolPathCache.delete(tool);\n\t}\n\n\t// Check our tools directory first\n\tconst localPath = join(TOOLS_DIR, config.binaryName + (platform() === \"win32\" ? \".exe\" : \"\"));\n\tif (existsSync(localPath)) {\n\t\tresolvedToolPathCache.set(tool, localPath);\n\t\treturn localPath;\n\t}\n\n\t// Check system PATH - if found, just return the command name (it's in PATH)\n\tconst systemBinaryNames = config.systemBinaryNames ?? [config.binaryName];\n\tfor (const systemBinaryName of systemBinaryNames) {\n\t\tif (commandExists(systemBinaryName)) {\n\t\t\tresolvedToolPathCache.set(tool, systemBinaryName);\n\t\t\treturn systemBinaryName;\n\t\t}\n\t}\n\n\treturn null;\n}\n\n// Fetch latest release version from GitHub\nasync function getLatestVersion(repo: string): Promise<string> {\n\tconst response = await fetch(`https://api.github.com/repos/${repo}/releases/latest`, {\n\t\theaders: { \"User-Agent\": `${APP_NAME}-coding-agent` },\n\t\tsignal: AbortSignal.timeout(NETWORK_TIMEOUT_MS),\n\t});\n\n\tif (!response.ok) {\n\t\tthrow new Error(`GitHub API error: ${response.status}`);\n\t}\n\n\tconst data = (await response.json()) as { tag_name: string };\n\treturn data.tag_name.replace(/^v/, \"\");\n}\n\n// Best-effort SHA-256 verification: fetch \"<downloadUrl>.sha256\" and, when it is\n// served (HTTP 200), verify the downloaded file against it. A 404 (or any other\n// non-200 / network error) means no published checksum, so verification is\n// skipped rather than treated as a failure. A genuine mismatch throws.\nasync function verifyChecksum(downloadUrl: string, filePath: string): Promise<void> {\n\tlet checksumResponse: Awaited<ReturnType<typeof fetch>>;\n\ttry {\n\t\tchecksumResponse = await fetch(`${downloadUrl}.sha256`, {\n\t\t\tsignal: AbortSignal.timeout(NETWORK_TIMEOUT_MS),\n\t\t});\n\t} catch {\n\t\t// Network error fetching the checksum is non-fatal for best-effort verification.\n\t\treturn;\n\t}\n\n\tif (checksumResponse.status !== 200) {\n\t\treturn;\n\t}\n\n\t// sha256 files are commonly \"<hex> <filename>\"; take the leading token.\n\tconst expectedHash = (await checksumResponse.text()).trim().split(/\\s+/)[0]?.toLowerCase();\n\tif (!expectedHash || !/^[0-9a-f]{64}$/.test(expectedHash)) {\n\t\t// Unusable checksum body: skip rather than fail (still best-effort).\n\t\treturn;\n\t}\n\n\tconst actualHash = createHash(\"sha256\").update(readFileSync(filePath)).digest(\"hex\");\n\tif (actualHash !== expectedHash) {\n\t\tthrow new Error(`Checksum mismatch for ${downloadUrl}: expected ${expectedHash}, got ${actualHash}`);\n\t}\n}\n\n/**\n * Live download progress: bytes received so far and the total from\n * Content-Length (null when the server doesn't send one). Callers use it to\n * drive a progress bar instead of an indefinite spinner.\n */\nexport type DownloadProgress = (receivedBytes: number, totalBytes: number | null) => void;\n\n// Download a file from URL into `dest`, validating integrity. Throws (and removes\n// the partial file) on a truncated transfer (bytes written != Content-Length when\n// the header is present) or a SHA-256 mismatch, so a corrupt artifact is never\n// left behind. Exported for tests.\nexport async function downloadFile(url: string, dest: string, onProgress?: DownloadProgress): Promise<void> {\n\ttry {\n\t\tconst response = await fetch(url, {\n\t\t\tsignal: AbortSignal.timeout(DOWNLOAD_TIMEOUT_MS),\n\t\t});\n\n\t\tif (!response.ok) {\n\t\t\tthrow new Error(`Failed to download: ${response.status}`);\n\t\t}\n\n\t\tif (!response.body) {\n\t\t\tthrow new Error(\"No response body\");\n\t\t}\n\n\t\tconst contentLengthHeader = response.headers.get(\"content-length\");\n\t\tconst expectedBytes =\n\t\t\tcontentLengthHeader !== null && contentLengthHeader.trim() !== \"\" ? Number(contentLengthHeader) : null;\n\n\t\tconst fileStream = createWriteStream(dest);\n\t\tconst source = Readable.fromWeb(response.body as Parameters<typeof Readable.fromWeb>[0]);\n\t\tif (onProgress) {\n\t\t\tlet received = 0;\n\t\t\tsource.on(\"data\", (chunk: Buffer) => {\n\t\t\t\treceived += chunk.length;\n\t\t\t\tonProgress(received, expectedBytes);\n\t\t\t});\n\t\t}\n\t\tawait pipeline(source, fileStream);\n\n\t\tif (expectedBytes !== null && Number.isFinite(expectedBytes)) {\n\t\t\tconst bytesWritten = statSync(dest).size;\n\t\t\tif (bytesWritten !== expectedBytes) {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t`Truncated download from ${url}: expected ${expectedBytes} bytes, received ${bytesWritten}`,\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\n\t\tawait verifyChecksum(url, dest);\n\t} catch (e) {\n\t\t// Never leave a partial/corrupt file behind on any failure.\n\t\trmSync(dest, { force: true });\n\t\tthrow e;\n\t}\n}\n\nfunction findBinaryRecursively(rootDir: string, binaryFileName: string): string | null {\n\tconst stack: string[] = [rootDir];\n\n\twhile (stack.length > 0) {\n\t\tconst currentDir = stack.pop();\n\t\tif (!currentDir) continue;\n\n\t\tconst entries = readdirSync(currentDir, { withFileTypes: true });\n\t\tfor (const entry of entries) {\n\t\t\tconst fullPath = join(currentDir, entry.name);\n\t\t\tif (entry.isFile() && entry.name === binaryFileName) {\n\t\t\t\treturn fullPath;\n\t\t\t}\n\t\t\tif (entry.isDirectory()) {\n\t\t\t\tstack.push(fullPath);\n\t\t\t}\n\t\t}\n\t}\n\n\treturn null;\n}\n\n// Download and install a tool\nasync function downloadTool(tool: ManagedTool, onProgress?: DownloadProgress): Promise<string> {\n\tconst config = TOOLS[tool];\n\tif (!config) throw new Error(`Unknown tool: ${tool}`);\n\n\tconst plat = platform();\n\tconst architecture = arch();\n\n\t// Get latest version\n\tconst version = await getLatestVersion(config.repo);\n\n\t// Get asset name for this platform\n\tconst assetName = config.getAssetName(version, plat, architecture);\n\tif (!assetName) {\n\t\tthrow new Error(`Unsupported platform: ${plat}/${architecture}`);\n\t}\n\n\t// Create tools directory\n\tmkdirSync(TOOLS_DIR, { recursive: true });\n\n\tconst downloadUrl = `https://github.com/${config.repo}/releases/download/${config.tagPrefix}${version}/${assetName}`;\n\tconst archivePath = join(TOOLS_DIR, assetName);\n\tconst binaryExt = plat === \"win32\" ? \".exe\" : \"\";\n\tconst binaryPath = join(TOOLS_DIR, config.binaryName + binaryExt);\n\n\t// Download to a unique temp path, validate, then atomically rename into place.\n\t// Writing the shared archive path directly would leave a corrupt partial behind\n\t// if the transfer fails or is truncated. fd/rg/webtools can also download\n\t// concurrently at startup, so the per-attempt temp name must be unique.\n\tconst tempArchivePath = `${archivePath}.${process.pid}.${randomBytes(6).toString(\"hex\")}.part`;\n\n\t// Extract into a unique temp directory. fd and rg downloads can run concurrently\n\t// during startup, so sharing a fixed directory causes races.\n\tconst extractDir = join(\n\t\tTOOLS_DIR,\n\t\t`extract_tmp_${config.binaryName}_${process.pid}_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`,\n\t);\n\n\ttry {\n\t\t// One retry (2 attempts total) around download + integrity verification.\n\t\t// downloadFile removes its own partial on failure, so each attempt is clean.\n\t\tlet lastError: unknown;\n\t\tlet downloaded = false;\n\t\tfor (let attempt = 1; attempt <= 2 && !downloaded; attempt++) {\n\t\t\ttry {\n\t\t\t\tawait downloadFile(downloadUrl, tempArchivePath, onProgress);\n\t\t\t\tdownloaded = true;\n\t\t\t} catch (e) {\n\t\t\t\tlastError = e;\n\t\t\t\trmSync(tempArchivePath, { force: true });\n\t\t\t}\n\t\t}\n\t\tif (!downloaded) {\n\t\t\tthrow lastError instanceof Error ? lastError : new Error(String(lastError));\n\t\t}\n\n\t\t// Atomic publish of the verified archive, then extract.\n\t\trenameSync(tempArchivePath, archivePath);\n\t\tmkdirSync(extractDir, { recursive: true });\n\n\t\tif (assetName.endsWith(\".tar.gz\")) {\n\t\t\tconst extractResult = spawnSync(\"tar\", [\"xzf\", archivePath, \"-C\", extractDir], { stdio: \"pipe\" });\n\t\t\tif (extractResult.error || extractResult.status !== 0) {\n\t\t\t\tconst errMsg = extractResult.error?.message ?? extractResult.stderr?.toString().trim() ?? \"unknown error\";\n\t\t\t\tthrow new Error(`Failed to extract ${assetName}: ${errMsg}`);\n\t\t\t}\n\t\t} else if (assetName.endsWith(\".zip\")) {\n\t\t\tawait extractZip(archivePath, { dir: extractDir });\n\t\t} else {\n\t\t\tthrow new Error(`Unsupported archive format: ${assetName}`);\n\t\t}\n\n\t\t// Find the binary in extracted files. Some archives contain files directly\n\t\t// at root, others nest under a versioned subdirectory.\n\t\tconst binaryFileName = config.binaryName + binaryExt;\n\t\tconst extractedDir = join(extractDir, assetName.replace(/\\.(tar\\.gz|zip)$/, \"\"));\n\t\tconst extractedBinaryCandidates = [join(extractedDir, binaryFileName), join(extractDir, binaryFileName)];\n\t\tlet extractedBinary = extractedBinaryCandidates.find((candidate) => existsSync(candidate));\n\n\t\tif (!extractedBinary) {\n\t\t\textractedBinary = findBinaryRecursively(extractDir, binaryFileName) ?? undefined;\n\t\t}\n\n\t\tif (extractedBinary) {\n\t\t\trenameSync(extractedBinary, binaryPath);\n\t\t} else {\n\t\t\tthrow new Error(`Binary not found in archive: expected ${binaryFileName} under ${extractDir}`);\n\t\t}\n\n\t\t// Make executable (Unix only)\n\t\tif (plat !== \"win32\") {\n\t\t\tchmodSync(binaryPath, 0o755);\n\t\t}\n\t} finally {\n\t\t// Guaranteed cleanup of every transient artifact on ANY outcome: the temp\n\t\t// download (if a failure left it before the rename), the published archive,\n\t\t// and the temp extract dir.\n\t\trmSync(tempArchivePath, { force: true });\n\t\trmSync(archivePath, { force: true });\n\t\trmSync(extractDir, { recursive: true, force: true });\n\t}\n\n\treturn binaryPath;\n}\n\n// Termux package names for tools\nconst TERMUX_PACKAGES: Record<string, string> = {\n\tfd: \"fd\",\n\trg: \"ripgrep\",\n\twebtools: \"webtools\",\n\tvoicetools: \"voicetools\",\n\tembsearch: \"embsearch\",\n};\n\n// Ensure a tool is available, downloading if necessary\n// Returns the path to the tool, or null if unavailable\n// onProgress receives live byte counts while the release archive downloads.\nexport async function ensureTool(\n\ttool: ManagedTool,\n\tsilent: boolean = false,\n\tonProgress?: DownloadProgress,\n): Promise<string | undefined> {\n\tconst existingPath = getToolPath(tool);\n\tif (existingPath) {\n\t\treturn existingPath;\n\t}\n\n\tconst config = TOOLS[tool];\n\tif (!config) return undefined;\n\n\tif (isOfflineModeEnabled()) {\n\t\tif (!silent) {\n\t\t\tconsole.log(chalk.yellow(`${config.name} not found. Offline mode enabled, skipping download.`));\n\t\t}\n\t\treturn undefined;\n\t}\n\n\t// On Android/Termux, Linux binaries don't work due to Bionic libc incompatibility.\n\t// Users must install via pkg.\n\tif (platform() === \"android\") {\n\t\tconst pkgName = TERMUX_PACKAGES[tool] ?? tool;\n\t\tif (!silent) {\n\t\t\tconsole.log(chalk.yellow(`${config.name} not found. Install with: pkg install ${pkgName}`));\n\t\t}\n\t\treturn undefined;\n\t}\n\n\t// Tool not found - download it\n\tif (!silent) {\n\t\tconsole.log(chalk.dim(`${config.name} not found. Downloading...`));\n\t}\n\n\ttry {\n\t\tconst path = await downloadTool(tool, onProgress);\n\t\tif (!silent) {\n\t\t\tconsole.log(chalk.dim(`${config.name} installed to ${path}`));\n\t\t}\n\t\treturn path;\n\t} catch (e) {\n\t\tif (!silent) {\n\t\t\tconsole.log(chalk.yellow(`Failed to download ${config.name}: ${e instanceof Error ? e.message : e}`));\n\t\t}\n\t\treturn undefined;\n\t}\n}\n"]}
@@ -149,8 +149,14 @@ function commandExists(cmd) {
149
149
  // successful resolution so we never re-run the synchronous spawnSync probe in
150
150
  // commandExists() on every grep/find/glob invocation, which blocks the event loop.
151
151
  const resolvedToolPathCache = new Map();
152
- // Get the path to a tool (system-wide or in our tools dir)
153
- function getToolPath(tool) {
152
+ /**
153
+ * Path to an already-available tool (system-wide or in our tools dir), or null.
154
+ *
155
+ * Exported as the never-downloads counterpart to {@link ensureTool}: a caller
156
+ * that wants a capability *if it happens to be installed* — and must not spend a
157
+ * download to get it — asks here and degrades when the answer is null.
158
+ */
159
+ export function getToolPath(tool) {
154
160
  const config = TOOLS[tool];
155
161
  if (!config)
156
162
  return null;
@@ -1 +1 @@
1
- {"version":3,"file":"tools-manager.js","sourceRoot":"","sources":["../../src/utils/tools-manager.ts"],"names":[],"mappings":"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","sourcesContent":["import chalk from \"chalk\";\nimport { spawnSync } from \"child_process\";\nimport { createHash, randomBytes } from \"crypto\";\nimport extractZip from \"extract-zip\";\nimport {\n\tchmodSync,\n\tcreateWriteStream,\n\texistsSync,\n\tmkdirSync,\n\treaddirSync,\n\treadFileSync,\n\trenameSync,\n\trmSync,\n\tstatSync,\n} from \"fs\";\nimport { arch, platform } from \"os\";\nimport { join } from \"path\";\nimport { Readable } from \"stream\";\nimport { pipeline } from \"stream/promises\";\nimport { APP_NAME, getBinDir } from \"../config.js\";\n\nconst TOOLS_DIR = getBinDir();\nconst NETWORK_TIMEOUT_MS = 10_000;\nconst DOWNLOAD_TIMEOUT_MS = 120_000;\n\nfunction isOfflineModeEnabled(): boolean {\n\tconst value = process.env.HOOCODE_OFFLINE;\n\tif (!value) return false;\n\treturn value === \"1\" || value.toLowerCase() === \"true\" || value.toLowerCase() === \"yes\";\n}\n\n/** Tools whose binaries hoocode can resolve from PATH or download on demand. */\nexport type ManagedTool = \"fd\" | \"rg\" | \"webtools\" | \"voicetools\" | \"embsearch\";\n\ninterface ToolConfig {\n\tname: string;\n\trepo: string; // GitHub repo (e.g., \"sharkdp/fd\")\n\tbinaryName: string; // Name of the binary inside the archive\n\tsystemBinaryNames?: string[]; // Alternative system command names to try before downloading\n\ttagPrefix: string; // Prefix for tags (e.g., \"v\" for v1.0.0, \"\" for 1.0.0)\n\tgetAssetName: (version: string, plat: string, architecture: string) => string | null;\n}\n\nconst TOOLS: Record<string, ToolConfig> = {\n\tfd: {\n\t\tname: \"fd\",\n\t\trepo: \"sharkdp/fd\",\n\t\tbinaryName: \"fd\",\n\t\tsystemBinaryNames: [\"fd\", \"fdfind\"],\n\t\ttagPrefix: \"v\",\n\t\tgetAssetName: (version, plat, architecture) => {\n\t\t\tif (plat === \"darwin\") {\n\t\t\t\tconst archStr = architecture === \"arm64\" ? \"aarch64\" : \"x86_64\";\n\t\t\t\treturn `fd-v${version}-${archStr}-apple-darwin.tar.gz`;\n\t\t\t} else if (plat === \"linux\") {\n\t\t\t\tconst archStr = architecture === \"arm64\" ? \"aarch64\" : \"x86_64\";\n\t\t\t\treturn `fd-v${version}-${archStr}-unknown-linux-gnu.tar.gz`;\n\t\t\t} else if (plat === \"win32\") {\n\t\t\t\tconst archStr = architecture === \"arm64\" ? \"aarch64\" : \"x86_64\";\n\t\t\t\treturn `fd-v${version}-${archStr}-pc-windows-msvc.zip`;\n\t\t\t}\n\t\t\treturn null;\n\t\t},\n\t},\n\trg: {\n\t\tname: \"ripgrep\",\n\t\trepo: \"BurntSushi/ripgrep\",\n\t\tbinaryName: \"rg\",\n\t\ttagPrefix: \"\",\n\t\tgetAssetName: (version, plat, architecture) => {\n\t\t\tif (plat === \"darwin\") {\n\t\t\t\tconst archStr = architecture === \"arm64\" ? \"aarch64\" : \"x86_64\";\n\t\t\t\treturn `ripgrep-${version}-${archStr}-apple-darwin.tar.gz`;\n\t\t\t} else if (plat === \"linux\") {\n\t\t\t\tif (architecture === \"arm64\") {\n\t\t\t\t\treturn `ripgrep-${version}-aarch64-unknown-linux-gnu.tar.gz`;\n\t\t\t\t}\n\t\t\t\treturn `ripgrep-${version}-x86_64-unknown-linux-musl.tar.gz`;\n\t\t\t} else if (plat === \"win32\") {\n\t\t\t\tconst archStr = architecture === \"arm64\" ? \"aarch64\" : \"x86_64\";\n\t\t\t\treturn `ripgrep-${version}-${archStr}-pc-windows-msvc.zip`;\n\t\t\t}\n\t\t\treturn null;\n\t\t},\n\t},\n\twebtools: {\n\t\tname: \"webtools\",\n\t\trepo: \"kolisachint/webtools\",\n\t\tbinaryName: \"webtools\",\n\t\ttagPrefix: \"v\",\n\t\t// Release assets follow Rust target triples: webtools-<arch>-<target>.<ext>.\n\t\t// Some platforms may not be published yet; a missing asset 404s and ensureTool\n\t\t// degrades gracefully (returns undefined, tools fall back to an error message).\n\t\tgetAssetName: (_version, plat, architecture) => {\n\t\t\tconst archStr = architecture === \"arm64\" ? \"aarch64\" : \"x86_64\";\n\t\t\tif (plat === \"darwin\") {\n\t\t\t\treturn `webtools-${archStr}-apple-darwin.tar.gz`;\n\t\t\t} else if (plat === \"linux\") {\n\t\t\t\t// The upstream release matrix publishes aarch64 Linux as a musl\n\t\t\t\t// build and x86_64 as gnu; asking for aarch64-gnu only ever 404s.\n\t\t\t\tconst libc = architecture === \"arm64\" ? \"musl\" : \"gnu\";\n\t\t\t\treturn `webtools-${archStr}-unknown-linux-${libc}.tar.gz`;\n\t\t\t} else if (plat === \"win32\") {\n\t\t\t\treturn `webtools-${archStr}-pc-windows-msvc.zip`;\n\t\t\t}\n\t\t\treturn null;\n\t\t},\n\t},\n\tvoicetools: {\n\t\tname: \"voicetools\",\n\t\trepo: \"kolisachint/voicetools\",\n\t\tbinaryName: \"voicetools\",\n\t\ttagPrefix: \"v\",\n\t\t// Release archives follow Rust target triples: voicetools-<arch>-<target>.<ext>.\n\t\t// A missing platform asset 404s and ensureTool degrades gracefully (returns\n\t\t// undefined; the voice-transcribe caller then surfaces an error message).\n\t\tgetAssetName: (_version, plat, architecture) => {\n\t\t\tconst archStr = architecture === \"arm64\" ? \"aarch64\" : \"x86_64\";\n\t\t\tif (plat === \"darwin\") {\n\t\t\t\treturn `voicetools-${archStr}-apple-darwin.tar.gz`;\n\t\t\t} else if (plat === \"linux\") {\n\t\t\t\treturn `voicetools-${archStr}-unknown-linux-gnu.tar.gz`;\n\t\t\t} else if (plat === \"win32\") {\n\t\t\t\treturn `voicetools-${archStr}-pc-windows-msvc.zip`;\n\t\t\t}\n\t\t\treturn null;\n\t\t},\n\t},\n\tembsearch: {\n\t\tname: \"embsearch\",\n\t\trepo: \"kolisachint/embeddingsearchtools\",\n\t\tbinaryName: \"embsearch\",\n\t\ttagPrefix: \"v\",\n\t\t// Release archives follow Rust target triples: embsearch-<arch>-<target>.<ext>.\n\t\t// Must be the ONNX build (model bundled); the mock build is rejected by the\n\t\t// EmbsearchService. A missing platform asset 404s and ensureTool degrades\n\t\t// gracefully (returns undefined; the search tool stays lexical-only).\n\t\tgetAssetName: (_version, plat, architecture) => {\n\t\t\tconst archStr = architecture === \"arm64\" ? \"aarch64\" : \"x86_64\";\n\t\t\tif (plat === \"darwin\") {\n\t\t\t\treturn `embsearch-${archStr}-apple-darwin.tar.gz`;\n\t\t\t} else if (plat === \"linux\") {\n\t\t\t\treturn `embsearch-${archStr}-unknown-linux-gnu.tar.gz`;\n\t\t\t} else if (plat === \"win32\") {\n\t\t\t\treturn `embsearch-${archStr}-pc-windows-msvc.zip`;\n\t\t\t}\n\t\t\treturn null;\n\t\t},\n\t},\n};\n\n// Check if a command exists in PATH by trying to run it\nfunction commandExists(cmd: string): boolean {\n\ttry {\n\t\tconst result = spawnSync(cmd, [\"--version\"], { stdio: \"pipe\" });\n\t\t// Check for ENOENT error (command not found)\n\t\treturn result.error === undefined || result.error === null;\n\t} catch {\n\t\treturn false;\n\t}\n}\n\n// Resolved tool paths are stable for the life of the process. Cache the first\n// successful resolution so we never re-run the synchronous spawnSync probe in\n// commandExists() on every grep/find/glob invocation, which blocks the event loop.\nconst resolvedToolPathCache = new Map<ManagedTool, string>();\n\n// Get the path to a tool (system-wide or in our tools dir)\nfunction getToolPath(tool: ManagedTool): string | null {\n\tconst config = TOOLS[tool];\n\tif (!config) return null;\n\n\t// Explicit binary override (per-tool env var, e.g. HOOCODE_WEBTOOLS_BINARY).\n\t// Lets a developer point at a locally built binary that predates a release,\n\t// bypassing the tools-dir/PATH resolution and download. Authoritative when set\n\t// and the path exists.\n\tconst overrideEnv = `HOOCODE_${tool.toUpperCase()}_BINARY`;\n\tconst override = process.env[overrideEnv]?.trim();\n\tif (override && existsSync(override)) {\n\t\treturn override;\n\t}\n\n\t// Reuse a previously resolved path. A bare command name resolves via PATH;\n\t// an absolute path must still exist (revalidate cheaply with existsSync).\n\tconst cached = resolvedToolPathCache.get(tool);\n\tif (cached !== undefined) {\n\t\tconst isAbsolutePath = cached.includes(\"/\") || cached.includes(\"\\\\\");\n\t\tif (!isAbsolutePath || existsSync(cached)) {\n\t\t\treturn cached;\n\t\t}\n\t\tresolvedToolPathCache.delete(tool);\n\t}\n\n\t// Check our tools directory first\n\tconst localPath = join(TOOLS_DIR, config.binaryName + (platform() === \"win32\" ? \".exe\" : \"\"));\n\tif (existsSync(localPath)) {\n\t\tresolvedToolPathCache.set(tool, localPath);\n\t\treturn localPath;\n\t}\n\n\t// Check system PATH - if found, just return the command name (it's in PATH)\n\tconst systemBinaryNames = config.systemBinaryNames ?? [config.binaryName];\n\tfor (const systemBinaryName of systemBinaryNames) {\n\t\tif (commandExists(systemBinaryName)) {\n\t\t\tresolvedToolPathCache.set(tool, systemBinaryName);\n\t\t\treturn systemBinaryName;\n\t\t}\n\t}\n\n\treturn null;\n}\n\n// Fetch latest release version from GitHub\nasync function getLatestVersion(repo: string): Promise<string> {\n\tconst response = await fetch(`https://api.github.com/repos/${repo}/releases/latest`, {\n\t\theaders: { \"User-Agent\": `${APP_NAME}-coding-agent` },\n\t\tsignal: AbortSignal.timeout(NETWORK_TIMEOUT_MS),\n\t});\n\n\tif (!response.ok) {\n\t\tthrow new Error(`GitHub API error: ${response.status}`);\n\t}\n\n\tconst data = (await response.json()) as { tag_name: string };\n\treturn data.tag_name.replace(/^v/, \"\");\n}\n\n// Best-effort SHA-256 verification: fetch \"<downloadUrl>.sha256\" and, when it is\n// served (HTTP 200), verify the downloaded file against it. A 404 (or any other\n// non-200 / network error) means no published checksum, so verification is\n// skipped rather than treated as a failure. A genuine mismatch throws.\nasync function verifyChecksum(downloadUrl: string, filePath: string): Promise<void> {\n\tlet checksumResponse: Awaited<ReturnType<typeof fetch>>;\n\ttry {\n\t\tchecksumResponse = await fetch(`${downloadUrl}.sha256`, {\n\t\t\tsignal: AbortSignal.timeout(NETWORK_TIMEOUT_MS),\n\t\t});\n\t} catch {\n\t\t// Network error fetching the checksum is non-fatal for best-effort verification.\n\t\treturn;\n\t}\n\n\tif (checksumResponse.status !== 200) {\n\t\treturn;\n\t}\n\n\t// sha256 files are commonly \"<hex> <filename>\"; take the leading token.\n\tconst expectedHash = (await checksumResponse.text()).trim().split(/\\s+/)[0]?.toLowerCase();\n\tif (!expectedHash || !/^[0-9a-f]{64}$/.test(expectedHash)) {\n\t\t// Unusable checksum body: skip rather than fail (still best-effort).\n\t\treturn;\n\t}\n\n\tconst actualHash = createHash(\"sha256\").update(readFileSync(filePath)).digest(\"hex\");\n\tif (actualHash !== expectedHash) {\n\t\tthrow new Error(`Checksum mismatch for ${downloadUrl}: expected ${expectedHash}, got ${actualHash}`);\n\t}\n}\n\n/**\n * Live download progress: bytes received so far and the total from\n * Content-Length (null when the server doesn't send one). Callers use it to\n * drive a progress bar instead of an indefinite spinner.\n */\nexport type DownloadProgress = (receivedBytes: number, totalBytes: number | null) => void;\n\n// Download a file from URL into `dest`, validating integrity. Throws (and removes\n// the partial file) on a truncated transfer (bytes written != Content-Length when\n// the header is present) or a SHA-256 mismatch, so a corrupt artifact is never\n// left behind. Exported for tests.\nexport async function downloadFile(url: string, dest: string, onProgress?: DownloadProgress): Promise<void> {\n\ttry {\n\t\tconst response = await fetch(url, {\n\t\t\tsignal: AbortSignal.timeout(DOWNLOAD_TIMEOUT_MS),\n\t\t});\n\n\t\tif (!response.ok) {\n\t\t\tthrow new Error(`Failed to download: ${response.status}`);\n\t\t}\n\n\t\tif (!response.body) {\n\t\t\tthrow new Error(\"No response body\");\n\t\t}\n\n\t\tconst contentLengthHeader = response.headers.get(\"content-length\");\n\t\tconst expectedBytes =\n\t\t\tcontentLengthHeader !== null && contentLengthHeader.trim() !== \"\" ? Number(contentLengthHeader) : null;\n\n\t\tconst fileStream = createWriteStream(dest);\n\t\tconst source = Readable.fromWeb(response.body as Parameters<typeof Readable.fromWeb>[0]);\n\t\tif (onProgress) {\n\t\t\tlet received = 0;\n\t\t\tsource.on(\"data\", (chunk: Buffer) => {\n\t\t\t\treceived += chunk.length;\n\t\t\t\tonProgress(received, expectedBytes);\n\t\t\t});\n\t\t}\n\t\tawait pipeline(source, fileStream);\n\n\t\tif (expectedBytes !== null && Number.isFinite(expectedBytes)) {\n\t\t\tconst bytesWritten = statSync(dest).size;\n\t\t\tif (bytesWritten !== expectedBytes) {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t`Truncated download from ${url}: expected ${expectedBytes} bytes, received ${bytesWritten}`,\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\n\t\tawait verifyChecksum(url, dest);\n\t} catch (e) {\n\t\t// Never leave a partial/corrupt file behind on any failure.\n\t\trmSync(dest, { force: true });\n\t\tthrow e;\n\t}\n}\n\nfunction findBinaryRecursively(rootDir: string, binaryFileName: string): string | null {\n\tconst stack: string[] = [rootDir];\n\n\twhile (stack.length > 0) {\n\t\tconst currentDir = stack.pop();\n\t\tif (!currentDir) continue;\n\n\t\tconst entries = readdirSync(currentDir, { withFileTypes: true });\n\t\tfor (const entry of entries) {\n\t\t\tconst fullPath = join(currentDir, entry.name);\n\t\t\tif (entry.isFile() && entry.name === binaryFileName) {\n\t\t\t\treturn fullPath;\n\t\t\t}\n\t\t\tif (entry.isDirectory()) {\n\t\t\t\tstack.push(fullPath);\n\t\t\t}\n\t\t}\n\t}\n\n\treturn null;\n}\n\n// Download and install a tool\nasync function downloadTool(tool: ManagedTool, onProgress?: DownloadProgress): Promise<string> {\n\tconst config = TOOLS[tool];\n\tif (!config) throw new Error(`Unknown tool: ${tool}`);\n\n\tconst plat = platform();\n\tconst architecture = arch();\n\n\t// Get latest version\n\tconst version = await getLatestVersion(config.repo);\n\n\t// Get asset name for this platform\n\tconst assetName = config.getAssetName(version, plat, architecture);\n\tif (!assetName) {\n\t\tthrow new Error(`Unsupported platform: ${plat}/${architecture}`);\n\t}\n\n\t// Create tools directory\n\tmkdirSync(TOOLS_DIR, { recursive: true });\n\n\tconst downloadUrl = `https://github.com/${config.repo}/releases/download/${config.tagPrefix}${version}/${assetName}`;\n\tconst archivePath = join(TOOLS_DIR, assetName);\n\tconst binaryExt = plat === \"win32\" ? \".exe\" : \"\";\n\tconst binaryPath = join(TOOLS_DIR, config.binaryName + binaryExt);\n\n\t// Download to a unique temp path, validate, then atomically rename into place.\n\t// Writing the shared archive path directly would leave a corrupt partial behind\n\t// if the transfer fails or is truncated. fd/rg/webtools can also download\n\t// concurrently at startup, so the per-attempt temp name must be unique.\n\tconst tempArchivePath = `${archivePath}.${process.pid}.${randomBytes(6).toString(\"hex\")}.part`;\n\n\t// Extract into a unique temp directory. fd and rg downloads can run concurrently\n\t// during startup, so sharing a fixed directory causes races.\n\tconst extractDir = join(\n\t\tTOOLS_DIR,\n\t\t`extract_tmp_${config.binaryName}_${process.pid}_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`,\n\t);\n\n\ttry {\n\t\t// One retry (2 attempts total) around download + integrity verification.\n\t\t// downloadFile removes its own partial on failure, so each attempt is clean.\n\t\tlet lastError: unknown;\n\t\tlet downloaded = false;\n\t\tfor (let attempt = 1; attempt <= 2 && !downloaded; attempt++) {\n\t\t\ttry {\n\t\t\t\tawait downloadFile(downloadUrl, tempArchivePath, onProgress);\n\t\t\t\tdownloaded = true;\n\t\t\t} catch (e) {\n\t\t\t\tlastError = e;\n\t\t\t\trmSync(tempArchivePath, { force: true });\n\t\t\t}\n\t\t}\n\t\tif (!downloaded) {\n\t\t\tthrow lastError instanceof Error ? lastError : new Error(String(lastError));\n\t\t}\n\n\t\t// Atomic publish of the verified archive, then extract.\n\t\trenameSync(tempArchivePath, archivePath);\n\t\tmkdirSync(extractDir, { recursive: true });\n\n\t\tif (assetName.endsWith(\".tar.gz\")) {\n\t\t\tconst extractResult = spawnSync(\"tar\", [\"xzf\", archivePath, \"-C\", extractDir], { stdio: \"pipe\" });\n\t\t\tif (extractResult.error || extractResult.status !== 0) {\n\t\t\t\tconst errMsg = extractResult.error?.message ?? extractResult.stderr?.toString().trim() ?? \"unknown error\";\n\t\t\t\tthrow new Error(`Failed to extract ${assetName}: ${errMsg}`);\n\t\t\t}\n\t\t} else if (assetName.endsWith(\".zip\")) {\n\t\t\tawait extractZip(archivePath, { dir: extractDir });\n\t\t} else {\n\t\t\tthrow new Error(`Unsupported archive format: ${assetName}`);\n\t\t}\n\n\t\t// Find the binary in extracted files. Some archives contain files directly\n\t\t// at root, others nest under a versioned subdirectory.\n\t\tconst binaryFileName = config.binaryName + binaryExt;\n\t\tconst extractedDir = join(extractDir, assetName.replace(/\\.(tar\\.gz|zip)$/, \"\"));\n\t\tconst extractedBinaryCandidates = [join(extractedDir, binaryFileName), join(extractDir, binaryFileName)];\n\t\tlet extractedBinary = extractedBinaryCandidates.find((candidate) => existsSync(candidate));\n\n\t\tif (!extractedBinary) {\n\t\t\textractedBinary = findBinaryRecursively(extractDir, binaryFileName) ?? undefined;\n\t\t}\n\n\t\tif (extractedBinary) {\n\t\t\trenameSync(extractedBinary, binaryPath);\n\t\t} else {\n\t\t\tthrow new Error(`Binary not found in archive: expected ${binaryFileName} under ${extractDir}`);\n\t\t}\n\n\t\t// Make executable (Unix only)\n\t\tif (plat !== \"win32\") {\n\t\t\tchmodSync(binaryPath, 0o755);\n\t\t}\n\t} finally {\n\t\t// Guaranteed cleanup of every transient artifact on ANY outcome: the temp\n\t\t// download (if a failure left it before the rename), the published archive,\n\t\t// and the temp extract dir.\n\t\trmSync(tempArchivePath, { force: true });\n\t\trmSync(archivePath, { force: true });\n\t\trmSync(extractDir, { recursive: true, force: true });\n\t}\n\n\treturn binaryPath;\n}\n\n// Termux package names for tools\nconst TERMUX_PACKAGES: Record<string, string> = {\n\tfd: \"fd\",\n\trg: \"ripgrep\",\n\twebtools: \"webtools\",\n\tvoicetools: \"voicetools\",\n\tembsearch: \"embsearch\",\n};\n\n// Ensure a tool is available, downloading if necessary\n// Returns the path to the tool, or null if unavailable\n// onProgress receives live byte counts while the release archive downloads.\nexport async function ensureTool(\n\ttool: ManagedTool,\n\tsilent: boolean = false,\n\tonProgress?: DownloadProgress,\n): Promise<string | undefined> {\n\tconst existingPath = getToolPath(tool);\n\tif (existingPath) {\n\t\treturn existingPath;\n\t}\n\n\tconst config = TOOLS[tool];\n\tif (!config) return undefined;\n\n\tif (isOfflineModeEnabled()) {\n\t\tif (!silent) {\n\t\t\tconsole.log(chalk.yellow(`${config.name} not found. Offline mode enabled, skipping download.`));\n\t\t}\n\t\treturn undefined;\n\t}\n\n\t// On Android/Termux, Linux binaries don't work due to Bionic libc incompatibility.\n\t// Users must install via pkg.\n\tif (platform() === \"android\") {\n\t\tconst pkgName = TERMUX_PACKAGES[tool] ?? tool;\n\t\tif (!silent) {\n\t\t\tconsole.log(chalk.yellow(`${config.name} not found. Install with: pkg install ${pkgName}`));\n\t\t}\n\t\treturn undefined;\n\t}\n\n\t// Tool not found - download it\n\tif (!silent) {\n\t\tconsole.log(chalk.dim(`${config.name} not found. Downloading...`));\n\t}\n\n\ttry {\n\t\tconst path = await downloadTool(tool, onProgress);\n\t\tif (!silent) {\n\t\t\tconsole.log(chalk.dim(`${config.name} installed to ${path}`));\n\t\t}\n\t\treturn path;\n\t} catch (e) {\n\t\tif (!silent) {\n\t\t\tconsole.log(chalk.yellow(`Failed to download ${config.name}: ${e instanceof Error ? e.message : e}`));\n\t\t}\n\t\treturn undefined;\n\t}\n}\n"]}
1
+ {"version":3,"file":"tools-manager.js","sourceRoot":"","sources":["../../src/utils/tools-manager.ts"],"names":[],"mappings":"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","sourcesContent":["import chalk from \"chalk\";\nimport { spawnSync } from \"child_process\";\nimport { createHash, randomBytes } from \"crypto\";\nimport extractZip from \"extract-zip\";\nimport {\n\tchmodSync,\n\tcreateWriteStream,\n\texistsSync,\n\tmkdirSync,\n\treaddirSync,\n\treadFileSync,\n\trenameSync,\n\trmSync,\n\tstatSync,\n} from \"fs\";\nimport { arch, platform } from \"os\";\nimport { join } from \"path\";\nimport { Readable } from \"stream\";\nimport { pipeline } from \"stream/promises\";\nimport { APP_NAME, getBinDir } from \"../config.js\";\n\nconst TOOLS_DIR = getBinDir();\nconst NETWORK_TIMEOUT_MS = 10_000;\nconst DOWNLOAD_TIMEOUT_MS = 120_000;\n\nfunction isOfflineModeEnabled(): boolean {\n\tconst value = process.env.HOOCODE_OFFLINE;\n\tif (!value) return false;\n\treturn value === \"1\" || value.toLowerCase() === \"true\" || value.toLowerCase() === \"yes\";\n}\n\n/** Tools whose binaries hoocode can resolve from PATH or download on demand. */\nexport type ManagedTool = \"fd\" | \"rg\" | \"webtools\" | \"voicetools\" | \"embsearch\";\n\ninterface ToolConfig {\n\tname: string;\n\trepo: string; // GitHub repo (e.g., \"sharkdp/fd\")\n\tbinaryName: string; // Name of the binary inside the archive\n\tsystemBinaryNames?: string[]; // Alternative system command names to try before downloading\n\ttagPrefix: string; // Prefix for tags (e.g., \"v\" for v1.0.0, \"\" for 1.0.0)\n\tgetAssetName: (version: string, plat: string, architecture: string) => string | null;\n}\n\nconst TOOLS: Record<string, ToolConfig> = {\n\tfd: {\n\t\tname: \"fd\",\n\t\trepo: \"sharkdp/fd\",\n\t\tbinaryName: \"fd\",\n\t\tsystemBinaryNames: [\"fd\", \"fdfind\"],\n\t\ttagPrefix: \"v\",\n\t\tgetAssetName: (version, plat, architecture) => {\n\t\t\tif (plat === \"darwin\") {\n\t\t\t\tconst archStr = architecture === \"arm64\" ? \"aarch64\" : \"x86_64\";\n\t\t\t\treturn `fd-v${version}-${archStr}-apple-darwin.tar.gz`;\n\t\t\t} else if (plat === \"linux\") {\n\t\t\t\tconst archStr = architecture === \"arm64\" ? \"aarch64\" : \"x86_64\";\n\t\t\t\treturn `fd-v${version}-${archStr}-unknown-linux-gnu.tar.gz`;\n\t\t\t} else if (plat === \"win32\") {\n\t\t\t\tconst archStr = architecture === \"arm64\" ? \"aarch64\" : \"x86_64\";\n\t\t\t\treturn `fd-v${version}-${archStr}-pc-windows-msvc.zip`;\n\t\t\t}\n\t\t\treturn null;\n\t\t},\n\t},\n\trg: {\n\t\tname: \"ripgrep\",\n\t\trepo: \"BurntSushi/ripgrep\",\n\t\tbinaryName: \"rg\",\n\t\ttagPrefix: \"\",\n\t\tgetAssetName: (version, plat, architecture) => {\n\t\t\tif (plat === \"darwin\") {\n\t\t\t\tconst archStr = architecture === \"arm64\" ? \"aarch64\" : \"x86_64\";\n\t\t\t\treturn `ripgrep-${version}-${archStr}-apple-darwin.tar.gz`;\n\t\t\t} else if (plat === \"linux\") {\n\t\t\t\tif (architecture === \"arm64\") {\n\t\t\t\t\treturn `ripgrep-${version}-aarch64-unknown-linux-gnu.tar.gz`;\n\t\t\t\t}\n\t\t\t\treturn `ripgrep-${version}-x86_64-unknown-linux-musl.tar.gz`;\n\t\t\t} else if (plat === \"win32\") {\n\t\t\t\tconst archStr = architecture === \"arm64\" ? \"aarch64\" : \"x86_64\";\n\t\t\t\treturn `ripgrep-${version}-${archStr}-pc-windows-msvc.zip`;\n\t\t\t}\n\t\t\treturn null;\n\t\t},\n\t},\n\twebtools: {\n\t\tname: \"webtools\",\n\t\trepo: \"kolisachint/webtools\",\n\t\tbinaryName: \"webtools\",\n\t\ttagPrefix: \"v\",\n\t\t// Release assets follow Rust target triples: webtools-<arch>-<target>.<ext>.\n\t\t// Some platforms may not be published yet; a missing asset 404s and ensureTool\n\t\t// degrades gracefully (returns undefined, tools fall back to an error message).\n\t\tgetAssetName: (_version, plat, architecture) => {\n\t\t\tconst archStr = architecture === \"arm64\" ? \"aarch64\" : \"x86_64\";\n\t\t\tif (plat === \"darwin\") {\n\t\t\t\treturn `webtools-${archStr}-apple-darwin.tar.gz`;\n\t\t\t} else if (plat === \"linux\") {\n\t\t\t\t// The upstream release matrix publishes aarch64 Linux as a musl\n\t\t\t\t// build and x86_64 as gnu; asking for aarch64-gnu only ever 404s.\n\t\t\t\tconst libc = architecture === \"arm64\" ? \"musl\" : \"gnu\";\n\t\t\t\treturn `webtools-${archStr}-unknown-linux-${libc}.tar.gz`;\n\t\t\t} else if (plat === \"win32\") {\n\t\t\t\treturn `webtools-${archStr}-pc-windows-msvc.zip`;\n\t\t\t}\n\t\t\treturn null;\n\t\t},\n\t},\n\tvoicetools: {\n\t\tname: \"voicetools\",\n\t\trepo: \"kolisachint/voicetools\",\n\t\tbinaryName: \"voicetools\",\n\t\ttagPrefix: \"v\",\n\t\t// Release archives follow Rust target triples: voicetools-<arch>-<target>.<ext>.\n\t\t// A missing platform asset 404s and ensureTool degrades gracefully (returns\n\t\t// undefined; the voice-transcribe caller then surfaces an error message).\n\t\tgetAssetName: (_version, plat, architecture) => {\n\t\t\tconst archStr = architecture === \"arm64\" ? \"aarch64\" : \"x86_64\";\n\t\t\tif (plat === \"darwin\") {\n\t\t\t\treturn `voicetools-${archStr}-apple-darwin.tar.gz`;\n\t\t\t} else if (plat === \"linux\") {\n\t\t\t\treturn `voicetools-${archStr}-unknown-linux-gnu.tar.gz`;\n\t\t\t} else if (plat === \"win32\") {\n\t\t\t\treturn `voicetools-${archStr}-pc-windows-msvc.zip`;\n\t\t\t}\n\t\t\treturn null;\n\t\t},\n\t},\n\tembsearch: {\n\t\tname: \"embsearch\",\n\t\trepo: \"kolisachint/embeddingsearchtools\",\n\t\tbinaryName: \"embsearch\",\n\t\ttagPrefix: \"v\",\n\t\t// Release archives follow Rust target triples: embsearch-<arch>-<target>.<ext>.\n\t\t// Must be the ONNX build (model bundled); the mock build is rejected by the\n\t\t// EmbsearchService. A missing platform asset 404s and ensureTool degrades\n\t\t// gracefully (returns undefined; the search tool stays lexical-only).\n\t\tgetAssetName: (_version, plat, architecture) => {\n\t\t\tconst archStr = architecture === \"arm64\" ? \"aarch64\" : \"x86_64\";\n\t\t\tif (plat === \"darwin\") {\n\t\t\t\treturn `embsearch-${archStr}-apple-darwin.tar.gz`;\n\t\t\t} else if (plat === \"linux\") {\n\t\t\t\treturn `embsearch-${archStr}-unknown-linux-gnu.tar.gz`;\n\t\t\t} else if (plat === \"win32\") {\n\t\t\t\treturn `embsearch-${archStr}-pc-windows-msvc.zip`;\n\t\t\t}\n\t\t\treturn null;\n\t\t},\n\t},\n};\n\n// Check if a command exists in PATH by trying to run it\nfunction commandExists(cmd: string): boolean {\n\ttry {\n\t\tconst result = spawnSync(cmd, [\"--version\"], { stdio: \"pipe\" });\n\t\t// Check for ENOENT error (command not found)\n\t\treturn result.error === undefined || result.error === null;\n\t} catch {\n\t\treturn false;\n\t}\n}\n\n// Resolved tool paths are stable for the life of the process. Cache the first\n// successful resolution so we never re-run the synchronous spawnSync probe in\n// commandExists() on every grep/find/glob invocation, which blocks the event loop.\nconst resolvedToolPathCache = new Map<ManagedTool, string>();\n\n/**\n * Path to an already-available tool (system-wide or in our tools dir), or null.\n *\n * Exported as the never-downloads counterpart to {@link ensureTool}: a caller\n * that wants a capability *if it happens to be installed* — and must not spend a\n * download to get it — asks here and degrades when the answer is null.\n */\nexport function getToolPath(tool: ManagedTool): string | null {\n\tconst config = TOOLS[tool];\n\tif (!config) return null;\n\n\t// Explicit binary override (per-tool env var, e.g. HOOCODE_WEBTOOLS_BINARY).\n\t// Lets a developer point at a locally built binary that predates a release,\n\t// bypassing the tools-dir/PATH resolution and download. Authoritative when set\n\t// and the path exists.\n\tconst overrideEnv = `HOOCODE_${tool.toUpperCase()}_BINARY`;\n\tconst override = process.env[overrideEnv]?.trim();\n\tif (override && existsSync(override)) {\n\t\treturn override;\n\t}\n\n\t// Reuse a previously resolved path. A bare command name resolves via PATH;\n\t// an absolute path must still exist (revalidate cheaply with existsSync).\n\tconst cached = resolvedToolPathCache.get(tool);\n\tif (cached !== undefined) {\n\t\tconst isAbsolutePath = cached.includes(\"/\") || cached.includes(\"\\\\\");\n\t\tif (!isAbsolutePath || existsSync(cached)) {\n\t\t\treturn cached;\n\t\t}\n\t\tresolvedToolPathCache.delete(tool);\n\t}\n\n\t// Check our tools directory first\n\tconst localPath = join(TOOLS_DIR, config.binaryName + (platform() === \"win32\" ? \".exe\" : \"\"));\n\tif (existsSync(localPath)) {\n\t\tresolvedToolPathCache.set(tool, localPath);\n\t\treturn localPath;\n\t}\n\n\t// Check system PATH - if found, just return the command name (it's in PATH)\n\tconst systemBinaryNames = config.systemBinaryNames ?? [config.binaryName];\n\tfor (const systemBinaryName of systemBinaryNames) {\n\t\tif (commandExists(systemBinaryName)) {\n\t\t\tresolvedToolPathCache.set(tool, systemBinaryName);\n\t\t\treturn systemBinaryName;\n\t\t}\n\t}\n\n\treturn null;\n}\n\n// Fetch latest release version from GitHub\nasync function getLatestVersion(repo: string): Promise<string> {\n\tconst response = await fetch(`https://api.github.com/repos/${repo}/releases/latest`, {\n\t\theaders: { \"User-Agent\": `${APP_NAME}-coding-agent` },\n\t\tsignal: AbortSignal.timeout(NETWORK_TIMEOUT_MS),\n\t});\n\n\tif (!response.ok) {\n\t\tthrow new Error(`GitHub API error: ${response.status}`);\n\t}\n\n\tconst data = (await response.json()) as { tag_name: string };\n\treturn data.tag_name.replace(/^v/, \"\");\n}\n\n// Best-effort SHA-256 verification: fetch \"<downloadUrl>.sha256\" and, when it is\n// served (HTTP 200), verify the downloaded file against it. A 404 (or any other\n// non-200 / network error) means no published checksum, so verification is\n// skipped rather than treated as a failure. A genuine mismatch throws.\nasync function verifyChecksum(downloadUrl: string, filePath: string): Promise<void> {\n\tlet checksumResponse: Awaited<ReturnType<typeof fetch>>;\n\ttry {\n\t\tchecksumResponse = await fetch(`${downloadUrl}.sha256`, {\n\t\t\tsignal: AbortSignal.timeout(NETWORK_TIMEOUT_MS),\n\t\t});\n\t} catch {\n\t\t// Network error fetching the checksum is non-fatal for best-effort verification.\n\t\treturn;\n\t}\n\n\tif (checksumResponse.status !== 200) {\n\t\treturn;\n\t}\n\n\t// sha256 files are commonly \"<hex> <filename>\"; take the leading token.\n\tconst expectedHash = (await checksumResponse.text()).trim().split(/\\s+/)[0]?.toLowerCase();\n\tif (!expectedHash || !/^[0-9a-f]{64}$/.test(expectedHash)) {\n\t\t// Unusable checksum body: skip rather than fail (still best-effort).\n\t\treturn;\n\t}\n\n\tconst actualHash = createHash(\"sha256\").update(readFileSync(filePath)).digest(\"hex\");\n\tif (actualHash !== expectedHash) {\n\t\tthrow new Error(`Checksum mismatch for ${downloadUrl}: expected ${expectedHash}, got ${actualHash}`);\n\t}\n}\n\n/**\n * Live download progress: bytes received so far and the total from\n * Content-Length (null when the server doesn't send one). Callers use it to\n * drive a progress bar instead of an indefinite spinner.\n */\nexport type DownloadProgress = (receivedBytes: number, totalBytes: number | null) => void;\n\n// Download a file from URL into `dest`, validating integrity. Throws (and removes\n// the partial file) on a truncated transfer (bytes written != Content-Length when\n// the header is present) or a SHA-256 mismatch, so a corrupt artifact is never\n// left behind. Exported for tests.\nexport async function downloadFile(url: string, dest: string, onProgress?: DownloadProgress): Promise<void> {\n\ttry {\n\t\tconst response = await fetch(url, {\n\t\t\tsignal: AbortSignal.timeout(DOWNLOAD_TIMEOUT_MS),\n\t\t});\n\n\t\tif (!response.ok) {\n\t\t\tthrow new Error(`Failed to download: ${response.status}`);\n\t\t}\n\n\t\tif (!response.body) {\n\t\t\tthrow new Error(\"No response body\");\n\t\t}\n\n\t\tconst contentLengthHeader = response.headers.get(\"content-length\");\n\t\tconst expectedBytes =\n\t\t\tcontentLengthHeader !== null && contentLengthHeader.trim() !== \"\" ? Number(contentLengthHeader) : null;\n\n\t\tconst fileStream = createWriteStream(dest);\n\t\tconst source = Readable.fromWeb(response.body as Parameters<typeof Readable.fromWeb>[0]);\n\t\tif (onProgress) {\n\t\t\tlet received = 0;\n\t\t\tsource.on(\"data\", (chunk: Buffer) => {\n\t\t\t\treceived += chunk.length;\n\t\t\t\tonProgress(received, expectedBytes);\n\t\t\t});\n\t\t}\n\t\tawait pipeline(source, fileStream);\n\n\t\tif (expectedBytes !== null && Number.isFinite(expectedBytes)) {\n\t\t\tconst bytesWritten = statSync(dest).size;\n\t\t\tif (bytesWritten !== expectedBytes) {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t`Truncated download from ${url}: expected ${expectedBytes} bytes, received ${bytesWritten}`,\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\n\t\tawait verifyChecksum(url, dest);\n\t} catch (e) {\n\t\t// Never leave a partial/corrupt file behind on any failure.\n\t\trmSync(dest, { force: true });\n\t\tthrow e;\n\t}\n}\n\nfunction findBinaryRecursively(rootDir: string, binaryFileName: string): string | null {\n\tconst stack: string[] = [rootDir];\n\n\twhile (stack.length > 0) {\n\t\tconst currentDir = stack.pop();\n\t\tif (!currentDir) continue;\n\n\t\tconst entries = readdirSync(currentDir, { withFileTypes: true });\n\t\tfor (const entry of entries) {\n\t\t\tconst fullPath = join(currentDir, entry.name);\n\t\t\tif (entry.isFile() && entry.name === binaryFileName) {\n\t\t\t\treturn fullPath;\n\t\t\t}\n\t\t\tif (entry.isDirectory()) {\n\t\t\t\tstack.push(fullPath);\n\t\t\t}\n\t\t}\n\t}\n\n\treturn null;\n}\n\n// Download and install a tool\nasync function downloadTool(tool: ManagedTool, onProgress?: DownloadProgress): Promise<string> {\n\tconst config = TOOLS[tool];\n\tif (!config) throw new Error(`Unknown tool: ${tool}`);\n\n\tconst plat = platform();\n\tconst architecture = arch();\n\n\t// Get latest version\n\tconst version = await getLatestVersion(config.repo);\n\n\t// Get asset name for this platform\n\tconst assetName = config.getAssetName(version, plat, architecture);\n\tif (!assetName) {\n\t\tthrow new Error(`Unsupported platform: ${plat}/${architecture}`);\n\t}\n\n\t// Create tools directory\n\tmkdirSync(TOOLS_DIR, { recursive: true });\n\n\tconst downloadUrl = `https://github.com/${config.repo}/releases/download/${config.tagPrefix}${version}/${assetName}`;\n\tconst archivePath = join(TOOLS_DIR, assetName);\n\tconst binaryExt = plat === \"win32\" ? \".exe\" : \"\";\n\tconst binaryPath = join(TOOLS_DIR, config.binaryName + binaryExt);\n\n\t// Download to a unique temp path, validate, then atomically rename into place.\n\t// Writing the shared archive path directly would leave a corrupt partial behind\n\t// if the transfer fails or is truncated. fd/rg/webtools can also download\n\t// concurrently at startup, so the per-attempt temp name must be unique.\n\tconst tempArchivePath = `${archivePath}.${process.pid}.${randomBytes(6).toString(\"hex\")}.part`;\n\n\t// Extract into a unique temp directory. fd and rg downloads can run concurrently\n\t// during startup, so sharing a fixed directory causes races.\n\tconst extractDir = join(\n\t\tTOOLS_DIR,\n\t\t`extract_tmp_${config.binaryName}_${process.pid}_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`,\n\t);\n\n\ttry {\n\t\t// One retry (2 attempts total) around download + integrity verification.\n\t\t// downloadFile removes its own partial on failure, so each attempt is clean.\n\t\tlet lastError: unknown;\n\t\tlet downloaded = false;\n\t\tfor (let attempt = 1; attempt <= 2 && !downloaded; attempt++) {\n\t\t\ttry {\n\t\t\t\tawait downloadFile(downloadUrl, tempArchivePath, onProgress);\n\t\t\t\tdownloaded = true;\n\t\t\t} catch (e) {\n\t\t\t\tlastError = e;\n\t\t\t\trmSync(tempArchivePath, { force: true });\n\t\t\t}\n\t\t}\n\t\tif (!downloaded) {\n\t\t\tthrow lastError instanceof Error ? lastError : new Error(String(lastError));\n\t\t}\n\n\t\t// Atomic publish of the verified archive, then extract.\n\t\trenameSync(tempArchivePath, archivePath);\n\t\tmkdirSync(extractDir, { recursive: true });\n\n\t\tif (assetName.endsWith(\".tar.gz\")) {\n\t\t\tconst extractResult = spawnSync(\"tar\", [\"xzf\", archivePath, \"-C\", extractDir], { stdio: \"pipe\" });\n\t\t\tif (extractResult.error || extractResult.status !== 0) {\n\t\t\t\tconst errMsg = extractResult.error?.message ?? extractResult.stderr?.toString().trim() ?? \"unknown error\";\n\t\t\t\tthrow new Error(`Failed to extract ${assetName}: ${errMsg}`);\n\t\t\t}\n\t\t} else if (assetName.endsWith(\".zip\")) {\n\t\t\tawait extractZip(archivePath, { dir: extractDir });\n\t\t} else {\n\t\t\tthrow new Error(`Unsupported archive format: ${assetName}`);\n\t\t}\n\n\t\t// Find the binary in extracted files. Some archives contain files directly\n\t\t// at root, others nest under a versioned subdirectory.\n\t\tconst binaryFileName = config.binaryName + binaryExt;\n\t\tconst extractedDir = join(extractDir, assetName.replace(/\\.(tar\\.gz|zip)$/, \"\"));\n\t\tconst extractedBinaryCandidates = [join(extractedDir, binaryFileName), join(extractDir, binaryFileName)];\n\t\tlet extractedBinary = extractedBinaryCandidates.find((candidate) => existsSync(candidate));\n\n\t\tif (!extractedBinary) {\n\t\t\textractedBinary = findBinaryRecursively(extractDir, binaryFileName) ?? undefined;\n\t\t}\n\n\t\tif (extractedBinary) {\n\t\t\trenameSync(extractedBinary, binaryPath);\n\t\t} else {\n\t\t\tthrow new Error(`Binary not found in archive: expected ${binaryFileName} under ${extractDir}`);\n\t\t}\n\n\t\t// Make executable (Unix only)\n\t\tif (plat !== \"win32\") {\n\t\t\tchmodSync(binaryPath, 0o755);\n\t\t}\n\t} finally {\n\t\t// Guaranteed cleanup of every transient artifact on ANY outcome: the temp\n\t\t// download (if a failure left it before the rename), the published archive,\n\t\t// and the temp extract dir.\n\t\trmSync(tempArchivePath, { force: true });\n\t\trmSync(archivePath, { force: true });\n\t\trmSync(extractDir, { recursive: true, force: true });\n\t}\n\n\treturn binaryPath;\n}\n\n// Termux package names for tools\nconst TERMUX_PACKAGES: Record<string, string> = {\n\tfd: \"fd\",\n\trg: \"ripgrep\",\n\twebtools: \"webtools\",\n\tvoicetools: \"voicetools\",\n\tembsearch: \"embsearch\",\n};\n\n// Ensure a tool is available, downloading if necessary\n// Returns the path to the tool, or null if unavailable\n// onProgress receives live byte counts while the release archive downloads.\nexport async function ensureTool(\n\ttool: ManagedTool,\n\tsilent: boolean = false,\n\tonProgress?: DownloadProgress,\n): Promise<string | undefined> {\n\tconst existingPath = getToolPath(tool);\n\tif (existingPath) {\n\t\treturn existingPath;\n\t}\n\n\tconst config = TOOLS[tool];\n\tif (!config) return undefined;\n\n\tif (isOfflineModeEnabled()) {\n\t\tif (!silent) {\n\t\t\tconsole.log(chalk.yellow(`${config.name} not found. Offline mode enabled, skipping download.`));\n\t\t}\n\t\treturn undefined;\n\t}\n\n\t// On Android/Termux, Linux binaries don't work due to Bionic libc incompatibility.\n\t// Users must install via pkg.\n\tif (platform() === \"android\") {\n\t\tconst pkgName = TERMUX_PACKAGES[tool] ?? tool;\n\t\tif (!silent) {\n\t\t\tconsole.log(chalk.yellow(`${config.name} not found. Install with: pkg install ${pkgName}`));\n\t\t}\n\t\treturn undefined;\n\t}\n\n\t// Tool not found - download it\n\tif (!silent) {\n\t\tconsole.log(chalk.dim(`${config.name} not found. Downloading...`));\n\t}\n\n\ttry {\n\t\tconst path = await downloadTool(tool, onProgress);\n\t\tif (!silent) {\n\t\t\tconsole.log(chalk.dim(`${config.name} installed to ${path}`));\n\t\t}\n\t\treturn path;\n\t} catch (e) {\n\t\tif (!silent) {\n\t\t\tconsole.log(chalk.yellow(`Failed to download ${config.name}: ${e instanceof Error ? e.message : e}`));\n\t\t}\n\t\treturn undefined;\n\t}\n}\n"]}
package/docs/themes.md CHANGED
@@ -7,6 +7,7 @@ Themes are JSON files that define colors for the TUI.
7
7
  ## Table of Contents
8
8
 
9
9
  - [Locations](#locations)
10
+ - [Built-in Themes](#built-in-themes)
10
11
  - [Selecting a Theme](#selecting-a-theme)
11
12
  - [Creating a Custom Theme](#creating-a-custom-theme)
12
13
  - [Theme Format](#theme-format)
@@ -18,7 +19,7 @@ Themes are JSON files that define colors for the TUI.
18
19
 
19
20
  HooCode loads themes from:
20
21
 
21
- - Built-in: `dark`, `light`
22
+ - Built-in: every `*.json` in the shipped theme directory (see [Built-in Themes](#built-in-themes))
22
23
  - Global: `~/.hoocode/agent/themes/*.json`
23
24
  - Project: `.hoocode/themes/*.json`
24
25
  - Packages: `themes/` directories or `pi.themes` entries in `package.json`
@@ -27,6 +28,39 @@ HooCode loads themes from:
27
28
 
28
29
  Disable discovery with `--no-themes`.
29
30
 
31
+ ## Built-in Themes
32
+
33
+ | Theme | Background | Notes |
34
+ |-------|------------|-------|
35
+ | `dark` | dark | Default dark theme |
36
+ | `light` | light | Default light theme |
37
+ | `high-contrast-dark` | black | Maximum contrast, bright saturated hues |
38
+ | `high-contrast-light` | white | Maximum contrast, deep saturated hues |
39
+ | `warm-dark` | warm near-black | Low-glare amber/sand palette, no harsh blues |
40
+ | `warm-light` | warm paper | Dark ink on cream, less glare than pure white |
41
+ | `colorsafe-dark` | dark | Okabe-Ito hues, no red/green pairs |
42
+ | `colorsafe-light` | white | Okabe-Ito hues, no red/green pairs |
43
+
44
+ The six themes after `dark`/`light` are built for low vision. Every color they
45
+ draw clears **WCAG AAA (7:1)** against every surface the TUI paints behind it —
46
+ the page, selected rows, user messages, and all three tool-box states — and none
47
+ of them defer to the terminal's default foreground, so contrast does not depend
48
+ on your terminal's own palette. `test/theme-contrast.test.ts` enforces this, so
49
+ a future edit cannot quietly wash one of them out.
50
+
51
+ Picking between them:
52
+
53
+ - **Contrast above all** → `high-contrast-dark` / `high-contrast-light`.
54
+ - **Bright screens hurt, or you read for long stretches** → `warm-dark` /
55
+ `warm-light`. Same 7:1 floor, warmer and less glaring.
56
+ - **Red/green are hard to tell apart** → `colorsafe-dark` / `colorsafe-light`.
57
+ Success and error are teal and orange, and no pair of tokens relies on a
58
+ red-versus-green distinction.
59
+
60
+ Set the matching terminal background for the theme you pick (black-ish for the
61
+ dark themes, white-ish for the light ones) — hoocode colors the text, but the
62
+ canvas behind it belongs to your terminal.
63
+
30
64
  ## Selecting a Theme
31
65
 
32
66
  Select a theme via `/settings` or in `settings.json`:
@@ -124,6 +158,7 @@ vim ~/.hoocode/agent/themes/my-theme.json
124
158
  {
125
159
  "$schema": "https://raw.githubusercontent.com/kolisachint/hoocode/main/packages/coding-agent/src/modes/interactive/theme/theme-schema.json",
126
160
  "name": "my-theme",
161
+ "description": "One line shown next to the name in the theme picker",
127
162
  "vars": {
128
163
  "blue": "#0066cc",
129
164
  "gray": 242
@@ -138,6 +173,7 @@ vim ~/.hoocode/agent/themes/my-theme.json
138
173
  ```
139
174
 
140
175
  - `name` is required and must be unique.
176
+ - `description` is optional. `/settings` shows it beside the theme name.
141
177
  - `vars` is optional. Define reusable colors here, then reference them in `colors`.
142
178
  - `colors` must define all 51 required tokens.
143
179
 
@@ -280,7 +316,14 @@ echo $COLORTERM # Should output "truecolor" or "24bit"
280
316
 
281
317
  **Dark terminals:** Use bright, saturated colors with higher contrast.
282
318
 
283
- **Light terminals:** Use darker, muted colors with lower contrast.
319
+ **Light terminals:** Use dark, saturated colors. Muting them costs contrast; if
320
+ you want a softer look, warm the background rather than lightening the text.
321
+
322
+ **Contrast:** Aim for 4.5:1 against your terminal background, or 7:1 if the
323
+ theme has to stay readable for low vision. Check every token against the
324
+ selected-row and tool-box backgrounds too, not just the page — those are where
325
+ themes usually lose contrast. `test/theme-contrast.test.ts` shows how the
326
+ built-in accessible themes are verified.
284
327
 
285
328
  **Color harmony:** Start with a base palette (Nord, Gruvbox, Tokyo Night), define it in `vars`, and reference consistently.
286
329
 
@@ -293,3 +336,15 @@ echo $COLORTERM # Should output "truecolor" or "24bit"
293
336
  See the built-in themes:
294
337
  - [dark.json](../src/modes/interactive/theme/dark.json)
295
338
  - [light.json](../src/modes/interactive/theme/light.json)
339
+ - [high-contrast-dark.json](../src/modes/interactive/theme/high-contrast-dark.json)
340
+ - [high-contrast-light.json](../src/modes/interactive/theme/high-contrast-light.json)
341
+ - [warm-dark.json](../src/modes/interactive/theme/warm-dark.json)
342
+ - [warm-light.json](../src/modes/interactive/theme/warm-light.json)
343
+ - [colorsafe-dark.json](../src/modes/interactive/theme/colorsafe-dark.json)
344
+ - [colorsafe-light.json](../src/modes/interactive/theme/colorsafe-light.json)
345
+
346
+ Preview any of them, with per-token contrast ratios:
347
+
348
+ ```bash
349
+ npx tsx test/test-theme-colors.ts theme warm-dark
350
+ ```
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@kolisachint/hoocode-extension-custom-provider-anthropic",
3
3
  "private": true,
4
- "version": "0.3.3",
4
+ "version": "0.3.5",
5
5
  "type": "module",
6
6
  "engines": {
7
7
  "bun": ">=1.0.0"
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@kolisachint/hoocode-extension-custom-provider-gitlab-duo",
3
3
  "private": true,
4
- "version": "0.3.3",
4
+ "version": "0.3.5",
5
5
  "type": "module",
6
6
  "engines": {
7
7
  "bun": ">=1.0.0"
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@kolisachint/hoocode-extension-sandbox",
3
3
  "private": true,
4
- "version": "0.3.3",
4
+ "version": "0.3.5",
5
5
  "type": "module",
6
6
  "engines": {
7
7
  "bun": ">=1.0.0"
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@kolisachint/hoocode-extension-with-deps",
3
3
  "private": true,
4
- "version": "0.3.3",
4
+ "version": "0.3.5",
5
5
  "type": "module",
6
6
  "engines": {
7
7
  "bun": ">=1.0.0"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kolisachint/hoocode-agent",
3
- "version": "0.5.3",
3
+ "version": "0.5.5",
4
4
  "description": "Coding agent CLI with read, bash, edit, write tools and session management",
5
5
  "type": "module",
6
6
  "hoocodeConfig": {
@@ -42,15 +42,16 @@
42
42
  "copy-assets": "shx mkdir -p dist/modes/interactive/theme && shx cp src/modes/interactive/theme/*.json dist/modes/interactive/theme/ && shx mkdir -p dist/core/export-html/vendor && shx cp src/core/export-html/template.html src/core/export-html/template.css src/core/export-html/template.js dist/core/export-html/ && shx cp src/core/export-html/vendor/*.js dist/core/export-html/vendor/ && shx mkdir -p dist/core/extensions/plugins && shx cp -r src/core/extensions/plugins/default-marketplace dist/core/extensions/plugins/",
43
43
  "copy-binary-assets": "shx cp package.json dist/ && shx cp README.md dist/ && shx cp CHANGELOG.md dist/ && shx mkdir -p dist/theme && shx cp src/modes/interactive/theme/*.json dist/theme/ && shx mkdir -p dist/export-html/vendor && shx cp src/core/export-html/template.html dist/export-html/ && shx cp src/core/export-html/vendor/*.js dist/export-html/vendor/ && shx cp -r docs dist/ && shx cp -r examples dist/ && shx cp ../../node_modules/@silvia-odwyer/photon-node/photon_rs_bg.wasm dist/",
44
44
  "test": "vitest --run",
45
+ "plugin-drift-check": "bun scripts/plugin-drift-check.ts",
45
46
  "search-eval": "bun scripts/search-eval.ts",
46
47
  "search-eval:gold": "bun scripts/search-eval-gold.ts",
47
48
  "search-eval:compare": "bun scripts/search-eval-compare.ts",
48
49
  "prepublishOnly": "npm run clean && npm run build"
49
50
  },
50
51
  "dependencies": {
51
- "@kolisachint/hoocode-agent-core": "^0.5.3",
52
- "@kolisachint/hoocode-ai": "^0.5.3",
53
- "@kolisachint/hoocode-tui": "^0.5.3",
52
+ "@kolisachint/hoocode-agent-core": "^0.5.5",
53
+ "@kolisachint/hoocode-ai": "^0.5.5",
54
+ "@kolisachint/hoocode-tui": "^0.5.5",
54
55
  "@silvia-odwyer/photon-node": "^0.3.4",
55
56
  "chalk": "^5.5.0",
56
57
  "cli-highlight": "^2.1.11",