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
|
@@ -0,0 +1,1516 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interactive REPL Engine
|
|
3
|
+
*
|
|
4
|
+
* Claude-like interactive chat with streaming, tool display,
|
|
5
|
+
* slash commands, and session management.
|
|
6
|
+
*
|
|
7
|
+
* Self-contained module - depends only on Node.js builtins,
|
|
8
|
+
* chalk, ora, and the stream-client/renderer siblings.
|
|
9
|
+
*
|
|
10
|
+
* @package profclaw-interactive (future standalone)
|
|
11
|
+
*/
|
|
12
|
+
import * as readline from 'node:readline';
|
|
13
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
14
|
+
import { join } from 'node:path';
|
|
15
|
+
import { homedir } from 'node:os';
|
|
16
|
+
import chalk from 'chalk';
|
|
17
|
+
import { createRenderer, formatElapsed, formatTokens } from './renderer.js';
|
|
18
|
+
// Module-level state used by slash command handlers and the REPL loop
|
|
19
|
+
let lastAssistantResponse = '';
|
|
20
|
+
let lastUserMessage = '';
|
|
21
|
+
let pendingRetry = '';
|
|
22
|
+
let activeAbort = null;
|
|
23
|
+
let activeSpinnerInterval = null;
|
|
24
|
+
function formatRelativeTime(dateStr) {
|
|
25
|
+
const diff = Date.now() - new Date(dateStr).getTime();
|
|
26
|
+
if (diff < 60000)
|
|
27
|
+
return 'just now';
|
|
28
|
+
if (diff < 3600000)
|
|
29
|
+
return `${Math.floor(diff / 60000)}m ago`;
|
|
30
|
+
if (diff < 86400000)
|
|
31
|
+
return `${Math.floor(diff / 3600000)}h ago`;
|
|
32
|
+
return `${Math.floor(diff / 86400000)}d ago`;
|
|
33
|
+
}
|
|
34
|
+
import { streamChat, createConversation } from './stream-client.js';
|
|
35
|
+
import { ensureServer } from './auto-serve.js';
|
|
36
|
+
import { showPicker } from './picker.js';
|
|
37
|
+
// History file
|
|
38
|
+
const HISTORY_DIR = join(homedir(), '.profclaw');
|
|
39
|
+
const HISTORY_FILE = join(HISTORY_DIR, 'chat_history');
|
|
40
|
+
const SESSION_FILE = join(HISTORY_DIR, 'last_session');
|
|
41
|
+
const MAX_HISTORY = 500;
|
|
42
|
+
function loadHistory() {
|
|
43
|
+
try {
|
|
44
|
+
if (existsSync(HISTORY_FILE)) {
|
|
45
|
+
return readFileSync(HISTORY_FILE, 'utf-8').split('\n').filter(Boolean);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
catch { /* empty */ }
|
|
49
|
+
return [];
|
|
50
|
+
}
|
|
51
|
+
function saveHistory(lines) {
|
|
52
|
+
try {
|
|
53
|
+
if (!existsSync(HISTORY_DIR)) {
|
|
54
|
+
mkdirSync(HISTORY_DIR, { recursive: true });
|
|
55
|
+
}
|
|
56
|
+
writeFileSync(HISTORY_FILE, lines.slice(-MAX_HISTORY).join('\n') + '\n');
|
|
57
|
+
}
|
|
58
|
+
catch { /* empty */ }
|
|
59
|
+
}
|
|
60
|
+
function loadLastSession() {
|
|
61
|
+
try {
|
|
62
|
+
if (existsSync(SESSION_FILE)) {
|
|
63
|
+
const data = readFileSync(SESSION_FILE, 'utf-8').trim();
|
|
64
|
+
if (data)
|
|
65
|
+
return data;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
catch { /* empty */ }
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
function saveLastSession(conversationId) {
|
|
72
|
+
try {
|
|
73
|
+
if (!existsSync(HISTORY_DIR)) {
|
|
74
|
+
mkdirSync(HISTORY_DIR, { recursive: true });
|
|
75
|
+
}
|
|
76
|
+
writeFileSync(SESSION_FILE, conversationId);
|
|
77
|
+
}
|
|
78
|
+
catch { /* empty */ }
|
|
79
|
+
}
|
|
80
|
+
/** Fetch recent sessions from the server */
|
|
81
|
+
async function fetchSessions(serverUrl, limit = 10) {
|
|
82
|
+
try {
|
|
83
|
+
const response = await fetch(`${serverUrl}/api/chat/conversations/recent?limit=${limit}`, {
|
|
84
|
+
headers: { 'Accept': 'application/json' },
|
|
85
|
+
}).catch(() => null);
|
|
86
|
+
if (!response || !response.ok)
|
|
87
|
+
return [];
|
|
88
|
+
const data = await response.json();
|
|
89
|
+
return data.conversations || [];
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
return [];
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
/** Auto-generate a session title from the first message (fire-and-forget) */
|
|
96
|
+
async function autoTitleSession(serverUrl, conversationId, firstMessage) {
|
|
97
|
+
// Generate a short title from the first message (truncate to ~50 chars)
|
|
98
|
+
const title = firstMessage.length <= 50
|
|
99
|
+
? firstMessage
|
|
100
|
+
: firstMessage.slice(0, 47).replace(/\s+\S*$/, '') + '...';
|
|
101
|
+
try {
|
|
102
|
+
await fetch(`${serverUrl}/api/chat/conversations/${conversationId}`, {
|
|
103
|
+
method: 'PATCH',
|
|
104
|
+
headers: { 'Content-Type': 'application/json' },
|
|
105
|
+
body: JSON.stringify({ title }),
|
|
106
|
+
}).catch(() => null);
|
|
107
|
+
}
|
|
108
|
+
catch { /* fire-and-forget */ }
|
|
109
|
+
}
|
|
110
|
+
// Banner
|
|
111
|
+
function printBanner(state) {
|
|
112
|
+
const H = '\u2500';
|
|
113
|
+
const DOT = '\u00b7';
|
|
114
|
+
const w = Math.min((process.stdout.columns || 80) - 4, 68);
|
|
115
|
+
const gold = chalk.hex('#F6C453');
|
|
116
|
+
const border = chalk.hex('#3C414B');
|
|
117
|
+
const dim = chalk.dim;
|
|
118
|
+
console.log('');
|
|
119
|
+
console.log(' ' + border(H.repeat(w)));
|
|
120
|
+
console.log(' ' + gold.bold('profClaw'));
|
|
121
|
+
const mode = state.agenticMode ? gold('agentic') : dim('chat');
|
|
122
|
+
const model = dim(state.model || 'default');
|
|
123
|
+
const sessionInfo = state.session ? dim(`resumed ${state.session.conversationId.slice(0, 8)}`) : dim('new session');
|
|
124
|
+
console.log(` ${mode} ${dim(DOT)} ${model} ${dim(DOT)} ${sessionInfo} ${dim(DOT)} ${dim('/help')}`);
|
|
125
|
+
console.log(' ' + border(H.repeat(w)));
|
|
126
|
+
console.log('');
|
|
127
|
+
}
|
|
128
|
+
// Slash Commands
|
|
129
|
+
function buildCommands() {
|
|
130
|
+
return [
|
|
131
|
+
{
|
|
132
|
+
name: 'help',
|
|
133
|
+
aliases: ['?', 'h'],
|
|
134
|
+
description: 'Show available commands',
|
|
135
|
+
handler: (_args, _state) => {
|
|
136
|
+
const d = chalk.dim;
|
|
137
|
+
const c = chalk.cyan;
|
|
138
|
+
console.log('');
|
|
139
|
+
console.log(d(' Chat'));
|
|
140
|
+
console.log(` ${c('/model')} <name> ${d('sonnet, opus, haiku, gpt-4o, llama3.2')}`);
|
|
141
|
+
console.log(` ${c('/agentic')} ${d('toggle agentic mode (tools + multi-step)')}`);
|
|
142
|
+
console.log(` ${c('/effort')} <level> ${d('low, medium, high')}`);
|
|
143
|
+
console.log(` ${c('/thinking')} ${d('toggle thinking display')}`);
|
|
144
|
+
console.log('');
|
|
145
|
+
console.log(d(' Automation'));
|
|
146
|
+
console.log(` ${c('/cron')} [list|add] ${d('manage scheduled jobs')}`);
|
|
147
|
+
console.log(` ${c('/feeds')} [list] ${d('manage RSS feeds')}`);
|
|
148
|
+
console.log('');
|
|
149
|
+
console.log(d(' Tools'));
|
|
150
|
+
console.log(` ${c('/run')} <cmd> ${d('run shell command (!cmd shortcut)')}`);
|
|
151
|
+
console.log(` ${c('/copy')} ${d('copy last response to clipboard')}`);
|
|
152
|
+
console.log(` ${c('/save')} <file> ${d('save last response to file')}`);
|
|
153
|
+
console.log(` ${c('/retry')} [model] ${d('retry last message (optionally with different model)')}`);
|
|
154
|
+
console.log(` ${c('/status')} ${d('server & provider status')}`);
|
|
155
|
+
console.log(` ${c('/providers')} ${d('list AI providers')}`);
|
|
156
|
+
console.log(` ${c('/tools')} ${d('list available tools')}`);
|
|
157
|
+
console.log(` ${c('/skills')} [list|install|search] ${d('manage skills')}`);
|
|
158
|
+
console.log(` ${c('/whoami')} ${d('show current config')}`);
|
|
159
|
+
console.log('');
|
|
160
|
+
console.log(d(' Session'));
|
|
161
|
+
console.log(` ${c('/sessions')} ${d('list recent conversations')}`);
|
|
162
|
+
console.log(` ${c('/resume')} <id> ${d('switch to a previous session')}`);
|
|
163
|
+
console.log(` ${c('/new')} ${d('start fresh session')}`);
|
|
164
|
+
console.log(` ${c('/session')} ${d('show current session info')}`);
|
|
165
|
+
console.log(` ${c('/compact')} ${d('compact history to save tokens')}`);
|
|
166
|
+
console.log(` ${c('/clear')} ${d('clear screen')}`);
|
|
167
|
+
console.log(` ${c('/exit')} ${d('quit')}`);
|
|
168
|
+
console.log('');
|
|
169
|
+
console.log(d(' Ctrl+C clear \u00b7 2x exit \u00b7 Ctrl+D quit'));
|
|
170
|
+
console.log(d(' Alt+Enter newline \u00b7 Ctrl+L clear \u00b7 Ctrl+P sessions \u00b7 Shift+Tab thinking'));
|
|
171
|
+
console.log('');
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
name: 'exit',
|
|
176
|
+
aliases: ['quit', 'q'],
|
|
177
|
+
description: 'Exit chat',
|
|
178
|
+
handler: () => {
|
|
179
|
+
// Will be replaced with exitGracefully at runtime
|
|
180
|
+
process.exit(0);
|
|
181
|
+
},
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
name: 'clear',
|
|
185
|
+
aliases: ['cls'],
|
|
186
|
+
description: 'Clear screen',
|
|
187
|
+
handler: () => {
|
|
188
|
+
console.clear();
|
|
189
|
+
},
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
name: 'new',
|
|
193
|
+
aliases: ['reset'],
|
|
194
|
+
description: 'Start new session',
|
|
195
|
+
handler: async (_args, state) => {
|
|
196
|
+
state.session = null;
|
|
197
|
+
state.sessionTokens = 0;
|
|
198
|
+
state.sessionCost = 0;
|
|
199
|
+
state.messageCount = 0;
|
|
200
|
+
// Clear saved session so next launch also starts fresh
|
|
201
|
+
try {
|
|
202
|
+
const { unlinkSync } = require('fs');
|
|
203
|
+
unlinkSync(join(homedir(), '.profclaw', 'last_session'));
|
|
204
|
+
}
|
|
205
|
+
catch { /* ok */ }
|
|
206
|
+
console.log(chalk.dim(' New session. Next message starts fresh.'));
|
|
207
|
+
},
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
name: 'session',
|
|
211
|
+
aliases: ['sess'],
|
|
212
|
+
description: 'Show session info',
|
|
213
|
+
handler: (_args, state) => {
|
|
214
|
+
if (!state.session) {
|
|
215
|
+
console.log(chalk.dim(' No active session. Send a message to start one.'));
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
const d = chalk.dim;
|
|
219
|
+
console.log('');
|
|
220
|
+
console.log(` ${d('id')} ${chalk.cyan(state.session.conversationId.slice(0, 12))}`);
|
|
221
|
+
console.log(` ${d('mode')} ${state.agenticMode ? chalk.hex('#F6C453')('agentic') : 'chat'}`);
|
|
222
|
+
console.log(` ${d('model')} ${state.model || 'default'}`);
|
|
223
|
+
console.log(` ${d('msgs')} ${state.messageCount}`);
|
|
224
|
+
console.log(` ${d('tokens')} ${formatTokens(state.sessionTokens)}`);
|
|
225
|
+
if (state.sessionCost > 0)
|
|
226
|
+
console.log(` ${d('cost')} $${state.sessionCost.toFixed(4)}`);
|
|
227
|
+
console.log('');
|
|
228
|
+
},
|
|
229
|
+
},
|
|
230
|
+
{
|
|
231
|
+
name: 'sessions',
|
|
232
|
+
aliases: ['ls'],
|
|
233
|
+
description: 'Pick a session to resume',
|
|
234
|
+
handler: async (_args, state) => {
|
|
235
|
+
const sessions = await fetchSessions(state.serverUrl, 20);
|
|
236
|
+
if (sessions.length === 0) {
|
|
237
|
+
console.log(chalk.dim(' No sessions found.'));
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
const selected = await showPicker({
|
|
241
|
+
title: 'Sessions',
|
|
242
|
+
filterable: true,
|
|
243
|
+
pageSize: 8,
|
|
244
|
+
items: sessions.map((s) => ({
|
|
245
|
+
id: s.id,
|
|
246
|
+
label: s.title || '(untitled)',
|
|
247
|
+
description: formatRelativeTime(s.createdAt),
|
|
248
|
+
dimLabel: !s.title,
|
|
249
|
+
})),
|
|
250
|
+
});
|
|
251
|
+
if (!selected)
|
|
252
|
+
return; // Cancelled
|
|
253
|
+
const match = sessions.find((s) => s.id === selected);
|
|
254
|
+
if (!match)
|
|
255
|
+
return;
|
|
256
|
+
state.session = {
|
|
257
|
+
conversationId: match.id,
|
|
258
|
+
mode: state.agenticMode ? 'agentic' : 'chat',
|
|
259
|
+
title: match.title,
|
|
260
|
+
totalTokens: 0,
|
|
261
|
+
totalCost: 0,
|
|
262
|
+
messageCount: 0,
|
|
263
|
+
createdAt: new Date(match.createdAt),
|
|
264
|
+
};
|
|
265
|
+
state.sessionTokens = 0;
|
|
266
|
+
state.sessionCost = 0;
|
|
267
|
+
state.messageCount = 0;
|
|
268
|
+
saveLastSession(match.id);
|
|
269
|
+
console.log(chalk.dim(` Resumed: ${match.title || match.id.slice(0, 8)}`));
|
|
270
|
+
},
|
|
271
|
+
},
|
|
272
|
+
{
|
|
273
|
+
name: 'resume',
|
|
274
|
+
aliases: ['switch'],
|
|
275
|
+
description: 'Resume a session (or pick)',
|
|
276
|
+
args: '[session-id]',
|
|
277
|
+
handler: async (args, state) => {
|
|
278
|
+
if (!args[0]) {
|
|
279
|
+
// No arg - show session picker
|
|
280
|
+
const sessions = await fetchSessions(state.serverUrl, 20);
|
|
281
|
+
if (sessions.length === 0) {
|
|
282
|
+
console.log(chalk.dim(' No sessions.'));
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
const selected = await showPicker({
|
|
286
|
+
title: 'Resume Session',
|
|
287
|
+
filterable: true,
|
|
288
|
+
pageSize: 8,
|
|
289
|
+
items: sessions.map((s) => ({
|
|
290
|
+
id: s.id,
|
|
291
|
+
label: s.title || '(untitled)',
|
|
292
|
+
description: formatRelativeTime(s.createdAt),
|
|
293
|
+
dimLabel: !s.title,
|
|
294
|
+
})),
|
|
295
|
+
});
|
|
296
|
+
if (!selected)
|
|
297
|
+
return;
|
|
298
|
+
args = [selected];
|
|
299
|
+
}
|
|
300
|
+
const prefix = args[0];
|
|
301
|
+
const sessions = await fetchSessions(state.serverUrl, 50);
|
|
302
|
+
const match = sessions.find((s) => s.id.startsWith(prefix));
|
|
303
|
+
if (!match) {
|
|
304
|
+
console.log(chalk.dim(` No session matching "${prefix}".`));
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
state.session = {
|
|
308
|
+
conversationId: match.id,
|
|
309
|
+
mode: state.agenticMode ? 'agentic' : 'chat',
|
|
310
|
+
title: match.title,
|
|
311
|
+
totalTokens: 0,
|
|
312
|
+
totalCost: 0,
|
|
313
|
+
messageCount: 0,
|
|
314
|
+
createdAt: new Date(match.createdAt),
|
|
315
|
+
};
|
|
316
|
+
state.sessionTokens = 0;
|
|
317
|
+
state.sessionCost = 0;
|
|
318
|
+
state.messageCount = 0;
|
|
319
|
+
saveLastSession(match.id);
|
|
320
|
+
console.log(chalk.dim(` Resumed: ${match.title || match.id.slice(0, 8)}`));
|
|
321
|
+
},
|
|
322
|
+
},
|
|
323
|
+
{
|
|
324
|
+
name: 'compact',
|
|
325
|
+
aliases: [],
|
|
326
|
+
description: 'Compact conversation history to save tokens',
|
|
327
|
+
handler: async (_args, state) => {
|
|
328
|
+
if (!state.session) {
|
|
329
|
+
console.log(chalk.dim(' No active session.'));
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
try {
|
|
333
|
+
const response = await fetch(`${state.serverUrl}/api/chat/conversations/${state.session.conversationId}/compact`, {
|
|
334
|
+
method: 'POST',
|
|
335
|
+
headers: { 'Content-Type': 'application/json' },
|
|
336
|
+
}).catch(() => null);
|
|
337
|
+
if (response?.ok) {
|
|
338
|
+
console.log(chalk.dim(' History compacted. Older messages summarized to save tokens.'));
|
|
339
|
+
}
|
|
340
|
+
else {
|
|
341
|
+
console.log(chalk.dim(' Compact not available for this conversation.'));
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
catch {
|
|
345
|
+
console.log(chalk.dim(' Failed to compact.'));
|
|
346
|
+
}
|
|
347
|
+
},
|
|
348
|
+
},
|
|
349
|
+
{
|
|
350
|
+
name: 'model',
|
|
351
|
+
aliases: ['m'],
|
|
352
|
+
description: 'Pick or set model',
|
|
353
|
+
args: '[name]',
|
|
354
|
+
handler: async (args, state) => {
|
|
355
|
+
if (args.length > 0) {
|
|
356
|
+
state.model = args[0];
|
|
357
|
+
console.log(chalk.green(` Model: ${state.model}`));
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
const selected = await showPicker({
|
|
361
|
+
title: `Model (current: ${state.model || 'default'})`,
|
|
362
|
+
items: [
|
|
363
|
+
{ id: 'gpt4o', label: 'GPT-4o', description: 'Azure' },
|
|
364
|
+
{ id: 'gpt4o-mini', label: 'GPT-4o Mini', description: 'Azure fast' },
|
|
365
|
+
{ id: 'gpt-4.1', label: 'GPT-4.1', description: 'Azure latest' },
|
|
366
|
+
{ id: 'sonnet', label: 'Claude Sonnet', description: 'Anthropic' },
|
|
367
|
+
{ id: 'opus', label: 'Claude Opus', description: 'Anthropic' },
|
|
368
|
+
{ id: 'haiku', label: 'Claude Haiku', description: 'Anthropic fast' },
|
|
369
|
+
{ id: 'gemini-2.0-flash', label: 'Gemini Flash', description: 'Google' },
|
|
370
|
+
{ id: 'llama3.2', label: 'Llama 3.2', description: 'Ollama local' },
|
|
371
|
+
{ id: 'llama-3.3-70b', label: 'Llama 3.3 70B', description: 'Cerebras' },
|
|
372
|
+
],
|
|
373
|
+
filterable: true,
|
|
374
|
+
pageSize: 9,
|
|
375
|
+
});
|
|
376
|
+
if (selected) {
|
|
377
|
+
state.model = selected;
|
|
378
|
+
console.log(chalk.green(` Model: ${state.model}`));
|
|
379
|
+
}
|
|
380
|
+
},
|
|
381
|
+
},
|
|
382
|
+
{
|
|
383
|
+
name: 'agentic',
|
|
384
|
+
aliases: ['agent', 'a'],
|
|
385
|
+
description: 'Toggle agentic mode',
|
|
386
|
+
handler: (_args, state) => {
|
|
387
|
+
state.agenticMode = !state.agenticMode;
|
|
388
|
+
console.log(chalk.green(` Agentic mode: ${state.agenticMode ? chalk.yellow('enabled') : 'disabled'}`));
|
|
389
|
+
if (state.agenticMode) {
|
|
390
|
+
console.log(chalk.dim(' Agent has access to all tools (web search, file ops, code execution, etc.)'));
|
|
391
|
+
}
|
|
392
|
+
},
|
|
393
|
+
},
|
|
394
|
+
{
|
|
395
|
+
name: 'tools',
|
|
396
|
+
aliases: ['t'],
|
|
397
|
+
description: 'Toggle tool display',
|
|
398
|
+
handler: (_args, state) => {
|
|
399
|
+
state.showTools = !state.showTools;
|
|
400
|
+
console.log(chalk.green(` Tool display: ${state.showTools ? 'shown' : 'hidden'}`));
|
|
401
|
+
},
|
|
402
|
+
},
|
|
403
|
+
{
|
|
404
|
+
name: 'thinking',
|
|
405
|
+
aliases: ['think'],
|
|
406
|
+
description: 'Toggle thinking display',
|
|
407
|
+
handler: (_args, state) => {
|
|
408
|
+
state.showThinking = !state.showThinking;
|
|
409
|
+
console.log(chalk.green(` Thinking: ${state.showThinking ? 'shown' : 'hidden'}`));
|
|
410
|
+
},
|
|
411
|
+
},
|
|
412
|
+
{
|
|
413
|
+
name: 'usage',
|
|
414
|
+
aliases: ['u'],
|
|
415
|
+
description: 'Toggle usage display',
|
|
416
|
+
handler: (_args, state) => {
|
|
417
|
+
state.showUsage = !state.showUsage;
|
|
418
|
+
console.log(chalk.green(` Usage display: ${state.showUsage ? 'shown' : 'hidden'}`));
|
|
419
|
+
},
|
|
420
|
+
},
|
|
421
|
+
{
|
|
422
|
+
name: 'effort',
|
|
423
|
+
aliases: ['e'],
|
|
424
|
+
description: 'Set thinking effort level',
|
|
425
|
+
args: '<low|medium|high>',
|
|
426
|
+
handler: (args, state) => {
|
|
427
|
+
const level = args[0];
|
|
428
|
+
if (!level || !['low', 'medium', 'high'].includes(level)) {
|
|
429
|
+
console.log(chalk.dim(` Current effort: ${state.effort}`));
|
|
430
|
+
console.log(chalk.dim(' Options: low (fast), medium (balanced), high (thorough)'));
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
state.effort = level;
|
|
434
|
+
console.log(chalk.green(` Effort set to: ${state.effort}`));
|
|
435
|
+
},
|
|
436
|
+
},
|
|
437
|
+
{
|
|
438
|
+
name: 'status',
|
|
439
|
+
aliases: ['st'],
|
|
440
|
+
description: 'Server and provider status',
|
|
441
|
+
handler: async (_args, state) => {
|
|
442
|
+
try {
|
|
443
|
+
const [health, providers] = await Promise.all([
|
|
444
|
+
fetch(`${state.serverUrl}/health`).then((r) => r.json()).catch(() => null),
|
|
445
|
+
fetch(`${state.serverUrl}/api/chat/providers`).then((r) => r.json()).catch(() => null),
|
|
446
|
+
]);
|
|
447
|
+
const d = chalk.dim;
|
|
448
|
+
console.log('');
|
|
449
|
+
if (health) {
|
|
450
|
+
console.log(` ${d('server')} ${chalk.hex('#7DD3A5')('online')} ${d(`v${health.version}`)} ${d(`(${health.mode})`)}`);
|
|
451
|
+
}
|
|
452
|
+
else {
|
|
453
|
+
console.log(` ${d('server')} ${chalk.hex('#F97066')('offline')}`);
|
|
454
|
+
}
|
|
455
|
+
if (providers) {
|
|
456
|
+
const provList = providers.providers;
|
|
457
|
+
const defaultProv = providers.default;
|
|
458
|
+
for (const p of provList || []) {
|
|
459
|
+
const icon = p.healthy ? chalk.hex('#7DD3A5')('\u2713') : chalk.hex('#F97066')('\u2717');
|
|
460
|
+
const isDefault = p.type === defaultProv ? chalk.hex('#F6C453')(' *') : '';
|
|
461
|
+
console.log(` ${d('provider')} ${icon} ${p.type}${isDefault}`);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
console.log('');
|
|
465
|
+
}
|
|
466
|
+
catch {
|
|
467
|
+
console.log(chalk.dim(' Cannot reach server.'));
|
|
468
|
+
}
|
|
469
|
+
},
|
|
470
|
+
},
|
|
471
|
+
{
|
|
472
|
+
name: 'run',
|
|
473
|
+
aliases: ['exec', '!'],
|
|
474
|
+
description: 'Run a shell command',
|
|
475
|
+
args: '<command>',
|
|
476
|
+
handler: async (args, _state) => {
|
|
477
|
+
if (args.length === 0) {
|
|
478
|
+
console.log(chalk.dim(' Usage: /run <command> or !<command>'));
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
const cmd = args.join(' ');
|
|
482
|
+
console.log(chalk.dim(` $ ${cmd}`));
|
|
483
|
+
try {
|
|
484
|
+
const { execSync } = await import('node:child_process');
|
|
485
|
+
const output = execSync(cmd, { encoding: 'utf-8', timeout: 30000, maxBuffer: 100000 });
|
|
486
|
+
if (output.trim())
|
|
487
|
+
console.log(output.trimEnd());
|
|
488
|
+
}
|
|
489
|
+
catch (err) {
|
|
490
|
+
const e = err;
|
|
491
|
+
console.log(chalk.hex('#F97066')(` ${e.stderr?.trim() || e.message || 'Command failed'}`));
|
|
492
|
+
}
|
|
493
|
+
},
|
|
494
|
+
},
|
|
495
|
+
{
|
|
496
|
+
name: 'copy',
|
|
497
|
+
aliases: ['cp'],
|
|
498
|
+
description: 'Copy last response to clipboard',
|
|
499
|
+
handler: async (_args, _state) => {
|
|
500
|
+
// lastResponse is captured from the stream buffer
|
|
501
|
+
try {
|
|
502
|
+
const { execSync } = await import('node:child_process');
|
|
503
|
+
const text = lastAssistantResponse;
|
|
504
|
+
if (!text) {
|
|
505
|
+
console.log(chalk.dim(' No response to copy.'));
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
execSync('pbcopy', { input: text, encoding: 'utf-8' });
|
|
509
|
+
console.log(chalk.dim(' Copied to clipboard.'));
|
|
510
|
+
}
|
|
511
|
+
catch {
|
|
512
|
+
console.log(chalk.dim(' Clipboard not available (pbcopy not found).'));
|
|
513
|
+
}
|
|
514
|
+
},
|
|
515
|
+
},
|
|
516
|
+
{
|
|
517
|
+
name: 'save',
|
|
518
|
+
aliases: [],
|
|
519
|
+
description: 'Save last response to a file',
|
|
520
|
+
args: '<filename>',
|
|
521
|
+
handler: async (args, _state) => {
|
|
522
|
+
if (!args[0]) {
|
|
523
|
+
console.log(chalk.dim(' Usage: /save <filename>'));
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
if (!lastAssistantResponse) {
|
|
527
|
+
console.log(chalk.dim(' No response to save.'));
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
try {
|
|
531
|
+
writeFileSync(args[0], lastAssistantResponse, 'utf-8');
|
|
532
|
+
console.log(chalk.dim(` Saved to ${args[0]}`));
|
|
533
|
+
}
|
|
534
|
+
catch (err) {
|
|
535
|
+
console.log(chalk.dim(` Failed: ${err instanceof Error ? err.message : 'unknown'}`));
|
|
536
|
+
}
|
|
537
|
+
},
|
|
538
|
+
},
|
|
539
|
+
{
|
|
540
|
+
name: 'retry',
|
|
541
|
+
aliases: ['r'],
|
|
542
|
+
description: 'Retry last message (optionally with different model)',
|
|
543
|
+
args: '[model]',
|
|
544
|
+
handler: async (args, state) => {
|
|
545
|
+
if (!lastUserMessage) {
|
|
546
|
+
console.log(chalk.dim(' No message to retry.'));
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
// Optionally switch model for comparison
|
|
550
|
+
const prevModel = state.model;
|
|
551
|
+
if (args[0]) {
|
|
552
|
+
state.model = args[0];
|
|
553
|
+
console.log(chalk.dim(` Retrying with ${state.model} (was: ${prevModel || 'default'})`));
|
|
554
|
+
if (state.lastResponseCache) {
|
|
555
|
+
console.log(chalk.dim(` Previous (${state.lastResponseCache.model}): ${state.lastResponseCache.content.slice(0, 80)}${state.lastResponseCache.content.length > 80 ? '...' : ''}`));
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
else {
|
|
559
|
+
console.log(chalk.dim(` Retrying: ${lastUserMessage.slice(0, 50)}...`));
|
|
560
|
+
}
|
|
561
|
+
pendingRetry = lastUserMessage;
|
|
562
|
+
},
|
|
563
|
+
},
|
|
564
|
+
{
|
|
565
|
+
name: 'skills',
|
|
566
|
+
aliases: [],
|
|
567
|
+
description: 'Manage skills',
|
|
568
|
+
args: '[list|install <source>|search <query>]',
|
|
569
|
+
handler: async (args, state) => {
|
|
570
|
+
const sub = args[0] || 'list';
|
|
571
|
+
if (sub === 'list') {
|
|
572
|
+
try {
|
|
573
|
+
const response = await fetch(`${state.serverUrl}/api/skills`).catch(() => null);
|
|
574
|
+
if (!response?.ok) {
|
|
575
|
+
console.log(chalk.dim(' Cannot fetch skills.'));
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
const data = await response.json();
|
|
579
|
+
const skills = data.skills || [];
|
|
580
|
+
const d = chalk.dim;
|
|
581
|
+
console.log('');
|
|
582
|
+
console.log(` ${data.stats?.total || 0} skills loaded, ${data.stats?.eligible || 0} eligible`);
|
|
583
|
+
console.log('');
|
|
584
|
+
const bySource = new Map();
|
|
585
|
+
for (const s of skills) {
|
|
586
|
+
const src = s.source || 'bundled';
|
|
587
|
+
if (!bySource.has(src))
|
|
588
|
+
bySource.set(src, []);
|
|
589
|
+
bySource.get(src).push(s.name);
|
|
590
|
+
}
|
|
591
|
+
for (const [src, names] of bySource) {
|
|
592
|
+
console.log(d(` ${src}: ${names.slice(0, 8).join(', ')}${names.length > 8 ? ` +${names.length - 8} more` : ''}`));
|
|
593
|
+
}
|
|
594
|
+
console.log('');
|
|
595
|
+
}
|
|
596
|
+
catch {
|
|
597
|
+
console.log(chalk.dim(' Cannot fetch skills.'));
|
|
598
|
+
}
|
|
599
|
+
return;
|
|
600
|
+
}
|
|
601
|
+
if (sub === 'install') {
|
|
602
|
+
const source = args.slice(1).join(' ');
|
|
603
|
+
if (!source) {
|
|
604
|
+
console.log(chalk.dim(' Usage: /skills install <github-user/repo> or <url>'));
|
|
605
|
+
console.log(chalk.dim(' Example: /skills install blader/humanizer'));
|
|
606
|
+
return;
|
|
607
|
+
}
|
|
608
|
+
console.log(chalk.dim(` Installing skill from ${source}...`));
|
|
609
|
+
try {
|
|
610
|
+
const response = await fetch(`${state.serverUrl}/api/skills/marketplace/install`, {
|
|
611
|
+
method: 'POST',
|
|
612
|
+
headers: { 'Content-Type': 'application/json' },
|
|
613
|
+
body: JSON.stringify({ source }),
|
|
614
|
+
}).catch(() => null);
|
|
615
|
+
if (!response?.ok) {
|
|
616
|
+
console.log(chalk.hex('#F97066')(' Install failed. Check the source URL.'));
|
|
617
|
+
return;
|
|
618
|
+
}
|
|
619
|
+
const data = await response.json();
|
|
620
|
+
console.log(chalk.hex('#7DD3A5')(` Installed: ${data.data?.skillName || source} v${data.data?.version || '?'}`));
|
|
621
|
+
}
|
|
622
|
+
catch {
|
|
623
|
+
console.log(chalk.hex('#F97066')(' Install failed.'));
|
|
624
|
+
}
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
627
|
+
if (sub === 'search') {
|
|
628
|
+
const query = args.slice(1).join(' ');
|
|
629
|
+
if (!query) {
|
|
630
|
+
console.log(chalk.dim(' Usage: /skills search <query>'));
|
|
631
|
+
return;
|
|
632
|
+
}
|
|
633
|
+
console.log(chalk.dim(` Searching ClawHub for "${query}"...`));
|
|
634
|
+
try {
|
|
635
|
+
const response = await fetch(`${state.serverUrl}/api/skills/marketplace/search?q=${encodeURIComponent(query)}`).catch(() => null);
|
|
636
|
+
if (!response?.ok) {
|
|
637
|
+
console.log(chalk.dim(' No results or registry unavailable.'));
|
|
638
|
+
return;
|
|
639
|
+
}
|
|
640
|
+
const data = await response.json();
|
|
641
|
+
const results = data.data || [];
|
|
642
|
+
if (results.length === 0) {
|
|
643
|
+
console.log(chalk.dim(' No skills found.'));
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
646
|
+
console.log('');
|
|
647
|
+
for (const r of results.slice(0, 10)) {
|
|
648
|
+
console.log(` ${chalk.cyan(r.name)} ${chalk.dim(r.description || '')} ${chalk.dim(`(${r.installs || 0} installs)`)}`);
|
|
649
|
+
}
|
|
650
|
+
console.log('');
|
|
651
|
+
console.log(chalk.dim(' Install: /skills install <name>'));
|
|
652
|
+
}
|
|
653
|
+
catch {
|
|
654
|
+
console.log(chalk.dim(' Search failed.'));
|
|
655
|
+
}
|
|
656
|
+
return;
|
|
657
|
+
}
|
|
658
|
+
console.log(chalk.dim(' Usage: /skills [list|install <source>|search <query>]'));
|
|
659
|
+
},
|
|
660
|
+
},
|
|
661
|
+
{
|
|
662
|
+
name: 'tools',
|
|
663
|
+
aliases: [],
|
|
664
|
+
description: 'List available tools',
|
|
665
|
+
handler: async (_args, state) => {
|
|
666
|
+
try {
|
|
667
|
+
const response = await fetch(`${state.serverUrl}/api/tools`).catch(() => null);
|
|
668
|
+
if (!response?.ok) {
|
|
669
|
+
console.log(chalk.dim(' Cannot fetch tools.'));
|
|
670
|
+
return;
|
|
671
|
+
}
|
|
672
|
+
const data = await response.json();
|
|
673
|
+
const tools = data.tools || [];
|
|
674
|
+
const byCategory = new Map();
|
|
675
|
+
for (const t of tools) {
|
|
676
|
+
const cat = t.category || 'other';
|
|
677
|
+
if (!byCategory.has(cat))
|
|
678
|
+
byCategory.set(cat, []);
|
|
679
|
+
byCategory.get(cat).push(t.name);
|
|
680
|
+
}
|
|
681
|
+
console.log('');
|
|
682
|
+
for (const [cat, names] of byCategory) {
|
|
683
|
+
console.log(chalk.dim(` ${cat}: ${names.join(', ')}`));
|
|
684
|
+
}
|
|
685
|
+
console.log(chalk.dim(`\n ${tools.length} tools total`));
|
|
686
|
+
console.log('');
|
|
687
|
+
}
|
|
688
|
+
catch {
|
|
689
|
+
console.log(chalk.dim(' Cannot fetch tools.'));
|
|
690
|
+
}
|
|
691
|
+
},
|
|
692
|
+
},
|
|
693
|
+
{
|
|
694
|
+
name: 'providers',
|
|
695
|
+
aliases: ['p'],
|
|
696
|
+
description: 'List AI providers',
|
|
697
|
+
handler: async (_args, state) => {
|
|
698
|
+
try {
|
|
699
|
+
const response = await fetch(`${state.serverUrl}/api/chat/providers`).catch(() => null);
|
|
700
|
+
if (!response?.ok) {
|
|
701
|
+
console.log(chalk.dim(' Cannot fetch providers.'));
|
|
702
|
+
return;
|
|
703
|
+
}
|
|
704
|
+
const data = await response.json();
|
|
705
|
+
console.log('');
|
|
706
|
+
for (const p of data.providers) {
|
|
707
|
+
const icon = p.healthy ? chalk.hex('#7DD3A5')('\u2713') : chalk.hex('#F97066')('\u2717');
|
|
708
|
+
const def = p.type === data.default ? chalk.hex('#F6C453')(' (default)') : '';
|
|
709
|
+
console.log(` ${icon} ${p.type}${def}`);
|
|
710
|
+
}
|
|
711
|
+
console.log('');
|
|
712
|
+
}
|
|
713
|
+
catch {
|
|
714
|
+
console.log(chalk.dim(' Cannot fetch providers.'));
|
|
715
|
+
}
|
|
716
|
+
},
|
|
717
|
+
},
|
|
718
|
+
{
|
|
719
|
+
name: 'whoami',
|
|
720
|
+
aliases: [],
|
|
721
|
+
description: 'Show current config',
|
|
722
|
+
handler: (_args, state) => {
|
|
723
|
+
const d = chalk.dim;
|
|
724
|
+
console.log('');
|
|
725
|
+
console.log(` ${d('mode')} ${state.agenticMode ? chalk.hex('#F6C453')('agentic') : 'chat'}`);
|
|
726
|
+
console.log(` ${d('model')} ${state.model || 'default'}`);
|
|
727
|
+
console.log(` ${d('effort')} ${state.effort}`);
|
|
728
|
+
console.log(` ${d('thinking')} ${state.showThinking ? 'shown' : 'hidden'}`);
|
|
729
|
+
console.log(` ${d('tools')} ${state.showTools ? 'shown' : 'hidden'}`);
|
|
730
|
+
console.log(` ${d('usage')} ${state.showUsage ? 'shown' : 'hidden'}`);
|
|
731
|
+
console.log(` ${d('server')} ${state.serverUrl}`);
|
|
732
|
+
if (state.session) {
|
|
733
|
+
console.log(` ${d('session')} ${state.session.conversationId.slice(0, 12)}`);
|
|
734
|
+
console.log(` ${d('messages')} ${state.messageCount}`);
|
|
735
|
+
console.log(` ${d('tokens')} ${formatTokens(state.sessionTokens)}`);
|
|
736
|
+
}
|
|
737
|
+
console.log('');
|
|
738
|
+
},
|
|
739
|
+
},
|
|
740
|
+
{
|
|
741
|
+
name: 'plugins',
|
|
742
|
+
aliases: ['plugin'],
|
|
743
|
+
description: 'Manage plugins',
|
|
744
|
+
args: '[list|load <path>|unload <id>]',
|
|
745
|
+
handler: async (args, state) => {
|
|
746
|
+
const sub = args[0] || 'list';
|
|
747
|
+
if (sub === 'list') {
|
|
748
|
+
try {
|
|
749
|
+
const response = await fetch(`${state.serverUrl}/api/plugins`).catch(() => null);
|
|
750
|
+
if (!response?.ok) {
|
|
751
|
+
console.log(chalk.dim(' No plugins loaded. Plugins require pro mode.'));
|
|
752
|
+
return;
|
|
753
|
+
}
|
|
754
|
+
const data = await response.json();
|
|
755
|
+
const plugins = data.plugins || [];
|
|
756
|
+
if (plugins.length === 0) {
|
|
757
|
+
console.log(chalk.dim(' No plugins loaded.'));
|
|
758
|
+
console.log(chalk.dim(' Install: ~/.profclaw/plugins/<name>/index.js'));
|
|
759
|
+
return;
|
|
760
|
+
}
|
|
761
|
+
console.log('');
|
|
762
|
+
for (const p of plugins) {
|
|
763
|
+
console.log(` ${chalk.cyan(p.name)} v${p.version} ${chalk.dim(`(${p.tools} tools)`)}`);
|
|
764
|
+
}
|
|
765
|
+
console.log('');
|
|
766
|
+
}
|
|
767
|
+
catch {
|
|
768
|
+
console.log(chalk.dim(' Cannot fetch plugins.'));
|
|
769
|
+
}
|
|
770
|
+
return;
|
|
771
|
+
}
|
|
772
|
+
console.log(chalk.dim(' Usage: /plugins [list|load <path>|unload <id>]'));
|
|
773
|
+
},
|
|
774
|
+
},
|
|
775
|
+
{
|
|
776
|
+
name: 'cron',
|
|
777
|
+
aliases: [],
|
|
778
|
+
description: 'List or manage scheduled jobs',
|
|
779
|
+
args: '[list|add|trigger <id>]',
|
|
780
|
+
handler: async (args, state) => {
|
|
781
|
+
const sub = args[0] || 'list';
|
|
782
|
+
if (sub === 'list') {
|
|
783
|
+
try {
|
|
784
|
+
const response = await fetch(`${state.serverUrl}/api/cron`, {
|
|
785
|
+
headers: { 'Accept': 'application/json' },
|
|
786
|
+
}).catch(() => null);
|
|
787
|
+
if (!response || !response.ok) {
|
|
788
|
+
console.log(chalk.dim(' Could not fetch cron jobs. Is the server running?'));
|
|
789
|
+
return;
|
|
790
|
+
}
|
|
791
|
+
const data = await response.json();
|
|
792
|
+
const jobs = data.jobs || [];
|
|
793
|
+
if (jobs.length === 0) {
|
|
794
|
+
console.log(chalk.dim(' No cron jobs configured.'));
|
|
795
|
+
console.log(chalk.dim(' Create one in chat: "Set up a daily AI news digest at 7am and send to my Telegram"'));
|
|
796
|
+
return;
|
|
797
|
+
}
|
|
798
|
+
console.log('');
|
|
799
|
+
console.log(chalk.bold(` ${jobs.length} Cron Jobs:`));
|
|
800
|
+
for (const job of jobs) {
|
|
801
|
+
const statusIcon = job.status === 'active' ? chalk.green('active') : chalk.yellow(job.status);
|
|
802
|
+
const schedule = job.cronExpression || 'event-driven';
|
|
803
|
+
console.log(` ${chalk.cyan(job.name)} (${job.jobType}) - ${schedule} [${statusIcon}]`);
|
|
804
|
+
}
|
|
805
|
+
console.log('');
|
|
806
|
+
}
|
|
807
|
+
catch {
|
|
808
|
+
console.log(chalk.dim(' Could not connect to server.'));
|
|
809
|
+
}
|
|
810
|
+
return;
|
|
811
|
+
}
|
|
812
|
+
if (sub === 'add') {
|
|
813
|
+
console.log(chalk.dim(' To create a cron job, describe what you want in the chat:'));
|
|
814
|
+
console.log(chalk.dim(' Example: "Every weekday at 7am, search for AI news and send a summary to my Telegram"'));
|
|
815
|
+
console.log(chalk.dim(' The agent will use the cron_create tool to set it up.'));
|
|
816
|
+
return;
|
|
817
|
+
}
|
|
818
|
+
console.log(chalk.dim(' Usage: /cron [list|add]'));
|
|
819
|
+
},
|
|
820
|
+
},
|
|
821
|
+
{
|
|
822
|
+
name: 'feeds',
|
|
823
|
+
aliases: ['feed'],
|
|
824
|
+
description: 'Manage RSS feeds',
|
|
825
|
+
args: '[list|add <url>|bundles|poll]',
|
|
826
|
+
handler: async (args, state) => {
|
|
827
|
+
const sub = args[0] || 'list';
|
|
828
|
+
if (sub === 'list') {
|
|
829
|
+
try {
|
|
830
|
+
const response = await fetch(`${state.serverUrl}/api/feeds`, {
|
|
831
|
+
headers: { 'Accept': 'application/json' },
|
|
832
|
+
}).catch(() => null);
|
|
833
|
+
if (!response || !response.ok) {
|
|
834
|
+
console.log(chalk.dim(' Could not fetch feeds. Is the server running?'));
|
|
835
|
+
return;
|
|
836
|
+
}
|
|
837
|
+
const data = await response.json();
|
|
838
|
+
const feeds = data.data || [];
|
|
839
|
+
if (feeds.length === 0) {
|
|
840
|
+
console.log(chalk.dim(' No feeds configured. Install a bundle:'));
|
|
841
|
+
console.log(chalk.dim(' /feeds bundles - List available bundles'));
|
|
842
|
+
console.log(chalk.dim(' Or in chat: "Add the AI news feed bundle"'));
|
|
843
|
+
return;
|
|
844
|
+
}
|
|
845
|
+
console.log('');
|
|
846
|
+
console.log(chalk.bold(` ${feeds.length} Feeds:`));
|
|
847
|
+
for (const feed of feeds) {
|
|
848
|
+
const status = feed.enabled ? chalk.green('on') : chalk.dim('off');
|
|
849
|
+
console.log(` ${chalk.cyan(feed.name)} (${feed.category}) - ${feed.articleCount} articles [${status}]`);
|
|
850
|
+
}
|
|
851
|
+
console.log('');
|
|
852
|
+
}
|
|
853
|
+
catch {
|
|
854
|
+
console.log(chalk.dim(' Could not connect to server.'));
|
|
855
|
+
}
|
|
856
|
+
return;
|
|
857
|
+
}
|
|
858
|
+
if (sub === 'bundles') {
|
|
859
|
+
console.log('');
|
|
860
|
+
console.log(chalk.bold(' Available Feed Bundles:'));
|
|
861
|
+
console.log(` ${chalk.cyan('ai-news')} - OpenAI, Anthropic, HuggingFace, TechCrunch AI, MIT Tech Review`);
|
|
862
|
+
console.log(` ${chalk.cyan('dev-news')} - Hacker News, Lobsters, Dev.to`);
|
|
863
|
+
console.log(` ${chalk.cyan('security')} - Krebs, The Hacker News, Schneier`);
|
|
864
|
+
console.log('');
|
|
865
|
+
console.log(chalk.dim(' Install in chat: "Install the ai-news feed bundle"'));
|
|
866
|
+
return;
|
|
867
|
+
}
|
|
868
|
+
if (sub === 'poll') {
|
|
869
|
+
console.log(chalk.dim(' In chat: "Poll all feeds for new articles"'));
|
|
870
|
+
return;
|
|
871
|
+
}
|
|
872
|
+
if (sub === 'add') {
|
|
873
|
+
const url = args[1];
|
|
874
|
+
if (!url) {
|
|
875
|
+
console.log(chalk.dim(' Usage: /feeds add <url>'));
|
|
876
|
+
console.log(chalk.dim(' Or in chat: "Add this RSS feed: https://example.com/feed.xml"'));
|
|
877
|
+
return;
|
|
878
|
+
}
|
|
879
|
+
console.log(chalk.dim(` In chat: "Add an RSS feed from ${url}"`));
|
|
880
|
+
return;
|
|
881
|
+
}
|
|
882
|
+
console.log(chalk.dim(' Usage: /feeds [list|add <url>|bundles|poll]'));
|
|
883
|
+
},
|
|
884
|
+
},
|
|
885
|
+
];
|
|
886
|
+
}
|
|
887
|
+
// Main REPL
|
|
888
|
+
export async function startInteractiveREPL(options) {
|
|
889
|
+
const render = createRenderer();
|
|
890
|
+
const commands = buildCommands();
|
|
891
|
+
const history = loadHistory();
|
|
892
|
+
// Multiline input state
|
|
893
|
+
let multilineBuffer = [];
|
|
894
|
+
let isMultiline = false;
|
|
895
|
+
// Initialize state
|
|
896
|
+
const state = {
|
|
897
|
+
session: null,
|
|
898
|
+
model: options.model,
|
|
899
|
+
provider: options.provider,
|
|
900
|
+
agenticMode: options.agentic ?? true, // Always agentic by default - that's the point
|
|
901
|
+
showThinking: options.showThinking ?? false,
|
|
902
|
+
showTools: true,
|
|
903
|
+
showUsage: true,
|
|
904
|
+
effort: options.effort || 'medium',
|
|
905
|
+
isStreaming: false,
|
|
906
|
+
sessionTokens: 0,
|
|
907
|
+
sessionCost: 0,
|
|
908
|
+
messageCount: 0,
|
|
909
|
+
serverUrl: options.server.baseUrl,
|
|
910
|
+
};
|
|
911
|
+
// Resume session: explicit > last saved > new
|
|
912
|
+
const sessionToResume = options.sessionId || loadLastSession();
|
|
913
|
+
if (sessionToResume) {
|
|
914
|
+
state.session = {
|
|
915
|
+
conversationId: sessionToResume,
|
|
916
|
+
mode: state.agenticMode ? 'agentic' : 'chat',
|
|
917
|
+
totalTokens: 0,
|
|
918
|
+
totalCost: 0,
|
|
919
|
+
messageCount: 0,
|
|
920
|
+
createdAt: new Date(),
|
|
921
|
+
};
|
|
922
|
+
}
|
|
923
|
+
// Ensure server is running (auto-start if needed)
|
|
924
|
+
const serverCheck = await ensureServer(options.server.baseUrl, { autoStart: true });
|
|
925
|
+
if (!serverCheck.ok) {
|
|
926
|
+
console.log(chalk.red(' ' + (serverCheck.error || 'Cannot connect to server')));
|
|
927
|
+
console.log('');
|
|
928
|
+
console.log(chalk.dim(' To start manually:'));
|
|
929
|
+
console.log(chalk.dim(' pnpm dev # Development mode (with hot reload)'));
|
|
930
|
+
console.log(chalk.dim(' pnpm start # Production mode'));
|
|
931
|
+
console.log('');
|
|
932
|
+
process.exit(1);
|
|
933
|
+
}
|
|
934
|
+
// State variables for /copy, /save, /retry are at module level
|
|
935
|
+
// Dynamic prompt builder - shows session context inline
|
|
936
|
+
const updatePrompt = () => {
|
|
937
|
+
const parts = [];
|
|
938
|
+
if (state.model)
|
|
939
|
+
parts.push(state.model);
|
|
940
|
+
if (state.sessionTokens > 0)
|
|
941
|
+
parts.push(formatTokens(state.sessionTokens));
|
|
942
|
+
const promptStr = parts.length > 0
|
|
943
|
+
? chalk.dim(`${parts.join(' \u00b7 ')} > `)
|
|
944
|
+
: chalk.dim('> ');
|
|
945
|
+
rl.setPrompt(promptStr);
|
|
946
|
+
};
|
|
947
|
+
printBanner(state);
|
|
948
|
+
// Load and show recent history for resumed sessions
|
|
949
|
+
if (state.session) {
|
|
950
|
+
try {
|
|
951
|
+
const response = await fetch(`${options.server.baseUrl}/api/chat/conversations/${state.session.conversationId}/messages?limit=6`).catch(() => null);
|
|
952
|
+
if (response?.ok) {
|
|
953
|
+
const data = await response.json();
|
|
954
|
+
const msgs = data.messages || [];
|
|
955
|
+
if (msgs.length > 0) {
|
|
956
|
+
console.log(chalk.dim(' Recent history:'));
|
|
957
|
+
for (const m of msgs.slice(-6)) {
|
|
958
|
+
const role = m.role === 'user' ? chalk.dim(' you: ') : chalk.dim(' ai: ');
|
|
959
|
+
const preview = m.content.slice(0, 60).replace(/\n/g, ' ');
|
|
960
|
+
console.log(`${role}${chalk.dim(preview)}${m.content.length > 60 ? chalk.dim('...') : ''}`);
|
|
961
|
+
}
|
|
962
|
+
console.log('');
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
catch { /* skip history load errors */ }
|
|
967
|
+
}
|
|
968
|
+
// Keep stdin flowing so readline doesn't auto-close after async handlers
|
|
969
|
+
if (process.stdin.isPaused()) {
|
|
970
|
+
process.stdin.resume();
|
|
971
|
+
}
|
|
972
|
+
// Build slash command completions with descriptions
|
|
973
|
+
const commandEntries = commands.map((c) => ({
|
|
974
|
+
cmd: `/${c.name}`,
|
|
975
|
+
desc: c.description,
|
|
976
|
+
args: c.args || '',
|
|
977
|
+
}));
|
|
978
|
+
// Tab completer - shows formatted list with descriptions
|
|
979
|
+
const completer = (line) => {
|
|
980
|
+
if (line.startsWith('/')) {
|
|
981
|
+
const prefix = line.toLowerCase();
|
|
982
|
+
const matches = commandEntries.filter((e) => e.cmd.startsWith(prefix));
|
|
983
|
+
if (matches.length === 0) {
|
|
984
|
+
return [commandEntries.map((e) => e.cmd), line];
|
|
985
|
+
}
|
|
986
|
+
if (matches.length === 1) {
|
|
987
|
+
// Exact single match - auto-complete it
|
|
988
|
+
const suffix = matches[0].args ? ' ' : '';
|
|
989
|
+
return [[matches[0].cmd + suffix], line];
|
|
990
|
+
}
|
|
991
|
+
// Multiple matches - show with descriptions
|
|
992
|
+
// Readline displays these as the "options" list
|
|
993
|
+
const display = matches.map((e) => {
|
|
994
|
+
const padded = e.cmd.padEnd(16);
|
|
995
|
+
return `${padded}${e.desc}`;
|
|
996
|
+
});
|
|
997
|
+
// Return just the command names for actual completion
|
|
998
|
+
return [display, line];
|
|
999
|
+
}
|
|
1000
|
+
return [[], line];
|
|
1001
|
+
};
|
|
1002
|
+
// Create readline interface
|
|
1003
|
+
const rl = readline.createInterface({
|
|
1004
|
+
input: process.stdin,
|
|
1005
|
+
output: process.stdout,
|
|
1006
|
+
prompt: chalk.dim('> '),
|
|
1007
|
+
history,
|
|
1008
|
+
historySize: MAX_HISTORY,
|
|
1009
|
+
removeHistoryDuplicates: true,
|
|
1010
|
+
terminal: true,
|
|
1011
|
+
completer,
|
|
1012
|
+
});
|
|
1013
|
+
// Keybindings via keypress events
|
|
1014
|
+
if (process.stdin.isTTY) {
|
|
1015
|
+
let pasteBuffer = '';
|
|
1016
|
+
let pasteTimer = null;
|
|
1017
|
+
process.stdin.on('keypress', (_ch, key) => {
|
|
1018
|
+
if (!key)
|
|
1019
|
+
return;
|
|
1020
|
+
// Alt+Enter: multiline input
|
|
1021
|
+
if (key.name === 'return' && key.meta) {
|
|
1022
|
+
multilineBuffer.push(rl.line);
|
|
1023
|
+
rl.line = '';
|
|
1024
|
+
rl.cursor = 0;
|
|
1025
|
+
process.stdout.write('\n' + chalk.dim(' ... '));
|
|
1026
|
+
isMultiline = true;
|
|
1027
|
+
return;
|
|
1028
|
+
}
|
|
1029
|
+
// Ctrl+L: clear screen (keep prompt)
|
|
1030
|
+
if (key.ctrl && key.name === 'l') {
|
|
1031
|
+
console.clear();
|
|
1032
|
+
printBanner(state);
|
|
1033
|
+
rl.prompt(true);
|
|
1034
|
+
return;
|
|
1035
|
+
}
|
|
1036
|
+
// Ctrl+P: session picker
|
|
1037
|
+
if (key.ctrl && key.name === 'p') {
|
|
1038
|
+
if (!state.isStreaming) {
|
|
1039
|
+
const sessCmd = commands.find((c) => c.name === 'sessions');
|
|
1040
|
+
if (sessCmd) {
|
|
1041
|
+
const result = sessCmd.handler([], state);
|
|
1042
|
+
if (result instanceof Promise)
|
|
1043
|
+
result.then(() => { updatePrompt(); rl.prompt(); });
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
return;
|
|
1047
|
+
}
|
|
1048
|
+
// Shift+Tab: toggle thinking display
|
|
1049
|
+
if (key.shift && key.name === 'tab') {
|
|
1050
|
+
state.showThinking = !state.showThinking;
|
|
1051
|
+
process.stdout.write(`\r\x1b[K${chalk.dim(` Thinking: ${state.showThinking ? 'shown' : 'hidden'}`)}\n`);
|
|
1052
|
+
rl.prompt(true);
|
|
1053
|
+
return;
|
|
1054
|
+
}
|
|
1055
|
+
// Paste detection: rapid character input (burst coalescer)
|
|
1056
|
+
if (!key.ctrl && !key.meta && _ch && _ch.length === 1 && _ch >= ' ') {
|
|
1057
|
+
pasteBuffer += _ch;
|
|
1058
|
+
if (pasteTimer)
|
|
1059
|
+
clearTimeout(pasteTimer);
|
|
1060
|
+
pasteTimer = setTimeout(() => {
|
|
1061
|
+
// If we got 5+ chars in a burst, it was a paste
|
|
1062
|
+
if (pasteBuffer.length >= 5 && pasteBuffer.includes('\n')) {
|
|
1063
|
+
// Convert pasted newlines to multiline mode
|
|
1064
|
+
const lines = pasteBuffer.split('\n');
|
|
1065
|
+
if (lines.length > 1) {
|
|
1066
|
+
// First line is already in readline, add rest to multiline buffer
|
|
1067
|
+
for (let i = 0; i < lines.length - 1; i++) {
|
|
1068
|
+
multilineBuffer.push(lines[i]);
|
|
1069
|
+
}
|
|
1070
|
+
isMultiline = true;
|
|
1071
|
+
process.stdout.write(`\n${chalk.dim(` ... (${lines.length} lines pasted)`)}\n`);
|
|
1072
|
+
const lastLine = lines[lines.length - 1];
|
|
1073
|
+
rl.line = lastLine;
|
|
1074
|
+
rl.cursor = lastLine.length;
|
|
1075
|
+
rl.prompt(true);
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
pasteBuffer = '';
|
|
1079
|
+
}, 10); // 10ms window for paste detection
|
|
1080
|
+
}
|
|
1081
|
+
});
|
|
1082
|
+
}
|
|
1083
|
+
// Track Ctrl+C for double-tap exit
|
|
1084
|
+
let lastCtrlC = 0;
|
|
1085
|
+
rl.on('SIGINT', () => {
|
|
1086
|
+
const now = Date.now();
|
|
1087
|
+
if (state.isStreaming) {
|
|
1088
|
+
// Cancel stream, spinner, and fetch
|
|
1089
|
+
if (activeAbort) {
|
|
1090
|
+
activeAbort.abort();
|
|
1091
|
+
activeAbort = null;
|
|
1092
|
+
}
|
|
1093
|
+
if (activeSpinnerInterval) {
|
|
1094
|
+
clearInterval(activeSpinnerInterval);
|
|
1095
|
+
activeSpinnerInterval = null;
|
|
1096
|
+
}
|
|
1097
|
+
process.stdout.write('\r\x1b[K'); // Clear spinner line
|
|
1098
|
+
state.isStreaming = false;
|
|
1099
|
+
console.log(chalk.dim(' Cancelled.'));
|
|
1100
|
+
rl.prompt();
|
|
1101
|
+
return;
|
|
1102
|
+
}
|
|
1103
|
+
// Clear multiline state
|
|
1104
|
+
if (isMultiline) {
|
|
1105
|
+
multilineBuffer = [];
|
|
1106
|
+
isMultiline = false;
|
|
1107
|
+
}
|
|
1108
|
+
if (now - lastCtrlC < 1500) {
|
|
1109
|
+
exitGracefully();
|
|
1110
|
+
}
|
|
1111
|
+
lastCtrlC = now;
|
|
1112
|
+
console.log(chalk.dim('\n Ctrl+C again to exit'));
|
|
1113
|
+
rl.prompt();
|
|
1114
|
+
});
|
|
1115
|
+
// Handle Ctrl+D / stream close
|
|
1116
|
+
// Only exit if the user explicitly closed (not if readline auto-closes after async)
|
|
1117
|
+
let userRequestedExit = false;
|
|
1118
|
+
rl.on('close', () => {
|
|
1119
|
+
if (!userRequestedExit) {
|
|
1120
|
+
// Readline closed unexpectedly (e.g., after async handler) - reopen
|
|
1121
|
+
// This happens in Node.js when async handlers are used with readline
|
|
1122
|
+
return;
|
|
1123
|
+
}
|
|
1124
|
+
saveHistory(history);
|
|
1125
|
+
console.log('');
|
|
1126
|
+
process.exit(0);
|
|
1127
|
+
});
|
|
1128
|
+
// Graceful exit helper
|
|
1129
|
+
const exitGracefully = () => {
|
|
1130
|
+
userRequestedExit = true;
|
|
1131
|
+
saveHistory(history);
|
|
1132
|
+
console.log('');
|
|
1133
|
+
rl.close();
|
|
1134
|
+
process.exit(0);
|
|
1135
|
+
};
|
|
1136
|
+
// Override exit command to use graceful exit
|
|
1137
|
+
const exitCmd = commands.find((c) => c.name === 'exit');
|
|
1138
|
+
if (exitCmd) {
|
|
1139
|
+
exitCmd.handler = () => exitGracefully();
|
|
1140
|
+
}
|
|
1141
|
+
// Handle input
|
|
1142
|
+
rl.on('line', async (line) => {
|
|
1143
|
+
let input;
|
|
1144
|
+
if (isMultiline) {
|
|
1145
|
+
multilineBuffer.push(line);
|
|
1146
|
+
input = multilineBuffer.join('\n').trim();
|
|
1147
|
+
multilineBuffer = [];
|
|
1148
|
+
isMultiline = false;
|
|
1149
|
+
}
|
|
1150
|
+
else {
|
|
1151
|
+
input = line.trim();
|
|
1152
|
+
}
|
|
1153
|
+
if (!input) {
|
|
1154
|
+
rl.prompt();
|
|
1155
|
+
return;
|
|
1156
|
+
}
|
|
1157
|
+
// Save to history
|
|
1158
|
+
history.push(input);
|
|
1159
|
+
// Handle slash commands
|
|
1160
|
+
if (input.startsWith('/')) {
|
|
1161
|
+
const [cmdName, ...args] = input.slice(1).split(/\s+/);
|
|
1162
|
+
const cmd = commands.find((c) => c.name === cmdName.toLowerCase() || c.aliases.includes(cmdName.toLowerCase()));
|
|
1163
|
+
if (cmd) {
|
|
1164
|
+
await cmd.handler(args, state);
|
|
1165
|
+
}
|
|
1166
|
+
else {
|
|
1167
|
+
render.error(`Unknown command: /${cmdName}. Type /help for commands.`);
|
|
1168
|
+
}
|
|
1169
|
+
updatePrompt();
|
|
1170
|
+
rl.prompt();
|
|
1171
|
+
return;
|
|
1172
|
+
}
|
|
1173
|
+
// Handle !command shortcut for shell
|
|
1174
|
+
if (input.startsWith('!')) {
|
|
1175
|
+
const shellCmd = input.slice(1).trim();
|
|
1176
|
+
if (shellCmd) {
|
|
1177
|
+
const runCmd = commands.find((c) => c.name === 'run');
|
|
1178
|
+
if (runCmd)
|
|
1179
|
+
await runCmd.handler(shellCmd.split(/\s+/), state);
|
|
1180
|
+
}
|
|
1181
|
+
rl.prompt();
|
|
1182
|
+
return;
|
|
1183
|
+
}
|
|
1184
|
+
// Send message
|
|
1185
|
+
lastUserMessage = input;
|
|
1186
|
+
const messageStartTime = Date.now();
|
|
1187
|
+
const response = await handleMessage(input, state, options, render, rl, history);
|
|
1188
|
+
if (response) {
|
|
1189
|
+
lastAssistantResponse = response;
|
|
1190
|
+
// Cache for /retry comparison
|
|
1191
|
+
state.lastResponseCache = {
|
|
1192
|
+
content: response,
|
|
1193
|
+
model: state.model || 'default',
|
|
1194
|
+
durationMs: Date.now() - messageStartTime,
|
|
1195
|
+
};
|
|
1196
|
+
}
|
|
1197
|
+
// Auto-generate session title after first message
|
|
1198
|
+
if (state.messageCount === 1 && state.session && !state.session.title) {
|
|
1199
|
+
autoTitleSession(state.serverUrl, state.session.conversationId, input).catch(() => { });
|
|
1200
|
+
}
|
|
1201
|
+
// Handle pending retry
|
|
1202
|
+
if (pendingRetry) {
|
|
1203
|
+
const retryMsg = pendingRetry;
|
|
1204
|
+
pendingRetry = '';
|
|
1205
|
+
lastUserMessage = retryMsg;
|
|
1206
|
+
const retryResponse = await handleMessage(retryMsg, state, options, render, rl, history);
|
|
1207
|
+
if (retryResponse)
|
|
1208
|
+
lastAssistantResponse = retryResponse;
|
|
1209
|
+
}
|
|
1210
|
+
updatePrompt();
|
|
1211
|
+
rl.prompt();
|
|
1212
|
+
});
|
|
1213
|
+
// Start
|
|
1214
|
+
updatePrompt();
|
|
1215
|
+
rl.prompt();
|
|
1216
|
+
}
|
|
1217
|
+
async function handleMessage(content, state, options, render, _rl, chatHistory) {
|
|
1218
|
+
let capturedResponse = '';
|
|
1219
|
+
// Ensure we have a session
|
|
1220
|
+
if (!state.session) {
|
|
1221
|
+
const result = await createConversation(options.server, {
|
|
1222
|
+
mode: 'agentic',
|
|
1223
|
+
presetId: 'agentic',
|
|
1224
|
+
});
|
|
1225
|
+
if ('error' in result) {
|
|
1226
|
+
render.error(result.error);
|
|
1227
|
+
return '';
|
|
1228
|
+
}
|
|
1229
|
+
state.session = {
|
|
1230
|
+
conversationId: result.conversationId,
|
|
1231
|
+
mode: state.agenticMode ? 'agentic' : 'chat',
|
|
1232
|
+
totalTokens: 0,
|
|
1233
|
+
totalCost: 0,
|
|
1234
|
+
messageCount: 0,
|
|
1235
|
+
createdAt: new Date(),
|
|
1236
|
+
};
|
|
1237
|
+
// Persist for auto-resume next time
|
|
1238
|
+
saveLastSession(result.conversationId);
|
|
1239
|
+
}
|
|
1240
|
+
// Display user message
|
|
1241
|
+
render.userMessage(content);
|
|
1242
|
+
state.messageCount++;
|
|
1243
|
+
// Start streaming
|
|
1244
|
+
const startTime = Date.now();
|
|
1245
|
+
state.isStreaming = true;
|
|
1246
|
+
state.streamStartTime = startTime;
|
|
1247
|
+
// Simple inline spinner (no ora - it fights readline in TTY)
|
|
1248
|
+
const frames = ['\u280b', '\u2819', '\u2839', '\u2838', '\u283c', '\u2834', '\u2826', '\u2827', '\u2807', '\u280f'];
|
|
1249
|
+
let frameIdx = 0;
|
|
1250
|
+
let spinnerStopped = false;
|
|
1251
|
+
const spinnerInterval = setInterval(() => {
|
|
1252
|
+
if (!spinnerStopped) {
|
|
1253
|
+
const elapsed = formatElapsed(Date.now() - startTime);
|
|
1254
|
+
const frame = frames[frameIdx % frames.length];
|
|
1255
|
+
process.stdout.write(`\r${chalk.dim(` ${frame} ${elapsed}`)}\x1b[K`);
|
|
1256
|
+
frameIdx++;
|
|
1257
|
+
}
|
|
1258
|
+
}, 80);
|
|
1259
|
+
activeSpinnerInterval = spinnerInterval;
|
|
1260
|
+
const stopSpinner = () => {
|
|
1261
|
+
if (!spinnerStopped) {
|
|
1262
|
+
spinnerStopped = true;
|
|
1263
|
+
clearInterval(spinnerInterval);
|
|
1264
|
+
process.stdout.write('\r\x1b[K'); // Clear the spinner line
|
|
1265
|
+
}
|
|
1266
|
+
};
|
|
1267
|
+
let assistantStarted = false;
|
|
1268
|
+
let currentThinking = false;
|
|
1269
|
+
try {
|
|
1270
|
+
const abortController = new AbortController();
|
|
1271
|
+
activeAbort = abortController;
|
|
1272
|
+
for await (const event of streamChat(options.server, {
|
|
1273
|
+
conversationId: state.session.conversationId,
|
|
1274
|
+
content,
|
|
1275
|
+
model: state.model,
|
|
1276
|
+
provider: state.provider,
|
|
1277
|
+
agentic: state.agenticMode,
|
|
1278
|
+
showThinking: state.showThinking,
|
|
1279
|
+
effort: state.effort,
|
|
1280
|
+
signal: abortController.signal,
|
|
1281
|
+
})) {
|
|
1282
|
+
if (!state.isStreaming) {
|
|
1283
|
+
abortController.abort();
|
|
1284
|
+
break;
|
|
1285
|
+
}
|
|
1286
|
+
switch (event.type) {
|
|
1287
|
+
case 'content_delta': {
|
|
1288
|
+
if (!assistantStarted) {
|
|
1289
|
+
stopSpinner();
|
|
1290
|
+
render.assistantStart(state.model);
|
|
1291
|
+
assistantStarted = true;
|
|
1292
|
+
}
|
|
1293
|
+
if (currentThinking) {
|
|
1294
|
+
render.thinkingEnd();
|
|
1295
|
+
currentThinking = false;
|
|
1296
|
+
}
|
|
1297
|
+
const chunk = event.data.content;
|
|
1298
|
+
if (chunk) {
|
|
1299
|
+
render.assistantDelta(chunk);
|
|
1300
|
+
capturedResponse += chunk;
|
|
1301
|
+
}
|
|
1302
|
+
break;
|
|
1303
|
+
}
|
|
1304
|
+
case 'thinking_start':
|
|
1305
|
+
case 'thinking:start': {
|
|
1306
|
+
if (!assistantStarted) {
|
|
1307
|
+
stopSpinner();
|
|
1308
|
+
render.assistantStart(state.model);
|
|
1309
|
+
assistantStarted = true;
|
|
1310
|
+
}
|
|
1311
|
+
if (state.showThinking) {
|
|
1312
|
+
render.thinkingStart();
|
|
1313
|
+
currentThinking = true;
|
|
1314
|
+
}
|
|
1315
|
+
break;
|
|
1316
|
+
}
|
|
1317
|
+
case 'thinking_update':
|
|
1318
|
+
case 'thinking:update': {
|
|
1319
|
+
if (state.showThinking && currentThinking) {
|
|
1320
|
+
const chunk = event.data.content;
|
|
1321
|
+
if (chunk)
|
|
1322
|
+
render.thinkingDelta(chunk);
|
|
1323
|
+
}
|
|
1324
|
+
break;
|
|
1325
|
+
}
|
|
1326
|
+
case 'thinking_end': {
|
|
1327
|
+
if (currentThinking) {
|
|
1328
|
+
render.thinkingEnd();
|
|
1329
|
+
currentThinking = false;
|
|
1330
|
+
}
|
|
1331
|
+
break;
|
|
1332
|
+
}
|
|
1333
|
+
case 'tool_call':
|
|
1334
|
+
case 'tool:call': {
|
|
1335
|
+
const toolName = event.data.name || event.data.toolName || 'unknown';
|
|
1336
|
+
const toolArgs = event.data.arguments || event.data.args || {};
|
|
1337
|
+
// Always show tool activity (minimal line, not gated on showTools)
|
|
1338
|
+
stopSpinner();
|
|
1339
|
+
if (!assistantStarted) {
|
|
1340
|
+
render.assistantStart(state.model);
|
|
1341
|
+
assistantStarted = true;
|
|
1342
|
+
}
|
|
1343
|
+
if (state.showTools) {
|
|
1344
|
+
// Verbose: show full tool call with args
|
|
1345
|
+
render.toolCall(toolName, toolArgs);
|
|
1346
|
+
}
|
|
1347
|
+
else {
|
|
1348
|
+
// Minimal: just show what's happening
|
|
1349
|
+
const firstArg = Object.values(toolArgs)[0];
|
|
1350
|
+
const preview = firstArg ? String(firstArg).slice(0, 50) : '';
|
|
1351
|
+
console.log(chalk.dim(` ${chalk.cyan(toolName)} ${preview}`));
|
|
1352
|
+
}
|
|
1353
|
+
break;
|
|
1354
|
+
}
|
|
1355
|
+
case 'tool_result':
|
|
1356
|
+
case 'tool:result': {
|
|
1357
|
+
const resultName = event.data.name || event.data.toolName || 'unknown';
|
|
1358
|
+
const result = event.data.result || event.data.output;
|
|
1359
|
+
const success = event.data.success !== false;
|
|
1360
|
+
const durationMs = event.data.durationMs;
|
|
1361
|
+
if (state.showTools) {
|
|
1362
|
+
render.toolResult(resultName, result, success, durationMs);
|
|
1363
|
+
}
|
|
1364
|
+
else {
|
|
1365
|
+
// Minimal: just show ok/fail
|
|
1366
|
+
const icon = success ? chalk.hex('#7DD3A5')('\u2713') : chalk.hex('#F97066')('\u2717');
|
|
1367
|
+
const dur = durationMs ? chalk.dim(` ${formatElapsed(durationMs)}`) : '';
|
|
1368
|
+
console.log(chalk.dim(` ${icon}${dur}`));
|
|
1369
|
+
}
|
|
1370
|
+
break;
|
|
1371
|
+
}
|
|
1372
|
+
case 'step_start':
|
|
1373
|
+
case 'step:start': {
|
|
1374
|
+
// Show step progress
|
|
1375
|
+
const stepNum = event.data.step;
|
|
1376
|
+
if (stepNum && stepNum > 1) {
|
|
1377
|
+
// Show step indicator for multi-step work
|
|
1378
|
+
process.stdout.write(chalk.dim(` step ${stepNum}...\r`));
|
|
1379
|
+
}
|
|
1380
|
+
break;
|
|
1381
|
+
}
|
|
1382
|
+
case 'step_complete':
|
|
1383
|
+
case 'step:complete': {
|
|
1384
|
+
// Step done - spinner continues for next step
|
|
1385
|
+
break;
|
|
1386
|
+
}
|
|
1387
|
+
case 'thinking:end': {
|
|
1388
|
+
if (currentThinking) {
|
|
1389
|
+
render.thinkingEnd();
|
|
1390
|
+
currentThinking = false;
|
|
1391
|
+
}
|
|
1392
|
+
// In agentic mode, thinking:end.text contains the actual response
|
|
1393
|
+
const responseText = event.data.text;
|
|
1394
|
+
if (responseText) {
|
|
1395
|
+
stopSpinner();
|
|
1396
|
+
if (!assistantStarted) {
|
|
1397
|
+
render.assistantStart(state.model);
|
|
1398
|
+
assistantStarted = true;
|
|
1399
|
+
}
|
|
1400
|
+
// Typewriter effect: emit word-by-word for streaming feel
|
|
1401
|
+
const words = responseText.split(/(\s+)/);
|
|
1402
|
+
for (let wi = 0; wi < words.length; wi++) {
|
|
1403
|
+
render.assistantDelta(words[wi]);
|
|
1404
|
+
if (wi % 4 === 3 && wi < words.length - 1) {
|
|
1405
|
+
await new Promise((r) => setTimeout(r, 12));
|
|
1406
|
+
}
|
|
1407
|
+
}
|
|
1408
|
+
capturedResponse += responseText;
|
|
1409
|
+
}
|
|
1410
|
+
break;
|
|
1411
|
+
}
|
|
1412
|
+
case 'summary': {
|
|
1413
|
+
// Final summary - show if we haven't captured any response yet
|
|
1414
|
+
const summaryText = event.data.summary || event.data.content || event.data.text;
|
|
1415
|
+
if (summaryText && !capturedResponse) {
|
|
1416
|
+
stopSpinner();
|
|
1417
|
+
if (!assistantStarted) {
|
|
1418
|
+
render.assistantStart(state.model);
|
|
1419
|
+
assistantStarted = true;
|
|
1420
|
+
}
|
|
1421
|
+
render.assistantDelta(summaryText);
|
|
1422
|
+
capturedResponse += summaryText;
|
|
1423
|
+
}
|
|
1424
|
+
break;
|
|
1425
|
+
}
|
|
1426
|
+
case 'complete': {
|
|
1427
|
+
stopSpinner();
|
|
1428
|
+
if (assistantStarted) {
|
|
1429
|
+
render.assistantEnd();
|
|
1430
|
+
}
|
|
1431
|
+
// Display usage - data may contain usage directly or nested
|
|
1432
|
+
const rawUsage = event.data.usage;
|
|
1433
|
+
const usage = rawUsage || (event.data.totalTokens ? {
|
|
1434
|
+
promptTokens: event.data.inputTokens || 0,
|
|
1435
|
+
completionTokens: event.data.outputTokens || 0,
|
|
1436
|
+
totalTokens: event.data.totalTokens || 0,
|
|
1437
|
+
cost: event.data.cost,
|
|
1438
|
+
} : undefined);
|
|
1439
|
+
const durationMs = Date.now() - startTime;
|
|
1440
|
+
if (usage && state.showUsage) {
|
|
1441
|
+
render.usage(usage, durationMs);
|
|
1442
|
+
state.sessionTokens += usage.totalTokens || 0;
|
|
1443
|
+
state.sessionCost += usage.cost || 0;
|
|
1444
|
+
}
|
|
1445
|
+
else if (state.showUsage) {
|
|
1446
|
+
console.log(chalk.dim(` ${formatElapsed(durationMs)}`));
|
|
1447
|
+
}
|
|
1448
|
+
// Session footer - running totals
|
|
1449
|
+
if (state.showUsage && state.messageCount > 0) {
|
|
1450
|
+
render.sessionFooter({
|
|
1451
|
+
sessionTokens: state.sessionTokens,
|
|
1452
|
+
sessionCost: state.sessionCost,
|
|
1453
|
+
messageCount: state.messageCount,
|
|
1454
|
+
model: state.model,
|
|
1455
|
+
});
|
|
1456
|
+
}
|
|
1457
|
+
break;
|
|
1458
|
+
}
|
|
1459
|
+
case 'error': {
|
|
1460
|
+
stopSpinner();
|
|
1461
|
+
const errMsg = event.data.message || 'Unknown error';
|
|
1462
|
+
// If streaming failed, fall back to non-streaming
|
|
1463
|
+
if (!assistantStarted && errMsg.includes('No output')) {
|
|
1464
|
+
const { sendMessage } = await import('./stream-client.js');
|
|
1465
|
+
const fallbackResult = await sendMessage(options.server, state.session.conversationId, content, { model: state.model, tools: state.agenticMode });
|
|
1466
|
+
if ('error' in fallbackResult) {
|
|
1467
|
+
render.error(fallbackResult.error);
|
|
1468
|
+
}
|
|
1469
|
+
else {
|
|
1470
|
+
render.assistantStart(fallbackResult.model);
|
|
1471
|
+
render.assistantDelta(fallbackResult.content);
|
|
1472
|
+
render.assistantEnd();
|
|
1473
|
+
}
|
|
1474
|
+
break;
|
|
1475
|
+
}
|
|
1476
|
+
render.error(errMsg);
|
|
1477
|
+
break;
|
|
1478
|
+
}
|
|
1479
|
+
case 'session_start':
|
|
1480
|
+
case 'user_message':
|
|
1481
|
+
// Acknowledgment events - no display needed
|
|
1482
|
+
break;
|
|
1483
|
+
default:
|
|
1484
|
+
// Unknown event type - skip
|
|
1485
|
+
break;
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1489
|
+
catch (error) {
|
|
1490
|
+
stopSpinner();
|
|
1491
|
+
if (error instanceof Error && error.name === 'AbortError') {
|
|
1492
|
+
console.log(chalk.dim('\n Cancelled.'));
|
|
1493
|
+
}
|
|
1494
|
+
else {
|
|
1495
|
+
render.error(error instanceof Error ? error.message : 'Stream failed');
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1498
|
+
finally {
|
|
1499
|
+
stopSpinner(); // Always stop spinner before clearing interval to avoid leaked frame writes
|
|
1500
|
+
clearInterval(spinnerInterval);
|
|
1501
|
+
activeSpinnerInterval = null;
|
|
1502
|
+
activeAbort = null;
|
|
1503
|
+
state.isStreaming = false;
|
|
1504
|
+
state.streamStartTime = undefined;
|
|
1505
|
+
// Ensure we end properly
|
|
1506
|
+
if (assistantStarted && !state.isStreaming) {
|
|
1507
|
+
// assistantEnd was called in 'complete' handler
|
|
1508
|
+
}
|
|
1509
|
+
// Save history periodically
|
|
1510
|
+
if (state.messageCount % 5 === 0) {
|
|
1511
|
+
saveHistory(chatHistory);
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1514
|
+
return capturedResponse;
|
|
1515
|
+
}
|
|
1516
|
+
//# sourceMappingURL=repl.js.map
|