@calliopelabs/cli 0.8.20 → 2.0.2

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 (402) hide show
  1. package/dist/agents/agent-config-loader.d.ts +60 -0
  2. package/dist/agents/agent-config-loader.d.ts.map +1 -0
  3. package/dist/agents/agent-config-loader.js +402 -0
  4. package/dist/agents/agent-config-loader.js.map +1 -0
  5. package/dist/agents/agent-config-presets.d.ts +10 -0
  6. package/dist/agents/agent-config-presets.d.ts.map +1 -0
  7. package/dist/agents/agent-config-presets.js +940 -0
  8. package/dist/agents/agent-config-presets.js.map +1 -0
  9. package/dist/agents/agent-config-types.d.ts +145 -0
  10. package/dist/agents/agent-config-types.d.ts.map +1 -0
  11. package/dist/agents/agent-config-types.js +12 -0
  12. package/dist/agents/agent-config-types.js.map +1 -0
  13. package/dist/{agterm → agents}/agent-detection.d.ts +1 -1
  14. package/dist/{agterm → agents}/agent-detection.d.ts.map +1 -1
  15. package/dist/{agterm → agents}/agent-detection.js +21 -5
  16. package/dist/agents/agent-detection.js.map +1 -0
  17. package/dist/agents/aggregator.d.ts +19 -0
  18. package/dist/agents/aggregator.d.ts.map +1 -0
  19. package/dist/agents/aggregator.js +141 -0
  20. package/dist/agents/aggregator.js.map +1 -0
  21. package/dist/{agterm → agents}/cli-backend.d.ts +1 -1
  22. package/dist/{agterm → agents}/cli-backend.d.ts.map +1 -1
  23. package/dist/{agterm → agents}/cli-backend.js +90 -12
  24. package/dist/agents/cli-backend.js.map +1 -0
  25. package/dist/agents/council-types.d.ts +113 -0
  26. package/dist/agents/council-types.d.ts.map +1 -0
  27. package/dist/agents/council-types.js +81 -0
  28. package/dist/agents/council-types.js.map +1 -0
  29. package/dist/agents/council.d.ts +107 -0
  30. package/dist/agents/council.d.ts.map +1 -0
  31. package/dist/agents/council.js +586 -0
  32. package/dist/agents/council.js.map +1 -0
  33. package/dist/agents/decomposer.d.ts +33 -0
  34. package/dist/agents/decomposer.d.ts.map +1 -0
  35. package/dist/agents/decomposer.js +138 -0
  36. package/dist/agents/decomposer.js.map +1 -0
  37. package/dist/agents/dynamic-tools.d.ts +52 -0
  38. package/dist/agents/dynamic-tools.d.ts.map +1 -0
  39. package/dist/agents/dynamic-tools.js +395 -0
  40. package/dist/agents/dynamic-tools.js.map +1 -0
  41. package/dist/agents/index.d.ts +29 -0
  42. package/dist/agents/index.d.ts.map +1 -0
  43. package/dist/agents/index.js +29 -0
  44. package/dist/agents/index.js.map +1 -0
  45. package/dist/agents/installer.d.ts +39 -0
  46. package/dist/agents/installer.d.ts.map +1 -0
  47. package/dist/agents/installer.js +205 -0
  48. package/dist/agents/installer.js.map +1 -0
  49. package/dist/{agterm → agents}/orchestrator.d.ts +7 -2
  50. package/dist/agents/orchestrator.d.ts.map +1 -0
  51. package/dist/{agterm → agents}/orchestrator.js +22 -2
  52. package/dist/agents/orchestrator.js.map +1 -0
  53. package/dist/agents/sdk-backend.d.ts +63 -0
  54. package/dist/agents/sdk-backend.d.ts.map +1 -0
  55. package/dist/agents/sdk-backend.js +489 -0
  56. package/dist/agents/sdk-backend.js.map +1 -0
  57. package/dist/agents/swarm-types.d.ts +83 -0
  58. package/dist/agents/swarm-types.d.ts.map +1 -0
  59. package/dist/agents/swarm-types.js +20 -0
  60. package/dist/agents/swarm-types.js.map +1 -0
  61. package/dist/agents/swarm.d.ts +74 -0
  62. package/dist/agents/swarm.d.ts.map +1 -0
  63. package/dist/agents/swarm.js +307 -0
  64. package/dist/agents/swarm.js.map +1 -0
  65. package/dist/{agterm → agents}/tools.d.ts +7 -5
  66. package/dist/agents/tools.d.ts.map +1 -0
  67. package/dist/agents/tools.js +776 -0
  68. package/dist/agents/tools.js.map +1 -0
  69. package/dist/{agterm → agents}/types.d.ts +14 -2
  70. package/dist/agents/types.d.ts.map +1 -0
  71. package/dist/{agterm → agents}/types.js +2 -2
  72. package/dist/agents/types.js.map +1 -0
  73. package/dist/api-server.d.ts +26 -0
  74. package/dist/api-server.d.ts.map +1 -0
  75. package/dist/api-server.js +230 -0
  76. package/dist/api-server.js.map +1 -0
  77. package/dist/auto-checkpoint.d.ts +35 -0
  78. package/dist/auto-checkpoint.d.ts.map +1 -0
  79. package/dist/auto-checkpoint.js +143 -0
  80. package/dist/auto-checkpoint.js.map +1 -0
  81. package/dist/auto-compressor.d.ts +44 -0
  82. package/dist/auto-compressor.d.ts.map +1 -0
  83. package/dist/auto-compressor.js +145 -0
  84. package/dist/auto-compressor.js.map +1 -0
  85. package/dist/background-jobs.d.ts +45 -0
  86. package/dist/background-jobs.d.ts.map +1 -0
  87. package/dist/background-jobs.js +122 -0
  88. package/dist/background-jobs.js.map +1 -0
  89. package/dist/bin.d.ts +6 -2
  90. package/dist/bin.d.ts.map +1 -1
  91. package/dist/bin.js +127 -24
  92. package/dist/bin.js.map +1 -1
  93. package/dist/checkpoint.d.ts +49 -0
  94. package/dist/checkpoint.d.ts.map +1 -0
  95. package/dist/checkpoint.js +219 -0
  96. package/dist/checkpoint.js.map +1 -0
  97. package/dist/circuit-breaker/breaker.d.ts +80 -0
  98. package/dist/circuit-breaker/breaker.d.ts.map +1 -0
  99. package/dist/circuit-breaker/breaker.js +408 -0
  100. package/dist/circuit-breaker/breaker.js.map +1 -0
  101. package/dist/circuit-breaker/defaults.d.ts +8 -0
  102. package/dist/circuit-breaker/defaults.d.ts.map +1 -0
  103. package/dist/circuit-breaker/defaults.js +35 -0
  104. package/dist/circuit-breaker/defaults.js.map +1 -0
  105. package/dist/circuit-breaker/index.d.ts +9 -0
  106. package/dist/circuit-breaker/index.d.ts.map +1 -0
  107. package/dist/circuit-breaker/index.js +8 -0
  108. package/dist/circuit-breaker/index.js.map +1 -0
  109. package/dist/circuit-breaker/types.d.ts +77 -0
  110. package/dist/circuit-breaker/types.d.ts.map +1 -0
  111. package/dist/circuit-breaker/types.js +8 -0
  112. package/dist/circuit-breaker/types.js.map +1 -0
  113. package/dist/circuit-breaker.d.ts +8 -0
  114. package/dist/circuit-breaker.d.ts.map +1 -0
  115. package/dist/circuit-breaker.js +7 -0
  116. package/dist/circuit-breaker.js.map +1 -0
  117. package/dist/cli/agent.d.ts +9 -0
  118. package/dist/cli/agent.d.ts.map +1 -0
  119. package/dist/cli/agent.js +262 -0
  120. package/dist/cli/agent.js.map +1 -0
  121. package/dist/cli/commands.d.ts +12 -0
  122. package/dist/cli/commands.d.ts.map +1 -0
  123. package/dist/{cli.js → cli/commands.js} +285 -422
  124. package/dist/cli/commands.js.map +1 -0
  125. package/dist/cli/index.d.ts +8 -0
  126. package/dist/cli/index.d.ts.map +1 -0
  127. package/dist/cli/index.js +222 -0
  128. package/dist/cli/index.js.map +1 -0
  129. package/dist/cli/types.d.ts +30 -0
  130. package/dist/cli/types.d.ts.map +1 -0
  131. package/dist/cli/types.js +20 -0
  132. package/dist/cli/types.js.map +1 -0
  133. package/dist/companions.d.ts +54 -0
  134. package/dist/companions.d.ts.map +1 -0
  135. package/dist/companions.js +440 -0
  136. package/dist/companions.js.map +1 -0
  137. package/dist/config.d.ts +23 -1
  138. package/dist/config.d.ts.map +1 -1
  139. package/dist/config.js +95 -22
  140. package/dist/config.js.map +1 -1
  141. package/dist/diff.d.ts +27 -0
  142. package/dist/diff.d.ts.map +1 -1
  143. package/dist/diff.js +415 -10
  144. package/dist/diff.js.map +1 -1
  145. package/dist/errors.d.ts.map +1 -1
  146. package/dist/errors.js +20 -11
  147. package/dist/errors.js.map +1 -1
  148. package/dist/git-status.d.ts +23 -0
  149. package/dist/git-status.d.ts.map +1 -0
  150. package/dist/git-status.js +92 -0
  151. package/dist/git-status.js.map +1 -0
  152. package/dist/headless.d.ts +25 -0
  153. package/dist/headless.d.ts.map +1 -0
  154. package/dist/headless.js +182 -0
  155. package/dist/headless.js.map +1 -0
  156. package/dist/hud/api.d.ts +35 -0
  157. package/dist/hud/api.d.ts.map +1 -0
  158. package/dist/hud/api.js +448 -0
  159. package/dist/hud/api.js.map +1 -0
  160. package/dist/hud/palettes.d.ts +9 -0
  161. package/dist/hud/palettes.d.ts.map +1 -0
  162. package/dist/hud/palettes.js +280 -0
  163. package/dist/hud/palettes.js.map +1 -0
  164. package/dist/hud/skins.d.ts +12 -0
  165. package/dist/hud/skins.d.ts.map +1 -0
  166. package/dist/hud/skins.js +365 -0
  167. package/dist/hud/skins.js.map +1 -0
  168. package/dist/hud/theme-packs/api.d.ts +51 -0
  169. package/dist/hud/theme-packs/api.d.ts.map +1 -0
  170. package/dist/hud/theme-packs/api.js +145 -0
  171. package/dist/hud/theme-packs/api.js.map +1 -0
  172. package/dist/hud/theme-packs/index.d.ts +18 -0
  173. package/dist/hud/theme-packs/index.d.ts.map +1 -0
  174. package/dist/hud/theme-packs/index.js +38 -0
  175. package/dist/hud/theme-packs/index.js.map +1 -0
  176. package/dist/hud/theme-packs/types.d.ts +29 -0
  177. package/dist/hud/theme-packs/types.d.ts.map +1 -0
  178. package/dist/hud/theme-packs/types.js +9 -0
  179. package/dist/hud/theme-packs/types.js.map +1 -0
  180. package/dist/hud/types.d.ts +182 -0
  181. package/dist/hud/types.d.ts.map +1 -0
  182. package/dist/hud/types.js +7 -0
  183. package/dist/hud/types.js.map +1 -0
  184. package/dist/idle-eviction.d.ts +34 -0
  185. package/dist/idle-eviction.d.ts.map +1 -0
  186. package/dist/idle-eviction.js +78 -0
  187. package/dist/idle-eviction.js.map +1 -0
  188. package/dist/index.d.ts +9 -3
  189. package/dist/index.d.ts.map +1 -1
  190. package/dist/index.js +9 -3
  191. package/dist/index.js.map +1 -1
  192. package/dist/iteration-ledger.d.ts +105 -0
  193. package/dist/iteration-ledger.d.ts.map +1 -0
  194. package/dist/iteration-ledger.js +237 -0
  195. package/dist/iteration-ledger.js.map +1 -0
  196. package/dist/markdown.d.ts.map +1 -1
  197. package/dist/markdown.js +1 -27
  198. package/dist/markdown.js.map +1 -1
  199. package/dist/mcp.d.ts +35 -0
  200. package/dist/mcp.d.ts.map +1 -1
  201. package/dist/mcp.js +291 -7
  202. package/dist/mcp.js.map +1 -1
  203. package/dist/memory.d.ts.map +1 -1
  204. package/dist/memory.js +12 -2
  205. package/dist/memory.js.map +1 -1
  206. package/dist/model-detection.d.ts +5 -0
  207. package/dist/model-detection.d.ts.map +1 -1
  208. package/dist/model-detection.js +278 -10
  209. package/dist/model-detection.js.map +1 -1
  210. package/dist/model-router.d.ts.map +1 -1
  211. package/dist/model-router.js +33 -11
  212. package/dist/model-router.js.map +1 -1
  213. package/dist/plugins.d.ts +8 -0
  214. package/dist/plugins.d.ts.map +1 -1
  215. package/dist/plugins.js +97 -6
  216. package/dist/plugins.js.map +1 -1
  217. package/dist/providers/anthropic.d.ts +10 -0
  218. package/dist/providers/anthropic.d.ts.map +1 -0
  219. package/dist/providers/anthropic.js +221 -0
  220. package/dist/providers/anthropic.js.map +1 -0
  221. package/dist/providers/bedrock.d.ts +17 -0
  222. package/dist/providers/bedrock.d.ts.map +1 -0
  223. package/dist/providers/bedrock.js +574 -0
  224. package/dist/providers/bedrock.js.map +1 -0
  225. package/dist/providers/compat.d.ts +13 -0
  226. package/dist/providers/compat.d.ts.map +1 -0
  227. package/dist/providers/compat.js +202 -0
  228. package/dist/providers/compat.js.map +1 -0
  229. package/dist/providers/google.d.ts +10 -0
  230. package/dist/providers/google.d.ts.map +1 -0
  231. package/dist/providers/google.js +203 -0
  232. package/dist/providers/google.js.map +1 -0
  233. package/dist/providers/index.d.ts +23 -0
  234. package/dist/providers/index.d.ts.map +1 -0
  235. package/dist/providers/index.js +145 -0
  236. package/dist/providers/index.js.map +1 -0
  237. package/dist/providers/ollama.d.ts +17 -0
  238. package/dist/providers/ollama.d.ts.map +1 -0
  239. package/dist/providers/ollama.js +289 -0
  240. package/dist/providers/ollama.js.map +1 -0
  241. package/dist/providers/openai.d.ts +121 -0
  242. package/dist/providers/openai.d.ts.map +1 -0
  243. package/dist/providers/openai.js +485 -0
  244. package/dist/providers/openai.js.map +1 -0
  245. package/dist/providers/types.d.ts +63 -0
  246. package/dist/providers/types.d.ts.map +1 -0
  247. package/dist/providers/types.js +164 -0
  248. package/dist/providers/types.js.map +1 -0
  249. package/dist/sandbox-native.d.ts +59 -0
  250. package/dist/sandbox-native.d.ts.map +1 -0
  251. package/dist/sandbox-native.js +292 -0
  252. package/dist/sandbox-native.js.map +1 -0
  253. package/dist/sandbox.d.ts +2 -2
  254. package/dist/sandbox.d.ts.map +1 -1
  255. package/dist/sandbox.js +59 -13
  256. package/dist/sandbox.js.map +1 -1
  257. package/dist/scope.d.ts +3 -1
  258. package/dist/scope.d.ts.map +1 -1
  259. package/dist/scope.js +13 -1
  260. package/dist/scope.js.map +1 -1
  261. package/dist/session-timeout.d.ts +31 -0
  262. package/dist/session-timeout.d.ts.map +1 -0
  263. package/dist/session-timeout.js +100 -0
  264. package/dist/session-timeout.js.map +1 -0
  265. package/dist/setup.d.ts.map +1 -1
  266. package/dist/setup.js +29 -17
  267. package/dist/setup.js.map +1 -1
  268. package/dist/smart-router.d.ts +73 -0
  269. package/dist/smart-router.d.ts.map +1 -0
  270. package/dist/smart-router.js +332 -0
  271. package/dist/smart-router.js.map +1 -0
  272. package/dist/storage.d.ts +19 -0
  273. package/dist/storage.d.ts.map +1 -1
  274. package/dist/storage.js +164 -1
  275. package/dist/storage.js.map +1 -1
  276. package/dist/streaming.d.ts +4 -0
  277. package/dist/streaming.d.ts.map +1 -1
  278. package/dist/streaming.js +12 -0
  279. package/dist/streaming.js.map +1 -1
  280. package/dist/styles.d.ts +32 -0
  281. package/dist/styles.d.ts.map +1 -1
  282. package/dist/styles.js +91 -0
  283. package/dist/styles.js.map +1 -1
  284. package/dist/summarization.d.ts +1 -1
  285. package/dist/summarization.js +4 -4
  286. package/dist/summarization.js.map +1 -1
  287. package/dist/terminal-image.d.ts +115 -0
  288. package/dist/terminal-image.d.ts.map +1 -0
  289. package/dist/terminal-image.js +766 -0
  290. package/dist/terminal-image.js.map +1 -0
  291. package/dist/terminal-recording.d.ts +55 -0
  292. package/dist/terminal-recording.d.ts.map +1 -0
  293. package/dist/terminal-recording.js +182 -0
  294. package/dist/terminal-recording.js.map +1 -0
  295. package/dist/themes.d.ts +19 -35
  296. package/dist/themes.d.ts.map +1 -1
  297. package/dist/themes.js +101 -210
  298. package/dist/themes.js.map +1 -1
  299. package/dist/tmux.d.ts +35 -0
  300. package/dist/tmux.d.ts.map +1 -0
  301. package/dist/tmux.js +106 -0
  302. package/dist/tmux.js.map +1 -0
  303. package/dist/tools.d.ts +3 -3
  304. package/dist/tools.d.ts.map +1 -1
  305. package/dist/tools.js +587 -45
  306. package/dist/tools.js.map +1 -1
  307. package/dist/trust.d.ts +53 -0
  308. package/dist/trust.d.ts.map +1 -0
  309. package/dist/trust.js +154 -0
  310. package/dist/trust.js.map +1 -0
  311. package/dist/types.d.ts +7 -3
  312. package/dist/types.d.ts.map +1 -1
  313. package/dist/types.js +70 -32
  314. package/dist/types.js.map +1 -1
  315. package/dist/ui/agent.d.ts +61 -0
  316. package/dist/ui/agent.d.ts.map +1 -0
  317. package/dist/ui/agent.js +768 -0
  318. package/dist/ui/agent.js.map +1 -0
  319. package/dist/ui/chat-input.d.ts +32 -0
  320. package/dist/ui/chat-input.d.ts.map +1 -0
  321. package/dist/ui/chat-input.js +355 -0
  322. package/dist/ui/chat-input.js.map +1 -0
  323. package/dist/ui/commands.d.ts +92 -0
  324. package/dist/ui/commands.d.ts.map +1 -0
  325. package/dist/ui/commands.js +3006 -0
  326. package/dist/ui/commands.js.map +1 -0
  327. package/dist/ui/completions.d.ts +22 -0
  328. package/dist/ui/completions.d.ts.map +1 -0
  329. package/dist/ui/completions.js +215 -0
  330. package/dist/ui/completions.js.map +1 -0
  331. package/dist/ui/components.d.ts +38 -0
  332. package/dist/ui/components.d.ts.map +1 -0
  333. package/dist/ui/components.js +422 -0
  334. package/dist/ui/components.js.map +1 -0
  335. package/dist/ui/context.d.ts +12 -0
  336. package/dist/ui/context.d.ts.map +1 -0
  337. package/dist/ui/context.js +102 -0
  338. package/dist/ui/context.js.map +1 -0
  339. package/dist/ui/error-boundary.d.ts +33 -0
  340. package/dist/ui/error-boundary.d.ts.map +1 -0
  341. package/dist/ui/error-boundary.js +94 -0
  342. package/dist/ui/error-boundary.js.map +1 -0
  343. package/dist/ui/frame.d.ts +13 -0
  344. package/dist/ui/frame.d.ts.map +1 -0
  345. package/dist/ui/frame.js +89 -0
  346. package/dist/ui/frame.js.map +1 -0
  347. package/dist/ui/index.d.ts +12 -0
  348. package/dist/ui/index.d.ts.map +1 -0
  349. package/dist/ui/index.js +928 -0
  350. package/dist/ui/index.js.map +1 -0
  351. package/dist/ui/messages.d.ts +19 -0
  352. package/dist/ui/messages.d.ts.map +1 -0
  353. package/dist/ui/messages.js +181 -0
  354. package/dist/ui/messages.js.map +1 -0
  355. package/dist/ui/modals.d.ts +52 -0
  356. package/dist/ui/modals.d.ts.map +1 -0
  357. package/dist/ui/modals.js +204 -0
  358. package/dist/ui/modals.js.map +1 -0
  359. package/dist/ui/pack-picker.d.ts +12 -0
  360. package/dist/ui/pack-picker.d.ts.map +1 -0
  361. package/dist/ui/pack-picker.js +101 -0
  362. package/dist/ui/pack-picker.js.map +1 -0
  363. package/dist/ui/status-bar.d.ts +20 -0
  364. package/dist/ui/status-bar.d.ts.map +1 -0
  365. package/dist/ui/status-bar.js +41 -0
  366. package/dist/ui/status-bar.js.map +1 -0
  367. package/dist/ui/theme-picker.d.ts +24 -0
  368. package/dist/ui/theme-picker.d.ts.map +1 -0
  369. package/dist/ui/theme-picker.js +190 -0
  370. package/dist/ui/theme-picker.js.map +1 -0
  371. package/dist/ui/types.d.ts +62 -0
  372. package/dist/ui/types.d.ts.map +1 -0
  373. package/dist/ui/types.js +7 -0
  374. package/dist/ui/types.js.map +1 -0
  375. package/dist/version-check.d.ts.map +1 -1
  376. package/dist/version-check.js +1 -9
  377. package/dist/version-check.js.map +1 -1
  378. package/package.json +8 -3
  379. package/dist/agterm/agent-detection.js.map +0 -1
  380. package/dist/agterm/cli-backend.js.map +0 -1
  381. package/dist/agterm/index.d.ts +0 -12
  382. package/dist/agterm/index.d.ts.map +0 -1
  383. package/dist/agterm/index.js +0 -15
  384. package/dist/agterm/index.js.map +0 -1
  385. package/dist/agterm/orchestrator.d.ts.map +0 -1
  386. package/dist/agterm/orchestrator.js.map +0 -1
  387. package/dist/agterm/tools.d.ts.map +0 -1
  388. package/dist/agterm/tools.js +0 -278
  389. package/dist/agterm/tools.js.map +0 -1
  390. package/dist/agterm/types.d.ts.map +0 -1
  391. package/dist/agterm/types.js.map +0 -1
  392. package/dist/cli.d.ts +0 -14
  393. package/dist/cli.d.ts.map +0 -1
  394. package/dist/cli.js.map +0 -1
  395. package/dist/providers.d.ts +0 -51
  396. package/dist/providers.d.ts.map +0 -1
  397. package/dist/providers.js +0 -1146
  398. package/dist/providers.js.map +0 -1
  399. package/dist/ui-cli.d.ts +0 -17
  400. package/dist/ui-cli.d.ts.map +0 -1
  401. package/dist/ui-cli.js +0 -3730
  402. package/dist/ui-cli.js.map +0 -1
