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,36 @@
1
+ import { countSplitLines } from './path-utils.mjs';
2
+
3
+ export const SHELL_OUTPUT_MAX_CHARS = 30_000;
4
+
5
+ export const SMART_BASH_MAX_LINES = 400;
6
+ export const SMART_BASH_MAX_BYTES = 30_000;
7
+ export const SMART_BASH_HEAD_LINES = 80;
8
+ export const SMART_BASH_TAIL_LINES = 80;
9
+
10
+ export function smartMiddleTruncate(content) {
11
+ const s = typeof content === 'string' ? content : String(content ?? '');
12
+ if (s.length <= SMART_BASH_MAX_BYTES) {
13
+ const fastLines = s.split('\n');
14
+ if (fastLines.length <= SMART_BASH_MAX_LINES) return s;
15
+ const head = fastLines.slice(0, SMART_BASH_HEAD_LINES).join('\n');
16
+ const tail = fastLines.slice(-SMART_BASH_TAIL_LINES).join('\n');
17
+ const middle = fastLines.length - SMART_BASH_HEAD_LINES - SMART_BASH_TAIL_LINES;
18
+ return `${head}\n\n... [TRUNCATED — ${middle} lines middle elided; total ${fastLines.length} lines. Rerun with tighter filters for more] ...\n\n${tail}`;
19
+ }
20
+ const lines = s.split('\n');
21
+ if (lines.length <= SMART_BASH_MAX_LINES) {
22
+ const head = s.slice(0, SMART_BASH_MAX_BYTES);
23
+ return `${head}\n\n... [TRUNCATED — output exceeded ${Math.round(SMART_BASH_MAX_BYTES / 1024)} KB on a single line] ...`;
24
+ }
25
+ const head = lines.slice(0, SMART_BASH_HEAD_LINES).join('\n');
26
+ const tail = lines.slice(-SMART_BASH_TAIL_LINES).join('\n');
27
+ const middle = lines.length - SMART_BASH_HEAD_LINES - SMART_BASH_TAIL_LINES;
28
+ const totalKb = Math.round(s.length / 1024);
29
+ return `${head}\n\n... [TRUNCATED — ${middle} lines middle elided; total ${lines.length} lines / ${totalKb} KB. Rerun with tighter filters for more] ...\n\n${tail}`;
30
+ }
31
+
32
+ export function capShellOutput(content) {
33
+ const s = typeof content === 'string' ? content : String(content ?? '');
34
+ if (s.length <= SHELL_OUTPUT_MAX_CHARS && countSplitLines(s) <= SMART_BASH_MAX_LINES) return s;
35
+ return smartMiddleTruncate(s);
36
+ }
@@ -0,0 +1,214 @@
1
+ import { spawnSync } from 'child_process';
2
+ import { existsSync } from 'fs';
3
+ import { basename, dirname, join } from 'path';
4
+
5
+ let _resolvedShell = null;
6
+ // Per-kind cache for resolveShellFor(). 'default' aliases resolveShell()'s
7
+ // singleton; 'bash'/'powershell' get their own memoized slots. ONLY a
8
+ // successful (non-null) resolution is cached — a resolution MISS is
9
+ // deliberately not memoized so the next call re-probes (Git Bash / pwsh may
10
+ // be installed mid-session, and PATH can change for a long-lived host).
11
+ const _resolvedShellByKind = new Map();
12
+
13
+ function shellTypeFor(shell) {
14
+ const stem = basename(String(shell || '')).toLowerCase().replace(/\.exe$/, '');
15
+ if (stem === 'pwsh' || stem === 'powershell') return 'powershell';
16
+ if (stem === 'bash' || stem === 'zsh' || stem === 'sh') return 'posix';
17
+ return process.platform === 'win32' ? 'powershell' : 'posix';
18
+ }
19
+
20
+ function shellSpec(shell, shellType = shellTypeFor(shell)) {
21
+ if (shellType === 'powershell') {
22
+ return {
23
+ shell,
24
+ shellArg: '-Command',
25
+ shellArgs: ['-NoLogo', '-NoProfile', '-NonInteractive', '-WindowStyle', 'Hidden', '-Command'],
26
+ shellType,
27
+ };
28
+ }
29
+ return { shell, shellArg: '-c', shellArgs: ['-c'], shellType };
30
+ }
31
+
32
+ function firstExistingPathFromWhere(commandName) {
33
+ try {
34
+ const r = spawnSync('cmd.exe', ['/d', '/s', '/c', `where ${commandName}`], {
35
+ stdio: ['ignore', 'pipe', 'ignore'],
36
+ windowsHide: true,
37
+ timeout: 1000,
38
+ });
39
+ if (r.status !== 0 || !r.stdout) return null;
40
+ const lines = r.stdout.toString('utf8').split(/\r?\n/).map(s => s.trim()).filter(Boolean);
41
+ return lines.find(p => existsSync(p)) || null;
42
+ } catch {
43
+ return null;
44
+ }
45
+ }
46
+
47
+ function resolveWindowsPowerShell() {
48
+ const pwsh = firstExistingPathFromWhere('pwsh.exe');
49
+ if (pwsh) return shellSpec(pwsh, 'powershell');
50
+
51
+ const systemRoot = process.env.SystemRoot || process.env.WINDIR || 'C:\\Windows';
52
+ const bundled = join(systemRoot, 'System32', 'WindowsPowerShell', 'v1.0', 'powershell.exe');
53
+ if (existsSync(bundled)) return shellSpec(bundled, 'powershell');
54
+
55
+ const powershell = firstExistingPathFromWhere('powershell.exe');
56
+ if (powershell) return shellSpec(powershell, 'powershell');
57
+
58
+ return shellSpec('powershell.exe', 'powershell');
59
+ }
60
+
61
+ export function resolveShell() {
62
+ if (_resolvedShell) return _resolvedShell;
63
+ const isWindows = process.platform === 'win32'
64
+ || !!(process.env.WINDIR || process.env.SystemRoot);
65
+ if (!isWindows) {
66
+ _resolvedShell = shellSpec('/bin/sh', 'posix');
67
+ return _resolvedShell;
68
+ }
69
+ const explicit = process.env.CLAUDE_CODE_SHELL;
70
+ if (explicit && shellTypeFor(explicit) === 'powershell') {
71
+ _resolvedShell = shellSpec(explicit);
72
+ return _resolvedShell;
73
+ }
74
+ _resolvedShell = resolveWindowsPowerShell();
75
+ return _resolvedShell;
76
+ }
77
+
78
+ function _isWindows() {
79
+ return process.platform === 'win32' || !!(process.env.WINDIR || process.env.SystemRoot);
80
+ }
81
+
82
+ // Resolve Git Bash on Windows. Strategy (invariant-based, no silent fallback):
83
+ // 1. Iterate ALL `where git.exe` hits — a shim (Scoop/Chocolatey/winget under
84
+ // a \shims\ dir) or a second install can be the FIRST match yet not a real
85
+ // Git-for-Windows layout with a sibling bash.exe. For each, walk a few
86
+ // ancestor levels of <gitDir> and probe the two known launcher shapes
87
+ // (<root>\bin\bash.exe, <root>\usr\bin\bash.exe). This tolerates git.exe
88
+ // living under cmd\, bin\, or mingw64\bin\ without a fixed sibling distance.
89
+ // 2. `where bash.exe` EXCLUDING any hit under System32 — that path is the WSL
90
+ // `bash.exe` launcher, which is the wrong target (launches a Linux distro,
91
+ // not Git-for-Windows bash).
92
+ // Returns a posix shellSpec, or null when Git Bash is genuinely not installed.
93
+ function resolveWindowsGitBash() {
94
+ for (const git of allExistingPathsFromWhere('git.exe')) {
95
+ const bash = probeGitBashFromGitExe(git);
96
+ if (bash) return shellSpec(bash, 'posix');
97
+ }
98
+ // Fallback: a bare `bash.exe` on PATH, but never the System32 WSL launcher.
99
+ const bash = firstExistingPathFromWhereExcluding('bash.exe', /\\system32\\/i);
100
+ if (bash) return shellSpec(bash, 'posix');
101
+ // Final fallback: probe well-known Git-for-Windows install roots on the
102
+ // filesystem directly. PATH-independent and spawn-free, so it still resolves
103
+ // when a long-lived server process inherited an env without Git\cmd, or when
104
+ // the `where` probe times out (both observed in practice). Windows-only:
105
+ // this function is reached solely via the _isWindows() branch in
106
+ // resolveShellFor, so the hardcoded Windows paths never run on other OSes.
107
+ const direct = gitBashFromKnownWindowsRoots();
108
+ if (direct) return shellSpec(direct, 'posix');
109
+ return null;
110
+ }
111
+
112
+ // Probe standard Git-for-Windows install roots via the filesystem only.
113
+ // Env-var roots first (machine ProgramFiles + per-user LOCALAPPDATA installs),
114
+ // then fixed Program Files fallbacks for the rare case those vars are empty.
115
+ function gitBashFromKnownWindowsRoots() {
116
+ const env = process.env;
117
+ const roots = [];
118
+ for (const base of [env.ProgramW6432, env.ProgramFiles, env['ProgramFiles(x86)']]) {
119
+ if (base) roots.push(join(base, 'Git'));
120
+ }
121
+ if (env.LOCALAPPDATA) roots.push(join(env.LOCALAPPDATA, 'Programs', 'Git'));
122
+ roots.push('C:\\Program Files\\Git', 'C:\\Program Files (x86)\\Git');
123
+ for (const root of roots) {
124
+ for (const rel of [['bin', 'bash.exe'], ['usr', 'bin', 'bash.exe']]) {
125
+ const cand = join(root, ...rel);
126
+ if (existsSync(cand)) return cand;
127
+ }
128
+ }
129
+ return null;
130
+ }
131
+
132
+ // Walk up to 3 ancestor levels from <gitExe>'s directory, probing each
133
+ // candidate install root for bin\bash.exe then usr\bin\bash.exe. Covers
134
+ // git.exe under <root>\cmd\, <root>\bin\, and <root>\mingw64\bin\.
135
+ function probeGitBashFromGitExe(gitExe) {
136
+ let dir = dirname(gitExe);
137
+ for (let level = 0; level < 3 && dir; level++) {
138
+ for (const rel of [['bin', 'bash.exe'], ['usr', 'bin', 'bash.exe']]) {
139
+ const cand = join(dir, ...rel);
140
+ if (existsSync(cand)) return cand;
141
+ }
142
+ const parent = dirname(dir);
143
+ if (parent === dir) break;
144
+ dir = parent;
145
+ }
146
+ return null;
147
+ }
148
+
149
+ function allExistingPathsFromWhere(commandName) {
150
+ try {
151
+ const r = spawnSync('cmd.exe', ['/d', '/s', '/c', `where ${commandName}`], {
152
+ stdio: ['ignore', 'pipe', 'ignore'],
153
+ windowsHide: true,
154
+ timeout: 1000,
155
+ });
156
+ if (r.status !== 0 || !r.stdout) return [];
157
+ return r.stdout.toString('utf8').split(/\r?\n/).map(s => s.trim())
158
+ .filter(Boolean).filter(p => existsSync(p));
159
+ } catch {
160
+ return [];
161
+ }
162
+ }
163
+
164
+ function firstExistingPathFromWhereExcluding(commandName, excludeRe) {
165
+ try {
166
+ const r = spawnSync('cmd.exe', ['/d', '/s', '/c', `where ${commandName}`], {
167
+ stdio: ['ignore', 'pipe', 'ignore'],
168
+ windowsHide: true,
169
+ timeout: 1000,
170
+ });
171
+ if (r.status !== 0 || !r.stdout) return null;
172
+ const lines = r.stdout.toString('utf8').split(/\r?\n/).map(s => s.trim()).filter(Boolean);
173
+ return lines.find(p => existsSync(p) && !excludeRe.test(p)) || null;
174
+ } catch {
175
+ return null;
176
+ }
177
+ }
178
+
179
+ // Kind-aware shell resolution. kind:
180
+ // 'default' → identical to resolveShell() (PowerShell on Windows, /bin/sh elsewhere).
181
+ // 'bash' → on Windows, Git Bash (or null if not installed); elsewhere /bin/sh (POSIX is already bash-compatible).
182
+ // 'powershell' → on Windows, resolveShell(); elsewhere pwsh if present, else null.
183
+ // Each kind is memoized independently, but ONLY on success: a resolution miss
184
+ // (null) is not cached, so the caller's clear-error path is re-probed on the
185
+ // next call rather than pinned for the life of the process.
186
+ export function resolveShellFor(kind = 'default') {
187
+ if (kind == null || kind === 'default') return resolveShell();
188
+ if (_resolvedShellByKind.has(kind)) return _resolvedShellByKind.get(kind);
189
+
190
+ let spec = null;
191
+ if (kind === 'bash') {
192
+ spec = _isWindows() ? resolveWindowsGitBash() : shellSpec('/bin/sh', 'posix');
193
+ } else if (kind === 'powershell') {
194
+ if (_isWindows()) {
195
+ spec = resolveShell();
196
+ } else {
197
+ const pwsh = (() => {
198
+ try {
199
+ const r = spawnSync('which', ['pwsh'], { stdio: ['ignore', 'pipe', 'ignore'], timeout: 1000 });
200
+ if (r.status !== 0 || !r.stdout) return null;
201
+ const p = r.stdout.toString('utf8').split(/\r?\n/).map(s => s.trim()).find(Boolean);
202
+ return p && existsSync(p) ? p : null;
203
+ } catch { return null; }
204
+ })();
205
+ spec = pwsh ? shellSpec(pwsh, 'powershell') : null;
206
+ }
207
+ } else {
208
+ spec = resolveShell();
209
+ }
210
+
211
+ // Cache positively only — see the _resolvedShellByKind / doc comments above.
212
+ if (spec) _resolvedShellByKind.set(kind, spec);
213
+ return spec;
214
+ }
@@ -0,0 +1,143 @@
1
+ import { hashText } from './hash-utils.mjs';
2
+
3
+ /** Mirrors read-single-tool.mjs detectReadEncoding (BOM-only, no sniffing). */
4
+ export function detectReadEncodingFromBuffer(buf) {
5
+ if (!Buffer.isBuffer(buf) || buf.length < 2) {
6
+ return { encoding: 'utf8', bomLen: 0 };
7
+ }
8
+ if (buf[0] === 0xff && buf[1] === 0xfe) {
9
+ return { encoding: 'utf16le', bomLen: 2 };
10
+ }
11
+ if (buf[0] === 0xfe && buf[1] === 0xff) {
12
+ return { encoding: 'utf16be', bomLen: 2 };
13
+ }
14
+ if (buf.length >= 3 && buf[0] === 0xef && buf[1] === 0xbb && buf[2] === 0xbf) {
15
+ return { encoding: 'utf8', bomLen: 3 };
16
+ }
17
+ return { encoding: 'utf8', bomLen: 0 };
18
+ }
19
+
20
+ /** Decode on-disk bytes the same way as a full read (for snapshot hash / stale checks). */
21
+ export function decodeRawBufferForSnapshotCheck(rawBuf) {
22
+ const enc = detectReadEncodingFromBuffer(rawBuf);
23
+ // Strip the BOM for BOTH encodings (bomLen is 0 when absent, so non-BOM
24
+ // utf8 is unchanged). The read path hashes BOM-stripped content; a utf8 BOM
25
+ // left in here produced a hash mismatch and false "modified since read".
26
+ if (enc.encoding === 'utf16le') {
27
+ return rawBuf.subarray(enc.bomLen).toString('utf16le');
28
+ }
29
+ if (enc.encoding === 'utf16be') {
30
+ // No Node 'utf16be' encoding: swap pairs to LE (even length) then decode.
31
+ const body = rawBuf.subarray(enc.bomLen);
32
+ const even = body.length & ~1;
33
+ return Buffer.from(body.subarray(0, even)).swap16().toString('utf16le');
34
+ }
35
+ return rawBuf.subarray(enc.bomLen).toString('utf-8');
36
+ }
37
+
38
+ export function rangeHashesForReadRanges(content, ranges) {
39
+ const rows = Array.isArray(ranges) ? ranges : [];
40
+ if (rows.length === 0) return [];
41
+ // Split CRLF-insensitively: streaming reads strip a trailing \r per
42
+ // line before hashing (read-streaming.mjs), so the regular path and
43
+ // snapshot validation must normalise \r the same way or CRLF files
44
+ // produce mismatched rangeHashes (false "modified since read").
45
+ const lines = String(content ?? '').split(/\r?\n/);
46
+ const out = [];
47
+ for (const range of rows) {
48
+ if (!range) continue;
49
+ const startLine = Math.max(1, Number(range.startLine));
50
+ const endLine = range.endLine === Infinity ? Infinity : Number(range.endLine);
51
+ if (!Number.isFinite(startLine) || (!Number.isFinite(endLine) && endLine !== Infinity)) continue;
52
+ if (endLine !== Infinity && endLine < startLine) continue;
53
+ const startIdx = startLine - 1;
54
+ const endIdx = endLine === Infinity ? lines.length : Math.min(lines.length, endLine);
55
+ out.push({ startLine, endLine, hash: hashText(lines.slice(startIdx, endIdx).join('\n')) });
56
+ }
57
+ return out;
58
+ }
59
+
60
+ export function rangeHashesFromRenderedReadText(rendered, ranges) {
61
+ const rows = Array.isArray(ranges) ? ranges : [];
62
+ if (rows.length === 0) return [];
63
+ const byLine = new Map();
64
+ for (const line of String(rendered ?? '').split('\n')) {
65
+ const m = /^(\d+)[\t│→]/.exec(line);
66
+ if (!m) continue;
67
+ byLine.set(Number(m[1]), line.slice(m[0].length));
68
+ }
69
+ const out = [];
70
+ for (const range of rows) {
71
+ if (!range) continue;
72
+ const startLine = Math.max(1, Number(range.startLine));
73
+ const endLine = range.endLine === Infinity ? Infinity : Number(range.endLine);
74
+ if (!Number.isFinite(startLine) || !Number.isFinite(endLine) || endLine < startLine) continue;
75
+ const lines = [];
76
+ let complete = true;
77
+ for (let lineNo = startLine; lineNo <= endLine; lineNo++) {
78
+ if (!byLine.has(lineNo)) { complete = false; break; }
79
+ lines.push(byLine.get(lineNo));
80
+ }
81
+ if (complete) out.push({ startLine, endLine, hash: hashText(lines.join('\n')) });
82
+ }
83
+ return out;
84
+ }
85
+
86
+ export function statMatchesSnapshot(stat, snapshot) {
87
+ if (!stat || !snapshot) return false;
88
+ if (typeof snapshot.size !== 'number' || stat.size !== snapshot.size) return false;
89
+ if (!Number.isFinite(snapshot.mtimeMs) || Math.abs(stat.mtimeMs - snapshot.mtimeMs) > 1) {
90
+ return false;
91
+ }
92
+ if (Number.isFinite(snapshot.ctimeMs)) {
93
+ if (!Number.isFinite(stat.ctimeMs) || Math.abs(stat.ctimeMs - snapshot.ctimeMs) > 1) {
94
+ return false;
95
+ }
96
+ }
97
+ return true;
98
+ }
99
+
100
+ export function normaliseRangeHashEntry(row) {
101
+ if (!row || typeof row.hash !== 'string' || !row.hash) return null;
102
+ const startLine = Math.max(1, Number(row.startLine));
103
+ const endLine = row.endLine === Infinity ? Infinity : Number(row.endLine);
104
+ if (!Number.isFinite(startLine) || (!Number.isFinite(endLine) && endLine !== Infinity)) return null;
105
+ if (endLine !== Infinity && endLine < startLine) return null;
106
+ return { startLine, endLine, hash: row.hash };
107
+ }
108
+
109
+ export function snapshotCoversFullFile(snapshot) {
110
+ if (!snapshot || !Array.isArray(snapshot.ranges)) return false;
111
+ return snapshot.ranges.some((r) => r.startLine <= 1 && r.endLine === Infinity);
112
+ }
113
+
114
+ /** Logical line count captured at read-time (wc-l compatible). Undefined if absent. */
115
+ export function snapshotFileLineCount(snapshot) {
116
+ const n = snapshot?.fileLineCount;
117
+ return Number.isFinite(n) && n >= 0 ? Math.trunc(n) : undefined;
118
+ }
119
+
120
+ // Finite full-coverage check. A single full-file read records the
121
+ // {1, Infinity} sentinel (snapshotCoversFullFile), but a large file is
122
+ // output-capped so isFullFileView never fires and only finite ranges are
123
+ // recorded. Paging through the whole file then yields contiguous finite
124
+ // ranges that still cover every line — recognise that as full coverage so
125
+ // overwrite isn't permanently blocked. Caller passes the file's current
126
+ // line count; invariant: covered iff merged ranges span [1, lineCount].
127
+ export function snapshotRangesCoverAllLines(snapshot, lineCount) {
128
+ if (!snapshot || !Array.isArray(snapshot.ranges) || snapshot.ranges.length === 0) return false;
129
+ if (!(lineCount > 0)) return true;
130
+ const ranges = snapshot.ranges
131
+ .map((r) => ({
132
+ s: Math.max(1, Number(r.startLine) || 1),
133
+ e: r.endLine === Infinity ? lineCount : Math.min(lineCount, Number(r.endLine) || 0),
134
+ }))
135
+ .filter((r) => r.e >= r.s)
136
+ .sort((a, b) => a.s - b.s);
137
+ let covered = 0;
138
+ for (const r of ranges) {
139
+ if (r.s > covered + 1) break;
140
+ if (r.e > covered) covered = r.e;
141
+ }
142
+ return covered >= lineCount;
143
+ }
@@ -0,0 +1,206 @@
1
+ import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, unlinkSync } from 'fs';
2
+ import { join } from 'path';
3
+ import { writeJsonAtomicSync } from '../../../../shared/atomic-file.mjs';
4
+
5
+ // Mixdog read/write/edit share a scoped snapshot store. Value stores the
6
+ // mtime + size at read-time. A missing scope is fail-closed: snapshots without
7
+ // a real scope id are NOT recorded under a shared default bucket (would let
8
+ // worker A's read satisfy worker B's edit-gate across sessions via a persisted
9
+ // __global__.json).
10
+ const readFilesByScope = new Map(); // scope → Map(fullPath → { mtimeMs, size, ...meta })
11
+
12
+ // ── Disk-persisted snapshot store ────────────────────────────────────────
13
+ // Mirror the in-memory readFilesByScope to per-scope JSON files under
14
+ // `${PLUGIN_DATA}/read-snapshots/`. Hydration is lazy (first scope access);
15
+ // flush is debounced and drained synchronously on process exit.
16
+ const SNAPSHOT_DIR = (() => {
17
+ try {
18
+ const dataDir = process.env.CLAUDE_PLUGIN_DATA;
19
+ if (!dataDir) return null;
20
+ const dir = join(dataDir, 'read-snapshots');
21
+ // R4 data-at-rest: snapshot entries reveal which files an actor
22
+ // has read; clamp dir to owner-only on POSIX (advisory on Windows).
23
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true, mode: 0o700 });
24
+ return dir;
25
+ } catch { return null; }
26
+ })();
27
+
28
+ const SNAPSHOT_STALE_MS = 30 * 24 * 60 * 60 * 1000;
29
+ (function sweepStaleSnapshotScopes() {
30
+ if (!SNAPSHOT_DIR) return;
31
+ let entries;
32
+ try { entries = readdirSync(SNAPSHOT_DIR); }
33
+ catch { return; }
34
+ const now = Date.now();
35
+ for (const name of entries) {
36
+ if (!name.endsWith('.json')) continue;
37
+ const full = join(SNAPSHOT_DIR, name);
38
+ try {
39
+ const st = statSync(full);
40
+ if ((now - st.mtimeMs) > SNAPSHOT_STALE_MS) unlinkSync(full);
41
+ } catch { /* missing / race — skip */ }
42
+ }
43
+ })();
44
+
45
+ const PERSIST_DEBOUNCE_MS = 500;
46
+ const persistPending = new Map(); // scopeKey → Timeout
47
+ const scopeHydrated = new Set();
48
+
49
+ function snapshotScopeFilePath(scopeKey) {
50
+ if (!SNAPSHOT_DIR) return null;
51
+ const safe = String(scopeKey).replace(/[^A-Za-z0-9._-]/g, '_').slice(0, 200);
52
+ return join(SNAPSHOT_DIR, `${safe}.json`);
53
+ }
54
+
55
+ // R3 H1: persisted snapshots are read-back from disk on lazy hydration and
56
+ // directly trusted by the edit-gate (`isSnapshotStale` early-returns false
57
+ // on stat match). A malformed / forged entry must NEVER reach the gate, so
58
+ // every field that downstream code consumes is shape-validated here. Bad
59
+ // entries are dropped silently (treated as "never read") rather than
60
+ // surfaced — the model will simply be forced to re-read before edit.
61
+ const MAX_RANGES_PER_ENTRY = 4096;
62
+ function isValidPersistedSnapshotEntry(snap) {
63
+ if (!snap || typeof snap !== 'object' || Array.isArray(snap)) return false;
64
+ if (!Number.isFinite(snap.mtimeMs)) return false;
65
+ if (!Number.isFinite(snap.size) || snap.size < 0) return false;
66
+ if (snap.ctimeMs !== undefined && !Number.isFinite(snap.ctimeMs)) return false;
67
+ if (snap.contentHash !== undefined && snap.contentHash !== null) {
68
+ if (typeof snap.contentHash !== 'string') return false;
69
+ if (!/^[a-f0-9]{1,128}$/i.test(snap.contentHash)) return false;
70
+ }
71
+ if (snap.ranges !== undefined) {
72
+ if (!Array.isArray(snap.ranges)) return false;
73
+ if (snap.ranges.length > MAX_RANGES_PER_ENTRY) return false;
74
+ }
75
+ if (snap.rangeHashes !== undefined) {
76
+ if (!Array.isArray(snap.rangeHashes)) return false;
77
+ if (snap.rangeHashes.length > MAX_RANGES_PER_ENTRY) return false;
78
+ }
79
+ if (snap.rangeHash !== undefined && snap.rangeHash !== null && typeof snap.rangeHash !== 'string') return false;
80
+ if (snap.grepOnly !== undefined && typeof snap.grepOnly !== 'boolean') return false;
81
+ return true;
82
+ }
83
+
84
+ function loadScopeFromDisk(scopeKey) {
85
+ const path = snapshotScopeFilePath(scopeKey);
86
+ if (!path || !existsSync(path)) return new Map();
87
+ try {
88
+ const raw = readFileSync(path, 'utf-8');
89
+ const obj = _reviveInfinitySentinels(JSON.parse(raw));
90
+ if (!obj || typeof obj !== 'object' || Array.isArray(obj)) return new Map();
91
+ const map = new Map();
92
+ for (const [fp, snap] of Object.entries(obj)) {
93
+ if (typeof fp !== 'string' || !fp) continue;
94
+ if (!isValidPersistedSnapshotEntry(snap)) continue;
95
+ map.set(fp, snap);
96
+ }
97
+ return map;
98
+ } catch { return new Map(); }
99
+ }
100
+
101
+ // JSON cannot represent Infinity (it serializes to null), which silently turns
102
+ // a full-file range { endLine: Infinity } into { endLine: null } on persist and
103
+ // breaks the `endLine === Infinity` full-coverage checks after hydrate. Convert
104
+ // Infinity to a unique sentinel on write and back on read. Shape-agnostic deep
105
+ // clone (does NOT mutate the live in-memory snapshot objects).
106
+ const INFINITY_SENTINEL = '__mixdog_Infinity_sentinel__';
107
+ function _withInfinitySentinels(value) {
108
+ if (value === Infinity) return INFINITY_SENTINEL;
109
+ if (Array.isArray(value)) return value.map(_withInfinitySentinels);
110
+ if (value && typeof value === 'object') {
111
+ const out = {};
112
+ for (const [k, v] of Object.entries(value)) out[k] = _withInfinitySentinels(v);
113
+ return out;
114
+ }
115
+ return value;
116
+ }
117
+ function _reviveInfinitySentinels(value) {
118
+ if (value === INFINITY_SENTINEL) return Infinity;
119
+ if (Array.isArray(value)) return value.map(_reviveInfinitySentinels);
120
+ if (value && typeof value === 'object') {
121
+ const out = {};
122
+ for (const [k, v] of Object.entries(value)) out[k] = _reviveInfinitySentinels(v);
123
+ return out;
124
+ }
125
+ return value;
126
+ }
127
+
128
+ function persistScopeSync(scopeKey) {
129
+ const path = snapshotScopeFilePath(scopeKey);
130
+ if (!path) return;
131
+ const readFiles = readFilesByScope.get(scopeKey);
132
+ if (!readFiles || readFiles.size === 0) {
133
+ // Empty scope: remove on-disk file so it doesn't grow stale.
134
+ try { if (existsSync(path)) unlinkSync(path); } catch {}
135
+ return;
136
+ }
137
+ const obj = {};
138
+ for (const [fp, snap] of readFiles.entries()) obj[fp] = snap;
139
+ try { writeJsonAtomicSync(path, _withInfinitySentinels(obj), { compact: true, lock: true, mode: 0o600 }); } catch {}
140
+ }
141
+
142
+ export function deleteReadSnapshotPathEverywhere(fullPath) {
143
+ for (const [scopeKey, readFiles] of readFilesByScope.entries()) {
144
+ if (readFiles.delete(fullPath)) scheduleScopePersist(scopeKey);
145
+ }
146
+ if (!SNAPSHOT_DIR) return;
147
+ let entries;
148
+ try { entries = readdirSync(SNAPSHOT_DIR); }
149
+ catch { return; }
150
+ for (const name of entries) {
151
+ if (!name.endsWith('.json')) continue;
152
+ const p = join(SNAPSHOT_DIR, name);
153
+ try {
154
+ const obj = JSON.parse(readFileSync(p, 'utf-8'));
155
+ if (!obj || typeof obj !== 'object' || !Object.prototype.hasOwnProperty.call(obj, fullPath)) continue;
156
+ delete obj[fullPath];
157
+ if (Object.keys(obj).length === 0) unlinkSync(p);
158
+ else writeJsonAtomicSync(p, obj, { compact: true, lock: true, mode: 0o600 });
159
+ } catch {}
160
+ }
161
+ }
162
+
163
+ export function scheduleScopePersist(scopeKey) {
164
+ if (scopeKey === null || scopeKey === undefined) return;
165
+ if (persistPending.has(scopeKey)) return;
166
+ const t = setTimeout(() => {
167
+ persistPending.delete(scopeKey);
168
+ try { persistScopeSync(scopeKey); } catch {}
169
+ }, PERSIST_DEBOUNCE_MS);
170
+ if (t.unref) t.unref();
171
+ persistPending.set(scopeKey, t);
172
+ }
173
+
174
+ function flushAllScopesSync() {
175
+ for (const [key, timer] of persistPending) {
176
+ try { clearTimeout(timer); } catch {}
177
+ try { persistScopeSync(key); } catch {}
178
+ }
179
+ persistPending.clear();
180
+ }
181
+ process.on('exit', flushAllScopesSync);
182
+
183
+ export function readScopeKey(scope) {
184
+ return scope ? String(scope) : null;
185
+ }
186
+
187
+ export function readFilesForScope(scope) {
188
+ const key = readScopeKey(scope);
189
+ if (key === null) {
190
+ // No scope id: fail-closed. Return a fresh ephemeral Map that is
191
+ // never cached in readFilesByScope, never hydrated, never
192
+ // persisted to disk. Writes are dropped on the floor, lookups
193
+ // return undefined → edit-gate cannot pass without a real scoped
194
+ // read. Closes the cross-session bypass via __global__.json.
195
+ return new Map();
196
+ }
197
+ let readFiles = readFilesByScope.get(key);
198
+ if (!readFiles) {
199
+ // Lazy hydrate: pull persisted snapshots for this scope from disk
200
+ // on first access. Subsequent gets stay in-memory.
201
+ readFiles = scopeHydrated.has(key) ? new Map() : loadScopeFromDisk(key);
202
+ scopeHydrated.add(key);
203
+ readFilesByScope.set(key, readFiles);
204
+ }
205
+ return readFiles;
206
+ }