profclaw 2.0.0 → 2.3.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 (477) hide show
  1. package/.env.example +14 -2
  2. package/CHANGELOG.md +37 -0
  3. package/CONTRIBUTING.md +20 -0
  4. package/Dockerfile +39 -23
  5. package/README.md +174 -40
  6. package/dist/agents/agent-summary.d.ts +44 -0
  7. package/dist/agents/agent-summary.js +176 -0
  8. package/dist/agents/auto-memory.d.ts +59 -0
  9. package/dist/agents/auto-memory.js +310 -0
  10. package/dist/agents/checkpoint-manager.d.ts +76 -0
  11. package/dist/agents/checkpoint-manager.js +156 -0
  12. package/dist/agents/circuit-breaker.d.ts +54 -0
  13. package/dist/agents/circuit-breaker.js +153 -0
  14. package/dist/agents/context-compactor.d.ts +70 -0
  15. package/dist/agents/context-compactor.js +361 -0
  16. package/dist/agents/conversation-branch.d.ts +55 -0
  17. package/dist/agents/conversation-branch.js +115 -0
  18. package/dist/agents/error-recovery.d.ts +44 -0
  19. package/dist/agents/error-recovery.js +197 -0
  20. package/dist/agents/events.d.ts +76 -0
  21. package/dist/agents/events.js +8 -0
  22. package/dist/agents/executor.d.ts +35 -1
  23. package/dist/agents/executor.js +573 -124
  24. package/dist/agents/file-snapshots.d.ts +72 -0
  25. package/dist/agents/file-snapshots.js +182 -0
  26. package/dist/agents/index.d.ts +2 -0
  27. package/dist/agents/orchestrator.d.ts +183 -0
  28. package/dist/agents/orchestrator.js +495 -0
  29. package/dist/agents/permissions.d.ts +63 -0
  30. package/dist/agents/permissions.js +138 -0
  31. package/dist/agents/plan-mode.d.ts +71 -0
  32. package/dist/agents/plan-mode.js +186 -0
  33. package/dist/agents/prompt-suggestions.d.ts +33 -0
  34. package/dist/agents/prompt-suggestions.js +208 -0
  35. package/dist/agents/rate-limit-monitor.d.ts +56 -0
  36. package/dist/agents/rate-limit-monitor.js +224 -0
  37. package/dist/agents/result-store.d.ts +37 -0
  38. package/dist/agents/result-store.js +104 -0
  39. package/dist/agents/session-diff.d.ts +43 -0
  40. package/dist/agents/session-diff.js +258 -0
  41. package/dist/agents/tool-loader.d.ts +65 -0
  42. package/dist/agents/tool-loader.js +273 -0
  43. package/dist/agents/transcript.d.ts +96 -0
  44. package/dist/agents/transcript.js +267 -0
  45. package/dist/agents/worktree-manager.d.ts +80 -0
  46. package/dist/agents/worktree-manager.js +203 -0
  47. package/dist/auth/middleware.js +14 -0
  48. package/dist/chat/agentic-executor.d.ts +7 -0
  49. package/dist/chat/agentic-executor.js +253 -30
  50. package/dist/chat/conversations.d.ts +2 -0
  51. package/dist/chat/conversations.js +8 -0
  52. package/dist/chat/execution/executor.d.ts +8 -0
  53. package/dist/chat/execution/executor.js +157 -103
  54. package/dist/chat/execution/index.js +5 -1
  55. package/dist/chat/execution/tool-router.js +40 -14
  56. package/dist/chat/execution/tools/canvas.d.ts +2 -2
  57. package/dist/chat/execution/tools/code-tools.d.ts +66 -0
  58. package/dist/chat/execution/tools/code-tools.js +353 -0
  59. package/dist/chat/execution/tools/complete-task.d.ts +2 -2
  60. package/dist/chat/execution/tools/cron-tool.d.ts +26 -10
  61. package/dist/chat/execution/tools/cron-tool.js +48 -3
  62. package/dist/chat/execution/tools/decompose-task.d.ts +58 -0
  63. package/dist/chat/execution/tools/decompose-task.js +166 -0
  64. package/dist/chat/execution/tools/exec.d.ts +5 -2
  65. package/dist/chat/execution/tools/exec.js +294 -155
  66. package/dist/chat/execution/tools/feed-tools.d.ts +77 -0
  67. package/dist/chat/execution/tools/feed-tools.js +142 -0
  68. package/dist/chat/execution/tools/file-ops.d.ts +1 -1
  69. package/dist/chat/execution/tools/github.d.ts +2 -2
  70. package/dist/chat/execution/tools/index.d.ts +14 -2
  71. package/dist/chat/execution/tools/index.js +86 -12
  72. package/dist/chat/execution/tools/integrations.d.ts +8 -8
  73. package/dist/chat/execution/tools/multi-patch.d.ts +53 -0
  74. package/dist/chat/execution/tools/multi-patch.js +373 -0
  75. package/dist/chat/execution/tools/openai-image-gen.d.ts +2 -2
  76. package/dist/chat/execution/tools/plan-tools.d.ts +80 -0
  77. package/dist/chat/execution/tools/plan-tools.js +134 -0
  78. package/dist/chat/execution/tools/profclaw-ops.d.ts +6 -6
  79. package/dist/chat/execution/tools/repl-tool.d.ts +46 -0
  80. package/dist/chat/execution/tools/repl-tool.js +207 -0
  81. package/dist/chat/execution/tools/sessions-send.d.ts +2 -2
  82. package/dist/chat/execution/tools/slack-actions.d.ts +2 -2
  83. package/dist/chat/execution/tools/system.d.ts +4 -4
  84. package/dist/chat/execution/tools/todo-tool.d.ts +72 -0
  85. package/dist/chat/execution/tools/todo-tool.js +152 -0
  86. package/dist/chat/execution/tools/tool-search.d.ts +50 -0
  87. package/dist/chat/execution/tools/tool-search.js +155 -0
  88. package/dist/chat/execution/types.d.ts +21 -14
  89. package/dist/chat/execution/workflows/executor.d.ts +11 -0
  90. package/dist/chat/execution/workflows/executor.js +99 -1
  91. package/dist/chat/execution/workflows/types.d.ts +35 -2
  92. package/dist/chat/format/index.d.ts +1 -0
  93. package/dist/chat/format/index.js +1 -0
  94. package/dist/chat/format/markdown-convert.d.ts +23 -0
  95. package/dist/chat/format/markdown-convert.js +149 -0
  96. package/dist/chat/index.d.ts +1 -1
  97. package/dist/chat/index.js +1 -1
  98. package/dist/chat/message-handler.js +58 -6
  99. package/dist/chat/proactive/index.d.ts +62 -0
  100. package/dist/chat/proactive/index.js +147 -0
  101. package/dist/chat/prompt-adapter.js +2 -0
  102. package/dist/chat/providers/irc/index.d.ts +2 -2
  103. package/dist/chat/providers/telegram/index.d.ts +2 -0
  104. package/dist/chat/providers/telegram/index.js +21 -2
  105. package/dist/chat/system-prompts.d.ts +3 -3
  106. package/dist/chat/system-prompts.js +116 -61
  107. package/dist/chat/tools.d.ts +6 -6
  108. package/dist/cli/commands/auth.js +64 -1
  109. package/dist/cli/commands/chat.js +1327 -138
  110. package/dist/cli/commands/doctor.js +222 -16
  111. package/dist/cli/commands/history.d.ts +3 -0
  112. package/dist/cli/commands/history.js +138 -0
  113. package/dist/cli/commands/import.d.ts +44 -0
  114. package/dist/cli/commands/import.js +526 -0
  115. package/dist/cli/commands/init.d.ts +13 -0
  116. package/dist/cli/commands/init.js +381 -0
  117. package/dist/cli/commands/onboard.js +210 -30
  118. package/dist/cli/commands/plan.d.ts +13 -0
  119. package/dist/cli/commands/plan.js +184 -0
  120. package/dist/cli/commands/serve.d.ts +7 -0
  121. package/dist/cli/commands/serve.js +77 -18
  122. package/dist/cli/commands/setup.js +84 -3
  123. package/dist/cli/commands/status.js +54 -0
  124. package/dist/cli/commands/tools.js +13 -13
  125. package/dist/cli/commands/tui.js +26 -1
  126. package/dist/cli/index.js +19 -0
  127. package/dist/cli/ink/App.d.ts +13 -0
  128. package/dist/cli/ink/App.js +6 -0
  129. package/dist/cli/ink/ChatApp.d.ts +60 -0
  130. package/dist/cli/ink/ChatApp.js +325 -0
  131. package/dist/cli/ink/DashboardApp.d.ts +3 -0
  132. package/dist/cli/ink/DashboardApp.js +143 -0
  133. package/dist/cli/ink/components/AgentStatus.d.ts +17 -0
  134. package/dist/cli/ink/components/AgentStatus.js +33 -0
  135. package/dist/cli/ink/components/ChannelList.d.ts +17 -0
  136. package/dist/cli/ink/components/ChannelList.js +33 -0
  137. package/dist/cli/ink/components/ConnectionStatus.d.ts +19 -0
  138. package/dist/cli/ink/components/ConnectionStatus.js +50 -0
  139. package/dist/cli/ink/components/CostBar.d.ts +19 -0
  140. package/dist/cli/ink/components/CostBar.js +35 -0
  141. package/dist/cli/ink/components/HookStatus.d.ts +15 -0
  142. package/dist/cli/ink/components/HookStatus.js +7 -0
  143. package/dist/cli/ink/components/PermissionPrompt.d.ts +21 -0
  144. package/dist/cli/ink/components/PermissionPrompt.js +51 -0
  145. package/dist/cli/ink/components/PlanView.d.ts +23 -0
  146. package/dist/cli/ink/components/PlanView.js +26 -0
  147. package/dist/cli/ink/components/ProviderList.d.ts +16 -0
  148. package/dist/cli/ink/components/ProviderList.js +16 -0
  149. package/dist/cli/ink/components/SessionHeader.d.ts +15 -0
  150. package/dist/cli/ink/components/SessionHeader.js +15 -0
  151. package/dist/cli/ink/components/StreamingMessage.d.ts +20 -0
  152. package/dist/cli/ink/components/StreamingMessage.js +27 -0
  153. package/dist/cli/ink/components/SuggestionBar.d.ts +15 -0
  154. package/dist/cli/ink/components/SuggestionBar.js +28 -0
  155. package/dist/cli/ink/components/SystemHealth.d.ts +20 -0
  156. package/dist/cli/ink/components/SystemHealth.js +39 -0
  157. package/dist/cli/ink/components/TaskList.d.ts +17 -0
  158. package/dist/cli/ink/components/TaskList.js +59 -0
  159. package/dist/cli/ink/components/ToolCall.d.ts +17 -0
  160. package/dist/cli/ink/components/ToolCall.js +43 -0
  161. package/dist/cli/ink/index.d.ts +23 -0
  162. package/dist/cli/ink/index.js +15 -0
  163. package/dist/cli/ink/keybindings.d.ts +35 -0
  164. package/dist/cli/ink/keybindings.js +105 -0
  165. package/dist/cli/ink/markdown-renderer.d.ts +47 -0
  166. package/dist/cli/ink/markdown-renderer.js +380 -0
  167. package/dist/cli/ink/output-styles.d.ts +39 -0
  168. package/dist/cli/ink/output-styles.js +119 -0
  169. package/dist/cli/interactive/auto-serve.d.ts +38 -0
  170. package/dist/cli/interactive/auto-serve.js +209 -0
  171. package/dist/cli/interactive/index.d.ts +25 -0
  172. package/dist/cli/interactive/index.js +23 -0
  173. package/dist/cli/interactive/picker.d.ts +28 -0
  174. package/dist/cli/interactive/picker.js +153 -0
  175. package/dist/cli/interactive/renderer.d.ts +26 -0
  176. package/dist/cli/interactive/renderer.js +555 -0
  177. package/dist/cli/interactive/repl.d.ts +14 -0
  178. package/dist/cli/interactive/repl.js +1516 -0
  179. package/dist/cli/interactive/stream-client.d.ts +59 -0
  180. package/dist/cli/interactive/stream-client.js +506 -0
  181. package/dist/cli/interactive/types.d.ts +121 -0
  182. package/dist/cli/interactive/types.js +10 -0
  183. package/dist/cli/utils/api.d.ts +9 -0
  184. package/dist/cli/utils/api.js +58 -2
  185. package/dist/cron/natural-language.d.ts +54 -0
  186. package/dist/cron/natural-language.js +569 -0
  187. package/dist/cron/scheduler.d.ts +34 -4
  188. package/dist/cron/scheduler.js +141 -9
  189. package/dist/cron/templates.d.ts +1 -1
  190. package/dist/cron/templates.js +219 -0
  191. package/dist/feeds/feed-engine.d.ts +149 -0
  192. package/dist/feeds/feed-engine.js +600 -0
  193. package/dist/feeds/index.d.ts +9 -0
  194. package/dist/feeds/index.js +8 -0
  195. package/dist/gateway/router.js +6 -1
  196. package/dist/hooks/built-in/audit-log.d.ts +11 -0
  197. package/dist/hooks/built-in/audit-log.js +84 -0
  198. package/dist/hooks/built-in/cost-warning.d.ts +10 -0
  199. package/dist/hooks/built-in/cost-warning.js +59 -0
  200. package/dist/hooks/built-in/dangerous-tool.d.ts +32 -0
  201. package/dist/hooks/built-in/dangerous-tool.js +80 -0
  202. package/dist/hooks/built-in/index.d.ts +14 -0
  203. package/dist/hooks/built-in/index.js +20 -0
  204. package/dist/hooks/index.d.ts +3 -0
  205. package/dist/hooks/index.js +6 -0
  206. package/dist/hooks/loader.d.ts +17 -0
  207. package/dist/hooks/loader.js +142 -0
  208. package/dist/hooks/registry.d.ts +62 -0
  209. package/dist/hooks/registry.js +127 -0
  210. package/dist/integrations/web-search.js +6 -3
  211. package/dist/memory/index.d.ts +1 -0
  212. package/dist/memory/index.js +2 -0
  213. package/dist/memory/memory-service.js +14 -13
  214. package/dist/memory/observational.d.ts +62 -0
  215. package/dist/memory/observational.js +396 -0
  216. package/dist/middleware/content-filter.d.ts +22 -0
  217. package/dist/middleware/content-filter.js +69 -0
  218. package/dist/middleware/request-validator.d.ts +159 -0
  219. package/dist/middleware/request-validator.js +93 -0
  220. package/dist/plugins/sdk.d.ts +22 -0
  221. package/dist/plugins/sdk.js +17 -1
  222. package/dist/projects/types.d.ts +10 -10
  223. package/dist/providers/ai-sdk.d.ts +11 -0
  224. package/dist/providers/ai-sdk.js +78 -19
  225. package/dist/providers/core/models.js +67 -1
  226. package/dist/providers/core/types.d.ts +3 -0
  227. package/dist/providers/index.d.ts +1 -0
  228. package/dist/providers/index.js +1 -0
  229. package/dist/providers/smart-router.d.ts +118 -0
  230. package/dist/providers/smart-router.js +685 -0
  231. package/dist/providers/types.d.ts +16 -16
  232. package/dist/routes/agents.js +50 -0
  233. package/dist/routes/auth.js +8 -1
  234. package/dist/routes/chat.js +259 -14
  235. package/dist/routes/costs.js +32 -0
  236. package/dist/routes/cron.js +80 -5
  237. package/dist/routes/feeds.d.ts +10 -0
  238. package/dist/routes/feeds.js +168 -0
  239. package/dist/routes/mcp.js +21 -0
  240. package/dist/routes/memory.js +130 -0
  241. package/dist/routes/skills.js +129 -0
  242. package/dist/routes/teams.d.ts +9 -0
  243. package/dist/routes/teams.js +210 -0
  244. package/dist/routes/tools.js +14 -0
  245. package/dist/routes/users.js +2 -0
  246. package/dist/server/route-loader.js +3 -1
  247. package/dist/server/stream-bridge.d.ts +19 -0
  248. package/dist/server/stream-bridge.js +38 -0
  249. package/dist/server.js +310 -36
  250. package/dist/skills/marketplace.d.ts +87 -0
  251. package/dist/skills/marketplace.js +268 -0
  252. package/dist/storage/index.d.ts +14 -0
  253. package/dist/storage/index.js +64 -3
  254. package/dist/storage/schema.d.ts +4 -4
  255. package/dist/storage/schema.js +1 -1
  256. package/dist/teams/index.d.ts +110 -0
  257. package/dist/teams/index.js +365 -0
  258. package/dist/tickets/types.d.ts +39 -39
  259. package/dist/types/summary.d.ts +5 -5
  260. package/dist/utils/auto-update.d.ts +23 -0
  261. package/dist/utils/auto-update.js +157 -0
  262. package/dist/utils/offline-detect.d.ts +55 -0
  263. package/dist/utils/offline-detect.js +121 -0
  264. package/dist/utils/pid-file.d.ts +36 -0
  265. package/dist/utils/pid-file.js +125 -0
  266. package/dist/utils/prevent-sleep.d.ts +27 -0
  267. package/dist/utils/prevent-sleep.js +95 -0
  268. package/dist/utils/sanitizer.d.ts +57 -0
  269. package/dist/utils/sanitizer.js +174 -0
  270. package/docker-compose.yml +13 -137
  271. package/package.json +31 -6
  272. package/skills/deploy-cloudflare/SKILL.md +62 -0
  273. package/skills/deploy-vercel/SKILL.md +67 -0
  274. package/skills/docker-deploy/SKILL.md +158 -0
  275. package/skills/full-stack-builder/SKILL.md +92 -0
  276. package/skills/humanizer/SKILL.md +488 -0
  277. package/skills/profclaw-assistant/SKILL.md +85 -56
  278. package/ui/dist/assets/ActivityView-Da6HQ8ws.js +1 -0
  279. package/ui/dist/assets/AgentList-H58R0kX3.js +1 -0
  280. package/ui/dist/assets/AnalyticsDashboard-B28AeRsc.js +1 -0
  281. package/ui/dist/assets/AreaChart-DwLzeAQ8.js +1 -0
  282. package/ui/dist/assets/CartesianChart-piKQsTAR.js +36 -0
  283. package/ui/dist/assets/ChatView-Dbj8rxU5.js +10 -0
  284. package/ui/dist/assets/CostsDashboard-C6F5df0F.js +1 -0
  285. package/ui/dist/assets/EstimateSelect-wga0rUKX.js +1 -0
  286. package/ui/dist/assets/FeedDashboard-Ef6FEzEf.js +1 -0
  287. package/ui/dist/assets/FloatingChatbot-0yhkUeCq.js +2 -0
  288. package/ui/dist/assets/InviteCodeManagement-tUXfx4Bc.js +2 -0
  289. package/ui/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  290. package/ui/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  291. package/ui/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  292. package/ui/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  293. package/ui/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  294. package/ui/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  295. package/ui/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  296. package/ui/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  297. package/ui/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  298. package/ui/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  299. package/ui/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  300. package/ui/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  301. package/ui/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  302. package/ui/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  303. package/ui/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  304. package/ui/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  305. package/ui/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  306. package/ui/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  307. package/ui/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  308. package/ui/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  309. package/ui/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  310. package/ui/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  311. package/ui/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  312. package/ui/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  313. package/ui/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  314. package/ui/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  315. package/ui/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  316. package/ui/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  317. package/ui/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  318. package/ui/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  319. package/ui/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  320. package/ui/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  321. package/ui/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  322. package/ui/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  323. package/ui/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  324. package/ui/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  325. package/ui/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  326. package/ui/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  327. package/ui/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  328. package/ui/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  329. package/ui/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  330. package/ui/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  331. package/ui/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  332. package/ui/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  333. package/ui/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  334. package/ui/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  335. package/ui/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  336. package/ui/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  337. package/ui/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  338. package/ui/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  339. package/ui/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  340. package/ui/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  341. package/ui/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  342. package/ui/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  343. package/ui/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  344. package/ui/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  345. package/ui/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  346. package/ui/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  347. package/ui/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  348. package/ui/dist/assets/MarketplacePage-DGrLtolR.js +1 -0
  349. package/ui/dist/assets/{ProjectDetail--gPfvATB.js → ProjectDetail-BgWvA6UP.js} +2 -2
  350. package/ui/dist/assets/ProjectIcon-TDuliqwg.js +1 -0
  351. package/ui/dist/assets/ProjectList-DXgM4187.js +1 -0
  352. package/ui/dist/assets/{Settings-1kaf7VHE.js → Settings-xA8Zhi3p.js} +3 -3
  353. package/ui/dist/assets/StatusIndicator-B8vfFAxg.js +1 -0
  354. package/ui/dist/assets/{SummaryDetail-DV3AmONk.js → SummaryDetail-eedRfTtF.js} +2 -2
  355. package/ui/dist/assets/SummaryList-CLNX2P84.js +1 -0
  356. package/ui/dist/assets/TaskDetail-BtWclNiR.js +1 -0
  357. package/ui/dist/assets/TaskList-JjODAgSc.js +1 -0
  358. package/ui/dist/assets/TeamsPage-CBK5iAyx.js +1 -0
  359. package/ui/dist/assets/TicketBoard-C9aJNNYp.js +1 -0
  360. package/ui/dist/assets/TicketDetail-CV5JmL31.js +1 -0
  361. package/ui/dist/assets/TicketList-68V4lzUz.js +1 -0
  362. package/ui/dist/assets/UserManagement-DKTb6UpE.js +2 -0
  363. package/ui/dist/assets/ViewSwitcher-BvdQht2Y.js +1 -0
  364. package/ui/dist/assets/alert-dialog-O6qBOv_f.js +1 -0
  365. package/ui/dist/assets/{archive-BsLa7brF.js → archive-CX7UvZpf.js} +1 -1
  366. package/ui/dist/assets/badge-BbfnwDJP.js +1 -0
  367. package/ui/dist/assets/{bug-CmZI7jqM.js → bug-3WX0394-.js} +1 -1
  368. package/ui/dist/assets/{calendar-Bf0t2B82.js → calendar-buq77TaS.js} +1 -1
  369. package/ui/dist/assets/{chevron-up-CoUsOvZj.js → chevron-up-4ymsBJRo.js} +1 -1
  370. package/ui/dist/assets/{circle-x-Dgw11LgT.js → circle-x-ByXtwR6Y.js} +1 -1
  371. package/ui/dist/assets/constants-CVdDh7p0.js +1 -0
  372. package/ui/dist/assets/{cpu-DiPMQICV.js → cpu-CiuoCeDY.js} +1 -1
  373. package/ui/dist/assets/{database-BwTfupDi.js → database-BWXSBg5a.js} +1 -1
  374. package/ui/dist/assets/dialog-lDg4NvMV.js +1 -0
  375. package/ui/dist/assets/dollar-sign-Dmijd6DP.js +1 -0
  376. package/ui/dist/assets/{ellipsis-UJx4Xnf6.js → ellipsis-vT-5SVIp.js} +1 -1
  377. package/ui/dist/assets/{ellipsis-vertical-DoULYEKn.js → ellipsis-vertical-B4tAX_bL.js} +1 -1
  378. package/ui/dist/assets/feature-markdown-DBo5Q7uq.js +289 -0
  379. package/ui/dist/assets/{file-code-B2UrJLwi.js → file-code-ChOWdKXf.js} +1 -1
  380. package/ui/dist/assets/{folder-open-BJi577JH.js → folder-open-CkOUbIiP.js} +1 -1
  381. package/ui/dist/assets/{funnel-PkaD_HSR.js → funnel-uZkoSPP-.js} +1 -1
  382. package/ui/dist/assets/{git-branch-C_K30am0.js → git-branch-CwvUxj7C.js} +1 -1
  383. package/ui/dist/assets/{git-commit-horizontal-IT6ecxWQ.js → git-commit-horizontal-lxPzCvhD.js} +1 -1
  384. package/ui/dist/assets/{globe-f-jjCa_g.js → globe-D_WpJdsj.js} +1 -1
  385. package/ui/dist/assets/{hash-DNEv8Elo.js → hash-CA0hJ7vh.js} +1 -1
  386. package/ui/dist/assets/{history-BQSBo_1Q.js → history-CEWMLELs.js} +1 -1
  387. package/ui/dist/assets/inbox-ehtOptTV.js +1 -0
  388. package/ui/dist/assets/{index-nS3EErDg.js → index-C1rhbml4.js} +3 -3
  389. package/ui/dist/assets/index-CR8gfCHv.js +1 -0
  390. package/ui/dist/assets/index-DVXMpWai.js +1 -0
  391. package/ui/dist/assets/index-DaDJMM2v.js +1 -0
  392. package/ui/dist/assets/index-Djdq_0xu.js +1 -0
  393. package/ui/dist/assets/index-M46Gcb8K.css +1 -0
  394. package/ui/dist/assets/index-sEBN7w8J.js +109 -0
  395. package/ui/dist/assets/layers-C6Mhf8Nk.js +1 -0
  396. package/ui/dist/assets/{layout-dashboard-CcKt1hXw.js → layout-dashboard-BRUM0kpT.js} +1 -1
  397. package/ui/dist/assets/{lightbulb-Jhp7FvbU.js → lightbulb-CN2q1O5l.js} +1 -1
  398. package/ui/dist/assets/{link-2-DdO7ostF.js → link-2-CC4E28yJ.js} +1 -1
  399. package/ui/dist/assets/markdown-renderer-C6ENbB0u.js +10 -0
  400. package/ui/dist/assets/markdown-renderer-CfVKi3_s.css +1 -0
  401. package/ui/dist/assets/{message-square-CBgH65X7.js → message-square-Bbb241am.js} +1 -1
  402. package/ui/dist/assets/package-CwT3IzFx.js +1 -0
  403. package/ui/dist/assets/{pencil-CYXCEPQh.js → pencil-CDmiHzoj.js} +1 -1
  404. package/ui/dist/assets/{play-d1ONaH0-.js → play-tjy5jMQX.js} +1 -1
  405. package/ui/dist/assets/popover-CFHZf3pE.js +1 -0
  406. package/ui/dist/assets/progress-BboUHuLf.js +1 -0
  407. package/ui/dist/assets/{rocket-DhvUetsk.js → rocket--8ZHgdXJ.js} +1 -1
  408. package/ui/dist/assets/select-n108rXny.js +1 -0
  409. package/ui/dist/assets/{send-P0r5D7td.js → send-Cu7MR2Qh.js} +1 -1
  410. package/ui/dist/assets/server-DmodxZp-.js +1 -0
  411. package/ui/dist/assets/{settings-2-ocj5uHGx.js → settings-2-CkRNqkxj.js} +1 -1
  412. package/ui/dist/assets/sheet-7aUJkyzU.js +1 -0
  413. package/ui/dist/assets/{shield-check-DfVdE34l.js → shield-check-ViI3nMKE.js} +1 -1
  414. package/ui/dist/assets/{shield-off-8l0J6R-8.js → shield-off-DN3tagkn.js} +1 -1
  415. package/ui/dist/assets/skeleton-C0E1Nfro.js +1 -0
  416. package/ui/dist/assets/{smartphone-77_g3SQn.js → smartphone-5FghiXpA.js} +1 -1
  417. package/ui/dist/assets/{square-Ckzsr3Rn.js → square-DiNSx8K7.js} +1 -1
  418. package/ui/dist/assets/star-DhdFVidS.js +1 -0
  419. package/ui/dist/assets/switch-CIgVbZqQ.js +1 -0
  420. package/ui/dist/assets/table-BC56FVMV.js +1 -0
  421. package/ui/dist/assets/{tag-C35mgoEv.js → tag-CH_vQliw.js} +1 -1
  422. package/ui/dist/assets/textarea-CxDiKFXZ.js +1 -0
  423. package/ui/dist/assets/{timer-DHJBS2w1.js → timer-CdGVTb60.js} +1 -1
  424. package/ui/dist/assets/tooltip-BuW9A5UO.js +1 -0
  425. package/ui/dist/assets/{trash-2-keO5tKPE.js → trash-2-CucZJL0N.js} +1 -1
  426. package/ui/dist/assets/trending-down-BCezC1z2.js +1 -0
  427. package/ui/dist/assets/{trending-up-BBv8KbrY.js → trending-up-C5OYX2k2.js} +1 -1
  428. package/ui/dist/assets/vendor-query-C0p7b53x.js +4 -0
  429. package/ui/dist/assets/vendor-react-Mc5_kfYG.js +12 -0
  430. package/ui/dist/assets/vendor-ui-CXPAUBFv.js +51 -0
  431. package/ui/dist/assets/{wand-sparkles-BusQ52ZK.js → wand-sparkles-BaH6pJnc.js} +1 -1
  432. package/ui/dist/assets/{wrench-02YaOSiK.js → wrench-CjxjP6ok.js} +1 -1
  433. package/ui/dist/index.html +5 -2
  434. package/ui/dist/assets/ActivityView-BeshZXmE.js +0 -1
  435. package/ui/dist/assets/AgentList-QVHeldPi.js +0 -1
  436. package/ui/dist/assets/AnalyticsDashboard-C9R97dXE.js +0 -1
  437. package/ui/dist/assets/AreaChart-B73gaiVQ.js +0 -1
  438. package/ui/dist/assets/CartesianChart-CXYL2R-A.js +0 -36
  439. package/ui/dist/assets/ChatView-qjZErlrp.js +0 -10
  440. package/ui/dist/assets/CostsDashboard-O4DCLy1i.js +0 -1
  441. package/ui/dist/assets/EstimateSelect-1hQf4Osk.js +0 -1
  442. package/ui/dist/assets/FloatingChatbot-BZBzrJxd.js +0 -2
  443. package/ui/dist/assets/InviteCodeManagement-LEoGqYeA.js +0 -2
  444. package/ui/dist/assets/ProjectIcon-Chd_ofsw.js +0 -1
  445. package/ui/dist/assets/ProjectList-BvUfVB6c.js +0 -4
  446. package/ui/dist/assets/StatusIndicator-pRfkNcdV.js +0 -1
  447. package/ui/dist/assets/SummaryList-B6kXiTxL.js +0 -1
  448. package/ui/dist/assets/TaskDetail-CV6CzbOS.js +0 -1
  449. package/ui/dist/assets/TaskList-DL7brQKd.js +0 -1
  450. package/ui/dist/assets/TicketBoard-CrkkFj7C.js +0 -1
  451. package/ui/dist/assets/TicketDetail-hnnYclpD.js +0 -1
  452. package/ui/dist/assets/TicketList-CsDAyQCL.js +0 -1
  453. package/ui/dist/assets/UserManagement-BGFQyTSr.js +0 -2
  454. package/ui/dist/assets/ViewSwitcher-CnL0OqA8.js +0 -1
  455. package/ui/dist/assets/alert-dialog-SKM6Gjz4.js +0 -7
  456. package/ui/dist/assets/badge-CVHH1a-v.js +0 -1
  457. package/ui/dist/assets/brain-6o_RHVpe.js +0 -1
  458. package/ui/dist/assets/constants-Q5Zivl-a.js +0 -1
  459. package/ui/dist/assets/dialog-BUqN9Psj.js +0 -1
  460. package/ui/dist/assets/index-BUmnwPQr.js +0 -1
  461. package/ui/dist/assets/index-BfF5G4Cw.js +0 -1
  462. package/ui/dist/assets/index-C8N375c2.js +0 -1
  463. package/ui/dist/assets/index-CuxnqVVz.css +0 -1
  464. package/ui/dist/assets/index-CzMM3QOk.js +0 -1
  465. package/ui/dist/assets/index-lJlNPFVJ.js +0 -67
  466. package/ui/dist/assets/layers-jFqVrWMD.js +0 -1
  467. package/ui/dist/assets/markdown-renderer-n1ToC4XQ.js +0 -36
  468. package/ui/dist/assets/popover-CrmZgoWu.js +0 -1
  469. package/ui/dist/assets/progress-uv4SPkL5.js +0 -1
  470. package/ui/dist/assets/select-CeQ8AHGy.js +0 -1
  471. package/ui/dist/assets/server-CVHTZB72.js +0 -1
  472. package/ui/dist/assets/sheet-DuQExSho.js +0 -1
  473. package/ui/dist/assets/skeleton-TFiFJ3nB.js +0 -1
  474. package/ui/dist/assets/switch-CAePkd6k.js +0 -1
  475. package/ui/dist/assets/table-ZjAZ7hkt.js +0 -1
  476. package/ui/dist/assets/textarea-DsVPnSgR.js +0 -1
  477. package/ui/dist/assets/tooltip-BYmQ331W.js +0 -1
