mixdog 0.7.1

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 (404) hide show
  1. package/.claude-plugin/marketplace.json +31 -0
  2. package/.claude-plugin/plugin.json +20 -0
  3. package/.gitattributes +34 -0
  4. package/.mcp.json +14 -0
  5. package/ARCHITECTURE.md +77 -0
  6. package/CHANGELOG.md +7 -0
  7. package/CONTRIBUTING.md +45 -0
  8. package/DATA-FLOW.md +79 -0
  9. package/LICENSE +21 -0
  10. package/README.md +389 -0
  11. package/SECURITY.md +138 -0
  12. package/UNINSTALL.md +112 -0
  13. package/agents/maintenance.md +5 -0
  14. package/agents/memory-classification.md +30 -0
  15. package/agents/scheduler-task.md +18 -0
  16. package/agents/webhook-handler.md +27 -0
  17. package/agents/worker.md +24 -0
  18. package/bin/bridge +133 -0
  19. package/bin/statusline-launcher.mjs +78 -0
  20. package/bin/statusline-lib.mjs +550 -0
  21. package/bin/statusline.mjs +607 -0
  22. package/bun.lock +802 -0
  23. package/commands/config.md +16 -0
  24. package/commands/doctor.md +13 -0
  25. package/commands/setup.md +17 -0
  26. package/defaults/cycle3-review-prompt.md +90 -0
  27. package/defaults/hidden-roles.json +65 -0
  28. package/defaults/memory-chunk-prompt.md +63 -0
  29. package/defaults/memory-promote-prompt.md +135 -0
  30. package/defaults/mixdog-config.template.json +27 -0
  31. package/defaults/user-workflow.json +8 -0
  32. package/defaults/user-workflow.md +12 -0
  33. package/hooks/hooks.json +73 -0
  34. package/hooks/lib/active-instance.cjs +77 -0
  35. package/hooks/lib/permission-evaluator.cjs +411 -0
  36. package/hooks/lib/permission-route.cjs +63 -0
  37. package/hooks/lib/permission-rules.cjs +170 -0
  38. package/hooks/lib/settings-loader.cjs +116 -0
  39. package/hooks/post-tool-use.cjs +84 -0
  40. package/hooks/pre-mcp-sandbox.cjs +158 -0
  41. package/hooks/pre-tool-subagent.cjs +253 -0
  42. package/hooks/session-start.cjs +1372 -0
  43. package/hooks/turn-timer.cjs +82 -0
  44. package/lib/claude-md-writer.cjs +386 -0
  45. package/lib/config-cjs.cjs +61 -0
  46. package/lib/hook-pipe-path.cjs +10 -0
  47. package/lib/keychain-cjs.cjs +263 -0
  48. package/lib/plugin-paths.cjs +61 -0
  49. package/lib/rules-builder.cjs +241 -0
  50. package/lib/text-utils.cjs +61 -0
  51. package/native/README.md +117 -0
  52. package/native/prebuilt/linux-aarch64/mixdog-shim +0 -0
  53. package/native/prebuilt/linux-x86_64/mixdog-shim +0 -0
  54. package/native/prebuilt/macos-aarch64/mixdog-shim +0 -0
  55. package/native/prebuilt/macos-x86_64/mixdog-shim +0 -0
  56. package/native/prebuilt/windows-x86_64/mixdog-shim.exe +0 -0
  57. package/package.json +107 -0
  58. package/prompts/code-review.txt +16 -0
  59. package/prompts/security-audit.txt +17 -0
  60. package/rules/bridge/00-common.md +39 -0
  61. package/rules/bridge/20-skip-protocol.md +18 -0
  62. package/rules/bridge/30-explorer.md +33 -0
  63. package/rules/bridge/40-cycle1-agent.md +52 -0
  64. package/rules/bridge/41-cycle2-agent.md +62 -0
  65. package/rules/bridge/42-cycle3-agent.md +44 -0
  66. package/rules/lead/00-tool-lead.md +61 -0
  67. package/rules/lead/01-general.md +23 -0
  68. package/rules/lead/02-channels.md +49 -0
  69. package/rules/lead/03-team.md +27 -0
  70. package/rules/lead/04-workflow.md +20 -0
  71. package/rules/shared/00-language.md +14 -0
  72. package/rules/shared/01-tool.md +138 -0
  73. package/scripts/bootstrap.mjs +184 -0
  74. package/scripts/bridge-unify-smoke.mjs +308 -0
  75. package/scripts/build-runtime-linux.sh +348 -0
  76. package/scripts/build-runtime-macos.sh +217 -0
  77. package/scripts/build-runtime-windows.ps1 +242 -0
  78. package/scripts/builtin-utils-smoke.mjs +392 -0
  79. package/scripts/check-json.mjs +45 -0
  80. package/scripts/check-syntax-changed.mjs +102 -0
  81. package/scripts/check-syntax.mjs +58 -0
  82. package/scripts/code-graph-batch.test.mjs +33 -0
  83. package/scripts/config-preserve-smoke.mjs +180 -0
  84. package/scripts/doctor.mjs +484 -0
  85. package/scripts/edit-normalize-fuzz.mjs +130 -0
  86. package/scripts/edit-normalize-smoke.mjs +401 -0
  87. package/scripts/edit-operation-smoke.mjs +369 -0
  88. package/scripts/edit2-smoke.mjs +63 -0
  89. package/scripts/fuzzy-e2e.mjs +28 -0
  90. package/scripts/fuzzy-smoke.mjs +26 -0
  91. package/scripts/generate-runtime-manifest.mjs +166 -0
  92. package/scripts/guard-smoke.mjs +66 -0
  93. package/scripts/hidden-role-schema-smoke.mjs +162 -0
  94. package/scripts/hook-routing-smoke.mjs +29 -0
  95. package/scripts/inject-input.ps1 +204 -0
  96. package/scripts/io-complex-smoke.mjs +667 -0
  97. package/scripts/io-explore-bench.mjs +424 -0
  98. package/scripts/io-guardrails-smoke.mjs +205 -0
  99. package/scripts/io-mini-bench-baseline.json +11 -0
  100. package/scripts/io-mini-bench.mjs +216 -0
  101. package/scripts/io-route-harness.mjs +933 -0
  102. package/scripts/io-telemetry-report.mjs +691 -0
  103. package/scripts/mutation-bench.mjs +564 -0
  104. package/scripts/mutation-io-smoke.mjs +1081 -0
  105. package/scripts/native-patch-bridge-smoke.mjs +288 -0
  106. package/scripts/native-patch-smoke.mjs +304 -0
  107. package/scripts/patch-interior-context-smoke.mjs +49 -0
  108. package/scripts/patch-newline-utf8-smoke.mjs +157 -0
  109. package/scripts/perf-hook-smoke.mjs +71 -0
  110. package/scripts/permission-eval-smoke.mjs +426 -0
  111. package/scripts/prep-patch.mjs +53 -0
  112. package/scripts/prep-shim.mjs +96 -0
  113. package/scripts/provider-cache-smoke.mjs +687 -0
  114. package/scripts/report-runtime-health.mjs +132 -0
  115. package/scripts/run-mcp.mjs +1547 -0
  116. package/scripts/salvage-v4a-shatter.test.mjs +58 -0
  117. package/scripts/scoped-cache-io-smoke.mjs +103 -0
  118. package/scripts/shell-policy-round3-smoke.mjs +46 -0
  119. package/scripts/smoke-runtime-negative.ps1 +100 -0
  120. package/scripts/smoke-runtime-negative.sh +95 -0
  121. package/scripts/stall-policy-smoke.mjs +50 -0
  122. package/scripts/start-memory-worker.mjs +23 -0
  123. package/scripts/statusline-launcher-smoke.mjs +82 -0
  124. package/scripts/stress-atomic-write.mjs +1028 -0
  125. package/scripts/test-config-rmw-restore.mjs +122 -0
  126. package/scripts/test-fault-inject.mjs +164 -0
  127. package/scripts/test-large-file.mjs +174 -0
  128. package/scripts/tool-edge-smoke.mjs +209 -0
  129. package/scripts/uninstall.mjs +201 -0
  130. package/scripts/webhook-selfheal-smoke.mjs +29 -0
  131. package/scripts/write-overwrite-guard-smoke.mjs +56 -0
  132. package/server-main.mjs +3055 -0
  133. package/server.mjs +468 -0
  134. package/setup/config-merge.mjs +254 -0
  135. package/setup/install.mjs +120 -0
  136. package/setup/launch-core.mjs +507 -0
  137. package/setup/launch.mjs +101 -0
  138. package/setup/setup-server.mjs +3206 -0
  139. package/setup/setup.html +3693 -0
  140. package/skills/retro-skill-proposer/SKILL.md +92 -0
  141. package/skills/schedule-add/SKILL.md +77 -0
  142. package/skills/setup/SKILL.md +346 -0
  143. package/skills/webhook-add/SKILL.md +81 -0
  144. package/src/agent/bridge-stall-watchdog.mjs +337 -0
  145. package/src/agent/index.mjs +2138 -0
  146. package/src/agent/orchestrator/activity-bus.mjs +38 -0
  147. package/src/agent/orchestrator/ai-wrapped-dispatch.mjs +1010 -0
  148. package/src/agent/orchestrator/bridge-retry.mjs +220 -0
  149. package/src/agent/orchestrator/bridge-trace.mjs +583 -0
  150. package/src/agent/orchestrator/cache-mtime.mjs +58 -0
  151. package/src/agent/orchestrator/config.mjs +358 -0
  152. package/src/agent/orchestrator/context/collect.mjs +651 -0
  153. package/src/agent/orchestrator/dispatch-persist.mjs +549 -0
  154. package/src/agent/orchestrator/drain-registry.mjs +50 -0
  155. package/src/agent/orchestrator/explore-validator.mjs +8 -0
  156. package/src/agent/orchestrator/internal-roles.mjs +118 -0
  157. package/src/agent/orchestrator/internal-tools.mjs +88 -0
  158. package/src/agent/orchestrator/jobs.mjs +116 -0
  159. package/src/agent/orchestrator/mcp/client.mjs +364 -0
  160. package/src/agent/orchestrator/providers/anthropic-betas.mjs +21 -0
  161. package/src/agent/orchestrator/providers/anthropic-oauth.mjs +1745 -0
  162. package/src/agent/orchestrator/providers/anthropic.mjs +437 -0
  163. package/src/agent/orchestrator/providers/gemini.mjs +1175 -0
  164. package/src/agent/orchestrator/providers/grok-oauth.mjs +782 -0
  165. package/src/agent/orchestrator/providers/model-catalog.mjs +241 -0
  166. package/src/agent/orchestrator/providers/openai-compat.mjs +1467 -0
  167. package/src/agent/orchestrator/providers/openai-oauth-ws.mjs +1890 -0
  168. package/src/agent/orchestrator/providers/openai-oauth.mjs +1307 -0
  169. package/src/agent/orchestrator/providers/openai-ws.mjs +104 -0
  170. package/src/agent/orchestrator/providers/registry.mjs +192 -0
  171. package/src/agent/orchestrator/providers/retry-classifier.mjs +325 -0
  172. package/src/agent/orchestrator/session/abort-lookup.mjs +13 -0
  173. package/src/agent/orchestrator/session/cache/post-edit-marks.mjs +42 -0
  174. package/src/agent/orchestrator/session/cache/prefetch-cache.mjs +142 -0
  175. package/src/agent/orchestrator/session/cache/read-cache.mjs +319 -0
  176. package/src/agent/orchestrator/session/cache/scoped-cache-outcome.mjs +11 -0
  177. package/src/agent/orchestrator/session/cache/scoped-cache.mjs +361 -0
  178. package/src/agent/orchestrator/session/cache/util.mjs +49 -0
  179. package/src/agent/orchestrator/session/loop.mjs +1478 -0
  180. package/src/agent/orchestrator/session/manager.mjs +1975 -0
  181. package/src/agent/orchestrator/session/read-dedup.mjs +6 -0
  182. package/src/agent/orchestrator/session/result-classification.mjs +65 -0
  183. package/src/agent/orchestrator/session/save-session-worker.mjs +18 -0
  184. package/src/agent/orchestrator/session/store.mjs +624 -0
  185. package/src/agent/orchestrator/session/stream-watchdog.mjs +130 -0
  186. package/src/agent/orchestrator/session/tool-result-offload.mjs +166 -0
  187. package/src/agent/orchestrator/session/trim.mjs +491 -0
  188. package/src/agent/orchestrator/smart-bridge/CACHE-SHARD.md +115 -0
  189. package/src/agent/orchestrator/smart-bridge/bridge-llm.mjs +327 -0
  190. package/src/agent/orchestrator/smart-bridge/cache-obs.mjs +150 -0
  191. package/src/agent/orchestrator/smart-bridge/cache-strategy.mjs +228 -0
  192. package/src/agent/orchestrator/smart-bridge/index.mjs +215 -0
  193. package/src/agent/orchestrator/smart-bridge/profiles.mjs +37 -0
  194. package/src/agent/orchestrator/smart-bridge/registry.mjs +348 -0
  195. package/src/agent/orchestrator/smart-bridge/session-builder.mjs +116 -0
  196. package/src/agent/orchestrator/stall-policy.mjs +195 -0
  197. package/src/agent/orchestrator/tool-loop-guard.mjs +75 -0
  198. package/src/agent/orchestrator/tools/bash-policy-scan.mjs +77 -0
  199. package/src/agent/orchestrator/tools/bash-session.mjs +721 -0
  200. package/src/agent/orchestrator/tools/builtin/advisory-lock.mjs +171 -0
  201. package/src/agent/orchestrator/tools/builtin/arg-guard.mjs +455 -0
  202. package/src/agent/orchestrator/tools/builtin/atomic-write.mjs +236 -0
  203. package/src/agent/orchestrator/tools/builtin/bash-tool.mjs +480 -0
  204. package/src/agent/orchestrator/tools/builtin/binary-file.mjs +76 -0
  205. package/src/agent/orchestrator/tools/builtin/builtin-tools.mjs +256 -0
  206. package/src/agent/orchestrator/tools/builtin/cache-layers.mjs +386 -0
  207. package/src/agent/orchestrator/tools/builtin/cwd-utils.mjs +37 -0
  208. package/src/agent/orchestrator/tools/builtin/device-paths.mjs +154 -0
  209. package/src/agent/orchestrator/tools/builtin/diagnostics-tool.mjs +292 -0
  210. package/src/agent/orchestrator/tools/builtin/diff-utils.mjs +109 -0
  211. package/src/agent/orchestrator/tools/builtin/edit-base-guard.mjs +58 -0
  212. package/src/agent/orchestrator/tools/builtin/edit-byte-plan.mjs +240 -0
  213. package/src/agent/orchestrator/tools/builtin/edit-byte-utils.mjs +113 -0
  214. package/src/agent/orchestrator/tools/builtin/edit-commit.mjs +74 -0
  215. package/src/agent/orchestrator/tools/builtin/edit-context-utils.mjs +242 -0
  216. package/src/agent/orchestrator/tools/builtin/edit-diagnostics.mjs +211 -0
  217. package/src/agent/orchestrator/tools/builtin/edit-engine.mjs +1364 -0
  218. package/src/agent/orchestrator/tools/builtin/edit-failure-context.mjs +126 -0
  219. package/src/agent/orchestrator/tools/builtin/edit-hint.mjs +141 -0
  220. package/src/agent/orchestrator/tools/builtin/edit-match-utils.mjs +194 -0
  221. package/src/agent/orchestrator/tools/builtin/edit-partial-write.mjs +60 -0
  222. package/src/agent/orchestrator/tools/builtin/edit-stale-refresh.mjs +168 -0
  223. package/src/agent/orchestrator/tools/builtin/edit-tool.mjs +173 -0
  224. package/src/agent/orchestrator/tools/builtin/edit-utf8-guard.mjs +48 -0
  225. package/src/agent/orchestrator/tools/builtin/fs-reachability.mjs +48 -0
  226. package/src/agent/orchestrator/tools/builtin/fuzzy-match.mjs +99 -0
  227. package/src/agent/orchestrator/tools/builtin/glob-walk.mjs +170 -0
  228. package/src/agent/orchestrator/tools/builtin/grep-formatting.mjs +113 -0
  229. package/src/agent/orchestrator/tools/builtin/hash-utils.mjs +6 -0
  230. package/src/agent/orchestrator/tools/builtin/list-formatting.mjs +7 -0
  231. package/src/agent/orchestrator/tools/builtin/list-tool.mjs +593 -0
  232. package/src/agent/orchestrator/tools/builtin/native-edit-runner.mjs +89 -0
  233. package/src/agent/orchestrator/tools/builtin/notebook-edit-tool.mjs +300 -0
  234. package/src/agent/orchestrator/tools/builtin/open-config-tool.mjs +26 -0
  235. package/src/agent/orchestrator/tools/builtin/path-diagnostics.mjs +152 -0
  236. package/src/agent/orchestrator/tools/builtin/path-locks.mjs +35 -0
  237. package/src/agent/orchestrator/tools/builtin/path-utils.mjs +201 -0
  238. package/src/agent/orchestrator/tools/builtin/read-args.mjs +103 -0
  239. package/src/agent/orchestrator/tools/builtin/read-batch.mjs +172 -0
  240. package/src/agent/orchestrator/tools/builtin/read-constants.mjs +40 -0
  241. package/src/agent/orchestrator/tools/builtin/read-formatting.mjs +118 -0
  242. package/src/agent/orchestrator/tools/builtin/read-image-resize.mjs +189 -0
  243. package/src/agent/orchestrator/tools/builtin/read-image.mjs +88 -0
  244. package/src/agent/orchestrator/tools/builtin/read-lines.mjs +12 -0
  245. package/src/agent/orchestrator/tools/builtin/read-mode-tool.mjs +455 -0
  246. package/src/agent/orchestrator/tools/builtin/read-open.mjs +190 -0
  247. package/src/agent/orchestrator/tools/builtin/read-range-index.mjs +271 -0
  248. package/src/agent/orchestrator/tools/builtin/read-ranges.mjs +26 -0
  249. package/src/agent/orchestrator/tools/builtin/read-single-tool.mjs +728 -0
  250. package/src/agent/orchestrator/tools/builtin/read-snapshot-runtime.mjs +173 -0
  251. package/src/agent/orchestrator/tools/builtin/read-special-files.mjs +268 -0
  252. package/src/agent/orchestrator/tools/builtin/read-streaming.mjs +602 -0
  253. package/src/agent/orchestrator/tools/builtin/read-tool.mjs +530 -0
  254. package/src/agent/orchestrator/tools/builtin/read-windows.mjs +107 -0
  255. package/src/agent/orchestrator/tools/builtin/rename-tool.mjs +196 -0
  256. package/src/agent/orchestrator/tools/builtin/rg-runner.mjs +422 -0
  257. package/src/agent/orchestrator/tools/builtin/search-builders.mjs +158 -0
  258. package/src/agent/orchestrator/tools/builtin/search-tool.mjs +869 -0
  259. package/src/agent/orchestrator/tools/builtin/shell-analysis.mjs +653 -0
  260. package/src/agent/orchestrator/tools/builtin/shell-jobs.mjs +936 -0
  261. package/src/agent/orchestrator/tools/builtin/shell-output.mjs +36 -0
  262. package/src/agent/orchestrator/tools/builtin/shell-runtime.mjs +214 -0
  263. package/src/agent/orchestrator/tools/builtin/snapshot-helpers.mjs +143 -0
  264. package/src/agent/orchestrator/tools/builtin/snapshot-store.mjs +206 -0
  265. package/src/agent/orchestrator/tools/builtin/snapshot-validation.mjs +98 -0
  266. package/src/agent/orchestrator/tools/builtin/text-stats.mjs +69 -0
  267. package/src/agent/orchestrator/tools/builtin/windows-roots.mjs +23 -0
  268. package/src/agent/orchestrator/tools/builtin/write-tool.mjs +401 -0
  269. package/src/agent/orchestrator/tools/builtin.mjs +500 -0
  270. package/src/agent/orchestrator/tools/code-graph-prewarm-worker.mjs +39 -0
  271. package/src/agent/orchestrator/tools/code-graph-tool-defs.mjs +24 -0
  272. package/src/agent/orchestrator/tools/code-graph.mjs +4095 -0
  273. package/src/agent/orchestrator/tools/cwd-tool.mjs +298 -0
  274. package/src/agent/orchestrator/tools/destructive-warning.mjs +323 -0
  275. package/src/agent/orchestrator/tools/edit-normalize.mjs +603 -0
  276. package/src/agent/orchestrator/tools/env-scrub.mjs +100 -0
  277. package/src/agent/orchestrator/tools/graph-binary-fetcher.mjs +144 -0
  278. package/src/agent/orchestrator/tools/graph-manifest.json +26 -0
  279. package/src/agent/orchestrator/tools/host-input.mjs +204 -0
  280. package/src/agent/orchestrator/tools/mutation-content-cache.mjs +67 -0
  281. package/src/agent/orchestrator/tools/mutation-planner.mjs +75 -0
  282. package/src/agent/orchestrator/tools/next-call-utils.mjs +48 -0
  283. package/src/agent/orchestrator/tools/patch-binary-fetcher.mjs +133 -0
  284. package/src/agent/orchestrator/tools/patch-manifest.json +26 -0
  285. package/src/agent/orchestrator/tools/patch-tool-defs.mjs +20 -0
  286. package/src/agent/orchestrator/tools/patch.mjs +2754 -0
  287. package/src/agent/orchestrator/tools/progress-message.mjs +118 -0
  288. package/src/agent/orchestrator/tools/result-compression.mjs +279 -0
  289. package/src/agent/orchestrator/tools/shell-command.mjs +865 -0
  290. package/src/agent/orchestrator/tools/shell-exec-policy.mjs +89 -0
  291. package/src/agent/orchestrator/tools/shell-policy-danger-target.mjs +27 -0
  292. package/src/agent/orchestrator/tools/shell-policy-imports.mjs +7 -0
  293. package/src/agent/orchestrator/tools/shell-policy.mjs +345 -0
  294. package/src/agent/orchestrator/tools/shell-snapshot.mjs +313 -0
  295. package/src/agent/orchestrator/workflow-store.mjs +93 -0
  296. package/src/agent/tool-defs.mjs +103 -0
  297. package/src/channels/backends/discord.mjs +784 -0
  298. package/src/channels/data/voice-runtime-manifest.json +138 -0
  299. package/src/channels/index.mjs +3229 -0
  300. package/src/channels/lib/cli-worker-host.mjs +12 -0
  301. package/src/channels/lib/config-lock.mjs +13 -0
  302. package/src/channels/lib/config.mjs +292 -0
  303. package/src/channels/lib/drop-trace.mjs +71 -0
  304. package/src/channels/lib/event-pipeline.mjs +81 -0
  305. package/src/channels/lib/event-queue.mjs +345 -0
  306. package/src/channels/lib/executor.mjs +168 -0
  307. package/src/channels/lib/format.mjs +188 -0
  308. package/src/channels/lib/holidays.mjs +138 -0
  309. package/src/channels/lib/hook-pipe-server.mjs +802 -0
  310. package/src/channels/lib/interaction-workflows.mjs +184 -0
  311. package/src/channels/lib/memory-client.mjs +149 -0
  312. package/src/channels/lib/output-forwarder.mjs +765 -0
  313. package/src/channels/lib/runtime-paths.mjs +479 -0
  314. package/src/channels/lib/scheduler.mjs +723 -0
  315. package/src/channels/lib/session-control.mjs +36 -0
  316. package/src/channels/lib/session-discovery.mjs +103 -0
  317. package/src/channels/lib/settings.mjs +11 -0
  318. package/src/channels/lib/state-file.mjs +68 -0
  319. package/src/channels/lib/status-snapshot.mjs +219 -0
  320. package/src/channels/lib/tool-format.mjs +140 -0
  321. package/src/channels/lib/transcript-discovery.mjs +195 -0
  322. package/src/channels/lib/voice-runtime-fetcher.mjs +734 -0
  323. package/src/channels/lib/webhook.mjs +1179 -0
  324. package/src/channels/lib/whisper-server.mjs +477 -0
  325. package/src/channels/tool-defs.mjs +170 -0
  326. package/src/daemon/host.mjs +118 -0
  327. package/src/daemon/mcp-transport.mjs +47 -0
  328. package/src/daemon/session.mjs +100 -0
  329. package/src/daemon/thin-client.mjs +71 -0
  330. package/src/daemon/transport.mjs +163 -0
  331. package/src/memory/data/runtime-manifest.json +40 -0
  332. package/src/memory/index.mjs +3305 -0
  333. package/src/memory/lib/agent-ipc.mjs +93 -0
  334. package/src/memory/lib/bridge-trace-queries.mjs +120 -0
  335. package/src/memory/lib/core-memory-store.mjs +330 -0
  336. package/src/memory/lib/embedding-provider.mjs +269 -0
  337. package/src/memory/lib/embedding-worker.mjs +323 -0
  338. package/src/memory/lib/llm-worker-host.mjs +17 -0
  339. package/src/memory/lib/memory-cycle.mjs +11 -0
  340. package/src/memory/lib/memory-cycle1.mjs +641 -0
  341. package/src/memory/lib/memory-cycle2.mjs +1284 -0
  342. package/src/memory/lib/memory-cycle3.mjs +540 -0
  343. package/src/memory/lib/memory-embed.mjs +299 -0
  344. package/src/memory/lib/memory-extraction.mjs +5 -0
  345. package/src/memory/lib/memory-maintenance-store.mjs +32 -0
  346. package/src/memory/lib/memory-ops-policy.mjs +190 -0
  347. package/src/memory/lib/memory-recall-id-patch.mjs +15 -0
  348. package/src/memory/lib/memory-recall-read-query.mjs +7 -0
  349. package/src/memory/lib/memory-recall-scope-filter.mjs +63 -0
  350. package/src/memory/lib/memory-recall-store.mjs +621 -0
  351. package/src/memory/lib/memory-retrievers.mjs +112 -0
  352. package/src/memory/lib/memory-score.mjs +71 -0
  353. package/src/memory/lib/memory-text-utils.mjs +58 -0
  354. package/src/memory/lib/memory.mjs +412 -0
  355. package/src/memory/lib/model-profile.mjs +85 -0
  356. package/src/memory/lib/pg/adapter.mjs +308 -0
  357. package/src/memory/lib/pg/process.mjs +360 -0
  358. package/src/memory/lib/pg/supervisor.mjs +396 -0
  359. package/src/memory/lib/project-id-resolver.mjs +86 -0
  360. package/src/memory/lib/runtime-fetcher.mjs +442 -0
  361. package/src/memory/lib/trace-store.mjs +728 -0
  362. package/src/memory/tool-defs.mjs +79 -0
  363. package/src/search/index.mjs +1173 -0
  364. package/src/search/lib/backends/anthropic-oauth.mjs +98 -0
  365. package/src/search/lib/backends/exa.mjs +50 -0
  366. package/src/search/lib/backends/firecrawl.mjs +61 -0
  367. package/src/search/lib/backends/gemini-api.mjs +83 -0
  368. package/src/search/lib/backends/grok-oauth.mjs +86 -0
  369. package/src/search/lib/backends/index.mjs +150 -0
  370. package/src/search/lib/backends/openai-api.mjs +144 -0
  371. package/src/search/lib/backends/openai-oauth.mjs +98 -0
  372. package/src/search/lib/backends/openai-web-search.mjs +76 -0
  373. package/src/search/lib/backends/tavily.mjs +55 -0
  374. package/src/search/lib/backends/xai-api.mjs +113 -0
  375. package/src/search/lib/cache.mjs +131 -0
  376. package/src/search/lib/config.mjs +192 -0
  377. package/src/search/lib/formatter.mjs +115 -0
  378. package/src/search/lib/provider-usage.mjs +67 -0
  379. package/src/search/lib/providers.mjs +47 -0
  380. package/src/search/lib/search-intent.mjs +109 -0
  381. package/src/search/lib/setup-handler.mjs +261 -0
  382. package/src/search/lib/state.mjs +201 -0
  383. package/src/search/lib/web-tools.mjs +1207 -0
  384. package/src/search/tool-defs.mjs +83 -0
  385. package/src/setup/defender-exclusion.mjs +183 -0
  386. package/src/shared/abort-controller.mjs +15 -0
  387. package/src/shared/atomic-file.mjs +420 -0
  388. package/src/shared/config.mjs +350 -0
  389. package/src/shared/daemon-recycle.mjs +108 -0
  390. package/src/shared/disable-claude-builtins.mjs +88 -0
  391. package/src/shared/err-text.mjs +12 -0
  392. package/src/shared/llm/cost.mjs +66 -0
  393. package/src/shared/llm/http-agent.mjs +123 -0
  394. package/src/shared/llm/index.mjs +41 -0
  395. package/src/shared/llm/pid-cleanup.mjs +27 -0
  396. package/src/shared/llm/usage-log.mjs +47 -0
  397. package/src/shared/plugin-paths.mjs +58 -0
  398. package/src/shared/schedules-store.mjs +70 -0
  399. package/src/shared/seed.mjs +119 -0
  400. package/src/shared/user-cwd.mjs +213 -0
  401. package/src/shared/user-data-guard.mjs +238 -0
  402. package/src/status/aggregator.mjs +584 -0
  403. package/src/status/server.mjs +413 -0
  404. package/tools.json +1653 -0
