@calliopelabs/cli 2.3.0 → 3.0.0

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 (538) hide show
  1. package/README.md +45 -644
  2. package/dist/acp.d.ts +78 -0
  3. package/dist/acp.d.ts.map +1 -0
  4. package/dist/acp.js +572 -0
  5. package/dist/acp.js.map +1 -0
  6. package/dist/auto-compressor.d.ts.map +1 -1
  7. package/dist/auto-compressor.js +9 -5
  8. package/dist/auto-compressor.js.map +1 -1
  9. package/dist/bin.d.ts +9 -5
  10. package/dist/bin.d.ts.map +1 -1
  11. package/dist/bin.js +108 -76
  12. package/dist/bin.js.map +1 -1
  13. package/dist/budget.d.ts +64 -0
  14. package/dist/budget.d.ts.map +1 -0
  15. package/dist/budget.js +157 -0
  16. package/dist/budget.js.map +1 -0
  17. package/dist/checkpoint.d.ts +63 -30
  18. package/dist/checkpoint.d.ts.map +1 -1
  19. package/dist/checkpoint.js +249 -150
  20. package/dist/checkpoint.js.map +1 -1
  21. package/dist/circuit-breaker/breaker.js.map +1 -1
  22. package/dist/config.d.ts +86 -81
  23. package/dist/config.d.ts.map +1 -1
  24. package/dist/config.js +237 -305
  25. package/dist/config.js.map +1 -1
  26. package/dist/cost.d.ts +114 -0
  27. package/dist/cost.d.ts.map +1 -0
  28. package/dist/cost.js +499 -0
  29. package/dist/cost.js.map +1 -0
  30. package/dist/diff.d.ts.map +1 -1
  31. package/dist/diff.js +53 -11
  32. package/dist/diff.js.map +1 -1
  33. package/dist/errors.d.ts.map +1 -1
  34. package/dist/errors.js +33 -6
  35. package/dist/errors.js.map +1 -1
  36. package/dist/files.js.map +1 -1
  37. package/dist/fleet.d.ts +30 -0
  38. package/dist/fleet.d.ts.map +1 -0
  39. package/dist/fleet.js +86 -0
  40. package/dist/fleet.js.map +1 -0
  41. package/dist/fuzzy-search.d.ts.map +1 -1
  42. package/dist/fuzzy-search.js +2 -1
  43. package/dist/fuzzy-search.js.map +1 -1
  44. package/dist/git-status.js.map +1 -1
  45. package/dist/headless.d.ts +1 -2
  46. package/dist/headless.d.ts.map +1 -1
  47. package/dist/headless.js +222 -26
  48. package/dist/headless.js.map +1 -1
  49. package/dist/hooks.d.ts +8 -2
  50. package/dist/hooks.d.ts.map +1 -1
  51. package/dist/hooks.js +97 -11
  52. package/dist/hooks.js.map +1 -1
  53. package/dist/hud/api.d.ts +3 -18
  54. package/dist/hud/api.d.ts.map +1 -1
  55. package/dist/hud/api.js +9 -352
  56. package/dist/hud/api.js.map +1 -1
  57. package/dist/hud/palettes.d.ts.map +1 -1
  58. package/dist/hud/palettes.js +0 -180
  59. package/dist/hud/palettes.js.map +1 -1
  60. package/dist/hud/skins.d.ts.map +1 -1
  61. package/dist/hud/skins.js +0 -263
  62. package/dist/hud/skins.js.map +1 -1
  63. package/dist/hud/types.d.ts +2 -5
  64. package/dist/hud/types.d.ts.map +1 -1
  65. package/dist/index.d.ts +1 -4
  66. package/dist/index.d.ts.map +1 -1
  67. package/dist/index.js +1 -4
  68. package/dist/index.js.map +1 -1
  69. package/dist/iteration-ledger.d.ts.map +1 -1
  70. package/dist/iteration-ledger.js +3 -2
  71. package/dist/iteration-ledger.js.map +1 -1
  72. package/dist/local-model.d.ts +150 -0
  73. package/dist/local-model.d.ts.map +1 -0
  74. package/dist/local-model.js +408 -0
  75. package/dist/local-model.js.map +1 -0
  76. package/dist/markdown.d.ts.map +1 -1
  77. package/dist/markdown.js +32 -10
  78. package/dist/markdown.js.map +1 -1
  79. package/dist/mcp.d.ts +35 -5
  80. package/dist/mcp.d.ts.map +1 -1
  81. package/dist/mcp.js +190 -13
  82. package/dist/mcp.js.map +1 -1
  83. package/dist/memory.d.ts.map +1 -1
  84. package/dist/memory.js +6 -4
  85. package/dist/memory.js.map +1 -1
  86. package/dist/model-detection.d.ts +14 -1
  87. package/dist/model-detection.d.ts.map +1 -1
  88. package/dist/model-detection.js +308 -115
  89. package/dist/model-detection.js.map +1 -1
  90. package/dist/parallel-tools.d.ts +9 -1
  91. package/dist/parallel-tools.d.ts.map +1 -1
  92. package/dist/parallel-tools.js +6 -5
  93. package/dist/parallel-tools.js.map +1 -1
  94. package/dist/plugins.d.ts +68 -0
  95. package/dist/plugins.d.ts.map +1 -1
  96. package/dist/plugins.js +201 -3
  97. package/dist/plugins.js.map +1 -1
  98. package/dist/policy.d.ts +43 -0
  99. package/dist/policy.d.ts.map +1 -0
  100. package/dist/policy.js +139 -0
  101. package/dist/policy.js.map +1 -0
  102. package/dist/project-config.d.ts +1 -2
  103. package/dist/project-config.d.ts.map +1 -1
  104. package/dist/project-config.js +0 -3
  105. package/dist/project-config.js.map +1 -1
  106. package/dist/providers/anthropic.d.ts.map +1 -1
  107. package/dist/providers/anthropic.js +36 -2
  108. package/dist/providers/anthropic.js.map +1 -1
  109. package/dist/providers/bedrock.d.ts.map +1 -1
  110. package/dist/providers/bedrock.js +83 -22
  111. package/dist/providers/bedrock.js.map +1 -1
  112. package/dist/providers/compat.d.ts +21 -0
  113. package/dist/providers/compat.d.ts.map +1 -1
  114. package/dist/providers/compat.js +176 -6
  115. package/dist/providers/compat.js.map +1 -1
  116. package/dist/providers/google.js.map +1 -1
  117. package/dist/providers/index.d.ts +19 -4
  118. package/dist/providers/index.d.ts.map +1 -1
  119. package/dist/providers/index.js +62 -9
  120. package/dist/providers/index.js.map +1 -1
  121. package/dist/providers/ollama.d.ts +2 -2
  122. package/dist/providers/ollama.d.ts.map +1 -1
  123. package/dist/providers/ollama.js +26 -6
  124. package/dist/providers/ollama.js.map +1 -1
  125. package/dist/providers/openai.d.ts.map +1 -1
  126. package/dist/providers/openai.js +14 -10
  127. package/dist/providers/openai.js.map +1 -1
  128. package/dist/providers/types.d.ts +8 -0
  129. package/dist/providers/types.d.ts.map +1 -1
  130. package/dist/providers/types.js +19 -10
  131. package/dist/providers/types.js.map +1 -1
  132. package/dist/replay.d.ts +37 -0
  133. package/dist/replay.d.ts.map +1 -0
  134. package/dist/replay.js +168 -0
  135. package/dist/replay.js.map +1 -0
  136. package/dist/risk.d.ts.map +1 -1
  137. package/dist/risk.js +15 -5
  138. package/dist/risk.js.map +1 -1
  139. package/dist/router.d.ts +133 -0
  140. package/dist/router.d.ts.map +1 -0
  141. package/dist/{model-router.js → router.js} +336 -10
  142. package/dist/router.js.map +1 -0
  143. package/dist/runlog.d.ts +185 -0
  144. package/dist/runlog.d.ts.map +1 -0
  145. package/dist/runlog.js +398 -0
  146. package/dist/runlog.js.map +1 -0
  147. package/dist/{sandbox.d.ts → sandbox/docker.d.ts} +1 -1
  148. package/dist/sandbox/docker.d.ts.map +1 -0
  149. package/dist/{sandbox.js → sandbox/docker.js} +1 -1
  150. package/dist/sandbox/docker.js.map +1 -0
  151. package/dist/sandbox/index.d.ts +36 -0
  152. package/dist/sandbox/index.d.ts.map +1 -0
  153. package/dist/sandbox/index.js +62 -0
  154. package/dist/sandbox/index.js.map +1 -0
  155. package/dist/{sandbox-native.d.ts → sandbox/native.d.ts} +2 -1
  156. package/dist/sandbox/native.d.ts.map +1 -0
  157. package/dist/{sandbox-native.js → sandbox/native.js} +38 -6
  158. package/dist/sandbox/native.js.map +1 -0
  159. package/dist/scope.d.ts +10 -0
  160. package/dist/scope.d.ts.map +1 -1
  161. package/dist/scope.js +76 -16
  162. package/dist/scope.js.map +1 -1
  163. package/dist/scuttlebot/client.d.ts +83 -0
  164. package/dist/scuttlebot/client.d.ts.map +1 -0
  165. package/dist/scuttlebot/client.js +350 -0
  166. package/dist/scuttlebot/client.js.map +1 -0
  167. package/dist/scuttlebot/config.d.ts +28 -0
  168. package/dist/scuttlebot/config.d.ts.map +1 -0
  169. package/dist/scuttlebot/config.js +91 -0
  170. package/dist/scuttlebot/config.js.map +1 -0
  171. package/dist/scuttlebot/http-client.d.ts +63 -0
  172. package/dist/scuttlebot/http-client.d.ts.map +1 -0
  173. package/dist/scuttlebot/http-client.js +124 -0
  174. package/dist/scuttlebot/http-client.js.map +1 -0
  175. package/dist/scuttlebot/index.d.ts +13 -0
  176. package/dist/scuttlebot/index.d.ts.map +1 -0
  177. package/dist/scuttlebot/index.js +10 -0
  178. package/dist/scuttlebot/index.js.map +1 -0
  179. package/dist/scuttlebot/irc-client.d.ts +124 -0
  180. package/dist/scuttlebot/irc-client.d.ts.map +1 -0
  181. package/dist/scuttlebot/irc-client.js +599 -0
  182. package/dist/scuttlebot/irc-client.js.map +1 -0
  183. package/dist/setup.d.ts.map +1 -1
  184. package/dist/setup.js +22 -54
  185. package/dist/setup.js.map +1 -1
  186. package/dist/skills.d.ts +58 -0
  187. package/dist/skills.d.ts.map +1 -1
  188. package/dist/skills.js +186 -16
  189. package/dist/skills.js.map +1 -1
  190. package/dist/storage.d.ts +0 -4
  191. package/dist/storage.d.ts.map +1 -1
  192. package/dist/storage.js +87 -8
  193. package/dist/storage.js.map +1 -1
  194. package/dist/streaming.d.ts +25 -0
  195. package/dist/streaming.d.ts.map +1 -1
  196. package/dist/streaming.js +59 -0
  197. package/dist/streaming.js.map +1 -1
  198. package/dist/styles.d.ts +6 -0
  199. package/dist/styles.d.ts.map +1 -1
  200. package/dist/styles.js +11 -0
  201. package/dist/styles.js.map +1 -1
  202. package/dist/summarization.js.map +1 -1
  203. package/dist/terminal-image.js +1 -1
  204. package/dist/terminal-image.js.map +1 -1
  205. package/dist/themes.d.ts +5 -4
  206. package/dist/themes.d.ts.map +1 -1
  207. package/dist/themes.js +35 -90
  208. package/dist/themes.js.map +1 -1
  209. package/dist/tools.d.ts +27 -3
  210. package/dist/tools.d.ts.map +1 -1
  211. package/dist/tools.js +352 -209
  212. package/dist/tools.js.map +1 -1
  213. package/dist/trust.d.ts +16 -3
  214. package/dist/trust.d.ts.map +1 -1
  215. package/dist/trust.js +23 -4
  216. package/dist/trust.js.map +1 -1
  217. package/dist/types.d.ts +4 -3
  218. package/dist/types.d.ts.map +1 -1
  219. package/dist/types.js +34 -44
  220. package/dist/types.js.map +1 -1
  221. package/dist/ui/agent.d.ts +4 -7
  222. package/dist/ui/agent.d.ts.map +1 -1
  223. package/dist/ui/agent.js +354 -73
  224. package/dist/ui/agent.js.map +1 -1
  225. package/dist/ui/animations.d.ts +37 -0
  226. package/dist/ui/animations.d.ts.map +1 -0
  227. package/dist/ui/animations.js +227 -0
  228. package/dist/ui/animations.js.map +1 -0
  229. package/dist/ui/chat-input-keys.d.ts +42 -0
  230. package/dist/ui/chat-input-keys.d.ts.map +1 -0
  231. package/dist/ui/chat-input-keys.js +360 -0
  232. package/dist/ui/chat-input-keys.js.map +1 -0
  233. package/dist/ui/chat-input.d.ts +5 -2
  234. package/dist/ui/chat-input.d.ts.map +1 -1
  235. package/dist/ui/chat-input.js +54 -308
  236. package/dist/ui/chat-input.js.map +1 -1
  237. package/dist/ui/commands.d.ts +15 -45
  238. package/dist/ui/commands.d.ts.map +1 -1
  239. package/dist/ui/commands.js +461 -2636
  240. package/dist/ui/commands.js.map +1 -1
  241. package/dist/ui/completions.d.ts +1 -0
  242. package/dist/ui/completions.d.ts.map +1 -1
  243. package/dist/ui/completions.js +36 -107
  244. package/dist/ui/completions.js.map +1 -1
  245. package/dist/ui/components.d.ts +14 -6
  246. package/dist/ui/components.d.ts.map +1 -1
  247. package/dist/ui/components.js +17 -226
  248. package/dist/ui/components.js.map +1 -1
  249. package/dist/ui/context.js +6 -6
  250. package/dist/ui/context.js.map +1 -1
  251. package/dist/ui/debug-log.d.ts +11 -0
  252. package/dist/ui/debug-log.d.ts.map +1 -0
  253. package/dist/ui/debug-log.js +21 -0
  254. package/dist/ui/debug-log.js.map +1 -0
  255. package/dist/ui/error-boundary.d.ts.map +1 -1
  256. package/dist/ui/error-boundary.js.map +1 -1
  257. package/dist/ui/frame.d.ts.map +1 -1
  258. package/dist/ui/frame.js +1 -3
  259. package/dist/ui/frame.js.map +1 -1
  260. package/dist/ui/index.d.ts +11 -3
  261. package/dist/ui/index.d.ts.map +1 -1
  262. package/dist/ui/index.js +46 -887
  263. package/dist/ui/index.js.map +1 -1
  264. package/dist/ui/input-utils.d.ts +20 -0
  265. package/dist/ui/input-utils.d.ts.map +1 -0
  266. package/dist/ui/input-utils.js +35 -0
  267. package/dist/ui/input-utils.js.map +1 -0
  268. package/dist/ui/messages.d.ts +8 -7
  269. package/dist/ui/messages.d.ts.map +1 -1
  270. package/dist/ui/messages.js +51 -35
  271. package/dist/ui/messages.js.map +1 -1
  272. package/dist/ui/modals/index.d.ts +11 -0
  273. package/dist/ui/modals/index.d.ts.map +1 -0
  274. package/dist/ui/modals/index.js +10 -0
  275. package/dist/ui/modals/index.js.map +1 -0
  276. package/dist/ui/{modals.d.ts → modals/prompts.d.ts} +5 -20
  277. package/dist/ui/modals/prompts.d.ts.map +1 -0
  278. package/dist/ui/{modals.js → modals/prompts.js} +14 -94
  279. package/dist/ui/modals/prompts.js.map +1 -0
  280. package/dist/ui/modals/selectors.d.ts +33 -0
  281. package/dist/ui/modals/selectors.d.ts.map +1 -0
  282. package/dist/ui/modals/selectors.js +111 -0
  283. package/dist/ui/modals/selectors.js.map +1 -0
  284. package/dist/ui/modals/setup.d.ts +17 -0
  285. package/dist/ui/modals/setup.d.ts.map +1 -0
  286. package/dist/ui/modals/setup.js +51 -0
  287. package/dist/ui/modals/setup.js.map +1 -0
  288. package/dist/ui/regions/input-region.d.ts +40 -0
  289. package/dist/ui/regions/input-region.d.ts.map +1 -0
  290. package/dist/ui/regions/input-region.js +116 -0
  291. package/dist/ui/regions/input-region.js.map +1 -0
  292. package/dist/ui/regions/modal-host.d.ts +41 -0
  293. package/dist/ui/regions/modal-host.d.ts.map +1 -0
  294. package/dist/ui/regions/modal-host.js +51 -0
  295. package/dist/ui/regions/modal-host.js.map +1 -0
  296. package/dist/ui/regions/render-probe.d.ts +22 -0
  297. package/dist/ui/regions/render-probe.d.ts.map +1 -0
  298. package/dist/ui/regions/render-probe.js +45 -0
  299. package/dist/ui/regions/render-probe.js.map +1 -0
  300. package/dist/ui/regions/static-scrollback.d.ts +39 -0
  301. package/dist/ui/regions/static-scrollback.d.ts.map +1 -0
  302. package/dist/ui/regions/static-scrollback.js +68 -0
  303. package/dist/ui/regions/static-scrollback.js.map +1 -0
  304. package/dist/ui/regions/status-region.d.ts +24 -0
  305. package/dist/ui/regions/status-region.d.ts.map +1 -0
  306. package/dist/ui/regions/status-region.js +17 -0
  307. package/dist/ui/regions/status-region.js.map +1 -0
  308. package/dist/ui/regions/transcript-region.d.ts +41 -0
  309. package/dist/ui/regions/transcript-region.d.ts.map +1 -0
  310. package/dist/ui/regions/transcript-region.js +52 -0
  311. package/dist/ui/regions/transcript-region.js.map +1 -0
  312. package/dist/ui/self-restart.d.ts +9 -0
  313. package/dist/ui/self-restart.d.ts.map +1 -0
  314. package/dist/ui/self-restart.js +24 -0
  315. package/dist/ui/self-restart.js.map +1 -0
  316. package/dist/ui/state/use-chat-controller.d.ts +23 -0
  317. package/dist/ui/state/use-chat-controller.d.ts.map +1 -0
  318. package/dist/ui/state/use-chat-controller.js +643 -0
  319. package/dist/ui/state/use-chat-controller.js.map +1 -0
  320. package/dist/ui/state/use-loop-state.d.ts +23 -0
  321. package/dist/ui/state/use-loop-state.d.ts.map +1 -0
  322. package/dist/ui/state/use-loop-state.js +39 -0
  323. package/dist/ui/state/use-loop-state.js.map +1 -0
  324. package/dist/ui/state/use-modal-state.d.ts +52 -0
  325. package/dist/ui/state/use-modal-state.d.ts.map +1 -0
  326. package/dist/ui/state/use-modal-state.js +44 -0
  327. package/dist/ui/state/use-modal-state.js.map +1 -0
  328. package/dist/ui/state/use-model-state.d.ts +29 -0
  329. package/dist/ui/state/use-model-state.d.ts.map +1 -0
  330. package/dist/ui/state/use-model-state.js +47 -0
  331. package/dist/ui/state/use-model-state.js.map +1 -0
  332. package/dist/ui/state/use-processing-state.d.ts +24 -0
  333. package/dist/ui/state/use-processing-state.d.ts.map +1 -0
  334. package/dist/ui/state/use-processing-state.js +46 -0
  335. package/dist/ui/state/use-processing-state.js.map +1 -0
  336. package/dist/ui/state/use-queue-state.d.ts +19 -0
  337. package/dist/ui/state/use-queue-state.d.ts.map +1 -0
  338. package/dist/ui/state/use-queue-state.js +30 -0
  339. package/dist/ui/state/use-queue-state.js.map +1 -0
  340. package/dist/ui/state/use-session-init.d.ts +22 -0
  341. package/dist/ui/state/use-session-init.d.ts.map +1 -0
  342. package/dist/ui/state/use-session-init.js +77 -0
  343. package/dist/ui/state/use-session-init.js.map +1 -0
  344. package/dist/ui/state/use-session-stats.d.ts +16 -0
  345. package/dist/ui/state/use-session-stats.d.ts.map +1 -0
  346. package/dist/ui/state/use-session-stats.js +18 -0
  347. package/dist/ui/state/use-session-stats.js.map +1 -0
  348. package/dist/ui/state/use-terminal-width.d.ts +8 -0
  349. package/dist/ui/state/use-terminal-width.d.ts.map +1 -0
  350. package/dist/ui/state/use-terminal-width.js +22 -0
  351. package/dist/ui/state/use-terminal-width.js.map +1 -0
  352. package/dist/ui/state/use-transcript-state.d.ts +21 -0
  353. package/dist/ui/state/use-transcript-state.d.ts.map +1 -0
  354. package/dist/ui/state/use-transcript-state.js +49 -0
  355. package/dist/ui/state/use-transcript-state.js.map +1 -0
  356. package/dist/ui/status-bar.d.ts +4 -1
  357. package/dist/ui/status-bar.d.ts.map +1 -1
  358. package/dist/ui/status-bar.js +13 -3
  359. package/dist/ui/status-bar.js.map +1 -1
  360. package/dist/ui/types.d.ts +3 -0
  361. package/dist/ui/types.d.ts.map +1 -1
  362. package/dist/version-check.d.ts.map +1 -1
  363. package/dist/version-check.js +10 -0
  364. package/dist/version-check.js.map +1 -1
  365. package/package.json +12 -8
  366. package/dist/agents/agent-config-loader.d.ts +0 -60
  367. package/dist/agents/agent-config-loader.d.ts.map +0 -1
  368. package/dist/agents/agent-config-loader.js +0 -402
  369. package/dist/agents/agent-config-loader.js.map +0 -1
  370. package/dist/agents/agent-config-presets.d.ts +0 -10
  371. package/dist/agents/agent-config-presets.d.ts.map +0 -1
  372. package/dist/agents/agent-config-presets.js +0 -940
  373. package/dist/agents/agent-config-presets.js.map +0 -1
  374. package/dist/agents/agent-config-types.d.ts +0 -145
  375. package/dist/agents/agent-config-types.d.ts.map +0 -1
  376. package/dist/agents/agent-config-types.js +0 -12
  377. package/dist/agents/agent-config-types.js.map +0 -1
  378. package/dist/agents/agent-detection.d.ts +0 -35
  379. package/dist/agents/agent-detection.d.ts.map +0 -1
  380. package/dist/agents/agent-detection.js +0 -110
  381. package/dist/agents/agent-detection.js.map +0 -1
  382. package/dist/agents/aggregator.d.ts +0 -19
  383. package/dist/agents/aggregator.d.ts.map +0 -1
  384. package/dist/agents/aggregator.js +0 -141
  385. package/dist/agents/aggregator.js.map +0 -1
  386. package/dist/agents/cli-backend.d.ts +0 -32
  387. package/dist/agents/cli-backend.d.ts.map +0 -1
  388. package/dist/agents/cli-backend.js +0 -271
  389. package/dist/agents/cli-backend.js.map +0 -1
  390. package/dist/agents/council-types.d.ts +0 -115
  391. package/dist/agents/council-types.d.ts.map +0 -1
  392. package/dist/agents/council-types.js +0 -81
  393. package/dist/agents/council-types.js.map +0 -1
  394. package/dist/agents/council.d.ts +0 -112
  395. package/dist/agents/council.d.ts.map +0 -1
  396. package/dist/agents/council.js +0 -722
  397. package/dist/agents/council.js.map +0 -1
  398. package/dist/agents/decomposer.d.ts +0 -33
  399. package/dist/agents/decomposer.d.ts.map +0 -1
  400. package/dist/agents/decomposer.js +0 -138
  401. package/dist/agents/decomposer.js.map +0 -1
  402. package/dist/agents/dynamic-tools.d.ts +0 -52
  403. package/dist/agents/dynamic-tools.d.ts.map +0 -1
  404. package/dist/agents/dynamic-tools.js +0 -395
  405. package/dist/agents/dynamic-tools.js.map +0 -1
  406. package/dist/agents/index.d.ts +0 -29
  407. package/dist/agents/index.d.ts.map +0 -1
  408. package/dist/agents/index.js +0 -29
  409. package/dist/agents/index.js.map +0 -1
  410. package/dist/agents/installer.d.ts +0 -39
  411. package/dist/agents/installer.d.ts.map +0 -1
  412. package/dist/agents/installer.js +0 -205
  413. package/dist/agents/installer.js.map +0 -1
  414. package/dist/agents/orchestrator.d.ts +0 -106
  415. package/dist/agents/orchestrator.d.ts.map +0 -1
  416. package/dist/agents/orchestrator.js +0 -363
  417. package/dist/agents/orchestrator.js.map +0 -1
  418. package/dist/agents/sdk-backend.d.ts +0 -63
  419. package/dist/agents/sdk-backend.d.ts.map +0 -1
  420. package/dist/agents/sdk-backend.js +0 -489
  421. package/dist/agents/sdk-backend.js.map +0 -1
  422. package/dist/agents/swarm-types.d.ts +0 -84
  423. package/dist/agents/swarm-types.d.ts.map +0 -1
  424. package/dist/agents/swarm-types.js +0 -20
  425. package/dist/agents/swarm-types.js.map +0 -1
  426. package/dist/agents/swarm.d.ts +0 -79
  427. package/dist/agents/swarm.d.ts.map +0 -1
  428. package/dist/agents/swarm.js +0 -375
  429. package/dist/agents/swarm.js.map +0 -1
  430. package/dist/agents/tools.d.ts +0 -26
  431. package/dist/agents/tools.d.ts.map +0 -1
  432. package/dist/agents/tools.js +0 -776
  433. package/dist/agents/tools.js.map +0 -1
  434. package/dist/agents/types.d.ts +0 -108
  435. package/dist/agents/types.d.ts.map +0 -1
  436. package/dist/agents/types.js +0 -43
  437. package/dist/agents/types.js.map +0 -1
  438. package/dist/api-server.d.ts +0 -36
  439. package/dist/api-server.d.ts.map +0 -1
  440. package/dist/api-server.js +0 -255
  441. package/dist/api-server.js.map +0 -1
  442. package/dist/auto-checkpoint.d.ts +0 -35
  443. package/dist/auto-checkpoint.d.ts.map +0 -1
  444. package/dist/auto-checkpoint.js +0 -143
  445. package/dist/auto-checkpoint.js.map +0 -1
  446. package/dist/background-jobs.d.ts +0 -45
  447. package/dist/background-jobs.d.ts.map +0 -1
  448. package/dist/background-jobs.js +0 -127
  449. package/dist/background-jobs.js.map +0 -1
  450. package/dist/branching.d.ts +0 -56
  451. package/dist/branching.d.ts.map +0 -1
  452. package/dist/branching.js +0 -211
  453. package/dist/branching.js.map +0 -1
  454. package/dist/cli/agent.d.ts +0 -9
  455. package/dist/cli/agent.d.ts.map +0 -1
  456. package/dist/cli/agent.js +0 -314
  457. package/dist/cli/agent.js.map +0 -1
  458. package/dist/cli/commands.d.ts +0 -12
  459. package/dist/cli/commands.d.ts.map +0 -1
  460. package/dist/cli/commands.js +0 -943
  461. package/dist/cli/commands.js.map +0 -1
  462. package/dist/cli/index.d.ts +0 -8
  463. package/dist/cli/index.d.ts.map +0 -1
  464. package/dist/cli/index.js +0 -237
  465. package/dist/cli/index.js.map +0 -1
  466. package/dist/cli/types.d.ts +0 -33
  467. package/dist/cli/types.d.ts.map +0 -1
  468. package/dist/cli/types.js +0 -21
  469. package/dist/cli/types.js.map +0 -1
  470. package/dist/companions.d.ts +0 -54
  471. package/dist/companions.d.ts.map +0 -1
  472. package/dist/companions.js +0 -440
  473. package/dist/companions.js.map +0 -1
  474. package/dist/completion.d.ts +0 -75
  475. package/dist/completion.d.ts.map +0 -1
  476. package/dist/completion.js +0 -234
  477. package/dist/completion.js.map +0 -1
  478. package/dist/file-watcher.d.ts +0 -91
  479. package/dist/file-watcher.d.ts.map +0 -1
  480. package/dist/file-watcher.js +0 -269
  481. package/dist/file-watcher.js.map +0 -1
  482. package/dist/hud/theme-packs/api.d.ts +0 -51
  483. package/dist/hud/theme-packs/api.d.ts.map +0 -1
  484. package/dist/hud/theme-packs/api.js +0 -145
  485. package/dist/hud/theme-packs/api.js.map +0 -1
  486. package/dist/hud/theme-packs/index.d.ts +0 -18
  487. package/dist/hud/theme-packs/index.d.ts.map +0 -1
  488. package/dist/hud/theme-packs/index.js +0 -38
  489. package/dist/hud/theme-packs/index.js.map +0 -1
  490. package/dist/hud/theme-packs/types.d.ts +0 -29
  491. package/dist/hud/theme-packs/types.d.ts.map +0 -1
  492. package/dist/hud/theme-packs/types.js +0 -9
  493. package/dist/hud/theme-packs/types.js.map +0 -1
  494. package/dist/idle-eviction.d.ts +0 -34
  495. package/dist/idle-eviction.d.ts.map +0 -1
  496. package/dist/idle-eviction.js +0 -78
  497. package/dist/idle-eviction.js.map +0 -1
  498. package/dist/keyboard.d.ts +0 -57
  499. package/dist/keyboard.d.ts.map +0 -1
  500. package/dist/keyboard.js +0 -265
  501. package/dist/keyboard.js.map +0 -1
  502. package/dist/model-router.d.ts +0 -67
  503. package/dist/model-router.d.ts.map +0 -1
  504. package/dist/model-router.js.map +0 -1
  505. package/dist/providers/openai-compat-shims.d.ts +0 -31
  506. package/dist/providers/openai-compat-shims.d.ts.map +0 -1
  507. package/dist/providers/openai-compat-shims.js +0 -179
  508. package/dist/providers/openai-compat-shims.js.map +0 -1
  509. package/dist/sandbox-native.d.ts.map +0 -1
  510. package/dist/sandbox-native.js.map +0 -1
  511. package/dist/sandbox.d.ts.map +0 -1
  512. package/dist/sandbox.js.map +0 -1
  513. package/dist/session-timeout.d.ts +0 -31
  514. package/dist/session-timeout.d.ts.map +0 -1
  515. package/dist/session-timeout.js +0 -100
  516. package/dist/session-timeout.js.map +0 -1
  517. package/dist/smart-router.d.ts +0 -73
  518. package/dist/smart-router.d.ts.map +0 -1
  519. package/dist/smart-router.js +0 -332
  520. package/dist/smart-router.js.map +0 -1
  521. package/dist/terminal-recording.d.ts +0 -55
  522. package/dist/terminal-recording.d.ts.map +0 -1
  523. package/dist/terminal-recording.js +0 -182
  524. package/dist/terminal-recording.js.map +0 -1
  525. package/dist/tmux.d.ts +0 -35
  526. package/dist/tmux.d.ts.map +0 -1
  527. package/dist/tmux.js +0 -106
  528. package/dist/tmux.js.map +0 -1
  529. package/dist/ui/modals.d.ts.map +0 -1
  530. package/dist/ui/modals.js.map +0 -1
  531. package/dist/ui/pack-picker.d.ts +0 -12
  532. package/dist/ui/pack-picker.d.ts.map +0 -1
  533. package/dist/ui/pack-picker.js +0 -101
  534. package/dist/ui/pack-picker.js.map +0 -1
  535. package/dist/ui/theme-picker.d.ts +0 -24
  536. package/dist/ui/theme-picker.d.ts.map +0 -1
  537. package/dist/ui/theme-picker.js +0 -190
  538. package/dist/ui/theme-picker.js.map +0 -1
