@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,227 @@
1
+ /**
2
+ * CLAUDE.md template management.
3
+ *
4
+ * Templates are reusable markdown snippets that can be appended to project CLAUDE.md files.
5
+ * Stored in <dataDir>/claude-templates/*.md with frontmatter metadata.
6
+ * Injection is idempotent — marked with <!-- forge:template:<id> --> comments.
7
+ */
8
+
9
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, readdirSync, unlinkSync } from 'node:fs';
10
+ import { join, basename } from 'node:path';
11
+ import { getDataDir } from './dirs';
12
+ import YAML from 'yaml';
13
+
14
+ const TEMPLATES_DIR = join(getDataDir(), 'claude-templates');
15
+
16
+ export interface ClaudeTemplate {
17
+ id: string; // filename without .md
18
+ name: string;
19
+ description: string;
20
+ tags: string[];
21
+ builtin: boolean;
22
+ isDefault: boolean; // auto-inject into new projects
23
+ content: string; // markdown body (without frontmatter)
24
+ }
25
+
26
+ function ensureDir() {
27
+ if (!existsSync(TEMPLATES_DIR)) mkdirSync(TEMPLATES_DIR, { recursive: true });
28
+ }
29
+
30
+ // ─── Built-in templates ──────────────────────────────────────
31
+
32
+ const BUILTINS: Record<string, { name: string; description: string; tags: string[]; content: string }> = {
33
+ 'typescript-rules': {
34
+ name: 'TypeScript Rules',
35
+ description: 'TypeScript coding conventions and best practices',
36
+ tags: ['typescript', 'code-style'],
37
+ content: `## TypeScript Rules
38
+ - Use \`const\` by default, \`let\` only when needed
39
+ - Prefer explicit return types on exported functions
40
+ - Use \`interface\` for object shapes, \`type\` for unions/intersections
41
+ - Avoid \`any\` — use \`unknown\` + type guards
42
+ - Prefer early returns over nested if/else`,
43
+ },
44
+ 'git-workflow': {
45
+ name: 'Git Workflow',
46
+ description: 'Git commit and branch conventions',
47
+ tags: ['git', 'workflow'],
48
+ content: `## Git Workflow
49
+ - Commit messages: imperative mood, concise (e.g. "add feature X", "fix bug in Y")
50
+ - Branch naming: \`feature/<name>\`, \`fix/<name>\`, \`chore/<name>\`
51
+ - Always create a new branch for changes, never commit directly to main
52
+ - Run tests before committing`,
53
+ },
54
+ 'obsidian-vault': {
55
+ name: 'Obsidian Vault',
56
+ description: 'Obsidian vault integration for note search and management',
57
+ tags: ['obsidian', 'docs'],
58
+ content: `## Obsidian Vault
59
+ When I ask about my notes, use bash to search and read files from the vault directory.
60
+ Example: find <vault_path> -name "*.md" | head -20`,
61
+ },
62
+ 'security': {
63
+ name: 'Security Rules',
64
+ description: 'Security best practices for code generation',
65
+ tags: ['security'],
66
+ content: `## Security Rules
67
+ - Never hardcode secrets, API keys, or passwords
68
+ - Validate all user inputs at system boundaries
69
+ - Use parameterized queries for database operations
70
+ - Sanitize outputs to prevent XSS
71
+ - Follow OWASP top 10 guidelines`,
72
+ },
73
+ };
74
+
75
+ /** Ensure built-in templates exist on disk */
76
+ export function ensureBuiltins() {
77
+ ensureDir();
78
+ for (const [id, tmpl] of Object.entries(BUILTINS)) {
79
+ const file = join(TEMPLATES_DIR, `${id}.md`);
80
+ if (!existsSync(file)) {
81
+ const frontmatter = YAML.stringify({ name: tmpl.name, description: tmpl.description, tags: tmpl.tags, builtin: true });
82
+ writeFileSync(file, `---\n${frontmatter}---\n\n${tmpl.content}\n`, 'utf-8');
83
+ }
84
+ }
85
+ }
86
+
87
+ // ─── CRUD ────────────────────────────────────────────────────
88
+
89
+ function parseTemplate(filePath: string): ClaudeTemplate | null {
90
+ try {
91
+ const raw = readFileSync(filePath, 'utf-8');
92
+ const id = basename(filePath, '.md');
93
+ // Parse frontmatter
94
+ const fmMatch = raw.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
95
+ if (!fmMatch) return { id, name: id, description: '', tags: [], builtin: false, isDefault: false, content: raw.trim() };
96
+ const meta = YAML.parse(fmMatch[1]) || {};
97
+ return {
98
+ id,
99
+ name: meta.name || id,
100
+ description: meta.description || '',
101
+ tags: meta.tags || [],
102
+ builtin: !!meta.builtin,
103
+ isDefault: !!meta.isDefault,
104
+ content: fmMatch[2].trim(),
105
+ };
106
+ } catch { return null; }
107
+ }
108
+
109
+ export function listTemplates(): ClaudeTemplate[] {
110
+ ensureDir();
111
+ ensureBuiltins();
112
+ const files = readdirSync(TEMPLATES_DIR).filter(f => f.endsWith('.md')).sort();
113
+ return files.map(f => parseTemplate(join(TEMPLATES_DIR, f))).filter(Boolean) as ClaudeTemplate[];
114
+ }
115
+
116
+ export function getTemplate(id: string): ClaudeTemplate | null {
117
+ const file = join(TEMPLATES_DIR, `${id}.md`);
118
+ if (!existsSync(file)) return null;
119
+ return parseTemplate(file);
120
+ }
121
+
122
+ export function saveTemplate(id: string, name: string, description: string, tags: string[], content: string, isDefault?: boolean): void {
123
+ ensureDir();
124
+ // Preserve builtin flag if editing an existing built-in template
125
+ const existing = getTemplate(id);
126
+ const builtin = existing?.builtin || false;
127
+ const frontmatter = YAML.stringify({ name, description, tags, builtin, isDefault: !!isDefault });
128
+ writeFileSync(join(TEMPLATES_DIR, `${id}.md`), `---\n${frontmatter}---\n\n${content}\n`, 'utf-8');
129
+ }
130
+
131
+ /** Toggle default flag on a template */
132
+ export function setTemplateDefault(id: string, isDefault: boolean): boolean {
133
+ const tmpl = getTemplate(id);
134
+ if (!tmpl) return false;
135
+ const file = join(TEMPLATES_DIR, `${id}.md`);
136
+ const raw = readFileSync(file, 'utf-8');
137
+ const fmMatch = raw.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
138
+ if (!fmMatch) return false;
139
+ const meta = YAML.parse(fmMatch[1]) || {};
140
+ meta.isDefault = isDefault;
141
+ writeFileSync(file, `---\n${YAML.stringify(meta)}---\n${fmMatch[2]}`, 'utf-8');
142
+ return true;
143
+ }
144
+
145
+ /** Auto-inject all default templates into a project if not already present */
146
+ export function applyDefaultTemplates(projectPath: string): string[] {
147
+ const claudeMdPath = join(projectPath, 'CLAUDE.md');
148
+ const templates = listTemplates();
149
+ const injected: string[] = [];
150
+ for (const tmpl of templates) {
151
+ if (tmpl.isDefault && !isInjected(claudeMdPath, tmpl.id)) {
152
+ if (injectTemplate(claudeMdPath, tmpl.id)) {
153
+ injected.push(tmpl.id);
154
+ }
155
+ }
156
+ }
157
+ return injected;
158
+ }
159
+
160
+ export function deleteTemplate(id: string): boolean {
161
+ const tmpl = getTemplate(id);
162
+ if (!tmpl || tmpl.builtin) return false; // can't delete builtins
163
+ const file = join(TEMPLATES_DIR, `${id}.md`);
164
+ try { unlinkSync(file); return true; } catch { return false; }
165
+ }
166
+
167
+ // ─── Injection ───────────────────────────────────────────────
168
+
169
+ const MARKER_START = (id: string) => `<!-- forge:template:${id} -->`;
170
+ const MARKER_END = (id: string) => `<!-- /forge:template:${id} -->`;
171
+
172
+ /** Check if a template is already injected in a CLAUDE.md */
173
+ export function isInjected(claudeMdPath: string, templateId: string): boolean {
174
+ if (!existsSync(claudeMdPath)) return false;
175
+ const content = readFileSync(claudeMdPath, 'utf-8');
176
+ return content.includes(MARKER_START(templateId));
177
+ }
178
+
179
+ /** Get list of template IDs injected in a CLAUDE.md */
180
+ export function getInjectedTemplates(claudeMdPath: string): string[] {
181
+ if (!existsSync(claudeMdPath)) return [];
182
+ const content = readFileSync(claudeMdPath, 'utf-8');
183
+ const ids: string[] = [];
184
+ const regex = /<!-- forge:template:(\S+) -->/g;
185
+ let match;
186
+ while ((match = regex.exec(content)) !== null) {
187
+ ids.push(match[1]);
188
+ }
189
+ return ids;
190
+ }
191
+
192
+ /** Append a template to a CLAUDE.md file. Returns false if already injected. */
193
+ export function injectTemplate(claudeMdPath: string, templateId: string): boolean {
194
+ const tmpl = getTemplate(templateId);
195
+ if (!tmpl) return false;
196
+ if (isInjected(claudeMdPath, templateId)) return false;
197
+
198
+ const block = `\n${MARKER_START(templateId)}\n${tmpl.content}\n${MARKER_END(templateId)}\n`;
199
+
200
+ if (existsSync(claudeMdPath)) {
201
+ const existing = readFileSync(claudeMdPath, 'utf-8');
202
+ writeFileSync(claudeMdPath, existing.trimEnd() + '\n' + block, 'utf-8');
203
+ } else {
204
+ // Create new CLAUDE.md
205
+ const dir = join(claudeMdPath, '..');
206
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
207
+ writeFileSync(claudeMdPath, block.trimStart(), 'utf-8');
208
+ }
209
+ return true;
210
+ }
211
+
212
+ /** Remove a template from a CLAUDE.md file */
213
+ export function removeTemplate(claudeMdPath: string, templateId: string): boolean {
214
+ if (!existsSync(claudeMdPath)) return false;
215
+ const content = readFileSync(claudeMdPath, 'utf-8');
216
+ const start = MARKER_START(templateId);
217
+ const end = MARKER_END(templateId);
218
+ const startIdx = content.indexOf(start);
219
+ const endIdx = content.indexOf(end);
220
+ if (startIdx === -1 || endIdx === -1) return false;
221
+
222
+ const before = content.slice(0, startIdx).trimEnd();
223
+ const after = content.slice(endIdx + end.length).trimStart();
224
+ const newContent = before + (after ? '\n\n' + after : '') + '\n';
225
+ writeFileSync(claudeMdPath, newContent, 'utf-8');
226
+ return true;
227
+ }
@@ -0,0 +1,424 @@
1
+ /**
2
+ * Cloudflare Tunnel (cloudflared) integration.
3
+ * Zero-config mode: no account needed, gives a temporary public URL.
4
+ */
5
+
6
+ import { spawn, execSync, type ChildProcess } from 'node:child_process';
7
+ import { existsSync, mkdirSync, chmodSync, createWriteStream, unlinkSync, writeFileSync, readFileSync } from 'node:fs';
8
+ import { platform, arch } from 'node:os';
9
+ import { join } from 'node:path';
10
+ import https from 'node:https';
11
+ import http from 'node:http';
12
+ import { getConfigDir, getDataDir } from './dirs';
13
+
14
+ const BIN_DIR = join(getConfigDir(), 'bin');
15
+ const BIN_NAME = platform() === 'win32' ? 'cloudflared.exe' : 'cloudflared';
16
+ const BIN_PATH = join(BIN_DIR, BIN_NAME);
17
+
18
+ // ─── Download URL resolution ────────────────────────────────────
19
+
20
+ function getDownloadUrl(): string {
21
+ const os = platform();
22
+ const cpu = arch();
23
+ const base = 'https://github.com/cloudflare/cloudflared/releases/latest/download';
24
+
25
+ if (os === 'darwin') {
26
+ return cpu === 'arm64'
27
+ ? `${base}/cloudflared-darwin-arm64.tgz`
28
+ : `${base}/cloudflared-darwin-amd64.tgz`;
29
+ }
30
+ if (os === 'linux') {
31
+ if (cpu === 'arm64') return `${base}/cloudflared-linux-arm64`;
32
+ if (cpu === 'arm') return `${base}/cloudflared-linux-arm`;
33
+ return `${base}/cloudflared-linux-amd64`;
34
+ }
35
+ if (os === 'win32') {
36
+ return `${base}/cloudflared-windows-amd64.exe`;
37
+ }
38
+ throw new Error(`Unsupported platform: ${os}/${cpu}`);
39
+ }
40
+
41
+ // ─── Download helper ────────────────────────────────────────────
42
+
43
+ const DOWNLOAD_TIMEOUT_MS = 120_000; // 2 minutes total per redirect hop
44
+
45
+ function followRedirects(url: string, dest: string, redirectsLeft = 10): Promise<void> {
46
+ return new Promise((resolve, reject) => {
47
+ const client = url.startsWith('https') ? https : http;
48
+ const req = client.get(url, { headers: { 'User-Agent': 'forge/1.0' } }, (res) => {
49
+ if (res.statusCode && res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
50
+ res.resume(); // drain redirect response
51
+ if (redirectsLeft <= 0) {
52
+ reject(new Error('Too many redirects'));
53
+ return;
54
+ }
55
+ followRedirects(res.headers.location, dest, redirectsLeft - 1).then(resolve, reject);
56
+ return;
57
+ }
58
+ if (res.statusCode !== 200) {
59
+ res.resume();
60
+ reject(new Error(`Download failed: HTTP ${res.statusCode}`));
61
+ return;
62
+ }
63
+ const file = createWriteStream(dest);
64
+ res.pipe(file);
65
+ file.on('finish', () => file.close(() => resolve()));
66
+ file.on('error', (err) => { try { unlinkSync(dest); } catch {} reject(err); });
67
+ res.on('error', (err) => { try { unlinkSync(dest); } catch {} reject(err); });
68
+ });
69
+ req.setTimeout(DOWNLOAD_TIMEOUT_MS, () => {
70
+ req.destroy(new Error(`Download timed out after ${DOWNLOAD_TIMEOUT_MS / 1000}s`));
71
+ });
72
+ req.on('error', reject);
73
+ });
74
+ }
75
+
76
+ // Guard against concurrent downloads
77
+ let downloadPromise: Promise<string> | null = null;
78
+
79
+ export async function downloadCloudflared(): Promise<string> {
80
+ if (existsSync(BIN_PATH)) return BIN_PATH;
81
+ if (downloadPromise) return downloadPromise;
82
+
83
+ downloadPromise = (async () => {
84
+ mkdirSync(BIN_DIR, { recursive: true });
85
+ const url = getDownloadUrl();
86
+ const isTgz = url.endsWith('.tgz');
87
+ const tmpPath = isTgz ? `${BIN_PATH}.tgz` : `${BIN_PATH}.tmp`;
88
+
89
+ // Clean up any leftover partial files from a previous failed attempt
90
+ try { unlinkSync(tmpPath); } catch {}
91
+
92
+ console.log(`[cloudflared] Downloading from ${url}...`);
93
+ try {
94
+ await followRedirects(url, tmpPath);
95
+ } catch (e) {
96
+ try { unlinkSync(tmpPath); } catch {}
97
+ throw e;
98
+ }
99
+
100
+ if (isTgz) {
101
+ // Extract tgz (macOS)
102
+ try {
103
+ execSync(`tar -xzf "${tmpPath}" -C "${BIN_DIR}"`, { encoding: 'utf-8' });
104
+ } finally {
105
+ try { unlinkSync(tmpPath); } catch {}
106
+ }
107
+ } else {
108
+ // Rename .tmp to final name atomically
109
+ const { renameSync } = require('node:fs');
110
+ renameSync(tmpPath, BIN_PATH);
111
+ }
112
+
113
+ if (platform() !== 'win32') {
114
+ chmodSync(BIN_PATH, 0o755);
115
+ }
116
+
117
+ console.log(`[cloudflared] Installed to ${BIN_PATH}`);
118
+ return BIN_PATH;
119
+ })().finally(() => {
120
+ downloadPromise = null;
121
+ });
122
+
123
+ return downloadPromise;
124
+ }
125
+
126
+ export function isInstalled(): boolean {
127
+ return existsSync(BIN_PATH);
128
+ }
129
+
130
+ // ─── Tunnel process management ──────────────────────────────────
131
+ // Use globalThis to persist state across hot-reloads
132
+
133
+ interface TunnelState {
134
+ process: ChildProcess | null;
135
+ url: string | null;
136
+ status: 'stopped' | 'starting' | 'running' | 'error';
137
+ error: string | null;
138
+ log: string[];
139
+ }
140
+
141
+ const stateKey = Symbol.for('mw-tunnel-state');
142
+ const gAny = globalThis as any;
143
+ if (!gAny[stateKey]) {
144
+ gAny[stateKey] = { process: null, url: null, status: 'stopped', error: null, log: [] } as TunnelState;
145
+ }
146
+ const state: TunnelState = gAny[stateKey];
147
+
148
+ const MAX_LOG_LINES = 100;
149
+ const TUNNEL_STATE_FILE = join(getDataDir(), 'tunnel-state.json');
150
+
151
+ function saveTunnelState() {
152
+ try {
153
+ writeFileSync(TUNNEL_STATE_FILE, JSON.stringify({
154
+ url: state.url, status: state.status, error: state.error, pid: state.process?.pid || null,
155
+ }));
156
+ } catch {}
157
+ }
158
+
159
+ function loadTunnelState(): { url: string | null; status: string; error: string | null; pid: number | null } {
160
+ try {
161
+ return JSON.parse(readFileSync(TUNNEL_STATE_FILE, 'utf-8'));
162
+ } catch {
163
+ return { url: null, status: 'stopped', error: null, pid: null };
164
+ }
165
+ }
166
+
167
+ function pushLog(line: string) {
168
+ state.log.push(line);
169
+ if (state.log.length > MAX_LOG_LINES) state.log.shift();
170
+ }
171
+
172
+ export async function startTunnel(localPort: number = parseInt(process.env.PORT || '8403')): Promise<{ url?: string; error?: string }> {
173
+ console.log(`[tunnel] Starting tunnel on port ${localPort}...`);
174
+ // Prevent concurrent starts: state.process is already spawned, or another call is
175
+ // mid-flight between the guard and spawn (the async download window).
176
+ if (state.process || state.status === 'starting') {
177
+ return state.url ? { url: state.url } : { error: 'Tunnel is starting...' };
178
+ }
179
+
180
+ // Check if another process already has a tunnel running
181
+ const saved = loadTunnelState();
182
+ if (saved.pid && saved.status === 'running' && saved.url) {
183
+ try { process.kill(saved.pid, 0); return { url: saved.url }; } catch {}
184
+ }
185
+
186
+ // Claim 'starting' before any async work so concurrent callers are blocked
187
+ // from this point onward (pgrep kill + download can take seconds).
188
+ state.status = 'starting';
189
+ state.url = null;
190
+ state.error = null;
191
+ state.log = [];
192
+
193
+ // Kill ALL existing cloudflared processes to prevent duplicates
194
+ try {
195
+ const { execSync } = require('node:child_process');
196
+ const pids = execSync("pgrep -f 'cloudflared tunnel'", { encoding: 'utf-8', timeout: 3000, stdio: ['pipe', 'pipe', 'pipe'] }).trim();
197
+ for (const pid of pids.split('\n').filter(Boolean)) {
198
+ try { process.kill(parseInt(pid), 'SIGTERM'); } catch {}
199
+ }
200
+ } catch {}
201
+
202
+ // Generate new session code for remote login 2FA
203
+ try {
204
+ const { rotateSessionCode } = require('./password');
205
+ rotateSessionCode();
206
+ } catch {}
207
+
208
+ let binPath: string;
209
+ try {
210
+ binPath = await downloadCloudflared();
211
+ } catch (e) {
212
+ const msg = e instanceof Error ? e.message : String(e);
213
+ state.status = 'error';
214
+ state.error = msg;
215
+ return { error: msg };
216
+ }
217
+
218
+ return new Promise((resolve) => {
219
+ let resolved = false;
220
+
221
+ state.process = spawn(binPath, ['tunnel', '--url', `http://localhost:${localPort}`], {
222
+ stdio: ['ignore', 'pipe', 'pipe'],
223
+ env: { ...process.env },
224
+ });
225
+
226
+ const handleOutput = (data: Buffer) => {
227
+ const text = data.toString();
228
+ for (const line of text.split('\n')) {
229
+ if (!line.trim()) continue;
230
+ pushLog(line);
231
+
232
+ const urlMatch = line.match(/(https:\/\/[a-z0-9-]+\.trycloudflare\.com)/);
233
+ if (urlMatch && !state.url) {
234
+ state.url = urlMatch[1];
235
+ state.status = 'running';
236
+ saveTunnelState();
237
+ console.log(`[cloudflared] Tunnel URL: ${state.url}`);
238
+ startHealthCheck();
239
+ if (!resolved) {
240
+ resolved = true;
241
+ resolve({ url: state.url });
242
+ }
243
+ }
244
+ }
245
+ };
246
+
247
+ state.process.stdout?.on('data', handleOutput);
248
+ state.process.stderr?.on('data', handleOutput);
249
+
250
+ state.process.on('error', (err) => {
251
+ state.status = 'error';
252
+ state.error = err.message;
253
+ pushLog(`[error] ${err.message}`);
254
+ console.error(`[tunnel] Error: ${err.message}`);
255
+ if (!resolved) {
256
+ resolved = true;
257
+ resolve({ error: err.message });
258
+ }
259
+ });
260
+
261
+ state.process.on('exit', (code) => {
262
+ const recentLog = state.log.slice(-5).join(' ').slice(0, 200);
263
+ const reason = code !== 0 ? `cloudflared failed (exit ${code}): ${recentLog || 'no output'}` : 'cloudflared stopped';
264
+ console.log(`[tunnel] ${reason}`);
265
+ state.process = null;
266
+ if (state.status !== 'error') {
267
+ state.status = 'stopped';
268
+ }
269
+ state.url = null;
270
+ saveTunnelState();
271
+ pushLog(`[exit] ${reason}`);
272
+ if (!resolved) {
273
+ resolved = true;
274
+ resolve({ error: reason });
275
+ }
276
+ });
277
+
278
+ setTimeout(() => {
279
+ if (!resolved) {
280
+ resolved = true;
281
+ if (!state.url) {
282
+ state.status = 'error';
283
+ state.error = 'Timeout waiting for tunnel URL';
284
+ resolve({ error: 'Timeout waiting for tunnel URL (30s)' });
285
+ }
286
+ }
287
+ }, 30000);
288
+ });
289
+ }
290
+
291
+ export function stopTunnel() {
292
+ console.log('[tunnel] Stopping tunnel');
293
+ stopHealthCheck();
294
+ if (state.process) {
295
+ state.process.kill('SIGTERM');
296
+ state.process = null;
297
+ }
298
+ // Also kill by saved PID in case another worker started it
299
+ const saved = loadTunnelState();
300
+ if (saved.pid) {
301
+ try { process.kill(saved.pid, 'SIGTERM'); } catch {}
302
+ }
303
+ state.url = null;
304
+ state.status = 'stopped';
305
+ state.error = null;
306
+ saveTunnelState();
307
+ }
308
+
309
+ export function getTunnelStatus() {
310
+ // If this worker has the process, use in-memory state
311
+ if (state.process) {
312
+ return {
313
+ status: state.status,
314
+ url: state.url,
315
+ error: state.error,
316
+ installed: isInstalled(),
317
+ log: state.log.slice(-20),
318
+ };
319
+ }
320
+ // Otherwise read from file (another worker may have started it)
321
+ const saved = loadTunnelState();
322
+ if (saved.pid && saved.status === 'running') {
323
+ try { process.kill(saved.pid, 0); } catch {
324
+ // Process dead — clear stale state
325
+ return { status: 'stopped' as const, url: null, error: null, installed: isInstalled(), log: [] };
326
+ }
327
+ }
328
+ return {
329
+ status: (saved.status || 'stopped') as TunnelState['status'],
330
+ url: saved.url,
331
+ error: saved.error,
332
+ installed: isInstalled(),
333
+ log: state.log.slice(-20),
334
+ };
335
+ }
336
+
337
+ // ─── Tunnel health check ──────────────────────────────────────
338
+
339
+ let healthCheckTimer: ReturnType<typeof setInterval> | null = null;
340
+ let consecutiveFailures = 0;
341
+ const MAX_FAILURES = 3;
342
+ const HEALTH_CHECK_INTERVAL = 60_000; // 60s
343
+
344
+ async function checkTunnelHealth() {
345
+ if (state.status !== 'running' || !state.url) return;
346
+
347
+ try {
348
+ const controller = new AbortController();
349
+ const timeout = setTimeout(() => controller.abort(), 10_000);
350
+ const res = await fetch(state.url, {
351
+ method: 'HEAD',
352
+ signal: controller.signal,
353
+ redirect: 'manual',
354
+ });
355
+ clearTimeout(timeout);
356
+
357
+ // Any response (including 302 to login) means tunnel is alive
358
+ if (res.status > 0) {
359
+ if (consecutiveFailures > 0) {
360
+ pushLog(`[health] Tunnel recovered after ${consecutiveFailures} failures`);
361
+ }
362
+ consecutiveFailures = 0;
363
+ return;
364
+ }
365
+ } catch {
366
+ // fetch failed — tunnel likely down
367
+ }
368
+
369
+ consecutiveFailures++;
370
+ pushLog(`[health] Tunnel unreachable (${consecutiveFailures}/${MAX_FAILURES})`);
371
+
372
+ if (consecutiveFailures >= MAX_FAILURES) {
373
+ pushLog('[health] Tunnel appears dead — restarting...');
374
+ state.status = 'error';
375
+ state.error = 'Tunnel unreachable — restarting';
376
+
377
+ // Kill old process and restart
378
+ if (state.process) {
379
+ state.process.kill('SIGTERM');
380
+ state.process = null;
381
+ }
382
+ state.url = null;
383
+ consecutiveFailures = 0;
384
+
385
+ // Restart after a short delay
386
+ setTimeout(async () => {
387
+ const result = await startTunnel();
388
+ if (result.url) {
389
+ pushLog(`[health] Tunnel restarted: ${result.url}`);
390
+ // Notify via Telegram if configured
391
+ try {
392
+ const { loadSettings } = await import('./settings');
393
+ const settings = loadSettings();
394
+ if (settings.telegramBotToken && settings.telegramChatId) {
395
+ await fetch(`https://api.telegram.org/bot${settings.telegramBotToken}/sendMessage`, {
396
+ method: 'POST',
397
+ headers: { 'Content-Type': 'application/json' },
398
+ body: JSON.stringify({
399
+ chat_id: settings.telegramChatId,
400
+ text: `🔄 Tunnel restarted\n\nNew URL: ${result.url}`,
401
+ disable_web_page_preview: true,
402
+ }),
403
+ });
404
+ }
405
+ } catch {}
406
+ } else {
407
+ pushLog(`[health] Tunnel restart failed: ${result.error}`);
408
+ }
409
+ }, 3000);
410
+ }
411
+ }
412
+
413
+ export function startHealthCheck() {
414
+ if (healthCheckTimer) return;
415
+ healthCheckTimer = setInterval(checkTunnelHealth, HEALTH_CHECK_INTERVAL);
416
+ }
417
+
418
+ export function stopHealthCheck() {
419
+ if (healthCheckTimer) {
420
+ clearInterval(healthCheckTimer);
421
+ healthCheckTimer = null;
422
+ }
423
+ consecutiveFailures = 0;
424
+ }