crewly 1.0.6 → 1.0.8

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 (321) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +146 -40
  3. package/config/roles/architect/prompt.md +11 -0
  4. package/config/roles/backend-developer/prompt.md +11 -0
  5. package/config/roles/designer/prompt.md +11 -0
  6. package/config/roles/developer/prompt.md +18 -4
  7. package/config/roles/frontend-developer/prompt.md +11 -0
  8. package/config/roles/fullstack-dev/prompt.md +11 -0
  9. package/config/roles/generalist/prompt.md +11 -0
  10. package/config/roles/orchestrator/prompt.md +53 -10
  11. package/config/roles/product-manager/prompt.md +18 -4
  12. package/config/roles/qa/prompt.md +11 -0
  13. package/config/roles/qa-engineer/prompt.md +11 -0
  14. package/config/roles/sales/prompt.md +11 -0
  15. package/config/roles/support/prompt.md +11 -0
  16. package/config/roles/tpm/prompt.md +11 -0
  17. package/config/skills/orchestrator/complete-task/execute.sh +1 -0
  18. package/config/templates/agent-claude-md.md +16 -0
  19. package/config/templates/research-team.json +22 -0
  20. package/config/templates/startup-team.json +22 -0
  21. package/config/templates/web-dev-team.json +22 -0
  22. package/dist/backend/backend/src/constants.d.ts +61 -1
  23. package/dist/backend/backend/src/constants.d.ts.map +1 -1
  24. package/dist/backend/backend/src/constants.js +65 -7
  25. package/dist/backend/backend/src/constants.js.map +1 -1
  26. package/dist/backend/backend/src/controllers/slack/slack.controller.d.ts.map +1 -1
  27. package/dist/backend/backend/src/controllers/slack/slack.controller.js +7 -0
  28. package/dist/backend/backend/src/controllers/slack/slack.controller.js.map +1 -1
  29. package/dist/backend/backend/src/controllers/task-management/task-management.controller.d.ts +7 -0
  30. package/dist/backend/backend/src/controllers/task-management/task-management.controller.d.ts.map +1 -1
  31. package/dist/backend/backend/src/controllers/task-management/task-management.controller.js +174 -4
  32. package/dist/backend/backend/src/controllers/task-management/task-management.controller.js.map +1 -1
  33. package/dist/backend/backend/src/index.d.ts.map +1 -1
  34. package/dist/backend/backend/src/index.js +34 -0
  35. package/dist/backend/backend/src/index.js.map +1 -1
  36. package/dist/backend/backend/src/routes/modules/task-management.routes.d.ts.map +1 -1
  37. package/dist/backend/backend/src/routes/modules/task-management.routes.js +2 -0
  38. package/dist/backend/backend/src/routes/modules/task-management.routes.js.map +1 -1
  39. package/dist/backend/backend/src/services/agent/agent-registration.service.d.ts +8 -0
  40. package/dist/backend/backend/src/services/agent/agent-registration.service.d.ts.map +1 -1
  41. package/dist/backend/backend/src/services/agent/agent-registration.service.js +75 -17
  42. package/dist/backend/backend/src/services/agent/agent-registration.service.js.map +1 -1
  43. package/dist/backend/backend/src/services/agent/context-window-monitor.service.d.ts +222 -0
  44. package/dist/backend/backend/src/services/agent/context-window-monitor.service.d.ts.map +1 -0
  45. package/dist/backend/backend/src/services/agent/context-window-monitor.service.js +621 -0
  46. package/dist/backend/backend/src/services/agent/context-window-monitor.service.js.map +1 -0
  47. package/dist/backend/backend/src/services/index.d.ts +3 -0
  48. package/dist/backend/backend/src/services/index.d.ts.map +1 -1
  49. package/dist/backend/backend/src/services/index.js +5 -0
  50. package/dist/backend/backend/src/services/index.js.map +1 -1
  51. package/dist/backend/backend/src/services/mcp-client.d.ts +233 -0
  52. package/dist/backend/backend/src/services/mcp-client.d.ts.map +1 -0
  53. package/dist/backend/backend/src/services/mcp-client.js +297 -0
  54. package/dist/backend/backend/src/services/mcp-client.js.map +1 -0
  55. package/dist/backend/backend/src/services/mcp-server.d.ts +167 -0
  56. package/dist/backend/backend/src/services/mcp-server.d.ts.map +1 -0
  57. package/dist/backend/backend/src/services/mcp-server.js +586 -0
  58. package/dist/backend/backend/src/services/mcp-server.js.map +1 -0
  59. package/dist/backend/backend/src/services/messaging/message-queue.service.d.ts +2 -0
  60. package/dist/backend/backend/src/services/messaging/message-queue.service.d.ts.map +1 -1
  61. package/dist/backend/backend/src/services/messaging/message-queue.service.js +6 -1
  62. package/dist/backend/backend/src/services/messaging/message-queue.service.js.map +1 -1
  63. package/dist/backend/backend/src/services/messaging/queue-processor.service.d.ts +10 -0
  64. package/dist/backend/backend/src/services/messaging/queue-processor.service.d.ts.map +1 -1
  65. package/dist/backend/backend/src/services/messaging/queue-processor.service.js +94 -38
  66. package/dist/backend/backend/src/services/messaging/queue-processor.service.js.map +1 -1
  67. package/dist/backend/backend/src/services/quality/index.d.ts +1 -0
  68. package/dist/backend/backend/src/services/quality/index.d.ts.map +1 -1
  69. package/dist/backend/backend/src/services/quality/index.js +1 -0
  70. package/dist/backend/backend/src/services/quality/index.js.map +1 -1
  71. package/dist/backend/backend/src/services/quality/task-output-validator.service.d.ts +84 -0
  72. package/dist/backend/backend/src/services/quality/task-output-validator.service.d.ts.map +1 -0
  73. package/dist/backend/backend/src/services/quality/task-output-validator.service.js +163 -0
  74. package/dist/backend/backend/src/services/quality/task-output-validator.service.js.map +1 -0
  75. package/dist/backend/backend/src/services/runtime-adapter.d.ts +234 -0
  76. package/dist/backend/backend/src/services/runtime-adapter.d.ts.map +1 -0
  77. package/dist/backend/backend/src/services/runtime-adapter.js +180 -0
  78. package/dist/backend/backend/src/services/runtime-adapter.js.map +1 -0
  79. package/dist/backend/backend/src/services/slack/slack-credentials.service.d.ts +33 -0
  80. package/dist/backend/backend/src/services/slack/slack-credentials.service.d.ts.map +1 -0
  81. package/dist/backend/backend/src/services/slack/slack-credentials.service.js +103 -0
  82. package/dist/backend/backend/src/services/slack/slack-credentials.service.js.map +1 -0
  83. package/dist/backend/backend/src/services/slack/slack-initializer.d.ts +9 -1
  84. package/dist/backend/backend/src/services/slack/slack-initializer.d.ts.map +1 -1
  85. package/dist/backend/backend/src/services/slack/slack-initializer.js +31 -2
  86. package/dist/backend/backend/src/services/slack/slack-initializer.js.map +1 -1
  87. package/dist/backend/backend/src/types/event-bus.types.d.ts +2 -2
  88. package/dist/backend/backend/src/types/event-bus.types.d.ts.map +1 -1
  89. package/dist/backend/backend/src/types/event-bus.types.js +2 -0
  90. package/dist/backend/backend/src/types/event-bus.types.js.map +1 -1
  91. package/dist/backend/backend/src/types/index.d.ts +1 -0
  92. package/dist/backend/backend/src/types/index.d.ts.map +1 -1
  93. package/dist/backend/backend/src/types/index.js +2 -0
  94. package/dist/backend/backend/src/types/index.js.map +1 -1
  95. package/dist/backend/backend/src/types/messaging.types.d.ts +9 -6
  96. package/dist/backend/backend/src/types/messaging.types.d.ts.map +1 -1
  97. package/dist/backend/backend/src/types/messaging.types.js +10 -3
  98. package/dist/backend/backend/src/types/messaging.types.js.map +1 -1
  99. package/dist/backend/backend/src/types/task-output.types.d.ts +78 -0
  100. package/dist/backend/backend/src/types/task-output.types.d.ts.map +1 -0
  101. package/dist/backend/backend/src/types/task-output.types.js +27 -0
  102. package/dist/backend/backend/src/types/task-output.types.js.map +1 -0
  103. package/dist/backend/backend/src/websocket/terminal.gateway.d.ts +13 -0
  104. package/dist/backend/backend/src/websocket/terminal.gateway.d.ts.map +1 -1
  105. package/dist/backend/backend/src/websocket/terminal.gateway.js +12 -0
  106. package/dist/backend/backend/src/websocket/terminal.gateway.js.map +1 -1
  107. package/dist/cli/backend/src/constants.d.ts +751 -0
  108. package/dist/cli/backend/src/constants.d.ts.map +1 -0
  109. package/dist/cli/backend/src/constants.js +550 -0
  110. package/dist/cli/backend/src/constants.js.map +1 -0
  111. package/dist/cli/backend/src/models/Project.d.ts +18 -0
  112. package/dist/cli/backend/src/models/Project.d.ts.map +1 -0
  113. package/dist/cli/backend/src/models/Project.js +70 -0
  114. package/dist/cli/backend/src/models/Project.js.map +1 -0
  115. package/dist/cli/backend/src/models/ScheduledMessage.d.ts +27 -0
  116. package/dist/cli/backend/src/models/ScheduledMessage.d.ts.map +1 -0
  117. package/dist/cli/backend/src/models/ScheduledMessage.js +50 -0
  118. package/dist/cli/backend/src/models/ScheduledMessage.js.map +1 -0
  119. package/dist/cli/backend/src/models/Team.d.ts +20 -0
  120. package/dist/cli/backend/src/models/Team.d.ts.map +1 -0
  121. package/dist/cli/backend/src/models/Team.js +120 -0
  122. package/dist/cli/backend/src/models/Team.js.map +1 -0
  123. package/dist/cli/backend/src/models/Ticket.d.ts +24 -0
  124. package/dist/cli/backend/src/models/Ticket.d.ts.map +1 -0
  125. package/dist/cli/backend/src/models/Ticket.js +102 -0
  126. package/dist/cli/backend/src/models/Ticket.js.map +1 -0
  127. package/dist/cli/backend/src/models/index.d.ts +5 -0
  128. package/dist/cli/backend/src/models/index.d.ts.map +1 -0
  129. package/dist/cli/backend/src/models/index.js +5 -0
  130. package/dist/cli/backend/src/models/index.js.map +1 -0
  131. package/dist/cli/backend/src/services/core/config.service.d.ts +91 -0
  132. package/dist/cli/backend/src/services/core/config.service.d.ts.map +1 -0
  133. package/dist/cli/backend/src/services/core/config.service.js +246 -0
  134. package/dist/cli/backend/src/services/core/config.service.js.map +1 -0
  135. package/dist/cli/backend/src/services/core/logger.service.d.ts +70 -0
  136. package/dist/cli/backend/src/services/core/logger.service.d.ts.map +1 -0
  137. package/dist/cli/backend/src/services/core/logger.service.js +350 -0
  138. package/dist/cli/backend/src/services/core/logger.service.js.map +1 -0
  139. package/dist/cli/backend/src/services/core/storage.service.d.ts +269 -0
  140. package/dist/cli/backend/src/services/core/storage.service.d.ts.map +1 -0
  141. package/dist/cli/backend/src/services/core/storage.service.js +1406 -0
  142. package/dist/cli/backend/src/services/core/storage.service.js.map +1 -0
  143. package/dist/cli/backend/src/services/core/teams-backup.service.d.ts +92 -0
  144. package/dist/cli/backend/src/services/core/teams-backup.service.d.ts.map +1 -0
  145. package/dist/cli/backend/src/services/core/teams-backup.service.js +120 -0
  146. package/dist/cli/backend/src/services/core/teams-backup.service.js.map +1 -0
  147. package/dist/cli/backend/src/services/knowledge/knowledge-search.service.d.ts +125 -0
  148. package/dist/cli/backend/src/services/knowledge/knowledge-search.service.d.ts.map +1 -0
  149. package/dist/cli/backend/src/services/knowledge/knowledge-search.service.js +247 -0
  150. package/dist/cli/backend/src/services/knowledge/knowledge-search.service.js.map +1 -0
  151. package/dist/cli/backend/src/services/knowledge/knowledge.service.d.ts +153 -0
  152. package/dist/cli/backend/src/services/knowledge/knowledge.service.d.ts.map +1 -0
  153. package/dist/cli/backend/src/services/knowledge/knowledge.service.js +409 -0
  154. package/dist/cli/backend/src/services/knowledge/knowledge.service.js.map +1 -0
  155. package/dist/cli/backend/src/services/mcp-server.d.ts +167 -0
  156. package/dist/cli/backend/src/services/mcp-server.d.ts.map +1 -0
  157. package/dist/cli/backend/src/services/mcp-server.js +586 -0
  158. package/dist/cli/backend/src/services/mcp-server.js.map +1 -0
  159. package/dist/cli/backend/src/services/memory/agent-memory.service.d.ts +259 -0
  160. package/dist/cli/backend/src/services/memory/agent-memory.service.d.ts.map +1 -0
  161. package/dist/cli/backend/src/services/memory/agent-memory.service.js +539 -0
  162. package/dist/cli/backend/src/services/memory/agent-memory.service.js.map +1 -0
  163. package/dist/cli/backend/src/services/memory/memory.service.d.ts +306 -0
  164. package/dist/cli/backend/src/services/memory/memory.service.d.ts.map +1 -0
  165. package/dist/cli/backend/src/services/memory/memory.service.js +517 -0
  166. package/dist/cli/backend/src/services/memory/memory.service.js.map +1 -0
  167. package/dist/cli/backend/src/services/memory/project-memory.service.d.ts +252 -0
  168. package/dist/cli/backend/src/services/memory/project-memory.service.d.ts.map +1 -0
  169. package/dist/cli/backend/src/services/memory/project-memory.service.js +600 -0
  170. package/dist/cli/backend/src/services/memory/project-memory.service.js.map +1 -0
  171. package/dist/cli/backend/src/types/auto-assign.types.d.ts +271 -0
  172. package/dist/cli/backend/src/types/auto-assign.types.d.ts.map +1 -0
  173. package/dist/cli/backend/src/types/auto-assign.types.js +136 -0
  174. package/dist/cli/backend/src/types/auto-assign.types.js.map +1 -0
  175. package/dist/cli/backend/src/types/budget.types.d.ts +217 -0
  176. package/dist/cli/backend/src/types/budget.types.d.ts.map +1 -0
  177. package/dist/cli/backend/src/types/budget.types.js +82 -0
  178. package/dist/cli/backend/src/types/budget.types.js.map +1 -0
  179. package/dist/cli/backend/src/types/chat.types.d.ts +550 -0
  180. package/dist/cli/backend/src/types/chat.types.d.ts.map +1 -0
  181. package/dist/cli/backend/src/types/chat.types.js +743 -0
  182. package/dist/cli/backend/src/types/chat.types.js.map +1 -0
  183. package/dist/cli/backend/src/types/continuation.types.d.ts +237 -0
  184. package/dist/cli/backend/src/types/continuation.types.d.ts.map +1 -0
  185. package/dist/cli/backend/src/types/continuation.types.js +10 -0
  186. package/dist/cli/backend/src/types/continuation.types.js.map +1 -0
  187. package/dist/cli/backend/src/types/index.d.ts +164 -0
  188. package/dist/cli/backend/src/types/index.d.ts.map +1 -0
  189. package/dist/cli/backend/src/types/index.js +25 -0
  190. package/dist/cli/backend/src/types/index.js.map +1 -0
  191. package/dist/cli/backend/src/types/knowledge.types.d.ts +195 -0
  192. package/dist/cli/backend/src/types/knowledge.types.d.ts.map +1 -0
  193. package/dist/cli/backend/src/types/knowledge.types.js +38 -0
  194. package/dist/cli/backend/src/types/knowledge.types.js.map +1 -0
  195. package/dist/cli/backend/src/types/memory.types.d.ts +587 -0
  196. package/dist/cli/backend/src/types/memory.types.d.ts.map +1 -0
  197. package/dist/cli/backend/src/types/memory.types.js +47 -0
  198. package/dist/cli/backend/src/types/memory.types.js.map +1 -0
  199. package/dist/cli/backend/src/types/quality-gate.types.d.ts +171 -0
  200. package/dist/cli/backend/src/types/quality-gate.types.d.ts.map +1 -0
  201. package/dist/cli/backend/src/types/quality-gate.types.js +42 -0
  202. package/dist/cli/backend/src/types/quality-gate.types.js.map +1 -0
  203. package/dist/cli/backend/src/types/role.types.d.ts +260 -0
  204. package/dist/cli/backend/src/types/role.types.d.ts.map +1 -0
  205. package/dist/cli/backend/src/types/role.types.js +238 -0
  206. package/dist/cli/backend/src/types/role.types.js.map +1 -0
  207. package/dist/cli/backend/src/types/scheduler.types.d.ts +254 -0
  208. package/dist/cli/backend/src/types/scheduler.types.d.ts.map +1 -0
  209. package/dist/cli/backend/src/types/scheduler.types.js +32 -0
  210. package/dist/cli/backend/src/types/scheduler.types.js.map +1 -0
  211. package/dist/cli/backend/src/types/settings.types.d.ts +178 -0
  212. package/dist/cli/backend/src/types/settings.types.d.ts.map +1 -0
  213. package/dist/cli/backend/src/types/settings.types.js +206 -0
  214. package/dist/cli/backend/src/types/settings.types.js.map +1 -0
  215. package/dist/cli/backend/src/types/skill.types.d.ts +515 -0
  216. package/dist/cli/backend/src/types/skill.types.d.ts.map +1 -0
  217. package/dist/cli/backend/src/types/skill.types.js +481 -0
  218. package/dist/cli/backend/src/types/skill.types.js.map +1 -0
  219. package/dist/cli/backend/src/types/sop.types.d.ts +224 -0
  220. package/dist/cli/backend/src/types/sop.types.d.ts.map +1 -0
  221. package/dist/cli/backend/src/types/sop.types.js +85 -0
  222. package/dist/cli/backend/src/types/sop.types.js.map +1 -0
  223. package/dist/cli/backend/src/types/task-output.types.d.ts +78 -0
  224. package/dist/cli/backend/src/types/task-output.types.d.ts.map +1 -0
  225. package/dist/cli/backend/src/types/task-output.types.js +27 -0
  226. package/dist/cli/backend/src/types/task-output.types.js.map +1 -0
  227. package/dist/cli/backend/src/utils/file-io.utils.d.ts +102 -0
  228. package/dist/cli/backend/src/utils/file-io.utils.d.ts.map +1 -0
  229. package/dist/cli/backend/src/utils/file-io.utils.js +214 -0
  230. package/dist/cli/backend/src/utils/file-io.utils.js.map +1 -0
  231. package/dist/cli/backend/src/utils/terminal-output.utils.d.ts +54 -0
  232. package/dist/cli/backend/src/utils/terminal-output.utils.d.ts.map +1 -0
  233. package/dist/cli/backend/src/utils/terminal-output.utils.js +97 -0
  234. package/dist/cli/backend/src/utils/terminal-output.utils.js.map +1 -0
  235. package/dist/cli/cli/src/commands/mcp-server.d.ts +38 -0
  236. package/dist/cli/cli/src/commands/mcp-server.d.ts.map +1 -0
  237. package/dist/cli/cli/src/commands/mcp-server.js +49 -0
  238. package/dist/cli/cli/src/commands/mcp-server.js.map +1 -0
  239. package/dist/cli/cli/src/commands/onboard.d.ts +61 -7
  240. package/dist/cli/cli/src/commands/onboard.d.ts.map +1 -1
  241. package/dist/cli/cli/src/commands/onboard.js +192 -19
  242. package/dist/cli/cli/src/commands/onboard.js.map +1 -1
  243. package/dist/cli/cli/src/commands/publish.d.ts +27 -0
  244. package/dist/cli/cli/src/commands/publish.d.ts.map +1 -0
  245. package/dist/cli/cli/src/commands/publish.js +69 -0
  246. package/dist/cli/cli/src/commands/publish.js.map +1 -0
  247. package/dist/cli/cli/src/commands/start.d.ts.map +1 -1
  248. package/dist/cli/cli/src/commands/start.js +12 -2
  249. package/dist/cli/cli/src/commands/start.js.map +1 -1
  250. package/dist/cli/cli/src/index.js +14 -0
  251. package/dist/cli/cli/src/index.js.map +1 -1
  252. package/dist/cli/cli/src/utils/archive-creator.d.ts +82 -0
  253. package/dist/cli/cli/src/utils/archive-creator.d.ts.map +1 -0
  254. package/dist/cli/cli/src/utils/archive-creator.js +105 -0
  255. package/dist/cli/cli/src/utils/archive-creator.js.map +1 -0
  256. package/dist/cli/cli/src/utils/package-validator.d.ts +62 -0
  257. package/dist/cli/cli/src/utils/package-validator.d.ts.map +1 -0
  258. package/dist/cli/cli/src/utils/package-validator.js +122 -0
  259. package/dist/cli/cli/src/utils/package-validator.js.map +1 -0
  260. package/dist/cli/cli/src/utils/process-cleanup.d.ts +15 -0
  261. package/dist/cli/cli/src/utils/process-cleanup.d.ts.map +1 -0
  262. package/dist/cli/cli/src/utils/process-cleanup.js +76 -0
  263. package/dist/cli/cli/src/utils/process-cleanup.js.map +1 -0
  264. package/dist/cli/cli/src/utils/templates.d.ts +71 -0
  265. package/dist/cli/cli/src/utils/templates.d.ts.map +1 -0
  266. package/dist/cli/cli/src/utils/templates.js +91 -0
  267. package/dist/cli/cli/src/utils/templates.js.map +1 -0
  268. package/frontend/dist/assets/{index-523c7fce.js → index-68d1eb5a.js} +71 -71
  269. package/frontend/dist/assets/{index-4c050f52.css → index-c5043a83.css} +1 -1
  270. package/frontend/dist/assets/nunito-cyrillic-400-normal-e44e669f.woff2 +0 -0
  271. package/frontend/dist/assets/nunito-cyrillic-400-normal-ff8e8bdd.woff +0 -0
  272. package/frontend/dist/assets/nunito-cyrillic-500-normal-2159679b.woff +0 -0
  273. package/frontend/dist/assets/nunito-cyrillic-500-normal-61a3b80e.woff2 +0 -0
  274. package/frontend/dist/assets/nunito-cyrillic-600-normal-ac046097.woff +0 -0
  275. package/frontend/dist/assets/nunito-cyrillic-600-normal-e61eb97b.woff2 +0 -0
  276. package/frontend/dist/assets/nunito-cyrillic-700-normal-8fcefcc9.woff2 +0 -0
  277. package/frontend/dist/assets/nunito-cyrillic-700-normal-b9684104.woff +0 -0
  278. package/frontend/dist/assets/nunito-cyrillic-800-normal-40253beb.woff +0 -0
  279. package/frontend/dist/assets/nunito-cyrillic-800-normal-d80292de.woff2 +0 -0
  280. package/frontend/dist/assets/nunito-cyrillic-ext-400-normal-20d73ae7.woff2 +0 -0
  281. package/frontend/dist/assets/nunito-cyrillic-ext-400-normal-d48c37c9.woff +0 -0
  282. package/frontend/dist/assets/nunito-cyrillic-ext-500-normal-16197abd.woff +0 -0
  283. package/frontend/dist/assets/nunito-cyrillic-ext-500-normal-9dcfe9b5.woff2 +0 -0
  284. package/frontend/dist/assets/nunito-cyrillic-ext-600-normal-d53e9851.woff2 +0 -0
  285. package/frontend/dist/assets/nunito-cyrillic-ext-600-normal-e3d0201f.woff +0 -0
  286. package/frontend/dist/assets/nunito-cyrillic-ext-700-normal-5936f6ac.woff2 +0 -0
  287. package/frontend/dist/assets/nunito-cyrillic-ext-700-normal-c8c02775.woff +0 -0
  288. package/frontend/dist/assets/nunito-cyrillic-ext-800-normal-217b8f51.woff +0 -0
  289. package/frontend/dist/assets/nunito-cyrillic-ext-800-normal-796cf7bd.woff2 +0 -0
  290. package/frontend/dist/assets/nunito-latin-400-normal-a5906e15.woff2 +0 -0
  291. package/frontend/dist/assets/nunito-latin-400-normal-b51e7635.woff +0 -0
  292. package/frontend/dist/assets/nunito-latin-500-normal-23ae3083.woff2 +0 -0
  293. package/frontend/dist/assets/nunito-latin-500-normal-be14dbc6.woff +0 -0
  294. package/frontend/dist/assets/nunito-latin-600-normal-06a9c8b3.woff +0 -0
  295. package/frontend/dist/assets/nunito-latin-600-normal-45f437de.woff2 +0 -0
  296. package/frontend/dist/assets/nunito-latin-700-normal-ce9107dc.woff +0 -0
  297. package/frontend/dist/assets/nunito-latin-700-normal-fa89300b.woff2 +0 -0
  298. package/frontend/dist/assets/nunito-latin-800-normal-0ca02785.woff +0 -0
  299. package/frontend/dist/assets/nunito-latin-800-normal-2363d3ed.woff2 +0 -0
  300. package/frontend/dist/assets/nunito-latin-ext-400-normal-67250a41.woff2 +0 -0
  301. package/frontend/dist/assets/nunito-latin-ext-400-normal-d7e2415e.woff +0 -0
  302. package/frontend/dist/assets/nunito-latin-ext-500-normal-06f35d1c.woff +0 -0
  303. package/frontend/dist/assets/nunito-latin-ext-500-normal-343e7adc.woff2 +0 -0
  304. package/frontend/dist/assets/nunito-latin-ext-600-normal-5a8efd17.woff +0 -0
  305. package/frontend/dist/assets/nunito-latin-ext-600-normal-a7ba5f4f.woff2 +0 -0
  306. package/frontend/dist/assets/nunito-latin-ext-700-normal-0a4e4a02.woff2 +0 -0
  307. package/frontend/dist/assets/nunito-latin-ext-700-normal-0c607961.woff +0 -0
  308. package/frontend/dist/assets/nunito-latin-ext-800-normal-39f54b55.woff2 +0 -0
  309. package/frontend/dist/assets/nunito-latin-ext-800-normal-466d0211.woff +0 -0
  310. package/frontend/dist/assets/nunito-vietnamese-400-normal-2a755616.woff2 +0 -0
  311. package/frontend/dist/assets/nunito-vietnamese-400-normal-9c01ea9f.woff +0 -0
  312. package/frontend/dist/assets/nunito-vietnamese-500-normal-452e5e08.woff +0 -0
  313. package/frontend/dist/assets/nunito-vietnamese-500-normal-dc98d965.woff2 +0 -0
  314. package/frontend/dist/assets/nunito-vietnamese-600-normal-2ffbb85f.woff +0 -0
  315. package/frontend/dist/assets/nunito-vietnamese-600-normal-cf95b95d.woff2 +0 -0
  316. package/frontend/dist/assets/nunito-vietnamese-700-normal-0e29c28c.woff2 +0 -0
  317. package/frontend/dist/assets/nunito-vietnamese-700-normal-7793b75e.woff +0 -0
  318. package/frontend/dist/assets/nunito-vietnamese-800-normal-5baf507e.woff +0 -0
  319. package/frontend/dist/assets/nunito-vietnamese-800-normal-fac6740e.woff2 +0 -0
  320. package/frontend/dist/index.html +2 -2
  321. package/package.json +15 -5