package/dist/tools.js CHANGED
@@ -6,15 +6,15 @@
6
6
  import { spawn } from 'child_process';
7
7
  import * as fs from 'fs';
8
8
  import * as path from 'path';
9
- import * as sandbox from './sandbox.js';
10
- import * as nativeSandbox from './sandbox-native.js';
11
- import { getAgtermTools, isAgtermTool, executeAgtermTool } from './agents/index.js';
9
+ import * as os from 'os';
10
+ import { StringDecoder } from 'string_decoder';
11
+ import * as sandbox from './sandbox/index.js';
12
12
  import { validatePath as scopeValidatePath, isInScope } from './scope.js';
13
13
  import { getPluginTools, isPluginTool, executePluginTool } from './plugins.js';
14
14
  import config from './config.js';
15
- import { applySkin, applyPalette, listSkins, listPalettes } from './hud/api.js';
16
- import { listCompanions } from './companions.js';
17
15
  import { generateDiff as generateFileDiff } from './diff.js';
16
+ import { fleetActive, fleetMirrorToolCall } from './fleet.js';
17
+ import { computeAnchorHash, anchorHashFooter } from './local-model.js';
18
18
  /**
19
19
  * Available tools for the agent
20
20
  */
@@ -270,36 +270,21 @@ export const TOOLS = [
270
270
  },