@@ -6,9 +6,13 @@
6
6
  */
7
7
  import { Command } from 'commander';
8
8
  import chalk from 'chalk';
9
- import * as readline from 'node:readline';
10
9
  import { api } from '../utils/api.js';
11
10
  import { error, success, spinner, info } from '../utils/output.js';
11
+ import { getSessionDiffTracker } from '../../agents/session-diff.js';
12
+ import { getFileSnapshotManager } from '../../agents/file-snapshots.js';
13
+ import { getPromptSuggestionEngine } from '../../agents/prompt-suggestions.js';
14
+ import { getRateLimitMonitor } from '../../agents/rate-limit-monitor.js';
15
+ import { getErrorRecoveryAdvisor } from '../../agents/error-recovery.js';
12
16
  // === Helpers ===
13
17
  /**
14
18
  * Format tool call for display
@@ -119,161 +123,1265 @@ async function executeSingleShotWithTools(message, options) {
119
123
  }
120
124
  }
121
125
  /**
122
- * Start interactive REPL mode
126
+ * Launch the Ink-based TUI chat interface.
127
+ * Wired to the actual profClaw streaming API via SSE.
123
128
  */
124
- async function startREPL(options) {
125
- console.log('');
126
- console.log(chalk.cyan.bold('profClaw Chat'));
127
- console.log(chalk.dim('Interactive AI assistant with profClaw intelligence'));
128
- console.log(chalk.dim('Type /help for commands, /exit to quit'));
129
- console.log('');
130
- // Create or resume conversation
131
- let conversationId = options.session;
132
- if (!conversationId) {
133
- const spin = spinner('Starting session...').start();
134
- const result = await api.post('/api/chat/conversations', {
135
- presetId: options.agentic ? 'agentic' : 'profclaw-assistant',
136
- mode: options.agentic ? 'agentic' : 'chat',
137
- });
138
- spin.stop();
139
- if (!result.ok) {
140
- error(result.error || 'Failed to create conversation');
141
- process.exit(1);
129
+ async function startTUI(options) {
130
+ const { render } = await import('ink');
131
+ const React = (await import('react')).default;
132
+ const { ChatApp } = await import('../ink/ChatApp.js');
133
+ const { streamChat, createConversation: createConv } = await import('../interactive/stream-client.js');
134
+ const { getConfig } = await import('../utils/config.js');
135
+ const { detectBaseUrl } = await import('../utils/api.js');
136
+ const { getOfflineDetector } = await import('../../utils/offline-detect.js');
137
+ const config = getConfig();
138
+ // Auto-detect the running profClaw server via shared utility
139
+ const resolvedUrl = await detectBaseUrl();
140
+ const serverConfig = {
141
+ baseUrl: resolvedUrl,
142
+ apiToken: config.apiToken,
143
+ };
144
+ let availableProviders = [];
145
+ let availableModels = [];
146
+ let detectedModel = options.model ?? 'auto';
147
+ let detectedProvider = 'auto';
148
+ try {
149
+ const headers = { 'Accept': 'application/json' };
150
+ if (serverConfig.apiToken)
151
+ headers['Authorization'] = `Bearer ${serverConfig.apiToken}`;
152
+ const [provRes, modRes] = await Promise.allSettled([
153
+ fetch(`${serverConfig.baseUrl}/api/chat/providers`, { headers }).then(r => r.ok ? r.json() : null),
154
+ fetch(`${serverConfig.baseUrl}/api/chat/models`, { headers }).then(r => r.ok ? r.json() : null),
155
+ ]);
156
+ // Build providers list from API
157
+ if (provRes.status === 'fulfilled' && provRes.value?.providers) {
158
+ const provData = provRes.value;
159
+ const healthy = provData.providers.filter(p => p.enabled && p.healthy);
160
+ // Auto-detect best provider (prefer local first)
161
+ if (!options.model) {
162
+ const preferred = ['ollama', 'anthropic', 'openai', 'azure', 'google', 'cerebras'];
163
+ const best = preferred.find(p => healthy.some(h => h.type === p)) ?? healthy[0]?.type ?? provData.default;
164
+ detectedProvider = best ?? 'auto';
165
+ const modelMap = {
166
+ ollama: 'llama3.2',
167
+ anthropic: 'claude-sonnet',
168
+ openai: 'gpt-4o',
169
+ azure: 'gpt-4o',
170
+ google: 'gemini-2.0-flash',
171
+ cerebras: 'llama-3.3-70b',
172
+ };
173
+ detectedModel = modelMap[detectedProvider] ?? 'auto';
174
+ }
175
+ // Show all providers: configured ones at top, unconfigured greyed out
176
+ const configured = provData.providers.filter(p => p.enabled && p.healthy);
177
+ const unconfigured = provData.providers.filter(p => !p.enabled || !p.healthy);
178
+ availableProviders = [
179
+ ...configured.map(p => ({
180
+ label: p.type,
181
+ value: p.type,
182
+ description: `${p.message ?? 'configured'}${p.latencyMs ? ` · ${p.latencyMs}ms` : ''}`,
183
+ active: p.type === detectedProvider,
184
+ })),
185
+ ...unconfigured.map(p => ({
186
+ label: `${p.type} (not configured)`,
187
+ value: p.type,
188
+ description: p.healthy ? 'no API key' : (p.message ?? 'offline'),
189
+ active: false,
190
+ disabled: true,
191
+ })),
192
+ ];
193
+ }
194
+ // Build models list from API
195
+ if (modRes.status === 'fulfilled' && modRes.value?.models) {
196
+ // Only show models from configured (non-disabled) providers
197
+ const configuredProviders = new Set(availableProviders.filter(p => !p.disabled).map(p => p.value));
198
+ const seen = new Set();
199
+ availableModels = modRes.value.models
200
+ .filter(m => {
201
+ if (seen.has(m.id))
202
+ return false;
203
+ seen.add(m.id);
204
+ return configuredProviders.size === 0 || configuredProviders.has(m.provider);
205
+ })
206
+ .map(m => {
207
+ const costIn = m.costPer1MInput ?? 0;
208
+ const costOut = m.costPer1MOutput ?? 0;
209
+ const cost = costIn === 0 && costOut === 0 ? 'free' : `$${costIn}/$${costOut} per 1M`;
210
+ return {
211
+ label: m.name || m.id,
212
+ value: m.id,
213
+ description: `${m.provider} · ${cost}`,
214
+ active: m.id === detectedModel,
215
+ };
216
+ });
142
217
  }
143
- conversationId = result.data.conversation.id;
144
- info(`Session: ${conversationId.slice(0, 8)}... (${options.agentic ? 'agentic' : 'chat'} mode)`);
145
- }
146
- else {
147
- info(`Resuming session: ${conversationId.slice(0, 8)}...`);
148
218
  }
149
- const rl = readline.createInterface({
150
- input: process.stdin,
151
- output: process.stdout,
152
- prompt: chalk.green('> '),
153
- });
154
- const showHelp = () => {
155
- console.log('');
156
- console.log(chalk.yellow('Commands:'));
157
- console.log(' /exit, /quit, /q Exit chat');
158
- console.log(' /clear Clear screen');
159
- console.log(' /help, /? Show this help');
160
- console.log(' /session Show session ID');
161
- console.log(' /model <name> Change model');
162
- console.log(' /tools Toggle tool calling');
163
- console.log(' /agentic Toggle agentic mode');
164
- console.log('');
165
- };
166
- let useTools = options.tools ?? false;
219
+ catch { /* fall back to empty lists */ }
220
+ const sessionId = options.session ?? `tui-${Date.now().toString(36).slice(-4)}`;
221
+ // Mutable TUI state — mutated in place, then rerender() flushes to Ink
222
+ let currentModel = detectedModel;
223
+ let currentProvider = detectedProvider;
167
224
  let agenticMode = options.agentic ?? false;
168
- let model = options.model;
169
- const sendMessage = async (content) => {
170
- const spin = spinner('Thinking...').start();
225
+ let showThinking = false;
226
+ let showTools = true;
227
+ let effort = 'medium';
228
+ let tokensUsed = 0;
229
+ let estimatedCost = 0;
230
+ const tokensMax = 100_000;
231
+ const sessionStartTime = Date.now();
232
+ let agentStatus = 'idle';
233
+ let agentAction;
234
+ let stepCount = 0;
235
+ let elapsedMs = 0;
236
+ let streamingContent;
237
+ let conversationId;
238
+ let lastUserMessage = '';
239
+ let lastAssistantContent = '';
240
+ let currentSuggestions = [];
241
+ let rerenderFn = null;
242
+ let activeAbort = null;
243
+ // ── Connection / offline state ───────────────────────────────────────────────
244
+ let connectionStatus = 'connected';
245
+ let connectionLatencyMs;
246
+ // Start the offline detector
247
+ const offlineDetector = getOfflineDetector();
248
+ offlineDetector.start(resolvedUrl);
249
+ offlineDetector.onStatusChange((online) => {
250
+ connectionStatus = online ? 'connected' : 'disconnected';
251
+ if (online) {
252
+ // Replay any queued messages when server comes back
253
+ const queued = offlineDetector.drainQueue();
254
+ for (const cmd of queued) {
255
+ const payload = cmd.payload;
256
+ if (payload.message) {
257
+ void handleSubmit(payload.message);
258
+ }
259
+ }
260
+ }
261
+ rerender();
262
+ });
263
+ const messages = [];
264
+ function rerender() {
265
+ rerenderFn?.();
266
+ }
267
+ function pushInfo(content) {
268
+ messages.push({ role: 'assistant', content, timestamp: new Date() });
269
+ rerender();
270
+ }
271
+ /** Fetch recent sessions from server */
272
+ async function fetchSessions(limit = 15) {
171
273
  try {
172
- const endpoint = useTools || agenticMode
173
- ? `/api/chat/conversations/${conversationId}/messages/with-tools`
174
- : `/api/chat/conversations/${conversationId}/messages`;
175
- const result = await api.post(endpoint, {
176
- content,
177
- model,
178
- enableTools: useTools || agenticMode,
274
+ const headers = { 'Accept': 'application/json' };
275
+ if (serverConfig.apiToken)
276
+ headers['Authorization'] = `Bearer ${serverConfig.apiToken}`;
277
+ const res = await fetch(`${serverConfig.baseUrl}/api/chat/conversations/recent?limit=${limit}`, { headers });
278
+ if (!res.ok)
279
+ return [];
280
+ const data = await res.json();
281
+ return data.conversations ?? [];
282
+ }
283
+ catch {
284
+ return [];
285
+ }
286
+ }
287
+ /** Auto-title a conversation from the first message */
288
+ async function autoTitleConversation(convId, firstMsg) {
289
+ const title = firstMsg.length <= 50 ? firstMsg : firstMsg.slice(0, 47).replace(/\s+\S*$/, '') + '...';
290
+ try {
291
+ const headers = { 'Content-Type': 'application/json' };
292
+ if (serverConfig.apiToken)
293
+ headers['Authorization'] = `Bearer ${serverConfig.apiToken}`;
294
+ await fetch(`${serverConfig.baseUrl}/api/chat/conversations/${convId}`, {
295
+ method: 'PATCH',
296
+ headers,
297
+ body: JSON.stringify({ title }),
179
298
  });
180
- spin.stop();
181
- if (!result.ok) {
182
- error(result.error || 'Failed to send message');
183
- return;
299
+ }
300
+ catch { /* fire-and-forget */ }
301
+ }
302
+ /** Ensure a conversation exists, create one if not */
303
+ async function ensureConversation() {
304
+ if (conversationId)
305
+ return conversationId;
306
+ const result = await createConv(serverConfig, {
307
+ mode: agenticMode ? 'agentic' : 'chat',
308
+ presetId: agenticMode ? 'agentic' : 'profclaw-assistant',
309
+ });
310
+ if ('error' in result) {
311
+ pushInfo(`**Error:** ${result.error}`);
312
+ return null;
313
+ }
314
+ conversationId = result.conversationId;
315
+ return conversationId;
316
+ }
317
+ /** Handle slash commands — returns true if handled */
318
+ async function handleSlashCommand(cmd) {
319
+ const parts = cmd.slice(1).trim().split(/\s+/);
320
+ const command = parts[0]?.toLowerCase();
321
+ const args = parts.slice(1);
322
+ switch (command) {
323
+ case 'model':
324
+ case 'm': {
325
+ if (args[0]) {
326
+ currentModel = args[0];
327
+ // Update active flag in picker list
328
+ availableModels = availableModels.map(m => ({ ...m, active: m.value === currentModel }));
329
+ pushInfo(`Model switched to **${currentModel}**`);
330
+ }
331
+ else {
332
+ pushInfo(`Current model: **${currentModel}** via ${currentProvider}\n\nUsage: \`/model <name>\` (e.g. \`/model gpt-4o\`, \`/model claude-opus\`, \`/model llama3.2\`)`);
333
+ }
334
+ return true;
184
335
  }
185
- const data = result.data;
186
- // Print tool calls if any
187
- if (data.toolCalls && data.toolCalls.length > 0) {
188
- console.log('');
189
- console.log(chalk.yellow('Tool Calls:'));
190
- for (const tc of data.toolCalls) {
191
- console.log(formatToolCall(tc));
336
+ case 'provider':
337
+ case 'p': {
338
+ if (args[0]) {
339
+ currentProvider = args[0];
340
+ availableProviders = availableProviders.map(p => ({ ...p, active: p.value === currentProvider }));
341
+ pushInfo(`Provider switched to **${currentProvider}**`);
342
+ }
343
+ else {
344
+ pushInfo(`Current provider: **${currentProvider}**\n\nUsage: \`/provider <name>\` (e.g. \`/provider ollama\`, \`/provider anthropic\`)`);
192
345
  }
346
+ return true;
193
347
  }
194
- // Print response
195
- console.log('');
196
- console.log(chalk.cyan('profClaw') + (data.assistantMessage.model ? chalk.dim(` (${data.assistantMessage.model})`) : '') + ':');
197
- console.log(data.assistantMessage.content || '(no response)');
198
- if (data.usage) {
199
- console.log('');
200
- console.log(formatUsage(data.usage));
348
+ case 'agentic':
349
+ case 'agent': {
350
+ agenticMode = !agenticMode;
351
+ pushInfo(`Agentic mode: **${agenticMode ? 'ON' : 'OFF'}**${agenticMode ? '\nAgent has access to web search, file ops, code execution, and more.' : ''}`);
352
+ return true;
201
353
  }
202
- console.log('');
354
+ case 'effort': {
355
+ const level = args[0]?.toLowerCase();
356
+ if (level === 'low' || level === 'medium' || level === 'high') {
357
+ effort = level;
358
+ pushInfo(`Effort level set to **${effort}**`);
359
+ }
360
+ else {
361
+ pushInfo(`Current effort: **${effort}**\n\nUsage: \`/effort <low|medium|high>\``);
362
+ }
363
+ return true;
364
+ }
365
+ case 'thinking': {
366
+ showThinking = !showThinking;
367
+ pushInfo(`Thinking display: **${showThinking ? 'ON' : 'OFF'}**`);
368
+ return true;
369
+ }
370
+ case 'tools': {
371
+ showTools = !showTools;
372
+ pushInfo(`Tool display: **${showTools ? 'verbose' : 'minimal'}**`);
373
+ return true;
374
+ }
375
+ case 'new': {
376
+ conversationId = undefined;
377
+ messages.length = 0;
378
+ tokensUsed = 0;
379
+ estimatedCost = 0;
380
+ stepCount = 0;
381
+ lastUserMessage = '';
382
+ lastAssistantContent = '';
383
+ pushInfo('Started new conversation.');
384
+ return true;
385
+ }
386
+ case 'clear': {
387
+ messages.length = 0;
388
+ pushInfo('Display cleared. Conversation history is preserved on the server.');
389
+ return true;
390
+ }
391
+ case 'sessions':
392
+ case 'ls': {
393
+ agentStatus = 'thinking';
394
+ agentAction = 'Fetching sessions...';
395
+ rerender();
396
+ const sessions = await fetchSessions(20);
397
+ agentStatus = 'idle';
398
+ agentAction = undefined;
399
+ if (sessions.length === 0) {
400
+ pushInfo('No sessions found.');
401
+ }
402
+ else {
403
+ const list = sessions.map(s => {
404
+ const created = new Date(s.createdAt).toLocaleDateString();
405
+ const title = s.title || '(untitled)';
406
+ const modeIcon = s.mode === 'agentic' ? '🤖' : '💬';
407
+ return `${modeIcon} \`${s.id.slice(0, 8)}\` **${title}** — ${created}`;
408
+ }).join('\n');
409
+ pushInfo(`**Recent Sessions:**\n\n${list}\n\nResume with: \`/resume <id>\``);
410
+ }
411
+ return true;
412
+ }
413
+ case 'resume':
414
+ case 'switch': {
415
+ if (!args[0]) {
416
+ // No arg: show list and prompt
417
+ const sessions = await fetchSessions(20);
418
+ if (sessions.length === 0) {
419
+ pushInfo('No sessions to resume.');
420
+ return true;
421
+ }
422
+ const list = sessions.map(s => {
423
+ const created = new Date(s.createdAt).toLocaleDateString();
424
+ return `\`${s.id.slice(0, 8)}\` ${s.title || '(untitled)'} — ${created}`;
425
+ }).join('\n');
426
+ pushInfo(`**Sessions:**\n\n${list}\n\nUsage: \`/resume <id-prefix>\``);
427
+ return true;
428
+ }
429
+ const sessions = await fetchSessions(50);
430
+ const match = sessions.find(s => s.id.startsWith(args[0]));
431
+ if (!match) {
432
+ pushInfo(`No session found matching \`${args[0]}\`.`);
433
+ return true;
434
+ }
435
+ conversationId = match.id;
436
+ messages.length = 0;
437
+ tokensUsed = 0;
438
+ estimatedCost = 0;
439
+ stepCount = 0;
440
+ pushInfo(`Resumed session \`${match.id.slice(0, 8)}\`: **${match.title || '(untitled)'}**`);
441
+ return true;
442
+ }
443
+ case 'status': {
444
+ agentStatus = 'thinking';
445
+ agentAction = 'Checking server...';
446
+ rerender();
447
+ try {
448
+ const headers = { 'Accept': 'application/json' };
449
+ if (serverConfig.apiToken)
450
+ headers['Authorization'] = `Bearer ${serverConfig.apiToken}`;
451
+ const res = await fetch(`${serverConfig.baseUrl}/api/chat/providers`, { headers });
452
+ agentStatus = 'idle';
453
+ agentAction = undefined;
454
+ if (!res.ok) {
455
+ pushInfo(`**Server:** ${serverConfig.baseUrl} — offline (HTTP ${res.status})`);
456
+ }
457
+ else {
458
+ const data = await res.json();
459
+ const lines = (data.providers ?? []).map(p => {
460
+ const status = p.healthy ? '🟢' : p.enabled ? '🟡' : '⚫';
461
+ const latency = p.latencyMs ? ` · ${p.latencyMs}ms` : '';
462
+ return `${status} **${p.type}** ${p.message ?? ''}${latency}`;
463
+ });
464
+ pushInfo(`**Server:** ${serverConfig.baseUrl}\n\n${lines.join('\n')}`);
465
+ }
466
+ }
467
+ catch (err) {
468
+ agentStatus = 'idle';
469
+ agentAction = undefined;
470
+ pushInfo(`**Server:** ${serverConfig.baseUrl} — unreachable\n${err instanceof Error ? err.message : ''}`);
471
+ }
472
+ return true;
473
+ }
474
+ case 'run':
475
+ case 'exec': {
476
+ if (args.length === 0) {
477
+ pushInfo('Usage: `/run <command>`');
478
+ return true;
479
+ }
480
+ const cmdStr = args.join(' ');
481
+ agentStatus = 'executing';
482
+ agentAction = `$ ${cmdStr}`;
483
+ rerender();
484
+ try {
485
+ const { exec } = await import('node:child_process');
486
+ const output = await new Promise((resolve, reject) => {
487
+ exec(cmdStr, { encoding: 'utf-8', timeout: 30_000, maxBuffer: 500_000 }, (err, stdout, stderr) => {
488
+ if (err) {
489
+ const detail = (stderr?.trim() || err.message || 'Command failed');
490
+ reject(Object.assign(err, { detail }));
491
+ }
492
+ else {
493
+ resolve(stdout + (stderr ? `\nSTDERR:\n${stderr}` : ''));
494
+ }
495
+ });
496
+ });
497
+ agentStatus = 'idle';
498
+ agentAction = undefined;
499
+ pushInfo(`**$ ${cmdStr}**\n\`\`\`\n${output.trimEnd()}\n\`\`\``);
500
+ }
501
+ catch (err) {
502
+ agentStatus = 'idle';
503
+ agentAction = undefined;
504
+ const e = err;
505
+ pushInfo(`**$ ${cmdStr}** — failed\n\`\`\`\n${e.detail ?? e.message ?? 'Command failed'}\n\`\`\``);
506
+ }
507
+ return true;
508
+ }
509
+ case 'retry': {
510
+ if (!lastUserMessage) {
511
+ pushInfo('No previous message to retry.');
512
+ return true;
513
+ }
514
+ // Optionally use a different model for retry
515
+ if (args[0]) {
516
+ currentModel = args[0];
517
+ availableModels = availableModels.map(m => ({ ...m, active: m.value === currentModel }));
518
+ }
519
+ // Re-submit the last user message
520
+ await handleSubmit(lastUserMessage, true);
521
+ return true;
522
+ }
523
+ case 'diff': {
524
+ agentStatus = 'thinking';
525
+ agentAction = 'Generating diff...';
526
+ rerender();
527
+ const diffTracker = getSessionDiffTracker();
528
+ const changedFiles = diffTracker.getChangedFiles();
529
+ if (changedFiles.length === 0) {
530
+ agentStatus = 'idle';
531
+ agentAction = undefined;
532
+ pushInfo('No file changes in this session.');
533
+ return true;
534
+ }
535
+ const diffOutput = await diffTracker.generateDiff();
536
+ agentStatus = 'idle';
537
+ agentAction = undefined;
538
+ const summary = changedFiles
539
+ .map(f => `- \`${f.path}\` (${f.status})`)
540
+ .join('\n');
541
+ pushInfo(`**Session diff** — ${changedFiles.length} file(s) changed:\n\n${summary}\n\n\`\`\`diff\n${diffOutput.trimEnd()}\n\`\`\``);
542
+ return true;
543
+ }
544
+ case 'rewind': {
545
+ const snapshotManager = getFileSnapshotManager();
546
+ // /rewind --turn <n>
547
+ const turnFlagIdx = args.indexOf('--turn');
548
+ if (turnFlagIdx !== -1) {
549
+ const turnArg = args[turnFlagIdx + 1];
550
+ const turnIndex = parseInt(turnArg ?? '', 10);
551
+ if (isNaN(turnIndex)) {
552
+ pushInfo('Usage: `/rewind --turn <n>` where n is a turn number.');
553
+ return true;
554
+ }
555
+ agentStatus = 'thinking';
556
+ agentAction = `Rewinding turn ${turnIndex}...`;
557
+ rerender();
558
+ const results = await snapshotManager.rewindTurn(turnIndex);
559
+ agentStatus = 'idle';
560
+ agentAction = undefined;
561
+ if (results.length === 0) {
562
+ pushInfo(`No files were modified during turn ${turnIndex}.`);
563
+ return true;
564
+ }
565
+ const lines = results.map(r => `- \`${r.path}\` — ${r.restored ? 'restored' : 'no snapshot found'}`);
566
+ pushInfo(`**Rewind turn ${turnIndex}** — ${results.filter(r => r.restored).length}/${results.length} files restored:\n\n${lines.join('\n')}`);
567
+ return true;
568
+ }
569
+ // /rewind <path>
570
+ if (args[0] && !args[0].startsWith('--')) {
571
+ const filePath = args[0];
572
+ agentStatus = 'thinking';
573
+ agentAction = `Rewinding ${filePath}...`;
574
+ rerender();
575
+ const result = await snapshotManager.rewind(filePath);
576
+ agentStatus = 'idle';
577
+ agentAction = undefined;
578
+ if (!result.restored) {
579
+ pushInfo(`No snapshot found for \`${filePath}\`. The file may not have been modified this session.`);
580
+ }
581
+ else {
582
+ pushInfo(`Rewound \`${result.path}\` to snapshot from turn ${result.turnIndex}.`);
583
+ }
584
+ return true;
585
+ }
586
+ // /rewind (no args) — list modified files
587
+ const modifiedFiles = snapshotManager.getModifiedFiles();
588
+ if (modifiedFiles.length === 0) {
589
+ pushInfo('No file snapshots in this session.');
590
+ return true;
591
+ }
592
+ const fileList = modifiedFiles
593
+ .map(f => {
594
+ const age = new Date(f.lastModified).toLocaleTimeString();
595
+ return `- \`${f.path}\` — ${f.snapshotCount} snapshot(s), last at ${age}`;
596
+ })
597
+ .join('\n');
598
+ pushInfo(`**Snapshots this session** (${modifiedFiles.length} file(s)):\n\n${fileList}\n\nUse \`/rewind <path>\` to restore a file, or \`/rewind --turn <n>\` to rewind all changes from a turn.`);
599
+ return true;
600
+ }
601
+ case 'compact': {
602
+ const useLLM = args.includes('--llm');
603
+ const { ContextCompactor } = await import('../../agents/context-compactor.js');
604
+ const compactor = new ContextCompactor({
605
+ maxContextTokens: tokensMax,
606
+ compactionThreshold: Math.floor(tokensMax * 0.7),
607
+ preserveRecentTurns: 5,
608
+ summaryMaxTokens: 2_000,
609
+ });
610
+ // Build a ModelMessage[] from the display messages for token estimation
611
+ const modelMessages = messages.map((m) => ({
612
+ role: m.role,
613
+ content: m.content,
614
+ }));
615
+ const currentTokens = compactor.estimateTokens(modelMessages);
616
+ const threshold = Math.floor(tokensMax * 0.7);
617
+ if (currentTokens < threshold && !useLLM) {
618
+ pushInfo(`No compaction needed (${currentTokens.toLocaleString()}/${tokensMax.toLocaleString()} tokens — threshold: ${threshold.toLocaleString()})`);
619
+ return true;
620
+ }
621
+ agentStatus = 'thinking';
622
+ agentAction = useLLM ? 'Compacting with LLM...' : 'Compacting context...';
623
+ rerender();
624
+ try {
625
+ let result;
626
+ if (useLLM) {
627
+ // Build an apiCall using the current server/model config
628
+ const apiCall = async (prompt) => {
629
+ const headers = {
630
+ 'Content-Type': 'application/json',
631
+ };
632
+ if (serverConfig.apiToken) {
633
+ headers['Authorization'] = `Bearer ${serverConfig.apiToken}`;
634
+ }
635
+ const res = await fetch(`${serverConfig.baseUrl}/api/chat/completions`, {
636
+ method: 'POST',
637
+ headers,
638
+ body: JSON.stringify({
639
+ messages: [{ role: 'user', content: prompt }],
640
+ model: currentModel,
641
+ provider: currentProvider,
642
+ stream: false,
643
+ }),
644
+ });
645
+ if (!res.ok)
646
+ throw new Error(`HTTP ${res.status}`);
647
+ const data = await res.json();
648
+ return data.content ?? data.message?.content ?? '';
649
+ };
650
+ result = await compactor.compactWithLLM(modelMessages, apiCall);
651
+ }
652
+ else {
653
+ result = await compactor.compact(modelMessages);
654
+ }
655
+ agentStatus = 'idle';
656
+ agentAction = undefined;
657
+ if (!result.compacted) {
658
+ pushInfo(`No compaction needed (${currentTokens.toLocaleString()}/${tokensMax.toLocaleString()} tokens)`);
659
+ return true;
660
+ }
661
+ // Replace display messages with compacted set (convert back to display format)
662
+ messages.length = 0;
663
+ for (const m of result.messages) {
664
+ if (m.role === 'system') {
665
+ const content = typeof m.content === 'string' ? m.content : JSON.stringify(m.content);
666
+ messages.push({ role: 'assistant', content, timestamp: new Date() });
667
+ }
668
+ else if (m.role === 'user' || m.role === 'assistant') {
669
+ const content = typeof m.content === 'string' ? m.content : JSON.stringify(m.content);
670
+ messages.push({ role: m.role, content, timestamp: new Date() });
671
+ }
672
+ }
673
+ // Update token counter
674
+ tokensUsed = result.compactedTokens;
675
+ pushInfo(`**Context compacted** (${useLLM ? 'LLM' : 'local'} mode)\n` +
676
+ `${result.originalTokens.toLocaleString()} → ${result.compactedTokens.toLocaleString()} tokens · ${result.turnsCompacted} turn(s) summarized`);
677
+ }
678
+ catch (err) {
679
+ agentStatus = 'idle';
680
+ agentAction = undefined;
681
+ pushInfo(`**Compaction failed:** ${err instanceof Error ? err.message : 'Unknown error'}`);
682
+ }
683
+ return true;
684
+ }
685
+ case 'copy': {
686
+ const lastAssistant = [...messages].reverse().find(m => m.role === 'assistant');
687
+ if (!lastAssistant) {
688
+ pushInfo('No assistant message to copy.');
689
+ return true;
690
+ }
691
+ try {
692
+ const { spawn } = await import('node:child_process');
693
+ const clipCmd = process.platform === 'darwin' ? 'pbcopy' :
694
+ process.platform === 'win32' ? 'clip' :
695
+ 'xclip';
696
+ const clipArgs = process.platform === 'linux' ? ['-selection', 'clipboard'] : [];
697
+ const proc = spawn(clipCmd, clipArgs, { stdio: ['pipe', 'ignore', 'ignore'] });
698
+ proc.stdin.write(lastAssistant.content, 'utf-8');
699
+ proc.stdin.end();
700
+ await new Promise((resolve) => proc.on('close', () => resolve()));
701
+ pushInfo('Copied last response to clipboard.');
702
+ }
703
+ catch (err) {
704
+ pushInfo(`**Copy failed:** ${err instanceof Error ? err.message : 'Unknown error'}`);
705
+ }
706
+ return true;
707
+ }
708
+ case 'save': {
709
+ const savePath = args.join(' ').trim();
710
+ if (!savePath) {
711
+ pushInfo('Usage: `/save <filepath>`');
712
+ return true;
713
+ }
714
+ const lastAssistantMsg = [...messages].reverse().find(m => m.role === 'assistant');
715
+ if (!lastAssistantMsg) {
716
+ pushInfo('No assistant message to save.');
717
+ return true;
718
+ }
719
+ try {
720
+ const { writeFile } = await import('node:fs/promises');
721
+ await writeFile(savePath, lastAssistantMsg.content, 'utf-8');
722
+ pushInfo(`Saved to \`${savePath}\`.`);
723
+ }
724
+ catch (err) {
725
+ pushInfo(`**Save failed:** ${err instanceof Error ? err.message : 'Unknown error'}`);
726
+ }
727
+ return true;
728
+ }
729
+ case 'whoami': {
730
+ const msgCount = messages.length;
731
+ const tokenStr = tokensUsed >= 1000
732
+ ? `${(tokensUsed / 1000).toFixed(1)}K`
733
+ : String(tokensUsed);
734
+ const costStr = estimatedCost > 0
735
+ ? `$${estimatedCost.toFixed(4)}`
736
+ : '$0.000';
737
+ pushInfo([
738
+ '**Session Info**',
739
+ '',
740
+ ` **Model:** ${currentModel} via ${currentProvider}`,
741
+ ` **Mode:** ${agenticMode ? 'agentic (tools on)' : 'chat (agentic: off)'}`,
742
+ ` **Effort:** ${effort}`,
743
+ ` **Thinking:** ${showThinking ? 'visible' : 'hidden'}`,
744
+ ` **Tools:** ${showTools ? 'verbose' : 'minimal'}`,
745
+ ` **Server:** ${serverConfig.baseUrl}`,
746
+ ` **Session:** ${sessionId} (${msgCount} message${msgCount !== 1 ? 's' : ''}, ${tokenStr} tokens, ${costStr})`,
747
+ ].join('\n'));
748
+ return true;
749
+ }
750
+ case 'help':
751
+ case 'h':
752
+ case '?': {
753
+ pushInfo([
754
+ '**Slash Commands**',
755
+ '',
756
+ '**Chat**',
757
+ ' `/model <name>` — Switch AI model (e.g. gpt-4o, claude-opus, llama3.2)',
758
+ ' `/provider <name>` — Switch provider (anthropic, openai, ollama, etc.)',
759
+ ' `/agentic` — Toggle agentic mode (tools + multi-step reasoning)',
760
+ ' `/effort <low|medium|high>` — Set reasoning effort level',
761
+ ' `/thinking` — Toggle thinking display',
762
+ ' `/tools` — Toggle tool call verbosity',
763
+ '',
764
+ '**Session**',
765
+ ' `/new` — Start a fresh conversation',
766
+ ' `/sessions` — List recent conversations',
767
+ ' `/resume <id>` — Switch to a previous session',
768
+ ' `/checkpoints` — List agent execution checkpoints (auto-saved every 5 steps)',
769
+ ' `/clear` — Clear display (history preserved on server)',
770
+ '',
771
+ '**File Changes**',
772
+ ' `/diff` — Show unified diff of all file changes this session',
773
+ ' `/rewind` — List files with snapshots this session',
774
+ ' `/rewind <path>` — Restore a file to its last snapshot',
775
+ ' `/rewind --turn <n>` — Rewind all changes from turn N',
776
+ '',
777
+ '**Utilities**',
778
+ ' `/compact` — Compact context (summarize old turns)',
779
+ ' `/compact --llm` — Compact using LLM for richer summary',
780
+ ' `/status` — Server and provider health',
781
+ ' `/insights` — Session and server usage analytics',
782
+ ' `/run <cmd>` — Execute a shell command',
783
+ ' `/copy` — Copy last assistant message to clipboard',
784
+ ' `/save <path>` — Save last assistant message to a file',
785
+ ' `/whoami` — Show current model, mode, and session info',
786
+ ' `/retry [model]` — Retry last message (optionally with different model)',
787
+ ' `/help` — Show this help',
788
+ ' `/exit` — Quit',
789
+ ].join('\n'));
790
+ return true;
791
+ }
792
+ case 'checkpoints':
793
+ case 'cp': {
794
+ const { getCheckpointManager } = await import('../../agents/checkpoint-manager.js');
795
+ const cpManager = getCheckpointManager();
796
+ agentStatus = 'thinking';
797
+ agentAction = 'Loading checkpoints...';
798
+ rerender();
799
+ const list = await cpManager.list();
800
+ agentStatus = 'idle';
801
+ agentAction = undefined;
802
+ if (list.length === 0) {
803
+ pushInfo('No agent checkpoints found.\n\nCheckpoints are saved automatically every 5 steps during agentic execution.\nResume a checkpoint with: `profclaw chat --resume-checkpoint <sessionId>`');
804
+ return true;
805
+ }
806
+ const rows = list.map((cp) => {
807
+ const age = new Date(cp.updatedAt).toLocaleString();
808
+ const task = cp.taskDescription
809
+ ? cp.taskDescription.slice(0, 60) + (cp.taskDescription.length > 60 ? '...' : '')
810
+ : '(no description)';
811
+ return `\`${cp.sessionId.slice(0, 12)}\` step **${cp.step}** — ${task} — _${age}_`;
812
+ }).join('\n');
813
+ pushInfo(`**Agent Checkpoints** (${list.length}):\n\n${rows}\n\n` +
814
+ `Resume with: \`profclaw chat --resume-checkpoint <sessionId>\``);
815
+ return true;
816
+ }
817
+ case 'insights':
818
+ case 'stats': {
819
+ const sessionDuration = Date.now() - sessionStartTime;
820
+ const toolCallCount = messages.filter((m) => m.content.includes('⚙')).length;
821
+ const tokenStr = tokensUsed >= 1_000
822
+ ? `${(tokensUsed / 1000).toFixed(1)}K`
823
+ : String(tokensUsed);
824
+ const costStr = `$${(estimatedCost).toFixed(4)}`;
825
+ const durationStr = sessionDuration < 60_000
826
+ ? `${Math.floor(sessionDuration / 1000)}s`
827
+ : `${Math.floor(sessionDuration / 60_000)}m ${Math.floor((sessionDuration % 60_000) / 1000)}s`;
828
+ agentStatus = 'thinking';
829
+ agentAction = 'Fetching server stats...';
830
+ rerender();
831
+ const serverStatsRes = await api.get('/api/stats').catch(() => null);
832
+ const serverStats = serverStatsRes?.data;
833
+ agentStatus = 'idle';
834
+ agentAction = undefined;
835
+ pushInfo([
836
+ '**Session Insights**',
837
+ '',
838
+ ` Messages: ${messages.length}`,
839
+ ` Tokens used: ${tokenStr}`,
840
+ ` Est. cost: ${costStr}`,
841
+ ` Duration: ${durationStr}`,
842
+ ` Tool calls: ${toolCallCount}`,
843
+ ` Model: ${currentModel} via ${currentProvider}`,
844
+ ` Mode: ${agenticMode ? 'agentic' : 'chat'}`,
845
+ '',
846
+ '**Server Stats**',
847
+ '',
848
+ ` Total tasks: ${serverStats?.totalTasks ?? 'N/A'}`,
849
+ ` Active sessions:${serverStats?.activeSessions ?? 'N/A'}`,
850
+ ` Uptime: ${serverStats?.uptime ?? 'N/A'}`,
851
+ ].join('\n'));
852
+ return true;
853
+ }
854
+ default:
855
+ return false;
203
856
  }
204
- catch (err) {
205
- spin.stop();
206
- error(err instanceof Error ? err.message : 'Failed to send message');
857
+ }
858
+ /**
859
+ * Attempt to switch to the next healthy provider when the current one fails.
860
+ * Returns true if a switch was made (caller should retry).
861
+ */
862
+ function trySwitchProvider(errMsg, statusCode) {
863
+ const isProviderError = statusCode === 429 ||
864
+ statusCode === 503 ||
865
+ errMsg.includes('ECONNREFUSED') ||
866
+ errMsg.includes('fetch failed') ||
867
+ errMsg.includes('Connection refused');
868
+ if (!isProviderError)
869
+ return false;
870
+ const healthy = availableProviders.filter(p => !p.disabled && p.value !== currentProvider);
871
+ if (healthy.length === 0)
872
+ return false;
873
+ const next = healthy[0];
874
+ if (!next)
875
+ return false;
876
+ const previous = currentProvider;
877
+ currentProvider = next.value;
878
+ availableProviders = availableProviders.map(p => ({ ...p, active: p.value === currentProvider }));
879
+ // Update model to a sensible default for the new provider
880
+ const modelMap = {
881
+ ollama: 'llama3.2',
882
+ anthropic: 'claude-sonnet',
883
+ openai: 'gpt-4o',
884
+ azure: 'gpt-4o',
885
+ google: 'gemini-2.0-flash',
886
+ cerebras: 'llama-3.3-70b',
887
+ };
888
+ if (modelMap[currentProvider]) {
889
+ currentModel = modelMap[currentProvider];
890
+ availableModels = availableModels.map(m => ({ ...m, active: m.value === currentModel }));
207
891
  }
208
- };
209
- // Handle input
210
- rl.on('line', async (line) => {
211
- const input = line.trim();
212
- // Skip empty lines
213
- if (!input) {
214
- rl.prompt();
892
+ messages.push({
893
+ role: 'assistant',
894
+ content: `**${previous} unavailable**, switching to **${currentProvider}**...`,
895
+ timestamp: new Date(),
896
+ });
897
+ rerender();
898
+ return true;
899
+ }
900
+ /** Send a message and stream the response via SSE */
901
+ async function handleSubmit(userMessage, isRetry = false) {
902
+ // Slash commands
903
+ if (userMessage.startsWith('/')) {
904
+ const handled = await handleSlashCommand(userMessage);
905
+ if (handled)
906
+ return;
907
+ // Unknown slash command — fall through to send as message
908
+ }
909
+ // Queue the message locally if server is unreachable
910
+ if (!offlineDetector.getStatus()) {
911
+ offlineDetector.queueCommand({ message: userMessage });
912
+ messages.push({
913
+ role: 'assistant',
914
+ content: `**Offline** — message queued. Will send when server is back online.`,
915
+ timestamp: new Date(),
916
+ });
917
+ rerender();
918
+ return;
919
+ }
920
+ // Cancel any in-flight request
921
+ if (activeAbort) {
922
+ activeAbort.abort();
923
+ activeAbort = null;
924
+ }
925
+ if (!isRetry) {
926
+ lastUserMessage = userMessage;
927
+ }
928
+ messages.push({ role: 'user', content: userMessage, timestamp: new Date() });
929
+ currentSuggestions = [];
930
+ agentStatus = 'thinking';
931
+ agentAction = 'Connecting...';
932
+ streamingContent = '';
933
+ const startTime = Date.now();
934
+ rerender();
935
+ // Ensure conversation exists
936
+ const convId = await ensureConversation();
937
+ if (!convId) {
938
+ agentStatus = 'error';
939
+ streamingContent = undefined;
940
+ rerender();
215
941
  return;
216
942
  }
217
- // Handle commands
218
- if (input.startsWith('/')) {
219
- const [cmd, ...args] = input.slice(1).split(/\s+/);
220
- switch (cmd.toLowerCase()) {
221
- case 'exit':
222
- case 'quit':
223
- case 'q':
224
- console.log(chalk.dim('Goodbye!'));
225
- rl.close();
226
- process.exit(0);
227
- return;
228
- case 'clear':
229
- console.clear();
230
- rl.prompt();
231
- return;
232
- case 'help':
233
- case '?':
234
- showHelp();
235
- rl.prompt();
236
- return;
237
- case 'session':
238
- console.log(chalk.dim(`Session ID: ${conversationId}`));
239
- rl.prompt();
240
- return;
241
- case 'model':
242
- if (args[0]) {
243
- model = args[0];
244
- success(`Model set to: ${model}`);
943
+ // Auto-title on first message
944
+ const isFirstMessage = messages.filter(m => m.role === 'user').length === 1;
945
+ if (isFirstMessage) {
946
+ void autoTitleConversation(convId, userMessage);
947
+ }
948
+ agentAction = 'Waiting for response...';
949
+ rerender();
950
+ const abortController = new AbortController();
951
+ activeAbort = abortController;
952
+ let assistantStarted = false;
953
+ let accumulatedContent = '';
954
+ const toolsUsedThisTurn = [];
955
+ try {
956
+ for await (const event of streamChat(serverConfig, {
957
+ conversationId: convId,
958
+ content: userMessage,
959
+ model: currentModel !== 'auto' ? currentModel : undefined,
960
+ provider: currentProvider !== 'auto' ? currentProvider : undefined,
961
+ agentic: agenticMode,
962
+ showThinking,
963
+ effort,
964
+ signal: abortController.signal,
965
+ })) {
966
+ if (abortController.signal.aborted)
967
+ break;
968
+ elapsedMs = Date.now() - startTime;
969
+ switch (event.type) {
970
+ case 'thinking_start':
971
+ case 'thinking:start': {
972
+ agentStatus = 'thinking';
973
+ agentAction = 'Thinking...';
974
+ rerender();
975
+ break;
245
976
  }
246
- else {
247
- console.log(chalk.dim(`Current model: ${model || 'default'}`));
977
+ case 'thinking_update':
978
+ case 'thinking:update': {
979
+ if (showThinking) {
980
+ const chunk = event.data.content;
981
+ if (chunk) {
982
+ streamingContent = (streamingContent ?? '') + `*${chunk}*`;
983
+ rerender();
984
+ }
985
+ }
986
+ break;
987
+ }
988
+ case 'thinking:end':
989
+ case 'thinking_end': {
990
+ // In agentic mode, thinking:end.text may contain the actual response text
991
+ const responseText = event.data.text;
992
+ if (responseText) {
993
+ if (!assistantStarted) {
994
+ assistantStarted = true;
995
+ agentStatus = 'executing';
996
+ agentAction = 'Responding...';
997
+ streamingContent = '';
998
+ }
999
+ accumulatedContent += responseText;
1000
+ streamingContent = accumulatedContent;
1001
+ rerender();
1002
+ }
1003
+ break;
1004
+ }
1005
+ case 'content_delta': {
1006
+ if (!assistantStarted) {
1007
+ assistantStarted = true;
1008
+ agentStatus = 'executing';
1009
+ agentAction = 'Responding...';
1010
+ streamingContent = '';
1011
+ }
1012
+ const chunk = event.data.content;
1013
+ if (chunk) {
1014
+ accumulatedContent += chunk;
1015
+ streamingContent = accumulatedContent;
1016
+ rerender();
1017
+ }
1018
+ break;
248
1019
  }
249
- rl.prompt();
250
- return;
251
- case 'tools':
252
- useTools = !useTools;
253
- success(`Tools: ${useTools ? 'enabled' : 'disabled'}`);
254
- rl.prompt();
255
- return;
256
- case 'agentic':
257
- agenticMode = !agenticMode;
258
- useTools = agenticMode; // Agentic mode implies tools
259
- success(`Agentic mode: ${agenticMode ? 'enabled' : 'disabled'}`);
260
- rl.prompt();
261
- return;
262
- default:
263
- error(`Unknown command: /${cmd}`);
264
- rl.prompt();
265
- return;
266
- }
267
- }
268
- // Send message to AI
269
- await sendMessage(input);
270
- rl.prompt();
1020
+ case 'tool_call':
1021
+ case 'tool:call': {
1022
+ agentStatus = 'executing';
1023
+ const toolName = event.data.name || event.data.toolName || 'tool';
1024
+ const toolArgs = event.data.arguments || {};
1025
+ toolsUsedThisTurn.push(toolName);
1026
+ agentAction = `Using ${toolName}`;
1027
+ stepCount++;
1028
+ if (showTools) {
1029
+ // Verbose: show tool name + args as a message
1030
+ const argsPreview = Object.entries(toolArgs)
1031
+ .slice(0, 3)
1032
+ .map(([k, v]) => `${k}: ${String(v).slice(0, 60)}`)
1033
+ .join(', ');
1034
+ messages.push({
1035
+ role: 'assistant',
1036
+ content: `**Tool:** \`${toolName}\`${argsPreview ? `\n\`\`\`\n${argsPreview}\n\`\`\`` : ''}`,
1037
+ timestamp: new Date(),
1038
+ });
1039
+ }
1040
+ rerender();
1041
+ break;
1042
+ }
1043
+ case 'tool_result':
1044
+ case 'tool:result': {
1045
+ const resultName = event.data.name || event.data.toolName || 'tool';
1046
+ const succeeded = event.data.success !== false;
1047
+ const durMs = event.data.durationMs;
1048
+ if (showTools) {
1049
+ const durStr = durMs ? ` (${durMs}ms)` : '';
1050
+ const icon = succeeded ? '✓' : '✗';
1051
+ messages.push({
1052
+ role: 'assistant',
1053
+ content: `**${icon} ${resultName}**${durStr}`,
1054
+ timestamp: new Date(),
1055
+ });
1056
+ }
1057
+ agentAction = undefined;
1058
+ rerender();
1059
+ break;
1060
+ }
1061
+ case 'step_start':
1062
+ case 'step:start': {
1063
+ const stepNum = event.data.step;
1064
+ if (stepNum && stepNum > 1) {
1065
+ agentAction = `Step ${stepNum}...`;
1066
+ rerender();
1067
+ }
1068
+ break;
1069
+ }
1070
+ case 'step_complete':
1071
+ case 'step:complete': {
1072
+ agentAction = 'Processing...';
1073
+ rerender();
1074
+ break;
1075
+ }
1076
+ case 'summary': {
1077
+ const summaryText = event.data.summary || event.data.content || event.data.text;
1078
+ if (summaryText && !accumulatedContent) {
1079
+ if (!assistantStarted) {
1080
+ assistantStarted = true;
1081
+ agentStatus = 'executing';
1082
+ streamingContent = '';
1083
+ }
1084
+ accumulatedContent += summaryText;
1085
+ streamingContent = accumulatedContent;
1086
+ rerender();
1087
+ }
1088
+ break;
1089
+ }
1090
+ case 'complete': {
1091
+ // Extract usage from event
1092
+ const rawUsage = event.data.usage;
1093
+ const totalTok = rawUsage?.totalTokens || event.data.totalTokens || 0;
1094
+ const cost = rawUsage?.cost || event.data.cost || 0;
1095
+ if (totalTok > 0) {
1096
+ tokensUsed += totalTok;
1097
+ estimatedCost += cost;
1098
+ }
1099
+ // Finalize message
1100
+ if (accumulatedContent) {
1101
+ lastAssistantContent = accumulatedContent;
1102
+ messages.push({
1103
+ role: 'assistant',
1104
+ content: accumulatedContent,
1105
+ model: currentModel !== 'auto' ? currentModel : undefined,
1106
+ timestamp: new Date(),
1107
+ });
1108
+ }
1109
+ streamingContent = undefined;
1110
+ agentStatus = 'complete';
1111
+ agentAction = undefined;
1112
+ elapsedMs = Date.now() - startTime;
1113
+ // Generate prompt suggestions for display in SuggestionBar
1114
+ if (accumulatedContent) {
1115
+ const suggestionEngine = getPromptSuggestionEngine();
1116
+ currentSuggestions = suggestionEngine.generateSuggestions({
1117
+ lastUserMessage: lastUserMessage,
1118
+ lastAssistantResponse: accumulatedContent,
1119
+ toolsUsed: toolsUsedThisTurn,
1120
+ conversationLength: messages.length,
1121
+ });
1122
+ }
1123
+ else {
1124
+ currentSuggestions = [];
1125
+ }
1126
+ // Update rate limit state from response headers if provided
1127
+ const rlHeaders = event.data.rateLimitHeaders;
1128
+ if (rlHeaders && currentProvider !== 'auto') {
1129
+ const rateLimitMonitor = getRateLimitMonitor();
1130
+ rateLimitMonitor.updateFromHeaders(currentProvider, rlHeaders);
1131
+ // Show rate limit warning below the response if thresholds are crossed
1132
+ const warnResult = rateLimitMonitor.shouldWarn(currentProvider);
1133
+ if (warnResult.warn) {
1134
+ const warnColor = warnResult.level === 'critical'
1135
+ ? chalk.red
1136
+ : warnResult.level === 'warning'
1137
+ ? chalk.yellow
1138
+ : chalk.dim;
1139
+ messages.push({
1140
+ role: 'assistant',
1141
+ content: warnColor(warnResult.message),
1142
+ timestamp: new Date(),
1143
+ });
1144
+ // Suggest an alternative provider if approaching limits
1145
+ const knownProviders = availableProviders
1146
+ .filter(p => !p.disabled)
1147
+ .map(p => p.value);
1148
+ const suggestion = rateLimitMonitor.suggestAlternative(currentProvider, knownProviders);
1149
+ if (suggestion) {
1150
+ messages.push({
1151
+ role: 'assistant',
1152
+ content: chalk.dim(`Tip: switch to ${suggestion} with /provider ${suggestion}`),
1153
+ timestamp: new Date(),
1154
+ });
1155
+ }
1156
+ }
1157
+ }
1158
+ rerender();
1159
+ // Return to idle
1160
+ setTimeout(() => {
1161
+ agentStatus = 'idle';
1162
+ rerender();
1163
+ }, 1500);
1164
+ break;
1165
+ }
1166
+ case 'error': {
1167
+ const errMsg = event.data.message || 'Unknown error';
1168
+ const errStatusCode = event.data.statusCode;
1169
+ const errRlHeaders = event.data.rateLimitHeaders;
1170
+ // Update rate limit state even on error responses (headers may still be present)
1171
+ if (errRlHeaders && currentProvider !== 'auto') {
1172
+ getRateLimitMonitor().updateFromHeaders(currentProvider, errRlHeaders);
1173
+ }
1174
+ // Auto-switch provider on transient failures (429, 503, ECONNREFUSED)
1175
+ const switched = trySwitchProvider(errMsg, errStatusCode);
1176
+ if (switched) {
1177
+ // Abort current stream and retry the last message on the new provider
1178
+ abortController.abort();
1179
+ streamingContent = undefined;
1180
+ agentStatus = 'idle';
1181
+ agentAction = undefined;
1182
+ rerender();
1183
+ // Small delay for the switch message to render, then retry
1184
+ setTimeout(() => { void handleSubmit(userMessage, true); }, 500);
1185
+ return;
1186
+ }
1187
+ // Get recovery suggestions from the error recovery advisor
1188
+ const knownProviders = availableProviders
1189
+ .filter(p => !p.disabled)
1190
+ .map(p => p.value);
1191
+ const recoveryActions = getErrorRecoveryAdvisor().advise({
1192
+ message: errMsg,
1193
+ provider: currentProvider,
1194
+ model: currentModel,
1195
+ statusCode: errStatusCode,
1196
+ }, {
1197
+ availableProviders: knownProviders,
1198
+ retryCount: 0,
1199
+ maxRetries: 3,
1200
+ });
1201
+ // Build the error message with recovery suggestions appended
1202
+ const suggestionLines = recoveryActions
1203
+ .slice(0, 3)
1204
+ .map(a => chalk.dim(` • ${a.description}`));
1205
+ const fullErrContent = suggestionLines.length > 0
1206
+ ? `**Error:** ${errMsg}\n\n${chalk.dim('Suggestions:')}\n${suggestionLines.join('\n')}`
1207
+ : `**Error:** ${errMsg}`;
1208
+ messages.push({
1209
+ role: 'assistant',
1210
+ content: fullErrContent,
1211
+ timestamp: new Date(),
1212
+ });
1213
+ streamingContent = undefined;
1214
+ agentStatus = 'error';
1215
+ agentAction = undefined;
1216
+ rerender();
1217
+ setTimeout(() => {
1218
+ agentStatus = 'idle';
1219
+ rerender();
1220
+ }, 2000);
1221
+ break;
1222
+ }
1223
+ case 'connection_lost': {
1224
+ const attempt = event.data.attempt;
1225
+ const maxRetries = event.data.maxRetries;
1226
+ connectionStatus = 'reconnecting';
1227
+ agentAction = `Reconnecting (${attempt}/${maxRetries})...`;
1228
+ rerender();
1229
+ break;
1230
+ }
1231
+ case 'reconnected': {
1232
+ connectionStatus = 'connected';
1233
+ agentAction = 'Reconnected — resuming...';
1234
+ rerender();
1235
+ break;
1236
+ }
1237
+ case 'session_start':
1238
+ case 'session:start':
1239
+ case 'user_message':
1240
+ case 'message_saved':
1241
+ // Acknowledgment events — no UI update needed
1242
+ break;
1243
+ default:
1244
+ // Forward-compatible: ignore unknown event types
1245
+ break;
1246
+ }
1247
+ }
1248
+ }
1249
+ catch (err) {
1250
+ if (!abortController.signal.aborted) {
1251
+ const errMsg = err instanceof Error ? err.message : 'Stream error';
1252
+ messages.push({ role: 'assistant', content: `**Error:** ${errMsg}`, timestamp: new Date() });
1253
+ streamingContent = undefined;
1254
+ agentStatus = 'error';
1255
+ agentAction = undefined;
1256
+ rerender();
1257
+ setTimeout(() => {
1258
+ agentStatus = 'idle';
1259
+ rerender();
1260
+ }, 2000);
1261
+ }
1262
+ }
1263
+ finally {
1264
+ if (activeAbort === abortController)
1265
+ activeAbort = null;
1266
+ }
1267
+ }
1268
+ // Snapshot for Ink — all mutable state read at call time
1269
+ function buildProps() {
1270
+ return {
1271
+ sessionInfo: {
1272
+ model: currentModel,
1273
+ provider: currentProvider,
1274
+ sessionId,
1275
+ mode: (agenticMode ? 'agentic' : 'chat'),
1276
+ },
1277
+ tokensUsed,
1278
+ tokensMax,
1279
+ estimatedCost: estimatedCost > 0 ? estimatedCost : undefined,
1280
+ agentStatus,
1281
+ agentAction,
1282
+ stepCount,
1283
+ elapsedMs,
1284
+ messages: [...messages],
1285
+ streamingContent,
1286
+ suggestions: [...currentSuggestions],
1287
+ availableModels,
1288
+ availableProviders,
1289
+ connectionStatus,
1290
+ connectionLatencyMs,
1291
+ onSubmit: (msg) => { void handleSubmit(msg); },
1292
+ onCancel: () => {
1293
+ if (activeAbort) {
1294
+ activeAbort.abort();
1295
+ activeAbort = null;
1296
+ streamingContent = undefined;
1297
+ agentStatus = 'idle';
1298
+ agentAction = undefined;
1299
+ rerender();
1300
+ }
1301
+ },
1302
+ };
1303
+ }
1304
+ const { rerender: inkRerender, waitUntilExit } = render(React.createElement(ChatApp, buildProps()));
1305
+ rerenderFn = () => {
1306
+ inkRerender(React.createElement(ChatApp, buildProps()));
1307
+ };
1308
+ await waitUntilExit();
1309
+ // Clean up the offline detector polling loop
1310
+ offlineDetector.stop();
1311
+ }
1312
+ /**
1313
+ * Start interactive REPL mode
1314
+ * Uses the self-contained interactive module (extractable as standalone package)
1315
+ */
1316
+ async function startREPL(options) {
1317
+ const { startInteractiveREPL } = await import('../interactive/index.js');
1318
+ const { getConfig } = await import('../utils/config.js');
1319
+ const { detectBaseUrl } = await import('../utils/api.js');
1320
+ const config = getConfig();
1321
+ // Auto-detect the running profClaw server via shared utility
1322
+ const replBaseUrl = await detectBaseUrl();
1323
+ await startInteractiveREPL({
1324
+ server: {
1325
+ baseUrl: replBaseUrl,
1326
+ apiToken: config.apiToken,
1327
+ },
1328
+ model: options.model,
1329
+ sessionId: options.session,
1330
+ agentic: options.agentic,
1331
+ effort: 'medium',
271
1332
  });
272
- rl.on('close', () => {
273
- process.exit(0);
1333
+ }
1334
+ // === Print Mode (headless / CI) ===
1335
+ /**
1336
+ * Execute a single-shot chat and print ONLY the response text to stdout.
1337
+ * No spinners, no colors, no usage stats.
1338
+ * Errors go to stderr; exit code 1 on failure.
1339
+ */
1340
+ async function executePrint(message, options) {
1341
+ try {
1342
+ let responseContent;
1343
+ if (options.agentic) {
1344
+ const result = await api.post('/api/chat/with-tools', {
1345
+ messages: [{ role: 'user', content: message }],
1346
+ model: options.model,
1347
+ presetId: 'agentic',
1348
+ enableAllTools: true,
1349
+ securityMode: 'full',
1350
+ });
1351
+ if (!result.ok || result.data?.error) {
1352
+ process.stderr.write((result.error || result.data?.error || 'Chat failed') + '\n');
1353
+ process.exit(1);
1354
+ }
1355
+ responseContent = result.data?.content ?? result.data?.message?.content ?? '';
1356
+ }
1357
+ else {
1358
+ const result = await api.post('/api/chat/quick', {
1359
+ prompt: message,
1360
+ model: options.model,
1361
+ });
1362
+ if (!result.ok || result.data?.error) {
1363
+ process.stderr.write((result.error || result.data?.error || 'Chat failed') + '\n');
1364
+ process.exit(1);
1365
+ }
1366
+ responseContent = result.data?.content ?? result.data?.message?.content ?? '';
1367
+ }
1368
+ process.stdout.write(responseContent + '\n');
1369
+ }
1370
+ catch (err) {
1371
+ process.stderr.write((err instanceof Error ? err.message : 'Chat failed') + '\n');
1372
+ process.exit(1);
1373
+ }
1374
+ }
1375
+ /**
1376
+ * Read all of stdin until EOF and return as a string.
1377
+ */
1378
+ async function readStdin() {
1379
+ return new Promise((resolve, reject) => {
1380
+ const chunks = [];
1381
+ process.stdin.on('data', (chunk) => chunks.push(chunk));
1382
+ process.stdin.on('end', () => resolve(Buffer.concat(chunks).toString('utf-8').trim()));
1383
+ process.stdin.on('error', reject);
274
1384
  });
275
- // Start prompt
276
- rl.prompt();
277
1385
  }
278
1386
  // === CLI Commands ===
279
1387
  export function chatCommands() {
@@ -285,7 +1393,43 @@ export function chatCommands() {
285
1393
  .option('-a, --agentic', 'Enable agentic mode with all tools')
286
1394
  .option('-s, --session <id>', 'Resume existing session')
287
1395
  .option('--json', 'Output as JSON (single-shot only)')
1396
+ .option('--tui', 'Launch the Ink-based interactive TUI (experimental)')
1397
+ .option('-p, --print', 'Print mode: output response to stdout and exit (CI/scripts)')
1398
+ .option('--resume-checkpoint <sessionId>', 'Resume a saved agent checkpoint by session ID')
288
1399
  .action(async (message, options) => {
1400
+ // --print mode: headless, stdout only, no formatting
1401
+ if (options.print) {
1402
+ let msg = message;
1403
+ if (!msg) {
1404
+ // No inline argument — try reading from stdin pipe
1405
+ if (!process.stdin.isTTY) {
1406
+ msg = await readStdin();
1407
+ }
1408
+ if (!msg) {
1409
+ process.stderr.write('Error: --print requires a message argument or piped stdin\n');
1410
+ process.exit(1);
1411
+ }
1412
+ }
1413
+ await executePrint(msg, options);
1414
+ return;
1415
+ }
1416
+ // --resume-checkpoint: load a saved agent checkpoint and print summary
1417
+ if (options.resumeCheckpoint) {
1418
+ const { getCheckpointManager } = await import('../../agents/checkpoint-manager.js');
1419
+ const cpManager = getCheckpointManager();
1420
+ const cp = await cpManager.load(options.resumeCheckpoint);
1421
+ if (!cp) {
1422
+ error(`No checkpoint found for session ID: ${options.resumeCheckpoint}`);
1423
+ process.exit(1);
1424
+ }
1425
+ info(`Resuming from checkpoint — session ${cp.sessionId.slice(0, 12)} at step ${cp.currentStep}`);
1426
+ if (cp.taskDescription) {
1427
+ info(`Task: ${cp.taskDescription}`);
1428
+ }
1429
+ info(`To continue this session, open the TUI: profclaw chat --tui --session ${cp.sessionId}`);
1430
+ success(`Checkpoint loaded. ${cp.toolCallHistory.length} tool calls, ${cp.tokensUsed} tokens used.`);
1431
+ return;
1432
+ }
289
1433
  if (message) {
290
1434
  // Single-shot mode
291
1435
  if (options.tools || options.agentic) {
@@ -295,6 +1439,10 @@ export function chatCommands() {
295
1439
  await executeSingleShot(message, options);
296
1440
  }
297
1441
  }
1442
+ else if (options.tui) {
1443
+ // Ink TUI mode
1444
+ await startTUI(options);
1445
+ }
298
1446
  else {
299
1447
  // Interactive REPL mode
300
1448
  await startREPL(options);
@@ -355,6 +1503,47 @@ export function chatCommands() {
355
1503
  console.log('');
356
1504
  console.log(chalk.dim(`Resume with: profclaw chat -s <session-id>`));
357
1505
  });
1506
+ chat
1507
+ .command('checkpoints')
1508
+ .alias('cp')
1509
+ .description('List agent execution checkpoints saved to .profclaw/checkpoints/')
1510
+ .option('--json', 'Output as JSON')
1511
+ .option('--remove <sessionId>', 'Delete a checkpoint by session ID')
1512
+ .action(async (options) => {
1513
+ const { getCheckpointManager } = await import('../../agents/checkpoint-manager.js');
1514
+ const cpManager = getCheckpointManager();
1515
+ if (options.remove) {
1516
+ await cpManager.remove(options.remove);
1517
+ success(`Checkpoint removed: ${options.remove}`);
1518
+ return;
1519
+ }
1520
+ const spin = spinner('Loading checkpoints...').start();
1521
+ const list = await cpManager.list();
1522
+ spin.stop();
1523
+ if (options.json) {
1524
+ console.log(JSON.stringify(list, null, 2));
1525
+ return;
1526
+ }
1527
+ if (list.length === 0) {
1528
+ console.log('No agent checkpoints found.');
1529
+ console.log(chalk.dim('Checkpoints are saved automatically every 5 steps during agentic execution.'));
1530
+ return;
1531
+ }
1532
+ console.log('');
1533
+ console.log(chalk.bold(`Agent Checkpoints (${list.length}):`));
1534
+ console.log('');
1535
+ for (const cp of list) {
1536
+ const updated = new Date(cp.updatedAt).toLocaleString();
1537
+ const task = cp.taskDescription
1538
+ ? ` ${chalk.dim('"')}${chalk.white(cp.taskDescription.slice(0, 55))}${cp.taskDescription.length > 55 ? chalk.dim('...') : ''}${chalk.dim('"')}`
1539
+ : '';
1540
+ console.log(` ${chalk.cyan(cp.sessionId.slice(0, 12))} step ${chalk.yellow(String(cp.step))} ${chalk.dim(updated)}`);
1541
+ if (task)
1542
+ console.log(task);
1543
+ }
1544
+ console.log('');
1545
+ console.log(chalk.dim('Resume with: profclaw chat --resume-checkpoint <sessionId>'));
1546
+ });
358
1547
  return chat;
359
1548
  }
360
1549
  //# sourceMappingURL=chat.js.map