@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,195 @@
1
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
2
+ import { join, dirname } from 'node:path';
3
+ import YAML from 'yaml';
4
+ import { encryptSecret, decryptSecret, isEncrypted, SECRET_FIELDS } from './crypto';
5
+ import { getDataDir } from './dirs';
6
+
7
+ const DATA_DIR = getDataDir();
8
+ const SETTINGS_FILE = join(DATA_DIR, 'settings.yaml');
9
+
10
+ export interface AgentEntry {
11
+ // Base agent fields (for detected agents like claude, codex, aider)
12
+ path?: string; name?: string; enabled?: boolean;
13
+ flags?: string[]; taskFlags?: string; interactiveCmd?: string; resumeFlag?: string; outputFormat?: string;
14
+ models?: { terminal?: string; task?: string; telegram?: string; help?: string; mobile?: string };
15
+ skipPermissionsFlag?: string;
16
+ requiresTTY?: boolean;
17
+ // Profile fields (for profiles that extend a base agent)
18
+ base?: string; // base agent ID (e.g., 'claude') — makes this a profile
19
+ // API profile fields
20
+ type?: 'cli' | 'api'; // 'api' = API mode, default = 'cli'
21
+ provider?: string; // API provider (e.g., 'anthropic', 'google')
22
+ model?: string; // model override (for both CLI and API profiles)
23
+ apiKey?: string; // per-profile API key (encrypted)
24
+ env?: Record<string, string>; // environment variables injected when spawning CLI
25
+ cliType?: 'claude-code' | 'codex' | 'aider' | 'generic'; // CLI tool type — determines session support, resume flags, etc.
26
+ profile?: string; // linked profile ID — overrides model, env, etc. when launching
27
+ }
28
+
29
+ export interface ProviderEntry {
30
+ apiKey?: string; // encrypted, fallback to env var
31
+ defaultModel?: string;
32
+ enabled?: boolean;
33
+ }
34
+
35
+ export interface Settings {
36
+ projectRoots: string[];
37
+ docRoots: string[];
38
+ claudePath: string;
39
+ claudeHome: string;
40
+ telegramBotToken: string;
41
+ telegramChatId: string;
42
+ notifyOnComplete: boolean;
43
+ notifyOnFailure: boolean;
44
+ tunnelAutoStart: boolean;
45
+ telegramTunnelPassword: string;
46
+ taskModel: string;
47
+ pipelineModel: string;
48
+ telegramModel: string;
49
+ skipPermissions: boolean;
50
+ notificationRetentionDays: number;
51
+ skillsRepoUrl: string;
52
+ displayName: string;
53
+ displayEmail: string;
54
+ favoriteProjects: string[];
55
+ defaultAgent: string;
56
+ telegramAgent: string;
57
+ docsAgent: string;
58
+ agents: Record<string, AgentEntry>;
59
+ providers: Record<string, ProviderEntry>; // API provider configs
60
+ }
61
+
62
+ const defaults: Settings = {
63
+ projectRoots: [],
64
+ docRoots: [],
65
+ claudePath: '',
66
+ claudeHome: '',
67
+ telegramBotToken: '',
68
+ telegramChatId: '',
69
+ notifyOnComplete: true,
70
+ notifyOnFailure: true,
71
+ tunnelAutoStart: false,
72
+ telegramTunnelPassword: '',
73
+ taskModel: 'default',
74
+ pipelineModel: 'default',
75
+ telegramModel: 'sonnet',
76
+ skipPermissions: false,
77
+ notificationRetentionDays: 30,
78
+ skillsRepoUrl: 'https://raw.githubusercontent.com/aiwatching/forge-skills/main',
79
+ displayName: 'Forge',
80
+ displayEmail: '',
81
+ favoriteProjects: [],
82
+ defaultAgent: 'claude',
83
+ telegramAgent: '',
84
+ docsAgent: '',
85
+ agents: {},
86
+ providers: {},
87
+ };
88
+
89
+ /** Decrypt nested apiKey fields in agents and providers */
90
+ function decryptNestedSecrets(settings: Settings): void {
91
+ // Decrypt provider apiKeys
92
+ if (settings.providers) {
93
+ for (const p of Object.values(settings.providers)) {
94
+ if (p.apiKey && isEncrypted(p.apiKey)) {
95
+ p.apiKey = decryptSecret(p.apiKey);
96
+ }
97
+ }
98
+ }
99
+ // Decrypt agent profile apiKeys
100
+ if (settings.agents) {
101
+ for (const a of Object.values(settings.agents)) {
102
+ if (a.apiKey && isEncrypted(a.apiKey)) {
103
+ a.apiKey = decryptSecret(a.apiKey);
104
+ }
105
+ }
106
+ }
107
+ }
108
+
109
+ /** Encrypt nested apiKey fields in agents and providers */
110
+ function encryptNestedSecrets(settings: Settings): void {
111
+ if (settings.providers) {
112
+ for (const p of Object.values(settings.providers)) {
113
+ if (p.apiKey && !isEncrypted(p.apiKey)) {
114
+ p.apiKey = encryptSecret(p.apiKey);
115
+ }
116
+ }
117
+ }
118
+ if (settings.agents) {
119
+ for (const a of Object.values(settings.agents)) {
120
+ if (a.apiKey && !isEncrypted(a.apiKey)) {
121
+ a.apiKey = encryptSecret(a.apiKey);
122
+ }
123
+ }
124
+ }
125
+ }
126
+
127
+ /** Load settings with secrets decrypted (for internal use) */
128
+ export function loadSettings(): Settings {
129
+ if (!existsSync(SETTINGS_FILE)) return { ...defaults };
130
+ try {
131
+ const raw = readFileSync(SETTINGS_FILE, 'utf-8');
132
+ const parsed = { ...defaults, ...YAML.parse(raw) };
133
+ // Decrypt top-level secret fields
134
+ for (const field of SECRET_FIELDS) {
135
+ if (parsed[field] && isEncrypted(parsed[field])) {
136
+ parsed[field] = decryptSecret(parsed[field]);
137
+ }
138
+ }
139
+ // Decrypt nested apiKeys
140
+ decryptNestedSecrets(parsed);
141
+ return parsed;
142
+ } catch {
143
+ return { ...defaults };
144
+ }
145
+ }
146
+
147
+ /** Load settings with secrets masked (for API response to frontend) */
148
+ export function loadSettingsMasked(): Settings & { _secretStatus: Record<string, boolean> } {
149
+ const settings = loadSettings();
150
+ const status: Record<string, boolean> = {};
151
+ for (const field of SECRET_FIELDS) {
152
+ status[field] = !!settings[field];
153
+ settings[field] = settings[field] ? '••••••••' : '';
154
+ }
155
+ // Mask nested apiKeys
156
+ if (settings.providers) {
157
+ for (const [name, p] of Object.entries(settings.providers)) {
158
+ status[`providers.${name}.apiKey`] = !!p.apiKey;
159
+ p.apiKey = p.apiKey ? '••••••••' : '';
160
+ }
161
+ }
162
+ if (settings.agents) {
163
+ for (const [name, a] of Object.entries(settings.agents)) {
164
+ if (a.apiKey) {
165
+ status[`agents.${name}.apiKey`] = true;
166
+ a.apiKey = '••••••••';
167
+ }
168
+ }
169
+ }
170
+ return { ...settings, _secretStatus: status };
171
+ }
172
+
173
+ /** Save settings, encrypting secret fields */
174
+ export function saveSettings(settings: Settings) {
175
+ const dir = dirname(SETTINGS_FILE);
176
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
177
+ // Deep copy to avoid mutating original
178
+ const toSave = JSON.parse(JSON.stringify(settings));
179
+ // Encrypt top-level secret fields
180
+ for (const field of SECRET_FIELDS) {
181
+ if (toSave[field] && !isEncrypted(toSave[field])) {
182
+ toSave[field] = encryptSecret(toSave[field]);
183
+ }
184
+ }
185
+ // Encrypt nested apiKeys
186
+ encryptNestedSecrets(toSave);
187
+ writeFileSync(SETTINGS_FILE, YAML.stringify(toSave), 'utf-8');
188
+ }
189
+
190
+ /** Verify a secret field's current value */
191
+ export function verifySecret(field: string, value: string): boolean {
192
+ const settings = loadSettings();
193
+ const current = (settings as any)[field] || '';
194
+ return current === value;
195
+ }
@@ -0,0 +1,458 @@
1
+ /**
2
+ * Skills Marketplace — sync, install, uninstall from remote registry.
3
+ */
4
+
5
+ import { existsSync, readdirSync, statSync, readFileSync, writeFileSync, mkdirSync, rmSync, cpSync } from 'node:fs';
6
+ import { join, dirname, basename, relative, sep } from 'node:path';
7
+ import { homedir } from 'node:os';
8
+ import { getDb } from '@/src/core/db/database';
9
+ import { getDbPath } from '@/src/config';
10
+ import { loadSettings } from './settings';
11
+
12
+ type ItemType = 'skill' | 'command';
13
+
14
+ interface SkillItem {
15
+ name: string;
16
+ type: ItemType;
17
+ displayName: string;
18
+ description: string;
19
+ author: string;
20
+ version: string;
21
+ tags: string[];
22
+ score: number;
23
+ rating: number;
24
+ sourceUrl: string;
25
+ installedGlobal: boolean;
26
+ installedProjects: string[];
27
+ installedVersion: string;
28
+ hasUpdate: boolean;
29
+ deletedRemotely: boolean;
30
+ }
31
+
32
+ function db() {
33
+ return getDb(getDbPath());
34
+ }
35
+
36
+ function getBaseUrl(): string {
37
+ const settings = loadSettings();
38
+ return settings.skillsRepoUrl || 'https://raw.githubusercontent.com/aiwatching/forge-skills/main';
39
+ }
40
+
41
+ function getRepoInfo(): { owner: string; repo: string; branch: string } {
42
+ const url = getBaseUrl();
43
+ const match = url.match(/github\.com\/([^/]+)\/([^/]+)(?:\/tree\/([^/]+))?/) ||
44
+ url.match(/raw\.githubusercontent\.com\/([^/]+)\/([^/]+)\/([^/]+)/);
45
+ if (match) return { owner: match[1], repo: match[2], branch: match[3] || 'main' };
46
+ return { owner: 'aiwatching', repo: 'forge-skills', branch: 'main' };
47
+ }
48
+
49
+ function compareVersions(a: string, b: string): number {
50
+ const pa = (a || '0.0.0').split('.').map(Number);
51
+ const pb = (b || '0.0.0').split('.').map(Number);
52
+ for (let i = 0; i < 3; i++) {
53
+ const diff = (pa[i] || 0) - (pb[i] || 0);
54
+ if (diff !== 0) return diff;
55
+ }
56
+ return 0;
57
+ }
58
+
59
+ // ─── Sync ─────────────────────────────────────────────────────
60
+
61
+ /** Max info.json enrichments per sync (incremental) */
62
+ const ENRICH_BATCH_SIZE = 10;
63
+
64
+ export async function syncSkills(): Promise<{ synced: number; enriched: number; total?: number; remaining?: number; error?: string }> {
65
+ console.log('[skills] Syncing from registry...');
66
+ const baseUrl = getBaseUrl();
67
+
68
+ try {
69
+ // Step 1: Fetch registry.json (always fresh)
70
+ const controller = new AbortController();
71
+ const timeout = setTimeout(() => controller.abort(), 10000);
72
+ const cacheBust = `_t=${Date.now()}`;
73
+ const res = await fetch(`${baseUrl}/registry.json?${cacheBust}`, {
74
+ signal: controller.signal,
75
+ headers: { 'Accept': 'application/json', 'Cache-Control': 'no-cache' },
76
+ });
77
+ clearTimeout(timeout);
78
+
79
+ if (!res.ok) return { synced: 0, enriched: 0, error: `Registry fetch failed: ${res.status}` };
80
+
81
+ const data = await res.json();
82
+
83
+ // Parse registry items (v1 + v2 support)
84
+ let rawItems: any[] = [];
85
+ if (data.version === 2) {
86
+ rawItems = [
87
+ ...(data.skills || []).map((s: any) => ({ ...s, type: s.type || 'skill' })),
88
+ ...(data.commands || []).map((c: any) => ({ ...c, type: c.type || 'command' })),
89
+ ];
90
+ } else {
91
+ rawItems = (data.skills || []).map((s: any) => ({ ...s, type: s.type || 'command' }));
92
+ }
93
+
94
+ // Step 2: Upsert all items from registry.json directly (fast, no extra fetch)
95
+ const upsertStmt = db().prepare(`
96
+ INSERT OR REPLACE INTO skills (name, type, display_name, description, author, version, tags, score, rating, source_url, archive, synced_at,
97
+ installed_global, installed_projects, installed_version)
98
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?,
99
+ COALESCE((SELECT synced_at FROM skills WHERE name = ?), datetime('now')),
100
+ COALESCE((SELECT installed_global FROM skills WHERE name = ?), 0),
101
+ COALESCE((SELECT installed_projects FROM skills WHERE name = ?), '[]'),
102
+ COALESCE((SELECT installed_version FROM skills WHERE name = ?), ''))
103
+ `);
104
+
105
+ const tx = db().transaction(() => {
106
+ for (const s of rawItems) {
107
+ upsertStmt.run(
108
+ s.name || '', s.type || 'skill',
109
+ s.display_name || '', s.description || '',
110
+ (s.author?.name || s.author || '').toString(), s.version || '',
111
+ JSON.stringify(s.tags || []),
112
+ s.score ?? 0, s.rating ?? 0, s.source?.url || s.source_url || '',
113
+ '', // archive
114
+ s.name || '', s.name || '', s.name || '', s.name || ''
115
+ );
116
+ }
117
+ });
118
+ tx();
119
+
120
+ // Step 3: Handle items no longer in registry
121
+ const registryNames = new Set(rawItems.map((s: any) => s.name));
122
+ const dbItems = db().prepare('SELECT name, installed_global, installed_projects FROM skills').all() as any[];
123
+ for (const row of dbItems) {
124
+ if (!registryNames.has(row.name)) {
125
+ const hasLocal = !!row.installed_global || JSON.parse(row.installed_projects || '[]').length > 0;
126
+ if (hasLocal) {
127
+ db().prepare('UPDATE skills SET deleted_remotely = 1 WHERE name = ?').run(row.name);
128
+ } else {
129
+ db().prepare('DELETE FROM skills WHERE name = ?').run(row.name);
130
+ }
131
+ }
132
+ }
133
+
134
+ // Step 4: Incremental enrichment — fetch info.json for oldest-synced items
135
+ // Pick items whose synced_at is oldest (or version changed since last enrich)
136
+ const staleItems = db().prepare(`
137
+ SELECT name, type, version FROM skills
138
+ WHERE deleted_remotely = 0
139
+ ORDER BY synced_at ASC
140
+ LIMIT ?
141
+ `).all(ENRICH_BATCH_SIZE) as any[];
142
+
143
+ let enriched = 0;
144
+ const enrichStmt = db().prepare(`
145
+ UPDATE skills SET
146
+ version = COALESCE(?, version),
147
+ tags = COALESCE(?, tags),
148
+ score = COALESCE(?, score),
149
+ rating = COALESCE(?, rating),
150
+ description = COALESCE(?, description),
151
+ synced_at = datetime('now')
152
+ WHERE name = ?
153
+ `);
154
+
155
+ await Promise.all(staleItems.map(async (s: any) => {
156
+ try {
157
+ const repoDir = s.type === 'skill' ? 'skills' : 'commands';
158
+ let infoRes = await fetch(`${baseUrl}/${repoDir}/${s.name}/info.json?${cacheBust}`, { signal: AbortSignal.timeout(5000) });
159
+ if (!infoRes.ok) {
160
+ const altDir = s.type === 'skill' ? 'commands' : 'skills';
161
+ infoRes = await fetch(`${baseUrl}/${altDir}/${s.name}/info.json?${cacheBust}`, { signal: AbortSignal.timeout(5000) });
162
+ }
163
+ if (infoRes.ok) {
164
+ const info = await infoRes.json();
165
+ enrichStmt.run(
166
+ info.version || null,
167
+ info.tags?.length ? JSON.stringify(info.tags) : null,
168
+ info.score ?? null,
169
+ info.rating ?? null,
170
+ info.description || null,
171
+ s.name
172
+ );
173
+ enriched++;
174
+ } else {
175
+ // No info.json — just update synced_at so it rotates to the back
176
+ db().prepare('UPDATE skills SET synced_at = datetime(\'now\') WHERE name = ?').run(s.name);
177
+ }
178
+ } catch {
179
+ // Timeout/error — update synced_at to avoid retrying immediately
180
+ db().prepare('UPDATE skills SET synced_at = datetime(\'now\') WHERE name = ?').run(s.name);
181
+ }
182
+ }));
183
+
184
+ const totalCount = (db().prepare('SELECT count(*) as c FROM skills WHERE deleted_remotely = 0').get() as any).c;
185
+ const remaining = totalCount - ENRICH_BATCH_SIZE; // approximate items not yet enriched this round
186
+ console.log(`[skills] Synced ${rawItems.length} items, enriched ${enriched}/${staleItems.length} from info.json`);
187
+ return { synced: rawItems.length, enriched, total: totalCount, remaining: Math.max(0, remaining) };
188
+ } catch (e) {
189
+ const msg = e instanceof Error ? e.message : String(e);
190
+ console.error(`[skills] Sync failed:`, msg);
191
+ return { synced: 0, enriched: 0, error: msg };
192
+ }
193
+ }
194
+
195
+ // ─── List ────────────────────────────────────────────────────
196
+
197
+ export function listSkills(): SkillItem[] {
198
+ const rows = db().prepare('SELECT * FROM skills ORDER BY type ASC, score DESC, display_name ASC').all() as any[];
199
+ return rows.map(r => {
200
+ const installedVersion = r.installed_version || '';
201
+ const registryVersion = r.version || '';
202
+ const isInstalled = !!r.installed_global || JSON.parse(r.installed_projects || '[]').length > 0;
203
+ return {
204
+ name: r.name,
205
+ type: r.type || 'skill',
206
+ displayName: r.display_name,
207
+ description: r.description,
208
+ author: r.author,
209
+ version: registryVersion,
210
+ tags: JSON.parse(r.tags || '[]'),
211
+ score: r.score,
212
+ rating: r.rating || 0,
213
+ sourceUrl: r.source_url,
214
+ installedGlobal: !!r.installed_global,
215
+ installedProjects: JSON.parse(r.installed_projects || '[]'),
216
+ installedVersion,
217
+ hasUpdate: isInstalled && !!registryVersion && !!installedVersion && compareVersions(registryVersion, installedVersion) > 0,
218
+ deletedRemotely: !!r.deleted_remotely,
219
+ };
220
+ });
221
+ }
222
+
223
+ // ─── Download directory from GitHub ──────────────────────────
224
+
225
+ /** Recursively list all files in a skill/command directory via GitHub API */
226
+ async function listRepoFiles(name: string, type: ItemType): Promise<{ path: string; download_url: string }[]> {
227
+ const repo = getRepoInfo();
228
+ const files: { path: string; download_url: string }[] = [];
229
+
230
+ async function recurse(apiUrl: string, prefix: string) {
231
+ const res = await fetch(apiUrl, {
232
+ headers: { 'Accept': 'application/vnd.github.v3+json' },
233
+ });
234
+ if (!res.ok) return;
235
+ const items = await res.json();
236
+ if (!Array.isArray(items)) return;
237
+ for (const item of items) {
238
+ if (item.type === 'file' && item.download_url) {
239
+ files.push({ path: join(prefix, item.name), download_url: item.download_url });
240
+ } else if (item.type === 'dir') {
241
+ await recurse(item.url, join(prefix, item.name));
242
+ }
243
+ }
244
+ }
245
+
246
+ // Try skills/ first, then commands/
247
+ const dirs = type === 'skill' ? ['skills', 'commands'] : ['commands', 'skills'];
248
+ for (const dir of dirs) {
249
+ const url = `https://api.github.com/repos/${repo.owner}/${repo.repo}/contents/${dir}/${name}?ref=${repo.branch}`;
250
+ await recurse(url, '');
251
+ if (files.length > 0) return files;
252
+ }
253
+ return files;
254
+ }
255
+
256
+ async function downloadFile(url: string): Promise<string> {
257
+ const res = await fetch(url);
258
+ if (!res.ok) throw new Error(`Download failed: ${res.status}`);
259
+ return res.text();
260
+ }
261
+
262
+ // ─── Install ─────────────────────────────────────────────────
263
+
264
+ function getClaudeHome(): string {
265
+ const settings = loadSettings();
266
+ return settings.claudeHome || join(homedir(), '.claude');
267
+ }
268
+
269
+ function getSkillDir(name: string, type: ItemType, projectPath?: string): string {
270
+ const base = projectPath || getClaudeHome();
271
+ const subdir = type === 'skill' ? 'skills' : 'commands';
272
+ return join(base, '.claude', subdir, name);
273
+ }
274
+
275
+ export async function installGlobal(name: string): Promise<void> {
276
+ const skill = db().prepare('SELECT * FROM skills WHERE name = ?').get(name) as any;
277
+ if (!skill) throw new Error(`Skill "${name}" not found`);
278
+
279
+ const type: ItemType = skill.type || 'skill';
280
+ const claudeHome = getClaudeHome();
281
+ const subdir = type === 'skill' ? 'skills' : 'commands';
282
+ const targetDir = join(claudeHome, subdir, name);
283
+
284
+ const files = await listRepoFiles(name, type);
285
+ if (files.length === 0) throw new Error(`No files found for ${name}`);
286
+
287
+ mkdirSync(targetDir, { recursive: true });
288
+ for (const f of files) {
289
+ const content = await downloadFile(f.download_url);
290
+ const targetPath = join(targetDir, f.path);
291
+ mkdirSync(dirname(targetPath), { recursive: true });
292
+ writeFileSync(targetPath, content);
293
+ }
294
+
295
+ // Update installed state
296
+ db().prepare('UPDATE skills SET installed_global = 1, installed_version = ? WHERE name = ?')
297
+ .run(skill.version || '', name);
298
+ }
299
+
300
+ export async function installProject(name: string, projectPath: string): Promise<void> {
301
+ const skill = db().prepare('SELECT * FROM skills WHERE name = ?').get(name) as any;
302
+ if (!skill) throw new Error(`Skill "${name}" not found`);
303
+
304
+ const type: ItemType = skill.type || 'skill';
305
+ const subdir = type === 'skill' ? 'skills' : 'commands';
306
+ const targetDir = join(projectPath, '.claude', subdir, name);
307
+
308
+ const files = await listRepoFiles(name, type);
309
+ if (files.length === 0) throw new Error(`No files found for ${name}`);
310
+
311
+ mkdirSync(targetDir, { recursive: true });
312
+ for (const f of files) {
313
+ const content = await downloadFile(f.download_url);
314
+ const targetPath = join(targetDir, f.path);
315
+ mkdirSync(dirname(targetPath), { recursive: true });
316
+ writeFileSync(targetPath, content);
317
+ }
318
+
319
+ // Update installed state
320
+ const existing = JSON.parse(skill.installed_projects || '[]');
321
+ if (!existing.includes(projectPath)) existing.push(projectPath);
322
+ db().prepare('UPDATE skills SET installed_projects = ?, installed_version = ? WHERE name = ?')
323
+ .run(JSON.stringify(existing), skill.version || '', name);
324
+ }
325
+
326
+ // ─── Uninstall ───────────────────────────────────────────────
327
+
328
+ export function uninstallGlobal(name: string): void {
329
+ const skill = db().prepare('SELECT * FROM skills WHERE name = ?').get(name) as any;
330
+ if (!skill) return;
331
+
332
+ const type: ItemType = skill.type || 'skill';
333
+ const claudeHome = getClaudeHome();
334
+ const subdir = type === 'skill' ? 'skills' : 'commands';
335
+ const targetDir = join(claudeHome, subdir, name);
336
+
337
+ if (existsSync(targetDir)) rmSync(targetDir, { recursive: true, force: true });
338
+
339
+ db().prepare('UPDATE skills SET installed_global = 0 WHERE name = ?').run(name);
340
+ // Clear installed_version if no project installs remain
341
+ const remaining = JSON.parse(skill.installed_projects || '[]');
342
+ if (remaining.length === 0) {
343
+ db().prepare('UPDATE skills SET installed_version = ? WHERE name = ?').run('', name);
344
+ }
345
+ }
346
+
347
+ export function uninstallProject(name: string, projectPath: string): void {
348
+ const skill = db().prepare('SELECT * FROM skills WHERE name = ?').get(name) as any;
349
+ if (!skill) return;
350
+
351
+ const type: ItemType = skill.type || 'skill';
352
+ const subdir = type === 'skill' ? 'skills' : 'commands';
353
+ const targetDir = join(projectPath, '.claude', subdir, name);
354
+
355
+ if (existsSync(targetDir)) rmSync(targetDir, { recursive: true, force: true });
356
+
357
+ const existing = JSON.parse(skill.installed_projects || '[]').filter((p: string) => p !== projectPath);
358
+ db().prepare('UPDATE skills SET installed_projects = ? WHERE name = ?')
359
+ .run(JSON.stringify(existing), name);
360
+ // Clear installed_version if nothing remains
361
+ if (!skill.installed_global && existing.length === 0) {
362
+ db().prepare('UPDATE skills SET installed_version = ? WHERE name = ?').run('', name);
363
+ }
364
+ }
365
+
366
+ // ─── Refresh install state from filesystem ───────────────────
367
+
368
+ export function refreshInstallState(projectPaths: string[]): void {
369
+ const claudeHome = getClaudeHome();
370
+ const rows = db().prepare('SELECT name, type FROM skills').all() as any[];
371
+
372
+ for (const row of rows) {
373
+ const type: ItemType = row.type || 'skill';
374
+ const subdir = type === 'skill' ? 'skills' : 'commands';
375
+
376
+ // Check global
377
+ const globalDir = join(claudeHome, subdir, row.name);
378
+ const globalInstalled = existsSync(globalDir);
379
+
380
+ // Check projects
381
+ const installedIn: string[] = [];
382
+ for (const pp of projectPaths) {
383
+ const projDir = join(pp, '.claude', subdir, row.name);
384
+ if (existsSync(projDir)) installedIn.push(pp);
385
+ }
386
+
387
+ // Read installed version from info.json if available
388
+ let installedVersion = '';
389
+ const checkDirs = globalInstalled ? [globalDir] : installedIn.length > 0 ? [join(installedIn[0], '.claude', subdir, row.name)] : [];
390
+ for (const d of checkDirs) {
391
+ const infoPath = join(d, 'info.json');
392
+ if (existsSync(infoPath)) {
393
+ try {
394
+ const info = JSON.parse(readFileSync(infoPath, 'utf-8'));
395
+ installedVersion = info.version || '';
396
+ } catch {}
397
+ break;
398
+ }
399
+ }
400
+
401
+ db().prepare('UPDATE skills SET installed_global = ?, installed_projects = ?, installed_version = ? WHERE name = ?')
402
+ .run(globalInstalled ? 1 : 0, JSON.stringify(installedIn), installedVersion, row.name);
403
+ }
404
+ }
405
+
406
+ // ─── Check local modifications ───────────────────────────────
407
+
408
+ export async function checkLocalModified(name: string): Promise<boolean> {
409
+ const skill = db().prepare('SELECT * FROM skills WHERE name = ?').get(name) as any;
410
+ if (!skill) return false;
411
+
412
+ const type: ItemType = skill.type || 'skill';
413
+ const claudeHome = getClaudeHome();
414
+ const subdir = type === 'skill' ? 'skills' : 'commands';
415
+ const localDir = join(claudeHome, subdir, name);
416
+
417
+ if (!existsSync(localDir)) return false;
418
+
419
+ // Compare with remote files
420
+ try {
421
+ const remoteFiles = await listRepoFiles(name, type);
422
+ for (const rf of remoteFiles) {
423
+ const localPath = join(localDir, rf.path);
424
+ if (!existsSync(localPath)) return true;
425
+ const localContent = readFileSync(localPath, 'utf-8');
426
+ const remoteContent = await downloadFile(rf.download_url);
427
+ if (localContent !== remoteContent) return true;
428
+ }
429
+ } catch {
430
+ return false;
431
+ }
432
+
433
+ return false;
434
+ }
435
+
436
+ // ─── Purge deleted skill ─────────────────────────────────────
437
+
438
+ export function purgeDeletedSkill(name: string): void {
439
+ const skill = db().prepare('SELECT * FROM skills WHERE name = ?').get(name) as any;
440
+ if (!skill) return;
441
+
442
+ const type: ItemType = skill.type || 'skill';
443
+ const claudeHome = getClaudeHome();
444
+ const subdir = type === 'skill' ? 'skills' : 'commands';
445
+
446
+ // Remove global
447
+ const globalDir = join(claudeHome, subdir, name);
448
+ if (existsSync(globalDir)) rmSync(globalDir, { recursive: true, force: true });
449
+
450
+ // Remove from projects
451
+ const projects = JSON.parse(skill.installed_projects || '[]');
452
+ for (const pp of projects) {
453
+ const projDir = join(pp, '.claude', subdir, name);
454
+ if (existsSync(projDir)) rmSync(projDir, { recursive: true, force: true });
455
+ }
456
+
457
+ db().prepare('DELETE FROM skills WHERE name = ?').run(name);
458
+ }