271
271
  {
272
272
  name: 'configure',
273
- description: `Read, set, or list Calliope configuration options. Use this when the user asks to change settings, switch themes, providers, models, companions, or any preference through natural conversation. Always use action "list" first if you need to show available options.
273
+ description: `Read, set, or list Calliope configuration options. Use this when the user asks to change settings, switch themes, providers, models, or any preference through natural conversation. Always use action "list" first if you need to show available options.
274
274
 
275
275
  CONFIGURABLE SETTINGS:
276
276
  - defaultProvider: AI provider (anthropic, google, openai, together, openrouter, groq, fireworks, mistral, ollama, ai21, huggingface, litellm, bedrock, auto)
277
- - defaultModel: Model name string (provider-specific, e.g. "claude-sonnet-4-20250514", "gemini-2.0-flash", "gpt-4o")
278
- - persona: Agent persona style (calliope, muse, minimal)
277
+ - defaultModel: Model name string (provider-specific, e.g. "claude-sonnet-4-6", "gemini-2.0-flash", "gpt-4o")
279
278
  - maxIterations: Max agent loop iterations (0 = unlimited)
280
279
  - maxIterationTime: Max seconds per iteration (0 = no limit, default: 600)
281
- - fancyOutput: Enable rich formatting (true/false)
282
280
  - autoSaveHistory: Auto-save session history (true/false)
283
281
  - autoUpgrade: Check for updates on startup (true/false)
284
282
  - collapseTools: Auto-collapse tool output (true/false)
285
- - collapseThinking: Auto-collapse think blocks (true/false)
286
283
  - toolDisplayLimit: Show last N tools expanded (0 = all)
287
- - layout: UI layout (classic, response-top, response-bottom, split, zen, focus, dashboard, minimal)
288
- - density: Display density (normal, compact)
289
- - activeSkin: Terminal skin/theme name (use action "list" category "skins" to see options)
290
- - activePalette: Color palette name (use action "list" category "palettes" to see options)
291
- - activeCompanion: AI companion personality (use action "list" category "companions" to see options)
292
- - companionIntensity: Companion personality level (professional, immersive)
293
- - useEmojis: Show emojis in UI (true/false)
294
284
  - diffStyle: Diff display format (inline, unified, side-by-side)
295
- - borderStyle: UI border style (rounded, sharp, double, ascii, none)
296
- - bannerStyle: Startup banner (full, compact, none)
297
285
  - circuitBreakersEnabled: Safety circuit breakers (true/false)
298
286
  - sandboxMode: Code execution sandbox (auto, native, docker, off)
299
- - smartRoutingEnabled: Dynamic model routing (true/false)
300
- - smartRoutingCostSensitivity: Cost vs quality (0-1, 0=best quality, 1=cheapest)
301
- - recordSessions: Record session audit logs (true/false)
302
- - recordingRetentionDays: Auto-delete old recordings after N days (0 = keep forever)`,
287
+ - sessionLogLimit: Cap retained session log items (0 = unlimited)`,
303
288
  parameters: {
304
289
  type: 'object',
305
290
  properties: {
@@ -318,8 +303,8 @@ CONFIGURABLE SETTINGS:
318
303
  },
319
304
  category: {
320
305
  type: 'string',
321
- description: 'Category to list options for (for list action): skins, palettes, companions, providers, layouts, all',
322
- enum: ['skins', 'palettes', 'companions', 'providers', 'layouts', 'all'],
306
+ description: 'Category to list options for (for list action): providers, layouts, all',
307
+ enum: ['providers', 'layouts', 'all'],
323
308
  },
324
309
  },
325
310
  required: ['action'],
@@ -351,13 +336,9 @@ CONFIGURABLE SETTINGS:
351
336
  ];
