@aion0/forge 0.5.26 ā 0.5.28
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/.forge/worktrees/pipeline-4dd8dc2d/CLAUDE.md +86 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/README.md +136 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/RELEASE_NOTES.md +36 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/agents/route.ts +17 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/auth/[...nextauth]/route.ts +3 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/auth/verify/route.ts +46 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/claude/[id]/route.ts +31 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/claude/[id]/stream/route.ts +63 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/claude/route.ts +28 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/claude-sessions/[projectName]/entries/route.ts +23 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/claude-sessions/[projectName]/live/route.ts +72 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/claude-sessions/[projectName]/route.ts +37 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/claude-sessions/sync/route.ts +17 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/claude-templates/route.ts +145 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/code/route.ts +299 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/delivery/[id]/route.ts +62 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/delivery/route.ts +40 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/detect-cli/route.ts +46 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/docs/route.ts +176 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/docs/sessions/route.ts +54 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/favorites/route.ts +26 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/flows/route.ts +6 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/flows/run/route.ts +19 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/git/route.ts +149 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/help/route.ts +84 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/issue-scanner/route.ts +116 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/logs/route.ts +100 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/mobile-chat/route.ts +115 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/monitor/route.ts +74 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/notifications/route.ts +42 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/notify/test/route.ts +33 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/online/route.ts +40 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/pipelines/[id]/route.ts +41 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/pipelines/route.ts +90 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/plugins/route.ts +75 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/preview/[...path]/route.ts +64 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/preview/route.ts +156 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/project-pipelines/route.ts +91 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/project-sessions/route.ts +61 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/projects/route.ts +26 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/sessions/[id]/chat/route.ts +64 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/sessions/[id]/messages/route.ts +9 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/sessions/[id]/route.ts +17 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/sessions/route.ts +20 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/settings/route.ts +64 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/skills/local/route.ts +228 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/skills/route.ts +182 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/smith-templates/route.ts +81 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/status/route.ts +12 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/tabs/route.ts +25 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/tasks/[id]/route.ts +51 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/tasks/[id]/stream/route.ts +77 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/tasks/link/route.ts +37 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/tasks/route.ts +44 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/tasks/session/route.ts +14 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/telegram/route.ts +23 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/templates/route.ts +6 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/terminal-bell/route.ts +39 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/terminal-cwd/route.ts +19 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/terminal-state/route.ts +15 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/tunnel/route.ts +26 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/upgrade/route.ts +43 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/usage/route.ts +20 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/version/route.ts +78 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/watchers/route.ts +33 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/workspace/[id]/agents/route.ts +35 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/workspace/[id]/memory/route.ts +23 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/workspace/[id]/smith/route.ts +22 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/workspace/[id]/stream/route.ts +31 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/api/workspace/route.ts +79 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/global-error.tsx +21 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/globals.css +52 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/icon.ico +0 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/icon.png +0 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/icon.svg +106 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/layout.tsx +17 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/login/LoginForm.tsx +96 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/login/page.tsx +10 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/mobile/page.tsx +10 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/app/page.tsx +22 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/bin/forge-server.mjs +484 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/check-forge-status.sh +71 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/cli/mw.ts +579 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/BrowserPanel.tsx +175 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/ChatPanel.tsx +191 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/ClaudeTerminal.tsx +267 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/CodeViewer.tsx +787 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/ConversationEditor.tsx +411 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/ConversationGraphView.tsx +347 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/ConversationTerminalView.tsx +303 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/Dashboard.tsx +807 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/DashboardWrapper.tsx +9 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/DeliveryFlowEditor.tsx +491 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/DeliveryList.tsx +230 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/DeliveryWorkspace.tsx +589 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/DocTerminal.tsx +187 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/DocsViewer.tsx +574 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/HelpDialog.tsx +169 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/HelpTerminal.tsx +141 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/InlinePipelineView.tsx +111 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/LogViewer.tsx +194 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/MarkdownContent.tsx +73 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/MobileView.tsx +385 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/MonitorPanel.tsx +122 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/NewSessionModal.tsx +93 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/NewTaskModal.tsx +492 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/PipelineEditor.tsx +570 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/PipelineView.tsx +1018 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/PluginsPanel.tsx +472 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/ProjectDetail.tsx +1618 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/ProjectList.tsx +108 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/ProjectManager.tsx +401 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/SessionList.tsx +74 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/SessionView.tsx +726 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/SettingsModal.tsx +1647 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/SkillsPanel.tsx +969 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/StatusBar.tsx +99 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/TabBar.tsx +46 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/TaskBoard.tsx +113 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/TaskDetail.tsx +372 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/TerminalLauncher.tsx +398 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/TunnelToggle.tsx +206 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/UsagePanel.tsx +207 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/WebTerminal.tsx +1743 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/WorkspaceTree.tsx +221 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/components/WorkspaceView.tsx +4048 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/dev-test.sh +5 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/docs/Forge_Memory_Layer_Design.docx +0 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/docs/Forge_Strategy_Research_2026.docx +0 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/docs/LOCAL-DEPLOY.md +144 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/docs/roadmap-multi-agent-workflow.md +330 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/forge-logo.png +0 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/forge-logo.svg +106 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/hooks/useSidebarResize.ts +52 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/install.sh +29 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/instrumentation.ts +35 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/agents/claude-adapter.ts +104 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/agents/generic-adapter.ts +64 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/agents/index.ts +245 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/agents/types.ts +70 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/artifacts.ts +106 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/auth.ts +62 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/builtin-plugins/docker.yaml +70 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/builtin-plugins/http.yaml +66 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/builtin-plugins/jenkins.yaml +92 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/builtin-plugins/llm-vision.yaml +85 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/builtin-plugins/playwright.yaml +111 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/builtin-plugins/shell-command.yaml +60 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/builtin-plugins/slack.yaml +48 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/builtin-plugins/webhook.yaml +56 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/claude-process.ts +361 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/claude-sessions.ts +266 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/claude-templates.ts +227 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/cloudflared.ts +424 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/crypto.ts +67 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/delivery.ts +787 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/dirs.ts +99 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/flows.ts +86 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/forge-mcp-server.ts +732 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/forge-skills/forge-inbox.md +38 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/forge-skills/forge-send.md +47 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/forge-skills/forge-status.md +32 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/forge-skills/forge-workspace-sync.md +37 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/00-overview.md +40 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/01-settings.md +194 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/02-telegram.md +41 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/03-tunnel.md +31 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/04-tasks.md +52 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/05-pipelines.md +460 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/06-skills.md +43 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/07-projects.md +73 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/08-rules.md +53 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/09-issue-autofix.md +55 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/10-troubleshooting.md +89 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/11-workspace.md +810 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/CLAUDE.md +62 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/init.ts +266 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/issue-scanner.ts +298 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/logger.ts +79 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/notifications.ts +75 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/notify.ts +108 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/password.ts +97 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/pipeline-scheduler.ts +373 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/pipeline.ts +1565 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/plugins/executor.ts +347 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/plugins/registry.ts +228 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/plugins/types.ts +103 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/project-sessions.ts +53 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/projects.ts +86 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/session-manager.ts +156 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/session-utils.ts +53 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/session-watcher.ts +345 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/settings.ts +195 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/skills.ts +458 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/task-manager.ts +951 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/telegram-bot.ts +1477 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/telegram-standalone.ts +83 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/terminal-server.ts +70 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/terminal-standalone.ts +438 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/usage-scanner.ts +249 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/__tests__/state-machine.test.ts +388 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/__tests__/workspace.test.ts +311 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/agent-bus.ts +416 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/agent-worker.ts +655 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/backends/api-backend.ts +262 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/backends/cli-backend.ts +491 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/index.ts +84 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/manager.ts +136 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/orchestrator.ts +3415 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/persistence.ts +309 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/presets.ts +649 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/requests.ts +287 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/session-monitor.ts +240 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/skill-installer.ts +275 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/smith-memory.ts +498 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/types.ts +241 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/watch-manager.ts +560 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace-standalone.ts +978 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/middleware.ts +51 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/next.config.ts +26 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/package.json +74 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/pnpm-lock.yaml +3719 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/pnpm-workspace.yaml +1 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/postcss.config.mjs +7 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/publish.sh +133 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/scripts/bench/README.md +66 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/scripts/bench/results/.gitignore +2 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/scripts/bench/run.ts +635 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/scripts/bench/tasks/01-text-utils/task.md +26 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/scripts/bench/tasks/01-text-utils/validator.sh +46 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/scripts/bench/tasks/02-pagination/setup.sh +19 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/scripts/bench/tasks/02-pagination/task.md +48 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/scripts/bench/tasks/02-pagination/validator.sh +69 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/scripts/bench/tasks/03-bug-fix/setup.sh +82 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/scripts/bench/tasks/03-bug-fix/task.md +30 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/scripts/bench/tasks/03-bug-fix/validator.sh +29 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/scripts/verify-usage.ts +178 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/src/config/index.ts +129 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/src/core/db/database.ts +259 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/src/core/memory/strategy.ts +32 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/src/core/providers/chat.ts +65 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/src/core/providers/registry.ts +60 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/src/core/session/manager.ts +190 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/src/types/index.ts +129 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/start.sh +32 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/templates/smith-lead.json +45 -0
- package/.forge/worktrees/pipeline-4dd8dc2d/tsconfig.json +42 -0
- package/RELEASE_NOTES.md +10 -29
- package/app/api/terminal-bell/route.ts +6 -2
- package/app/api/terminal-cwd/route.ts +7 -4
- package/components/CodeViewer.tsx +3 -31
- package/components/Dashboard.tsx +34 -20
- package/components/WebTerminal.tsx +36 -2
- package/lib/terminal-standalone.ts +19 -2
- package/package.json +1 -1
|
@@ -0,0 +1,1477 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Telegram Bot ā remote interface for Forge.
|
|
3
|
+
*
|
|
4
|
+
* Optimized for mobile:
|
|
5
|
+
* - /tasks shows numbered list, reply with number to see details
|
|
6
|
+
* - Reply to task messages to send follow-ups
|
|
7
|
+
* - Plain text "project: instructions" to create tasks
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { loadSettings } from './settings';
|
|
11
|
+
import { createTask, getTask, listTasks, cancelTask, retryTask, onTaskEvent } from './task-manager';
|
|
12
|
+
import { scanProjects } from './projects';
|
|
13
|
+
import { listClaudeSessions, getSessionFilePath, readSessionEntries } from './claude-sessions';
|
|
14
|
+
import { listWatchers, createWatcher, deleteWatcher, toggleWatcher } from './session-watcher';
|
|
15
|
+
import { startTunnel, stopTunnel, getTunnelStatus } from './cloudflared';
|
|
16
|
+
// Password verification is done via require() in handler functions
|
|
17
|
+
import type { Task, TaskLogEntry } from '@/src/types';
|
|
18
|
+
|
|
19
|
+
// Persist state across hot-reloads
|
|
20
|
+
const globalKey = Symbol.for('mw-telegram-state');
|
|
21
|
+
const g = globalThis as any;
|
|
22
|
+
if (!g[globalKey]) g[globalKey] = { taskListenerAttached: false, processedMsgIds: new Set<number>() };
|
|
23
|
+
const botState: { taskListenerAttached: boolean; processedMsgIds: Set<number> } = g[globalKey];
|
|
24
|
+
|
|
25
|
+
// Track which Telegram message maps to which task (for reply-based interaction)
|
|
26
|
+
const taskMessageMap = new Map<number, string>(); // messageId ā taskId
|
|
27
|
+
const taskChatMap = new Map<string, number>(); // taskId ā chatId
|
|
28
|
+
|
|
29
|
+
// Numbered lists ā maps number (1-10) ā id for quick selection
|
|
30
|
+
const chatNumberedTasks = new Map<number, Map<number, string>>();
|
|
31
|
+
// Session selection: two-tier ā first pick project, then pick session
|
|
32
|
+
const chatNumberedSessions = new Map<number, Map<number, { projectName: string; sessionId: string }>>();
|
|
33
|
+
const chatNumberedProjects = new Map<number, Map<number, string>>();
|
|
34
|
+
// Track what the last numbered list was for
|
|
35
|
+
const chatListMode = new Map<number, 'tasks' | 'projects' | 'sessions' | 'task-create' | 'peek'>();
|
|
36
|
+
|
|
37
|
+
// Pending task creation: waiting for prompt text
|
|
38
|
+
const pendingTaskProject = new Map<number, { name: string; path: string }>(); // chatId ā project
|
|
39
|
+
|
|
40
|
+
// Pending note: waiting for content
|
|
41
|
+
const pendingNote = new Set<number>(); // chatIds waiting for note content
|
|
42
|
+
|
|
43
|
+
// Buffer for streaming logs
|
|
44
|
+
const logBuffers = new Map<string, { entries: string[]; timer: ReturnType<typeof setTimeout> | null }>();
|
|
45
|
+
|
|
46
|
+
// āāā Start/Stop āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
47
|
+
|
|
48
|
+
// telegram-standalone process is managed by forge-server.mjs
|
|
49
|
+
|
|
50
|
+
export function startTelegramBot() {
|
|
51
|
+
const settings = loadSettings();
|
|
52
|
+
if (!settings.telegramBotToken || !settings.telegramChatId) return;
|
|
53
|
+
|
|
54
|
+
// Set bot command menu
|
|
55
|
+
setBotCommands(settings.telegramBotToken);
|
|
56
|
+
|
|
57
|
+
// Listen for task events ā stream to Telegram (only once per worker)
|
|
58
|
+
if (!botState.taskListenerAttached) {
|
|
59
|
+
botState.taskListenerAttached = true;
|
|
60
|
+
onTaskEvent((taskId, event, data) => {
|
|
61
|
+
const settings = loadSettings();
|
|
62
|
+
if (!settings.telegramBotToken || !settings.telegramChatId) return;
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
const { pipelineTaskIds } = require('./pipeline');
|
|
66
|
+
if (pipelineTaskIds.has(taskId)) return;
|
|
67
|
+
} catch {}
|
|
68
|
+
|
|
69
|
+
const chatId = Number(settings.telegramChatId.split(',')[0].trim());
|
|
70
|
+
|
|
71
|
+
if (event === 'log') {
|
|
72
|
+
bufferLogEntry(taskId, chatId, data as TaskLogEntry);
|
|
73
|
+
} else if (event === 'status') {
|
|
74
|
+
handleStatusChange(taskId, chatId, data as string);
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Note: telegram-standalone process is started by forge-server.mjs, not here.
|
|
80
|
+
// This function only sets up the task event listener and bot commands.
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function stopTelegramBot() {
|
|
84
|
+
// telegram-standalone is managed by forge-server.mjs
|
|
85
|
+
// This is a no-op now, kept for API compatibility
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// āāā Message Handler āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
89
|
+
|
|
90
|
+
// Exported for API route ā called by telegram-standalone via /api/telegram
|
|
91
|
+
export async function handleTelegramMessage(msg: any) { return handleMessage(msg); }
|
|
92
|
+
|
|
93
|
+
async function handleMessage(msg: any) {
|
|
94
|
+
const chatId = msg.chat.id;
|
|
95
|
+
|
|
96
|
+
// Whitelist check ā only allow configured chat IDs, block all if not configured
|
|
97
|
+
const settings = loadSettings();
|
|
98
|
+
const allowedIds = settings.telegramChatId.split(',').map((s: string) => s.trim()).filter(Boolean);
|
|
99
|
+
if (allowedIds.length === 0 || !allowedIds.includes(String(chatId))) {
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Message received (logged silently)
|
|
104
|
+
// Dedup: skip if we already processed this message
|
|
105
|
+
const msgId = msg.message_id;
|
|
106
|
+
if (botState.processedMsgIds.has(msgId)) return;
|
|
107
|
+
botState.processedMsgIds.add(msgId);
|
|
108
|
+
// Keep set size bounded
|
|
109
|
+
if (botState.processedMsgIds.size > 200) {
|
|
110
|
+
const oldest = [...botState.processedMsgIds].slice(0, 100);
|
|
111
|
+
oldest.forEach(id => botState.processedMsgIds.delete(id));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const text: string = msg.text.trim();
|
|
115
|
+
const replyTo = msg.reply_to_message?.message_id;
|
|
116
|
+
|
|
117
|
+
// Check if waiting for note content
|
|
118
|
+
if (pendingNote.has(chatId) && !text.startsWith('/')) {
|
|
119
|
+
pendingNote.delete(chatId);
|
|
120
|
+
await sendNoteToDocsClaude(chatId, text);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Check if waiting for task prompt
|
|
125
|
+
const pending = pendingTaskProject.get(chatId);
|
|
126
|
+
if (pending && !text.startsWith('/')) {
|
|
127
|
+
pendingTaskProject.delete(chatId);
|
|
128
|
+
const task = createTask({
|
|
129
|
+
projectName: pending.name,
|
|
130
|
+
projectPath: pending.path,
|
|
131
|
+
prompt: text,
|
|
132
|
+
});
|
|
133
|
+
const msgId = await send(chatId, `ā
Task ${task.id} created\nš ${task.projectName}\n\n${text.slice(0, 200)}`);
|
|
134
|
+
if (msgId) { taskMessageMap.set(msgId, task.id); taskChatMap.set(task.id, chatId); }
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Check if replying to a task message ā follow-up
|
|
139
|
+
if (replyTo && taskMessageMap.has(replyTo)) {
|
|
140
|
+
const taskId = taskMessageMap.get(replyTo)!;
|
|
141
|
+
await handleFollowUp(chatId, taskId, text);
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Quick number selection (1-10) ā context-dependent
|
|
146
|
+
if (/^\d{1,2}$/.test(text)) {
|
|
147
|
+
const num = parseInt(text);
|
|
148
|
+
const mode = chatListMode.get(chatId);
|
|
149
|
+
|
|
150
|
+
if (mode === 'task-create') {
|
|
151
|
+
const projMap = chatNumberedProjects.get(chatId);
|
|
152
|
+
if (projMap?.has(num)) {
|
|
153
|
+
const projectName = projMap.get(num)!;
|
|
154
|
+
const projects = scanProjects();
|
|
155
|
+
const project = projects.find(p => p.name === projectName);
|
|
156
|
+
if (project) {
|
|
157
|
+
pendingTaskProject.set(chatId, { name: project.name, path: project.path });
|
|
158
|
+
await send(chatId, `š ${project.name}\n\nSend the task prompt:`);
|
|
159
|
+
}
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
} else if (mode === 'peek') {
|
|
163
|
+
const projMap = chatNumberedProjects.get(chatId);
|
|
164
|
+
if (projMap?.has(num)) {
|
|
165
|
+
await handlePeek(chatId, projMap.get(num)!);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
} else if (mode === 'projects') {
|
|
169
|
+
const projMap = chatNumberedProjects.get(chatId);
|
|
170
|
+
if (projMap?.has(num)) {
|
|
171
|
+
await sendSessionList(chatId, projMap.get(num)!);
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
} else if (mode === 'sessions') {
|
|
175
|
+
const sessMap = chatNumberedSessions.get(chatId);
|
|
176
|
+
if (sessMap?.has(num)) {
|
|
177
|
+
const { projectName, sessionId } = sessMap.get(num)!;
|
|
178
|
+
await sendSessionContent(chatId, projectName, sessionId);
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
} else {
|
|
182
|
+
const taskMap = chatNumberedTasks.get(chatId);
|
|
183
|
+
if (taskMap?.has(num)) {
|
|
184
|
+
await sendTaskDetail(chatId, taskMap.get(num)!);
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Commands
|
|
191
|
+
if (text.startsWith('/')) {
|
|
192
|
+
// Any new command cancels pending states
|
|
193
|
+
pendingTaskProject.delete(chatId);
|
|
194
|
+
pendingNote.delete(chatId);
|
|
195
|
+
|
|
196
|
+
const [cmd, ...args] = text.split(/\s+/);
|
|
197
|
+
switch (cmd) {
|
|
198
|
+
case '/start':
|
|
199
|
+
case '/help':
|
|
200
|
+
await sendHelp(chatId);
|
|
201
|
+
break;
|
|
202
|
+
case '/tasks':
|
|
203
|
+
case '/t':
|
|
204
|
+
await sendNumberedTaskList(chatId, args[0]);
|
|
205
|
+
break;
|
|
206
|
+
case '/new':
|
|
207
|
+
case '/task':
|
|
208
|
+
if (args.length > 0) {
|
|
209
|
+
await handleNewTask(chatId, args.join(' '));
|
|
210
|
+
} else {
|
|
211
|
+
await startTaskCreation(chatId);
|
|
212
|
+
}
|
|
213
|
+
break;
|
|
214
|
+
case '/sessions':
|
|
215
|
+
case '/s':
|
|
216
|
+
if (args[0]) {
|
|
217
|
+
await sendSessionList(chatId, args[0]);
|
|
218
|
+
} else {
|
|
219
|
+
await sendProjectListForSessions(chatId);
|
|
220
|
+
}
|
|
221
|
+
break;
|
|
222
|
+
case '/projects':
|
|
223
|
+
case '/p':
|
|
224
|
+
await sendProjectList(chatId);
|
|
225
|
+
break;
|
|
226
|
+
case '/agents':
|
|
227
|
+
await sendAgentList(chatId);
|
|
228
|
+
break;
|
|
229
|
+
case '/watch':
|
|
230
|
+
case '/w':
|
|
231
|
+
if (args.length > 0) {
|
|
232
|
+
await handleWatch(chatId, args[0], args[1]);
|
|
233
|
+
} else {
|
|
234
|
+
await sendWatcherList(chatId);
|
|
235
|
+
}
|
|
236
|
+
break;
|
|
237
|
+
case '/unwatch':
|
|
238
|
+
await handleUnwatch(chatId, args[0]);
|
|
239
|
+
break;
|
|
240
|
+
case '/docs':
|
|
241
|
+
case '/doc':
|
|
242
|
+
await handleDocs(chatId, args.join(' '));
|
|
243
|
+
break;
|
|
244
|
+
case '/peek':
|
|
245
|
+
case '/sessions':
|
|
246
|
+
case '/s':
|
|
247
|
+
if (args.length > 0) {
|
|
248
|
+
await handlePeek(chatId, args[0], args[1]);
|
|
249
|
+
} else {
|
|
250
|
+
await startPeekSelection(chatId);
|
|
251
|
+
}
|
|
252
|
+
break;
|
|
253
|
+
case '/note':
|
|
254
|
+
await handleDocsWrite(chatId, args.join(' '));
|
|
255
|
+
break;
|
|
256
|
+
case '/cancel':
|
|
257
|
+
await handleCancel(chatId, args[0]);
|
|
258
|
+
break;
|
|
259
|
+
case '/retry':
|
|
260
|
+
await handleRetry(chatId, args[0]);
|
|
261
|
+
break;
|
|
262
|
+
case '/tunnel':
|
|
263
|
+
await handleTunnelStatus(chatId);
|
|
264
|
+
break;
|
|
265
|
+
case '/tunnel_start':
|
|
266
|
+
await handleTunnelStart(chatId, args[0], msg.message_id);
|
|
267
|
+
break;
|
|
268
|
+
case '/tunnel_stop':
|
|
269
|
+
await handleTunnelStop(chatId);
|
|
270
|
+
break;
|
|
271
|
+
case '/tunnel_code':
|
|
272
|
+
await handleTunnelCode(chatId, args[0], msg.message_id);
|
|
273
|
+
break;
|
|
274
|
+
default:
|
|
275
|
+
await send(chatId, `Unknown command: ${cmd}\nUse /help to see available commands.`);
|
|
276
|
+
}
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Plain text ā try to parse as "project: task" format
|
|
281
|
+
const colonIdx = text.indexOf(':');
|
|
282
|
+
if (colonIdx > 0 && colonIdx < 30) {
|
|
283
|
+
const projectName = text.slice(0, colonIdx).trim();
|
|
284
|
+
const prompt = text.slice(colonIdx + 1).trim();
|
|
285
|
+
if (prompt) {
|
|
286
|
+
await handleNewTask(chatId, `${projectName} ${prompt}`);
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
await send(chatId,
|
|
292
|
+
`Send a task as:\nproject-name: your instructions\n\nOr use /help for all commands.`
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// āāā Command Handlers āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
297
|
+
|
|
298
|
+
async function sendHelp(chatId: number) {
|
|
299
|
+
await send(chatId,
|
|
300
|
+
`š¤ Forge\n\n` +
|
|
301
|
+
`š /task ā create task (interactive)\n` +
|
|
302
|
+
`/tasks ā task list\n\n` +
|
|
303
|
+
`š /sessions ā session summary (select project)\n` +
|
|
304
|
+
`š /docs ā docs summary / view file\n` +
|
|
305
|
+
`š /note ā quick note to docs\n\n` +
|
|
306
|
+
`š /watch <project> ā monitor session\n` +
|
|
307
|
+
`/watch ā list watchers\n` +
|
|
308
|
+
`/unwatch <id> ā stop\n\n` +
|
|
309
|
+
`š§ /cancel <id> /retry <id>\n` +
|
|
310
|
+
`/sessions ā browse sessions\n` +
|
|
311
|
+
`/projects ā list projects\n\n` +
|
|
312
|
+
`š /tunnel ā status\n` +
|
|
313
|
+
`/tunnel_start / /tunnel_stop\n` +
|
|
314
|
+
`/tunnel_code <admin_pw> ā get session code\n\n` +
|
|
315
|
+
`š¤ /agents ā list available agents\n` +
|
|
316
|
+
`Use @agent in /task to select (e.g. /task app @codex: review)\n\n` +
|
|
317
|
+
`Reply number to select`
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
async function sendAgentList(chatId: number) {
|
|
322
|
+
try {
|
|
323
|
+
const { listAgents, getDefaultAgentId } = require('./agents');
|
|
324
|
+
const agents = listAgents();
|
|
325
|
+
const defaultId = getDefaultAgentId();
|
|
326
|
+
if (agents.length === 0) {
|
|
327
|
+
await send(chatId, 'No agents detected.');
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
const lines = agents.map((a: any) =>
|
|
331
|
+
`${a.id === defaultId ? 'ā' : ' '} ${a.name} (${a.id})${a.detected === false ? ' ā ļø not installed' : ''}`
|
|
332
|
+
);
|
|
333
|
+
await send(chatId, `š¤ Agents:\n\n${lines.join('\n')}\n\nUse @agent in /task command`);
|
|
334
|
+
} catch {
|
|
335
|
+
await send(chatId, 'Failed to list agents.');
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
async function sendNumberedTaskList(chatId: number, statusFilter?: string) {
|
|
340
|
+
// Get running/queued first, then recent done/failed
|
|
341
|
+
const allTasks = listTasks(statusFilter as any || undefined);
|
|
342
|
+
|
|
343
|
+
// Sort: running first, then queued, then by recency
|
|
344
|
+
const prioritized = [
|
|
345
|
+
...allTasks.filter(t => t.status === 'running'),
|
|
346
|
+
...allTasks.filter(t => t.status === 'queued'),
|
|
347
|
+
...allTasks.filter(t => t.status !== 'running' && t.status !== 'queued'),
|
|
348
|
+
].slice(0, 10);
|
|
349
|
+
|
|
350
|
+
if (prioritized.length === 0) {
|
|
351
|
+
await send(chatId, 'No tasks found.');
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// Build numbered map
|
|
356
|
+
const numMap = new Map<number, string>();
|
|
357
|
+
const lines: string[] = [];
|
|
358
|
+
|
|
359
|
+
prioritized.forEach((t, i) => {
|
|
360
|
+
const num = i + 1;
|
|
361
|
+
numMap.set(num, t.id);
|
|
362
|
+
|
|
363
|
+
const icon = t.status === 'running' ? 'š' : t.status === 'queued' ? 'ā³' : t.status === 'done' ? 'ā
' : t.status === 'failed' ? 'ā' : 'āŖ';
|
|
364
|
+
const cost = t.costUSD != null ? ` $${t.costUSD.toFixed(3)}` : '';
|
|
365
|
+
const prompt = t.prompt.length > 40 ? t.prompt.slice(0, 40) + '...' : t.prompt;
|
|
366
|
+
|
|
367
|
+
lines.push(`${num}. ${icon} ${t.projectName}\n ${prompt}${cost}`);
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
chatNumberedTasks.set(chatId, numMap);
|
|
371
|
+
chatListMode.set(chatId, 'tasks');
|
|
372
|
+
|
|
373
|
+
await send(chatId,
|
|
374
|
+
`š Tasks ā reply number to see details\n\n${lines.join('\n\n')}`
|
|
375
|
+
);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
async function sendTaskDetail(chatId: number, taskId: string) {
|
|
379
|
+
const task = getTask(taskId);
|
|
380
|
+
if (!task) {
|
|
381
|
+
await send(chatId, `Task not found: ${taskId}`);
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const icon = task.status === 'done' ? 'ā
' : task.status === 'running' ? 'š' : task.status === 'failed' ? 'ā' : 'ā³';
|
|
386
|
+
|
|
387
|
+
let text = `${icon} ${task.projectName} [${task.id}]\n`;
|
|
388
|
+
text += `Status: ${task.status}\n`;
|
|
389
|
+
text += `Task: ${task.prompt}\n`;
|
|
390
|
+
|
|
391
|
+
if (task.startedAt) text += `Started: ${new Date(task.startedAt).toLocaleString()}\n`;
|
|
392
|
+
if (task.completedAt) text += `Done: ${new Date(task.completedAt).toLocaleString()}\n`;
|
|
393
|
+
if (task.costUSD != null) text += `Cost: $${task.costUSD.toFixed(4)}\n`;
|
|
394
|
+
if (task.error) text += `\nā Error: ${task.error}\n`;
|
|
395
|
+
|
|
396
|
+
if (task.resultSummary) {
|
|
397
|
+
const result = task.resultSummary.length > 1500
|
|
398
|
+
? task.resultSummary.slice(0, 1500) + '...'
|
|
399
|
+
: task.resultSummary;
|
|
400
|
+
text += `\n--- Result ---\n${result}`;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// Show recent log summary for running tasks
|
|
404
|
+
if (task.status === 'running' && task.log.length > 0) {
|
|
405
|
+
const recent = task.log
|
|
406
|
+
.filter(e => e.subtype === 'text' || e.subtype === 'tool_use')
|
|
407
|
+
.slice(-5)
|
|
408
|
+
.map(e => e.subtype === 'tool_use' ? `š§ ${e.tool}` : e.content.slice(0, 80))
|
|
409
|
+
.join('\n');
|
|
410
|
+
if (recent) text += `\n--- Recent ---\n${recent}`;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
const msgId = await send(chatId, text);
|
|
414
|
+
if (msgId) {
|
|
415
|
+
taskMessageMap.set(msgId, taskId);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// Show action hints
|
|
419
|
+
if (task.status === 'done') {
|
|
420
|
+
await send(chatId, `š¬ Reply to the message above to send follow-up`);
|
|
421
|
+
} else if (task.status === 'failed') {
|
|
422
|
+
await send(chatId, `š /retry ${task.id}`);
|
|
423
|
+
} else if (task.status === 'running' || task.status === 'queued') {
|
|
424
|
+
await send(chatId, `š /cancel ${task.id}`);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
async function sendProjectListForSessions(chatId: number) {
|
|
429
|
+
const projects = scanProjects();
|
|
430
|
+
if (projects.length === 0) {
|
|
431
|
+
await send(chatId, 'No projects found.');
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
const numMap = new Map<number, string>();
|
|
436
|
+
const lines: string[] = [];
|
|
437
|
+
|
|
438
|
+
projects.slice(0, 10).forEach((p, i) => {
|
|
439
|
+
const num = i + 1;
|
|
440
|
+
numMap.set(num, p.name);
|
|
441
|
+
lines.push(`${num}. ${p.name}${p.language ? ` (${p.language})` : ''}`);
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
chatNumberedProjects.set(chatId, numMap);
|
|
445
|
+
chatListMode.set(chatId, 'projects');
|
|
446
|
+
|
|
447
|
+
await send(chatId,
|
|
448
|
+
`š Select project ā reply number\n\n${lines.join('\n')}`
|
|
449
|
+
);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
async function sendSessionList(chatId: number, projectName: string) {
|
|
453
|
+
const sessions = listClaudeSessions(projectName);
|
|
454
|
+
if (sessions.length === 0) {
|
|
455
|
+
await send(chatId, `No sessions for ${projectName}`);
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
const numMap = new Map<number, { projectName: string; sessionId: string }>();
|
|
460
|
+
const lines: string[] = [];
|
|
461
|
+
|
|
462
|
+
sessions.slice(0, 10).forEach((s, i) => {
|
|
463
|
+
const num = i + 1;
|
|
464
|
+
numMap.set(num, { projectName, sessionId: s.sessionId });
|
|
465
|
+
const label = s.summary || s.firstPrompt || s.sessionId.slice(0, 8);
|
|
466
|
+
const msgs = s.messageCount != null ? ` (${s.messageCount} msgs)` : '';
|
|
467
|
+
const date = s.modified ? new Date(s.modified).toLocaleDateString() : '';
|
|
468
|
+
lines.push(`${num}. ${label}${msgs}\n ${date} ${s.gitBranch || ''}`);
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
chatNumberedSessions.set(chatId, numMap);
|
|
472
|
+
chatListMode.set(chatId, 'sessions');
|
|
473
|
+
|
|
474
|
+
await send(chatId,
|
|
475
|
+
`š ${projectName} sessions ā reply number\n\n${lines.join('\n\n')}`
|
|
476
|
+
);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
async function sendSessionContent(chatId: number, projectName: string, sessionId: string) {
|
|
480
|
+
const filePath = getSessionFilePath(projectName, sessionId);
|
|
481
|
+
if (!filePath) {
|
|
482
|
+
await send(chatId, 'Session file not found');
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
const entries = readSessionEntries(filePath);
|
|
487
|
+
if (entries.length === 0) {
|
|
488
|
+
await send(chatId, 'Session is empty');
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// Build a readable summary ā show user messages and assistant text, skip tool details
|
|
493
|
+
const parts: string[] = [];
|
|
494
|
+
let charCount = 0;
|
|
495
|
+
const MAX = 3500;
|
|
496
|
+
|
|
497
|
+
// Walk from end to get most recent content
|
|
498
|
+
for (let i = entries.length - 1; i >= 0 && charCount < MAX; i--) {
|
|
499
|
+
const e = entries[i];
|
|
500
|
+
let line = '';
|
|
501
|
+
if (e.type === 'user') {
|
|
502
|
+
line = `š¤ ${e.content}`;
|
|
503
|
+
} else if (e.type === 'assistant_text') {
|
|
504
|
+
line = `š¤ ${e.content.slice(0, 500)}`;
|
|
505
|
+
} else if (e.type === 'tool_use') {
|
|
506
|
+
line = `š§ ${e.toolName || 'tool'}`;
|
|
507
|
+
}
|
|
508
|
+
// Skip thinking, tool_result, system for brevity
|
|
509
|
+
if (!line) continue;
|
|
510
|
+
|
|
511
|
+
if (charCount + line.length > MAX) {
|
|
512
|
+
line = line.slice(0, MAX - charCount) + '...';
|
|
513
|
+
}
|
|
514
|
+
parts.unshift(line);
|
|
515
|
+
charCount += line.length;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
const header = `š Session: ${sessionId.slice(0, 8)}\nProject: ${projectName}\n${entries.length} entries\n\n`;
|
|
519
|
+
|
|
520
|
+
// Split into chunks for Telegram's 4096 limit
|
|
521
|
+
const fullText = header + parts.join('\n\n');
|
|
522
|
+
const chunks = splitMessage(fullText, 4000);
|
|
523
|
+
for (const chunk of chunks) {
|
|
524
|
+
await send(chatId, chunk);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
async function startPeekSelection(chatId: number) {
|
|
529
|
+
const projects = scanProjects();
|
|
530
|
+
if (projects.length === 0) {
|
|
531
|
+
await send(chatId, 'No projects configured.');
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// Filter to projects that have sessions
|
|
536
|
+
const withSessions = projects.filter(p => listClaudeSessions(p.name).length > 0);
|
|
537
|
+
if (withSessions.length === 0) {
|
|
538
|
+
await send(chatId, 'No projects with sessions found.');
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
const numbered = new Map<number, string>();
|
|
543
|
+
const lines = withSessions.slice(0, 15).map((p, i) => {
|
|
544
|
+
numbered.set(i + 1, p.name);
|
|
545
|
+
const sessions = listClaudeSessions(p.name);
|
|
546
|
+
const latest = sessions[0];
|
|
547
|
+
const info = latest?.summary || latest?.firstPrompt?.slice(0, 40) || '';
|
|
548
|
+
return `${i + 1}. ${p.name}${info ? `\n ${info}` : ''}`;
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
chatNumberedProjects.set(chatId, numbered);
|
|
552
|
+
chatListMode.set(chatId, 'peek');
|
|
553
|
+
|
|
554
|
+
await send(chatId, `š Peek ā select project:\n\n${lines.join('\n')}`);
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
async function handlePeek(chatId: number, projectArg?: string, sessionArg?: string) {
|
|
558
|
+
const projects = scanProjects();
|
|
559
|
+
|
|
560
|
+
// If no project specified, use the most recent task's project
|
|
561
|
+
let projectName = projectArg;
|
|
562
|
+
let sessionId = sessionArg;
|
|
563
|
+
|
|
564
|
+
if (!projectName) {
|
|
565
|
+
// Find most recent running or done task
|
|
566
|
+
const tasks = listTasks();
|
|
567
|
+
const recent = tasks.find(t => t.status === 'running') || tasks[0];
|
|
568
|
+
if (recent) {
|
|
569
|
+
projectName = recent.projectName;
|
|
570
|
+
} else {
|
|
571
|
+
await send(chatId, 'No project specified and no recent tasks.\nUsage: /peek [project] [sessionId]');
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
const project = projects.find(p => p.name === projectName || p.name.toLowerCase() === projectName!.toLowerCase());
|
|
577
|
+
if (!project) {
|
|
578
|
+
await send(chatId, `Project not found: ${projectName}`);
|
|
579
|
+
return;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// Find session
|
|
583
|
+
const sessions = listClaudeSessions(project.name);
|
|
584
|
+
if (sessions.length === 0) {
|
|
585
|
+
await send(chatId, `No sessions for ${project.name}`);
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
const session = sessionId
|
|
590
|
+
? sessions.find(s => s.sessionId.startsWith(sessionId!))
|
|
591
|
+
: sessions[0]; // most recent
|
|
592
|
+
|
|
593
|
+
if (!session) {
|
|
594
|
+
await send(chatId, `Session not found: ${sessionId}`);
|
|
595
|
+
return;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
const filePath = getSessionFilePath(project.name, session.sessionId);
|
|
599
|
+
if (!filePath) {
|
|
600
|
+
await send(chatId, 'Session file not found');
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
await send(chatId, `š Loading ${project.name} / ${session.sessionId.slice(0, 8)}...`);
|
|
605
|
+
|
|
606
|
+
const entries = readSessionEntries(filePath);
|
|
607
|
+
if (entries.length === 0) {
|
|
608
|
+
await send(chatId, 'Session is empty');
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
// Collect last N meaningful entries for raw display
|
|
613
|
+
const recentRaw: string[] = [];
|
|
614
|
+
let rawCount = 0;
|
|
615
|
+
for (let i = entries.length - 1; i >= 0 && rawCount < 8; i--) {
|
|
616
|
+
const e = entries[i];
|
|
617
|
+
if (e.type === 'user') {
|
|
618
|
+
recentRaw.unshift(`š¤ ${e.content.slice(0, 300)}`);
|
|
619
|
+
rawCount++;
|
|
620
|
+
} else if (e.type === 'assistant_text') {
|
|
621
|
+
recentRaw.unshift(`š¤ ${e.content.slice(0, 300)}`);
|
|
622
|
+
rawCount++;
|
|
623
|
+
} else if (e.type === 'tool_use') {
|
|
624
|
+
recentRaw.unshift(`š§ ${e.toolName || 'tool'}`);
|
|
625
|
+
rawCount++;
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
// Build context for AI summary (last ~50 entries)
|
|
630
|
+
const contextEntries: string[] = [];
|
|
631
|
+
let contextLen = 0;
|
|
632
|
+
const MAX_CONTEXT = 8000;
|
|
633
|
+
for (let i = entries.length - 1; i >= 0 && contextLen < MAX_CONTEXT; i--) {
|
|
634
|
+
const e = entries[i];
|
|
635
|
+
let line = '';
|
|
636
|
+
if (e.type === 'user') line = `User: ${e.content}`;
|
|
637
|
+
else if (e.type === 'assistant_text') line = `Assistant: ${e.content}`;
|
|
638
|
+
else if (e.type === 'tool_use') line = `Tool: ${e.toolName || 'tool'}`;
|
|
639
|
+
else continue;
|
|
640
|
+
if (contextLen + line.length > MAX_CONTEXT) break;
|
|
641
|
+
contextEntries.unshift(line);
|
|
642
|
+
contextLen += line.length;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
const telegramModel = loadSettings().telegramModel || 'sonnet';
|
|
646
|
+
const summary = contextEntries.length > 3
|
|
647
|
+
? await aiSummarize(contextEntries.join('\n'), 'Summarize this Claude Code session in 2-3 sentences. What was the user working on? What is the current status? Answer in the same language as the content.')
|
|
648
|
+
: '';
|
|
649
|
+
|
|
650
|
+
// Format output
|
|
651
|
+
const header = `š ${project.name} / ${session.sessionId.slice(0, 8)}\n${entries.length} entries${session.gitBranch ? ` ⢠${session.gitBranch}` : ''}${summary ? ` ⢠AI: ${telegramModel}` : ''}`;
|
|
652
|
+
|
|
653
|
+
const summaryBlock = summary
|
|
654
|
+
? `\n\nš Summary (${telegramModel}):\n${summary}`
|
|
655
|
+
: '';
|
|
656
|
+
|
|
657
|
+
const rawBlock = `\n\n--- Recent ---\n${recentRaw.join('\n\n')}`;
|
|
658
|
+
|
|
659
|
+
const fullText = header + summaryBlock + rawBlock;
|
|
660
|
+
const chunks = splitMessage(fullText, 4000);
|
|
661
|
+
for (const chunk of chunks) {
|
|
662
|
+
await send(chatId, chunk);
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
/**
|
|
667
|
+
* Parse task creation input. Supports:
|
|
668
|
+
* project-name instructions
|
|
669
|
+
* project-name -s sessionId instructions
|
|
670
|
+
* project-name -in 30m instructions
|
|
671
|
+
* project-name -at 2024-01-01T10:00 instructions
|
|
672
|
+
*/
|
|
673
|
+
async function startTaskCreation(chatId: number) {
|
|
674
|
+
const projects = scanProjects();
|
|
675
|
+
if (projects.length === 0) {
|
|
676
|
+
await send(chatId, 'No projects configured. Add project roots in Settings.');
|
|
677
|
+
return;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
const numbered = new Map<number, string>();
|
|
681
|
+
const lines = projects.slice(0, 15).map((p, i) => {
|
|
682
|
+
numbered.set(i + 1, p.name);
|
|
683
|
+
return `${i + 1}. ${p.name}`;
|
|
684
|
+
});
|
|
685
|
+
|
|
686
|
+
chatNumberedProjects.set(chatId, numbered);
|
|
687
|
+
chatListMode.set(chatId, 'task-create');
|
|
688
|
+
|
|
689
|
+
await send(chatId, `š New Task\n\nSelect project:\n${lines.join('\n')}`);
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
async function handleNewTask(chatId: number, input: string) {
|
|
693
|
+
if (!input) {
|
|
694
|
+
await send(chatId,
|
|
695
|
+
'Usage:\nproject: instructions\n\n' +
|
|
696
|
+
'Options:\n' +
|
|
697
|
+
' @agent ā use specific agent (e.g. @codex @aider)\n' +
|
|
698
|
+
' -s <sessionId> ā resume specific session\n' +
|
|
699
|
+
' -in 30m ā delay (e.g. 10m, 2h, 1d)\n' +
|
|
700
|
+
' -at 18:00 ā schedule at time\n\n' +
|
|
701
|
+
'Example:\nmy-app: Fix the login bug\nmy-app @codex: review code\nmy-app -s abc123 -in 1h: continue work'
|
|
702
|
+
);
|
|
703
|
+
return;
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
// Parse project name (before first space or colon)
|
|
707
|
+
const colonIdx = input.indexOf(':');
|
|
708
|
+
let projectPart: string;
|
|
709
|
+
let restPart: string;
|
|
710
|
+
|
|
711
|
+
if (colonIdx > 0 && colonIdx < 40) {
|
|
712
|
+
projectPart = input.slice(0, colonIdx).trim();
|
|
713
|
+
restPart = input.slice(colonIdx + 1).trim();
|
|
714
|
+
} else {
|
|
715
|
+
const spaceIdx = input.indexOf(' ');
|
|
716
|
+
if (spaceIdx < 0) {
|
|
717
|
+
await send(chatId, 'Please provide instructions after the project name.');
|
|
718
|
+
return;
|
|
719
|
+
}
|
|
720
|
+
projectPart = input.slice(0, spaceIdx).trim();
|
|
721
|
+
restPart = input.slice(spaceIdx + 1).trim();
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
const projects = scanProjects();
|
|
725
|
+
const project = projects.find(p => p.name === projectPart || p.name.toLowerCase() === projectPart.toLowerCase());
|
|
726
|
+
|
|
727
|
+
if (!project) {
|
|
728
|
+
const available = projects.slice(0, 10).map(p => ` ${p.name}`).join('\n');
|
|
729
|
+
await send(chatId, `Project not found: ${projectPart}\n\nAvailable:\n${available}`);
|
|
730
|
+
return;
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
// Parse flags
|
|
734
|
+
let sessionId: string | undefined;
|
|
735
|
+
let scheduledAt: string | undefined;
|
|
736
|
+
let agentId: string | undefined;
|
|
737
|
+
let tokens = restPart.split(/\s+/);
|
|
738
|
+
const promptTokens: string[] = [];
|
|
739
|
+
|
|
740
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
741
|
+
if (tokens[i] === '-s' && i + 1 < tokens.length) {
|
|
742
|
+
sessionId = tokens[++i];
|
|
743
|
+
} else if (tokens[i] === '-in' && i + 1 < tokens.length) {
|
|
744
|
+
scheduledAt = parseDelay(tokens[++i]);
|
|
745
|
+
} else if (tokens[i] === '-at' && i + 1 < tokens.length) {
|
|
746
|
+
scheduledAt = parseTimeAt(tokens[++i]);
|
|
747
|
+
} else if (tokens[i].startsWith('@')) {
|
|
748
|
+
agentId = tokens[i].slice(1); // @codex ā codex
|
|
749
|
+
} else {
|
|
750
|
+
promptTokens.push(tokens[i]);
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
const prompt = promptTokens.join(' ');
|
|
755
|
+
if (!prompt) {
|
|
756
|
+
await send(chatId, 'Please provide instructions.');
|
|
757
|
+
return;
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
// Use @agent if specified, else telegram default, else global default
|
|
761
|
+
const settings = loadSettings();
|
|
762
|
+
const resolvedAgent = agentId || settings.telegramAgent || undefined;
|
|
763
|
+
|
|
764
|
+
const task = createTask({
|
|
765
|
+
projectName: project.name,
|
|
766
|
+
projectPath: project.path,
|
|
767
|
+
prompt,
|
|
768
|
+
conversationId: sessionId,
|
|
769
|
+
scheduledAt,
|
|
770
|
+
agent: resolvedAgent,
|
|
771
|
+
});
|
|
772
|
+
|
|
773
|
+
let statusLine = 'Status: queued';
|
|
774
|
+
if (scheduledAt) {
|
|
775
|
+
statusLine = `Scheduled: ${new Date(scheduledAt).toLocaleString()}`;
|
|
776
|
+
}
|
|
777
|
+
if (sessionId) {
|
|
778
|
+
statusLine += `\nSession: ${sessionId.slice(0, 12)}`;
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
const msgId = await send(chatId,
|
|
782
|
+
`š Task created: ${task.id}\n${task.projectName}: ${prompt}\n\n${statusLine}`
|
|
783
|
+
);
|
|
784
|
+
|
|
785
|
+
if (msgId) {
|
|
786
|
+
taskMessageMap.set(msgId, task.id);
|
|
787
|
+
taskChatMap.set(task.id, chatId);
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
function parseDelay(s: string): string | undefined {
|
|
792
|
+
const match = s.match(/^(\d+)(m|h|d)$/);
|
|
793
|
+
if (!match) return undefined;
|
|
794
|
+
const val = Number(match[1]);
|
|
795
|
+
const unit = match[2];
|
|
796
|
+
const ms = unit === 'm' ? val * 60_000 : unit === 'h' ? val * 3600_000 : val * 86400_000;
|
|
797
|
+
return new Date(Date.now() + ms).toISOString();
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
function parseTimeAt(s: string): string | undefined {
|
|
801
|
+
// Try HH:MM format (today)
|
|
802
|
+
const timeMatch = s.match(/^(\d{1,2}):(\d{2})$/);
|
|
803
|
+
if (timeMatch) {
|
|
804
|
+
const now = new Date();
|
|
805
|
+
now.setHours(Number(timeMatch[1]), Number(timeMatch[2]), 0, 0);
|
|
806
|
+
if (now.getTime() < Date.now()) now.setDate(now.getDate() + 1); // next day
|
|
807
|
+
return now.toISOString();
|
|
808
|
+
}
|
|
809
|
+
// Try ISO or date format
|
|
810
|
+
try {
|
|
811
|
+
const d = new Date(s);
|
|
812
|
+
if (!isNaN(d.getTime())) return d.toISOString();
|
|
813
|
+
} catch {}
|
|
814
|
+
return undefined;
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
async function handleFollowUp(chatId: number, taskId: string, message: string) {
|
|
818
|
+
const task = getTask(taskId);
|
|
819
|
+
if (!task) {
|
|
820
|
+
await send(chatId, 'Task not found.');
|
|
821
|
+
return;
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
if (task.status === 'running') {
|
|
825
|
+
await send(chatId, 'ā³ Task still running, wait for it to finish.');
|
|
826
|
+
return;
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
const newTask = createTask({
|
|
830
|
+
projectName: task.projectName,
|
|
831
|
+
projectPath: task.projectPath,
|
|
832
|
+
prompt: message,
|
|
833
|
+
conversationId: task.conversationId || undefined,
|
|
834
|
+
});
|
|
835
|
+
|
|
836
|
+
const msgId = await send(chatId,
|
|
837
|
+
`š Follow-up: ${newTask.id}\nContinuing ${task.projectName} session\n\n${message}`
|
|
838
|
+
);
|
|
839
|
+
|
|
840
|
+
if (msgId) {
|
|
841
|
+
taskMessageMap.set(msgId, newTask.id);
|
|
842
|
+
taskChatMap.set(newTask.id, chatId);
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
async function sendProjectList(chatId: number) {
|
|
847
|
+
const projects = scanProjects();
|
|
848
|
+
const lines = projects.slice(0, 20).map(p =>
|
|
849
|
+
`${p.name}${p.language ? ` (${p.language})` : ''}`
|
|
850
|
+
);
|
|
851
|
+
await send(chatId, `š Projects\n\n${lines.join('\n')}\n\n${projects.length} total`);
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
async function handleCancel(chatId: number, taskId?: string) {
|
|
855
|
+
if (!taskId) { await send(chatId, 'Usage: /cancel <task-id>'); return; }
|
|
856
|
+
const ok = cancelTask(taskId);
|
|
857
|
+
await send(chatId, ok ? `š Task ${taskId} cancelled` : `Cannot cancel task ${taskId}`);
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
async function handleRetry(chatId: number, taskId?: string) {
|
|
861
|
+
if (!taskId) { await send(chatId, 'Usage: /retry <task-id>'); return; }
|
|
862
|
+
const newTask = retryTask(taskId);
|
|
863
|
+
if (!newTask) {
|
|
864
|
+
await send(chatId, `Cannot retry task ${taskId}`);
|
|
865
|
+
return;
|
|
866
|
+
}
|
|
867
|
+
const msgId = await send(chatId, `š Retrying as ${newTask.id}`);
|
|
868
|
+
if (msgId) {
|
|
869
|
+
taskMessageMap.set(msgId, newTask.id);
|
|
870
|
+
taskChatMap.set(newTask.id, chatId);
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
// āāā Watcher Commands āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
875
|
+
|
|
876
|
+
async function handleWatch(chatId: number, projectName?: string, sessionId?: string) {
|
|
877
|
+
if (!projectName) {
|
|
878
|
+
await send(chatId, 'Usage: /watch <project> [sessionId]\n\nMonitors a session and sends updates here.');
|
|
879
|
+
return;
|
|
880
|
+
}
|
|
881
|
+
const label = sessionId ? `${projectName}/${sessionId.slice(0, 8)}` : projectName;
|
|
882
|
+
const watcher = createWatcher({ projectName, sessionId, label });
|
|
883
|
+
await send(chatId, `š Watching: ${label}\nID: ${watcher.id}\nChecking every ${watcher.checkInterval}s`);
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
async function sendWatcherList(chatId: number) {
|
|
887
|
+
const all = listWatchers();
|
|
888
|
+
if (all.length === 0) {
|
|
889
|
+
await send(chatId, 'š No watchers.\n\nUse /watch <project> [sessionId] to add one.');
|
|
890
|
+
return;
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
const lines = all.map((w, i) => {
|
|
894
|
+
const status = w.active ? 'ā' : 'ā';
|
|
895
|
+
const target = w.sessionId ? `${w.projectName}/${w.sessionId.slice(0, 8)}` : w.projectName;
|
|
896
|
+
return `${status} ${w.id} ā ${target} (${w.checkInterval}s)`;
|
|
897
|
+
});
|
|
898
|
+
|
|
899
|
+
await send(chatId, `š Watchers\n\n${lines.join('\n')}\n\nUse /unwatch <id> to remove`);
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
async function handleUnwatch(chatId: number, watcherId?: string) {
|
|
903
|
+
if (!watcherId) {
|
|
904
|
+
await send(chatId, 'Usage: /unwatch <watcher-id>');
|
|
905
|
+
return;
|
|
906
|
+
}
|
|
907
|
+
deleteWatcher(watcherId);
|
|
908
|
+
await send(chatId, `š Watcher ${watcherId} removed`);
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
// āāā Tunnel Commands āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
912
|
+
|
|
913
|
+
async function handleTunnelStatus(chatId: number) {
|
|
914
|
+
const settings = loadSettings();
|
|
915
|
+
if (String(chatId) !== settings.telegramChatId) { await send(chatId, 'ā Unauthorized'); return; }
|
|
916
|
+
|
|
917
|
+
const status = getTunnelStatus();
|
|
918
|
+
if (status.status === 'running' && status.url) {
|
|
919
|
+
await sendHtml(chatId, `š Tunnel running:\n<a href="${status.url}">${status.url}</a>\n\n/tunnel_stop ā stop tunnel`);
|
|
920
|
+
} else if (status.status === 'starting') {
|
|
921
|
+
await send(chatId, 'ā³ Tunnel is starting...');
|
|
922
|
+
} else {
|
|
923
|
+
await send(chatId, `š Tunnel is ${status.status}\n\n/tunnel_start ā start tunnel`);
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
async function handleTunnelStart(chatId: number, password?: string, userMsgId?: number) {
|
|
928
|
+
const settings = loadSettings();
|
|
929
|
+
if (String(chatId) !== settings.telegramChatId) { await send(chatId, 'ā Unauthorized'); return; }
|
|
930
|
+
|
|
931
|
+
// Delete user's message containing password
|
|
932
|
+
if (userMsgId && password) deleteMessageLater(chatId, userMsgId, 0);
|
|
933
|
+
|
|
934
|
+
// Require admin password
|
|
935
|
+
if (!password) {
|
|
936
|
+
await send(chatId, 'š Usage: /tunnel_start <password>');
|
|
937
|
+
return;
|
|
938
|
+
}
|
|
939
|
+
const { verifyAdmin } = require('./password');
|
|
940
|
+
if (!verifyAdmin(password)) {
|
|
941
|
+
await send(chatId, 'ā Wrong password');
|
|
942
|
+
return;
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
// Check if tunnel is already running and still reachable
|
|
946
|
+
const status = getTunnelStatus();
|
|
947
|
+
if (status.status === 'running' && status.url) {
|
|
948
|
+
// Verify it's actually alive
|
|
949
|
+
let alive = false;
|
|
950
|
+
try {
|
|
951
|
+
const controller = new AbortController();
|
|
952
|
+
const timeout = setTimeout(() => controller.abort(), 8000);
|
|
953
|
+
const res = await fetch(status.url, { method: 'HEAD', signal: controller.signal, redirect: 'manual' });
|
|
954
|
+
clearTimeout(timeout);
|
|
955
|
+
alive = res.status > 0;
|
|
956
|
+
} catch {}
|
|
957
|
+
|
|
958
|
+
if (alive) {
|
|
959
|
+
await sendHtml(chatId, `š Tunnel already running:\n<a href="${status.url}">${status.url}</a>`);
|
|
960
|
+
return;
|
|
961
|
+
}
|
|
962
|
+
// Tunnel process alive but URL unreachable ā kill and restart
|
|
963
|
+
await send(chatId, 'š Tunnel URL unreachable, restarting...');
|
|
964
|
+
stopTunnel();
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
await send(chatId, 'š Starting tunnel...');
|
|
968
|
+
const result = await startTunnel();
|
|
969
|
+
if (result.url) {
|
|
970
|
+
const { getSessionCode } = require('./password');
|
|
971
|
+
const code = getSessionCode();
|
|
972
|
+
// Send URL + code, auto-delete after 60 seconds
|
|
973
|
+
const msgUrl = await sendHtml(chatId, `ā
Tunnel started:\n<a href="${result.url}">${result.url}</a>\n\nš Session code: <code>${code || 'N/A'}</code>\n\n<i>This message will be deleted in 60 seconds</i>`);
|
|
974
|
+
if (msgUrl) deleteMessageLater(chatId, msgUrl, 60);
|
|
975
|
+
} else {
|
|
976
|
+
await send(chatId, `ā Failed: ${result.error}`);
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
async function handleTunnelStop(chatId: number) {
|
|
981
|
+
const settings = loadSettings();
|
|
982
|
+
if (String(chatId) !== settings.telegramChatId) { await send(chatId, 'ā Unauthorized'); return; }
|
|
983
|
+
|
|
984
|
+
stopTunnel();
|
|
985
|
+
await send(chatId, 'š Tunnel stopped');
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
async function handleTunnelCode(chatId: number, password?: string, userMsgId?: number) {
|
|
989
|
+
const settings = loadSettings();
|
|
990
|
+
if (String(chatId) !== settings.telegramChatId) {
|
|
991
|
+
await send(chatId, 'ā Unauthorized');
|
|
992
|
+
return;
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
if (!password) {
|
|
996
|
+
await send(chatId, 'Usage: /tunnel_code <admin-password>');
|
|
997
|
+
return;
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
// Immediately delete user's message containing password
|
|
1001
|
+
if (userMsgId) deleteMessageLater(chatId, userMsgId, 0);
|
|
1002
|
+
|
|
1003
|
+
const { verifyAdmin, getSessionCode } = require('./password');
|
|
1004
|
+
if (!verifyAdmin(password)) {
|
|
1005
|
+
await send(chatId, 'ā Wrong password');
|
|
1006
|
+
return;
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
// Show the session code (for remote login 2FA)
|
|
1010
|
+
const code = getSessionCode();
|
|
1011
|
+
const status = getTunnelStatus();
|
|
1012
|
+
if (!code) {
|
|
1013
|
+
await send(chatId, 'ā ļø No session code. Start tunnel first to generate one.');
|
|
1014
|
+
return;
|
|
1015
|
+
}
|
|
1016
|
+
const labelId = await send(chatId, 'š Session code for remote login (auto-deletes in 30s):');
|
|
1017
|
+
const pwId = await sendHtml(chatId, `<code>${code}</code>`);
|
|
1018
|
+
if (labelId) deleteMessageLater(chatId, labelId);
|
|
1019
|
+
if (pwId) deleteMessageLater(chatId, pwId);
|
|
1020
|
+
if (status.status === 'running' && status.url) {
|
|
1021
|
+
const urlLabelId = await send(chatId, 'š URL:');
|
|
1022
|
+
const urlId = await sendHtml(chatId, `<a href="${status.url}">${status.url}</a>`);
|
|
1023
|
+
if (urlLabelId) deleteMessageLater(chatId, urlLabelId);
|
|
1024
|
+
if (urlId) deleteMessageLater(chatId, urlId);
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
// āāā AI Summarize (using Claude Code subscription) āāāāāāāāāāā
|
|
1029
|
+
|
|
1030
|
+
async function aiSummarize(content: string, instruction: string): Promise<string> {
|
|
1031
|
+
try {
|
|
1032
|
+
const settings = loadSettings();
|
|
1033
|
+
const claudePath = settings.claudePath || process.env.CLAUDE_PATH || 'claude';
|
|
1034
|
+
const model = settings.telegramModel || 'sonnet';
|
|
1035
|
+
const { execSync } = require('child_process');
|
|
1036
|
+
const { realpathSync } = require('fs');
|
|
1037
|
+
|
|
1038
|
+
// Resolve claude path
|
|
1039
|
+
let cmd = claudePath;
|
|
1040
|
+
try {
|
|
1041
|
+
const which = execSync(`which ${claudePath}`, { encoding: 'utf-8' }).trim();
|
|
1042
|
+
cmd = realpathSync(which);
|
|
1043
|
+
} catch {}
|
|
1044
|
+
|
|
1045
|
+
const args = ['-p', '--model', model, '--max-turns', '1'];
|
|
1046
|
+
const prompt = `${instruction}\n\nContent:\n${content.slice(0, 8000)}`;
|
|
1047
|
+
|
|
1048
|
+
let execCmd: string;
|
|
1049
|
+
if (cmd.endsWith('.js') || cmd.endsWith('.mjs')) {
|
|
1050
|
+
execCmd = `${process.execPath} ${cmd} ${args.join(' ')}`;
|
|
1051
|
+
} else {
|
|
1052
|
+
execCmd = `${cmd} ${args.join(' ')}`;
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
const result = execSync(execCmd, {
|
|
1056
|
+
input: prompt,
|
|
1057
|
+
encoding: 'utf-8',
|
|
1058
|
+
timeout: 30000,
|
|
1059
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
1060
|
+
env: { ...process.env, CLAUDECODE: undefined },
|
|
1061
|
+
}).trim();
|
|
1062
|
+
|
|
1063
|
+
return result.slice(0, 1000);
|
|
1064
|
+
} catch {
|
|
1065
|
+
return '';
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
// āāā Docs āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
1070
|
+
|
|
1071
|
+
async function handleDocs(chatId: number, input: string) {
|
|
1072
|
+
const settings = loadSettings();
|
|
1073
|
+
if (String(chatId) !== settings.telegramChatId) { await send(chatId, 'ā Unauthorized'); return; }
|
|
1074
|
+
|
|
1075
|
+
const docRoots = (settings.docRoots || []).map((r: string) => r.replace(/^~/, require('os').homedir()));
|
|
1076
|
+
if (docRoots.length === 0) {
|
|
1077
|
+
await send(chatId, 'ā ļø No document directories configured.\nAdd them in Settings ā Document Roots');
|
|
1078
|
+
return;
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
const docRoot = docRoots[0];
|
|
1082
|
+
const { homedir: getHome } = require('os');
|
|
1083
|
+
const { join, extname } = require('path');
|
|
1084
|
+
const { existsSync, readFileSync, readdirSync } = require('fs');
|
|
1085
|
+
|
|
1086
|
+
// /docs <filename> ā search and show file content
|
|
1087
|
+
if (input.trim()) {
|
|
1088
|
+
const query = input.trim().toLowerCase();
|
|
1089
|
+
|
|
1090
|
+
// Recursive search for matching .md files
|
|
1091
|
+
const matches: string[] = [];
|
|
1092
|
+
function searchDir(dir: string, depth: number) {
|
|
1093
|
+
if (depth > 5 || matches.length >= 5) return;
|
|
1094
|
+
try {
|
|
1095
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
1096
|
+
if (entry.name.startsWith('.') || entry.name === 'node_modules') continue;
|
|
1097
|
+
const full = join(dir, entry.name);
|
|
1098
|
+
if (entry.isDirectory()) {
|
|
1099
|
+
searchDir(full, depth + 1);
|
|
1100
|
+
} else if (entry.name.toLowerCase().includes(query) && extname(entry.name) === '.md') {
|
|
1101
|
+
matches.push(full);
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
} catch {}
|
|
1105
|
+
}
|
|
1106
|
+
searchDir(docRoot, 0);
|
|
1107
|
+
|
|
1108
|
+
if (matches.length === 0) {
|
|
1109
|
+
await send(chatId, `No docs matching "${input.trim()}"`);
|
|
1110
|
+
return;
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
// Show first match
|
|
1114
|
+
const filePath = matches[0];
|
|
1115
|
+
const relPath = filePath.replace(docRoot + '/', '');
|
|
1116
|
+
try {
|
|
1117
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
1118
|
+
const preview = content.slice(0, 3500);
|
|
1119
|
+
const truncated = content.length > 3500 ? '\n\n... (truncated)' : '';
|
|
1120
|
+
await send(chatId, `š ${relPath}\n\n${preview}${truncated}`);
|
|
1121
|
+
if (matches.length > 1) {
|
|
1122
|
+
const others = matches.slice(1).map(m => ` ${m.replace(docRoot + '/', '')}`).join('\n');
|
|
1123
|
+
await send(chatId, `Other matches:\n${others}`);
|
|
1124
|
+
}
|
|
1125
|
+
} catch {
|
|
1126
|
+
await send(chatId, `Failed to read: ${relPath}`);
|
|
1127
|
+
}
|
|
1128
|
+
return;
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
// /docs ā show summary of latest Claude session for docs
|
|
1132
|
+
const hash = docRoot.replace(/[^a-zA-Z0-9]/g, '-');
|
|
1133
|
+
const claudeDir = join(getHome(), '.claude', 'projects', hash);
|
|
1134
|
+
|
|
1135
|
+
if (!existsSync(claudeDir)) {
|
|
1136
|
+
await send(chatId, `š Docs: ${docRoot.split('/').pop()}\n\nNo Claude sessions yet. Open Docs tab to start.`);
|
|
1137
|
+
return;
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
// Find latest session
|
|
1141
|
+
let latestFile = '';
|
|
1142
|
+
let latestTime = 0;
|
|
1143
|
+
try {
|
|
1144
|
+
for (const f of readdirSync(claudeDir)) {
|
|
1145
|
+
if (!f.endsWith('.jsonl')) continue;
|
|
1146
|
+
const { statSync } = require('fs');
|
|
1147
|
+
const stat = statSync(join(claudeDir, f));
|
|
1148
|
+
if (stat.mtimeMs > latestTime) {
|
|
1149
|
+
latestTime = stat.mtimeMs;
|
|
1150
|
+
latestFile = f;
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
} catch {}
|
|
1154
|
+
|
|
1155
|
+
if (!latestFile) {
|
|
1156
|
+
await send(chatId, `š Docs: ${docRoot.split('/').pop()}\n\nNo sessions found.`);
|
|
1157
|
+
return;
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
const sessionId = latestFile.replace('.jsonl', '');
|
|
1161
|
+
const filePath = join(claudeDir, latestFile);
|
|
1162
|
+
|
|
1163
|
+
// Read recent entries
|
|
1164
|
+
let entries: string[] = [];
|
|
1165
|
+
try {
|
|
1166
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
1167
|
+
const lines = content.split('\n').filter(Boolean);
|
|
1168
|
+
const recentLines = lines.slice(-30);
|
|
1169
|
+
|
|
1170
|
+
for (const line of recentLines) {
|
|
1171
|
+
try {
|
|
1172
|
+
const entry = JSON.parse(line);
|
|
1173
|
+
if (entry.type === 'human' || entry.role === 'user') {
|
|
1174
|
+
const text = typeof entry.message === 'string' ? entry.message : entry.message?.content?.[0]?.text || '';
|
|
1175
|
+
if (text) entries.push(`š¤ ${text.slice(0, 200)}`);
|
|
1176
|
+
} else if (entry.type === 'assistant' && entry.message?.content) {
|
|
1177
|
+
for (const block of entry.message.content) {
|
|
1178
|
+
if (block.type === 'text' && block.text) {
|
|
1179
|
+
entries.push(`š¤ ${block.text.slice(0, 200)}`);
|
|
1180
|
+
} else if (block.type === 'tool_use') {
|
|
1181
|
+
entries.push(`š§ ${block.name || 'tool'}`);
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
} catch {}
|
|
1186
|
+
}
|
|
1187
|
+
} catch {}
|
|
1188
|
+
|
|
1189
|
+
const recent = entries.slice(-8).join('\n\n');
|
|
1190
|
+
const tModel = loadSettings().telegramModel || 'sonnet';
|
|
1191
|
+
const summary = entries.length > 3
|
|
1192
|
+
? await aiSummarize(entries.slice(-15).join('\n'), 'Summarize this Claude Code session in 2-3 sentences. What was the user working on? What is the current status? Answer in the same language as the content.')
|
|
1193
|
+
: '';
|
|
1194
|
+
const header = `š Docs: ${docRoot.split('/').pop()}\nš Session: ${sessionId.slice(0, 12)}${summary ? ` ⢠AI: ${tModel}` : ''}\n`;
|
|
1195
|
+
const summaryBlock = summary ? `\nš (${tModel}) ${summary}\n` : '';
|
|
1196
|
+
|
|
1197
|
+
const fullText = header + summaryBlock + '\n--- Recent ---\n' + recent;
|
|
1198
|
+
|
|
1199
|
+
const chunks = splitMessage(fullText, 4000);
|
|
1200
|
+
for (const chunk of chunks) {
|
|
1201
|
+
await send(chatId, chunk);
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
// āāā Docs Write (Quick Notes) āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
1206
|
+
|
|
1207
|
+
async function handleDocsWrite(chatId: number, content: string) {
|
|
1208
|
+
const settings = loadSettings();
|
|
1209
|
+
if (String(chatId) !== settings.telegramChatId) { await send(chatId, 'ā Unauthorized'); return; }
|
|
1210
|
+
|
|
1211
|
+
if (!content) {
|
|
1212
|
+
pendingNote.add(chatId);
|
|
1213
|
+
await send(chatId, 'š Send your note content:');
|
|
1214
|
+
return;
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
await sendNoteToDocsClaude(chatId, content);
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
async function sendNoteToDocsClaude(chatId: number, content: string) {
|
|
1221
|
+
const settings = loadSettings();
|
|
1222
|
+
const docRoots = (settings.docRoots || []).map((r: string) => r.replace(/^~/, require('os').homedir()));
|
|
1223
|
+
|
|
1224
|
+
if (docRoots.length === 0) {
|
|
1225
|
+
await send(chatId, 'ā ļø No document directories configured.');
|
|
1226
|
+
return;
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
const { execSync, spawnSync } = require('child_process');
|
|
1230
|
+
const { writeFileSync, unlinkSync } = require('fs');
|
|
1231
|
+
const { join } = require('path');
|
|
1232
|
+
const { homedir } = require('os');
|
|
1233
|
+
const SESSION_NAME = 'mw-docs-claude';
|
|
1234
|
+
const docRoot = docRoots[0];
|
|
1235
|
+
|
|
1236
|
+
// Check if the docs tmux session exists
|
|
1237
|
+
let sessionExists = false;
|
|
1238
|
+
try {
|
|
1239
|
+
execSync(`tmux has-session -t ${SESSION_NAME} 2>/dev/null`);
|
|
1240
|
+
sessionExists = true;
|
|
1241
|
+
} catch {}
|
|
1242
|
+
|
|
1243
|
+
// Auto-create session if it doesn't exist
|
|
1244
|
+
if (!sessionExists) {
|
|
1245
|
+
try {
|
|
1246
|
+
execSync(`tmux new-session -d -s ${SESSION_NAME} -x 120 -y 30`, { timeout: 5000 });
|
|
1247
|
+
// Wait for shell to initialize
|
|
1248
|
+
await new Promise(r => setTimeout(r, 500));
|
|
1249
|
+
// cd to doc root and start claude
|
|
1250
|
+
const sf = settings.skipPermissions ? ' --dangerously-skip-permissions' : '';
|
|
1251
|
+
spawnSync('tmux', ['send-keys', '-t', SESSION_NAME, `cd "${docRoot}" && claude -c${sf}`, 'Enter'], { timeout: 5000 });
|
|
1252
|
+
// Wait for Claude to start up
|
|
1253
|
+
await new Promise(r => setTimeout(r, 3000));
|
|
1254
|
+
await send(chatId, 'š Auto-started Docs Claude session.');
|
|
1255
|
+
} catch (err) {
|
|
1256
|
+
await send(chatId, 'ā Failed to create Docs Claude session.');
|
|
1257
|
+
return;
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
// Check if Claude is the active process (not shell)
|
|
1262
|
+
let paneCmd = '';
|
|
1263
|
+
try {
|
|
1264
|
+
paneCmd = execSync(`tmux display-message -p -t ${SESSION_NAME} '#{pane_current_command}'`, { encoding: 'utf-8', timeout: 2000 }).trim();
|
|
1265
|
+
} catch {}
|
|
1266
|
+
|
|
1267
|
+
// If Claude is not running, start it
|
|
1268
|
+
if (paneCmd === 'zsh' || paneCmd === 'bash' || paneCmd === 'fish' || !paneCmd) {
|
|
1269
|
+
try {
|
|
1270
|
+
const sf = settings.skipPermissions ? ' --dangerously-skip-permissions' : '';
|
|
1271
|
+
spawnSync('tmux', ['send-keys', '-t', SESSION_NAME, `cd "${docRoot}" && claude -c${sf}`, 'Enter'], { timeout: 5000 });
|
|
1272
|
+
await new Promise(r => setTimeout(r, 3000));
|
|
1273
|
+
await send(chatId, 'š Auto-started Claude in Docs session.');
|
|
1274
|
+
} catch {
|
|
1275
|
+
await send(chatId, 'ā Failed to start Claude in Docs session.');
|
|
1276
|
+
return;
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
// Write content to a temp file, then use tmux to send a prompt referencing it
|
|
1281
|
+
const { getDataDir: _getDataDir } = require('./dirs');
|
|
1282
|
+
const tmpFile = join(_getDataDir(), '.note-tmp.txt');
|
|
1283
|
+
try {
|
|
1284
|
+
writeFileSync(tmpFile, content, 'utf-8');
|
|
1285
|
+
|
|
1286
|
+
// Send a single-line prompt to Claude via tmux send-keys using the temp file
|
|
1287
|
+
const prompt = `Please read the file ${tmpFile} and save its content as a note in the appropriate location in my docs. Analyze the content to determine the best file and location. After saving, delete the temp file.`;
|
|
1288
|
+
|
|
1289
|
+
// Use tmux send-keys with literal flag to avoid interpretation issues
|
|
1290
|
+
spawnSync('tmux', ['send-keys', '-t', SESSION_NAME, '-l', prompt], { timeout: 5000 });
|
|
1291
|
+
// Send Enter separately
|
|
1292
|
+
spawnSync('tmux', ['send-keys', '-t', SESSION_NAME, 'Enter'], { timeout: 2000 });
|
|
1293
|
+
|
|
1294
|
+
await send(chatId, `š Note sent to Docs Claude:\n\n${content.slice(0, 200)}${content.length > 200 ? '...' : ''}`);
|
|
1295
|
+
} catch (err) {
|
|
1296
|
+
try { unlinkSync(tmpFile); } catch {}
|
|
1297
|
+
await send(chatId, 'ā Failed to send note to Claude session');
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
// āāā Real-time Streaming āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
1302
|
+
|
|
1303
|
+
function bufferLogEntry(taskId: string, chatId: number, entry: TaskLogEntry) {
|
|
1304
|
+
taskChatMap.set(taskId, chatId);
|
|
1305
|
+
|
|
1306
|
+
let buf = logBuffers.get(taskId);
|
|
1307
|
+
if (!buf) {
|
|
1308
|
+
buf = { entries: [], timer: null };
|
|
1309
|
+
logBuffers.set(taskId, buf);
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
let line = '';
|
|
1313
|
+
if (entry.subtype === 'tool_use') {
|
|
1314
|
+
line = `š§ ${entry.tool || 'tool'}: ${entry.content.slice(0, 80)}`;
|
|
1315
|
+
} else if (entry.subtype === 'text') {
|
|
1316
|
+
line = entry.content.slice(0, 200);
|
|
1317
|
+
} else if (entry.type === 'result') {
|
|
1318
|
+
line = `ā
${entry.content.slice(0, 200)}`;
|
|
1319
|
+
} else if (entry.subtype === 'error') {
|
|
1320
|
+
line = `ā ${entry.content.slice(0, 200)}`;
|
|
1321
|
+
}
|
|
1322
|
+
if (!line) return;
|
|
1323
|
+
|
|
1324
|
+
buf.entries.push(line);
|
|
1325
|
+
|
|
1326
|
+
if (!buf.timer) {
|
|
1327
|
+
buf.timer = setTimeout(() => flushLogBuffer(taskId, chatId), 3000);
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
async function flushLogBuffer(taskId: string, chatId: number) {
|
|
1332
|
+
const buf = logBuffers.get(taskId);
|
|
1333
|
+
if (!buf || buf.entries.length === 0) return;
|
|
1334
|
+
|
|
1335
|
+
const text = buf.entries.join('\n');
|
|
1336
|
+
buf.entries = [];
|
|
1337
|
+
buf.timer = null;
|
|
1338
|
+
|
|
1339
|
+
await send(chatId, text);
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
async function handleStatusChange(taskId: string, chatId: number, status: string) {
|
|
1343
|
+
await flushLogBuffer(taskId, chatId);
|
|
1344
|
+
|
|
1345
|
+
const task = getTask(taskId);
|
|
1346
|
+
if (!task) return;
|
|
1347
|
+
|
|
1348
|
+
const targetChat = taskChatMap.get(taskId) || chatId;
|
|
1349
|
+
|
|
1350
|
+
if (status === 'running') {
|
|
1351
|
+
const msgId = await send(targetChat,
|
|
1352
|
+
`š Started: ${taskId}\n${task.projectName}: ${task.prompt.slice(0, 100)}`
|
|
1353
|
+
);
|
|
1354
|
+
if (msgId) taskMessageMap.set(msgId, taskId);
|
|
1355
|
+
} else if (status === 'done') {
|
|
1356
|
+
const cost = task.costUSD != null ? `Cost: $${task.costUSD.toFixed(4)}\n` : '';
|
|
1357
|
+
const result = task.resultSummary ? task.resultSummary.slice(0, 800) : '';
|
|
1358
|
+
const msgId = await send(targetChat,
|
|
1359
|
+
`ā
Done: ${taskId}\n${task.projectName}\n${cost}${result ? `\n${result}` : ''}\n\nš¬ Reply to continue`
|
|
1360
|
+
);
|
|
1361
|
+
if (msgId) taskMessageMap.set(msgId, taskId);
|
|
1362
|
+
} else if (status === 'failed') {
|
|
1363
|
+
const msgId = await send(targetChat,
|
|
1364
|
+
`ā Failed: ${taskId}\n${task.error || 'Unknown error'}\n\n/retry ${taskId}`
|
|
1365
|
+
);
|
|
1366
|
+
if (msgId) taskMessageMap.set(msgId, taskId);
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
// āāā Telegram API āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
1371
|
+
|
|
1372
|
+
async function send(chatId: number, text: string): Promise<number | null> {
|
|
1373
|
+
const settings = loadSettings();
|
|
1374
|
+
if (!settings.telegramBotToken) return null;
|
|
1375
|
+
|
|
1376
|
+
try {
|
|
1377
|
+
const url = `https://api.telegram.org/bot${settings.telegramBotToken}/sendMessage`;
|
|
1378
|
+
const res = await fetch(url, {
|
|
1379
|
+
method: 'POST',
|
|
1380
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1381
|
+
body: JSON.stringify({
|
|
1382
|
+
chat_id: chatId,
|
|
1383
|
+
text,
|
|
1384
|
+
disable_web_page_preview: true,
|
|
1385
|
+
}),
|
|
1386
|
+
});
|
|
1387
|
+
|
|
1388
|
+
const data = await res.json();
|
|
1389
|
+
if (!data.ok) {
|
|
1390
|
+
console.error('[telegram] Send error:', data.description);
|
|
1391
|
+
return null;
|
|
1392
|
+
}
|
|
1393
|
+
return data.result?.message_id || null;
|
|
1394
|
+
} catch (err) {
|
|
1395
|
+
console.error('[telegram] Send failed:', err);
|
|
1396
|
+
return null;
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
/** Delete a message after a delay (seconds) */
|
|
1401
|
+
function deleteMessageLater(chatId: number, messageId: number, delaySec: number = 30) {
|
|
1402
|
+
setTimeout(async () => {
|
|
1403
|
+
const settings = loadSettings();
|
|
1404
|
+
if (!settings.telegramBotToken) return;
|
|
1405
|
+
try {
|
|
1406
|
+
await fetch(`https://api.telegram.org/bot${settings.telegramBotToken}/deleteMessage`, {
|
|
1407
|
+
method: 'POST',
|
|
1408
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1409
|
+
body: JSON.stringify({ chat_id: chatId, message_id: messageId }),
|
|
1410
|
+
});
|
|
1411
|
+
} catch {}
|
|
1412
|
+
}, delaySec * 1000);
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
/** Set bot command menu for quick access */
|
|
1416
|
+
async function setBotCommands(token: string) {
|
|
1417
|
+
try {
|
|
1418
|
+
await fetch(`https://api.telegram.org/bot${token}/setMyCommands`, {
|
|
1419
|
+
method: 'POST',
|
|
1420
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1421
|
+
body: JSON.stringify({
|
|
1422
|
+
commands: [
|
|
1423
|
+
{ command: 'task', description: 'Create task' },
|
|
1424
|
+
{ command: 'tasks', description: 'List tasks' },
|
|
1425
|
+
{ command: 'sessions', description: 'Session summary (AI)' },
|
|
1426
|
+
{ command: 'docs', description: 'Docs summary / view file' },
|
|
1427
|
+
{ command: 'note', description: 'Quick note to docs' },
|
|
1428
|
+
{ command: 'watch', description: 'Monitor session / list watchers' },
|
|
1429
|
+
{ command: 'tunnel', description: 'Tunnel status' },
|
|
1430
|
+
{ command: 'tunnel_start', description: 'Start tunnel' },
|
|
1431
|
+
{ command: 'tunnel_stop', description: 'Stop tunnel' },
|
|
1432
|
+
{ command: 'tunnel_code', description: 'Get session code for remote login' },
|
|
1433
|
+
{ command: 'help', description: 'Show help' },
|
|
1434
|
+
],
|
|
1435
|
+
}),
|
|
1436
|
+
});
|
|
1437
|
+
} catch {}
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1440
|
+
async function sendHtml(chatId: number, html: string): Promise<number | null> {
|
|
1441
|
+
const settings = loadSettings();
|
|
1442
|
+
if (!settings.telegramBotToken) return null;
|
|
1443
|
+
|
|
1444
|
+
try {
|
|
1445
|
+
const url = `https://api.telegram.org/bot${settings.telegramBotToken}/sendMessage`;
|
|
1446
|
+
const res = await fetch(url, {
|
|
1447
|
+
method: 'POST',
|
|
1448
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1449
|
+
body: JSON.stringify({
|
|
1450
|
+
chat_id: chatId,
|
|
1451
|
+
text: html,
|
|
1452
|
+
parse_mode: 'HTML',
|
|
1453
|
+
disable_web_page_preview: true,
|
|
1454
|
+
}),
|
|
1455
|
+
});
|
|
1456
|
+
|
|
1457
|
+
const data = await res.json();
|
|
1458
|
+
if (!data.ok) {
|
|
1459
|
+
return send(chatId, html.replace(/<[^>]+>/g, ''));
|
|
1460
|
+
}
|
|
1461
|
+
return data.result?.message_id || null;
|
|
1462
|
+
} catch {
|
|
1463
|
+
return send(chatId, html.replace(/<[^>]+>/g, ''));
|
|
1464
|
+
}
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
function splitMessage(text: string, maxLen: number): string[] {
|
|
1468
|
+
if (text.length <= maxLen) return [text];
|
|
1469
|
+
const chunks: string[] = [];
|
|
1470
|
+
while (text.length > 0) {
|
|
1471
|
+
const cut = text.lastIndexOf('\n', maxLen);
|
|
1472
|
+
const splitAt = cut > 0 ? cut : maxLen;
|
|
1473
|
+
chunks.push(text.slice(0, splitAt));
|
|
1474
|
+
text = text.slice(splitAt).trimStart();
|
|
1475
|
+
}
|
|
1476
|
+
return chunks;
|
|
1477
|
+
}
|