@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,53 @@
1
+ /**
2
+ * Project-level fixed session binding.
3
+ * Stores { projectPath: sessionId } in ~/.forge/data/project-sessions.json
4
+ */
5
+
6
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
7
+ import { join } from 'node:path';
8
+ import { getDataDir } from './dirs';
9
+
10
+ function getFilePath(): string {
11
+ const dir = getDataDir();
12
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
13
+ return join(dir, 'project-sessions.json');
14
+ }
15
+
16
+ let cache: Record<string, string> | null = null;
17
+
18
+ function loadAll(): Record<string, string> {
19
+ if (cache !== null) return cache;
20
+ const fp = getFilePath();
21
+ if (!existsSync(fp)) { cache = {}; return cache; }
22
+ try { cache = JSON.parse(readFileSync(fp, 'utf-8')) as Record<string, string>; } catch { cache = {}; }
23
+ return cache;
24
+ }
25
+
26
+ function saveAll(data: Record<string, string>): void {
27
+ cache = data;
28
+ writeFileSync(getFilePath(), JSON.stringify(data, null, 2));
29
+ }
30
+
31
+ /** Get the fixed session ID for a project */
32
+ export function getFixedSession(projectPath: string): string | undefined {
33
+ return loadAll()[projectPath] || undefined;
34
+ }
35
+
36
+ /** Set the fixed session ID for a project */
37
+ export function setFixedSession(projectPath: string, sessionId: string): void {
38
+ const data = loadAll();
39
+ data[projectPath] = sessionId;
40
+ saveAll(data);
41
+ }
42
+
43
+ /** Clear the fixed session for a project */
44
+ export function clearFixedSession(projectPath: string): void {
45
+ const data = loadAll();
46
+ delete data[projectPath];
47
+ saveAll(data);
48
+ }
49
+
50
+ /** Get all bindings */
51
+ export function getAllFixedSessions(): Record<string, string> {
52
+ return loadAll();
53
+ }
@@ -0,0 +1,86 @@
1
+ import { readdirSync, existsSync, statSync, readFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { loadSettings } from './settings';
4
+
5
+ export interface LocalProject {
6
+ name: string;
7
+ path: string;
8
+ root: string; // Which project root it came from
9
+ hasGit: boolean;
10
+ hasClaudeMd: boolean;
11
+ language: string | null;
12
+ lastModified: string;
13
+ }
14
+
15
+ export function scanProjects(): LocalProject[] {
16
+ const settings = loadSettings();
17
+ const roots = settings.projectRoots;
18
+
19
+ if (roots.length === 0) return [];
20
+
21
+ const projects: LocalProject[] = [];
22
+
23
+ for (const root of roots) {
24
+ if (!existsSync(root)) continue;
25
+
26
+ const entries = readdirSync(root, { withFileTypes: true });
27
+
28
+ for (const entry of entries) {
29
+ if (!entry.isDirectory()) continue;
30
+ if (entry.name.startsWith('.')) continue;
31
+
32
+ const projectPath = join(root, entry.name);
33
+
34
+ try {
35
+ const hasGit = existsSync(join(projectPath, '.git'));
36
+ const hasClaudeMd = existsSync(join(projectPath, 'CLAUDE.md'));
37
+ const language = detectLanguage(projectPath);
38
+ const stat = statSync(projectPath);
39
+
40
+ projects.push({
41
+ name: entry.name,
42
+ path: projectPath,
43
+ root,
44
+ hasGit,
45
+ hasClaudeMd,
46
+ language,
47
+ lastModified: stat.mtime.toISOString(),
48
+ });
49
+ } catch {
50
+ // Skip inaccessible directories
51
+ }
52
+ }
53
+ }
54
+
55
+ return projects.sort((a, b) => b.lastModified.localeCompare(a.lastModified));
56
+ }
57
+
58
+ function detectLanguage(projectPath: string): string | null {
59
+ const markers: [string, string][] = [
60
+ ['pom.xml', 'java'],
61
+ ['build.gradle', 'java'],
62
+ ['build.gradle.kts', 'kotlin'],
63
+ ['package.json', 'typescript'],
64
+ ['tsconfig.json', 'typescript'],
65
+ ['requirements.txt', 'python'],
66
+ ['pyproject.toml', 'python'],
67
+ ['go.mod', 'go'],
68
+ ['Cargo.toml', 'rust'],
69
+ ];
70
+
71
+ for (const [file, lang] of markers) {
72
+ if (existsSync(join(projectPath, file))) return lang;
73
+ }
74
+ return null;
75
+ }
76
+
77
+ export function getProjectInfo(name: string): LocalProject | null {
78
+ const projects = scanProjects();
79
+ return projects.find(p => p.name === name) || null;
80
+ }
81
+
82
+ export function getProjectClaudeMd(projectPath: string): string | null {
83
+ const claudeMdPath = join(projectPath, 'CLAUDE.md');
84
+ if (!existsSync(claudeMdPath)) return null;
85
+ return readFileSync(claudeMdPath, 'utf-8');
86
+ }
@@ -0,0 +1,156 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { getDb } from '@/src/core/db/database';
3
+ import { getDbPath, loadTemplate } from '@/src/config';
4
+ import { chatStream, type ChatResult } from '@/src/core/providers/chat';
5
+ import { getMemoryMessages } from '@/src/core/memory/strategy';
6
+ import type { Session, SessionStatus, Message, ProviderName, MemoryConfig } from '@/src/types';
7
+
8
+ // Re-export the SessionManager but as a singleton for server-side use
9
+ class SessionManager {
10
+ private db;
11
+
12
+ constructor() {
13
+ this.db = getDb(getDbPath());
14
+ }
15
+
16
+ create(opts: { name: string; templateId: string; provider?: ProviderName; model?: string }): Session {
17
+ const template = loadTemplate(opts.templateId);
18
+ if (!template) throw new Error(`Template not found: ${opts.templateId}`);
19
+
20
+ const id = randomUUID().slice(0, 8);
21
+ const provider = opts.provider || template.provider;
22
+ const model = opts.model || template.model || '';
23
+ const memoryConfig = JSON.stringify(template.memory);
24
+
25
+ this.db.prepare(`
26
+ INSERT INTO sessions (id, name, template_id, provider, model, status, memory_config, system_prompt)
27
+ VALUES (?, ?, ?, ?, ?, 'idle', ?, ?)
28
+ `).run(id, opts.name, opts.templateId, provider, model, memoryConfig, template.systemPrompt);
29
+
30
+ return this.get(id)!;
31
+ }
32
+
33
+ get(id: string): Session | null {
34
+ const row = this.db.prepare(`
35
+ SELECT s.*, COUNT(m.id) as message_count,
36
+ (SELECT content FROM messages WHERE session_id = s.id ORDER BY created_at DESC LIMIT 1) as last_message
37
+ FROM sessions s
38
+ LEFT JOIN messages m ON m.session_id = s.id
39
+ WHERE s.id = ?
40
+ GROUP BY s.id
41
+ `).get(id) as any;
42
+
43
+ if (!row) return null;
44
+ return this.rowToSession(row);
45
+ }
46
+
47
+ getByName(name: string): Session | null {
48
+ const row = this.db.prepare(`
49
+ SELECT s.*, COUNT(m.id) as message_count,
50
+ (SELECT content FROM messages WHERE session_id = s.id ORDER BY created_at DESC LIMIT 1) as last_message
51
+ FROM sessions s
52
+ LEFT JOIN messages m ON m.session_id = s.id
53
+ WHERE s.name = ?
54
+ GROUP BY s.id
55
+ `).get(name) as any;
56
+
57
+ if (!row) return null;
58
+ return this.rowToSession(row);
59
+ }
60
+
61
+ list(status?: SessionStatus): Session[] {
62
+ let query = `
63
+ SELECT s.*, COUNT(m.id) as message_count,
64
+ (SELECT content FROM messages WHERE session_id = s.id ORDER BY created_at DESC LIMIT 1) as last_message
65
+ FROM sessions s
66
+ LEFT JOIN messages m ON m.session_id = s.id
67
+ `;
68
+ const params: string[] = [];
69
+ if (status) {
70
+ query += ' WHERE s.status = ?';
71
+ params.push(status);
72
+ }
73
+ query += ' GROUP BY s.id ORDER BY s.updated_at DESC';
74
+
75
+ const rows = this.db.prepare(query).all(...params) as any[];
76
+ return rows.map(r => this.rowToSession(r));
77
+ }
78
+
79
+ updateStatus(id: string, status: SessionStatus) {
80
+ this.db.prepare(`UPDATE sessions SET status = ?, updated_at = datetime('now') WHERE id = ?`).run(status, id);
81
+ }
82
+
83
+ addMessage(sessionId: string, role: string, content: string, provider: string, model: string) {
84
+ this.db.prepare(`
85
+ INSERT INTO messages (session_id, role, content, provider, model)
86
+ VALUES (?, ?, ?, ?, ?)
87
+ `).run(sessionId, role, content, provider, model);
88
+ this.db.prepare(`UPDATE sessions SET updated_at = datetime('now') WHERE id = ?`).run(sessionId);
89
+ }
90
+
91
+ getMessages(sessionId: string, limit?: number): Message[] {
92
+ let query = 'SELECT * FROM messages WHERE session_id = ? ORDER BY created_at ASC';
93
+ const params: (string | number)[] = [sessionId];
94
+ if (limit) {
95
+ query = `SELECT * FROM (
96
+ SELECT * FROM messages WHERE session_id = ? ORDER BY created_at DESC LIMIT ?
97
+ ) ORDER BY created_at ASC`;
98
+ params.push(limit);
99
+ }
100
+ return this.db.prepare(query).all(...params) as Message[];
101
+ }
102
+
103
+ getMemoryMessages(sessionId: string): Message[] {
104
+ const session = this.get(sessionId);
105
+ if (!session) return [];
106
+ const allMessages = this.getMessages(sessionId);
107
+ return getMemoryMessages(allMessages, session.memory);
108
+ }
109
+
110
+ delete(id: string) {
111
+ this.db.prepare('DELETE FROM sessions WHERE id = ?').run(id);
112
+ }
113
+
114
+ recordUsage(sessionId: string, result: ChatResult) {
115
+ this.db.prepare(`
116
+ INSERT INTO usage (provider, model, session_id, input_tokens, output_tokens, cost)
117
+ VALUES (?, ?, ?, ?, ?, ?)
118
+ `).run(result.provider, result.model, sessionId, result.inputTokens, result.outputTokens, 0);
119
+ }
120
+
121
+ getUsageSummary() {
122
+ return this.db.prepare(`
123
+ SELECT provider,
124
+ SUM(input_tokens) as totalInput,
125
+ SUM(output_tokens) as totalOutput,
126
+ SUM(cost) as totalCost
127
+ FROM usage
128
+ WHERE created_at >= date('now', '-30 days')
129
+ GROUP BY provider
130
+ `).all() as any[];
131
+ }
132
+
133
+ private rowToSession(row: any): Session {
134
+ return {
135
+ id: row.id,
136
+ name: row.name,
137
+ templateId: row.template_id,
138
+ provider: row.provider,
139
+ model: row.model,
140
+ status: row.status,
141
+ memory: JSON.parse(row.memory_config),
142
+ systemPrompt: row.system_prompt,
143
+ messageCount: row.message_count || 0,
144
+ createdAt: row.created_at,
145
+ updatedAt: row.updated_at,
146
+ lastMessage: row.last_message || undefined,
147
+ };
148
+ }
149
+ }
150
+
151
+ // Singleton
152
+ let instance: SessionManager | null = null;
153
+ export function getSessionManager(): SessionManager {
154
+ if (!instance) instance = new SessionManager();
155
+ return instance;
156
+ }
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Shared session utilities for client-side components.
3
+ * Resolves fixedSessionId from project-level binding.
4
+ */
5
+
6
+ /** Fetch the fixedSessionId for a project. If not set, auto-binds the latest session. */
7
+ export async function resolveFixedSession(projectPath: string): Promise<string | null> {
8
+ try {
9
+ // Check existing binding
10
+ const res = await fetch(`/api/project-sessions?projectPath=${encodeURIComponent(projectPath)}`);
11
+ const data = await res.json();
12
+ if (data?.fixedSessionId) return data.fixedSessionId;
13
+
14
+ // Not set — find latest session and auto-bind
15
+ const projectName = projectPath.replace(/\/+$/, '').split('/').pop() || '';
16
+ const sessRes = await fetch(`/api/claude-sessions/${encodeURIComponent(projectName)}`);
17
+ const sessions = await sessRes.json();
18
+ if (!Array.isArray(sessions) || sessions.length === 0) return null;
19
+
20
+ const latestId = sessions[0].sessionId || sessions[0].id;
21
+ if (!latestId) return null;
22
+
23
+ // Save binding
24
+ await fetch('/api/project-sessions', {
25
+ method: 'POST',
26
+ headers: { 'Content-Type': 'application/json' },
27
+ body: JSON.stringify({ projectPath, fixedSessionId: latestId }),
28
+ });
29
+
30
+ return latestId;
31
+ } catch {
32
+ return null;
33
+ }
34
+ }
35
+
36
+ /** Build the resume flag: --resume <id> if fixedSession exists, else -c if hasSession */
37
+ export function buildResumeFlag(fixedSessionId: string | null, hasExistingSessions: boolean): string {
38
+ if (fixedSessionId) return ` --resume ${fixedSessionId}`;
39
+ if (hasExistingSessions) return ' -c';
40
+ return '';
41
+ }
42
+
43
+ const _mcpReady = new Set<string>();
44
+
45
+ /** Get --mcp-config flag for claude-code. Triggers server-side mcp.json creation (once per projectPath). */
46
+ export async function getMcpFlag(projectPath: string): Promise<string> {
47
+ if (!_mcpReady.has(projectPath)) {
48
+ await fetch(`/api/project-sessions?projectPath=${encodeURIComponent(projectPath)}`).catch(() => {});
49
+ _mcpReady.add(projectPath);
50
+ }
51
+ return ` --mcp-config "${projectPath}/.forge/mcp.json"`;
52
+ }
53
+
@@ -0,0 +1,345 @@
1
+ /**
2
+ * Session Watcher — monitors Claude CLI sessions and sends Telegram notifications.
3
+ *
4
+ * Watchers track entry counts in sessions. When new entries appear,
5
+ * a summary is sent to Telegram. Also detects idle/completed sessions.
6
+ */
7
+
8
+ import { randomUUID } from 'node:crypto';
9
+ import { getDb } from '@/src/core/db/database';
10
+ import { join } from 'node:path';
11
+ import { getDataDir } from './dirs';
12
+ import {
13
+ listClaudeSessions,
14
+ getSessionFilePath,
15
+ readSessionEntries,
16
+ type ClaudeSessionInfo,
17
+ type SessionEntry,
18
+ } from './claude-sessions';
19
+ import { scanProjects } from './projects';
20
+ import { loadSettings } from './settings';
21
+
22
+ const DB_PATH = join(getDataDir(), 'workflow.db');
23
+
24
+ // ─── Types ───────────────────────────────────────────────────
25
+
26
+ export interface SessionWatcher {
27
+ id: string;
28
+ projectName: string;
29
+ sessionId: string | null; // null = watch all sessions in project
30
+ label: string | null;
31
+ checkInterval: number; // seconds
32
+ lastEntryCount: number;
33
+ lastChecked: string | null;
34
+ notifyOnChange: boolean;
35
+ notifyOnIdle: boolean;
36
+ idleThreshold: number; // seconds
37
+ active: boolean;
38
+ createdAt: string;
39
+ }
40
+
41
+ // ─── Session cache sync ──────────────────────────────────────
42
+
43
+ export function syncSessionsToDb(projectName?: string) {
44
+ const db = getDb(DB_PATH);
45
+ const projects = projectName
46
+ ? [{ name: projectName }]
47
+ : scanProjects().map((p: { name: string }) => ({ name: p.name }));
48
+
49
+ const upsert = db.prepare(`
50
+ INSERT INTO cached_sessions (project_name, session_id, summary, first_prompt, message_count, created, modified, git_branch, file_size, last_synced)
51
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now'))
52
+ ON CONFLICT(project_name, session_id) DO UPDATE SET
53
+ summary = excluded.summary,
54
+ first_prompt = excluded.first_prompt,
55
+ message_count = excluded.message_count,
56
+ created = excluded.created,
57
+ modified = excluded.modified,
58
+ git_branch = excluded.git_branch,
59
+ file_size = excluded.file_size,
60
+ last_synced = datetime('now')
61
+ `);
62
+
63
+ const updateEntryCount = db.prepare(`
64
+ UPDATE cached_sessions SET entry_count = ? WHERE project_name = ? AND session_id = ?
65
+ `);
66
+
67
+ let totalSynced = 0;
68
+
69
+ for (const proj of projects) {
70
+ const sessions = listClaudeSessions(proj.name);
71
+ for (const s of sessions) {
72
+ upsert.run(
73
+ proj.name, s.sessionId, s.summary || null, s.firstPrompt || null,
74
+ s.messageCount || 0, s.created || null, s.modified || null,
75
+ s.gitBranch || null, s.fileSize,
76
+ );
77
+
78
+ // Count entries for watcher comparison
79
+ const fp = getSessionFilePath(proj.name, s.sessionId);
80
+ if (fp) {
81
+ try {
82
+ const entries = readSessionEntries(fp);
83
+ updateEntryCount.run(entries.length, proj.name, s.sessionId);
84
+ } catch {}
85
+ }
86
+
87
+ totalSynced++;
88
+ }
89
+ }
90
+
91
+ return totalSynced;
92
+ }
93
+
94
+ export function getCachedSessions(projectName: string): ClaudeSessionInfo[] {
95
+ const db = getDb(DB_PATH);
96
+ const rows = db.prepare(`
97
+ SELECT session_id, summary, first_prompt, message_count, created, modified, git_branch, file_size
98
+ FROM cached_sessions WHERE project_name = ? ORDER BY modified DESC
99
+ `).all(projectName) as any[];
100
+
101
+ return rows.map(r => ({
102
+ sessionId: r.session_id,
103
+ summary: r.summary,
104
+ firstPrompt: r.first_prompt,
105
+ messageCount: r.message_count,
106
+ created: r.created,
107
+ modified: r.modified,
108
+ gitBranch: r.git_branch,
109
+ fileSize: r.file_size,
110
+ }));
111
+ }
112
+
113
+ export function getAllCachedSessions(): Record<string, ClaudeSessionInfo[]> {
114
+ const db = getDb(DB_PATH);
115
+ const rows = db.prepare(`
116
+ SELECT project_name, session_id, summary, first_prompt, message_count, created, modified, git_branch, file_size
117
+ FROM cached_sessions ORDER BY project_name, modified DESC
118
+ `).all() as any[];
119
+
120
+ const result: Record<string, ClaudeSessionInfo[]> = {};
121
+ for (const r of rows) {
122
+ if (!result[r.project_name]) result[r.project_name] = [];
123
+ result[r.project_name].push({
124
+ sessionId: r.session_id,
125
+ summary: r.summary,
126
+ firstPrompt: r.first_prompt,
127
+ messageCount: r.message_count,
128
+ created: r.created,
129
+ modified: r.modified,
130
+ gitBranch: r.git_branch,
131
+ fileSize: r.file_size,
132
+ });
133
+ }
134
+ return result;
135
+ }
136
+
137
+ // ─── Watcher CRUD ────────────────────────────────────────────
138
+
139
+ export function createWatcher(opts: {
140
+ projectName: string;
141
+ sessionId?: string;
142
+ label?: string;
143
+ checkInterval?: number;
144
+ }): SessionWatcher {
145
+ const db = getDb(DB_PATH);
146
+ const id = randomUUID().slice(0, 8);
147
+
148
+ db.prepare(`
149
+ INSERT INTO session_watchers (id, project_name, session_id, label, check_interval)
150
+ VALUES (?, ?, ?, ?, ?)
151
+ `).run(id, opts.projectName, opts.sessionId || null, opts.label || null, opts.checkInterval || 60);
152
+
153
+ return getWatcher(id)!;
154
+ }
155
+
156
+ export function getWatcher(id: string): SessionWatcher | null {
157
+ const db = getDb(DB_PATH);
158
+ const row = db.prepare('SELECT * FROM session_watchers WHERE id = ?').get(id) as any;
159
+ return row ? mapWatcherRow(row) : null;
160
+ }
161
+
162
+ export function listWatchers(activeOnly = false): SessionWatcher[] {
163
+ const db = getDb(DB_PATH);
164
+ const sql = activeOnly
165
+ ? 'SELECT * FROM session_watchers WHERE active = 1 ORDER BY created_at DESC'
166
+ : 'SELECT * FROM session_watchers ORDER BY created_at DESC';
167
+ return (db.prepare(sql).all() as any[]).map(mapWatcherRow);
168
+ }
169
+
170
+ export function toggleWatcher(id: string, active: boolean) {
171
+ const db = getDb(DB_PATH);
172
+ db.prepare('UPDATE session_watchers SET active = ? WHERE id = ?').run(active ? 1 : 0, id);
173
+ }
174
+
175
+ export function deleteWatcher(id: string) {
176
+ const db = getDb(DB_PATH);
177
+ db.prepare('DELETE FROM session_watchers WHERE id = ?').run(id);
178
+ }
179
+
180
+ function mapWatcherRow(row: any): SessionWatcher {
181
+ return {
182
+ id: row.id,
183
+ projectName: row.project_name,
184
+ sessionId: row.session_id,
185
+ label: row.label,
186
+ checkInterval: row.check_interval,
187
+ lastEntryCount: row.last_entry_count,
188
+ lastChecked: row.last_checked,
189
+ notifyOnChange: !!row.notify_on_change,
190
+ notifyOnIdle: !!row.notify_on_idle,
191
+ idleThreshold: row.idle_threshold,
192
+ active: !!row.active,
193
+ createdAt: row.created_at,
194
+ };
195
+ }
196
+
197
+ // ─── Watcher check loop ─────────────────────────────────────
198
+
199
+ let watcherInterval: ReturnType<typeof setInterval> | null = null;
200
+
201
+ export function startWatcherLoop() {
202
+ if (watcherInterval) return;
203
+
204
+ // Initial sync
205
+ try { syncSessionsToDb(); } catch (e) { console.error('[watcher] Initial sync error:', e); }
206
+
207
+ // Check every 30 seconds
208
+ watcherInterval = setInterval(runWatcherCheck, 30_000);
209
+ console.log('[watcher] Started session watcher loop');
210
+ }
211
+
212
+ export function stopWatcherLoop() {
213
+ if (watcherInterval) {
214
+ clearInterval(watcherInterval);
215
+ watcherInterval = null;
216
+ }
217
+ }
218
+
219
+ async function runWatcherCheck() {
220
+ const db = getDb(DB_PATH);
221
+ const watchers = listWatchers(true);
222
+ if (watchers.length === 0) return;
223
+
224
+ const now = Date.now();
225
+
226
+ for (const w of watchers) {
227
+ // Check if it's time
228
+ if (w.lastChecked) {
229
+ const elapsed = (now - new Date(w.lastChecked).getTime()) / 1000;
230
+ if (elapsed < w.checkInterval) continue;
231
+ }
232
+
233
+ try {
234
+ if (w.sessionId) {
235
+ // Watch specific session
236
+ await checkSession(db, w, w.projectName, w.sessionId);
237
+ } else {
238
+ // Watch all sessions in project
239
+ const sessions = listClaudeSessions(w.projectName);
240
+ for (const s of sessions) {
241
+ await checkSession(db, w, w.projectName, s.sessionId);
242
+ }
243
+ }
244
+
245
+ // Update last checked
246
+ db.prepare('UPDATE session_watchers SET last_checked = datetime(\'now\') WHERE id = ?').run(w.id);
247
+ } catch (e) {
248
+ console.error(`[watcher] Error checking ${w.id}:`, e);
249
+ }
250
+ }
251
+
252
+ // Periodic session sync (every check cycle)
253
+ try { syncSessionsToDb(); } catch {}
254
+ }
255
+
256
+ async function checkSession(
257
+ db: ReturnType<typeof getDb>,
258
+ watcher: SessionWatcher,
259
+ projectName: string,
260
+ sessionId: string,
261
+ ) {
262
+ const fp = getSessionFilePath(projectName, sessionId);
263
+ if (!fp) return;
264
+
265
+ const entries = readSessionEntries(fp);
266
+ const currentCount = entries.length;
267
+
268
+ // Get last known count from cached_sessions
269
+ const cached = db.prepare(
270
+ 'SELECT entry_count FROM cached_sessions WHERE project_name = ? AND session_id = ?'
271
+ ).get(projectName, sessionId) as any;
272
+
273
+ const lastCount = cached?.entry_count || 0;
274
+
275
+ if (currentCount > lastCount && watcher.notifyOnChange) {
276
+ // New entries! Summarize the changes
277
+ const newEntries = entries.slice(lastCount);
278
+ const summary = summarizeEntries(newEntries);
279
+ const label = watcher.label || `${projectName}/${sessionId.slice(0, 8)}`;
280
+
281
+ await sendWatcherNotification(
282
+ `📋 *${esc(label)}*\n\n` +
283
+ `${summary}\n\n` +
284
+ `_${currentCount} total entries (+${currentCount - lastCount} new)_`
285
+ );
286
+
287
+ // Update cached entry count
288
+ db.prepare(
289
+ 'UPDATE cached_sessions SET entry_count = ? WHERE project_name = ? AND session_id = ?'
290
+ ).run(currentCount, projectName, sessionId);
291
+ }
292
+ }
293
+
294
+ function summarizeEntries(entries: SessionEntry[]): string {
295
+ const parts: string[] = [];
296
+ let assistantText = '';
297
+ let toolNames: string[] = [];
298
+
299
+ for (const e of entries) {
300
+ if (e.type === 'user') {
301
+ parts.push(`👤 ${e.content.slice(0, 150)}`);
302
+ } else if (e.type === 'assistant_text') {
303
+ assistantText = e.content.slice(0, 300);
304
+ } else if (e.type === 'tool_use' && e.toolName) {
305
+ toolNames.push(e.toolName);
306
+ }
307
+ }
308
+
309
+ if (toolNames.length > 0) {
310
+ const unique = [...new Set(toolNames)];
311
+ parts.push(`🔧 Tools: ${unique.join(', ')}`);
312
+ }
313
+
314
+ if (assistantText) {
315
+ parts.push(`🤖 ${assistantText}`);
316
+ }
317
+
318
+ return parts.join('\n') || 'Activity detected';
319
+ }
320
+
321
+ async function sendWatcherNotification(text: string) {
322
+ const settings = loadSettings();
323
+ const { telegramBotToken, telegramChatId } = settings;
324
+ if (!telegramBotToken || !telegramChatId) return;
325
+
326
+ try {
327
+ const url = `https://api.telegram.org/bot${telegramBotToken}/sendMessage`;
328
+ await fetch(url, {
329
+ method: 'POST',
330
+ headers: { 'Content-Type': 'application/json' },
331
+ body: JSON.stringify({
332
+ chat_id: telegramChatId,
333
+ text,
334
+ parse_mode: 'Markdown',
335
+ disable_web_page_preview: true,
336
+ }),
337
+ });
338
+ } catch (err) {
339
+ console.error('[watcher] Telegram send failed:', err);
340
+ }
341
+ }
342
+
343
+ function esc(s: string): string {
344
+ return s.replace(/[_*[\]()~`>#+=|{}.!-]/g, '\\$&');
345
+ }