352
337
  /**
353
338
  * Get all available tools
354
- * Includes agent tools when agentEnabled is true
355
339
  */
356
- export function getTools(agentEnabled = false) {
340
+ export function getTools() {
357
341
  const pluginTools = getPluginTools();
358
- if (agentEnabled) {
359
- return [...TOOLS, ...getAgtermTools(), ...pluginTools];
360
- }
361
342
  return [...TOOLS, ...pluginTools];
362
343
  }
363
344
  /**
@@ -368,14 +349,23 @@ function validatePath(filePath, cwd) {
368
349
  if (filePath.includes('\0')) {
369
350
  throw new Error(`Invalid path: contains null bytes`);
370
351
  }
371
- // Check raw input for path traversal attempts before resolution
352
+ // Check raw input for path traversal attempts before resolution.
353
+ // The scope manager is the single source of truth — no /tmp escape hatch (#139).
372
354
  if (filePath.includes('..')) {
373
355
  const resolved = path.resolve(cwd, filePath);
374
356
  const normalizedCwd = path.resolve(cwd);
375
- if (!resolved.startsWith(normalizedCwd + path.sep) && resolved !== normalizedCwd && !resolved.startsWith('/tmp/') && resolved !== '/tmp') {
357
+ if (!resolved.startsWith(normalizedCwd + path.sep) && resolved !== normalizedCwd) {
376
358
  throw new Error(`Path traversal detected: ${filePath} resolves outside allowed scope`);
377
359
  }
378
360
  }
361
+ // Block tool access to the CLI's own state directory (~/.calliope-cli):
362
+ // hooks, plugins, skills, trust, and MCP server configs there are a
363
+ // code-execution / trust foothold if the agent can plant or read them (#141).
364
+ const cliStateDir = path.join(os.homedir(), '.calliope-cli');
365
+ const absResolved = path.resolve(cwd, filePath);
366
+ if (absResolved === cliStateDir || absResolved.startsWith(cliStateDir + path.sep)) {
367
+ throw new Error(`Refusing tool access inside the Calliope state directory (${cliStateDir}); hooks/plugins/skills/trust there are protected`);
368
+ }
379
369
  // Primary validation via scope manager
380
370
  const validated = scopeValidatePath(filePath, cwd);
381
371
  return validated;
@@ -383,11 +373,11 @@ function validatePath(filePath, cwd) {
383
373
  /**
384
374
  * Execute a tool call
385
375
  */
386
- export async function executeTool(toolCall, cwd, timeout = 60000, onOutput) {
376
+ export async function executeTool(toolCall, cwd, timeout = 60000, onOutput, options) {
387
377
  const { id, name, arguments: args } = toolCall;
388
- // Handle agent tools
389
- if (isAgtermTool(name)) {
390
- return executeAgtermTool(toolCall, cwd);
378
+ // Mirror tool call to the fleet channel
379
+ if (fleetActive()) {
380
+ await fleetMirrorToolCall(name, args);
391
381
  }
392
382
  // Handle plugin tools
393
383
  if (isPluginTool(name)) {
@@ -407,7 +397,7 @@ export async function executeTool(toolCall, cwd, timeout = 60000, onOutput) {
407
397
  if (typeof args.path !== 'string') {
408
398
  return { toolCallId: id, result: 'Error: path must be a string', isError: true };
409
399
  }
410
- result = await readFile(args.path, cwd);
400
+ result = await readFile(args.path, cwd, options?.appendAnchorHash === true, options?.fs);
411
401
  break;
412
402
  }
413
403
  case 'write_file': {
@@ -417,7 +407,7 @@ export async function executeTool(toolCall, cwd, timeout = 60000, onOutput) {
417
407
  if (typeof args.content !== 'string') {
418
408
  return { toolCallId: id, result: 'Error: content must be a string', isError: true };
419
409
  }
420
- result = await writeFile(args.path, args.content, cwd);
410
+ result = await writeFile(args.path, args.content, cwd, options?.fs);
421
411
  break;
422
412
  }
423
413
  case 'list_files': {
@@ -502,42 +492,21 @@ export async function executeTool(toolCall, cwd, timeout = 60000, onOutput) {
502
492
  if (action === 'list') {
503
493
  const category = args.category || 'all';
504
494
  const sections = [];
505
- if (category === 'skins' || category === 'all') {
506
- const skins = listSkins();
507
- const current = config.get('activeSkin');
508
- sections.push('SKINS (activeSkin):\n' + skins.map(s => ` ${s.name === current ? '→ ' : ' '}${s.name} - ${s.description}`).join('\n'));
509
- }
510
- if (category === 'palettes' || category === 'all') {
511
- const palettes = listPalettes();
512
- const current = config.get('activePalette');
513
- sections.push('PALETTES (activePalette):\n' + palettes.map(p => ` ${p.name === current ? '→ ' : ' '}${p.name} - ${p.description}`).join('\n'));
514
- }
515
- if (category === 'companions' || category === 'all') {
516
- const companions = listCompanions();
517
- const current = config.get('activeCompanion');
518
- sections.push('COMPANIONS (activeCompanion):\n' + companions.map(c => ` ${c.name === current ? '→ ' : ' '}${c.name} - ${c.description}`).join('\n'));
519
- }
520
495
  if (category === 'providers' || category === 'all') {
521
496
  const providers = ['anthropic', 'google', 'openai', 'together', 'openrouter', 'groq', 'fireworks', 'mistral', 'ollama', 'ai21', 'huggingface', 'litellm', 'bedrock', 'auto'];
522
497
  const current = config.get('defaultProvider');
523
498
  sections.push('PROVIDERS (defaultProvider):\n' + providers.map(p => ` ${p === current ? '→ ' : ' '}${p}`).join('\n'));
524
499
  }
525
- if (category === 'layouts' || category === 'all') {
526
- const layouts = ['classic', 'response-top', 'response-bottom', 'split', 'zen', 'focus', 'dashboard', 'minimal'];
527
- const current = config.get('layout');
528
- sections.push('LAYOUTS (layout):\n' + layouts.map(l => ` ${l === current ? '→ ' : ' '}${l}`).join('\n'));
529
- }
530
500
  if (category === 'all') {
531
501
  // Also show current key settings
532
502
  const currentSettings = [
533
- `density: ${config.get('density')}`,
534
- `companionIntensity: ${config.get('companionIntensity')}`,
535
- `useEmojis: ${config.get('useEmojis')}`,
536
503
  `diffStyle: ${config.get('diffStyle')}`,
537
- `borderStyle: ${config.get('borderStyle')}`,
538
- `bannerStyle: ${config.get('bannerStyle')}`,
539
504
  `sandboxMode: ${config.get('sandboxMode')}`,
540
- `smartRoutingEnabled: ${config.get('smartRoutingEnabled')}`,
505
+ `collapseTools: ${config.get('collapseTools')}`,
506
+ `toolDisplayLimit: ${config.get('toolDisplayLimit')}`,
507
+ `maxIterations: ${config.get('maxIterations')}`,
508
+ `sessionLogLimit: ${config.get('sessionLogLimit')}`,
509
+ `routing.enabled: ${config.get('routing')?.enabled ?? false}`,
541
510
  `defaultModel: ${config.get('defaultModel') || '(auto)'}`,
542
511
  ];
543
512
  sections.push('CURRENT SETTINGS:\n' + currentSettings.map(s => ` ${s}`).join('\n'));
@@ -565,16 +534,11 @@ export async function executeTool(toolCall, cwd, timeout = 60000, onOutput) {
565
534
  }
566
535
  // Only allow setting safe keys through conversation (allowlist)
567
536
  const SAFE_CONFIG_KEYS = new Set([
568
- 'defaultProvider', 'defaultModel', 'persona', 'maxIterations', 'maxIterationTime',
569
- 'fancyOutput', 'autoSaveHistory', 'autoUpgrade',
570
- 'collapseTools', 'collapseThinking', 'toolDisplayLimit',
571
- 'layout', 'density',
572
- 'activeSkin', 'activePalette', 'activeCompanion', 'activeThemePack',
573
- 'companionIntensity', 'useEmojis', 'diffStyle', 'borderStyle', 'bannerStyle',
537
+ 'defaultProvider', 'defaultModel', 'maxIterations', 'maxIterationTime',
538
+ 'autoSaveHistory', 'autoUpgrade',
539
+ 'collapseTools', 'toolDisplayLimit', 'diffStyle',
574
540
  'circuitBreakersEnabled', 'sandboxMode',
575
- 'smartRoutingEnabled', 'smartRoutingCostSensitivity',
576
- 'recordSessions', 'recordingRetentionDays', 'sessionLogLimit',
577
- 'awsRegion', 'awsProfile',
541
+ 'sessionLogLimit',
578
542
  ]);
579
543
  if (!SAFE_CONFIG_KEYS.has(key)) {
580
544
  return { toolCallId: id, result: `Error: "${key}" cannot be set through conversation. Use /keys command, environment variables, or edit the config file directly.`, isError: true };
@@ -588,31 +552,6 @@ export async function executeTool(toolCall, cwd, timeout = 60000, onOutput) {
588
552
  else if (/^\d+(\.\d+)?$/.test(rawValue))
589
553
  parsedValue = Number(rawValue);
590
554
  try {
591
- // Special handling for HUD settings that need apply functions
592
- if (key === 'activeSkin') {
593
- const success = applySkin(rawValue);
594
- if (!success) {
595
- return { toolCallId: id, result: `Error: skin "${rawValue}" not found. Use action "list" category "skins" to see available skins.`, isError: true };
596
- }
597
- result = `Skin changed to "${rawValue}"`;
598
- break;
599
- }
600
- if (key === 'activePalette') {
601
- const success = applyPalette(rawValue);
602
- if (!success) {
603
- return { toolCallId: id, result: `Error: palette "${rawValue}" not found. Use action "list" category "palettes" to see available palettes.`, isError: true };
604
- }
605
- result = `Palette changed to "${rawValue}"`;
606
- break;
607
- }
608
- if (key === 'activeCompanion') {
609
- if (!listCompanions().some(c => c.name === rawValue)) {
610
- return { toolCallId: id, result: `Error: companion "${rawValue}" not found. Use action "list" category "companions" to see available companions.`, isError: true };
611
- }
612
- config.set('activeCompanion', rawValue);
613
- result = `Companion changed to "${rawValue}"`;
614
- break;
615
- }
616
555
  // Generic config set
617
556
  config.set(key, parsedValue);
618
557
  result = `Set ${key} = ${JSON.stringify(parsedValue)}`;
@@ -642,7 +581,10 @@ export async function executeTool(toolCall, cwd, timeout = 60000, onOutput) {
642
581
  return { toolCallId: id, result: 'Error: new_string must be a string', isError: true };
643
582
  }
644
583
  const replaceAll = args.replace_all === true;
645
- result = await editFile(args.path, args.old_string, args.new_string, replaceAll, cwd);
584
+ // anchor_hash is optional and accepted from any caller; it is only
585
+ // advertised in the simplified local schema (feature 4).
586
+ const anchorHash = typeof args.anchor_hash === 'string' ? args.anchor_hash : undefined;
587
+ result = await editFile(args.path, args.old_string, args.new_string, replaceAll, cwd, anchorHash, options?.fs);
646
588
  break;
647
589
  }
648
590
  case 'glob': {
@@ -686,6 +628,12 @@ export async function executeTool(toolCall, cwd, timeout = 60000, onOutput) {
686
628
  *
687
629
  * Patterns are tested against the normalized command (see normalizeCommand())
688
630
  * to defeat common bypass techniques like quoting, env prefixes, and subshells.
631
+ *
632
+ * SECURITY NOTE (#132): This denylist is ADVISORY / defense-in-depth only. It is
633
+ * fundamentally unwinnable to perfectly screen an arbitrary `bash -c` string, so
634
+ * treat this as a best-effort tripwire, NOT a security boundary. The real control
635
+ * is the native sandbox (see src/sandbox/native.ts); when no sandbox is available
636
+ * the shell tool fails closed (see shouldUseNativeSandbox / executeShell).
689
637
  */
690
638
  const BLOCKED_COMMANDS = [
691
639
  /^sudo\s/,
@@ -738,12 +686,29 @@ function normalizeCommand(command) {
738
686
  return cmd.trim();
739
687
  }
740
688
  /**
741
- * Check a command (and all sub-commands separated by ; or &&/||) against
742
- * the blocklist. Returns the matching pattern source string, or null if allowed.
689
+ * Split a shell command into sub-commands on every separator so anchored deny
690
+ * patterns (^sudo, ^su, ^rm -rf /, ...) are tested at the start of each fragment.
691
+ *
692
+ * Process two-char operators (&&, ||) before single & / | so the singles do not
693
+ * shred the two-char operators they are part of. Newline is also a separator.
694
+ * normalizeCommand() strips quotes for matching, so quoted separators are
695
+ * intentionally not special-cased here (advisory blocklist, #132).
696
+ */
697
+ function splitSubCommands(command) {
698
+ return command
699
+ // two-char logical operators first
700
+ .split(/\s*(?:&&|\|\|)\s*/)
701
+ // then single separators: ; & | and newline (also a trailing & background)
702
+ .flatMap((part) => part.split(/\s*[;&|\n]\s*/));
703
+ }
704
+ /**
705
+ * Check a command (and all sub-commands separated by ; && || & | or newline)
706
+ * against the blocklist. Returns the matching pattern source string, or null if
707
+ * allowed.
743
708
  */
744
709
  function matchesBlocklist(command) {
745
710
  // Split on command separators to check each sub-command
746
- const subCommands = command.split(/\s*(?:;|&&|\|\|)\s*/);
711
+ const subCommands = splitSubCommands(command);
747
712
  for (const sub of subCommands) {
748
713
  const normalized = normalizeCommand(sub);
749
714
  for (const pattern of BLOCKED_COMMANDS) {
@@ -811,29 +776,11 @@ function validateShellPaths(command, cwd) {
811
776
  for (const p of extractedPaths) {
812
777
  const allowed = isInScope(p, cwd);
813
778
  if (!allowed) {
814
- return 'Shell command blocked: "' + p + '" is outside allowed scope. Use /add-dir to expand scope.';
779
+ return 'Shell command blocked: "' + p + '" is outside allowed scope. Use /scope add to expand scope.';
815
780
  }
816
781
  }
817
782
  return null;
818
783
  }
819
- /**
820
- * Determine whether to use native sandboxing for shell commands based on config.
821
- *
822
- * sandboxMode values:
823
- * - 'auto': use native sandbox when available, otherwise run unsandboxed
824
- * - 'native': require native sandbox (fail if unavailable)
825
- * - 'docker': defer to Docker sandbox (handled elsewhere)
826
- * - 'off': no sandboxing
827
- */
828
- function shouldUseNativeSandbox() {
829
- const mode = config.get('sandboxMode') || 'auto';
830
- if (mode === 'off' || mode === 'docker')
831
- return 'skip';
832
- if (mode === 'native')
833
- return 'require';
834
- // 'auto': use if available
835
- return nativeSandbox.isNativeSandboxAvailable() ? 'use' : 'skip';
836
- }
837
784
  /**
838
785
  * Execute a shell command
839
786
  */
@@ -849,14 +796,22 @@ async function executeShell(command, cwd, timeout, onOutput) {
849
796
  return `Error: ${scopeError}`;
850
797
  }
851
798
  // Check if native sandbox should be used
852
- const sandboxDecision = shouldUseNativeSandbox();
853
- if (sandboxDecision === 'require' && !nativeSandbox.isNativeSandboxAvailable()) {
799
+ const sandboxDecision = sandbox.shouldUseNativeSandbox();
800
+ if (sandboxDecision === 'require' && !sandbox.isNativeSandboxAvailable()) {
854
801
  return 'Error: Native sandbox required (sandboxMode=native) but not available on this platform.';
855
802
  }
803
+ // 'auto' is best-effort: when no native backend exists (e.g. Linux/Windows
804
+ // without Seatbelt/Landlock) the command runs unsandboxed rather than failing,
805
+ // so shell execution keeps working on every platform. Users who require
806
+ // enforcement set sandboxMode=native (fail-closed above) or sandboxMode=docker.
807
+ // The sandbox hardening (#133 — network off by default, restricted reads)
808
+ // still applies whenever a sandbox IS active ('use'/'require'/docker).
856
809
  if (sandboxDecision === 'use' || sandboxDecision === 'require') {
857
- const result = await nativeSandbox.executeInNativeSandbox(command, cwd, {
810
+ // Network is OFF by default; opt in via CALLIOPE_SHELL_NETWORK=1 (#133).
811
+ const networkEnabled = process.env.CALLIOPE_SHELL_NETWORK === '1';
812
+ const result = await sandbox.executeInNativeSandbox(command, cwd, {
858
813
  timeout,
859
- networkEnabled: true,
814
+ networkEnabled,
860
815
  });
861
816
  // Shell tool output is transparent — same format as unsandboxed execution
862
817
  let output = result.stdout + (result.stderr ? `\nstderr: ${result.stderr}` : '');
@@ -925,30 +880,41 @@ async function executeShell(command, cwd, timeout, onOutput) {
925
880
  }
926
881
  /**
927
882
  * Read a file
883
+ *
884
+ * When `appendAnchorHash` is set (local backends only), the current content hash
885
+ * is appended so the model can echo it back on a subsequent anchored edit_file
886
+ * (stale-view protection, feature 4).
928
887
  */
929
- async function readFile(filePath, cwd) {
888
+ async function readFile(filePath, cwd, appendAnchorHash = false, fsDelegate) {
930
889
  const absPath = validatePath(filePath, cwd);
931
- if (!fs.existsSync(absPath)) {
932
- throw new Error(`File not found: ${absPath}`);
890
+ let content;
891
+ if (fsDelegate?.readTextFile) {
892
+ // Client-side fs (e.g. an ACP editor buffer that may hold unsaved changes).
893
+ // The client owns existence/size; a missing file surfaces as a thrown error
894
+ // that executeTool turns into a normal tool error.
895
+ content = await fsDelegate.readTextFile(absPath);
933
896
  }
934
- const stats = fs.statSync(absPath);
935
- if (stats.isDirectory()) {
936
- throw new Error(`Path is a directory: ${absPath}`);
937
- }
938
- // Check file size (limit to 1MB)
939
- if (stats.size > 1024 * 1024) {
940
- throw new Error(`File too large (${Math.round(stats.size / 1024)}KB). Max 1MB.`);
941
- }
942
- const content = fs.readFileSync(absPath, 'utf-8');
943
- // Inline file preview header (#119)
944
- const PREVIEW_CAP = 20;
945
- const allLines = content.split('\n');
946
- const totalLines = allLines.length;
947
- const previewLines = allLines.slice(0, PREVIEW_CAP);
897
+ else {
898
+ if (!fs.existsSync(absPath)) {
899
+ throw new Error(`File not found: ${absPath}`);
900
+ }
901
+ const stats = fs.statSync(absPath);
902
+ if (stats.isDirectory()) {
903
+ throw new Error(`Path is a directory: ${absPath}`);
904
+ }
905
+ // Check file size (limit to 1MB)
906
+ if (stats.size > 1024 * 1024) {
907
+ throw new Error(`File too large (${Math.round(stats.size / 1024)}KB). Max 1MB.`);
908
+ }
909
+ content = fs.readFileSync(absPath, 'utf-8');
910
+ }
911
+ // Inline file preview header (#119). The header carries the line count;
912
+ // the full content follows once — no preview/footer, those duplicated the
913
+ // first 20 lines on every read and cost ~20% extra tokens on short files.
914
+ const totalLines = content.split('\n').length;
948
915
  const header = `[file: ${filePath} \u2014 ${totalLines} line${totalLines !== 1 ? 's' : ''}]\n${'─'.repeat(40)}`;
949
- const previewBody = previewLines.join('\n');
950
- const footer = totalLines > PREVIEW_CAP ? `\n... (${totalLines - PREVIEW_CAP} more lines)` : '';
951
- return `${header}\n${previewBody}${footer}\n\n${content}`;
916
+ const footer = appendAnchorHash ? anchorHashFooter(computeAnchorHash(content)) : '';
917
+ return `${header}\n${content}${footer}`;
952
918
  }
953
919
  /**
954
920
  * Generate a simple line-diff between old and new content
@@ -963,9 +929,7 @@ function generateDiff(oldContent, newContent, maxLines = 20) {
963
929
  let deletions = 0;
964
930
  let changesShown = 0;
965
931
  let contextLines = 0;
966
- // Use density setting: compact = 1 context line, normal = 3
967
- const density = config.get('density') || 'normal';
968
- const contextWindow = density === 'compact' ? 1 : 3;
932
+ const contextWindow = 3;
969
933
  // Compute line number width
970
934
  const lineNumWidth = Math.max(4, maxIdx.toString().length);
971
935
  const padNum = (n) => String(n).padStart(lineNumWidth, ' ');
@@ -1013,39 +977,46 @@ function generateDiff(oldContent, newContent, maxLines = 20) {
1013
977
  /**
1014
978
  * Write a file
1015
979
  */
1016
- async function writeFile(filePath, content, cwd) {
980
+ async function writeFile(filePath, content, cwd, fsDelegate) {
1017
981
  const absPath = validatePath(filePath, cwd);
1018
- // Create directory if needed
1019
- const dir = path.dirname(absPath);
1020
- if (!fs.existsSync(dir)) {
1021
- fs.mkdirSync(dir, { recursive: true });
1022
- }
1023
982
  // Read existing content for diff
1024
983
  let oldContent = '';
1025
984
  let isNewFile = true;
1026
- if (fs.existsSync(absPath)) {
1027
- try {
1028
- const stats = fs.statSync(absPath);
1029
- if (!stats.isDirectory() && stats.size < 100 * 1024) {
1030
- oldContent = fs.readFileSync(absPath, 'utf-8');
985
+ if (fsDelegate?.writeTextFile) {
986
+ // Client-side fs: read the current buffer for the diff (absent = new file),
987
+ // then write through the client instead of touching local disk.
988
+ if (fsDelegate.readTextFile) {
989
+ try {
990
+ oldContent = await fsDelegate.readTextFile(absPath);
1031
991
  isNewFile = false;
1032
992
  }
993
+ catch {
994
+ oldContent = '';
995
+ isNewFile = true;
996
+ }
1033
997
  }
1034
- catch {
1035
- // Ignore errors reading old content
1036
- }
998
+ await fsDelegate.writeTextFile(absPath, content);
1037
999
  }
1038
- // Auto-checkpoint before overwriting existing files (#20)
1039
- if (!isNewFile && oldContent) {
1040
- try {
1041
- const { createCheckpoint } = require('./checkpoint.js');
1042
- createCheckpoint(absPath, oldContent);
1000
+ else {
1001
+ // Create directory if needed
1002
+ const dir = path.dirname(absPath);
1003
+ if (!fs.existsSync(dir)) {
1004
+ fs.mkdirSync(dir, { recursive: true });
1043
1005
  }
1044
- catch {
1045
- // Checkpoint module not available or failed - continue with write
1006
+ if (fs.existsSync(absPath)) {
1007
+ try {
1008
+ const stats = fs.statSync(absPath);
1009
+ if (!stats.isDirectory() && stats.size < 100 * 1024) {
1010
+ oldContent = fs.readFileSync(absPath, 'utf-8');
1011
+ isNewFile = false;
1012
+ }
1013
+ }
1014
+ catch {
1015
+ // Ignore errors reading old content
1016
+ }
1046
1017
  }
1018
+ fs.writeFileSync(absPath, content);
1047
1019
  }
1048
- fs.writeFileSync(absPath, content);
1049
1020
  // Generate diff output (#119)
1050
1021
  const DIFF_CAP = 50;
1051
1022
  const header = `[wrote: ${filePath}]\n${'─'.repeat(Math.min(filePath.length + 9, 60))}`;
@@ -1121,6 +1092,8 @@ function listFilesRecursive(dir, prefix, depth) {
1121
1092
  for (const entry of entries.slice(0, 50)) {
1122
1093
  if (entry.name.startsWith('.'))
1123
1094
  continue; // Skip hidden files
1095
+ if (entry.isDirectory() && WALK_IGNORED_DIRS.has(entry.name))
1096
+ continue;
1124
1097
  const entryPath = path.join(dir, entry.name);
1125
1098
  if (entry.isDirectory()) {
1126
1099
  lines.push(`${prefix}📁 ${entry.name}/`);
@@ -1145,12 +1118,10 @@ function listFilesRecursive(dir, prefix, depth) {
1145
1118
  * - 'off': no sandboxing
1146
1119
  */
1147
1120
  async function executeCode(language, code, cwd, timeout) {
1148
- const mode = config.get('sandboxMode') || 'auto';
1149
1121
  const sandboxLang = language === 'node' ? 'node' : language;
1150
- // Determine execution strategy
1151
- const useDocker = mode === 'docker' || (mode === 'auto' && sandbox.isDockerAvailable());
1152
- const useNative = mode === 'native' || (mode === 'auto' && !sandbox.isDockerAvailable() && nativeSandbox.isNativeSandboxAvailable());
1153
- if (useDocker) {
1122
+ // Determine execution strategy from sandboxMode config
1123
+ const strategy = sandbox.selectCodeSandbox();
1124
+ if (strategy === 'docker') {
1154
1125
  // Docker sandbox path (existing behaviour)
1155
1126
  const result = await sandbox.execute(sandboxLang, code, {
1156
1127
  timeout,
@@ -1169,7 +1140,7 @@ async function executeCode(language, code, cwd, timeout) {
1169
1140
  output += `Duration: ${result.duration}ms`;
1170
1141
  return output;
1171
1142
  }
1172
- if (useNative) {
1143
+ if (strategy === 'native') {
1173
1144
  // Native OS sandbox path — write code to temp file and execute
1174
1145
  const fs = await import('fs');
1175
1146
  const os = await import('os');
@@ -1182,7 +1153,7 @@ async function executeCode(language, code, cwd, timeout) {
1182
1153
  : language === 'node' ? `node "${codePath}"`
1183
1154
  : `bash "${codePath}"`;
1184
1155
  const startTime = Date.now();
1185
- const result = await nativeSandbox.executeInNativeSandbox(cmd, cwd, {
1156
+ const result = await sandbox.executeInNativeSandbox(cmd, cwd, {
1186
1157
  timeout,
1187
1158
  readOnlyPaths: [tempDir],
1188
1159
  });
@@ -1277,44 +1248,74 @@ async function webSearch(query, numResults) {
1277
1248
  /**
1278
1249
  * Execute git commands safely
1279
1250
  */
1251
+ // Tokenize git args supporting simple shell-style quoting so commit messages
1252
+ // like -m "fix: thing" parse into two tokens.
1253
+ function parseGitArgs(args) {
1254
+ const tokens = [];
1255
+ const re = /"((?:[^"\\]|\\.)*)"|'((?:[^'\\]|\\.)*)'|(\S+)/g;
1256
+ let m;
1257
+ while ((m = re.exec(args)) !== null) {
1258
+ tokens.push(m[1] ?? m[2] ?? m[3]);
1259
+ }
1260
+ return tokens;
1261
+ }
1262
+ // Single-quote each token so the final string is safe to pass through bash -c
1263
+ // (what executeShell does). A single quote inside a single-quoted string is
1264
+ // escaped as '\'' .
1265
+ function shellEscape(s) {
1266
+ return `'${s.replace(/'/g, `'\\''`)}'`;
1267
+ }
1268
+ // Git-specific flags that cause arbitrary command execution even when passed
1269
+ // without a shell (e.g. over SSH via upload-pack/receive-pack). Blocklisted
1270
+ // because the shell-metachar strip used before did not catch them.
1271
+ const DANGEROUS_GIT_FLAG_RE = /^(--upload-pack|--receive-pack|--exec|--config-env)(=|$)/;
1280
1272
  async function executeGit(operation, args, cwd) {
1281
1273
  const allowedOps = ['status', 'diff', 'log', 'branch', 'add', 'commit', 'push', 'pull', 'stash'];
1282
1274
  if (!allowedOps.includes(operation)) {
1283
1275
  return `Error: Unknown git operation: ${operation}. Allowed: ${allowedOps.join(', ')}`;
1284
1276
  }
1285
- // Sanitize args to prevent command injection via shell metacharacters
1286
- const safeArgs = args.replace(/[;&|`$(){}!#\n\r]/g, '');
1277
+ const tokens = parseGitArgs(args);
1278
+ for (const tok of tokens) {
1279
+ if (DANGEROUS_GIT_FLAG_RE.test(tok)) {
1280
+ return `Error: git flag "${tok.split('=')[0]}" is not allowed (RCE risk)`;
1281
+ }
1282
+ // ext:: remote protocol runs an arbitrary command on the client side.
1283
+ if (tok.startsWith('ext::')) {
1284
+ return 'Error: git "ext::" remote protocol is not allowed (RCE risk)';
1285
+ }
1286
+ }
1287
+ const quoted = tokens.map(shellEscape).join(' ');
1287
1288
  let command;
1288
1289
  switch (operation) {
1289
1290
  case 'status':
1290
1291
  command = 'git status --short';
1291
1292
  break;
1292
1293
  case 'diff':
1293
- command = `git diff ${safeArgs}`.trim();
1294
+ command = `git diff ${quoted}`.trim();
1294
1295
  break;
1295
1296
  case 'log':
1296
- command = `git log --oneline -20 ${safeArgs}`.trim();
1297
+ command = `git log --oneline -20 ${quoted}`.trim();
1297
1298
  break;
1298
1299
  case 'branch':
1299
- command = `git branch ${safeArgs}`.trim();
1300
+ command = `git branch ${quoted}`.trim();
1300
1301
  break;
1301
1302
  case 'add':
1302
- command = `git add ${safeArgs || '.'}`.trim();
1303
+ command = tokens.length ? `git add ${quoted}` : 'git add .';
1303
1304
  break;
1304
1305
  case 'commit':
1305
- if (!safeArgs.includes('-m')) {
1306
+ if (!tokens.includes('-m')) {
1306
1307
  return 'Error: commit requires -m "message"';
1307
1308
  }
1308
- command = `git commit ${safeArgs}`.trim();
1309
+ command = `git commit ${quoted}`.trim();
1309
1310
  break;
1310
1311
  case 'push':
1311
- command = `git push ${safeArgs}`.trim();
1312
+ command = `git push ${quoted}`.trim();
1312
1313
  break;
1313
1314
  case 'pull':
1314
- command = `git pull ${safeArgs}`.trim();
1315
+ command = `git pull ${quoted}`.trim();
1315
1316
  break;
1316
1317
  case 'stash':
1317
- command = `git stash ${safeArgs}`.trim();
1318
+ command = `git stash ${quoted}`.trim();
1318
1319
  break;
1319
1320
  default:
1320
1321
  return `Unknown operation: ${operation}`;
@@ -1347,16 +1348,42 @@ ${content}
1347
1348
  * Edit a file by replacing an exact string (in-place edit).
1348
1349
  * Supports single-occurrence enforcement or replace_all mode.
1349
1350
  */
1350
- async function editFile(filePath, oldString, newString, replaceAll, cwd) {
1351
+ async function editFile(filePath, oldString, newString, replaceAll, cwd, anchorHash, fsDelegate) {
1351
1352
  const absPath = validatePath(filePath, cwd);
1352
- if (!fs.existsSync(absPath)) {
1353
- throw new Error(`File not found: ${absPath}`);
1353
+ let content;
1354
+ if (fsDelegate?.readTextFile) {
1355
+ // Client-side fs: edit against the editor's current buffer, not disk.
1356
+ content = await fsDelegate.readTextFile(absPath);
1354
1357
  }
1355
- const stats = fs.statSync(absPath);
1356
- if (stats.isDirectory()) {
1357
- throw new Error(`Path is a directory: ${absPath}`);
1358
+ else {
1359
+ if (!fs.existsSync(absPath)) {
1360
+ throw new Error(`File not found: ${absPath}`);
1361
+ }
1362
+ const stats = fs.statSync(absPath);
1363
+ if (stats.isDirectory()) {
1364
+ throw new Error(`Path is a directory: ${absPath}`);
1365
+ }
1366
+ content = fs.readFileSync(absPath, 'utf-8');
1367
+ }
1368
+ // Persist the edited content through the delegate when available, else disk.
1369
+ const writeUpdated = async (updated) => {
1370
+ if (fsDelegate?.writeTextFile) {
1371
+ await fsDelegate.writeTextFile(absPath, updated);
1372
+ }
1373
+ else {
1374
+ fs.writeFileSync(absPath, updated);
1375
+ }
1376
+ };
1377
+ // Stale-view protection (feature 4): if the caller supplied an anchor_hash, it
1378
+ // must match the file's current content. A mismatch means the model is acting
1379
+ // on an out-of-date view — fail loudly and tell it to re-read. Absent hash =
1380
+ // no check (inert for cloud providers, which never see the param).
1381
+ if (anchorHash !== undefined) {
1382
+ const currentHash = computeAnchorHash(content);
1383
+ if (anchorHash !== currentHash) {
1384
+ throw new Error(`anchor_hash mismatch: file has changed since you read it (you sent "${anchorHash}", current is "${currentHash}"). Re-read ${filePath} before editing.`);
1385
+ }
1358
1386
  }
1359
- const content = fs.readFileSync(absPath, 'utf-8');
1360
1387
  // Helper to build a compact edit diff from old_string/new_string (#119)
1361
1388
  const buildEditDiff = (oldStr, newStr, fPath, count) => {
1362
1389
  const DIFF_CAP = 50;
@@ -1398,7 +1425,7 @@ async function editFile(filePath, oldString, newString, replaceAll, cwd) {
1398
1425
  if (count === 0) {
1399
1426
  throw new Error(`old_string not found in file: ${absPath}`);
1400
1427
  }
1401
- fs.writeFileSync(absPath, updated);
1428
+ await writeUpdated(updated);
1402
1429
  return buildEditDiff(oldString, newString, filePath, count);
1403
1430
  }
1404
1431
  // Count occurrences
@@ -1410,7 +1437,7 @@ async function editFile(filePath, oldString, newString, replaceAll, cwd) {
1410
1437
  throw new Error(`old_string matches ${occurrences} occurrences — use replace_all: true or make it more specific`);
1411
1438
  }
1412
1439
  const updated = content.replace(oldString, newString);
1413
- fs.writeFileSync(absPath, updated);
1440
+ await writeUpdated(updated);
1414
1441
  return buildEditDiff(oldString, newString, filePath, 1);
1415
1442
  }
1416
1443
  /**
@@ -1468,7 +1495,56 @@ function globToRegex(pattern) {
1468
1495
  /**
1469
1496
  * Recursively walk a directory and collect all file paths relative to the base.
1470
1497
  */
1471
- function walkDir(dir, base, results) {
1498
+ // Directories that are almost always noise when searching a project tree.
1499
+ // Keeping this list small and hardcoded (rather than parsing .gitignore) avoids
1500
+ // both performance cliffs on large monorepos and accidental scans of vendored
1501
+ // secrets in node_modules. Users who need to search these can use the shell tool.
1502
+ const WALK_IGNORED_DIRS = new Set([
1503
+ '.git',
1504
+ 'node_modules',
1505
+ 'dist',
1506
+ 'build',
1507
+ '.next',
1508
+ '.nuxt',
1509
+ '.turbo',
1510
+ '.cache',
1511
+ 'coverage',
1512
+ '.venv',
1513
+ 'venv',
1514
+ '__pycache__',
1515
+ 'target', // rust
1516
+ '.gradle',
1517
+ '.idea',
1518
+ '.vscode',
1519
+ ]);
1520
+ /** Maximum directory recursion depth before walkDir stops descending (#154). */
1521
+ const WALK_MAX_DEPTH = 40;
1522
+ /** Maximum number of entries walkDir will collect before it stops (#154). */
1523
+ const WALK_MAX_ENTRIES = 100000;
1524
+ /**
1525
+ * Recursively collect file paths under `dir` (relative to `base`).
1526
+ *
1527
+ * Hardened (#154): bounded recursion depth, a per-walk Set of visited real
1528
+ * directory paths to guard against directory cycles (bind mounts / hardlinks /
1529
+ * any future symlink-follow), and a cap on the total number of collected
1530
+ * entries. `depth` and `visited` are internal accumulators.
1531
+ */
1532
+ function walkDir(dir, base, results, depth = 0, visited = new Set()) {
1533
+ if (depth > WALK_MAX_DEPTH)
1534
+ return;
1535
+ if (results.length >= WALK_MAX_ENTRIES)
1536
+ return;
1537
+ // Guard against directory cycles by tracking canonical (real) paths.
1538
+ let realDir;
1539
+ try {
1540
+ realDir = fs.realpathSync(dir);
1541
+ }
1542
+ catch {
1543
+ return;
1544
+ }
1545
+ if (visited.has(realDir))
1546
+ return;
1547
+ visited.add(realDir);
1472
1548
  let entries;
1473
1549
  try {
1474
1550
  entries = fs.readdirSync(dir, { withFileTypes: true });
@@ -1477,10 +1553,14 @@ function walkDir(dir, base, results) {
1477
1553
  return;
1478
1554
  }
1479
1555
  for (const entry of entries) {
1556
+ if (results.length >= WALK_MAX_ENTRIES)
1557
+ return;
1558
+ if (entry.isDirectory() && WALK_IGNORED_DIRS.has(entry.name))
1559
+ continue;
1480
1560
  const fullPath = path.join(dir, entry.name);
1481
1561
  const relPath = path.relative(base, fullPath);
1482
1562
  if (entry.isDirectory()) {
1483
- walkDir(fullPath, base, results);
1563
+ walkDir(fullPath, base, results, depth + 1, visited);
1484
1564
  }
1485
1565
  else {
1486
1566
  results.push(relPath);
@@ -1516,6 +1596,73 @@ async function globFiles(pattern, searchCwd) {
1516
1596
  }
1517
1597
  return matched.join('\n');
1518
1598
  }
1599
+ /**
1600
+ * Scan a single file line-by-line and push matching lines into `results`,
1601
+ * without holding the entire file in memory (#154).
1602
+ *
1603
+ * Reads the file in fixed-size chunks, emits complete lines as they are found,
1604
+ * and stops as soon as `results` reaches `maxResults`.
1605
+ */
1606
+ function grepFileStreaming(filePath, regex, relPath, results, maxResults) {
1607
+ const CHUNK_SIZE = 64 * 1024;
1608
+ let fd;
1609
+ try {
1610
+ fd = fs.openSync(filePath, 'r');
1611
+ }
1612
+ catch {
1613
+ return;
1614
+ }
1615
+ try {
1616
+ // StringDecoder buffers partial multi-byte UTF-8 sequences that straddle a
1617
+ // chunk boundary, so lines decode correctly across reads.
1618
+ const decoder = new StringDecoder('utf-8');
1619
+ const buffer = Buffer.allocUnsafe(CHUNK_SIZE);
1620
+ let leftover = '';
1621
+ let lineNo = 0;
1622
+ let bytesRead;
1623
+ try {
1624
+ do {
1625
+ bytesRead = fs.readSync(fd, buffer, 0, CHUNK_SIZE, null);
1626
+ if (bytesRead <= 0)
1627
+ break;
1628
+ const text = leftover + decoder.write(buffer.subarray(0, bytesRead));
1629
+ const parts = text.split('\n');
1630
+ // The last element may be a partial line; carry it to the next chunk.
1631
+ leftover = parts.pop() ?? '';
1632
+ for (const line of parts) {
1633
+ lineNo++;
1634
+ if (regex.test(line)) {
1635
+ results.push(`${relPath}:${lineNo}: ${line}`);
1636
+ if (results.length >= maxResults)
1637
+ return;
1638
+ }
1639
+ }
1640
+ } while (bytesRead > 0);
1641
+ }
1642
+ catch {
1643
+ // Unreadable file (e.g. EISDIR, binary read error) — skip it, mirroring
1644
+ // the previous readFileSync try/catch behavior.
1645
+ return;
1646
+ }
1647
+ // Flush any bytes the decoder was still holding.
1648
+ leftover += decoder.end();
1649
+ // Final partial line (file not ending in newline).
1650
+ if (leftover.length > 0) {
1651
+ lineNo++;
1652
+ if (regex.test(leftover) && results.length < maxResults) {
1653
+ results.push(`${relPath}:${lineNo}: ${leftover}`);
1654
+ }
1655
+ }
1656
+ }
1657
+ finally {
1658
+ try {
1659
+ fs.closeSync(fd);
1660
+ }
1661
+ catch {
1662
+ /* ignore */
1663
+ }
1664
+ }
1665
+ }
1519
1666
  /**
1520
1667
  * Search file contents using a regex pattern (or literal string fallback).
1521
1668
  */
@@ -1568,25 +1715,21 @@ async function grepFiles(pattern, searchPath, cwd, globPattern, caseInsensitive)
1568
1715
  for (const filePath of filesToSearch) {
1569
1716
  if (results.length >= MAX_RESULTS)
1570
1717
  break;
1571
- let content;
1572
1718
  try {
1573
1719
  const fileStat = fs.statSync(filePath);
1720
+ if (!fileStat.isFile())
1721
+ continue; // skip dirs / symlinked dirs / sockets
1574
1722
  if (fileStat.size > 5 * 1024 * 1024)
1575
1723
  continue; // skip files > 5MB
1576
- content = fs.readFileSync(filePath, 'utf-8');
1577
1724
  }
1578
1725
  catch {
1579
1726
  continue;
1580
1727
  }
1581
- const lines = content.split('\n');
1582
1728
  const relPath = path.relative(cwd, filePath);
1583
- for (let lineIdx = 0; lineIdx < lines.length; lineIdx++) {
1584
- if (results.length >= MAX_RESULTS)
1585
- break;
1586
- if (regex.test(lines[lineIdx])) {
1587
- results.push(`${relPath}:${lineIdx + 1}: ${lines[lineIdx]}`);
1588
- }
1589
- }
1729
+ // Scan line-by-line without holding the whole file in memory (#154):
1730
+ // stream the file and only test each line as it is produced, breaking early
1731
+ // once MAX_RESULTS is reached.
1732
+ grepFileStreaming(filePath, regex, relPath, results, MAX_RESULTS);
1590
1733
  }
1591
1734
  if (results.length === 0) {
1592
1735
  return 'No matches found';