@aion0/forge 0.5.26 → 0.5.27

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (253) hide show
  1. package/.forge/worktrees/pipeline-4dd8dc2d/CLAUDE.md +86 -0
  2. package/.forge/worktrees/pipeline-4dd8dc2d/README.md +136 -0
  3. package/.forge/worktrees/pipeline-4dd8dc2d/RELEASE_NOTES.md +36 -0
  4. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/agents/route.ts +17 -0
  5. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/auth/[...nextauth]/route.ts +3 -0
  6. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/auth/verify/route.ts +46 -0
  7. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/claude/[id]/route.ts +31 -0
  8. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/claude/[id]/stream/route.ts +63 -0
  9. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/claude/route.ts +28 -0
  10. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/claude-sessions/[projectName]/entries/route.ts +23 -0
  11. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/claude-sessions/[projectName]/live/route.ts +72 -0
  12. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/claude-sessions/[projectName]/route.ts +37 -0
  13. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/claude-sessions/sync/route.ts +17 -0
  14. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/claude-templates/route.ts +145 -0
  15. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/code/route.ts +299 -0
  16. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/delivery/[id]/route.ts +62 -0
  17. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/delivery/route.ts +40 -0
  18. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/detect-cli/route.ts +46 -0
  19. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/docs/route.ts +176 -0
  20. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/docs/sessions/route.ts +54 -0
  21. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/favorites/route.ts +26 -0
  22. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/flows/route.ts +6 -0
  23. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/flows/run/route.ts +19 -0
  24. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/git/route.ts +149 -0
  25. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/help/route.ts +84 -0
  26. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/issue-scanner/route.ts +116 -0
  27. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/logs/route.ts +100 -0
  28. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/mobile-chat/route.ts +115 -0
  29. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/monitor/route.ts +74 -0
  30. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/notifications/route.ts +42 -0
  31. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/notify/test/route.ts +33 -0
  32. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/online/route.ts +40 -0
  33. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/pipelines/[id]/route.ts +41 -0
  34. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/pipelines/route.ts +90 -0
  35. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/plugins/route.ts +75 -0
  36. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/preview/[...path]/route.ts +64 -0
  37. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/preview/route.ts +156 -0
  38. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/project-pipelines/route.ts +91 -0
  39. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/project-sessions/route.ts +61 -0
  40. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/projects/route.ts +26 -0
  41. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/sessions/[id]/chat/route.ts +64 -0
  42. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/sessions/[id]/messages/route.ts +9 -0
  43. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/sessions/[id]/route.ts +17 -0
  44. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/sessions/route.ts +20 -0
  45. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/settings/route.ts +64 -0
  46. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/skills/local/route.ts +228 -0
  47. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/skills/route.ts +182 -0
  48. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/smith-templates/route.ts +81 -0
  49. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/status/route.ts +12 -0
  50. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/tabs/route.ts +25 -0
  51. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/tasks/[id]/route.ts +51 -0
  52. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/tasks/[id]/stream/route.ts +77 -0
  53. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/tasks/link/route.ts +37 -0
  54. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/tasks/route.ts +44 -0
  55. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/tasks/session/route.ts +14 -0
  56. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/telegram/route.ts +23 -0
  57. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/templates/route.ts +6 -0
  58. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/terminal-bell/route.ts +39 -0
  59. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/terminal-cwd/route.ts +19 -0
  60. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/terminal-state/route.ts +15 -0
  61. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/tunnel/route.ts +26 -0
  62. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/upgrade/route.ts +43 -0
  63. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/usage/route.ts +20 -0
  64. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/version/route.ts +78 -0
  65. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/watchers/route.ts +33 -0
  66. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/workspace/[id]/agents/route.ts +35 -0
  67. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/workspace/[id]/memory/route.ts +23 -0
  68. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/workspace/[id]/smith/route.ts +22 -0
  69. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/workspace/[id]/stream/route.ts +31 -0
  70. package/.forge/worktrees/pipeline-4dd8dc2d/app/api/workspace/route.ts +79 -0
  71. package/.forge/worktrees/pipeline-4dd8dc2d/app/global-error.tsx +21 -0
  72. package/.forge/worktrees/pipeline-4dd8dc2d/app/globals.css +52 -0
  73. package/.forge/worktrees/pipeline-4dd8dc2d/app/icon.ico +0 -0
  74. package/.forge/worktrees/pipeline-4dd8dc2d/app/icon.png +0 -0
  75. package/.forge/worktrees/pipeline-4dd8dc2d/app/icon.svg +106 -0
  76. package/.forge/worktrees/pipeline-4dd8dc2d/app/layout.tsx +17 -0
  77. package/.forge/worktrees/pipeline-4dd8dc2d/app/login/LoginForm.tsx +96 -0
  78. package/.forge/worktrees/pipeline-4dd8dc2d/app/login/page.tsx +10 -0
  79. package/.forge/worktrees/pipeline-4dd8dc2d/app/mobile/page.tsx +10 -0
  80. package/.forge/worktrees/pipeline-4dd8dc2d/app/page.tsx +22 -0
  81. package/.forge/worktrees/pipeline-4dd8dc2d/bin/forge-server.mjs +484 -0
  82. package/.forge/worktrees/pipeline-4dd8dc2d/check-forge-status.sh +71 -0
  83. package/.forge/worktrees/pipeline-4dd8dc2d/cli/mw.ts +579 -0
  84. package/.forge/worktrees/pipeline-4dd8dc2d/components/BrowserPanel.tsx +175 -0
  85. package/.forge/worktrees/pipeline-4dd8dc2d/components/ChatPanel.tsx +191 -0
  86. package/.forge/worktrees/pipeline-4dd8dc2d/components/ClaudeTerminal.tsx +267 -0
  87. package/.forge/worktrees/pipeline-4dd8dc2d/components/CodeViewer.tsx +787 -0
  88. package/.forge/worktrees/pipeline-4dd8dc2d/components/ConversationEditor.tsx +411 -0
  89. package/.forge/worktrees/pipeline-4dd8dc2d/components/ConversationGraphView.tsx +347 -0
  90. package/.forge/worktrees/pipeline-4dd8dc2d/components/ConversationTerminalView.tsx +303 -0
  91. package/.forge/worktrees/pipeline-4dd8dc2d/components/Dashboard.tsx +807 -0
  92. package/.forge/worktrees/pipeline-4dd8dc2d/components/DashboardWrapper.tsx +9 -0
  93. package/.forge/worktrees/pipeline-4dd8dc2d/components/DeliveryFlowEditor.tsx +491 -0
  94. package/.forge/worktrees/pipeline-4dd8dc2d/components/DeliveryList.tsx +230 -0
  95. package/.forge/worktrees/pipeline-4dd8dc2d/components/DeliveryWorkspace.tsx +589 -0
  96. package/.forge/worktrees/pipeline-4dd8dc2d/components/DocTerminal.tsx +187 -0
  97. package/.forge/worktrees/pipeline-4dd8dc2d/components/DocsViewer.tsx +574 -0
  98. package/.forge/worktrees/pipeline-4dd8dc2d/components/HelpDialog.tsx +169 -0
  99. package/.forge/worktrees/pipeline-4dd8dc2d/components/HelpTerminal.tsx +141 -0
  100. package/.forge/worktrees/pipeline-4dd8dc2d/components/InlinePipelineView.tsx +111 -0
  101. package/.forge/worktrees/pipeline-4dd8dc2d/components/LogViewer.tsx +194 -0
  102. package/.forge/worktrees/pipeline-4dd8dc2d/components/MarkdownContent.tsx +73 -0
  103. package/.forge/worktrees/pipeline-4dd8dc2d/components/MobileView.tsx +385 -0
  104. package/.forge/worktrees/pipeline-4dd8dc2d/components/MonitorPanel.tsx +122 -0
  105. package/.forge/worktrees/pipeline-4dd8dc2d/components/NewSessionModal.tsx +93 -0
  106. package/.forge/worktrees/pipeline-4dd8dc2d/components/NewTaskModal.tsx +492 -0
  107. package/.forge/worktrees/pipeline-4dd8dc2d/components/PipelineEditor.tsx +570 -0
  108. package/.forge/worktrees/pipeline-4dd8dc2d/components/PipelineView.tsx +1018 -0
  109. package/.forge/worktrees/pipeline-4dd8dc2d/components/PluginsPanel.tsx +472 -0
  110. package/.forge/worktrees/pipeline-4dd8dc2d/components/ProjectDetail.tsx +1618 -0
  111. package/.forge/worktrees/pipeline-4dd8dc2d/components/ProjectList.tsx +108 -0
  112. package/.forge/worktrees/pipeline-4dd8dc2d/components/ProjectManager.tsx +401 -0
  113. package/.forge/worktrees/pipeline-4dd8dc2d/components/SessionList.tsx +74 -0
  114. package/.forge/worktrees/pipeline-4dd8dc2d/components/SessionView.tsx +726 -0
  115. package/.forge/worktrees/pipeline-4dd8dc2d/components/SettingsModal.tsx +1647 -0
  116. package/.forge/worktrees/pipeline-4dd8dc2d/components/SkillsPanel.tsx +969 -0
  117. package/.forge/worktrees/pipeline-4dd8dc2d/components/StatusBar.tsx +99 -0
  118. package/.forge/worktrees/pipeline-4dd8dc2d/components/TabBar.tsx +46 -0
  119. package/.forge/worktrees/pipeline-4dd8dc2d/components/TaskBoard.tsx +113 -0
  120. package/.forge/worktrees/pipeline-4dd8dc2d/components/TaskDetail.tsx +372 -0
  121. package/.forge/worktrees/pipeline-4dd8dc2d/components/TerminalLauncher.tsx +398 -0
  122. package/.forge/worktrees/pipeline-4dd8dc2d/components/TunnelToggle.tsx +206 -0
  123. package/.forge/worktrees/pipeline-4dd8dc2d/components/UsagePanel.tsx +207 -0
  124. package/.forge/worktrees/pipeline-4dd8dc2d/components/WebTerminal.tsx +1743 -0
  125. package/.forge/worktrees/pipeline-4dd8dc2d/components/WorkspaceTree.tsx +221 -0
  126. package/.forge/worktrees/pipeline-4dd8dc2d/components/WorkspaceView.tsx +4048 -0
  127. package/.forge/worktrees/pipeline-4dd8dc2d/dev-test.sh +5 -0
  128. package/.forge/worktrees/pipeline-4dd8dc2d/docs/Forge_Memory_Layer_Design.docx +0 -0
  129. package/.forge/worktrees/pipeline-4dd8dc2d/docs/Forge_Strategy_Research_2026.docx +0 -0
  130. package/.forge/worktrees/pipeline-4dd8dc2d/docs/LOCAL-DEPLOY.md +144 -0
  131. package/.forge/worktrees/pipeline-4dd8dc2d/docs/roadmap-multi-agent-workflow.md +330 -0
  132. package/.forge/worktrees/pipeline-4dd8dc2d/forge-logo.png +0 -0
  133. package/.forge/worktrees/pipeline-4dd8dc2d/forge-logo.svg +106 -0
  134. package/.forge/worktrees/pipeline-4dd8dc2d/hooks/useSidebarResize.ts +52 -0
  135. package/.forge/worktrees/pipeline-4dd8dc2d/install.sh +29 -0
  136. package/.forge/worktrees/pipeline-4dd8dc2d/instrumentation.ts +35 -0
  137. package/.forge/worktrees/pipeline-4dd8dc2d/lib/agents/claude-adapter.ts +104 -0
  138. package/.forge/worktrees/pipeline-4dd8dc2d/lib/agents/generic-adapter.ts +64 -0
  139. package/.forge/worktrees/pipeline-4dd8dc2d/lib/agents/index.ts +245 -0
  140. package/.forge/worktrees/pipeline-4dd8dc2d/lib/agents/types.ts +70 -0
  141. package/.forge/worktrees/pipeline-4dd8dc2d/lib/artifacts.ts +106 -0
  142. package/.forge/worktrees/pipeline-4dd8dc2d/lib/auth.ts +62 -0
  143. package/.forge/worktrees/pipeline-4dd8dc2d/lib/builtin-plugins/docker.yaml +70 -0
  144. package/.forge/worktrees/pipeline-4dd8dc2d/lib/builtin-plugins/http.yaml +66 -0
  145. package/.forge/worktrees/pipeline-4dd8dc2d/lib/builtin-plugins/jenkins.yaml +92 -0
  146. package/.forge/worktrees/pipeline-4dd8dc2d/lib/builtin-plugins/llm-vision.yaml +85 -0
  147. package/.forge/worktrees/pipeline-4dd8dc2d/lib/builtin-plugins/playwright.yaml +111 -0
  148. package/.forge/worktrees/pipeline-4dd8dc2d/lib/builtin-plugins/shell-command.yaml +60 -0
  149. package/.forge/worktrees/pipeline-4dd8dc2d/lib/builtin-plugins/slack.yaml +48 -0
  150. package/.forge/worktrees/pipeline-4dd8dc2d/lib/builtin-plugins/webhook.yaml +56 -0
  151. package/.forge/worktrees/pipeline-4dd8dc2d/lib/claude-process.ts +361 -0
  152. package/.forge/worktrees/pipeline-4dd8dc2d/lib/claude-sessions.ts +266 -0
  153. package/.forge/worktrees/pipeline-4dd8dc2d/lib/claude-templates.ts +227 -0
  154. package/.forge/worktrees/pipeline-4dd8dc2d/lib/cloudflared.ts +424 -0
  155. package/.forge/worktrees/pipeline-4dd8dc2d/lib/crypto.ts +67 -0
  156. package/.forge/worktrees/pipeline-4dd8dc2d/lib/delivery.ts +787 -0
  157. package/.forge/worktrees/pipeline-4dd8dc2d/lib/dirs.ts +99 -0
  158. package/.forge/worktrees/pipeline-4dd8dc2d/lib/flows.ts +86 -0
  159. package/.forge/worktrees/pipeline-4dd8dc2d/lib/forge-mcp-server.ts +732 -0
  160. package/.forge/worktrees/pipeline-4dd8dc2d/lib/forge-skills/forge-inbox.md +38 -0
  161. package/.forge/worktrees/pipeline-4dd8dc2d/lib/forge-skills/forge-send.md +47 -0
  162. package/.forge/worktrees/pipeline-4dd8dc2d/lib/forge-skills/forge-status.md +32 -0
  163. package/.forge/worktrees/pipeline-4dd8dc2d/lib/forge-skills/forge-workspace-sync.md +37 -0
  164. package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/00-overview.md +40 -0
  165. package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/01-settings.md +194 -0
  166. package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/02-telegram.md +41 -0
  167. package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/03-tunnel.md +31 -0
  168. package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/04-tasks.md +52 -0
  169. package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/05-pipelines.md +460 -0
  170. package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/06-skills.md +43 -0
  171. package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/07-projects.md +73 -0
  172. package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/08-rules.md +53 -0
  173. package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/09-issue-autofix.md +55 -0
  174. package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/10-troubleshooting.md +89 -0
  175. package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/11-workspace.md +810 -0
  176. package/.forge/worktrees/pipeline-4dd8dc2d/lib/help-docs/CLAUDE.md +62 -0
  177. package/.forge/worktrees/pipeline-4dd8dc2d/lib/init.ts +266 -0
  178. package/.forge/worktrees/pipeline-4dd8dc2d/lib/issue-scanner.ts +298 -0
  179. package/.forge/worktrees/pipeline-4dd8dc2d/lib/logger.ts +79 -0
  180. package/.forge/worktrees/pipeline-4dd8dc2d/lib/notifications.ts +75 -0
  181. package/.forge/worktrees/pipeline-4dd8dc2d/lib/notify.ts +108 -0
  182. package/.forge/worktrees/pipeline-4dd8dc2d/lib/password.ts +97 -0
  183. package/.forge/worktrees/pipeline-4dd8dc2d/lib/pipeline-scheduler.ts +373 -0
  184. package/.forge/worktrees/pipeline-4dd8dc2d/lib/pipeline.ts +1565 -0
  185. package/.forge/worktrees/pipeline-4dd8dc2d/lib/plugins/executor.ts +347 -0
  186. package/.forge/worktrees/pipeline-4dd8dc2d/lib/plugins/registry.ts +228 -0
  187. package/.forge/worktrees/pipeline-4dd8dc2d/lib/plugins/types.ts +103 -0
  188. package/.forge/worktrees/pipeline-4dd8dc2d/lib/project-sessions.ts +53 -0
  189. package/.forge/worktrees/pipeline-4dd8dc2d/lib/projects.ts +86 -0
  190. package/.forge/worktrees/pipeline-4dd8dc2d/lib/session-manager.ts +156 -0
  191. package/.forge/worktrees/pipeline-4dd8dc2d/lib/session-utils.ts +53 -0
  192. package/.forge/worktrees/pipeline-4dd8dc2d/lib/session-watcher.ts +345 -0
  193. package/.forge/worktrees/pipeline-4dd8dc2d/lib/settings.ts +195 -0
  194. package/.forge/worktrees/pipeline-4dd8dc2d/lib/skills.ts +458 -0
  195. package/.forge/worktrees/pipeline-4dd8dc2d/lib/task-manager.ts +951 -0
  196. package/.forge/worktrees/pipeline-4dd8dc2d/lib/telegram-bot.ts +1477 -0
  197. package/.forge/worktrees/pipeline-4dd8dc2d/lib/telegram-standalone.ts +83 -0
  198. package/.forge/worktrees/pipeline-4dd8dc2d/lib/terminal-server.ts +70 -0
  199. package/.forge/worktrees/pipeline-4dd8dc2d/lib/terminal-standalone.ts +438 -0
  200. package/.forge/worktrees/pipeline-4dd8dc2d/lib/usage-scanner.ts +249 -0
  201. package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/__tests__/state-machine.test.ts +388 -0
  202. package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/__tests__/workspace.test.ts +311 -0
  203. package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/agent-bus.ts +416 -0
  204. package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/agent-worker.ts +655 -0
  205. package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/backends/api-backend.ts +262 -0
  206. package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/backends/cli-backend.ts +491 -0
  207. package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/index.ts +84 -0
  208. package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/manager.ts +136 -0
  209. package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/orchestrator.ts +3415 -0
  210. package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/persistence.ts +309 -0
  211. package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/presets.ts +649 -0
  212. package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/requests.ts +287 -0
  213. package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/session-monitor.ts +240 -0
  214. package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/skill-installer.ts +275 -0
  215. package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/smith-memory.ts +498 -0
  216. package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/types.ts +241 -0
  217. package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace/watch-manager.ts +560 -0
  218. package/.forge/worktrees/pipeline-4dd8dc2d/lib/workspace-standalone.ts +978 -0
  219. package/.forge/worktrees/pipeline-4dd8dc2d/middleware.ts +51 -0
  220. package/.forge/worktrees/pipeline-4dd8dc2d/next.config.ts +26 -0
  221. package/.forge/worktrees/pipeline-4dd8dc2d/package.json +74 -0
  222. package/.forge/worktrees/pipeline-4dd8dc2d/pnpm-lock.yaml +3719 -0
  223. package/.forge/worktrees/pipeline-4dd8dc2d/pnpm-workspace.yaml +1 -0
  224. package/.forge/worktrees/pipeline-4dd8dc2d/postcss.config.mjs +7 -0
  225. package/.forge/worktrees/pipeline-4dd8dc2d/publish.sh +133 -0
  226. package/.forge/worktrees/pipeline-4dd8dc2d/scripts/bench/README.md +66 -0
  227. package/.forge/worktrees/pipeline-4dd8dc2d/scripts/bench/results/.gitignore +2 -0
  228. package/.forge/worktrees/pipeline-4dd8dc2d/scripts/bench/run.ts +635 -0
  229. package/.forge/worktrees/pipeline-4dd8dc2d/scripts/bench/tasks/01-text-utils/task.md +26 -0
  230. package/.forge/worktrees/pipeline-4dd8dc2d/scripts/bench/tasks/01-text-utils/validator.sh +46 -0
  231. package/.forge/worktrees/pipeline-4dd8dc2d/scripts/bench/tasks/02-pagination/setup.sh +19 -0
  232. package/.forge/worktrees/pipeline-4dd8dc2d/scripts/bench/tasks/02-pagination/task.md +48 -0
  233. package/.forge/worktrees/pipeline-4dd8dc2d/scripts/bench/tasks/02-pagination/validator.sh +69 -0
  234. package/.forge/worktrees/pipeline-4dd8dc2d/scripts/bench/tasks/03-bug-fix/setup.sh +82 -0
  235. package/.forge/worktrees/pipeline-4dd8dc2d/scripts/bench/tasks/03-bug-fix/task.md +30 -0
  236. package/.forge/worktrees/pipeline-4dd8dc2d/scripts/bench/tasks/03-bug-fix/validator.sh +29 -0
  237. package/.forge/worktrees/pipeline-4dd8dc2d/scripts/verify-usage.ts +178 -0
  238. package/.forge/worktrees/pipeline-4dd8dc2d/src/config/index.ts +129 -0
  239. package/.forge/worktrees/pipeline-4dd8dc2d/src/core/db/database.ts +259 -0
  240. package/.forge/worktrees/pipeline-4dd8dc2d/src/core/memory/strategy.ts +32 -0
  241. package/.forge/worktrees/pipeline-4dd8dc2d/src/core/providers/chat.ts +65 -0
  242. package/.forge/worktrees/pipeline-4dd8dc2d/src/core/providers/registry.ts +60 -0
  243. package/.forge/worktrees/pipeline-4dd8dc2d/src/core/session/manager.ts +190 -0
  244. package/.forge/worktrees/pipeline-4dd8dc2d/src/types/index.ts +129 -0
  245. package/.forge/worktrees/pipeline-4dd8dc2d/start.sh +32 -0
  246. package/.forge/worktrees/pipeline-4dd8dc2d/templates/smith-lead.json +45 -0
  247. package/.forge/worktrees/pipeline-4dd8dc2d/tsconfig.json +42 -0
  248. package/RELEASE_NOTES.md +11 -28
  249. package/app/api/terminal-bell/route.ts +6 -2
  250. package/components/WebTerminal.tsx +36 -2
  251. package/lib/terminal-standalone.ts +19 -2
  252. package/next-env.d.ts +1 -1
  253. 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
+ }