package/dist/tools.js CHANGED
@@ -7,10 +7,13 @@ import { spawn } from 'child_process';
7
7
  import * as fs from 'fs';
8
8
  import * as path from 'path';
9
9
  import * as sandbox from './sandbox.js';
10
- import { getAgtermTools, isAgtermTool, executeAgtermTool } from './agterm/index.js';
11
- import { validatePath as scopeValidatePath } from './scope.js';
10
+ import * as nativeSandbox from './sandbox-native.js';
11
+ import { getAgtermTools, isAgtermTool, executeAgtermTool } from './agents/index.js';
12
+ import { validatePath as scopeValidatePath, isInScope } from './scope.js';
12
13
  import { getPluginTools, isPluginTool, executePluginTool } from './plugins.js';
13
14
  import config from './config.js';
15
+ import { applySkin, applyPalette, listSkins, listPalettes } from './hud/api.js';
16
+ import { listCompanions } from './companions.js';
14
17
  /**
15
18
  * Available tools for the agent
16
19
  */
@@ -92,6 +95,52 @@ export const TOOLS = [
92
95
  required: ['thought'],
93
96
  },
94
97
  },
98
+ {
99
+ name: 'ask_question',
100
+ description: 'Ask the user a clarifying question. Use in plan mode to gather requirements before finalizing a plan. Can present multiple choice options or ask freeform questions.',
101
+ parameters: {
102
+ type: 'object',
103
+ properties: {
104
+ question: {
105
+ type: 'string',
106
+ description: 'The question to ask the user',
107
+ },
108
+ options: {
109
+ type: 'array',
110
+ items: { type: 'string' },
111
+ description: 'Optional list of choices. If provided, displayed as numbered options. Omit for freeform questions.',
112
+ },
113
+ context: {
114
+ type: 'string',
115
+ description: 'Optional context explaining why this question matters for the plan',
116
+ },
117
+ },
118
+ required: ['question'],
119
+ },
120
+ },
121
+ {
122
+ name: 'create_plan',
123
+ description: 'Create a structured execution plan for a complex task. Use this when a task requires multiple steps. The plan will be shown to the user for approval before execution begins. Available in all modes including plan mode.',
124
+ parameters: {
125
+ type: 'object',
126
+ properties: {
127
+ title: {
128
+ type: 'string',
129
+ description: 'Brief title for the plan',
130
+ },
131
+ steps: {
132
+ type: 'array',
133
+ items: { type: 'string' },
134
+ description: 'Ordered list of steps to execute',
135
+ },
136
+ reasoning: {
137
+ type: 'string',
138
+ description: 'Brief explanation of the approach',
139
+ },
140
+ },
141
+ required: ['title', 'steps'],
142
+ },
143
+ },
95
144
  {
96
145
  name: 'execute_code',
97
146
  description: 'Execute code in a sandboxed environment. Supports Python, Node.js, and shell scripts.',
@@ -148,6 +197,63 @@ export const TOOLS = [
148
197
  required: ['operation'],
149
198
  },
150
199
  },
