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,593 @@
1
+ import { readdirSync } from 'fs';
2
+ import { basename, relative } from 'path';
3
+ import {
4
+ extractGlobBaseDirectory,
5
+ hasGlobMagic,
6
+ normalizeInputPath,
7
+ normalizeOutputPath,
8
+ resolveAgainstCwd,
9
+ } from './path-utils.mjs';
10
+ import { normalizeErrorMessage } from './path-diagnostics.mjs';
11
+ import { isUncPath, isWindowsDevicePath, hasUnsafeWin32Component } from './device-paths.mjs';
12
+ import { capShellOutput } from './shell-output.mjs';
13
+ import {
14
+ buildListCacheKey,
15
+ DEFAULT_IGNORE_GLOBS,
16
+ } from './search-builders.mjs';
17
+ import { markScopedCacheIncomplete } from '../../session/cache/scoped-cache-outcome.mjs';
18
+ import {
19
+ cacheGet,
20
+ cacheSet,
21
+ getCachedReadOnlyStat,
22
+ statPathsForMtime,
23
+ lstatPathsForMtime,
24
+ } from './cache-layers.mjs';
25
+ import {
26
+ compileSimpleGlob,
27
+ NOISE_DIR_NAMES,
28
+ walkDir,
29
+ } from './glob-walk.mjs';
30
+ import { formatMtime } from './list-formatting.mjs';
31
+ import { runRg } from './rg-runner.mjs';
32
+ import { fuzzyRank } from './fuzzy-match.mjs';
33
+ import { assertPathReachable } from './fs-reachability.mjs';
34
+
35
+ const FIND_WALK_TIMEOUT_MS = 20_000;
36
+ const LIST_WALK_TIMEOUT_MS = 20_000;
37
+ const LIST_ABSOLUTE_CAP = 50_000;
38
+
39
+ /** undefined / invalid / negative → defaultCap; 0 = no page cap (absolute caps still apply). */
40
+ function normalizeListHeadLimit(raw, defaultCap) {
41
+ if (raw === undefined || raw === null || raw === '') return defaultCap;
42
+ const n = Number(raw);
43
+ if (!Number.isFinite(n) || n < 0) return defaultCap;
44
+ return Math.floor(n);
45
+ }
46
+
47
+ // UNC / Windows-device / NTFS-ADS guard for directory-walking modes
48
+ // (list / tree / find). Walking a UNC share auto-authenticates to the
49
+ // remote host (NTLM hash leak); a raw-device / reserved-name path can
50
+ // hang or grant raw access. Mirrors the read path's string-based checks.
51
+ // Returns an Error string when the path is blocked, else null.
52
+ function listGuardPath(p) {
53
+ if (typeof isUncPath === 'function' && isUncPath(p))
54
+ return `Error: cannot walk UNC / SMB path (network credential leak risk): ${normalizeOutputPath(p)}`;
55
+ if (typeof isWindowsDevicePath === 'function' && isWindowsDevicePath(p))
56
+ return `Error: cannot walk Windows device path (reserved name or raw-device namespace): ${normalizeOutputPath(p)}`;
57
+ if (typeof hasUnsafeWin32Component === 'function' && hasUnsafeWin32Component(p))
58
+ return `Error: cannot walk Windows path with trailing dot/space or NTFS ADS suffix (bypasses device guard): ${normalizeOutputPath(p)}`;
59
+ return null;
60
+ }
61
+
62
+ export async function executeListTool(args, workDir, options = {}) {
63
+ if (typeof args.fuzzy === 'string' && args.fuzzy.length > 0) {
64
+ return executeFuzzyFind(args, workDir);
65
+ }
66
+ if (args.mode === 'tree') return executeTreeTool(args, workDir, options);
67
+ if (args.mode === 'find') return executeFindFilesTool(args, workDir, options);
68
+ args.path = normalizeInputPath(args.path);
69
+ if (!args.name && hasGlobMagic(args.path)) {
70
+ return executeFindFilesTool({ ...args, mode: 'find' }, workDir);
71
+ }
72
+ const inputPath = args.path || '.';
73
+ const depth = Math.min(Math.max(parseInt(args.depth ?? 1, 10) || 1, 1), 10);
74
+ const hidden = Boolean(args.hidden);
75
+ const sort = ['name', 'mtime', 'size'].includes(args.sort) ? args.sort : 'name';
76
+ const typeFilter = ['any', 'file', 'dir'].includes(args.type) ? args.type : 'any';
77
+ const headLimit = normalizeListHeadLimit(args.head_limit, 200);
78
+ const offset = typeof args.offset === 'number' && args.offset > 0 ? args.offset : 0;
79
+ const needsGlobalStat = sort === 'mtime' || sort === 'size';
80
+ const includeNoise = Boolean(args.include_noise);
81
+ const _listGuard = listGuardPath(inputPath);
82
+ if (_listGuard) return _listGuard;
83
+ const fullPath = resolveAgainstCwd(inputPath, workDir);
84
+ const _listGuardFull = listGuardPath(fullPath);
85
+ if (_listGuardFull) return _listGuardFull;
86
+ const cacheKey = buildListCacheKey({
87
+ mode: 'list',
88
+ inputPath: normalizeOutputPath(fullPath),
89
+ depth,
90
+ hidden,
91
+ sort,
92
+ typeFilter,
93
+ headLimit,
94
+ offset,
95
+ includeNoise,
96
+ });
97
+ const cached = cacheGet(cacheKey);
98
+ if (cached !== null) return cached;
99
+ try { await assertPathReachable(fullPath); }
100
+ catch (err) { return `Error: ${normalizeErrorMessage(err instanceof Error ? err.message : String(err))}`; }
101
+ let st;
102
+ try { st = getCachedReadOnlyStat(fullPath); }
103
+ catch (err) { return `Error: ${normalizeErrorMessage(err instanceof Error ? err.message : String(err))}`; }
104
+ if (!st.isDirectory()) return `Error: not a directory — ${normalizeOutputPath(fullPath)}`;
105
+
106
+ const rows = [];
107
+ // Width guard: depth is capped above, but a single very wide directory
108
+ // tree could push unbounded rows before sort/slice runs and exhaust
109
+ // memory. Mirror the find-mode FIND_ABSOLUTE_CAP + walk deadline so the
110
+ // accumulator stops growing once the cap or timeout trips. Small dirs
111
+ // never hit either bound, so normal behavior is unchanged.
112
+ let truncatedByCap = false;
113
+ const walkDeadline = Date.now() + LIST_WALK_TIMEOUT_MS;
114
+ walkDir(fullPath, {
115
+ hidden,
116
+ maxDepth: depth,
117
+ excludeDirNames: includeNoise ? null : NOISE_DIR_NAMES,
118
+ visit: (ent, entPath) => {
119
+ if (Date.now() > walkDeadline) { truncatedByCap = true; return false; }
120
+ const isDir = ent.isDirectory();
121
+ const isFile = ent.isFile();
122
+ if (typeFilter === 'file' && !isFile) return;
123
+ if (typeFilter === 'dir' && !isDir) return;
124
+ const entType = isDir ? 'dir' : (isFile ? 'file' : (ent.isSymbolicLink() ? 'symlink' : 'other'));
125
+ rows.push({
126
+ path: entPath,
127
+ type: entType,
128
+ size: 0,
129
+ mtimeMs: 0,
130
+ fullPath: entPath,
131
+ });
132
+ if (rows.length >= LIST_ABSOLUTE_CAP) {
133
+ truncatedByCap = true;
134
+ return false;
135
+ }
136
+ // Pre-sort truncation removed: a global name sort needs all
137
+ // candidates collected before slicing, otherwise the visible
138
+ // window depends on traversal order rather than sort order.
139
+ },
140
+ });
141
+
142
+ if (needsGlobalStat && rows.length > 0) {
143
+ // lstat: symlinks should report own metadata, not the target's.
144
+ const stats = await lstatPathsForMtime(rows.map((row) => row.fullPath), workDir, 64, { deadlineMs: 5000 });
145
+ for (let i = 0; i < rows.length; i++) {
146
+ const item = stats[i];
147
+ if (!item?.stat) continue;
148
+ rows[i].size = item.size;
149
+ rows[i].mtimeMs = item.mtimeMs;
150
+ }
151
+ }
152
+
153
+ if (sort === 'mtime') rows.sort((a, b) => b.mtimeMs - a.mtimeMs);
154
+ else if (sort === 'size') rows.sort((a, b) => b.size - a.size);
155
+ else rows.sort((a, b) => a.path.localeCompare(b.path));
156
+
157
+ const windowed = offset > 0 ? rows.slice(offset) : rows;
158
+ const sliced = headLimit > 0 ? windowed.slice(0, headLimit) : windowed;
159
+ if (!needsGlobalStat && sliced.length > 0) {
160
+ // Use lstat so a symlink reports its own size/mtime instead of
161
+ // the target's. The walker already typed symlinks from Dirent;
162
+ // following the link here would lie about the listed entry.
163
+ const stats = await lstatPathsForMtime(sliced.map((row) => row.fullPath), workDir, 64, { deadlineMs: 5000 });
164
+ for (let i = 0; i < sliced.length; i++) {
165
+ const item = stats[i];
166
+ if (!item?.stat) continue;
167
+ sliced[i].size = item.size;
168
+ sliced[i].mtimeMs = item.mtimeMs;
169
+ }
170
+ }
171
+ const lines = sliced.map(r =>
172
+ `${normalizeOutputPath(r.path)}\t${r.type}\t${r.size}\t${formatMtime(r.mtimeMs)}`);
173
+ if (windowed.length > sliced.length) lines.push(`... [entries ${offset + 1}-${offset + sliced.length} of ${rows.length}; pass offset:${offset + sliced.length} to continue]`);
174
+ if (truncatedByCap) lines.push(`... walk truncated at ${LIST_ABSOLUTE_CAP} rows or ${LIST_WALK_TIMEOUT_MS}ms timeout; narrow the path or lower depth for a complete listing`);
175
+ let emptyMsg = '(empty directory)';
176
+ if (lines.length === 0 && (typeFilter !== 'any' || hidden === false)) {
177
+ const filterParts = [];
178
+ if (typeFilter !== 'any') filterParts.push(`type=${typeFilter}`);
179
+ if (hidden === false) {
180
+ let hasHidden = false;
181
+ try {
182
+ const entries = readdirSync(fullPath, { withFileTypes: true });
183
+ hasHidden = entries.some(e => e.name && e.name.startsWith('.'));
184
+ } catch {}
185
+ if (hasHidden) filterParts.push(`hidden=false (dotfiles present — pass hidden:true to include)`);
186
+ else filterParts.push(`hidden=false`);
187
+ }
188
+ emptyMsg = `(no entries match filter) ${filterParts.join(', ')} path=${inputPath}`;
189
+ }
190
+ const out = lines.join('\n') || emptyMsg;
191
+ if (options?.scopedCacheOutcome && (truncatedByCap || windowed.length > sliced.length)) {
192
+ markScopedCacheIncomplete(options.scopedCacheOutcome);
193
+ }
194
+ cacheSet(cacheKey, out, { scopes: [fullPath] });
195
+ // ② completion progress (claude "Found N" parity). Best-effort, no-op
196
+ // when onProgress is absent (no progressToken).
197
+ if (typeof options?.onProgress === 'function') {
198
+ try { options.onProgress(`${windowed.length} entries`); } catch { /* best-effort */ }
199
+ }
200
+ return out;
201
+ }
202
+
203
+ export async function executeTreeTool(args, workDir, options = {}) {
204
+ args.path = normalizeInputPath(args.path);
205
+ const inputPath = args.path || '.';
206
+ const depth = Math.min(Math.max(parseInt(args.depth ?? 3, 10) || 3, 1), 6);
207
+ const hidden = Boolean(args.hidden);
208
+ const headLimit = normalizeListHeadLimit(args.head_limit, 200);
209
+ const offset = typeof args.offset === 'number' && args.offset > 0 ? args.offset : 0;
210
+ const includeNoise = Boolean(args.include_noise);
211
+ const _treeGuard = listGuardPath(inputPath);
212
+ if (_treeGuard) return _treeGuard;
213
+ const fullPath = resolveAgainstCwd(inputPath, workDir);
214
+ const _treeGuardFull = listGuardPath(fullPath);
215
+ if (_treeGuardFull) return _treeGuardFull;
216
+ const cacheKey = buildListCacheKey({
217
+ mode: 'tree',
218
+ inputPath: normalizeOutputPath(fullPath),
219
+ depth,
220
+ hidden,
221
+ sort: '',
222
+ typeFilter: '',
223
+ headLimit,
224
+ offset,
225
+ includeNoise,
226
+ });
227
+ const cached = cacheGet(cacheKey);
228
+ if (cached !== null) return cached;
229
+ try { await assertPathReachable(fullPath); }
230
+ catch (err) { return `Error: ${normalizeErrorMessage(err instanceof Error ? err.message : String(err))}`; }
231
+ let st;
232
+ try { st = getCachedReadOnlyStat(fullPath); }
233
+ catch (err) { return `Error: ${normalizeErrorMessage(err instanceof Error ? err.message : String(err))}`; }
234
+ if (!st.isDirectory()) return `Error: not a directory — ${normalizeOutputPath(fullPath)}`;
235
+ const lines = [`${normalizeOutputPath(fullPath)}/`];
236
+ const prefixStack = [''];
237
+ const TREE_BRANCH_LINE_CAP = 500;
238
+ walkDir(fullPath, {
239
+ hidden,
240
+ maxDepth: depth,
241
+ excludeDirNames: includeNoise ? null : NOISE_DIR_NAMES,
242
+ sort: (a, b) => {
243
+ const ad = a.isDirectory(), bd = b.isDirectory();
244
+ if (ad !== bd) return ad ? -1 : 1;
245
+ return a.name.localeCompare(b.name);
246
+ },
247
+ visit: (ent, _entPath, ctx) => {
248
+ const prefix = prefixStack[ctx.depth - 1] || '';
249
+ const branch = ctx.isLast ? '└── ' : '├── ';
250
+ const display = ent.isDirectory() ? `${ent.name}/` : ent.name;
251
+ lines.push(`${prefix}${branch}${display}`);
252
+ if (ent.isDirectory()) {
253
+ prefixStack[ctx.depth] = prefix + (ctx.isLast ? ' ' : '│ ');
254
+ }
255
+ if (headLimit !== 0) {
256
+ const gatherLimit = headLimit > 0
257
+ ? offset + headLimit + 1
258
+ : offset + TREE_BRANCH_LINE_CAP + 1;
259
+ // Exclude the root line (lines[0]) from the body-row count:
260
+ // the windowed slice operates on lines.slice(1), so gather
261
+ // must measure body rows, not total. Without -1 the sentinel
262
+ // "+N more entries" misfires off-by-one on the boundary.
263
+ if (lines.length - 1 >= gatherLimit) return false;
264
+ }
265
+ },
266
+ });
267
+ const root = lines[0];
268
+ const body = lines.slice(1);
269
+ const windowed = offset > 0 ? body.slice(offset) : body;
270
+ // head_limit:0 means "no cap" (Infinity); negative/NaN means "use default cap".
271
+ const branchLimit = headLimit === 0
272
+ ? Infinity
273
+ : (headLimit > 0 ? headLimit : TREE_BRANCH_LINE_CAP);
274
+ const sliced = branchLimit === Infinity ? windowed : windowed.slice(0, branchLimit);
275
+ const outLines = [root, ...sliced];
276
+ if (windowed.length > sliced.length) {
277
+ // The walk stops gathering at gatherLimit, so when body filled to the
278
+ // cap the true total is unknown — render `N+` so the caller keeps
279
+ // paging instead of reading the capped count as the real total.
280
+ const gatherCap = headLimit > 0 ? offset + headLimit + 1 : offset + TREE_BRANCH_LINE_CAP + 1;
281
+ const totalLabel = body.length >= gatherCap ? `${body.length}+` : `${body.length}`;
282
+ outLines.push(`... [entries ${offset + 1}-${offset + sliced.length} of ${totalLabel}; pass offset:${offset + sliced.length} to continue]`);
283
+ }
284
+ const TREE_OUTPUT_CHAR_CAP = 50_000;
285
+ let out = outLines.join('\n');
286
+ let outputCharTruncated = false;
287
+ if (out.length > TREE_OUTPUT_CHAR_CAP) {
288
+ outputCharTruncated = true;
289
+ out = out.slice(0, TREE_OUTPUT_CHAR_CAP) + `\n... [output truncated at ${Math.round(TREE_OUTPUT_CHAR_CAP/1024)} KB; narrow path or lower depth]`;
290
+ }
291
+ if (options?.scopedCacheOutcome && (windowed.length > sliced.length || outputCharTruncated)) {
292
+ markScopedCacheIncomplete(options.scopedCacheOutcome);
293
+ }
294
+ cacheSet(cacheKey, out, { scopes: [fullPath] });
295
+ return out;
296
+ }
297
+
298
+ // Fuzzy filename search (codex file-search / nucleo style): collect the file
299
+ // list via `rg --files`, then rank by subsequence score. Lets a partial name
300
+ // like "edeng" surface "edit-engine.mjs" in one call instead of guessing an
301
+ // exact glob pattern. Honors path/hidden/include_noise/depth/head_limit.
302
+ async function executeFuzzyFind(args, workDir) {
303
+ const query = String(args.fuzzy);
304
+ const inputPath = normalizeInputPath(args.path) || '.';
305
+ const guard = listGuardPath(inputPath);
306
+ if (guard) return guard;
307
+ const fullPath = resolveAgainstCwd(inputPath, workDir);
308
+ const guardFull = listGuardPath(fullPath);
309
+ if (guardFull) return guardFull;
310
+ const hidden = Boolean(args.hidden);
311
+ const includeNoise = Boolean(args.include_noise);
312
+ // head_limit:0 means "no cap" per list semantics — keep 0 distinct from default.
313
+ const headLimit = normalizeListHeadLimit(args.head_limit, 40);
314
+ const depth = args.depth != null ? Math.max(parseInt(args.depth, 10) || 1, 1) : null;
315
+ const rgArgs = ['--files', '--no-ignore'];
316
+ if (hidden) rgArgs.push('--hidden');
317
+ if (depth != null) rgArgs.push('--max-depth', String(depth));
318
+ if (!includeNoise) {
319
+ for (const ex of DEFAULT_IGNORE_GLOBS) rgArgs.push('--glob', ex);
320
+ }
321
+ rgArgs.push('.');
322
+ let stdout;
323
+ try {
324
+ stdout = await runRg(rgArgs, { cwd: fullPath });
325
+ } catch (err) {
326
+ return `Error: ${normalizeErrorMessage(err instanceof Error ? err.message : String(err))}`;
327
+ }
328
+ const items = String(stdout)
329
+ .split('\n')
330
+ // Strip only the trailing CR from rg's line split — do NOT trim, or a
331
+ // filename with leading/trailing spaces would be corrupted.
332
+ .map((p) => (p.endsWith('\r') ? p.slice(0, -1) : p))
333
+ .filter((p) => p.length > 0)
334
+ .map((p) => ({ path: normalizeOutputPath(p.replace(/^\.[/\\]/, '')) }));
335
+ const ranked = fuzzyRank(query, items, headLimit);
336
+ if (ranked.length === 0) return `(no fuzzy match for "${query}")`;
337
+ const out = ranked.map((r) => r.item.path).join('\n');
338
+ return headLimit > 0 && ranked.length >= headLimit
339
+ ? `${out}\n... (top ${headLimit}; raise head_limit for more)`
340
+ : out;
341
+ }
342
+
343
+ export async function executeFindFilesTool(args, workDir, options = {}) {
344
+ args.path = normalizeInputPath(args.path);
345
+ let inputPath = args.path || '.';
346
+ let namePattern = typeof args.name === 'string' ? args.name : null;
347
+ if (!namePattern && hasGlobMagic(inputPath)) {
348
+ const { baseDir, relativePattern } = extractGlobBaseDirectory(inputPath);
349
+ inputPath = baseDir || '.';
350
+ namePattern = relativePattern.replace(/^\/+/, '');
351
+ }
352
+ if (namePattern) namePattern = normalizeInputPath(namePattern).replace(/^\/+/, '');
353
+ const typeFilter = ['any', 'file', 'dir'].includes(args.type) ? args.type : 'any';
354
+ const sortMode = ['name', 'size', 'mtime'].includes(args.sort) ? args.sort : 'mtime';
355
+ const minSize = typeof args.min_size === 'number' && args.min_size > 0 ? args.min_size : null;
356
+ const maxSize = typeof args.max_size === 'number' && args.max_size >= 0 ? args.max_size : null;
357
+ const headLimit = normalizeListHeadLimit(args.head_limit, 100);
358
+ const offset = typeof args.offset === 'number' && args.offset > 0 ? args.offset : 0;
359
+ const includeNoise = Boolean(args.include_noise);
360
+ const hidden = Boolean(args.hidden);
361
+ // Clamp depth to >=1 when caller passes it; null means unbounded (legacy
362
+ // find-mode behavior). Forwarded to walkDir.maxDepth and the rg fast
363
+ // path's --max-depth so both code paths honor the cap consistently.
364
+ const depth = args.depth != null
365
+ ? Math.max(parseInt(args.depth, 10) || 1, 1)
366
+ : null;
367
+ const _findGuard = listGuardPath(inputPath);
368
+ if (_findGuard) return _findGuard;
369
+ const fullPath = resolveAgainstCwd(inputPath, workDir);
370
+ const _findGuardFull = listGuardPath(fullPath);
371
+ if (_findGuardFull) return _findGuardFull;
372
+ const cacheKey = buildListCacheKey({
373
+ mode: 'find',
374
+ inputPath: normalizeOutputPath(fullPath),
375
+ depth: depth ?? '',
376
+ hidden,
377
+ sort: sortMode,
378
+ typeFilter,
379
+ headLimit,
380
+ offset,
381
+ namePattern,
382
+ minSize,
383
+ maxSize,
384
+ modifiedAfter: args.modified_after || '',
385
+ modifiedBefore: args.modified_before || '',
386
+ includeNoise,
387
+ });
388
+ const cached = cacheGet(cacheKey);
389
+ if (cached !== null) return cached;
390
+
391
+ const parseTime = (v) => {
392
+ if (typeof v !== 'string') return null;
393
+ const m = v.match(/^(\d+)([hdm])$/);
394
+ if (m) {
395
+ const n = parseInt(m[1], 10);
396
+ const unit = m[2] === 'h' ? 3600 * 1000
397
+ : m[2] === 'd' ? 86400 * 1000
398
+ : 60 * 1000;
399
+ return Date.now() - n * unit;
400
+ }
401
+ const t = Date.parse(v);
402
+ return isNaN(t) ? null : t;
403
+ };
404
+ const after = parseTime(args.modified_after);
405
+ const before = parseTime(args.modified_before);
406
+ // An unparseable date must FAIL, not silently disable the filter — a
407
+ // caller who passed a filter believes the listing is filtered.
408
+ if (args.modified_after && after === null) {
409
+ return `Error: invalid modified_after ${JSON.stringify(args.modified_after)}; expected an ISO date/time or a relative window like 90m / 12h / 7d`;
410
+ }
411
+ if (args.modified_before && before === null) {
412
+ return `Error: invalid modified_before ${JSON.stringify(args.modified_before)}; expected an ISO date/time or a relative window like 90m / 12h / 7d`;
413
+ }
414
+
415
+ // `name` is documented as a SUBSTRING filter (use glob mode for patterns).
416
+ // Compile it as a glob ONLY when it actually contains glob metacharacters;
417
+ // otherwise match by case-insensitive contains. The bug was that EVERY name
418
+ // was glob-compiled, so a plain fragment like ".mjs" anchored-matched nothing.
419
+ const nameIsGlob = Boolean(namePattern && /[*?\[\]{}]/.test(namePattern));
420
+ let nameRegex = null, nameRootOptionalRegex = null;
421
+ if (namePattern && nameIsGlob) {
422
+ try {
423
+ // compileSimpleGlob throws (R16 DoS caps: >256 brace variants /
424
+ // oversized pattern/regex body) — convert to a tool-error string.
425
+ nameRegex = compileSimpleGlob(namePattern);
426
+ nameRootOptionalRegex = namePattern.startsWith('**/')
427
+ ? compileSimpleGlob(namePattern.slice(3))
428
+ : null;
429
+ } catch (err) {
430
+ return `Error: ${normalizeErrorMessage(err instanceof Error ? err.message : String(err))}`;
431
+ }
432
+ }
433
+ const nameLower = namePattern ? namePattern.toLowerCase() : null;
434
+ const namePatternHasPath = Boolean(namePattern && /[\\/]/.test(namePattern));
435
+ const matchesFindNamePattern = (entName, entPath) => {
436
+ if (!namePattern) return true;
437
+ const subject = namePatternHasPath
438
+ ? normalizeOutputPath(relative(fullPath, entPath))
439
+ : entName;
440
+ if (nameIsGlob) return nameRegex.test(subject) || Boolean(nameRootOptionalRegex?.test(subject));
441
+ return subject.toLowerCase().includes(nameLower);
442
+ };
443
+
444
+ try { await assertPathReachable(fullPath); }
445
+ catch (err) { return `Error: ${normalizeErrorMessage(err instanceof Error ? err.message : String(err))}`; }
446
+ let rootStat;
447
+ try { rootStat = getCachedReadOnlyStat(fullPath); }
448
+ catch (err) { return `Error: ${normalizeErrorMessage(err instanceof Error ? err.message : String(err))}`; }
449
+ if (!rootStat.isDirectory()) return `Error: not a directory — ${normalizeOutputPath(fullPath)}`;
450
+
451
+ const matches = [];
452
+ const FIND_ABSOLUTE_CAP = 50_000;
453
+ let truncatedByCap = false;
454
+ let rgStdoutTruncated = false;
455
+ let rgStdoutPartial = false;
456
+ const useBatchedStat = minSize === null && maxSize === null && after === null && before === null;
457
+ let handledByRgFiles = false;
458
+ if (useBatchedStat && typeFilter === 'file') {
459
+ try {
460
+ // --no-ignore: do not consult .gitignore. The slow walk path
461
+ // never honours .gitignore, so the fast path must match that
462
+ // contract — otherwise the rg branch silently returns fewer
463
+ // results than the fallback. Noise dirs are still excluded
464
+ // via DEFAULT_IGNORE_GLOBS below (unless include_noise).
465
+ const rgArgs = ['--files', '--no-ignore'];
466
+ if (hidden) rgArgs.push('--hidden');
467
+ if (depth != null) rgArgs.push('--max-depth', String(depth));
468
+ if (!includeNoise) {
469
+ for (const ex of DEFAULT_IGNORE_GLOBS) rgArgs.push('--glob', ex);
470
+ }
471
+ // Substring `name` (no glob metachars) → contains-glob so rg's
472
+ // pre-filter matches the JS matcher; explicit globs pass through.
473
+ if (namePattern) rgArgs.push('--iglob', nameIsGlob ? namePattern : `*${namePattern}*`);
474
+ rgArgs.push('.');
475
+ const stdout = await runRg(rgArgs, { cwd: fullPath });
476
+ rgStdoutTruncated = Boolean(stdout && typeof stdout === 'object' && stdout.truncated);
477
+ rgStdoutPartial = Boolean(stdout && typeof stdout === 'object' && stdout.partial);
478
+ const candidates = [];
479
+ for (const line of String(stdout).split('\n')) {
480
+ const trimmed = line.trim();
481
+ if (!trimmed) continue;
482
+ const candidate = resolveAgainstCwd(normalizeInputPath(trimmed), fullPath);
483
+ if (!matchesFindNamePattern(basename(candidate), candidate)) continue;
484
+ candidates.push(candidate);
485
+ if (candidates.length >= FIND_ABSOLUTE_CAP) {
486
+ truncatedByCap = true;
487
+ break;
488
+ }
489
+ }
490
+ const withStat = await statPathsForMtime(candidates, workDir, 64, { deadlineMs: 5000 });
491
+ for (const item of withStat) {
492
+ if (!item?.stat) continue;
493
+ matches.push({ path: item.full, size: item.size, mtimeMs: item.mtimeMs });
494
+ }
495
+ handledByRgFiles = true;
496
+ } catch {
497
+ handledByRgFiles = false;
498
+ }
499
+ }
500
+ if (!handledByRgFiles && useBatchedStat) {
501
+ const candidates = [];
502
+ const walkDeadline1 = Date.now() + FIND_WALK_TIMEOUT_MS;
503
+ walkDir(fullPath, {
504
+ hidden,
505
+ maxDepth: depth ?? Infinity,
506
+ excludeDirNames: includeNoise ? null : NOISE_DIR_NAMES,
507
+ visit: (ent, entPath) => {
508
+ if (Date.now() > walkDeadline1) { truncatedByCap = true; return false; }
509
+ const isDir = ent.isDirectory();
510
+ const isFile = ent.isFile();
511
+ if (typeFilter === 'file' && !isFile) return;
512
+ if (typeFilter === 'dir' && !isDir) return;
513
+ if (!matchesFindNamePattern(ent.name, entPath)) return;
514
+ candidates.push(entPath);
515
+ if (candidates.length >= FIND_ABSOLUTE_CAP) {
516
+ truncatedByCap = true;
517
+ return false;
518
+ }
519
+ },
520
+ });
521
+ const withStat = await statPathsForMtime(candidates, workDir, 64, { deadlineMs: 5000 });
522
+ for (const item of withStat) {
523
+ if (!item?.stat) continue;
524
+ matches.push({ path: item.full, size: item.size, mtimeMs: item.mtimeMs });
525
+ }
526
+ } else if (!handledByRgFiles) {
527
+ // Size filters only have meaning for files; when the caller passed
528
+ // min_size/max_size without also restricting type, narrow the
529
+ // result set to files so directories don't slip past with their
530
+ // (usually 0-byte) directory size.
531
+ const sizeFiltered = (minSize !== null || maxSize !== null);
532
+ const effectiveTypeFilter = sizeFiltered && typeFilter === 'any' ? 'file' : typeFilter;
533
+ const candidates = [];
534
+ const walkDeadline2 = Date.now() + FIND_WALK_TIMEOUT_MS;
535
+ walkDir(fullPath, {
536
+ hidden,
537
+ maxDepth: depth ?? Infinity,
538
+ excludeDirNames: includeNoise ? null : NOISE_DIR_NAMES,
539
+ visit: (ent, entPath) => {
540
+ if (Date.now() > walkDeadline2) { truncatedByCap = true; return false; }
541
+ const isDir = ent.isDirectory();
542
+ const isFile = ent.isFile();
543
+ if (effectiveTypeFilter === 'file' && !isFile) return;
544
+ if (effectiveTypeFilter === 'dir' && !isDir) return;
545
+ if (!matchesFindNamePattern(ent.name, entPath)) return;
546
+ candidates.push(entPath);
547
+ if (candidates.length >= FIND_ABSOLUTE_CAP) {
548
+ truncatedByCap = true;
549
+ return false;
550
+ }
551
+ },
552
+ });
553
+ const withStat = await statPathsForMtime(candidates, workDir, 64, { deadlineMs: 5000 });
554
+ for (const item of withStat) {
555
+ if (!item?.stat) continue;
556
+ const { stat, full: entPath, mtimeMs } = item;
557
+ if (stat.isFile()) {
558
+ if (minSize !== null && stat.size < minSize) continue;
559
+ if (maxSize !== null && stat.size > maxSize) continue;
560
+ }
561
+ if (after !== null && mtimeMs < after) continue;
562
+ if (before !== null && mtimeMs > before) continue;
563
+ matches.push({ path: entPath, size: stat.size, mtimeMs });
564
+ if (matches.length >= FIND_ABSOLUTE_CAP) {
565
+ truncatedByCap = true;
566
+ break;
567
+ }
568
+ }
569
+ }
570
+
571
+ matches.sort((a, b) => {
572
+ if (sortMode === 'name') return normalizeOutputPath(a.path).localeCompare(normalizeOutputPath(b.path));
573
+ if (sortMode === 'size') return b.size - a.size;
574
+ return b.mtimeMs - a.mtimeMs;
575
+ });
576
+ const windowed = offset > 0 ? matches.slice(offset) : matches;
577
+ const sliced = headLimit > 0 ? windowed.slice(0, headLimit) : windowed;
578
+ const lines = sliced.map(m =>
579
+ `${normalizeOutputPath(m.path)}\t${m.size}\t${formatMtime(m.mtimeMs)}`);
580
+ if (windowed.length > sliced.length) lines.push(`... [entries ${offset + 1}-${offset + sliced.length} of ${matches.length}; pass offset:${offset + sliced.length} to continue]`);
581
+ if (rgStdoutTruncated) lines.push('... [warning] rg stdout truncated at 20MB cap; results incomplete');
582
+ if (rgStdoutPartial) lines.push('... [warning] rg exit 2 (partial results); listing may be incomplete');
583
+ if (truncatedByCap) lines.push(`... walk truncated at ${FIND_ABSOLUTE_CAP} matches; narrow the scope (path/name/modified_after) for accurate global sort`);
584
+ const out = lines.join('\n') || '(no matches)';
585
+ if (options?.scopedCacheOutcome && (truncatedByCap || rgStdoutTruncated || rgStdoutPartial || windowed.length > sliced.length)) {
586
+ markScopedCacheIncomplete(options.scopedCacheOutcome);
587
+ }
588
+ const findIncomplete = truncatedByCap || rgStdoutTruncated || rgStdoutPartial || windowed.length > sliced.length;
589
+ if (!findIncomplete) {
590
+ cacheSet(cacheKey, out, { scopes: [fullPath] });
591
+ }
592
+ return out;
593
+ }
@@ -0,0 +1,89 @@
1
+ import { existsSync } from 'fs';
2
+ import { dirname, join, resolve } from 'path';
3
+ import { performance } from 'perf_hooks';
4
+ import { fileURLToPath } from 'url';
5
+ import { snapshotCoversFullFile } from './snapshot-helpers.mjs';
6
+ import { getPluginData } from '../../config.mjs';
7
+ import { findCachedPatchBinary } from '../patch-binary-fetcher.mjs';
8
+ import { runServerEdit } from '../patch.mjs';
9
+
10
+ const PLUGIN_ROOT = process.env.CLAUDE_PLUGIN_ROOT
11
+ || resolve(dirname(fileURLToPath(import.meta.url)), '../../../../..');
12
+ const NATIVE_EDIT_DEFAULT_BIN = join(
13
+ PLUGIN_ROOT,
14
+ 'native/mixdog-patch/target/release',
15
+ process.platform === 'win32' ? 'mixdog-patch.exe' : 'mixdog-patch',
16
+ );
17
+
18
+ export function nativeEditMode() {
19
+ return String(process.env.MIXDOG_EDIT_NATIVE || 'auto').toLowerCase();
20
+ }
21
+
22
+ export function nativeEditBinPath() {
23
+ const override = process.env.MIXDOG_EDIT_NATIVE_BIN || process.env.MIXDOG_PATCH_NATIVE_BIN;
24
+ if (override) return override;
25
+ if (existsSync(NATIVE_EDIT_DEFAULT_BIN)) return NATIVE_EDIT_DEFAULT_BIN;
26
+ return findCachedPatchBinary(getPluginData()) || NATIVE_EDIT_DEFAULT_BIN;
27
+ }
28
+
29
+ export function nativeEditShouldAttempt({ editSnapshot, oldStr, newStr, preloadedContent, preloadedRawBuf }) {
30
+ const mode = nativeEditMode();
31
+ if (/^(0|false|no|off|js|legacy)$/i.test(mode)) return false;
32
+ if (!existsSync(nativeEditBinPath())) return false;
33
+ if (!snapshotCoversFullFile(editSnapshot)) return false;
34
+ if (preloadedContent !== null || preloadedRawBuf !== null) return false;
35
+ if (typeof oldStr !== 'string' || oldStr.length === 0 || typeof newStr !== 'string') return false;
36
+ if (/^(1|true|yes|on|native)$/i.test(mode)) return true;
37
+ // auto: the persistent server removed per-call spawn cost, so route edits to
38
+ // native edit2 by default (B3). Same-size edits keep the JS in-place partial
39
+ // write, which rewrites bytes in place instead of the whole file.
40
+ const oldBytes = Buffer.byteLength(oldStr, 'utf-8');
41
+ const newBytes = Buffer.byteLength(newStr, 'utf-8');
42
+ if (oldBytes === newBytes) return false;
43
+ return true;
44
+ }
45
+
46
+ export async function runNativeExactEdit({ fullPath, oldStr, newStr, replaceAll, signal = null }) {
47
+ if (signal?.aborted) {
48
+ return { ok: false, fallback: false, error: signal.reason?.message || signal.reason || 'native edit aborted' };
49
+ }
50
+ const oldBuf = Buffer.from(oldStr, 'utf-8');
51
+ const newBuf = Buffer.from(newStr, 'utf-8');
52
+ const started = performance.now();
53
+ try {
54
+ // PARITY GUARD: the native engine MATCHES via the curly-quote fold
55
+ // tier but applies new_string verbatim, silently downgrading the
56
+ // file's typographic quotes (JS slow path preserves them via
57
+ // preserveQuoteTypography). When old_string carries quote-family
58
+ // chars — the only inputs that can land on the curly tier — probe
59
+ // with a dry run (persistent server, ~ms) and defer curly-tier
60
+ // matches to the JS editor.
61
+ if (/["'‘’“”]/.test(oldStr)) {
62
+ const probe = await runServerEdit({ fullPath, oldBuf, newBuf, replaceAll, dryRun: true, signal });
63
+ if (probe?.tier === 'curly') {
64
+ return { ok: false, fallback: true, error: 'curly-quote fold match — deferred to JS editor for typography preservation' };
65
+ }
66
+ }
67
+ const res = await runServerEdit({ fullPath, oldBuf, newBuf, replaceAll, signal });
68
+ return {
69
+ ok: true,
70
+ replacements: res.replacements,
71
+ readMs: res.readMs,
72
+ applyMs: res.applyMs,
73
+ writeMs: res.writeMs,
74
+ totalMs: res.totalMs,
75
+ roundtripMs: res.roundtripMs ?? (performance.now() - started),
76
+ stage: res.tier,
77
+ contentHash: res.contentHash,
78
+ };
79
+ } catch (err) {
80
+ if (err?.name === 'AbortError') {
81
+ return { ok: false, fallback: false, error: err.message };
82
+ }
83
+ const msg = String(err?.message || err);
84
+ // Tier misses and not-found map to a JS fallback; transport/spawn errors
85
+ // also fall back so a server hiccup never blocks an edit.
86
+ const fallback = /old_string (?:not found|found \d+ times)|not valid UTF-8|no exact match|not found|server/i.test(msg);
87
+ return { ok: false, fallback, error: msg };
88
+ }
89
+ }