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.
- package/.env.example +14 -2
- package/CHANGELOG.md +37 -0
- package/CONTRIBUTING.md +20 -0
- package/Dockerfile +39 -23
- package/README.md +174 -40
- package/dist/agents/agent-summary.d.ts +44 -0
- package/dist/agents/agent-summary.js +176 -0
- package/dist/agents/auto-memory.d.ts +59 -0
- package/dist/agents/auto-memory.js +310 -0
- package/dist/agents/checkpoint-manager.d.ts +76 -0
- package/dist/agents/checkpoint-manager.js +156 -0
- package/dist/agents/circuit-breaker.d.ts +54 -0
- package/dist/agents/circuit-breaker.js +153 -0
- package/dist/agents/context-compactor.d.ts +70 -0
- package/dist/agents/context-compactor.js +361 -0
- package/dist/agents/conversation-branch.d.ts +55 -0
- package/dist/agents/conversation-branch.js +115 -0
- package/dist/agents/error-recovery.d.ts +44 -0
- package/dist/agents/error-recovery.js +197 -0
- package/dist/agents/events.d.ts +76 -0
- package/dist/agents/events.js +8 -0
- package/dist/agents/executor.d.ts +35 -1
- package/dist/agents/executor.js +573 -124
- package/dist/agents/file-snapshots.d.ts +72 -0
- package/dist/agents/file-snapshots.js +182 -0
- package/dist/agents/index.d.ts +2 -0
- package/dist/agents/orchestrator.d.ts +183 -0
- package/dist/agents/orchestrator.js +495 -0
- package/dist/agents/permissions.d.ts +63 -0
- package/dist/agents/permissions.js +138 -0
- package/dist/agents/plan-mode.d.ts +71 -0
- package/dist/agents/plan-mode.js +186 -0
- package/dist/agents/prompt-suggestions.d.ts +33 -0
- package/dist/agents/prompt-suggestions.js +208 -0
- package/dist/agents/rate-limit-monitor.d.ts +56 -0
- package/dist/agents/rate-limit-monitor.js +224 -0
- package/dist/agents/result-store.d.ts +37 -0
- package/dist/agents/result-store.js +104 -0
- package/dist/agents/session-diff.d.ts +43 -0
- package/dist/agents/session-diff.js +258 -0
- package/dist/agents/tool-loader.d.ts +65 -0
- package/dist/agents/tool-loader.js +273 -0
- package/dist/agents/transcript.d.ts +96 -0
- package/dist/agents/transcript.js +267 -0
- package/dist/agents/worktree-manager.d.ts +80 -0
- package/dist/agents/worktree-manager.js +203 -0
- package/dist/auth/middleware.js +14 -0
- package/dist/chat/agentic-executor.d.ts +7 -0
- package/dist/chat/agentic-executor.js +253 -30
- package/dist/chat/conversations.d.ts +2 -0
- package/dist/chat/conversations.js +8 -0
- package/dist/chat/execution/executor.d.ts +8 -0
- package/dist/chat/execution/executor.js +157 -103
- package/dist/chat/execution/index.js +5 -1
- package/dist/chat/execution/tool-router.js +40 -14
- package/dist/chat/execution/tools/canvas.d.ts +2 -2
- package/dist/chat/execution/tools/code-tools.d.ts +66 -0
- package/dist/chat/execution/tools/code-tools.js +353 -0
- package/dist/chat/execution/tools/complete-task.d.ts +2 -2
- package/dist/chat/execution/tools/cron-tool.d.ts +26 -10
- package/dist/chat/execution/tools/cron-tool.js +48 -3
- package/dist/chat/execution/tools/decompose-task.d.ts +58 -0
- package/dist/chat/execution/tools/decompose-task.js +166 -0
- package/dist/chat/execution/tools/exec.d.ts +5 -2
- package/dist/chat/execution/tools/exec.js +294 -155
- package/dist/chat/execution/tools/feed-tools.d.ts +77 -0
- package/dist/chat/execution/tools/feed-tools.js +142 -0
- package/dist/chat/execution/tools/file-ops.d.ts +1 -1
- package/dist/chat/execution/tools/github.d.ts +2 -2
- package/dist/chat/execution/tools/index.d.ts +14 -2
- package/dist/chat/execution/tools/index.js +86 -12
- package/dist/chat/execution/tools/integrations.d.ts +8 -8
- package/dist/chat/execution/tools/multi-patch.d.ts +53 -0
- package/dist/chat/execution/tools/multi-patch.js +373 -0
- package/dist/chat/execution/tools/openai-image-gen.d.ts +2 -2
- package/dist/chat/execution/tools/plan-tools.d.ts +80 -0
- package/dist/chat/execution/tools/plan-tools.js +134 -0
- package/dist/chat/execution/tools/profclaw-ops.d.ts +6 -6
- package/dist/chat/execution/tools/repl-tool.d.ts +46 -0
- package/dist/chat/execution/tools/repl-tool.js +207 -0
- package/dist/chat/execution/tools/sessions-send.d.ts +2 -2
- package/dist/chat/execution/tools/slack-actions.d.ts +2 -2
- package/dist/chat/execution/tools/system.d.ts +4 -4
- package/dist/chat/execution/tools/todo-tool.d.ts +72 -0
- package/dist/chat/execution/tools/todo-tool.js +152 -0
- package/dist/chat/execution/tools/tool-search.d.ts +50 -0
- package/dist/chat/execution/tools/tool-search.js +155 -0
- package/dist/chat/execution/types.d.ts +21 -14
- package/dist/chat/execution/workflows/executor.d.ts +11 -0
- package/dist/chat/execution/workflows/executor.js +99 -1
- package/dist/chat/execution/workflows/types.d.ts +35 -2
- package/dist/chat/format/index.d.ts +1 -0
- package/dist/chat/format/index.js +1 -0
- package/dist/chat/format/markdown-convert.d.ts +23 -0
- package/dist/chat/format/markdown-convert.js +149 -0
- package/dist/chat/index.d.ts +1 -1
- package/dist/chat/index.js +1 -1
- package/dist/chat/message-handler.js +58 -6
- package/dist/chat/proactive/index.d.ts +62 -0
- package/dist/chat/proactive/index.js +147 -0
- package/dist/chat/prompt-adapter.js +2 -0
- package/dist/chat/providers/irc/index.d.ts +2 -2
- package/dist/chat/providers/telegram/index.d.ts +2 -0
- package/dist/chat/providers/telegram/index.js +21 -2
- package/dist/chat/system-prompts.d.ts +3 -3
- package/dist/chat/system-prompts.js +116 -61
- package/dist/chat/tools.d.ts +6 -6
- package/dist/cli/commands/auth.js +64 -1
- package/dist/cli/commands/chat.js +1327 -138
- package/dist/cli/commands/doctor.js +222 -16
- package/dist/cli/commands/history.d.ts +3 -0
- package/dist/cli/commands/history.js +138 -0
- package/dist/cli/commands/import.d.ts +44 -0
- package/dist/cli/commands/import.js +526 -0
- package/dist/cli/commands/init.d.ts +13 -0
- package/dist/cli/commands/init.js +381 -0
- package/dist/cli/commands/onboard.js +210 -30
- package/dist/cli/commands/plan.d.ts +13 -0
- package/dist/cli/commands/plan.js +184 -0
- package/dist/cli/commands/serve.d.ts +7 -0
- package/dist/cli/commands/serve.js +77 -18
- package/dist/cli/commands/setup.js +84 -3
- package/dist/cli/commands/status.js +54 -0
- package/dist/cli/commands/tools.js +13 -13
- package/dist/cli/commands/tui.js +26 -1
- package/dist/cli/index.js +19 -0
- package/dist/cli/ink/App.d.ts +13 -0
- package/dist/cli/ink/App.js +6 -0
- package/dist/cli/ink/ChatApp.d.ts +60 -0
- package/dist/cli/ink/ChatApp.js +325 -0
- package/dist/cli/ink/DashboardApp.d.ts +3 -0
- package/dist/cli/ink/DashboardApp.js +143 -0
- package/dist/cli/ink/components/AgentStatus.d.ts +17 -0
- package/dist/cli/ink/components/AgentStatus.js +33 -0
- package/dist/cli/ink/components/ChannelList.d.ts +17 -0
- package/dist/cli/ink/components/ChannelList.js +33 -0
- package/dist/cli/ink/components/ConnectionStatus.d.ts +19 -0
- package/dist/cli/ink/components/ConnectionStatus.js +50 -0
- package/dist/cli/ink/components/CostBar.d.ts +19 -0
- package/dist/cli/ink/components/CostBar.js +35 -0
- package/dist/cli/ink/components/HookStatus.d.ts +15 -0
- package/dist/cli/ink/components/HookStatus.js +7 -0
- package/dist/cli/ink/components/PermissionPrompt.d.ts +21 -0
- package/dist/cli/ink/components/PermissionPrompt.js +51 -0
- package/dist/cli/ink/components/PlanView.d.ts +23 -0
- package/dist/cli/ink/components/PlanView.js +26 -0
- package/dist/cli/ink/components/ProviderList.d.ts +16 -0
- package/dist/cli/ink/components/ProviderList.js +16 -0
- package/dist/cli/ink/components/SessionHeader.d.ts +15 -0
- package/dist/cli/ink/components/SessionHeader.js +15 -0
- package/dist/cli/ink/components/StreamingMessage.d.ts +20 -0
- package/dist/cli/ink/components/StreamingMessage.js +27 -0
- package/dist/cli/ink/components/SuggestionBar.d.ts +15 -0
- package/dist/cli/ink/components/SuggestionBar.js +28 -0
- package/dist/cli/ink/components/SystemHealth.d.ts +20 -0
- package/dist/cli/ink/components/SystemHealth.js +39 -0
- package/dist/cli/ink/components/TaskList.d.ts +17 -0
- package/dist/cli/ink/components/TaskList.js +59 -0
- package/dist/cli/ink/components/ToolCall.d.ts +17 -0
- package/dist/cli/ink/components/ToolCall.js +43 -0
- package/dist/cli/ink/index.d.ts +23 -0
- package/dist/cli/ink/index.js +15 -0
- package/dist/cli/ink/keybindings.d.ts +35 -0
- package/dist/cli/ink/keybindings.js +105 -0
- package/dist/cli/ink/markdown-renderer.d.ts +47 -0
- package/dist/cli/ink/markdown-renderer.js +380 -0
- package/dist/cli/ink/output-styles.d.ts +39 -0
- package/dist/cli/ink/output-styles.js +119 -0
- package/dist/cli/interactive/auto-serve.d.ts +38 -0
- package/dist/cli/interactive/auto-serve.js +209 -0
- package/dist/cli/interactive/index.d.ts +25 -0
- package/dist/cli/interactive/index.js +23 -0
- package/dist/cli/interactive/picker.d.ts +28 -0
- package/dist/cli/interactive/picker.js +153 -0
- package/dist/cli/interactive/renderer.d.ts +26 -0
- package/dist/cli/interactive/renderer.js +555 -0
- package/dist/cli/interactive/repl.d.ts +14 -0
- package/dist/cli/interactive/repl.js +1516 -0
- package/dist/cli/interactive/stream-client.d.ts +59 -0
- package/dist/cli/interactive/stream-client.js +506 -0
- package/dist/cli/interactive/types.d.ts +121 -0
- package/dist/cli/interactive/types.js +10 -0
- package/dist/cli/utils/api.d.ts +9 -0
- package/dist/cli/utils/api.js +58 -2
- package/dist/cron/natural-language.d.ts +54 -0
- package/dist/cron/natural-language.js +569 -0
- package/dist/cron/scheduler.d.ts +34 -4
- package/dist/cron/scheduler.js +141 -9
- package/dist/cron/templates.d.ts +1 -1
- package/dist/cron/templates.js +219 -0
- package/dist/feeds/feed-engine.d.ts +149 -0
- package/dist/feeds/feed-engine.js +600 -0
- package/dist/feeds/index.d.ts +9 -0
- package/dist/feeds/index.js +8 -0
- package/dist/gateway/router.js +6 -1
- package/dist/hooks/built-in/audit-log.d.ts +11 -0
- package/dist/hooks/built-in/audit-log.js +84 -0
- package/dist/hooks/built-in/cost-warning.d.ts +10 -0
- package/dist/hooks/built-in/cost-warning.js +59 -0
- package/dist/hooks/built-in/dangerous-tool.d.ts +32 -0
- package/dist/hooks/built-in/dangerous-tool.js +80 -0
- package/dist/hooks/built-in/index.d.ts +14 -0
- package/dist/hooks/built-in/index.js +20 -0
- package/dist/hooks/index.d.ts +3 -0
- package/dist/hooks/index.js +6 -0
- package/dist/hooks/loader.d.ts +17 -0
- package/dist/hooks/loader.js +142 -0
- package/dist/hooks/registry.d.ts +62 -0
- package/dist/hooks/registry.js +127 -0
- package/dist/integrations/web-search.js +6 -3
- package/dist/memory/index.d.ts +1 -0
- package/dist/memory/index.js +2 -0
- package/dist/memory/memory-service.js +14 -13
- package/dist/memory/observational.d.ts +62 -0
- package/dist/memory/observational.js +396 -0
- package/dist/middleware/content-filter.d.ts +22 -0
- package/dist/middleware/content-filter.js +69 -0
- package/dist/middleware/request-validator.d.ts +159 -0
- package/dist/middleware/request-validator.js +93 -0
- package/dist/plugins/sdk.d.ts +22 -0
- package/dist/plugins/sdk.js +17 -1
- package/dist/projects/types.d.ts +10 -10
- package/dist/providers/ai-sdk.d.ts +11 -0
- package/dist/providers/ai-sdk.js +78 -19
- package/dist/providers/core/models.js +67 -1
- package/dist/providers/core/types.d.ts +3 -0
- package/dist/providers/index.d.ts +1 -0
- package/dist/providers/index.js +1 -0
- package/dist/providers/smart-router.d.ts +118 -0
- package/dist/providers/smart-router.js +685 -0
- package/dist/providers/types.d.ts +16 -16
- package/dist/routes/agents.js +50 -0
- package/dist/routes/auth.js +8 -1
- package/dist/routes/chat.js +259 -14
- package/dist/routes/costs.js +32 -0
- package/dist/routes/cron.js +80 -5
- package/dist/routes/feeds.d.ts +10 -0
- package/dist/routes/feeds.js +168 -0
- package/dist/routes/mcp.js +21 -0
- package/dist/routes/memory.js +130 -0
- package/dist/routes/skills.js +129 -0
- package/dist/routes/teams.d.ts +9 -0
- package/dist/routes/teams.js +210 -0
- package/dist/routes/tools.js +14 -0
- package/dist/routes/users.js +2 -0
- package/dist/server/route-loader.js +3 -1
- package/dist/server/stream-bridge.d.ts +19 -0
- package/dist/server/stream-bridge.js +38 -0
- package/dist/server.js +310 -36
- package/dist/skills/marketplace.d.ts +87 -0
- package/dist/skills/marketplace.js +268 -0
- package/dist/storage/index.d.ts +14 -0
- package/dist/storage/index.js +64 -3
- package/dist/storage/schema.d.ts +4 -4
- package/dist/storage/schema.js +1 -1
- package/dist/teams/index.d.ts +110 -0
- package/dist/teams/index.js +365 -0
- package/dist/tickets/types.d.ts +39 -39
- package/dist/types/summary.d.ts +5 -5
- package/dist/utils/auto-update.d.ts +23 -0
- package/dist/utils/auto-update.js +157 -0
- package/dist/utils/offline-detect.d.ts +55 -0
- package/dist/utils/offline-detect.js +121 -0
- package/dist/utils/pid-file.d.ts +36 -0
- package/dist/utils/pid-file.js +125 -0
- package/dist/utils/prevent-sleep.d.ts +27 -0
- package/dist/utils/prevent-sleep.js +95 -0
- package/dist/utils/sanitizer.d.ts +57 -0
- package/dist/utils/sanitizer.js +174 -0
- package/docker-compose.yml +13 -137
- package/package.json +31 -6
- package/skills/deploy-cloudflare/SKILL.md +62 -0
- package/skills/deploy-vercel/SKILL.md +67 -0
- package/skills/docker-deploy/SKILL.md +158 -0
- package/skills/full-stack-builder/SKILL.md +92 -0
- package/skills/humanizer/SKILL.md +488 -0
- package/skills/profclaw-assistant/SKILL.md +85 -56
- package/ui/dist/assets/ActivityView-Da6HQ8ws.js +1 -0
- package/ui/dist/assets/AgentList-H58R0kX3.js +1 -0
- package/ui/dist/assets/AnalyticsDashboard-B28AeRsc.js +1 -0
- package/ui/dist/assets/AreaChart-DwLzeAQ8.js +1 -0
- package/ui/dist/assets/CartesianChart-piKQsTAR.js +36 -0
- package/ui/dist/assets/ChatView-Dbj8rxU5.js +10 -0
- package/ui/dist/assets/CostsDashboard-C6F5df0F.js +1 -0
- package/ui/dist/assets/EstimateSelect-wga0rUKX.js +1 -0
- package/ui/dist/assets/FeedDashboard-Ef6FEzEf.js +1 -0
- package/ui/dist/assets/FloatingChatbot-0yhkUeCq.js +2 -0
- package/ui/dist/assets/InviteCodeManagement-tUXfx4Bc.js +2 -0
- package/ui/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
- package/ui/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
- package/ui/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
- package/ui/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
- package/ui/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
- package/ui/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
- package/ui/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
- package/ui/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
- package/ui/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
- package/ui/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
- package/ui/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
- package/ui/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
- package/ui/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
- package/ui/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
- package/ui/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
- package/ui/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
- package/ui/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
- package/ui/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
- package/ui/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
- package/ui/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
- package/ui/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
- package/ui/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
- package/ui/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
- package/ui/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
- package/ui/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
- package/ui/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
- package/ui/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
- package/ui/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
- package/ui/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
- package/ui/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
- package/ui/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
- package/ui/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
- package/ui/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
- package/ui/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
- package/ui/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
- package/ui/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
- package/ui/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
- package/ui/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
- package/ui/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
- package/ui/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
- package/ui/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
- package/ui/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
- package/ui/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
- package/ui/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
- package/ui/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
- package/ui/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
- package/ui/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
- package/ui/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
- package/ui/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
- package/ui/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
- package/ui/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
- package/ui/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
- package/ui/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
- package/ui/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
- package/ui/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
- package/ui/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
- package/ui/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
- package/ui/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
- package/ui/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
- package/ui/dist/assets/MarketplacePage-DGrLtolR.js +1 -0
- package/ui/dist/assets/{ProjectDetail--gPfvATB.js → ProjectDetail-BgWvA6UP.js} +2 -2
- package/ui/dist/assets/ProjectIcon-TDuliqwg.js +1 -0
- package/ui/dist/assets/ProjectList-DXgM4187.js +1 -0
- package/ui/dist/assets/{Settings-1kaf7VHE.js → Settings-xA8Zhi3p.js} +3 -3
- package/ui/dist/assets/StatusIndicator-B8vfFAxg.js +1 -0
- package/ui/dist/assets/{SummaryDetail-DV3AmONk.js → SummaryDetail-eedRfTtF.js} +2 -2
- package/ui/dist/assets/SummaryList-CLNX2P84.js +1 -0
- package/ui/dist/assets/TaskDetail-BtWclNiR.js +1 -0
- package/ui/dist/assets/TaskList-JjODAgSc.js +1 -0
- package/ui/dist/assets/TeamsPage-CBK5iAyx.js +1 -0
- package/ui/dist/assets/TicketBoard-C9aJNNYp.js +1 -0
- package/ui/dist/assets/TicketDetail-CV5JmL31.js +1 -0
- package/ui/dist/assets/TicketList-68V4lzUz.js +1 -0
- package/ui/dist/assets/UserManagement-DKTb6UpE.js +2 -0
- package/ui/dist/assets/ViewSwitcher-BvdQht2Y.js +1 -0
- package/ui/dist/assets/alert-dialog-O6qBOv_f.js +1 -0
- package/ui/dist/assets/{archive-BsLa7brF.js → archive-CX7UvZpf.js} +1 -1
- package/ui/dist/assets/badge-BbfnwDJP.js +1 -0
- package/ui/dist/assets/{bug-CmZI7jqM.js → bug-3WX0394-.js} +1 -1
- package/ui/dist/assets/{calendar-Bf0t2B82.js → calendar-buq77TaS.js} +1 -1
- package/ui/dist/assets/{chevron-up-CoUsOvZj.js → chevron-up-4ymsBJRo.js} +1 -1
- package/ui/dist/assets/{circle-x-Dgw11LgT.js → circle-x-ByXtwR6Y.js} +1 -1
- package/ui/dist/assets/constants-CVdDh7p0.js +1 -0
- package/ui/dist/assets/{cpu-DiPMQICV.js → cpu-CiuoCeDY.js} +1 -1
- package/ui/dist/assets/{database-BwTfupDi.js → database-BWXSBg5a.js} +1 -1
- package/ui/dist/assets/dialog-lDg4NvMV.js +1 -0
- package/ui/dist/assets/dollar-sign-Dmijd6DP.js +1 -0
- package/ui/dist/assets/{ellipsis-UJx4Xnf6.js → ellipsis-vT-5SVIp.js} +1 -1
- package/ui/dist/assets/{ellipsis-vertical-DoULYEKn.js → ellipsis-vertical-B4tAX_bL.js} +1 -1
- package/ui/dist/assets/feature-markdown-DBo5Q7uq.js +289 -0
- package/ui/dist/assets/{file-code-B2UrJLwi.js → file-code-ChOWdKXf.js} +1 -1
- package/ui/dist/assets/{folder-open-BJi577JH.js → folder-open-CkOUbIiP.js} +1 -1
- package/ui/dist/assets/{funnel-PkaD_HSR.js → funnel-uZkoSPP-.js} +1 -1
- package/ui/dist/assets/{git-branch-C_K30am0.js → git-branch-CwvUxj7C.js} +1 -1
- package/ui/dist/assets/{git-commit-horizontal-IT6ecxWQ.js → git-commit-horizontal-lxPzCvhD.js} +1 -1
- package/ui/dist/assets/{globe-f-jjCa_g.js → globe-D_WpJdsj.js} +1 -1
- package/ui/dist/assets/{hash-DNEv8Elo.js → hash-CA0hJ7vh.js} +1 -1
- package/ui/dist/assets/{history-BQSBo_1Q.js → history-CEWMLELs.js} +1 -1
- package/ui/dist/assets/inbox-ehtOptTV.js +1 -0
- package/ui/dist/assets/{index-nS3EErDg.js → index-C1rhbml4.js} +3 -3
- package/ui/dist/assets/index-CR8gfCHv.js +1 -0
- package/ui/dist/assets/index-DVXMpWai.js +1 -0
- package/ui/dist/assets/index-DaDJMM2v.js +1 -0
- package/ui/dist/assets/index-Djdq_0xu.js +1 -0
- package/ui/dist/assets/index-M46Gcb8K.css +1 -0
- package/ui/dist/assets/index-sEBN7w8J.js +109 -0
- package/ui/dist/assets/layers-C6Mhf8Nk.js +1 -0
- package/ui/dist/assets/{layout-dashboard-CcKt1hXw.js → layout-dashboard-BRUM0kpT.js} +1 -1
- package/ui/dist/assets/{lightbulb-Jhp7FvbU.js → lightbulb-CN2q1O5l.js} +1 -1
- package/ui/dist/assets/{link-2-DdO7ostF.js → link-2-CC4E28yJ.js} +1 -1
- package/ui/dist/assets/markdown-renderer-C6ENbB0u.js +10 -0
- package/ui/dist/assets/markdown-renderer-CfVKi3_s.css +1 -0
- package/ui/dist/assets/{message-square-CBgH65X7.js → message-square-Bbb241am.js} +1 -1
- package/ui/dist/assets/package-CwT3IzFx.js +1 -0
- package/ui/dist/assets/{pencil-CYXCEPQh.js → pencil-CDmiHzoj.js} +1 -1
- package/ui/dist/assets/{play-d1ONaH0-.js → play-tjy5jMQX.js} +1 -1
- package/ui/dist/assets/popover-CFHZf3pE.js +1 -0
- package/ui/dist/assets/progress-BboUHuLf.js +1 -0
- package/ui/dist/assets/{rocket-DhvUetsk.js → rocket--8ZHgdXJ.js} +1 -1
- package/ui/dist/assets/select-n108rXny.js +1 -0
- package/ui/dist/assets/{send-P0r5D7td.js → send-Cu7MR2Qh.js} +1 -1
- package/ui/dist/assets/server-DmodxZp-.js +1 -0
- package/ui/dist/assets/{settings-2-ocj5uHGx.js → settings-2-CkRNqkxj.js} +1 -1
- package/ui/dist/assets/sheet-7aUJkyzU.js +1 -0
- package/ui/dist/assets/{shield-check-DfVdE34l.js → shield-check-ViI3nMKE.js} +1 -1
- package/ui/dist/assets/{shield-off-8l0J6R-8.js → shield-off-DN3tagkn.js} +1 -1
- package/ui/dist/assets/skeleton-C0E1Nfro.js +1 -0
- package/ui/dist/assets/{smartphone-77_g3SQn.js → smartphone-5FghiXpA.js} +1 -1
- package/ui/dist/assets/{square-Ckzsr3Rn.js → square-DiNSx8K7.js} +1 -1
- package/ui/dist/assets/star-DhdFVidS.js +1 -0
- package/ui/dist/assets/switch-CIgVbZqQ.js +1 -0
- package/ui/dist/assets/table-BC56FVMV.js +1 -0
- package/ui/dist/assets/{tag-C35mgoEv.js → tag-CH_vQliw.js} +1 -1
- package/ui/dist/assets/textarea-CxDiKFXZ.js +1 -0
- package/ui/dist/assets/{timer-DHJBS2w1.js → timer-CdGVTb60.js} +1 -1
- package/ui/dist/assets/tooltip-BuW9A5UO.js +1 -0
- package/ui/dist/assets/{trash-2-keO5tKPE.js → trash-2-CucZJL0N.js} +1 -1
- package/ui/dist/assets/trending-down-BCezC1z2.js +1 -0
- package/ui/dist/assets/{trending-up-BBv8KbrY.js → trending-up-C5OYX2k2.js} +1 -1
- package/ui/dist/assets/vendor-query-C0p7b53x.js +4 -0
- package/ui/dist/assets/vendor-react-Mc5_kfYG.js +12 -0
- package/ui/dist/assets/vendor-ui-CXPAUBFv.js +51 -0
- package/ui/dist/assets/{wand-sparkles-BusQ52ZK.js → wand-sparkles-BaH6pJnc.js} +1 -1
- package/ui/dist/assets/{wrench-02YaOSiK.js → wrench-CjxjP6ok.js} +1 -1
- package/ui/dist/index.html +5 -2
- package/ui/dist/assets/ActivityView-BeshZXmE.js +0 -1
- package/ui/dist/assets/AgentList-QVHeldPi.js +0 -1
- package/ui/dist/assets/AnalyticsDashboard-C9R97dXE.js +0 -1
- package/ui/dist/assets/AreaChart-B73gaiVQ.js +0 -1
- package/ui/dist/assets/CartesianChart-CXYL2R-A.js +0 -36
- package/ui/dist/assets/ChatView-qjZErlrp.js +0 -10
- package/ui/dist/assets/CostsDashboard-O4DCLy1i.js +0 -1
- package/ui/dist/assets/EstimateSelect-1hQf4Osk.js +0 -1
- package/ui/dist/assets/FloatingChatbot-BZBzrJxd.js +0 -2
- package/ui/dist/assets/InviteCodeManagement-LEoGqYeA.js +0 -2
- package/ui/dist/assets/ProjectIcon-Chd_ofsw.js +0 -1
- package/ui/dist/assets/ProjectList-BvUfVB6c.js +0 -4
- package/ui/dist/assets/StatusIndicator-pRfkNcdV.js +0 -1
- package/ui/dist/assets/SummaryList-B6kXiTxL.js +0 -1
- package/ui/dist/assets/TaskDetail-CV6CzbOS.js +0 -1
- package/ui/dist/assets/TaskList-DL7brQKd.js +0 -1
- package/ui/dist/assets/TicketBoard-CrkkFj7C.js +0 -1
- package/ui/dist/assets/TicketDetail-hnnYclpD.js +0 -1
- package/ui/dist/assets/TicketList-CsDAyQCL.js +0 -1
- package/ui/dist/assets/UserManagement-BGFQyTSr.js +0 -2
- package/ui/dist/assets/ViewSwitcher-CnL0OqA8.js +0 -1
- package/ui/dist/assets/alert-dialog-SKM6Gjz4.js +0 -7
- package/ui/dist/assets/badge-CVHH1a-v.js +0 -1
- package/ui/dist/assets/brain-6o_RHVpe.js +0 -1
- package/ui/dist/assets/constants-Q5Zivl-a.js +0 -1
- package/ui/dist/assets/dialog-BUqN9Psj.js +0 -1
- package/ui/dist/assets/index-BUmnwPQr.js +0 -1
- package/ui/dist/assets/index-BfF5G4Cw.js +0 -1
- package/ui/dist/assets/index-C8N375c2.js +0 -1
- package/ui/dist/assets/index-CuxnqVVz.css +0 -1
- package/ui/dist/assets/index-CzMM3QOk.js +0 -1
- package/ui/dist/assets/index-lJlNPFVJ.js +0 -67
- package/ui/dist/assets/layers-jFqVrWMD.js +0 -1
- package/ui/dist/assets/markdown-renderer-n1ToC4XQ.js +0 -36
- package/ui/dist/assets/popover-CrmZgoWu.js +0 -1
- package/ui/dist/assets/progress-uv4SPkL5.js +0 -1
- package/ui/dist/assets/select-CeQ8AHGy.js +0 -1
- package/ui/dist/assets/server-CVHTZB72.js +0 -1
- package/ui/dist/assets/sheet-DuQExSho.js +0 -1
- package/ui/dist/assets/skeleton-TFiFJ3nB.js +0 -1
- package/ui/dist/assets/switch-CAePkd6k.js +0 -1
- package/ui/dist/assets/table-ZjAZ7hkt.js +0 -1
- package/ui/dist/assets/textarea-DsVPnSgR.js +0 -1
- 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
|
-
*
|
|
126
|
+
* Launch the Ink-based TUI chat interface.
|
|
127
|
+
* Wired to the actual profClaw streaming API via SSE.
|
|
123
128
|
*/
|
|
124
|
-
async function
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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
|
|
169
|
-
|
|
170
|
-
|
|
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
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
const
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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
|
-
|
|
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
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
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
|
-
//
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
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
|
-
|
|
247
|
-
|
|
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
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
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
|
-
|
|
273
|
-
|
|
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
|