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,173 @@
1
+ import { readFileSync, statSync } from 'fs';
2
+ import { hashText } from './hash-utils.mjs';
3
+ import { mergeReadRanges } from './read-ranges.mjs';
4
+ import {
5
+ normaliseRangeHashEntry,
6
+ snapshotCoversFullFile,
7
+ statMatchesSnapshot,
8
+ decodeRawBufferForSnapshotCheck,
9
+ } from './snapshot-helpers.mjs';
10
+ import {
11
+ rawContentCacheGet,
12
+ rawContentCacheSet,
13
+ } from './cache-layers.mjs';
14
+ import {
15
+ readFilesForScope,
16
+ readScopeKey,
17
+ scheduleScopePersist,
18
+ } from './snapshot-store.mjs';
19
+ import {
20
+ isSnapshotStale as isSnapshotStaleImpl,
21
+ readContentIfSnapshotHashMatches as readContentIfSnapshotHashMatchesImpl,
22
+ } from './snapshot-validation.mjs';
23
+
24
+ export function readTextForSnapshotCheck(fullPath, cache = null, st = null) {
25
+ let statForRawCache = st;
26
+ const getCachedRaw = () => {
27
+ try {
28
+ if (!statForRawCache) statForRawCache = statSync(fullPath);
29
+ return rawContentCacheGet(fullPath, statForRawCache);
30
+ } catch {
31
+ return null;
32
+ }
33
+ };
34
+ if (cache && typeof cache.readTextSync === 'function') {
35
+ const entry = typeof cache.getEntry === 'function' ? cache.getEntry(fullPath) : null;
36
+ if (typeof entry?.content === 'string') return entry.content;
37
+ if (!Buffer.isBuffer(entry?.rawBuf) && typeof cache.seedBuffer === 'function') {
38
+ const cachedRaw = getCachedRaw();
39
+ if (cachedRaw) cache.seedBuffer(fullPath, cachedRaw);
40
+ }
41
+ return cache.readTextSync(fullPath);
42
+ }
43
+ if (cache && typeof cache.content === 'string' && Buffer.isBuffer(cache.rawBuf)) {
44
+ return cache.content;
45
+ }
46
+ const cachedRaw = getCachedRaw();
47
+ const rawBuf = cachedRaw || readFileSync(fullPath);
48
+ const content = decodeRawBufferForSnapshotCheck(rawBuf);
49
+ if (cache) {
50
+ cache.rawBuf = rawBuf;
51
+ cache.content = content;
52
+ }
53
+ if (!cachedRaw && statForRawCache) rawContentCacheSet(fullPath, statForRawCache, rawBuf);
54
+ return content;
55
+ }
56
+
57
+ export function recordReadSnapshot(fullPath, st, scope = null, meta = {}) {
58
+ const readFiles = readFilesForScope(scope);
59
+ let mtimeMs;
60
+ let ctimeMs;
61
+ let size;
62
+ try {
63
+ if (st && typeof st.mtimeMs === 'number') {
64
+ mtimeMs = st.mtimeMs;
65
+ ctimeMs = st.ctimeMs;
66
+ size = st.size;
67
+ } else {
68
+ const fresh = statSync(fullPath);
69
+ mtimeMs = fresh.mtimeMs;
70
+ ctimeMs = fresh.ctimeMs;
71
+ size = fresh.size;
72
+ }
73
+ } catch {
74
+ mtimeMs = Date.now();
75
+ ctimeMs = mtimeMs;
76
+ size = 0;
77
+ }
78
+ const incomingRanges = Array.isArray(meta.ranges)
79
+ ? meta.ranges
80
+ : [{ startLine: 1, endLine: Infinity }];
81
+ const replaceExisting = meta.replaceExisting === true;
82
+ const existing = replaceExisting ? null : readFiles.get(fullPath);
83
+ const sameFile = existing
84
+ && statMatchesSnapshot({ mtimeMs, ctimeMs, size }, existing)
85
+ && Array.isArray(existing.ranges);
86
+ const merged = sameFile
87
+ ? mergeReadRanges([...existing.ranges, ...incomingRanges])
88
+ : mergeReadRanges(incomingRanges);
89
+ // fileLineCount is omitted here so it can ONLY be set via the explicit
90
+ // guard below (which excludes source==='read_batch_sliced'); otherwise a
91
+ // caller passing fileLineCount with a batch source would leak it through
92
+ // restMeta and bypass the fail-closed batch path.
93
+ const { ranges: _omitRanges, rangeHash: _omitRangeHash, rangeHashes: _omitRangeHashes, replaceExisting: _omitReplaceExisting, fileLineCount: _omitFileLineCount, ...restMeta } = meta;
94
+ const next = { ...restMeta, mtimeMs, ctimeMs, size, ranges: merged };
95
+ if (!next.contentHash && sameFile && existing.contentHash) {
96
+ next.contentHash = existing.contentHash;
97
+ }
98
+ // Provenance: a snapshot is "grep-only" while EVERY contributing read was a
99
+ // single-file grep (match lines only, never the whole file). Any real
100
+ // read/edit/write clears it permanently. NOTE: grepOnly does not itself gate
101
+ // edits or overwrites — the write-overwrite gate keys on full-file PROOF
102
+ // (contentHash + full coverage), which a single-file grep snapshot acquires
103
+ // via the auto-hash below, so grep satisfies BOTH the edit gate and the
104
+ // write-overwrite gate; grepOnly only tailors the code-10 message when proof
105
+ // is absent. Sticky across merges in both orders: read→grep keeps it false
106
+ // (existing.grepOnly === false wins), and grep→read rebuilds it false
107
+ // because a read uses replaceExisting.
108
+ const incomingIsGrep = meta.source === 'grep';
109
+ next.grepOnly = incomingIsGrep && (sameFile ? existing.grepOnly === true : true);
110
+ const rangeHashRows = [];
111
+ if (sameFile && Array.isArray(existing.rangeHashes)) {
112
+ for (const row of existing.rangeHashes) {
113
+ const nextRow = normaliseRangeHashEntry(row);
114
+ if (nextRow) rangeHashRows.push(nextRow);
115
+ }
116
+ } else if (sameFile && existing.rangeHash && Array.isArray(existing.ranges) && existing.ranges.length === 1) {
117
+ const nextRow = normaliseRangeHashEntry({ ...existing.ranges[0], hash: existing.rangeHash });
118
+ if (nextRow) rangeHashRows.push(nextRow);
119
+ }
120
+ if (meta.rangeHash && Array.isArray(meta.ranges) && meta.ranges.length === 1) {
121
+ const nextRow = normaliseRangeHashEntry({ ...meta.ranges[0], hash: meta.rangeHash });
122
+ if (nextRow) rangeHashRows.push(nextRow);
123
+ }
124
+ if (Array.isArray(meta.rangeHashes)) {
125
+ for (const row of meta.rangeHashes) {
126
+ const nextRow = normaliseRangeHashEntry(row);
127
+ if (nextRow) rangeHashRows.push(nextRow);
128
+ }
129
+ }
130
+ if (!next.contentHash && snapshotCoversFullFile(next)) {
131
+ try {
132
+ const content = decodeRawBufferForSnapshotCheck(readFileSync(fullPath));
133
+ next.contentHash = hashText(content);
134
+ } catch {}
135
+ }
136
+ if (!next.contentHash && !snapshotCoversFullFile(next) && rangeHashRows.length > 0) {
137
+ const seen = new Set();
138
+ next.rangeHashes = rangeHashRows.filter((row) => {
139
+ const key = `${row.startLine}:${row.endLine}:${row.hash}`;
140
+ if (seen.has(key)) return false;
141
+ seen.add(key);
142
+ return true;
143
+ });
144
+ }
145
+ const batchSliced = meta.source === 'read_batch_sliced';
146
+ if (!batchSliced && Number.isFinite(meta.fileLineCount) && meta.fileLineCount >= 0) {
147
+ next.fileLineCount = Math.trunc(meta.fileLineCount);
148
+ } else if (!batchSliced && sameFile && Number.isFinite(existing?.fileLineCount) && existing.fileLineCount >= 0) {
149
+ next.fileLineCount = Math.trunc(existing.fileLineCount);
150
+ }
151
+ readFiles.set(fullPath, next);
152
+ scheduleScopePersist(readScopeKey(scope));
153
+ }
154
+
155
+ export function getReadSnapshot(fullPath, scope = null) {
156
+ return readFilesForScope(scope).get(fullPath);
157
+ }
158
+
159
+ export function isSnapshotStale(stat, snapshot, fullPath = '', readCache = null) {
160
+ return isSnapshotStaleImpl(stat, snapshot, {
161
+ fullPath,
162
+ readCache,
163
+ readTextForSnapshotCheck,
164
+ });
165
+ }
166
+
167
+ export function readContentIfSnapshotHashMatches(fullPath, snapshot, readCache = null, st = null) {
168
+ return readContentIfSnapshotHashMatchesImpl(fullPath, snapshot, {
169
+ readCache,
170
+ st,
171
+ readTextForSnapshotCheck,
172
+ });
173
+ }
@@ -0,0 +1,268 @@
1
+ import { readFile, stat } from 'fs/promises';
2
+ import { open } from 'fs/promises';
3
+ import { createRequire } from 'module';
4
+ import { READ_MAX_SIZE_BYTES } from './read-constants.mjs';
5
+ import { imageBlocksFromBuffer } from './read-image-resize.mjs';
6
+
7
+ const requireCjs = createRequire(import.meta.url);
8
+ const DEFAULT_READ_MAX_OUTPUT_BYTES = 100 * 1024;
9
+
10
+ // PDFs at or under this size are emitted as an Anthropic base64 document
11
+ // block (the model reads the rendered PDF directly). Mirrors CC's
12
+ // PDF_TARGET_RAW_SIZE (20MB raw → ~27MB base64, under the 32MB request cap).
13
+ // Larger PDFs fall back to pdf-parse TEXT extraction.
14
+ const PDF_DOCUMENT_MAX_BYTES = 20 * 1024 * 1024;
15
+
16
+ // %PDF- magic bytes (0x25 0x50 0x44 0x46 0x2D). A document block must only be
17
+ // emitted for a real PDF — sending a non-PDF blob as application/pdf would
18
+ // poison the conversation history (the API/model rejects the malformed block).
19
+ const PDF_MAGIC = Buffer.from('%PDF-', 'latin1');
20
+
21
+ // Per-output text ceiling inside a notebook. A single cell output larger than
22
+ // this is replaced with a jq hint (port of CC's large-notebook guidance) so a
23
+ // runaway stdout / data dump can't blow the read budget.
24
+ const IPYNB_OUTPUT_MAX_CHARS = 10_000;
25
+
26
+ // Read the leading bytes and confirm the %PDF- magic header. Returns false on
27
+ // any IO error (caller treats a non-PDF as text-fallback).
28
+ async function fileStartsWithPdfMagic(fullPath) {
29
+ let fh;
30
+ try {
31
+ fh = await open(fullPath, 'r');
32
+ const head = Buffer.alloc(PDF_MAGIC.length);
33
+ const { bytesRead } = await fh.read(head, 0, PDF_MAGIC.length, 0);
34
+ return bytesRead === PDF_MAGIC.length && head.equals(PDF_MAGIC);
35
+ } catch {
36
+ return false;
37
+ } finally {
38
+ if (fh) { try { await fh.close(); } catch {} }
39
+ }
40
+ }
41
+
42
+ // Validate / parse a pages arg ("N" or "N-M", 1-based, span <=20). Returns
43
+ // { filter } on success, { error } (a string) on rejection, or { filter: null }
44
+ // when no pages arg was supplied.
45
+ function parsePagesArg(pagesArg) {
46
+ if (pagesArg == null || pagesArg === '') return { filter: null };
47
+ if (typeof pagesArg !== 'string') {
48
+ return { error: `Error: pages must be a string like "1-5"; got ${typeof pagesArg}` };
49
+ }
50
+ const trimmed = pagesArg.trim();
51
+ const m = trimmed.match(/^(\d+)(?:-(\d+))?$/);
52
+ if (!m) {
53
+ return { error: `Error: pages "${trimmed}" not in "N" or "N-M" form (1-based positive integers)` };
54
+ }
55
+ const from = parseInt(m[1], 10);
56
+ const to = m[2] ? parseInt(m[2], 10) : from;
57
+ if (from < 1 || to < 1) {
58
+ return { error: `Error: pages "${trimmed}" out of range — page numbers are 1-based` };
59
+ }
60
+ if (to < from) {
61
+ return { error: `Error: pages "${trimmed}" inverted — end (${to}) precedes start (${from})` };
62
+ }
63
+ if (to - from + 1 > 20) {
64
+ return { error: `Error: pages "${trimmed}" spans ${to - from + 1} pages; max 20 per request — narrow the range` };
65
+ }
66
+ return { filter: { from, to } };
67
+ }
68
+
69
+ // pdf-parse TEXT extraction. Fallback path for PDFs over the document-block
70
+ // size cap, and the always-path when a page range is requested (a base64
71
+ // document block can't be page-filtered, so a narrowed read keeps using text).
72
+ async function extractPdfTextBody(fullPath, pageFilter, maxOutputBytes) {
73
+ const pdfParse = requireCjs('pdf-parse');
74
+ const buf = await readFile(fullPath);
75
+ const pageTexts = [];
76
+ const data = await pdfParse(buf, {
77
+ pagerender: (pageData) => {
78
+ // pdf-parse exposes either `pageNumber` (1-based, pdf.js) or
79
+ // `_pageIndex` (0-based, internal); preferring pageIndex+1 over
80
+ // pageNumber dropped/duplicated pages when pdf.js renumbered with
81
+ // annotations or oddball page trees. Use pageNumber first.
82
+ const pageNum = (typeof pageData.pageNumber === 'number')
83
+ ? pageData.pageNumber
84
+ : ((pageData._pageIndex ?? pageData.pageIndex ?? 0) + 1);
85
+ if (pageFilter && (pageNum < pageFilter.from || pageNum > pageFilter.to)) return Promise.resolve('');
86
+ return pageData.getTextContent().then((tc) => {
87
+ const text = tc.items.map((i) => i.str).join(' ');
88
+ pageTexts.push({ page: pageNum, text });
89
+ return text;
90
+ });
91
+ },
92
+ });
93
+ let out = pageFilter
94
+ ? pageTexts.map((p) => `--- Page ${p.page} ---\n${p.text}`).join('\n\n')
95
+ : (data.text || '');
96
+ if (out.length > maxOutputBytes) {
97
+ out = out.slice(0, maxOutputBytes) + `\n\n... [PDF output truncated at ${Math.round(maxOutputBytes / 1024)} KB; use pages param to narrow]`;
98
+ }
99
+ return out || '(no text content extracted from PDF)';
100
+ }
101
+
102
+ export async function extractPdfText(fullPath, pagesArg, { maxOutputBytes = DEFAULT_READ_MAX_OUTPUT_BYTES, textOnly = false } = {}) {
103
+ try {
104
+ let pdfStat;
105
+ try { pdfStat = await stat(fullPath); } catch (e) {
106
+ return `Error: pdf stat failed — ${e instanceof Error ? e.message : String(e)}`;
107
+ }
108
+
109
+ const pages = parsePagesArg(pagesArg);
110
+ if (pages.error) return pages.error;
111
+
112
+ // Document-block path: whole-PDF read, no page range, within the size
113
+ // cap, and confirmed %PDF- magic. Emits a base64 document block the
114
+ // model reads natively (figures, layout, tables) instead of lossy
115
+ // pdf-parse text. The magic-byte guard prevents a non-PDF (mislabelled
116
+ // extension) from poisoning history as a malformed document block.
117
+ // textOnly (batch context) skips the block and always emits text.
118
+ if (!textOnly && !pages.filter && pdfStat.size <= PDF_DOCUMENT_MAX_BYTES) {
119
+ if (await fileStartsWithPdfMagic(fullPath)) {
120
+ const buf = await readFile(fullPath);
121
+ return {
122
+ content: [{
123
+ type: 'document',
124
+ source: {
125
+ type: 'base64',
126
+ media_type: 'application/pdf',
127
+ data: buf.toString('base64'),
128
+ },
129
+ }],
130
+ };
131
+ }
132
+ // Not a real PDF — fall through to pdf-parse, which surfaces a
133
+ // clean parse error rather than emitting a bad document block.
134
+ }
135
+
136
+ // TEXT fallback: >20MB PDFs, page-filtered reads, or non-magic files.
137
+ return await extractPdfTextBody(fullPath, pages.filter, maxOutputBytes);
138
+ } catch (err) {
139
+ return `Error: pdf-parse failed — ${err instanceof Error ? err.message : String(err)}`;
140
+ }
141
+ }
142
+
143
+ export async function extractIpynbText(fullPath, { maxOutputBytes = DEFAULT_READ_MAX_OUTPUT_BYTES, hasRangeArgs = false, textOnly = false } = {}) {
144
+ // Range args (offset/limit/line) don't map cleanly onto a Jupyter
145
+ // notebook: cells aren't line-addressable in the source JSON, so applying
146
+ // offset:200 against rendered code+markdown would slice mid-cell without
147
+ // matching the line numbers a follow-up `edit` would target. Refuse the
148
+ // range up front and direct the caller to the underlying .ipynb JSON.
149
+ if (hasRangeArgs) {
150
+ return 'Error: range args (offset/limit/line) are not supported for .ipynb extraction — cells are not line-addressable; rename to .json or call read on a converted file for line-level access';
151
+ }
152
+ try {
153
+ let nbStat;
154
+ try { nbStat = await stat(fullPath); } catch (e) {
155
+ return `Error: ipynb stat failed — ${e instanceof Error ? e.message : String(e)}`;
156
+ }
157
+ if (nbStat.size > READ_MAX_SIZE_BYTES) {
158
+ return `Error: notebook too large (size: ${nbStat.size}B, cap: ${READ_MAX_SIZE_BYTES}B) — use a cell range to narrow`;
159
+ }
160
+ const raw = await readFile(fullPath, 'utf-8');
161
+ const nb = JSON.parse(raw);
162
+ const cells = Array.isArray(nb.cells) ? nb.cells : [];
163
+
164
+ // Accumulate rendered cells as text, flushing to a text block whenever
165
+ // an image output is hit so block ORDER matches notebook order.
166
+ const blocks = [];
167
+ const textParts = [];
168
+ let textLen = 0;
169
+ const flushText = () => {
170
+ if (textParts.length === 0) return;
171
+ blocks.push({ type: 'text', text: textParts.join('\n\n') });
172
+ textParts.length = 0;
173
+ };
174
+ const pushText = (s) => { textParts.push(s); textLen += s.length; };
175
+
176
+ let cellIndex = -1;
177
+ for (const cell of cells) {
178
+ cellIndex += 1;
179
+ const src = Array.isArray(cell.source) ? cell.source.join('') : (cell.source || '');
180
+ if (cell.cell_type === 'markdown') {
181
+ pushText(src);
182
+ } else if (cell.cell_type === 'code') {
183
+ let block = src;
184
+ const outputs = Array.isArray(cell.outputs) ? cell.outputs : [];
185
+ // Collect image outputs to emit AFTER this cell's code block.
186
+ const pendingImages = [];
187
+ for (const out of outputs) {
188
+ const data = out.data || {};
189
+ if (data['text/plain'] || out.text) {
190
+ const rawTxt = data['text/plain']
191
+ ? (Array.isArray(data['text/plain']) ? data['text/plain'].join('') : data['text/plain'])
192
+ : (Array.isArray(out.text) ? out.text.join('') : out.text);
193
+ // Port CC behaviour: a single huge output is replaced
194
+ // with a jq hint rather than dumped inline.
195
+ if (typeof rawTxt === 'string' && rawTxt.length > IPYNB_OUTPUT_MAX_CHARS) {
196
+ block += `\n# Output: [large output omitted — ${rawTxt.length} chars; inspect with: cat "${fullPath}" | jq '.cells[${cellIndex}].outputs']`;
197
+ } else {
198
+ block += '\n# Output:\n' + rawTxt;
199
+ }
200
+ } else if (data['image/png'] || data['image/jpeg']) {
201
+ const isPng = !!data['image/png'];
202
+ const b64 = isPng ? data['image/png'] : data['image/jpeg'];
203
+ const b64str = Array.isArray(b64) ? b64.join('') : b64;
204
+ pendingImages.push({ mimeType: isPng ? 'image/png' : 'image/jpeg', b64: b64str });
205
+ block += `\n# Output: [image output — cell ${cellIndex}]`;
206
+ }
207
+ }
208
+ pushText('```python\n' + block + '\n```');
209
+ // Embed each image output as a real image block (resized via the
210
+ // shared helper). On fallback (sharp absent / decode failure) the
211
+ // image is left as the text placeholder already in `block`.
212
+ // textOnly (batch context) skips embedding entirely — the text
213
+ // placeholder in `block` is the only representation.
214
+ if (!textOnly && pendingImages.length > 0) {
215
+ flushText();
216
+ for (const img of pendingImages) {
217
+ let buf;
218
+ try { buf = Buffer.from(img.b64, 'base64'); } catch { buf = null; }
219
+ if (!buf || buf.length === 0) continue;
220
+ const built = await imageBlocksFromBuffer(buf, img.mimeType, { sourcePath: `${fullPath} [cell ${cellIndex}]` });
221
+ if (built) {
222
+ if (built.textBlock) blocks.push(built.textBlock);
223
+ blocks.push(built.imageBlock);
224
+ }
225
+ // built === null → sharp unavailable; placeholder text already emitted.
226
+ }
227
+ }
228
+ }
229
+ }
230
+ flushText();
231
+
232
+ // Output-byte cap on the combined TEXT. Image blocks are not counted
233
+ // (they're already size-bounded by the resize helper).
234
+ if (textLen > maxOutputBytes) {
235
+ // Trim trailing text blocks until under the cap, then mark the cut.
236
+ let running = 0;
237
+ const capped = [];
238
+ let truncated = false;
239
+ for (const b of blocks) {
240
+ if (b.type !== 'text') { capped.push(b); continue; }
241
+ if (running >= maxOutputBytes) { truncated = true; continue; }
242
+ if (running + b.text.length > maxOutputBytes) {
243
+ capped.push({ type: 'text', text: b.text.slice(0, maxOutputBytes - running) });
244
+ running = maxOutputBytes;
245
+ truncated = true;
246
+ } else {
247
+ capped.push(b);
248
+ running += b.text.length;
249
+ }
250
+ }
251
+ if (truncated) capped.push({ type: 'text', text: `\n\n... [notebook output truncated at ${Math.round(maxOutputBytes / 1024)} KB]` });
252
+ blocks.length = 0;
253
+ blocks.push(...capped);
254
+ }
255
+
256
+ const hasImageBlock = blocks.some((b) => b.type === 'image');
257
+ if (blocks.length === 0) return '(empty notebook)';
258
+ // No embedded images → return the joined TEXT string (backwards
259
+ // compatible: works in batch reads and the loop's text path). Images
260
+ // present → return a content-block ARRAY so the model can see them.
261
+ if (!hasImageBlock) {
262
+ return blocks.map((b) => b.text).join('\n\n') || '(empty notebook)';
263
+ }
264
+ return { content: blocks };
265
+ } catch (err) {
266
+ return `Error: ipynb parse failed — ${err instanceof Error ? err.message : String(err)}`;
267
+ }
268
+ }