@@ -0,0 +1,1406 @@
1
+ import * as fs from 'fs/promises';
2
+ import * as path from 'path';
3
+ import { existsSync, mkdirSync, watch } from 'fs';
4
+ import { parse as parseYAML } from 'yaml';
5
+ import { TeamModel, ProjectModel, TicketModel } from '../../models/index.js';
6
+ import { v4 as uuidv4 } from 'uuid';
7
+ import * as os from 'os';
8
+ import { CREWLY_CONSTANTS, RUNTIME_TYPES } from '../../constants.js';
9
+ import { LoggerService } from './logger.service.js';
10
+ import { TeamsBackupService } from './teams-backup.service.js';
11
+ import { atomicWriteFile, withOperationLock } from '../../utils/file-io.utils.js';
12
+ export class StorageService {
13
+ static instance = null;
14
+ static instanceHome = null;
15
+ crewlyHome;
16
+ /** @deprecated Use teamsDir instead - kept for migration */
17
+ teamsFile;
18
+ /** Directory containing individual team files */
19
+ teamsDir;
20
+ /** Orchestrator status file */
21
+ orchestratorFile;
22
+ projectsFile;
23
+ runtimeFile;
24
+ scheduledMessagesFile;
25
+ deliveryLogsFile;
26
+ recurringChecksFile;
27
+ oneTimeChecksFile;
28
+ logger;
29
+ /** Flag to track if migration has been performed */
30
+ migrationDone = false;
31
+ /** Debounce timer for teams backup to avoid I/O storms */
32
+ backupDebounceTimer = null;
33
+ BACKUP_DEBOUNCE_MS = 2000;
34
+ constructor(crewlyHome) {
35
+ this.logger = LoggerService.getInstance().createComponentLogger('StorageService');
36
+ this.crewlyHome = crewlyHome || path.join(os.homedir(), '.crewly');
37
+ this.teamsFile = path.join(this.crewlyHome, 'teams.json'); // Legacy, kept for migration
38
+ this.teamsDir = path.join(this.crewlyHome, 'teams');
39
+ // Orchestrator now uses directory structure: teams/orchestrator/config.json
40
+ this.orchestratorFile = path.join(this.teamsDir, 'orchestrator', 'config.json');
41
+ this.projectsFile = path.join(this.crewlyHome, 'projects.json');
42
+ this.runtimeFile = path.join(this.crewlyHome, 'runtime.json');
43
+ this.scheduledMessagesFile = path.join(this.crewlyHome, 'scheduled-messages.json');
44
+ this.deliveryLogsFile = path.join(this.crewlyHome, 'message-delivery-logs.json');
45
+ this.recurringChecksFile = path.join(this.crewlyHome, 'recurring-checks.json');
46
+ this.oneTimeChecksFile = path.join(this.crewlyHome, 'one-time-checks.json');
47
+ this.ensureDirectories();
48
+ this.logger.info('StorageService initialized', { crewlyHome: this.crewlyHome });
49
+ }
50
+ /**
51
+ * Get singleton instance of StorageService to prevent multiple instances
52
+ * from interfering with each other's file operations
53
+ */
54
+ static getInstance(crewlyHome) {
55
+ const homeDir = crewlyHome || path.join(os.homedir(), '.crewly');
56
+ // Return existing instance if it matches the same home directory
57
+ if (StorageService.instance && StorageService.instanceHome === homeDir) {
58
+ return StorageService.instance;
59
+ }
60
+ // Create new instance if none exists or home directory changed
61
+ StorageService.instance = new StorageService(homeDir);
62
+ StorageService.instanceHome = homeDir;
63
+ return StorageService.instance;
64
+ }
65
+ /**
66
+ * Clear singleton instance (useful for testing)
67
+ */
68
+ static clearInstance() {
69
+ StorageService.instance = null;
70
+ StorageService.instanceHome = null;
71
+ }
72
+ /**
73
+ * Ensures the crewly home and teams directories exist, creating them if necessary
74
+ */
75
+ ensureDirectories() {
76
+ if (!existsSync(this.crewlyHome)) {
77
+ mkdirSync(this.crewlyHome, { recursive: true });
78
+ }
79
+ if (!existsSync(this.teamsDir)) {
80
+ mkdirSync(this.teamsDir, { recursive: true });
81
+ }
82
+ // Ensure orchestrator directory exists
83
+ const orchestratorDir = path.join(this.teamsDir, 'orchestrator');
84
+ if (!existsSync(orchestratorDir)) {
85
+ mkdirSync(orchestratorDir, { recursive: true });
86
+ }
87
+ }
88
+ /**
89
+ * Get the directory path for a team.
90
+ *
91
+ * @param teamId - The team ID
92
+ * @returns Path to the team directory
93
+ */
94
+ getTeamDir(teamId) {
95
+ return path.join(this.teamsDir, teamId);
96
+ }
97
+ /**
98
+ * Get the prompts directory path for a team.
99
+ *
100
+ * @param teamId - The team ID
101
+ * @returns Path to the team's prompts directory
102
+ */
103
+ getTeamPromptsDir(teamId) {
104
+ return path.join(this.teamsDir, teamId, 'prompts');
105
+ }
106
+ /**
107
+ * Get the prompt file path for a team member.
108
+ *
109
+ * @param teamId - The team ID
110
+ * @param memberId - The member ID
111
+ * @returns Path to the member's prompt file
112
+ */
113
+ getMemberPromptPath(teamId, memberId) {
114
+ return path.join(this.getTeamPromptsDir(teamId), `${memberId}.md`);
115
+ }
116
+ /**
117
+ * Get the orchestrator prompt file path.
118
+ *
119
+ * @returns Path to the orchestrator's prompt file
120
+ */
121
+ getOrchestratorPromptPath() {
122
+ return path.join(this.teamsDir, 'orchestrator', 'prompt.md');
123
+ }
124
+ /**
125
+ * Migrate from old storage formats to new directory structure.
126
+ * Handles: teams.json -> teams/{team-id}/config.json
127
+ * teams/{team-id}.json -> teams/{team-id}/config.json
128
+ * teams/orchestrator.json -> teams/orchestrator/config.json
129
+ */
130
+ async migrateFromLegacyTeamsFile() {
131
+ if (this.migrationDone) {
132
+ return;
133
+ }
134
+ this.migrationDone = true;
135
+ // Migration 1: From old teams.json (single file with all teams)
136
+ if (existsSync(this.teamsFile)) {
137
+ try {
138
+ const content = await fs.readFile(this.teamsFile, 'utf-8');
139
+ const data = JSON.parse(content);
140
+ // Migrate orchestrator to directory structure
141
+ if (data.orchestrator) {
142
+ const orchestratorDir = path.join(this.teamsDir, 'orchestrator');
143
+ if (!existsSync(orchestratorDir)) {
144
+ mkdirSync(orchestratorDir, { recursive: true });
145
+ }
146
+ if (!existsSync(this.orchestratorFile)) {
147
+ await atomicWriteFile(this.orchestratorFile, JSON.stringify(data.orchestrator, null, 2));
148
+ this.logger.info('Migrated orchestrator to directory structure');
149
+ }
150
+ }
151
+ // Migrate teams to directory structure
152
+ const teams = data.teams || (Array.isArray(data) ? data : []);
153
+ for (const team of teams) {
154
+ if (team.id) {
155
+ await this.migrateTeamToDirectory(team);
156
+ }
157
+ }
158
+ // Backup legacy file
159
+ const backupPath = `${this.teamsFile}.migrated.${Date.now()}`;
160
+ await fs.rename(this.teamsFile, backupPath);
161
+ this.logger.info('Legacy teams.json backed up after migration', { backupPath });
162
+ }
163
+ catch (error) {
164
+ this.logger.warn('Error during legacy teams.json migration (non-fatal)', {
165
+ error: error instanceof Error ? error.message : String(error),
166
+ });
167
+ }
168
+ }
169
+ // Migration 2: From flat team files (teams/{team-id}.json) to directory structure
170
+ await this.migrateFlatTeamFiles();
171
+ // Migration 3: From flat orchestrator file (teams/orchestrator.json) to directory
172
+ const flatOrchestratorFile = path.join(this.teamsDir, 'orchestrator.json');
173
+ if (existsSync(flatOrchestratorFile) && !existsSync(this.orchestratorFile)) {
174
+ try {
175
+ const content = await fs.readFile(flatOrchestratorFile, 'utf-8');
176
+ const orchestrator = JSON.parse(content);
177
+ const orchestratorDir = path.join(this.teamsDir, 'orchestrator');
178
+ if (!existsSync(orchestratorDir)) {
179
+ mkdirSync(orchestratorDir, { recursive: true });
180
+ }
181
+ await atomicWriteFile(this.orchestratorFile, JSON.stringify(orchestrator, null, 2));
182
+ await fs.rename(flatOrchestratorFile, `${flatOrchestratorFile}.migrated.${Date.now()}`);
183
+ this.logger.info('Migrated flat orchestrator.json to directory structure');
184
+ }
185
+ catch (error) {
186
+ this.logger.warn('Error migrating flat orchestrator.json (non-fatal)', {
187
+ error: error instanceof Error ? error.message : String(error),
188
+ });
189
+ }
190
+ }
191
+ }
192
+ /**
193
+ * Migrate flat team files (teams/{team-id}.json) to directory structure (teams/{team-id}/config.json).
194
+ */
195
+ async migrateFlatTeamFiles() {
196
+ try {
197
+ const files = await fs.readdir(this.teamsDir);
198
+ for (const file of files) {
199
+ // Skip directories and non-json files
200
+ if (!file.endsWith('.json') || file === 'orchestrator.json')
201
+ continue;
202
+ const filePath = path.join(this.teamsDir, file);
203
+ const stat = await fs.stat(filePath);
204
+ if (stat.isDirectory())
205
+ continue;
206
+ try {
207
+ const content = await fs.readFile(filePath, 'utf-8');
208
+ const team = JSON.parse(content);
209
+ if (team.id) {
210
+ await this.migrateTeamToDirectory(team);
211
+ // Backup flat file
212
+ await fs.rename(filePath, `${filePath}.migrated.${Date.now()}`);
213
+ this.logger.info('Migrated flat team file to directory', { teamId: team.id });
214
+ }
215
+ }
216
+ catch (error) {
217
+ this.logger.warn('Error migrating flat team file (non-fatal)', {
218
+ file,
219
+ error: error instanceof Error ? error.message : String(error),
220
+ });
221
+ }
222
+ }
223
+ }
224
+ catch (error) {
225
+ this.logger.warn('Error scanning for flat team files (non-fatal)', {
226
+ error: error instanceof Error ? error.message : String(error),
227
+ });
228
+ }
229
+ }
230
+ /**
231
+ * Migrate a team to the new directory structure.
232
+ * Creates teams/{team-id}/config.json and teams/{team-id}/prompts/{member-id}.md
233
+ */
234
+ async migrateTeamToDirectory(team) {
235
+ const teamDir = this.getTeamDir(team.id);
236
+ const teamConfigFile = path.join(teamDir, 'config.json');
237
+ const promptsDir = this.getTeamPromptsDir(team.id);
238
+ // Create team directory structure
239
+ if (!existsSync(teamDir)) {
240
+ mkdirSync(teamDir, { recursive: true });
241
+ }
242
+ if (!existsSync(promptsDir)) {
243
+ mkdirSync(promptsDir, { recursive: true });
244
+ }
245
+ // Save team config (without inline prompts if they exist)
246
+ if (!existsSync(teamConfigFile)) {
247
+ await atomicWriteFile(teamConfigFile, JSON.stringify(team, null, 2));
248
+ }
249
+ // Extract member prompts to individual files
250
+ for (const member of team.members || []) {
251
+ if (member.systemPrompt) {
252
+ const promptPath = this.getMemberPromptPath(team.id, member.id);
253
+ if (!existsSync(promptPath)) {
254
+ await atomicWriteFile(promptPath, member.systemPrompt);
255
+ this.logger.debug('Extracted member prompt to file', {
256
+ teamId: team.id,
257
+ memberId: member.id,
258
+ promptPath,
259
+ });
260
+ }
261
+ }
262
+ }
263
+ this.logger.info('Migrated team to directory structure', {
264
+ teamId: team.id,
265
+ teamName: team.name,
266
+ memberCount: team.members?.length || 0,
267
+ });
268
+ }
269
+ /**
270
+ * Creates default orchestrator configuration object
271
+ * @returns Default orchestrator settings with inactive status
272
+ */
273
+ createDefaultOrchestrator() {
274
+ return {
275
+ sessionName: CREWLY_CONSTANTS.SESSIONS.ORCHESTRATOR_NAME,
276
+ agentStatus: CREWLY_CONSTANTS.AGENT_STATUSES.INACTIVE,
277
+ workingStatus: CREWLY_CONSTANTS.WORKING_STATUSES.IDLE,
278
+ runtimeType: RUNTIME_TYPES.CLAUDE_CODE,
279
+ createdAt: new Date().toISOString(),
280
+ updatedAt: new Date().toISOString()
281
+ };
282
+ }
283
+ /**
284
+ * Ensures a storage file exists and is valid JSON, creating or recovering it if needed
285
+ * @param filePath - Path to the storage file
286
+ * @param defaultContent - Default content to use if file needs to be created/recovered
287
+ */
288
+ async ensureFile(filePath, defaultContent = []) {
289
+ const fileName = path.basename(filePath);
290
+ if (!existsSync(filePath)) {
291
+ this.logger.info('Creating new storage file', { file: fileName });
292
+ await atomicWriteFile(filePath, JSON.stringify(defaultContent, null, 2));
293
+ }
294
+ else {
295
+ // File exists - validate it's not corrupted
296
+ try {
297
+ const content = await fs.readFile(filePath, 'utf-8');
298
+ const parsed = JSON.parse(content);
299
+ // If file is empty or has invalid content (null/undefined), reinitialize with defaults
300
+ if (!content.trim() || parsed === null || parsed === undefined) {
301
+ this.logger.warn('Storage file exists but appears empty/corrupted, creating backup and initializing with defaults', {
302
+ file: fileName,
303
+ contentLength: content?.length || 0,
304
+ });
305
+ // Backup even empty files for debugging
306
+ const backupPath = `${filePath}.empty.${Date.now()}`;
307
+ try {
308
+ await fs.copyFile(filePath, backupPath);
309
+ this.logger.info('Backed up empty file', { backupPath });
310
+ }
311
+ catch {
312
+ // Ignore backup errors
313
+ }
314
+ await atomicWriteFile(filePath, JSON.stringify(defaultContent, null, 2));
315
+ }
316
+ }
317
+ catch (error) {
318
+ // File exists but can't be parsed - back it up and create new one
319
+ this.logger.warn('Storage file exists but cannot be parsed, backing up and reinitializing', {
320
+ file: fileName,
321
+ error: error instanceof Error ? error.message : String(error),
322
+ });
323
+ const backupPath = `${filePath}.backup.${Date.now()}`;
324
+ try {
325
+ await fs.copyFile(filePath, backupPath);
326
+ this.logger.info('Backed up corrupted file', { backupPath });
327
+ }
328
+ catch (backupError) {
329
+ this.logger.error('Failed to backup corrupted file', {
330
+ error: backupError instanceof Error ? backupError.message : String(backupError),
331
+ });
332
+ }
333
+ await atomicWriteFile(filePath, JSON.stringify(defaultContent, null, 2));
334
+ }
335
+ }
336
+ }
337
+ // Team management
338
+ /**
339
+ * Get all teams from team directories.
340
+ * Each team is stored as teams/{teamId}/config.json for isolation.
341
+ *
342
+ * @returns Array of all teams
343
+ */
344
+ async getTeams() {
345
+ try {
346
+ // Migrate from legacy format if needed
347
+ await this.migrateFromLegacyTeamsFile();
348
+ // Ensure teams directory exists
349
+ if (!existsSync(this.teamsDir)) {
350
+ mkdirSync(this.teamsDir, { recursive: true });
351
+ return [];
352
+ }
353
+ // Read all team directories
354
+ const entries = await fs.readdir(this.teamsDir, { withFileTypes: true });
355
+ const teamDirs = entries.filter(e => e.isDirectory() && e.name !== 'orchestrator');
356
+ const teams = [];
357
+ for (const dir of teamDirs) {
358
+ try {
359
+ const configPath = path.join(this.teamsDir, dir.name, 'config.json');
360
+ if (!existsSync(configPath)) {
361
+ continue; // Skip directories without config.json
362
+ }
363
+ const content = await fs.readFile(configPath, 'utf-8');
364
+ const team = JSON.parse(content);
365
+ const processedTeam = TeamModel.fromJSON(team).toJSON();
366
+ teams.push(processedTeam);
367
+ }
368
+ catch (fileError) {
369
+ this.logger.warn('Error reading team config, skipping', {
370
+ teamDir: dir.name,
371
+ error: fileError instanceof Error ? fileError.message : String(fileError),
372
+ });
373
+ }
374
+ }
375
+ this.logger.debug('Retrieved teams from storage', { count: teams.length });
376
+ return teams;
377
+ }
378
+ catch (error) {
379
+ this.logger.error('Error reading teams', {
380
+ error: error instanceof Error ? error.message : String(error),
381
+ });
382
+ return [];
383
+ }
384
+ }
385
+ /**
386
+ * Save a team to its directory structure.
387
+ * Each team is stored as teams/{teamId}/config.json for isolation and resilience.
388
+ * Member prompts are saved to teams/{teamId}/prompts/{memberId}.md
389
+ *
390
+ * @param team - The team to save
391
+ */
392
+ async saveTeam(team) {
393
+ const teamDir = this.getTeamDir(team.id);
394
+ const teamFile = path.join(teamDir, 'config.json');
395
+ const promptsDir = this.getTeamPromptsDir(team.id);
396
+ // Use operation lock on the team directory
397
+ return withOperationLock(teamDir, async () => {
398
+ try {
399
+ // Ensure team directory structure exists
400
+ if (!existsSync(teamDir)) {
401
+ mkdirSync(teamDir, { recursive: true });
402
+ }
403
+ if (!existsSync(promptsDir)) {
404
+ mkdirSync(promptsDir, { recursive: true });
405
+ }
406
+ // Check if this is an update or create
407
+ const isUpdate = existsSync(teamFile);
408
+ // Write team config to file
409
+ await atomicWriteFile(teamFile, JSON.stringify(team, null, 2));
410
+ // Save member prompts to individual files
411
+ for (const member of team.members || []) {
412
+ if (member.systemPrompt) {
413
+ const promptPath = this.getMemberPromptPath(team.id, member.id);
414
+ await atomicWriteFile(promptPath, member.systemPrompt);
415
+ }
416
+ }
417
+ this.logger.info('Team saved successfully', {
418
+ teamId: team.id,
419
+ teamName: team.name,
420
+ action: isUpdate ? 'updated' : 'created',
421
+ memberCount: team.members?.length || 0,
422
+ filePath: teamFile,
423
+ });
424
+ // Update teams backup (fire-and-forget, non-blocking)
425
+ this.updateTeamsBackup();
426
+ }
427
+ catch (error) {
428
+ this.logger.error('Error saving team', {
429
+ teamId: team.id,
430
+ teamName: team.name,
431
+ error: error instanceof Error ? error.message : String(error),
432
+ filePath: teamFile,
433
+ });
434
+ throw error;
435
+ }
436
+ });
437
+ }
438
+ /**
439
+ * Save a member's prompt file.
440
+ *
441
+ * @param teamId - The team ID
442
+ * @param memberId - The member ID
443
+ * @param prompt - The prompt content
444
+ */
445
+ async saveMemberPrompt(teamId, memberId, prompt) {
446
+ const promptsDir = this.getTeamPromptsDir(teamId);
447
+ const promptPath = this.getMemberPromptPath(teamId, memberId);
448
+ if (!existsSync(promptsDir)) {
449
+ mkdirSync(promptsDir, { recursive: true });
450
+ }
451
+ await atomicWriteFile(promptPath, prompt);
452
+ this.logger.debug('Saved member prompt', { teamId, memberId, promptPath });
453
+ }
454
+ /**
455
+ * Get a member's prompt from file.
456
+ *
457
+ * @param teamId - The team ID
458
+ * @param memberId - The member ID
459
+ * @returns The prompt content or null if not found
460
+ */
461
+ async getMemberPrompt(teamId, memberId) {
462
+ const promptPath = this.getMemberPromptPath(teamId, memberId);
463
+ if (!existsSync(promptPath)) {
464
+ return null;
465
+ }
466
+ try {
467
+ return await fs.readFile(promptPath, 'utf-8');
468
+ }
469
+ catch (error) {
470
+ this.logger.warn('Error reading member prompt', {
471
+ teamId,
472
+ memberId,
473
+ error: error instanceof Error ? error.message : String(error),
474
+ });
475
+ return null;
476
+ }
477
+ }
478
+ /**
479
+ * Find a team member by their session name.
480
+ *
481
+ * @param sessionName - The session name to search for
482
+ * @returns Object with team and member, or null if not found
483
+ */
484
+ async findMemberBySessionName(sessionName) {
485
+ try {
486
+ const teams = await this.getTeams();
487
+ for (const team of teams) {
488
+ for (const member of team.members || []) {
489
+ if (member.sessionName === sessionName) {
490
+ return { team, member };
491
+ }
492
+ }
493
+ }
494
+ return null;
495
+ }
496
+ catch (error) {
497
+ this.logger.error('Error finding member by session name', {
498
+ sessionName,
499
+ error: error instanceof Error ? error.message : String(error),
500
+ });
501
+ return null;
502
+ }
503
+ }
504
+ /**
505
+ * Save the orchestrator's prompt file.
506
+ *
507
+ * @param prompt - The prompt content
508
+ */
509
+ async saveOrchestratorPrompt(prompt) {
510
+ const orchestratorDir = path.join(this.teamsDir, 'orchestrator');
511
+ if (!existsSync(orchestratorDir)) {
512
+ mkdirSync(orchestratorDir, { recursive: true });
513
+ }
514
+ const promptPath = this.getOrchestratorPromptPath();
515
+ await atomicWriteFile(promptPath, prompt);
516
+ this.logger.debug('Saved orchestrator prompt', { promptPath });
517
+ }
518
+ /**
519
+ * Get the orchestrator's prompt from file.
520
+ *
521
+ * @returns The prompt content or null if not found
522
+ */
523
+ async getOrchestratorPrompt() {
524
+ const promptPath = this.getOrchestratorPromptPath();
525
+ if (!existsSync(promptPath)) {
526
+ return null;
527
+ }
528
+ try {
529
+ return await fs.readFile(promptPath, 'utf-8');
530
+ }
531
+ catch (error) {
532
+ this.logger.warn('Error reading orchestrator prompt', {
533
+ error: error instanceof Error ? error.message : String(error),
534
+ });
535
+ return null;
536
+ }
537
+ }
538
+ /**
539
+ * Delete a team by removing its directory.
540
+ *
541
+ * @param id - The team ID to delete
542
+ */
543
+ async deleteTeam(id) {
544
+ const teamDir = this.getTeamDir(id);
545
+ try {
546
+ if (existsSync(teamDir)) {
547
+ // Remove team directory recursively
548
+ await fs.rm(teamDir, { recursive: true, force: true });
549
+ this.logger.info('Team deleted successfully', { teamId: id, teamDir });
550
+ // Update teams backup (fire-and-forget, non-blocking)
551
+ this.updateTeamsBackup();
552
+ }
553
+ else {
554
+ this.logger.warn('Team directory not found for deletion', { teamId: id, teamDir });
555
+ }
556
+ }
557
+ catch (error) {
558
+ this.logger.error('Error deleting team', {
559
+ teamId: id,
560
+ error: error instanceof Error ? error.message : String(error),
561
+ });
562
+ throw error;
563
+ }
564
+ }
565
+ // Project management
566
+ async getProjects() {
567
+ try {
568
+ await this.ensureFile(this.projectsFile);
569
+ const content = await fs.readFile(this.projectsFile, 'utf-8');
570
+ const projects = JSON.parse(content);
571
+ this.logger.debug('Retrieved projects from storage', { count: projects.length });
572
+ return projects.map(project => ProjectModel.fromJSON(project).toJSON());
573
+ }
574
+ catch (error) {
575
+ this.logger.error('Error reading projects', {
576
+ error: error instanceof Error ? error.message : String(error),
577
+ });
578
+ return [];
579
+ }
580
+ }
581
+ async addProject(projectPath) {
582
+ try {
583
+ // Resolve to absolute path to ensure .crewly is created in the correct location
584
+ // If path is relative, resolve it relative to the parent directory of the current working directory
585
+ let resolvedProjectPath;
586
+ if (path.isAbsolute(projectPath)) {
587
+ resolvedProjectPath = projectPath;
588
+ }
589
+ else {
590
+ // Resolve relative to parent directory (where sibling projects should be)
591
+ const parentDir = path.dirname(process.cwd());
592
+ resolvedProjectPath = path.resolve(parentDir, projectPath);
593
+ }
594
+ const projectName = path.basename(resolvedProjectPath);
595
+ const projectId = uuidv4();
596
+ const project = new ProjectModel({
597
+ id: projectId,
598
+ name: projectName,
599
+ path: resolvedProjectPath,
600
+ teams: {},
601
+ status: 'stopped',
602
+ });
603
+ // Ensure project directory and .crewly structure exists with template files
604
+ mkdirSync(resolvedProjectPath, { recursive: true });
605
+ const crewlyDir = path.join(resolvedProjectPath, '.crewly');
606
+ if (!existsSync(crewlyDir)) {
607
+ mkdirSync(crewlyDir, { recursive: true });
608
+ mkdirSync(path.join(crewlyDir, 'tasks'), { recursive: true });
609
+ mkdirSync(path.join(crewlyDir, 'specs'), { recursive: true });
610
+ mkdirSync(path.join(crewlyDir, 'memory'), { recursive: true });
611
+ mkdirSync(path.join(crewlyDir, 'prompts'), { recursive: true });
612
+ // Create template files
613
+ await this.createProjectTemplateFiles(crewlyDir, projectName);
614
+ }
615
+ await this.saveProject(project.toJSON());
616
+ return project.toJSON();
617
+ }
618
+ catch (error) {
619
+ this.logger.error('Error adding project', { error: error instanceof Error ? error.message : String(error) });
620
+ throw error;
621
+ }
622
+ }
623
+ async saveProject(project) {
624
+ try {
625
+ const projects = await this.getProjects();
626
+ const existingIndex = projects.findIndex(p => p.id === project.id);
627
+ const isUpdate = existingIndex >= 0;
628
+ if (isUpdate) {
629
+ projects[existingIndex] = project;
630
+ }
631
+ else {
632
+ projects.push(project);
633
+ }
634
+ const newContent = JSON.stringify(projects, null, 2);
635
+ await atomicWriteFile(this.projectsFile, newContent);
636
+ this.logger.info('Project saved successfully', {
637
+ projectId: project.id,
638
+ projectName: project.name,
639
+ action: isUpdate ? 'updated' : 'created',
640
+ totalProjects: projects.length,
641
+ filePath: this.projectsFile,
642
+ });
643
+ }
644
+ catch (error) {
645
+ this.logger.error('Error saving project', {
646
+ projectId: project.id,
647
+ projectName: project.name,
648
+ error: error instanceof Error ? error.message : String(error),
649
+ filePath: this.projectsFile,
650
+ });
651
+ throw error;
652
+ }
653
+ }
654
+ async deleteProject(id) {
655
+ try {
656
+ const projects = await this.getProjects();
657
+ const filteredProjects = projects.filter(p => p.id !== id);
658
+ await atomicWriteFile(this.projectsFile, JSON.stringify(filteredProjects, null, 2));
659
+ }
660
+ catch (error) {
661
+ this.logger.error('Error deleting project', { error: error instanceof Error ? error.message : String(error) });
662
+ throw error;
663
+ }
664
+ }
665
+ // Ticket management
666
+ async getTickets(projectPath, filter) {
667
+ try {
668
+ const resolvedProjectPath = path.resolve(projectPath);
669
+ const ticketsDir = path.join(resolvedProjectPath, '.crewly', 'tasks');
670
+ if (!existsSync(ticketsDir)) {
671
+ return [];
672
+ }
673
+ const files = await fs.readdir(ticketsDir);
674
+ const yamlFiles = files.filter(file => file.endsWith('.yaml') || file.endsWith('.yml'));
675
+ const tickets = [];
676
+ for (const file of yamlFiles) {
677
+ try {
678
+ const filePath = path.join(ticketsDir, file);
679
+ const content = await fs.readFile(filePath, 'utf-8');
680
+ const ticket = this.parseTicketYAML(content);
681
+ tickets.push(ticket);
682
+ }
683
+ catch (error) {
684
+ this.logger.error('Error parsing ticket file', { file, error: error instanceof Error ? error.message : String(error) });
685
+ }
686
+ }
687
+ // Apply filters
688
+ let filteredTickets = tickets;
689
+ if (filter) {
690
+ if (filter.status) {
691
+ filteredTickets = filteredTickets.filter(t => t.status === filter.status);
692
+ }
693
+ if (filter.assignedTo) {
694
+ filteredTickets = filteredTickets.filter(t => t.assignedTo === filter.assignedTo);
695
+ }
696
+ if (filter.projectId) {
697
+ filteredTickets = filteredTickets.filter(t => t.projectId === filter.projectId);
698
+ }
699
+ if (filter.priority) {
700
+ filteredTickets = filteredTickets.filter(t => t.priority === filter.priority);
701
+ }
702
+ }
703
+ return filteredTickets.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
704
+ }
705
+ catch (error) {
706
+ this.logger.error('Error reading tickets', { error: error instanceof Error ? error.message : String(error) });
707
+ return [];
708
+ }
709
+ }
710
+ async saveTicket(projectPath, ticket) {
711
+ try {
712
+ const resolvedProjectPath = path.resolve(projectPath);
713
+ const ticketsDir = path.join(resolvedProjectPath, '.crewly', 'tasks');
714
+ if (!existsSync(ticketsDir)) {
715
+ mkdirSync(ticketsDir, { recursive: true });
716
+ }
717
+ const ticketModel = TicketModel.fromJSON(ticket);
718
+ const filename = `${ticket.id}.yaml`;
719
+ const filePath = path.join(ticketsDir, filename);
720
+ await fs.writeFile(filePath, ticketModel.toYAML());
721
+ }
722
+ catch (error) {
723
+ this.logger.error('Error saving ticket', { error: error instanceof Error ? error.message : String(error) });
724
+ throw error;
725
+ }
726
+ }
727
+ async deleteTicket(projectPath, ticketId) {
728
+ try {
729
+ const resolvedProjectPath = path.resolve(projectPath);
730
+ const ticketsDir = path.join(resolvedProjectPath, '.crewly', 'tasks');
731
+ const filename = `${ticketId}.yaml`;
732
+ const filePath = path.join(ticketsDir, filename);
733
+ if (existsSync(filePath)) {
734
+ await fs.unlink(filePath);
735
+ }
736
+ }
737
+ catch (error) {
738
+ this.logger.error('Error deleting ticket', { error: error instanceof Error ? error.message : String(error) });
739
+ throw error;
740
+ }
741
+ }
742
+ parseTicketYAML(content) {
743
+ const lines = content.split('\n');
744
+ let frontmatterEnd = -1;
745
+ let frontmatterStart = -1;
746
+ // Find YAML frontmatter
747
+ for (let i = 0; i < lines.length; i++) {
748
+ if (lines[i].trim() === '---') {
749
+ if (frontmatterStart === -1) {
750
+ frontmatterStart = i;
751
+ }
752
+ else {
753
+ frontmatterEnd = i;
754
+ break;
755
+ }
756
+ }
757
+ }
758
+ let frontmatter = {};
759
+ let description = '';
760
+ if (frontmatterStart !== -1 && frontmatterEnd !== -1) {
761
+ const yamlContent = lines.slice(frontmatterStart + 1, frontmatterEnd).join('\n');
762
+ frontmatter = parseYAML(yamlContent) || {};
763
+ description = lines.slice(frontmatterEnd + 1).join('\n').trim();
764
+ }
765
+ else {
766
+ description = content.trim();
767
+ }
768
+ return {
769
+ id: frontmatter.id || uuidv4(),
770
+ title: frontmatter.title || 'Untitled',
771
+ description: description,
772
+ status: frontmatter.status || 'open',
773
+ assignedTo: frontmatter.assignedTo,
774
+ priority: frontmatter.priority || 'medium',
775
+ labels: frontmatter.labels || [],
776
+ projectId: frontmatter.projectId || '',
777
+ createdAt: frontmatter.createdAt || new Date().toISOString(),
778
+ updatedAt: frontmatter.updatedAt || new Date().toISOString(),
779
+ };
780
+ }
781
+ // File watching
782
+ watchProject(projectPath) {
783
+ const resolvedProjectPath = path.resolve(projectPath);
784
+ const crewlyDir = path.join(resolvedProjectPath, '.crewly');
785
+ return watch(crewlyDir, { recursive: true }, (eventType, filename) => {
786
+ if (filename) {
787
+ this.logger.info('File change detected', { eventType, filename: filename || 'unknown', projectPath: resolvedProjectPath });
788
+ // Emit events that can be handled by WebSocket gateway
789
+ }
790
+ });
791
+ }
792
+ // Runtime state management
793
+ async getRuntimeState() {
794
+ try {
795
+ await this.ensureFile(this.runtimeFile, {});
796
+ const content = await fs.readFile(this.runtimeFile, 'utf-8');
797
+ return JSON.parse(content);
798
+ }
799
+ catch (error) {
800
+ this.logger.error('Error reading runtime state', { error: error instanceof Error ? error.message : String(error) });
801
+ return {};
802
+ }
803
+ }
804
+ async saveRuntimeState(state) {
805
+ try {
806
+ await atomicWriteFile(this.runtimeFile, JSON.stringify(state, null, 2));
807
+ }
808
+ catch (error) {
809
+ this.logger.error('Error saving runtime state', { error: error instanceof Error ? error.message : String(error) });
810
+ throw error;
811
+ }
812
+ }
813
+ /**
814
+ * Create template files for a new project
815
+ */
816
+ async createProjectTemplateFiles(crewlyDir, projectName) {
817
+ try {
818
+ // Project specification template
819
+ const projectSpecPath = path.join(crewlyDir, 'specs', 'project.md');
820
+ const projectSpecTemplate = `# ${projectName} - Project Specification
821
+
822
+ ## Overview
823
+ Brief description of ${projectName} and its goals.
824
+
825
+ ## Requirements
826
+ - List key functional requirements
827
+ - Non-functional requirements
828
+ - Technical constraints
829
+
830
+ ## Architecture
831
+ High-level system architecture and technology stack.
832
+
833
+ ## Implementation Plan
834
+ ### Phase 1: Foundation
835
+ - Core functionality setup
836
+ - Basic project structure
837
+ - Initial testing framework
838
+
839
+ ### Phase 2: Features
840
+ - Main feature implementation
841
+ - Integration and testing
842
+ - Performance optimization
843
+
844
+ ### Phase 3: Polish
845
+ - UI/UX improvements
846
+ - Documentation completion
847
+ - Deployment preparation
848
+
849
+ ## Acceptance Criteria
850
+ - [ ] All requirements implemented
851
+ - [ ] Tests passing (>90% coverage)
852
+ - [ ] Documentation complete
853
+ - [ ] Performance targets met
854
+ - [ ] Security review completed
855
+ `;
856
+ await fs.writeFile(projectSpecPath, projectSpecTemplate, 'utf8');
857
+ // README for the .crewly directory
858
+ const readmePath = path.join(crewlyDir, 'README.md');
859
+ const readmeTemplate = `# Crewly Project Directory
860
+
861
+ This directory contains Crewly-specific files for **${projectName}** project orchestration.
862
+
863
+ ## Structure
864
+
865
+ - **specs/**: Project specifications and requirements
866
+ - **tasks/**: Task items in YAML + Markdown format
867
+ - **memory/**: Agent memory and context files
868
+ - **prompts/**: Custom system prompts for team members
869
+
870
+ ## Usage
871
+
872
+ Crewly agents automatically read from these directories to understand:
873
+ - Project requirements and specifications
874
+ - Current tasks and their status
875
+ - Historical context and decisions
876
+ - Role-specific instructions
877
+
878
+ All files in this directory are monitored by Crewly for real-time updates.
879
+
880
+ ## Getting Started
881
+
882
+ 1. Update \`specs/project.md\` with your project requirements
883
+ 2. Create task items in \`tasks/\` directory for specific tasks
884
+ 3. Customize team member prompts in \`prompts/\` as needed
885
+ 4. Let Crewly orchestrate your development workflow!
886
+ `;
887
+ await fs.writeFile(readmePath, readmeTemplate, 'utf8');
888
+ // Sample ticket template
889
+ const sampleTicketPath = path.join(crewlyDir, 'tasks', 'sample-setup-task.yaml');
890
+ const ticketTemplate = `---
891
+ id: sample-setup-task
892
+ title: Project Setup and Configuration
893
+ status: todo
894
+ priority: high
895
+ assignedTo: ""
896
+ estimatedHours: 4
897
+ createdAt: ${new Date().toISOString()}
898
+ updatedAt: ${new Date().toISOString()}
899
+ tags:
900
+ - setup
901
+ - configuration
902
+ - infrastructure
903
+ ---
904
+
905
+ # Project Setup and Configuration
906
+
907
+ ## Description
908
+ Set up the basic project infrastructure and configuration for ${projectName}.
909
+
910
+ ## Acceptance Criteria
911
+ - [ ] Project structure created
912
+ - [ ] Build system configured
913
+ - [ ] Testing framework set up
914
+ - [ ] CI/CD pipeline configured
915
+ - [ ] Documentation structure established
916
+ - [ ] Development environment documented
917
+
918
+ ## Implementation Notes
919
+ This is a foundational task that should be completed first before other development work begins.
920
+
921
+ ## Test Plan
922
+ - Verify build process works correctly
923
+ - Confirm tests can be run successfully
924
+ - Check all documentation is accessible
925
+ - Validate development environment setup instructions
926
+ `;
927
+ await fs.writeFile(sampleTicketPath, ticketTemplate, 'utf8');
928
+ this.logger.info('Created Crewly template files for project', { projectName });
929
+ }
930
+ catch (error) {
931
+ this.logger.error('Error creating project template files', { error: error instanceof Error ? error.message : String(error) });
932
+ // Don't throw - project can still work without template files
933
+ }
934
+ }
935
+ // Scheduled Messages management
936
+ async getScheduledMessages() {
937
+ try {
938
+ await this.ensureFile(this.scheduledMessagesFile, []);
939
+ const content = await fs.readFile(this.scheduledMessagesFile, 'utf-8');
940
+ // Handle empty content or malformed JSON
941
+ if (!content.trim()) {
942
+ await atomicWriteFile(this.scheduledMessagesFile, JSON.stringify([], null, 2));
943
+ return [];
944
+ }
945
+ try {
946
+ return JSON.parse(content);
947
+ }
948
+ catch (parseError) {
949
+ this.logger.error('Error parsing scheduled messages JSON, resetting file', { error: parseError instanceof Error ? parseError.message : String(parseError) });
950
+ // Reset the file with empty array if JSON is corrupted
951
+ await atomicWriteFile(this.scheduledMessagesFile, JSON.stringify([], null, 2));
952
+ return [];
953
+ }
954
+ }
955
+ catch (error) {
956
+ this.logger.error('Error reading scheduled messages', { error: error instanceof Error ? error.message : String(error) });
957
+ return [];
958
+ }
959
+ }
960
+ async saveScheduledMessage(scheduledMessage) {
961
+ try {
962
+ const messages = await this.getScheduledMessages();
963
+ const existingIndex = messages.findIndex(m => m.id === scheduledMessage.id);
964
+ if (existingIndex >= 0) {
965
+ messages[existingIndex] = scheduledMessage;
966
+ }
967
+ else {
968
+ messages.push(scheduledMessage);
969
+ }
970
+ await atomicWriteFile(this.scheduledMessagesFile, JSON.stringify(messages, null, 2));
971
+ }
972
+ catch (error) {
973
+ this.logger.error('Error saving scheduled message', { error: error instanceof Error ? error.message : String(error) });
974
+ throw error;
975
+ }
976
+ }
977
+ async getScheduledMessage(id) {
978
+ try {
979
+ const messages = await this.getScheduledMessages();
980
+ return messages.find(m => m.id === id);
981
+ }
982
+ catch (error) {
983
+ this.logger.error('Error getting scheduled message', { error: error instanceof Error ? error.message : String(error) });
984
+ return undefined;
985
+ }
986
+ }
987
+ async deleteScheduledMessage(id) {
988
+ try {
989
+ const messages = await this.getScheduledMessages();
990
+ const filteredMessages = messages.filter(m => m.id !== id);
991
+ if (filteredMessages.length === messages.length) {
992
+ return false; // Message not found
993
+ }
994
+ await atomicWriteFile(this.scheduledMessagesFile, JSON.stringify(filteredMessages, null, 2));
995
+ return true;
996
+ }
997
+ catch (error) {
998
+ this.logger.error('Error deleting scheduled message', { error: error instanceof Error ? error.message : String(error) });
999
+ throw error;
1000
+ }
1001
+ }
1002
+ // Recurring Checks persistence
1003
+ /**
1004
+ * Get all persisted recurring checks.
1005
+ *
1006
+ * @returns Array of persisted ScheduledCheck entries
1007
+ */
1008
+ async getRecurringChecks() {
1009
+ try {
1010
+ await this.ensureFile(this.recurringChecksFile, []);
1011
+ const content = await fs.readFile(this.recurringChecksFile, 'utf-8');
1012
+ if (!content.trim()) {
1013
+ await atomicWriteFile(this.recurringChecksFile, JSON.stringify([], null, 2));
1014
+ return [];
1015
+ }
1016
+ try {
1017
+ return JSON.parse(content);
1018
+ }
1019
+ catch (parseError) {
1020
+ this.logger.error('Error parsing recurring checks JSON, resetting file', {
1021
+ error: parseError instanceof Error ? parseError.message : String(parseError),
1022
+ });
1023
+ await atomicWriteFile(this.recurringChecksFile, JSON.stringify([], null, 2));
1024
+ return [];
1025
+ }
1026
+ }
1027
+ catch (error) {
1028
+ this.logger.error('Error reading recurring checks', {
1029
+ error: error instanceof Error ? error.message : String(error),
1030
+ });
1031
+ return [];
1032
+ }
1033
+ }
1034
+ /**
1035
+ * Save or update a recurring check entry on disk.
1036
+ *
1037
+ * @param check - The ScheduledCheck to persist
1038
+ */
1039
+ async saveRecurringCheck(check) {
1040
+ try {
1041
+ const checks = await this.getRecurringChecks();
1042
+ const existingIndex = checks.findIndex(c => c.id === check.id);
1043
+ if (existingIndex >= 0) {
1044
+ checks[existingIndex] = check;
1045
+ }
1046
+ else {
1047
+ checks.push(check);
1048
+ }
1049
+ await atomicWriteFile(this.recurringChecksFile, JSON.stringify(checks, null, 2));
1050
+ }
1051
+ catch (error) {
1052
+ this.logger.error('Error saving recurring check', {
1053
+ error: error instanceof Error ? error.message : String(error),
1054
+ });
1055
+ throw error;
1056
+ }
1057
+ }
1058
+ /**
1059
+ * Delete a persisted recurring check by ID.
1060
+ *
1061
+ * @param id - The check ID to delete
1062
+ * @returns true if the check was found and deleted
1063
+ */
1064
+ async deleteRecurringCheck(id) {
1065
+ try {
1066
+ const checks = await this.getRecurringChecks();
1067
+ const filtered = checks.filter(c => c.id !== id);
1068
+ if (filtered.length === checks.length) {
1069
+ return false;
1070
+ }
1071
+ await atomicWriteFile(this.recurringChecksFile, JSON.stringify(filtered, null, 2));
1072
+ return true;
1073
+ }
1074
+ catch (error) {
1075
+ this.logger.error('Error deleting recurring check', {
1076
+ error: error instanceof Error ? error.message : String(error),
1077
+ });
1078
+ throw error;
1079
+ }
1080
+ }
1081
+ /**
1082
+ * Clear all persisted recurring checks.
1083
+ */
1084
+ async clearRecurringChecks() {
1085
+ try {
1086
+ await atomicWriteFile(this.recurringChecksFile, JSON.stringify([], null, 2));
1087
+ }
1088
+ catch (error) {
1089
+ this.logger.error('Error clearing recurring checks', {
1090
+ error: error instanceof Error ? error.message : String(error),
1091
+ });
1092
+ throw error;
1093
+ }
1094
+ }
1095
+ // One-Time Checks persistence
1096
+ /**
1097
+ * Get all persisted one-time checks.
1098
+ *
1099
+ * @returns Array of persisted ScheduledCheck entries
1100
+ */
1101
+ async getOneTimeChecks() {
1102
+ try {
1103
+ await this.ensureFile(this.oneTimeChecksFile, []);
1104
+ const content = await fs.readFile(this.oneTimeChecksFile, 'utf-8');
1105
+ if (!content.trim()) {
1106
+ await atomicWriteFile(this.oneTimeChecksFile, JSON.stringify([], null, 2));
1107
+ return [];
1108
+ }
1109
+ try {
1110
+ return JSON.parse(content);
1111
+ }
1112
+ catch (parseError) {
1113
+ this.logger.error('Error parsing one-time checks JSON, resetting file', {
1114
+ error: parseError instanceof Error ? parseError.message : String(parseError),
1115
+ });
1116
+ await atomicWriteFile(this.oneTimeChecksFile, JSON.stringify([], null, 2));
1117
+ return [];
1118
+ }
1119
+ }
1120
+ catch (error) {
1121
+ this.logger.error('Error reading one-time checks', {
1122
+ error: error instanceof Error ? error.message : String(error),
1123
+ });
1124
+ return [];
1125
+ }
1126
+ }
1127
+ /**
1128
+ * Save or update a one-time check entry on disk.
1129
+ *
1130
+ * @param check - The ScheduledCheck to persist
1131
+ */
1132
+ async saveOneTimeCheck(check) {
1133
+ try {
1134
+ const checks = await this.getOneTimeChecks();
1135
+ const existingIndex = checks.findIndex(c => c.id === check.id);
1136
+ if (existingIndex >= 0) {
1137
+ checks[existingIndex] = check;
1138
+ }
1139
+ else {
1140
+ checks.push(check);
1141
+ }
1142
+ await atomicWriteFile(this.oneTimeChecksFile, JSON.stringify(checks, null, 2));
1143
+ }
1144
+ catch (error) {
1145
+ this.logger.error('Error saving one-time check', {
1146
+ error: error instanceof Error ? error.message : String(error),
1147
+ });
1148
+ throw error;
1149
+ }
1150
+ }
1151
+ /**
1152
+ * Delete a persisted one-time check by ID.
1153
+ *
1154
+ * @param id - The check ID to delete
1155
+ * @returns true if the check was found and deleted
1156
+ */
1157
+ async deleteOneTimeCheck(id) {
1158
+ try {
1159
+ const checks = await this.getOneTimeChecks();
1160
+ const filtered = checks.filter(c => c.id !== id);
1161
+ if (filtered.length === checks.length) {
1162
+ return false;
1163
+ }
1164
+ await atomicWriteFile(this.oneTimeChecksFile, JSON.stringify(filtered, null, 2));
1165
+ return true;
1166
+ }
1167
+ catch (error) {
1168
+ this.logger.error('Error deleting one-time check', {
1169
+ error: error instanceof Error ? error.message : String(error),
1170
+ });
1171
+ throw error;
1172
+ }
1173
+ }
1174
+ /**
1175
+ * Clear all persisted one-time checks.
1176
+ */
1177
+ async clearOneTimeChecks() {
1178
+ try {
1179
+ await atomicWriteFile(this.oneTimeChecksFile, JSON.stringify([], null, 2));
1180
+ }
1181
+ catch (error) {
1182
+ this.logger.error('Error clearing one-time checks', {
1183
+ error: error instanceof Error ? error.message : String(error),
1184
+ });
1185
+ throw error;
1186
+ }
1187
+ }
1188
+ // Message Delivery Logs management
1189
+ async getDeliveryLogs() {
1190
+ try {
1191
+ await this.ensureFile(this.deliveryLogsFile);
1192
+ const content = await fs.readFile(this.deliveryLogsFile, 'utf-8');
1193
+ const logs = JSON.parse(content);
1194
+ // Sort by sentAt (newest first)
1195
+ return logs.sort((a, b) => new Date(b.sentAt).getTime() - new Date(a.sentAt).getTime());
1196
+ }
1197
+ catch (error) {
1198
+ this.logger.error('Error reading delivery logs', { error: error instanceof Error ? error.message : String(error) });
1199
+ return [];
1200
+ }
1201
+ }
1202
+ async saveDeliveryLog(log) {
1203
+ try {
1204
+ const logs = await this.getDeliveryLogs();
1205
+ logs.unshift(log); // Add to beginning for newest first order
1206
+ // Keep only last 1000 logs to prevent file from getting too large
1207
+ const trimmedLogs = logs.slice(0, 1000);
1208
+ await atomicWriteFile(this.deliveryLogsFile, JSON.stringify(trimmedLogs, null, 2));
1209
+ }
1210
+ catch (error) {
1211
+ this.logger.error('Error saving delivery log', { error: error instanceof Error ? error.message : String(error) });
1212
+ throw error;
1213
+ }
1214
+ }
1215
+ async clearDeliveryLogs() {
1216
+ try {
1217
+ await atomicWriteFile(this.deliveryLogsFile, JSON.stringify([], null, 2));
1218
+ }
1219
+ catch (error) {
1220
+ this.logger.error('Error clearing delivery logs', { error: error instanceof Error ? error.message : String(error) });
1221
+ throw error;
1222
+ }
1223
+ }
1224
+ // Orchestrator management
1225
+ /**
1226
+ * Get orchestrator status from dedicated orchestrator.json file.
1227
+ *
1228
+ * @returns Orchestrator status object or null if not found
1229
+ */
1230
+ async getOrchestratorStatus() {
1231
+ try {
1232
+ // Migrate from legacy format if needed
1233
+ await this.migrateFromLegacyTeamsFile();
1234
+ // Check if orchestrator file exists
1235
+ if (!existsSync(this.orchestratorFile)) {
1236
+ // Create default orchestrator
1237
+ const orchestrator = this.createDefaultOrchestrator();
1238
+ await atomicWriteFile(this.orchestratorFile, JSON.stringify(orchestrator, null, 2));
1239
+ return orchestrator;
1240
+ }
1241
+ const content = await fs.readFile(this.orchestratorFile, 'utf-8');
1242
+ const orchestrator = JSON.parse(content);
1243
+ return orchestrator;
1244
+ }
1245
+ catch (error) {
1246
+ this.logger.error('Error reading orchestrator status', {
1247
+ error: error instanceof Error ? error.message : String(error),
1248
+ });
1249
+ return null;
1250
+ }
1251
+ }
1252
+ /**
1253
+ * Update agent status for orchestrator or any team member.
1254
+ * Orchestrator status is stored in teams/orchestrator.json.
1255
+ * Team member status is stored in the respective team's file.
1256
+ *
1257
+ * @param sessionName - Session name of the agent (CREWLY_CONSTANTS.SESSIONS.ORCHESTRATOR_NAME for orchestrator)
1258
+ * @param status - New agent status (CREWLY_CONSTANTS.AGENT_STATUSES.INACTIVE | CREWLY_CONSTANTS.AGENT_STATUSES.ACTIVATING | CREWLY_CONSTANTS.AGENT_STATUSES.ACTIVE)
1259
+ */
1260
+ async updateAgentStatus(sessionName, status) {
1261
+ // Handle orchestrator separately
1262
+ if (sessionName === CREWLY_CONSTANTS.SESSIONS.ORCHESTRATOR_NAME) {
1263
+ return withOperationLock(this.orchestratorFile, async () => {
1264
+ try {
1265
+ let orchestrator;
1266
+ if (existsSync(this.orchestratorFile)) {
1267
+ const content = await fs.readFile(this.orchestratorFile, 'utf-8');
1268
+ orchestrator = JSON.parse(content);
1269
+ }
1270
+ else {
1271
+ orchestrator = this.createDefaultOrchestrator();
1272
+ }
1273
+ orchestrator.agentStatus = status;
1274
+ orchestrator.updatedAt = new Date().toISOString();
1275
+ await atomicWriteFile(this.orchestratorFile, JSON.stringify(orchestrator, null, 2));
1276
+ this.logger.debug('Updated orchestrator status', { status });
1277
+ }
1278
+ catch (error) {
1279
+ this.logger.error('Error updating orchestrator status', {
1280
+ error: error instanceof Error ? error.message : String(error),
1281
+ });
1282
+ throw error;
1283
+ }
1284
+ });
1285
+ }
1286
+ // Handle regular team members - find the team containing this member
1287
+ try {
1288
+ const teams = await this.getTeams();
1289
+ let memberFound = false;
1290
+ for (const team of teams) {
1291
+ for (const member of team.members || []) {
1292
+ if (member.sessionName === sessionName) {
1293
+ member.agentStatus = status;
1294
+ member.updatedAt = new Date().toISOString();
1295
+ memberFound = true;
1296
+ // Save the updated team
1297
+ await this.saveTeam(team);
1298
+ this.logger.debug('Updated team member status', {
1299
+ sessionName,
1300
+ status,
1301
+ teamId: team.id,
1302
+ });
1303
+ break;
1304
+ }
1305
+ }
1306
+ if (memberFound)
1307
+ break;
1308
+ }
1309
+ if (!memberFound) {
1310
+ // DEBUG: This is expected for orphaned sessions from deleted/reconfigured teams.
1311
+ // Monitors (RuntimeExitMonitor, ActivityMonitor) may still track stale tmux sessions.
1312
+ this.logger.debug('Agent session not found in any team (likely orphaned)', { sessionName });
1313
+ }
1314
+ }
1315
+ catch (error) {
1316
+ this.logger.error('Error updating agent status', {
1317
+ sessionName,
1318
+ error: error instanceof Error ? error.message : String(error),
1319
+ });
1320
+ throw error;
1321
+ }
1322
+ }
1323
+ /**
1324
+ * Update team member runtime type
1325
+ */
1326
+ async updateTeamMemberRuntimeType(teamId, memberId, runtimeType) {
1327
+ try {
1328
+ const teams = await this.getTeams();
1329
+ const team = teams.find(t => t.id === teamId);
1330
+ if (!team) {
1331
+ throw new Error(`Team not found: ${teamId}`);
1332
+ }
1333
+ const member = team.members.find(m => m.id === memberId);
1334
+ if (!member) {
1335
+ throw new Error(`Team member not found: ${memberId} in team ${teamId}`);
1336
+ }
1337
+ member.runtimeType = runtimeType;
1338
+ member.updatedAt = new Date().toISOString();
1339
+ await this.saveTeam(team);
1340
+ }
1341
+ catch (error) {
1342
+ this.logger.error('Error updating team member runtime type', { teamId, memberId, error: error instanceof Error ? error.message : String(error) });
1343
+ throw error;
1344
+ }
1345
+ }
1346
+ /**
1347
+ * Update orchestrator runtime type in teams/orchestrator.json.
1348
+ *
1349
+ * @param runtimeType - The runtime type to set
1350
+ */
1351
+ async updateOrchestratorRuntimeType(runtimeType) {
1352
+ return withOperationLock(this.orchestratorFile, async () => {
1353
+ try {
1354
+ let orchestrator;
1355
+ if (existsSync(this.orchestratorFile)) {
1356
+ const content = await fs.readFile(this.orchestratorFile, 'utf-8');
1357
+ orchestrator = JSON.parse(content);
1358
+ }
1359
+ else {
1360
+ orchestrator = this.createDefaultOrchestrator();
1361
+ }
1362
+ orchestrator.runtimeType = runtimeType;
1363
+ orchestrator.updatedAt = new Date().toISOString();
1364
+ await atomicWriteFile(this.orchestratorFile, JSON.stringify(orchestrator, null, 2));
1365
+ this.logger.debug('Updated orchestrator runtime type', { runtimeType });
1366
+ }
1367
+ catch (error) {
1368
+ this.logger.error('Error updating orchestrator runtime type', {
1369
+ error: error instanceof Error ? error.message : String(error),
1370
+ });
1371
+ throw error;
1372
+ }
1373
+ });
1374
+ }
1375
+ /**
1376
+ * Update the teams backup file asynchronously with debouncing.
1377
+ *
1378
+ * When many teams are saved in rapid succession (e.g. ActivityMonitor
1379
+ * marking 14 stale agents), each saveTeam() call used to trigger a
1380
+ * full getTeams() + backup, flooding the libuv thread pool with I/O
1381
+ * and starving other requests. Debouncing collapses the burst into a
1382
+ * single backup operation.
1383
+ */
1384
+ updateTeamsBackup() {
1385
+ if (this.backupDebounceTimer) {
1386
+ clearTimeout(this.backupDebounceTimer);
1387
+ }
1388
+ this.backupDebounceTimer = setTimeout(() => {
1389
+ this.backupDebounceTimer = null;
1390
+ this.getTeams()
1391
+ .then((teams) => TeamsBackupService.getInstance().updateBackup(teams))
1392
+ .catch((error) => {
1393
+ this.logger.warn('Failed to update teams backup', {
1394
+ error: error instanceof Error ? error.message : String(error),
1395
+ });
1396
+ });
1397
+ }, this.BACKUP_DEBOUNCE_MS);
1398
+ }
1399
+ /**
1400
+ * @deprecated Use updateAgentStatus instead
1401
+ */
1402
+ async updateOrchestratorStatus(status) {
1403
+ return this.updateAgentStatus(CREWLY_CONSTANTS.SESSIONS.ORCHESTRATOR_NAME, status);
1404
+ }
1405
+ }
1406
+ //# sourceMappingURL=storage.service.js.map