200
+ {
201
+ name: 'configure',
202
+ 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.
203
+
204
+ CONFIGURABLE SETTINGS:
205
+ - defaultProvider: AI provider (anthropic, google, openai, together, openrouter, groq, fireworks, mistral, ollama, ai21, huggingface, litellm, bedrock, auto)
206
+ - defaultModel: Model name string (provider-specific, e.g. "claude-sonnet-4-20250514", "gemini-2.0-flash", "gpt-4o")
207
+ - persona: Agent persona style (calliope, muse, minimal)
208
+ - maxIterations: Max agent loop iterations (0 = unlimited)
209
+ - maxIterationTime: Max seconds per iteration (0 = no limit, default: 600)
210
+ - fancyOutput: Enable rich formatting (true/false)
211
+ - autoSaveHistory: Auto-save session history (true/false)
212
+ - autoUpgrade: Check for updates on startup (true/false)
213
+ - collapseTools: Auto-collapse tool output (true/false)
214
+ - collapseThinking: Auto-collapse think blocks (true/false)
215
+ - toolDisplayLimit: Show last N tools expanded (0 = all)
216
+ - layout: UI layout (classic, response-top, response-bottom, split, zen, focus, dashboard, minimal)
217
+ - density: Display density (normal, compact)
218
+ - activeSkin: Terminal skin/theme name (use action "list" category "skins" to see options)
219
+ - activePalette: Color palette name (use action "list" category "palettes" to see options)
220
+ - activeCompanion: AI companion personality (use action "list" category "companions" to see options)
221
+ - companionIntensity: Companion personality level (professional, immersive)
222
+ - useEmojis: Show emojis in UI (true/false)
223
+ - diffStyle: Diff display format (inline, unified, side-by-side)
224
+ - borderStyle: UI border style (rounded, sharp, double, ascii, none)
225
+ - bannerStyle: Startup banner (full, compact, none)
226
+ - circuitBreakersEnabled: Safety circuit breakers (true/false)
227
+ - sandboxMode: Code execution sandbox (auto, native, docker, off)
228
+ - smartRoutingEnabled: Dynamic model routing (true/false)
229
+ - smartRoutingCostSensitivity: Cost vs quality (0-1, 0=best quality, 1=cheapest)
230
+ - recordSessions: Record session audit logs (true/false)
231
+ - recordingRetentionDays: Auto-delete old recordings after N days (0 = keep forever)`,
232
+ parameters: {
233
+ type: 'object',
234
+ properties: {
235
+ action: {
236
+ type: 'string',
237
+ description: 'Action to perform: "get" reads a setting, "set" changes a setting, "list" shows available options for a category',
238
+ enum: ['get', 'set', 'list'],
239
+ },
240
+ key: {
241
+ type: 'string',
242
+ description: 'Config key name (for get/set actions)',
243
+ },
244
+ value: {
245
+ type: 'string',
246
+ description: 'New value to set (for set action). Use "true"/"false" for booleans, numbers as strings.',
247
+ },
248
+ category: {
249
+ type: 'string',
250
+ description: 'Category to list options for (for list action): skins, palettes, companions, providers, layouts, all',
251
+ enum: ['skins', 'palettes', 'companions', 'providers', 'layouts', 'all'],
252
+ },
253
+ },
254
+ required: ['action'],
255
+ },
256
+ },
151
257
  {
152
258
  name: 'mermaid',
153
259
  description: 'Generate a Mermaid diagram. The output can be rendered as a visual graph.',
@@ -174,11 +280,11 @@ export const TOOLS = [
174
280
  ];
175
281
  /**
176
282
  * Get all available tools
177
- * Includes agterm tools when agtermEnabled is true
283
+ * Includes agent tools when agentEnabled is true
178
284
  */
179
- export function getTools(agtermEnabled = false) {
285
+ export function getTools(agentEnabled = false) {
180
286
  const pluginTools = getPluginTools();
181
- if (agtermEnabled) {
287
+ if (agentEnabled) {
182
288
  return [...TOOLS, ...getAgtermTools(), ...pluginTools];
183
289
  }
184
290
  return [...TOOLS, ...pluginTools];
@@ -187,30 +293,28 @@ export function getTools(agtermEnabled = false) {
187
293
  * Validate path is within allowed directory (prevent path traversal)
188
294
  */
189
295
  function validatePath(filePath, cwd) {
190
- // Primary validation via scope manager
191
- const validated = scopeValidatePath(filePath, cwd);
192
- // Secondary validation: ensure path doesn't escape allowed directories
193
- const resolved = path.resolve(cwd, validated);
194
- const normalizedCwd = path.resolve(cwd);
195
- // Check for path traversal attempts
196
- if (validated.includes('..')) {
197
- // Ensure the resolved path is still within cwd or an allowed scope
198
- if (!resolved.startsWith(normalizedCwd) && !resolved.startsWith('/tmp')) {
296
+ // Check raw input for null bytes before any resolution (path injection attack)
297
+ if (filePath.includes('\0')) {
298
+ throw new Error(`Invalid path: contains null bytes`);
299
+ }
300
+ // Check raw input for path traversal attempts before resolution
301
+ if (filePath.includes('..')) {
302
+ const resolved = path.resolve(cwd, filePath);
303
+ const normalizedCwd = path.resolve(cwd);
304
+ if (!resolved.startsWith(normalizedCwd + path.sep) && resolved !== normalizedCwd && !resolved.startsWith('/tmp/') && resolved !== '/tmp') {
199
305
  throw new Error(`Path traversal detected: ${filePath} resolves outside allowed scope`);
200
306
  }
201
307
  }
202
- // Check for null bytes (path injection attack)
203
- if (validated.includes('\0')) {
204
- throw new Error(`Invalid path: contains null bytes`);
205
- }
308
+ // Primary validation via scope manager
309
+ const validated = scopeValidatePath(filePath, cwd);
206
310
  return validated;
207
311
  }
208
312
  /**
209
313
  * Execute a tool call
210
314
  */
211
- export async function executeTool(toolCall, cwd, timeout = 60000) {
315
+ export async function executeTool(toolCall, cwd, timeout = 60000, onOutput) {
212
316
  const { id, name, arguments: args } = toolCall;
213
- // Handle agterm tools
317
+ // Handle agent tools
214
318
  if (isAgtermTool(name)) {
215
319
  return executeAgtermTool(toolCall, cwd);
216
320
  }
@@ -225,7 +329,7 @@ export async function executeTool(toolCall, cwd, timeout = 60000) {
225
329
  if (typeof args.command !== 'string') {
226
330
  return { toolCallId: id, result: 'Error: command must be a string', isError: true };
227
331
  }
228
- result = await executeShell(args.command, cwd, timeout);
332
+ result = await executeShell(args.command, cwd, timeout, onOutput);
229
333
  break;
230
334
  }
231
335
  case 'read_file': {
@@ -258,6 +362,39 @@ export async function executeTool(toolCall, cwd, timeout = 60000) {
258
362
  result = 'Thought recorded.';
259
363
  break;
260
364
  }
365
+ case 'ask_question': {
366
+ if (typeof args.question !== 'string') {
367
+ return { toolCallId: id, result: 'Error: question must be a string', isError: true };
368
+ }
369
+ // The actual question display is handled by the UI layer (agent.ts)
370
+ // This just returns a placeholder that gets replaced with the user's answer
371
+ const options = Array.isArray(args.options) ? args.options : undefined;
372
+ const contextNote = typeof args.context === 'string' ? args.context : undefined;
373
+ let questionDisplay = args.question;
374
+ if (contextNote)
375
+ questionDisplay += `\n Context: ${contextNote}`;
376
+ if (options)
377
+ questionDisplay += '\n' + options.map((o, i) => ` ${i + 1}. ${o}`).join('\n');
378
+ result = `QUESTION:${questionDisplay}`;
379
+ break;
380
+ }
381
+ case 'create_plan': {
382
+ if (typeof args.title !== 'string') {
383
+ return { toolCallId: id, result: 'Error: title must be a string', isError: true };
384
+ }
385
+ if (!Array.isArray(args.steps) || args.steps.length === 0) {
386
+ return { toolCallId: id, result: 'Error: steps must be a non-empty array of strings', isError: true };
387
+ }
388
+ const planTitle = args.title;
389
+ const planSteps = args.steps;
390
+ const planReasoning = typeof args.reasoning === 'string' ? args.reasoning : undefined;
391
+ let planDisplay = `PLAN:${planTitle}\n`;
392
+ if (planReasoning)
393
+ planDisplay += `Approach: ${planReasoning}\n`;
394
+ planDisplay += '\n' + planSteps.map((s, i) => ` ${i + 1}. [ ] ${s}`).join('\n');
395
+ result = planDisplay;
396
+ break;
397
+ }
261
398
  case 'execute_code': {
262
399
  if (typeof args.language !== 'string' || !['python', 'node', 'bash'].includes(args.language)) {
263
400
  return { toolCallId: id, result: 'Error: language must be python, node, or bash', isError: true };
@@ -286,6 +423,134 @@ export async function executeTool(toolCall, cwd, timeout = 60000) {
286
423
  result = await executeGit(args.operation, gitArgs, cwd);
287
424
  break;
288
425
  }
426
+ case 'configure': {
427
+ const action = args.action;
428
+ if (!action || !['get', 'set', 'list'].includes(action)) {
429
+ return { toolCallId: id, result: 'Error: action must be "get", "set", or "list"', isError: true };
430
+ }
431
+ if (action === 'list') {
432
+ const category = args.category || 'all';
433
+ const sections = [];
434
+ if (category === 'skins' || category === 'all') {
435
+ const skins = listSkins();
436
+ const current = config.get('activeSkin');
437
+ sections.push('SKINS (activeSkin):\n' + skins.map(s => ` ${s.name === current ? '→ ' : ' '}${s.name} - ${s.description}`).join('\n'));
438
+ }
439
+ if (category === 'palettes' || category === 'all') {
440
+ const palettes = listPalettes();
441
+ const current = config.get('activePalette');
442
+ sections.push('PALETTES (activePalette):\n' + palettes.map(p => ` ${p.name === current ? '→ ' : ' '}${p.name} - ${p.description}`).join('\n'));
443
+ }
444
+ if (category === 'companions' || category === 'all') {
445
+ const companions = listCompanions();
446
+ const current = config.get('activeCompanion');
447
+ sections.push('COMPANIONS (activeCompanion):\n' + companions.map(c => ` ${c.name === current ? '→ ' : ' '}${c.name} - ${c.description}`).join('\n'));
448
+ }
449
+ if (category === 'providers' || category === 'all') {
450
+ const providers = ['anthropic', 'google', 'openai', 'together', 'openrouter', 'groq', 'fireworks', 'mistral', 'ollama', 'ai21', 'huggingface', 'litellm', 'bedrock', 'auto'];
451
+ const current = config.get('defaultProvider');
452
+ sections.push('PROVIDERS (defaultProvider):\n' + providers.map(p => ` ${p === current ? '→ ' : ' '}${p}`).join('\n'));
453
+ }
454
+ if (category === 'layouts' || category === 'all') {
455
+ const layouts = ['classic', 'response-top', 'response-bottom', 'split', 'zen', 'focus', 'dashboard', 'minimal'];
456
+ const current = config.get('layout');
457
+ sections.push('LAYOUTS (layout):\n' + layouts.map(l => ` ${l === current ? '→ ' : ' '}${l}`).join('\n'));
458
+ }
459
+ if (category === 'all') {
460
+ // Also show current key settings
461
+ const currentSettings = [
462
+ `density: ${config.get('density')}`,
463
+ `companionIntensity: ${config.get('companionIntensity')}`,
464
+ `useEmojis: ${config.get('useEmojis')}`,
465
+ `diffStyle: ${config.get('diffStyle')}`,
466
+ `borderStyle: ${config.get('borderStyle')}`,
467
+ `bannerStyle: ${config.get('bannerStyle')}`,
468
+ `sandboxMode: ${config.get('sandboxMode')}`,
469
+ `smartRoutingEnabled: ${config.get('smartRoutingEnabled')}`,
470
+ `defaultModel: ${config.get('defaultModel') || '(auto)'}`,
471
+ ];
472
+ sections.push('CURRENT SETTINGS:\n' + currentSettings.map(s => ` ${s}`).join('\n'));
473
+ }
474
+ result = sections.join('\n\n');
475
+ break;
476
+ }
477
+ if (action === 'get') {
478
+ const key = args.key;
479
+ if (!key) {
480
+ return { toolCallId: id, result: 'Error: key is required for get action', isError: true };
481
+ }
482
+ const val = config.get(key);
483
+ result = `${key} = ${JSON.stringify(val)}`;
484
+ break;
485
+ }
486
+ // action === 'set'
487
+ const key = args.key;
488
+ const rawValue = args.value;
489
+ if (!key) {
490
+ return { toolCallId: id, result: 'Error: key is required for set action', isError: true };
491
+ }
492
+ if (rawValue === undefined || rawValue === null) {
493
+ return { toolCallId: id, result: 'Error: value is required for set action', isError: true };
494
+ }
495
+ // Only allow setting safe keys through conversation (allowlist)
496
+ const SAFE_CONFIG_KEYS = new Set([
497
+ 'defaultProvider', 'defaultModel', 'persona', 'maxIterations', 'maxIterationTime',
498
+ 'fancyOutput', 'autoSaveHistory', 'autoUpgrade',
499
+ 'collapseTools', 'collapseThinking', 'toolDisplayLimit',
500
+ 'layout', 'density',
501
+ 'activeSkin', 'activePalette', 'activeCompanion', 'activeThemePack',
502
+ 'companionIntensity', 'useEmojis', 'diffStyle', 'borderStyle', 'bannerStyle',
503
+ 'circuitBreakersEnabled', 'sandboxMode',
504
+ 'smartRoutingEnabled', 'smartRoutingCostSensitivity',
505
+ 'recordSessions', 'recordingRetentionDays',
506
+ 'awsRegion', 'awsProfile',
507
+ ]);
508
+ if (!SAFE_CONFIG_KEYS.has(key)) {
509
+ return { toolCallId: id, result: `Error: "${key}" cannot be set through conversation. Use /keys command, environment variables, or edit the config file directly.`, isError: true };
510
+ }
511
+ // Parse the value to the correct type
512
+ let parsedValue = rawValue;
513
+ if (rawValue === 'true')
514
+ parsedValue = true;
515
+ else if (rawValue === 'false')
516
+ parsedValue = false;
517
+ else if (/^\d+(\.\d+)?$/.test(rawValue))
518
+ parsedValue = Number(rawValue);
519
+ try {
520
+ // Special handling for HUD settings that need apply functions
521
+ if (key === 'activeSkin') {
522
+ const success = applySkin(rawValue);
523
+ if (!success) {
524
+ return { toolCallId: id, result: `Error: skin "${rawValue}" not found. Use action "list" category "skins" to see available skins.`, isError: true };
525
+ }
526
+ result = `Skin changed to "${rawValue}"`;
527
+ break;
528
+ }
529
+ if (key === 'activePalette') {
530
+ const success = applyPalette(rawValue);
531
+ if (!success) {
532
+ return { toolCallId: id, result: `Error: palette "${rawValue}" not found. Use action "list" category "palettes" to see available palettes.`, isError: true };
533
+ }
534
+ result = `Palette changed to "${rawValue}"`;
535
+ break;
536
+ }
537
+ if (key === 'activeCompanion') {
538
+ if (!listCompanions().some(c => c.name === rawValue)) {
539
+ return { toolCallId: id, result: `Error: companion "${rawValue}" not found. Use action "list" category "companions" to see available companions.`, isError: true };
540
+ }
541
+ config.set('activeCompanion', rawValue);
542
+ result = `Companion changed to "${rawValue}"`;
543
+ break;
544
+ }
545
+ // Generic config set
546
+ config.set(key, parsedValue);
547
+ result = `Set ${key} = ${JSON.stringify(parsedValue)}`;
548
+ }
549
+ catch (err) {
550
+ return { toolCallId: id, result: `Error setting ${key}: ${err instanceof Error ? err.message : String(err)}`, isError: true };
551
+ }
552
+ break;
553
+ }
289
554
  case 'mermaid': {
290
555
  const diagramType = typeof args.type === 'string' ? args.type : 'flowchart';
291
556
  if (typeof args.content !== 'string') {
@@ -298,17 +563,207 @@ export async function executeTool(toolCall, cwd, timeout = 60000) {
298
563
  default:
299
564
  return { toolCallId: id, result: `Unknown tool: ${name}`, isError: true };
300
565
  }
301
- return { toolCallId: id, result };
566
+ // Generate human-friendly display summary for large results (#25)
567
+ const lines = result.split('\n');
568
+ let displayResult;
569
+ if (lines.length > 10) {
570
+ const preview = lines.slice(0, 5).join('\n');
571
+ displayResult = `${preview}\n... (${lines.length - 5} more lines)`;
572
+ }
573
+ return { toolCallId: id, result, displayResult };
302
574
  }
303
575
  catch (error) {
304
576
  const msg = error instanceof Error ? error.message : String(error);
305
577
  return { toolCallId: id, result: `Error: ${msg}`, isError: true };
306
578
  }
307
579
  }
580
+ /**
581
+ * Commands that are blocked outright (not just flagged as risky).
582
+ * These are destructive system-level commands that should never be run by an agent.
583
+ *
584
+ * Patterns are tested against the normalized command (see normalizeCommand())
585
+ * to defeat common bypass techniques like quoting, env prefixes, and subshells.
586
+ */
587
+ const BLOCKED_COMMANDS = [
588
+ /^sudo\s/,
589
+ /^su\s/,
590
+ /^rm\s+-rf\s+\//, // rm -rf /
591
+ /^rm\s+-fr\s+\//,
592
+ /^rm\s+-rf\s+~/, // rm -rf ~
593
+ /^rm\s+-fr\s+~/,
594
+ /^dd\s+.*of=\/dev\//, // dd to block devices
595
+ /^mkfs/,
596
+ /^fdisk/,
597
+ /^parted/,
598
+ /^format/,
599
+ />\s*\/dev\//, // redirect to devices
600
+ /^chmod\s+-R\s+777/,
601
+ /^curl.*\|\s*(sh|bash)/, // pipe to shell
602
+ /^wget.*\|\s*(sh|bash)/,
603
+ /\|\s*sh(\s|;|$)/, // pipe to sh (anywhere, not just end)
604
+ /\|\s*bash(\s|;|$)/, // pipe to bash (anywhere, not just end)
605
+ /\|\s*zsh(\s|;|$)/, // pipe to zsh
606
+ /bash\s+<\(/, // process substitution: bash <(...)
607
+ /sh\s+<\(/, // process substitution: sh <(...)
608
+ /zsh\s+<\(/, // process substitution: zsh <(...)
609
+ ];
610
+ /**
611
+ * Normalize a shell command to defeat common blocklist bypass techniques (#60).
612
+ *
613
+ * Handles:
614
+ * - Leading env-var assignments: \`VAR=1 sudo ...\` -> \`sudo ...\`
615
+ * - Subshell wrapping: \`(sudo rm ...)\` -> \`sudo rm ...\`
616
+ * - Quote insertion: \`'su'do\` or \`"su"do\` -> \`sudo\`
617
+ * - Backslash escaping: \`su\do\` -> \`sudo\`
618
+ *
619
+ * The result is used only for blocklist matching; the original command is still
620
+ * passed to the shell for execution.
621
+ */
622
+ function normalizeCommand(command) {
623
+ let cmd = command.trim();
624
+ // Strip leading subshell / group wrappers: ( ... ), { ... }
625
+ while ((cmd.startsWith('(') && cmd.endsWith(')')) ||
626
+ (cmd.startsWith('{') && cmd.endsWith('}'))) {
627
+ cmd = cmd.slice(1, -1).trim();
628
+ }
629
+ // Strip leading env-var assignments: FOO=bar BAZ="qux" command ...
630
+ cmd = cmd.replace(/^(\s*[A-Za-z_][A-Za-z0-9_]*=\S*\s+)+/, '');
631
+ // Remove inserted quotes that break up words: 'su'do -> sudo, "su"do -> sudo
632
+ cmd = cmd.replace(/['"]/g, '');
633
+ // Remove backslash escapes: su\do -> sudo
634
+ cmd = cmd.replace(/\\(.)/g, '$1');
635
+ return cmd.trim();
636
+ }
637
+ /**
638
+ * Check a command (and all sub-commands separated by ; or &&/||) against
639
+ * the blocklist. Returns the matching pattern source string, or null if allowed.
640
+ */
641
+ function matchesBlocklist(command) {
642
+ // Split on command separators to check each sub-command
643
+ const subCommands = command.split(/\s*(?:;|&&|\|\|)\s*/);
644
+ for (const sub of subCommands) {
645
+ const normalized = normalizeCommand(sub);
646
+ for (const pattern of BLOCKED_COMMANDS) {
647
+ if (pattern.test(normalized)) {
648
+ return pattern.source;
649
+ }
650
+ }
651
+ }
652
+ // Also test the full normalized command (for patterns that span separators, like pipes)
653
+ const fullNormalized = normalizeCommand(command);
654
+ for (const pattern of BLOCKED_COMMANDS) {
655
+ if (pattern.test(fullNormalized)) {
656
+ return pattern.source;
657
+ }
658
+ }
659
+ return null;
660
+ }
661
+ /**
662
+ * Extract file paths from a shell command for scope validation (#63).
663
+ *
664
+ * Looks for common file-access commands (cat, cp, mv, head, tail, etc.) and
665
+ * extracts the path arguments. Only absolute paths and paths starting with ~/
666
+ * are extracted, since relative paths are within the cwd which is already in scope.
667
+ *
668
+ * Returns an array of extracted paths (may be empty).
669
+ */
670
+ function extractFilePathsFromCommand(command) {
671
+ const paths = [];
672
+ // Commands that read or write files, followed by path arguments
673
+ const fileCommands = [
674
+ 'cat', 'head', 'tail', 'less', 'more', 'cp', 'mv', 'rm',
675
+ 'tee', 'touch', 'chmod', 'chown', 'ln', 'readlink',
676
+ 'source', '\\.',
677
+ ];
678
+ const cmdPattern = new RegExp('(?:^|[;&|]\\s*)(?:' + fileCommands.join('|') + ')\\s+' +
679
+ '(?:-[^\\s]*\\s+)*' +
680
+ '((?:\\/|~\\/)[^\\s;|&>]+)', 'g');
681
+ let match;
682
+ while ((match = cmdPattern.exec(command)) !== null) {
683
+ let p = match[1];
684
+ if (p.startsWith('~/')) {
685
+ p = path.join(process.env.HOME || '/tmp', p.slice(2));
686
+ }
687
+ p = p.replace(/['"]+$/, '');
688
+ paths.push(p);
689
+ }
690
+ // Also catch redirection targets: > /path, >> /path
691
+ const redirectPattern = />{1,2}\s*((?:\/|~\/)[^\s;|&]+)/g;
692
+ while ((match = redirectPattern.exec(command)) !== null) {
693
+ let p = match[1];
694
+ if (p.startsWith('~/')) {
695
+ p = path.join(process.env.HOME || '/tmp', p.slice(2));
696
+ }
697
+ p = p.replace(/['"]+$/, '');
698
+ paths.push(p);
699
+ }
700
+ return paths;
701
+ }
702
+ /**
703
+ * Validate that a shell command does not access files outside scope (#63).
704
+ * Returns an error message if a path violation is found, or null if ok.
705
+ */
706
+ function validateShellPaths(command, cwd) {
707
+ const extractedPaths = extractFilePathsFromCommand(command);
708
+ for (const p of extractedPaths) {
709
+ const allowed = isInScope(p, cwd);
710
+ if (!allowed) {
711
+ return 'Shell command blocked: "' + p + '" is outside allowed scope. Use /add-dir to expand scope.';
712
+ }
713
+ }
714
+ return null;
715
+ }
716
+ /**
717
+ * Determine whether to use native sandboxing for shell commands based on config.
718
+ *
719
+ * sandboxMode values:
720
+ * - 'auto': use native sandbox when available, otherwise run unsandboxed
721
+ * - 'native': require native sandbox (fail if unavailable)
722
+ * - 'docker': defer to Docker sandbox (handled elsewhere)
723
+ * - 'off': no sandboxing
724
+ */
725
+ function shouldUseNativeSandbox() {
726
+ const mode = config.get('sandboxMode') || 'auto';
727
+ if (mode === 'off' || mode === 'docker')
728
+ return 'skip';
729
+ if (mode === 'native')
730
+ return 'require';
731
+ // 'auto': use if available
732
+ return nativeSandbox.isNativeSandboxAvailable() ? 'use' : 'skip';
733
+ }
308
734
  /**
309
735
  * Execute a shell command
310
736
  */
311
- async function executeShell(command, cwd, timeout) {
737
+ async function executeShell(command, cwd, timeout, onOutput) {
738
+ // Check against blocked command patterns using normalized matching (#60)
739
+ const blocked = matchesBlocklist(command);
740
+ if (blocked) {
741
+ return `Error: Command blocked for safety. Pattern "${blocked}" is not allowed.`;
742
+ }
743
+ // Check file paths in shell commands against scope (#63)
744
+ const scopeError = validateShellPaths(command, cwd);
745
+ if (scopeError) {
746
+ return `Error: ${scopeError}`;
747
+ }
748
+ // Check if native sandbox should be used
749
+ const sandboxDecision = shouldUseNativeSandbox();
750
+ if (sandboxDecision === 'require' && !nativeSandbox.isNativeSandboxAvailable()) {
751
+ return 'Error: Native sandbox required (sandboxMode=native) but not available on this platform.';
752
+ }
753
+ if (sandboxDecision === 'use' || sandboxDecision === 'require') {
754
+ const result = await nativeSandbox.executeInNativeSandbox(command, cwd, {
755
+ timeout,
756
+ networkEnabled: true,
757
+ });
758
+ // Shell tool output is transparent — same format as unsandboxed execution
759
+ let output = result.stdout + (result.stderr ? `\nstderr: ${result.stderr}` : '');
760
+ if (result.exitCode !== 0) {
761
+ return `Exit code ${result.exitCode}\n${output}`;
762
+ }
763
+ return output || '(no output)';
764
+ }
765
+ // Fallback: unsandboxed execution
766
+ const MAX_OUTPUT_SIZE = 50000; // 50K chars max output
312
767
  return new Promise((resolve, reject) => {
313
768
  const proc = spawn('bash', ['-c', command], {
314
769
  cwd,
@@ -317,11 +772,30 @@ async function executeShell(command, cwd, timeout) {
317
772
  });
318
773
  let stdout = '';
319
774
  let stderr = '';
775
+ let truncated = false;
320
776
  proc.stdout.on('data', (data) => {
321
- stdout += data.toString();
777
+ const chunk = data.toString();
778
+ if (onOutput)
779
+ onOutput(chunk);
780
+ if (!truncated) {
781
+ stdout += chunk;
782
+ if (stdout.length > MAX_OUTPUT_SIZE) {
783
+ stdout = stdout.slice(0, MAX_OUTPUT_SIZE);
784
+ truncated = true;
785
+ }
786
+ }
322
787
  });
323
788
  proc.stderr.on('data', (data) => {
324
- stderr += data.toString();
789
+ const chunk = data.toString();
790
+ if (onOutput)
791
+ onOutput(chunk);
792
+ if (!truncated) {
793
+ stderr += chunk;
794
+ if (stderr.length > MAX_OUTPUT_SIZE) {
795
+ stderr = stderr.slice(0, MAX_OUTPUT_SIZE);
796
+ truncated = true;
797
+ }
798
+ }
325
799
  });
326
800
  const timer = setTimeout(() => {
327
801
  proc.kill('SIGTERM');
@@ -329,7 +803,10 @@ async function executeShell(command, cwd, timeout) {
329
803
  }, timeout);
330
804
  proc.on('close', (code) => {
331
805
  clearTimeout(timer);
332
- const output = stdout + (stderr ? `\nstderr: ${stderr}` : '');
806
+ let output = stdout + (stderr ? `\nstderr: ${stderr}` : '');
807
+ if (truncated) {
808
+ output += '\n\n[Output truncated at 50K chars. Use head/tail/grep to filter.]';
809
+ }
333
810
  if (code !== 0) {
334
811
  resolve(`Exit code ${code}\n${output}`);
335
812
  }
@@ -446,6 +923,16 @@ async function writeFile(filePath, content, cwd) {
446
923
  // Ignore errors reading old content
447
924
  }
448
925
  }
926
+ // Auto-checkpoint before overwriting existing files (#20)
927
+ if (!isNewFile && oldContent) {
928
+ try {
929
+ const { createCheckpoint } = require('./checkpoint.js');
930
+ createCheckpoint(absPath, oldContent);
931
+ }
932
+ catch {
933
+ // Checkpoint module not available or failed - continue with write
934
+ }
935
+ }
449
936
  fs.writeFileSync(absPath, content);
450
937
  // Generate diff output
451
938
  if (isNewFile) {
@@ -519,29 +1006,84 @@ function listFilesRecursive(dir, prefix, depth) {
519
1006
  return lines.join('\n');
520
1007
  }
521
1008
  /**
522
- * Execute code in a sandboxed environment
1009
+ * Execute code in a sandboxed environment.
1010
+ *
1011
+ * Sandbox selection order based on sandboxMode config:
1012
+ * - 'docker': Docker only (existing behaviour)
1013
+ * - 'native': native OS sandbox only
1014
+ * - 'auto': Docker first, then native sandbox, then unsandboxed
1015
+ * - 'off': no sandboxing
523
1016
  */
524
1017
  async function executeCode(language, code, cwd, timeout) {
525
- // Map language names to sandbox language types
1018
+ const mode = config.get('sandboxMode') || 'auto';
526
1019
  const sandboxLang = language === 'node' ? 'node' : language;
527
- // Try sandboxed execution first (using Docker if available)
528
- const result = await sandbox.execute(sandboxLang, code, {
529
- timeout,
530
- mountWorkdir: true,
531
- readOnly: false,
532
- });
533
- const sandboxIndicator = result.sandboxed ? '🔒 [sandboxed]' : '⚠️ [unsandboxed]';
534
- const statusIndicator = result.success ? '✓' : '✗';
535
- let output = `${sandboxIndicator} ${statusIndicator} [${language}]\n`;
536
- if (result.stdout) {
537
- output += `Output:\n${result.stdout}\n`;
1020
+ // Determine execution strategy
1021
+ const useDocker = mode === 'docker' || (mode === 'auto' && sandbox.isDockerAvailable());
1022
+ const useNative = mode === 'native' || (mode === 'auto' && !sandbox.isDockerAvailable() && nativeSandbox.isNativeSandboxAvailable());
1023
+ if (useDocker) {
1024
+ // Docker sandbox path (existing behaviour)
1025
+ const result = await sandbox.execute(sandboxLang, code, {
1026
+ timeout,
1027
+ mountWorkdir: true,
1028
+ readOnly: true,
1029
+ }, cwd);
1030
+ const sandboxIndicator = result.sandboxed ? '[sandboxed:docker]' : '[unsandboxed]';
1031
+ const statusIndicator = result.success ? 'ok' : 'err';
1032
+ let output = `${sandboxIndicator} ${statusIndicator} [${language}]\n`;
1033
+ if (result.stdout)
1034
+ output += `Output:\n${result.stdout}\n`;
1035
+ if (result.stderr)
1036
+ output += `Errors:\n${result.stderr}\n`;
1037
+ if (!result.success && !result.stdout && !result.stderr)
1038
+ output += `Exit code: ${result.exitCode}\n`;
1039
+ output += `Duration: ${result.duration}ms`;
1040
+ return output;
538
1041
  }
539
- if (result.stderr) {
540
- output += `Errors:\n${result.stderr}\n`;
1042
+ if (useNative) {
1043
+ // Native OS sandbox path — write code to temp file and execute
1044
+ const fs = await import('fs');
1045
+ const os = await import('os');
1046
+ const pathMod = await import('path');
1047
+ const tempDir = fs.mkdtempSync(pathMod.join(os.tmpdir(), 'calliope-native-'));
1048
+ const ext = language === 'python' ? 'py' : language === 'node' ? 'js' : 'sh';
1049
+ const codePath = pathMod.join(tempDir, `code.${ext}`);
1050
+ fs.writeFileSync(codePath, code);
1051
+ const cmd = language === 'python' ? `python3 "${codePath}"`
1052
+ : language === 'node' ? `node "${codePath}"`
1053
+ : `bash "${codePath}"`;
1054
+ const startTime = Date.now();
1055
+ const result = await nativeSandbox.executeInNativeSandbox(cmd, cwd, {
1056
+ timeout,
1057
+ readOnlyPaths: [tempDir],
1058
+ });
1059
+ // Cleanup temp
1060
+ try {
1061
+ fs.rmSync(tempDir, { recursive: true });
1062
+ }
1063
+ catch { /* ignore */ }
1064
+ const duration = Date.now() - startTime;
1065
+ const sandboxIndicator = result.sandboxed ? `[sandboxed:${result.backend}]` : '[unsandboxed]';
1066
+ const statusIndicator = result.exitCode === 0 ? 'ok' : 'err';
1067
+ let output = `${sandboxIndicator} ${statusIndicator} [${language}]\n`;
1068
+ if (result.stdout)
1069
+ output += `Output:\n${result.stdout}\n`;
1070
+ if (result.stderr)
1071
+ output += `Errors:\n${result.stderr}\n`;
1072
+ if (result.exitCode !== 0 && !result.stdout && !result.stderr)
1073
+ output += `Exit code: ${result.exitCode}\n`;
1074
+ output += `Duration: ${duration}ms`;
1075
+ return output;
541
1076
  }
542
- if (!result.success && !result.stdout && !result.stderr) {
1077
+ // Unsandboxed fallback (mode === 'off' or nothing available)
1078
+ const result = await sandbox.executeUnsafe(sandboxLang, code, timeout);
1079
+ const statusIndicator = result.success ? 'ok' : 'err';
1080
+ let output = `[unsandboxed] ${statusIndicator} [${language}]\n`;
1081
+ if (result.stdout)
1082
+ output += `Output:\n${result.stdout}\n`;
1083
+ if (result.stderr)
1084
+ output += `Errors:\n${result.stderr}\n`;
1085
+ if (!result.success && !result.stdout && !result.stderr)
543
1086
  output += `Exit code: ${result.exitCode}\n`;
544
- }
545
1087
  output += `Duration: ${result.duration}ms`;
546
1088
  return output;
547
1089
  }
@@ -610,8 +1152,8 @@ async function executeGit(operation, args, cwd) {
610
1152
  if (!allowedOps.includes(operation)) {
611
1153
  return `Error: Unknown git operation: ${operation}. Allowed: ${allowedOps.join(', ')}`;
612
1154
  }
613
- // Sanitize args to prevent command injection
614
- const safeArgs = args.replace(/[;&|`$]/g, '');
1155
+ // Sanitize args to prevent command injection via shell metacharacters
1156
+ const safeArgs = args.replace(/[;&|`$(){}!#\n\r]/g, '');
615
1157
  let command;
616
1158
  switch (operation) {
617
1159
  case 'status':