@@ -0,0 +1,455 @@
1
+ import { createReadStream, openSync, readSync, closeSync, readFileSync } from 'fs';
2
+ import { createInterface } from 'readline';
3
+ import { isBinaryFile } from './binary-file.mjs';
4
+ import { hashText } from './hash-utils.mjs';
5
+ import { normalizeOutputPath, countDisplayLines } from './path-utils.mjs';
6
+ import { normalizeErrorMessage } from './path-diagnostics.mjs';
7
+ import {
8
+ READ_MAX_SIZE_BYTES,
9
+ READ_STREAM_RANGE_MIN_BYTES,
10
+ } from './read-constants.mjs';
11
+ import {
12
+ LINE_NO_SEP,
13
+ renderReadLine,
14
+ } from './read-formatting.mjs';
15
+ import {
16
+ displayLineForRead,
17
+ splitRawLinesForHeadTail,
18
+ } from './read-lines.mjs';
19
+ import {
20
+ openForRead,
21
+ openTextPathForReadMeta,
22
+ detectReadEncoding,
23
+ } from './read-open.mjs';
24
+ import { capShellOutput } from './shell-output.mjs';
25
+ import { countTextStatsStreaming } from './text-stats.mjs';
26
+
27
+ function requireHelper(helpers, name) {
28
+ const fn = helpers?.[name];
29
+ if (typeof fn !== 'function') throw new Error(`missing read mode helper: ${name}`);
30
+ return fn;
31
+ }
32
+
33
+ // UTF-16 (LE/BE) files pass the binary NUL gate (BOM-exempt) but the
34
+ // head/tail/count/summary streaming and small-file paths decode bytes as
35
+ // UTF-8, emitting NUL-laden garbage. Detect the BOM up front and decode the
36
+ // whole file in-memory, bounded by the same READ_MAX_SIZE_BYTES cap the
37
+ // regular read path enforces. BE has no Node string encoding, so swap byte
38
+ // pairs to LE (even length) then decode as utf16le — mirrors read-single-tool.
39
+ // Returns null for non-UTF-16, { tooLarge:true } past the cap, or { content }.
40
+ function decodeUtf16Mode(meta) {
41
+ const enc = detectReadEncoding(meta.fullPath);
42
+ if (enc.encoding !== 'utf16le' && enc.encoding !== 'utf16be') return null;
43
+ if ((meta.st.size ?? 0) > READ_MAX_SIZE_BYTES) return { tooLarge: true };
44
+ const rawBuf = readFileSync(meta.fullPath);
45
+ if (enc.encoding === 'utf16le') {
46
+ return { content: rawBuf.subarray(enc.bomLen).toString('utf16le') };
47
+ }
48
+ const body = rawBuf.subarray(enc.bomLen);
49
+ const even = body.length & ~1;
50
+ return { content: Buffer.from(body.subarray(0, even)).swap16().toString('utf16le') };
51
+ }
52
+
53
+ function utf16TooLargeError(meta) {
54
+ return `Error: UTF-16 file too large for this mode (${meta.st.size} bytes exceeds ${READ_MAX_SIZE_BYTES}-byte cap): ${normalizeOutputPath(meta.fullPath)}`;
55
+ }
56
+
57
+ export async function executeHeadTool(args, workDir, readStateScope, helpers = {}) {
58
+ const streamHeadWindow = requireHelper(helpers, 'streamHeadWindow');
59
+ const recordReadSnapshot = requireHelper(helpers, 'recordReadSnapshot');
60
+ const n = Math.max(1, Math.min(parseInt(args.n ?? 20, 10) || 20, 2000));
61
+ let meta;
62
+ try { meta = openTextPathForReadMeta(args.path, workDir, { enforceSizeCap: false }); }
63
+ catch (err) { return `Error: ${err.message}`; }
64
+ const _u16 = decodeUtf16Mode(meta);
65
+ if (_u16) {
66
+ if (_u16.tooLarge) return utf16TooLargeError(meta);
67
+ const lines = splitRawLinesForHeadTail(_u16.content);
68
+ const sliced = lines.slice(0, n);
69
+ const rendered = sliced.map((l, i) => `${i + 1}${LINE_NO_SEP}${displayLineForRead(l, i)}`).join('\n');
70
+ const out = capShellOutput(rendered);
71
+ if (out === rendered && sliced.length > 0) {
72
+ const isFullFileView = sliced.length >= lines.length;
73
+ recordReadSnapshot(meta.fullPath, meta.st, readStateScope, {
74
+ source: 'read_head',
75
+ fileLineCount: lines.length,
76
+ ranges: isFullFileView ? [{ startLine: 1, endLine: Infinity }] : [{ startLine: 1, endLine: sliced.length }],
77
+ ...(isFullFileView ? { contentHash: hashText(_u16.content) } : { rangeHash: hashText(sliced.join('\n')) }),
78
+ });
79
+ }
80
+ return out;
81
+ }
82
+ if (meta.st.size > READ_STREAM_RANGE_MIN_BYTES) {
83
+ // Binary detection runs before the streamer for medium/large
84
+ // files — otherwise head would happily emit thousands of \uFFFD
85
+ // / NUL-laden lines from a packed binary. The small-file path
86
+ // below relies on openForRead's ETOOBIG branch + the existing
87
+ // small-file UTF-8 read, which already surfaces binary bytes
88
+ // via the same isBinaryFile check.
89
+ if (isBinaryFile(meta.fullPath, meta.st.size ?? 0)) {
90
+ return `Error: file appears to be binary (contains null bytes): ${normalizeOutputPath(meta.fullPath)}`;
91
+ }
92
+ try {
93
+ return await streamHeadWindow(meta.fullPath, meta.st, n, readStateScope, 'read_head_stream');
94
+ } catch (err) {
95
+ return `Error: ${normalizeErrorMessage(err instanceof Error ? err.message : String(err))}`;
96
+ }
97
+ }
98
+ let opened;
99
+ try { opened = await openForRead(args.path, workDir, {}); }
100
+ catch (err) {
101
+ if (err && err.code === 'ETOOBIG') {
102
+ if (err.fullPath && isBinaryFile(err.fullPath, err.size ?? 0)) {
103
+ return `Error: file appears to be binary (contains null bytes): ${normalizeOutputPath(err.fullPath)}`;
104
+ }
105
+ try {
106
+ const stream = createReadStream(err.fullPath, { encoding: 'utf-8' });
107
+ const rl = createInterface({ input: stream, crlfDelay: Infinity });
108
+ const collected = [];
109
+ const rawLines = [];
110
+ for await (let line of rl) {
111
+ if (collected.length === 0 && line.charCodeAt(0) === 0xFEFF) line = line.slice(1);
112
+ const lineNo = collected.length + 1;
113
+ rawLines.push(line);
114
+ collected.push(renderReadLine(lineNo, line));
115
+ if (collected.length >= n) { rl.close(); stream.destroy(); break; }
116
+ }
117
+ const rendered = collected.join('\n');
118
+ const out = capShellOutput(rendered);
119
+ if (out === rendered && rawLines.length > 0) {
120
+ recordReadSnapshot(err.fullPath, err.st, readStateScope, {
121
+ source: 'read_head_large',
122
+ ranges: [{ startLine: 1, endLine: rawLines.length }],
123
+ rangeHash: hashText(rawLines.join('\n')),
124
+ });
125
+ }
126
+ return out;
127
+ } catch (err2) {
128
+ return `Error: ${normalizeErrorMessage(err2 instanceof Error ? err2.message : String(err2))}`;
129
+ }
130
+ }
131
+ return `Error: ${err.message}`;
132
+ }
133
+ const lines = splitRawLinesForHeadTail(opened.content);
134
+ const sliced = lines.slice(0, n);
135
+ const rendered = sliced.map((l, i) => `${i + 1}${LINE_NO_SEP}${displayLineForRead(l, i)}`).join('\n');
136
+ const out = capShellOutput(rendered);
137
+ if (out === rendered && sliced.length > 0) {
138
+ const isFullFileView = sliced.length >= lines.length;
139
+ const snapshotMeta = {
140
+ source: 'read_head',
141
+ fileLineCount: lines.length,
142
+ ranges: isFullFileView
143
+ ? [{ startLine: 1, endLine: Infinity }]
144
+ : [{ startLine: 1, endLine: sliced.length }],
145
+ ...(isFullFileView ? { contentHash: hashText(opened.content) } : { rangeHash: hashText(sliced.join('\n')) }),
146
+ };
147
+ recordReadSnapshot(opened.fullPath, opened.st, readStateScope, snapshotMeta);
148
+ }
149
+ return out;
150
+ }
151
+
152
+ export async function executeTailTool(args, workDir, readStateScope, helpers = {}) {
153
+ const renderTailWindowSync = requireHelper(helpers, 'renderTailWindowSync');
154
+ const recordReadSnapshot = requireHelper(helpers, 'recordReadSnapshot');
155
+ const n = Math.max(1, Math.min(parseInt(args.n ?? 20, 10) || 20, 2000));
156
+ let meta;
157
+ try { meta = openTextPathForReadMeta(args.path, workDir, { enforceSizeCap: false }); }
158
+ catch (err) { return `Error: ${err.message}`; }
159
+ const _u16 = decodeUtf16Mode(meta);
160
+ if (_u16) {
161
+ if (_u16.tooLarge) return utf16TooLargeError(meta);
162
+ const lines = splitRawLinesForHeadTail(_u16.content);
163
+ const sliced = lines.slice(-n);
164
+ const startIdx = lines.length - sliced.length;
165
+ const rendered = sliced.map((l, i) => `${startIdx + i + 1}${LINE_NO_SEP}${displayLineForRead(l, startIdx + i)}`).join('\n');
166
+ const out = capShellOutput(rendered);
167
+ if (out === rendered && sliced.length > 0) {
168
+ const isFullFileView = sliced.length >= lines.length;
169
+ recordReadSnapshot(meta.fullPath, meta.st, readStateScope, {
170
+ source: 'read_tail',
171
+ fileLineCount: lines.length,
172
+ ranges: isFullFileView ? [{ startLine: 1, endLine: Infinity }] : [{ startLine: startIdx + 1, endLine: lines.length }],
173
+ ...(isFullFileView ? { contentHash: hashText(_u16.content) } : { rangeHash: hashText(sliced.join('\n')) }),
174
+ });
175
+ }
176
+ return out;
177
+ }
178
+ if (meta.st.size > READ_MAX_SIZE_BYTES) {
179
+ try {
180
+ if (isBinaryFile(meta.fullPath, meta.st.size ?? 0)) {
181
+ return `Error: file appears to be binary (contains null bytes): ${normalizeOutputPath(meta.fullPath)}`;
182
+ }
183
+ return renderTailWindowSync(meta.fullPath, meta.st, n, readStateScope, { exactLineNumbers: false, source: 'read_tail_large' });
184
+ } catch (err) {
185
+ return `Error: ${normalizeErrorMessage(err instanceof Error ? err.message : String(err))}`;
186
+ }
187
+ }
188
+ if (meta.st.size > READ_STREAM_RANGE_MIN_BYTES) {
189
+ if (isBinaryFile(meta.fullPath, meta.st.size ?? 0)) {
190
+ return `Error: file appears to be binary (contains null bytes): ${normalizeOutputPath(meta.fullPath)}`;
191
+ }
192
+ try {
193
+ return renderTailWindowSync(meta.fullPath, meta.st, n, readStateScope, { exactLineNumbers: true, source: 'read_tail_window' });
194
+ } catch (err) {
195
+ return `Error: ${normalizeErrorMessage(err instanceof Error ? err.message : String(err))}`;
196
+ }
197
+ }
198
+ let opened;
199
+ try { opened = await openForRead(args.path, workDir, {}); }
200
+ catch (err) {
201
+ if (err && err.code === 'ETOOBIG') {
202
+ try {
203
+ const { fullPath, st } = err;
204
+ if (isBinaryFile(fullPath, st.size ?? 0)) {
205
+ return `Error: file appears to be binary (contains null bytes): ${normalizeOutputPath(fullPath)}`;
206
+ }
207
+ return renderTailWindowSync(fullPath, st, n, readStateScope, { exactLineNumbers: false, source: 'read_tail_large' });
208
+ } catch (err2) {
209
+ return `Error: ${normalizeErrorMessage(err2 instanceof Error ? err2.message : String(err2))}`;
210
+ }
211
+ }
212
+ return `Error: ${err.message}`;
213
+ }
214
+ const lines = splitRawLinesForHeadTail(opened.content);
215
+ const sliced = lines.slice(-n);
216
+ const startIdx = lines.length - sliced.length;
217
+ const rendered = sliced.map((l, i) => `${startIdx + i + 1}${LINE_NO_SEP}${displayLineForRead(l, startIdx + i)}`).join('\n');
218
+ const out = capShellOutput(rendered);
219
+ if (out === rendered && sliced.length > 0) {
220
+ const isFullFileView = sliced.length >= lines.length;
221
+ const snapshotMeta = {
222
+ source: 'read_tail',
223
+ fileLineCount: lines.length,
224
+ ranges: isFullFileView
225
+ ? [{ startLine: 1, endLine: Infinity }]
226
+ : [{ startLine: startIdx + 1, endLine: lines.length }],
227
+ ...(isFullFileView ? { contentHash: hashText(opened.content) } : { rangeHash: hashText(sliced.join('\n')) }),
228
+ };
229
+ recordReadSnapshot(opened.fullPath, opened.st, readStateScope, snapshotMeta);
230
+ }
231
+ return out;
232
+ }
233
+
234
+ export async function executeWcTool(args, workDir, helpers = {}) {
235
+ const countLogicalLinesBytesSync = requireHelper(helpers, 'countLogicalLinesBytesSync');
236
+ let meta;
237
+ try { meta = openTextPathForReadMeta(args.path, workDir, { enforceSizeCap: false }); }
238
+ catch (err) { return `Error: ${err.message}`; }
239
+ const _u16 = decodeUtf16Mode(meta);
240
+ if (_u16) {
241
+ if (_u16.tooLarge) return utf16TooLargeError(meta);
242
+ const lines = countDisplayLines(_u16.content);
243
+ const words = (_u16.content.match(/\S+/g) || []).length;
244
+ return `lines\t${lines}\twords\t${words}\tbytes\t${meta.st.size}`;
245
+ }
246
+ if (meta.st.size > READ_MAX_SIZE_BYTES) {
247
+ if (isBinaryFile(meta.fullPath, meta.st.size ?? 0)) {
248
+ return `Error: file appears to be binary (contains null bytes): ${normalizeOutputPath(meta.fullPath)}`;
249
+ }
250
+ try {
251
+ const lines = countLogicalLinesBytesSync(meta.fullPath, meta.st.size, meta.st);
252
+ return `lines\t${lines}\twords\t-\tbytes\t${meta.st.size}\t(words skipped: file > cap)`;
253
+ } catch (err) {
254
+ return `Error: ${normalizeErrorMessage(err instanceof Error ? err.message : String(err))}`;
255
+ }
256
+ }
257
+ if (meta.st.size > READ_STREAM_RANGE_MIN_BYTES) {
258
+ if (isBinaryFile(meta.fullPath, meta.st.size ?? 0)) {
259
+ return `Error: file appears to be binary (contains null bytes): ${normalizeOutputPath(meta.fullPath)}`;
260
+ }
261
+ try {
262
+ const stats = await countTextStatsStreaming(meta.fullPath, meta.st.size);
263
+ return `lines\t${stats.lines}\twords\t${stats.words}\tbytes\t${stats.bytes}`;
264
+ } catch (err) {
265
+ return `Error: ${normalizeErrorMessage(err instanceof Error ? err.message : String(err))}`;
266
+ }
267
+ }
268
+ let opened;
269
+ try { opened = await openForRead(args.path, workDir, {}); }
270
+ catch (err) {
271
+ if (err && err.code === 'ETOOBIG') {
272
+ let lines = 0;
273
+ const stream = createReadStream(err.fullPath, { encoding: 'utf-8' });
274
+ const rl = createInterface({ input: stream, crlfDelay: Infinity });
275
+ for await (const _ of rl) lines++;
276
+ return `lines\t${lines}\twords\t-\tbytes\t${err.size}\t(words skipped: file > cap)`;
277
+ }
278
+ return `Error: ${err.message}`;
279
+ }
280
+ const { content, st } = opened;
281
+ const lines = countDisplayLines(content);
282
+ const words = (content.match(/\S+/g) || []).length;
283
+ return `lines\t${lines}\twords\t${words}\tbytes\t${st.size}`;
284
+ }
285
+
286
+ const SUMMARY_SYMBOL_RE = new RegExp([
287
+ '^\\s*(?:export\\s+)?(?:(?:async\\s+)?function|class|interface|type|enum)\\s+[A-Za-z_$][\\w$]*',
288
+ '^\\s*(?:export\\s+)?(?:const|let|var)\\s+[A-Za-z_$][\\w$]*\\s*=',
289
+ '^\\s*(?:def|class)\\s+[A-Za-z_][\\w]*',
290
+ '^\\s*(?:pub\\s+)?(?:async\\s+)?fn\\s+[A-Za-z_][\\w]*',
291
+ '^\\s*(?:pub\\s+)?(?:struct|enum|trait|impl)\\s+[A-Za-z_][\\w]*(?:<[^>]+>)?',
292
+ '^\\s*func\\s+(?:\\([^)]*\\)\\s*)?[A-Za-z_][\\w]*\\s*\\(',
293
+ '^\\s*type\\s+[A-Za-z_][\\w]*\\s+(?:struct|interface)\\b',
294
+ '^\\s*(?:(?:public|private|protected|internal|static|sealed|abstract|partial|async|final)\\s+)*(?:class|interface|enum|record|struct)\\s+[A-Za-z_][\\w]*',
295
+ '^\\s*(?:(?:public|private|protected|internal|static|virtual|override|async|final|synchronized|abstract|partial)\\s+)+(?:[\\w<>\\[\\],.?]+\\s+)+[A-Za-z_][\\w]*\\s*\\(',
296
+ '^\\s*#{1,3}\\s+\\S',
297
+ ].join('|'));
298
+
299
+ async function collectSummarySymbols(fullPath, limit) {
300
+ const out = [];
301
+ const stream = createReadStream(fullPath, { encoding: 'utf-8' });
302
+ const rl = createInterface({ input: stream, crlfDelay: Infinity });
303
+ let lineNo = 0;
304
+ try {
305
+ for await (let line of rl) {
306
+ lineNo += 1;
307
+ if (lineNo === 1 && line.charCodeAt(0) === 0xFEFF) line = line.slice(1);
308
+ if (!SUMMARY_SYMBOL_RE.test(line)) continue;
309
+ out.push(renderReadLine(lineNo, line.trimEnd(), { truncateLongLine: true }));
310
+ if (out.length >= limit) {
311
+ rl.close();
312
+ stream.destroy();
313
+ break;
314
+ }
315
+ }
316
+ } finally {
317
+ stream.destroy();
318
+ }
319
+ return out;
320
+ }
321
+
322
+ // In-memory variant for already-decoded text (UTF-16 path): same regex/limit
323
+ // as the streaming collector, but over a decoded string instead of a utf-8
324
+ // byte stream.
325
+ function collectSummarySymbolsFromContent(content, limit) {
326
+ const out = [];
327
+ const lines = String(content ?? '').split(/\r?\n/);
328
+ for (let i = 0; i < lines.length; i++) {
329
+ let line = lines[i];
330
+ if (i === 0 && line.charCodeAt(0) === 0xFEFF) line = line.slice(1);
331
+ if (!SUMMARY_SYMBOL_RE.test(line)) continue;
332
+ out.push(renderReadLine(i + 1, line.trimEnd(), { truncateLongLine: true }));
333
+ if (out.length >= limit) break;
334
+ }
335
+ return out;
336
+ }
337
+
338
+ export async function executeSummaryTool(args, workDir, readStateScope, helpers = {}) {
339
+ const countLogicalLinesBytesSync = requireHelper(helpers, 'countLogicalLinesBytesSync');
340
+ const recordReadSnapshot = requireHelper(helpers, 'recordReadSnapshot');
341
+ // Raised from 40/120 to 200/1000: large files (>1500 lines) lose most
342
+ // of their structure under a 40-cap, and the worker ends up calling
343
+ // find_symbol{mode:"overview"} as a follow-up. tail-trim in result-
344
+ // compression remains the actual bound on payload size, so a higher
345
+ // cap costs nothing on small files and surfaces full outline on large.
346
+ const limit = Math.max(1, Math.min(parseInt(args.n ?? args.limit ?? 200, 10) || 200, 1000));
347
+ let meta;
348
+ try { meta = openTextPathForReadMeta(args.path, workDir, { enforceSizeCap: false }); }
349
+ catch (err) { return `Error: ${normalizeErrorMessage(err.message, workDir)}`; }
350
+ if (isBinaryFile(meta.fullPath, meta.st.size ?? 0)) {
351
+ return `Error: file appears to be binary (contains null bytes): ${normalizeOutputPath(args.path || meta.fullPath)}`;
352
+ }
353
+
354
+ const _u16 = decodeUtf16Mode(meta);
355
+ if (_u16) {
356
+ if (_u16.tooLarge) return utf16TooLargeError(meta);
357
+ const stats = {
358
+ lines: countDisplayLines(_u16.content),
359
+ words: (_u16.content.match(/\S+/g) || []).length,
360
+ bytes: meta.st.size,
361
+ };
362
+ recordReadSnapshot(meta.fullPath, meta.st, readStateScope, { source: 'read_summary', ranges: [] });
363
+ const symbols = collectSummarySymbolsFromContent(_u16.content, limit);
364
+ const outputPath = normalizeOutputPath(args.path || meta.fullPath);
365
+ const out = [
366
+ `summary ${outputPath}`,
367
+ `lines\t${stats.lines}\twords\t${stats.words}\tbytes\t${stats.bytes}`,
368
+ `symbols\t${symbols.length}${symbols.length >= limit ? ` (capped at ${limit})` : ''}`,
369
+ ];
370
+ if (symbols.length > 0) out.push('', ...symbols);
371
+ else out.push('', '(no obvious symbols/headings found)');
372
+ return capShellOutput(out.join('\n'));
373
+ }
374
+
375
+ let stats;
376
+ try {
377
+ stats = meta.st.size > READ_MAX_SIZE_BYTES
378
+ ? { lines: countLogicalLinesBytesSync(meta.fullPath, meta.st.size, meta.st), words: '-', bytes: meta.st.size }
379
+ : (meta.st.size > READ_STREAM_RANGE_MIN_BYTES
380
+ ? await countTextStatsStreaming(meta.fullPath, meta.st.size)
381
+ : null);
382
+ if (!stats) {
383
+ const opened = await openForRead(args.path, workDir, {});
384
+ stats = {
385
+ lines: countDisplayLines(opened.content),
386
+ words: (opened.content.match(/\S+/g) || []).length,
387
+ bytes: opened.st.size,
388
+ };
389
+ }
390
+ } catch (err) {
391
+ return `Error: ${normalizeErrorMessage(err instanceof Error ? err.message : String(err))}`;
392
+ }
393
+
394
+ recordReadSnapshot(meta.fullPath, meta.st, readStateScope, {
395
+ source: 'read_summary',
396
+ ranges: [],
397
+ });
398
+
399
+ let symbols = [];
400
+ try {
401
+ symbols = await collectSummarySymbols(meta.fullPath, limit);
402
+ } catch { /* best-effort outline */ }
403
+ const outputPath = normalizeOutputPath(args.path || meta.fullPath);
404
+ const lines = [
405
+ `summary ${outputPath}`,
406
+ `lines\t${stats.lines}\twords\t${stats.words}\tbytes\t${stats.bytes}`,
407
+ `symbols\t${symbols.length}${symbols.length >= limit ? ` (capped at ${limit})` : ''}`,
408
+ ];
409
+ if (symbols.length > 0) {
410
+ lines.push('', ...symbols);
411
+ } else {
412
+ lines.push('', '(no obvious symbols/headings found)');
413
+ }
414
+ return capShellOutput(lines.join('\n'));
415
+ }
416
+
417
+ // Hex dump for byte-level/EOL inspection. Binary-safe (no isBinaryFile gate)
418
+ // because that is the point of the mode. Default 256 bytes, max 8192 per call,
419
+ // paginate via n + offset.
420
+ export async function executeHexTool(args, workDir, readStateScope, helpers = {}) {
421
+ const recordReadSnapshot = requireHelper(helpers, 'recordReadSnapshot');
422
+ const n = Math.max(16, Math.min(parseInt(args.n ?? 256, 10) || 256, 8192));
423
+ const offset = Math.max(0, parseInt(args.offset ?? 0, 10) || 0);
424
+ let meta;
425
+ try { meta = openTextPathForReadMeta(args.path, workDir, { enforceSizeCap: false, skipBinary: true }); }
426
+ catch (err) { return `Error: ${err.message}`; }
427
+ let fd;
428
+ try {
429
+ fd = openSync(meta.fullPath, 'r');
430
+ const buf = Buffer.alloc(n);
431
+ const bytesRead = readSync(fd, buf, 0, n, offset);
432
+ const slice = buf.subarray(0, bytesRead);
433
+ const lines = [
434
+ `hex ${normalizeOutputPath(args.path || meta.fullPath)} offset=${offset} read=${bytesRead}/${meta.st.size}`,
435
+ ];
436
+ for (let i = 0; i < bytesRead; i += 16) {
437
+ const chunk = slice.subarray(i, Math.min(i + 16, bytesRead));
438
+ const hex = Array.from(chunk).map((b) => b.toString(16).padStart(2, '0')).join(' ');
439
+ const ascii = Array.from(chunk)
440
+ .map((b) => (b >= 0x20 && b < 0x7f) ? String.fromCharCode(b) : '.')
441
+ .join('');
442
+ const addrHex = (offset + i).toString(16).padStart(8, '0');
443
+ lines.push(`${addrHex} ${hex.padEnd(48, ' ')} |${ascii}|`);
444
+ }
445
+ if (offset + bytesRead < meta.st.size) {
446
+ lines.push(`... ${meta.st.size - offset - bytesRead} more bytes (paginate via n + offset)`);
447
+ }
448
+ recordReadSnapshot(meta.fullPath, meta.st, readStateScope, { source: 'read_hex', ranges: [] });
449
+ return capShellOutput(lines.join('\n'));
450
+ } catch (err) {
451
+ return `Error: ${normalizeErrorMessage(err instanceof Error ? err.message : String(err))}`;
452
+ } finally {
453
+ if (fd !== undefined) try { closeSync(fd); } catch { /* fd may already be closed */ }
454
+ }
455
+ }
@@ -0,0 +1,190 @@
1
+ import { closeSync, lstatSync, openSync, readSync, realpathSync, statSync } from 'fs';
2
+ import { readFile } from 'fs/promises';
3
+ import { dirname } from 'path';
4
+ import {
5
+ normalizeInputPath,
6
+ normalizeOutputPath,
7
+ resolveAgainstCwd,
8
+ } from './path-utils.mjs';
9
+ import {
10
+ findSimilarFile,
11
+ listSiblings,
12
+ normalizeErrorMessage,
13
+ } from './path-diagnostics.mjs';
14
+ import { isBinaryFile } from './binary-file.mjs';
15
+ import { READ_MAX_SIZE_BYTES } from './read-constants.mjs';
16
+ import { normalizePathAndStripLineCoordinate } from './read-args.mjs';
17
+ import {
18
+ isBlockedDevicePath,
19
+ isSpecialFileStat,
20
+ isUncPath,
21
+ isWindowsDevicePath,
22
+ hasUnsafeWin32Component,
23
+ } from './device-paths.mjs';
24
+
25
+ function detectReadEncodingFromBuffer(head) {
26
+ const n = head.length;
27
+ if (n >= 2 && head[0] === 0xff && head[1] === 0xfe) {
28
+ return { encoding: 'utf16le', bomLen: 2 };
29
+ }
30
+ if (n >= 2 && head[0] === 0xfe && head[1] === 0xff) {
31
+ return { encoding: 'utf16be', bomLen: 2 };
32
+ }
33
+ if (n >= 3 && head[0] === 0xef && head[1] === 0xbb && head[2] === 0xbf) {
34
+ return { encoding: 'utf8', bomLen: 3 };
35
+ }
36
+ return { encoding: 'utf8', bomLen: 0 };
37
+ }
38
+
39
+ export function detectReadEncoding(fullPath) {
40
+ let fd;
41
+ try {
42
+ fd = openSync(fullPath, 'r');
43
+ const head = Buffer.alloc(3);
44
+ const n = readSync(fd, head, 0, 3, 0);
45
+ return detectReadEncodingFromBuffer(head.subarray(0, n));
46
+ } catch {
47
+ return { encoding: 'utf8', bomLen: 0 };
48
+ } finally {
49
+ if (fd !== undefined) { try { closeSync(fd); } catch {} }
50
+ }
51
+ }
52
+
53
+ function decodeReadBuffer(buf, enc) {
54
+ if (enc.encoding === 'utf16le') {
55
+ return buf.subarray(enc.bomLen).toString('utf16le');
56
+ }
57
+ if (enc.encoding === 'utf16be') {
58
+ // Node has no 'utf16be' string encoding; swap byte pairs to LE in a
59
+ // copy, then decode as utf16le. swap16 needs an even length, so drop a
60
+ // trailing odd byte before swapping.
61
+ const body = buf.subarray(enc.bomLen);
62
+ const even = body.length & ~1;
63
+ return Buffer.from(body.subarray(0, even)).swap16().toString('utf16le');
64
+ }
65
+ const slice = enc.bomLen ? buf.subarray(enc.bomLen) : buf;
66
+ try {
67
+ return new TextDecoder('utf-8', { fatal: true }).decode(slice);
68
+ } catch (err) {
69
+ const e = new Error(`invalid UTF-8 in text file: ${err instanceof Error ? err.message : String(err)}`);
70
+ e.code = 'EUTF8';
71
+ throw e;
72
+ }
73
+ }
74
+
75
+ /** String-phase guards shared by full read and mode reads. Returns error message or null. */
76
+ export function readPathStringGuardError(filePath, workDir) {
77
+ if (typeof filePath !== 'string' || !filePath) {
78
+ return 'path is required';
79
+ }
80
+ const guardedPath = normalizePathAndStripLineCoordinate(filePath, workDir);
81
+ if (isUncPath(guardedPath)) {
82
+ return `cannot read UNC / SMB path (network credential leak risk): ${normalizeOutputPath(guardedPath)}`;
83
+ }
84
+ if (isWindowsDevicePath(guardedPath)) {
85
+ return `cannot read Windows device path (reserved name or raw-device namespace): ${normalizeOutputPath(guardedPath)}`;
86
+ }
87
+ if (hasUnsafeWin32Component(guardedPath)) {
88
+ return `cannot read Windows path with trailing dot/space or NTFS ADS suffix (bypasses device guard): ${normalizeOutputPath(guardedPath)}`;
89
+ }
90
+ if (isBlockedDevicePath(guardedPath)) {
91
+ return `cannot read device file (would block or produce infinite output): ${normalizeOutputPath(guardedPath)}`;
92
+ }
93
+ const fullPath = resolveAgainstCwd(guardedPath, workDir);
94
+ if (isUncPath(fullPath)) {
95
+ return `cannot read UNC / SMB path (network credential leak risk): ${normalizeOutputPath(fullPath)}`;
96
+ }
97
+ if (isWindowsDevicePath(fullPath)) {
98
+ return `cannot read Windows device path (reserved name or raw-device namespace): ${normalizeOutputPath(fullPath)}`;
99
+ }
100
+ if (hasUnsafeWin32Component(fullPath)) {
101
+ return `cannot read Windows path with trailing dot/space or NTFS ADS suffix (bypasses device guard): ${normalizeOutputPath(fullPath)}`;
102
+ }
103
+ if (isBlockedDevicePath(fullPath)) {
104
+ return `cannot read device file (would block or produce infinite output): ${normalizeOutputPath(fullPath)}`;
105
+ }
106
+ return null;
107
+ }
108
+
109
+ /** Post-stat / symlink guards. Returns error message or null. */
110
+ export function readPathStatGuardError(userPath, fullPath, st) {
111
+ if (isSpecialFileStat(st)) {
112
+ return `cannot read special file (FIFO / character / block device / socket): ${normalizeOutputPath(userPath)}`;
113
+ }
114
+ try {
115
+ const lst = lstatSync(fullPath);
116
+ if (lst?.isSymbolicLink?.()) {
117
+ let realTarget = null;
118
+ try { realTarget = realpathSync(fullPath); } catch { realTarget = null; }
119
+ if (realTarget && realTarget !== fullPath) {
120
+ if (isBlockedDevicePath(realTarget)) {
121
+ return `cannot read device file via symlink (would block or produce infinite output): ${normalizeOutputPath(userPath)} → ${normalizeOutputPath(realTarget)}`;
122
+ }
123
+ if (isUncPath(realTarget)) {
124
+ return `cannot read UNC / SMB path via symlink (network credential leak risk): ${normalizeOutputPath(userPath)} → ${normalizeOutputPath(realTarget)}`;
125
+ }
126
+ if (isWindowsDevicePath(realTarget)) {
127
+ return `cannot read Windows device path via symlink (reserved name or raw-device namespace): ${normalizeOutputPath(userPath)} → ${normalizeOutputPath(realTarget)}`;
128
+ }
129
+ try {
130
+ const rst = statSync(realTarget);
131
+ if (isSpecialFileStat(rst)) {
132
+ return `cannot read special file via symlink (FIFO / character / block device / socket): ${normalizeOutputPath(userPath)} → ${normalizeOutputPath(realTarget)}`;
133
+ }
134
+ } catch { /* ENOENT surfaces later */ }
135
+ }
136
+ }
137
+ } catch { /* best-effort */ }
138
+ return null;
139
+ }
140
+
141
+ // Shared file-open prologue for read-flavoured tools (tail / wc / diff / modes).
142
+ export function openTextPathForReadMeta(filePath, workDir, opts = {}) {
143
+ const guardErr = readPathStringGuardError(filePath, workDir);
144
+ if (guardErr) {
145
+ throw Object.assign(new Error(guardErr), { code: 'EARG' });
146
+ }
147
+ const norm = normalizePathAndStripLineCoordinate(filePath, workDir);
148
+ const fullPath = resolveAgainstCwd(norm, workDir);
149
+ let st;
150
+ try { st = statSync(fullPath); }
151
+ catch (err) {
152
+ const similar = findSimilarFile(fullPath);
153
+ const siblings = listSiblings(dirname(fullPath));
154
+ const hint = (similar ? ` Did you mean "${normalizeOutputPath(similar)}"?` : '')
155
+ + ` Siblings: [${siblings.join(', ')}].`;
156
+ const msg = normalizeErrorMessage(err instanceof Error ? err.message : String(err)) + hint;
157
+ throw Object.assign(new Error(msg), { code: 'ENOENT' });
158
+ }
159
+ const statGuard = readPathStatGuardError(filePath, fullPath, st);
160
+ if (statGuard) {
161
+ throw Object.assign(new Error(statGuard), { code: 'ESPECIAL' });
162
+ }
163
+ if (opts.enforceSizeCap !== false && st.size > READ_MAX_SIZE_BYTES) {
164
+ throw Object.assign(
165
+ new Error(`file size ${st.size} bytes exceeds ${READ_MAX_SIZE_BYTES}-byte cap`),
166
+ { code: 'ETOOBIG', size: st.size, fullPath, st });
167
+ }
168
+ if (opts.skipBinary !== true && isBinaryFile(fullPath, st.size)) {
169
+ throw Object.assign(
170
+ new Error(`file appears to be binary (contains null bytes): ${normalizeOutputPath(norm)}`),
171
+ { code: 'EBINARY' });
172
+ }
173
+ return { fullPath, displayPath: normalizeOutputPath(norm), st, size: st.size };
174
+ }
175
+
176
+ export async function openForRead(filePath, workDir, opts = {}) {
177
+ const meta = openTextPathForReadMeta(filePath, workDir, opts);
178
+ const rawBuf = await readFile(meta.fullPath);
179
+ const enc = detectReadEncodingFromBuffer(rawBuf.subarray(0, Math.min(rawBuf.length, 3)));
180
+ let content;
181
+ try {
182
+ content = decodeReadBuffer(rawBuf, enc);
183
+ } catch (err) {
184
+ if (err?.code === 'EUTF8' && opts.routeInvalidUtf8ToHex === true) {
185
+ throw Object.assign(new Error(err.message), { code: 'EUTF8', fullPath: meta.fullPath, st: meta.st });
186
+ }
187
+ throw err;
188
+ }
189
+ return { ...meta, content, readEncoding: enc };
190
+ }