@proletariat/cli 0.3.81 → 0.3.83

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 (377) hide show
  1. package/README.md +2 -46
  2. package/dist/commands/action/run.js +2 -2
  3. package/dist/commands/action/run.js.map +1 -1
  4. package/dist/commands/agent/staff/add.d.ts +17 -1
  5. package/dist/commands/agent/staff/add.js +274 -1
  6. package/dist/commands/agent/staff/add.js.map +1 -1
  7. package/dist/commands/agent/staff/list.d.ts +10 -1
  8. package/dist/commands/agent/staff/list.js +103 -1
  9. package/dist/commands/agent/staff/list.js.map +1 -1
  10. package/dist/commands/agent/staff/remove.d.ts +18 -1
  11. package/dist/commands/agent/staff/remove.js +143 -1
  12. package/dist/commands/agent/staff/remove.js.map +1 -1
  13. package/dist/commands/{epic/activate.d.ts → pr/close.d.ts} +8 -2
  14. package/dist/commands/pr/close.js +157 -0
  15. package/dist/commands/pr/close.js.map +1 -0
  16. package/dist/commands/pr/index.js +6 -2
  17. package/dist/commands/pr/index.js.map +1 -1
  18. package/dist/commands/pr/list.d.ts +1 -0
  19. package/dist/commands/pr/list.js +37 -3
  20. package/dist/commands/pr/list.js.map +1 -1
  21. package/dist/commands/pr/merge.js +1 -1
  22. package/dist/commands/pr/merge.js.map +1 -1
  23. package/dist/commands/pr/status.js +33 -1
  24. package/dist/commands/pr/status.js.map +1 -1
  25. package/dist/commands/self-update.d.ts +7 -14
  26. package/dist/commands/self-update.js +11 -179
  27. package/dist/commands/self-update.js.map +1 -1
  28. package/dist/commands/{workflow/switch.d.ts → ticket/cancel.d.ts} +4 -2
  29. package/dist/commands/ticket/cancel.js +204 -0
  30. package/dist/commands/ticket/cancel.js.map +1 -0
  31. package/dist/commands/ticket/link/index.js +0 -12
  32. package/dist/commands/ticket/link/index.js.map +1 -1
  33. package/dist/commands/ticket/move.d.ts +1 -0
  34. package/dist/commands/ticket/move.js +10 -3
  35. package/dist/commands/ticket/move.js.map +1 -1
  36. package/dist/commands/update.d.ts +18 -0
  37. package/dist/commands/update.js +184 -0
  38. package/dist/commands/update.js.map +1 -0
  39. package/dist/lib/database/drizzle-schema.d.ts +301 -0
  40. package/dist/lib/database/drizzle-schema.js +22 -0
  41. package/dist/lib/database/drizzle-schema.js.map +1 -1
  42. package/dist/lib/database/index.d.ts +3 -0
  43. package/dist/lib/database/index.js +556 -479
  44. package/dist/lib/database/index.js.map +1 -1
  45. package/dist/lib/database/migrations/0006_drop_theme_names_used.d.ts +8 -0
  46. package/dist/lib/database/migrations/0006_drop_theme_names_used.js +30 -0
  47. package/dist/lib/database/migrations/0006_drop_theme_names_used.js.map +1 -0
  48. package/dist/lib/database/migrations/0007_add_worktree_columns.d.ts +8 -0
  49. package/dist/lib/database/migrations/0007_add_worktree_columns.js +24 -0
  50. package/dist/lib/database/migrations/0007_add_worktree_columns.js.map +1 -0
  51. package/dist/lib/database/migrations/0008_add_agent_mount_mode.d.ts +8 -0
  52. package/dist/lib/database/migrations/0008_add_agent_mount_mode.js +18 -0
  53. package/dist/lib/database/migrations/0008_add_agent_mount_mode.js.map +1 -0
  54. package/dist/lib/database/migrations/0009_create_media_items.d.ts +7 -0
  55. package/dist/lib/database/migrations/0009_create_media_items.js +29 -0
  56. package/dist/lib/database/migrations/0009_create_media_items.js.map +1 -0
  57. package/dist/lib/database/migrations/0010_add_ticket_position.d.ts +9 -0
  58. package/dist/lib/database/migrations/0010_add_ticket_position.js +44 -0
  59. package/dist/lib/database/migrations/0010_add_ticket_position.js.map +1 -0
  60. package/dist/lib/database/migrations/index.js +10 -0
  61. package/dist/lib/database/migrations/index.js.map +1 -1
  62. package/dist/lib/events/events.d.ts +2 -1
  63. package/dist/lib/pmo/storage/dependencies.js +45 -21
  64. package/dist/lib/pmo/storage/dependencies.js.map +1 -1
  65. package/dist/lib/pmo/storage/statuses.d.ts +2 -0
  66. package/dist/lib/pmo/storage/statuses.js +209 -150
  67. package/dist/lib/pmo/storage/statuses.js.map +1 -1
  68. package/dist/lib/pr/index.d.ts +14 -0
  69. package/dist/lib/pr/index.js +48 -0
  70. package/dist/lib/pr/index.js.map +1 -1
  71. package/dist/lib/telemetry/analytics.d.ts +5 -5
  72. package/dist/lib/telemetry/analytics.js +87 -21
  73. package/dist/lib/telemetry/analytics.js.map +1 -1
  74. package/dist/lib/update-check.js +1 -1
  75. package/dist/lib/update-check.js.map +1 -1
  76. package/dist/lib/update-prompt.js +1 -1
  77. package/dist/lib/work-lifecycle/events.d.ts +11 -0
  78. package/oclif.manifest.json +4823 -11486
  79. package/package.json +2 -1
  80. package/dist/commands/agents/index.d.ts +0 -11
  81. package/dist/commands/agents/index.js +0 -105
  82. package/dist/commands/agents/index.js.map +0 -1
  83. package/dist/commands/agents/themes/add-names.d.ts +0 -1
  84. package/dist/commands/agents/themes/add-names.js +0 -2
  85. package/dist/commands/agents/themes/add-names.js.map +0 -1
  86. package/dist/commands/agents/themes/create.d.ts +0 -1
  87. package/dist/commands/agents/themes/create.js +0 -2
  88. package/dist/commands/agents/themes/create.js.map +0 -1
  89. package/dist/commands/agents/themes/list.d.ts +0 -1
  90. package/dist/commands/agents/themes/list.js +0 -2
  91. package/dist/commands/agents/themes/list.js.map +0 -1
  92. package/dist/commands/category/create.d.ts +0 -19
  93. package/dist/commands/category/create.js +0 -130
  94. package/dist/commands/category/create.js.map +0 -1
  95. package/dist/commands/category/delete.d.ts +0 -18
  96. package/dist/commands/category/delete.js +0 -124
  97. package/dist/commands/category/delete.js.map +0 -1
  98. package/dist/commands/category/index.d.ts +0 -16
  99. package/dist/commands/category/index.js +0 -88
  100. package/dist/commands/category/index.js.map +0 -1
  101. package/dist/commands/category/list.d.ts +0 -19
  102. package/dist/commands/category/list.js +0 -111
  103. package/dist/commands/category/list.js.map +0 -1
  104. package/dist/commands/category/rename.d.ts +0 -19
  105. package/dist/commands/category/rename.js +0 -160
  106. package/dist/commands/category/rename.js.map +0 -1
  107. package/dist/commands/epic/activate.js +0 -107
  108. package/dist/commands/epic/activate.js.map +0 -1
  109. package/dist/commands/epic/archive.d.ts +0 -15
  110. package/dist/commands/epic/archive.js +0 -118
  111. package/dist/commands/epic/archive.js.map +0 -1
  112. package/dist/commands/epic/create.d.ts +0 -17
  113. package/dist/commands/epic/create.js +0 -179
  114. package/dist/commands/epic/create.js.map +0 -1
  115. package/dist/commands/epic/delete.d.ts +0 -15
  116. package/dist/commands/epic/delete.js +0 -129
  117. package/dist/commands/epic/delete.js.map +0 -1
  118. package/dist/commands/epic/index.d.ts +0 -14
  119. package/dist/commands/epic/index.js +0 -88
  120. package/dist/commands/epic/index.js.map +0 -1
  121. package/dist/commands/epic/list.d.ts +0 -13
  122. package/dist/commands/epic/list.js +0 -103
  123. package/dist/commands/epic/list.js.map +0 -1
  124. package/dist/commands/epic/move.d.ts +0 -16
  125. package/dist/commands/epic/move.js +0 -162
  126. package/dist/commands/epic/move.js.map +0 -1
  127. package/dist/commands/epic/progress.d.ts +0 -17
  128. package/dist/commands/epic/progress.js +0 -170
  129. package/dist/commands/epic/progress.js.map +0 -1
  130. package/dist/commands/epic/project.d.ts +0 -16
  131. package/dist/commands/epic/project.js +0 -199
  132. package/dist/commands/epic/project.js.map +0 -1
  133. package/dist/commands/epic/reorder.d.ts +0 -22
  134. package/dist/commands/epic/reorder.js +0 -152
  135. package/dist/commands/epic/reorder.js.map +0 -1
  136. package/dist/commands/epic/show.d.ts +0 -13
  137. package/dist/commands/epic/show.js +0 -17
  138. package/dist/commands/epic/show.js.map +0 -1
  139. package/dist/commands/epic/spec.d.ts +0 -17
  140. package/dist/commands/epic/spec.js +0 -189
  141. package/dist/commands/epic/spec.js.map +0 -1
  142. package/dist/commands/epic/ticket.d.ts +0 -21
  143. package/dist/commands/epic/ticket.js +0 -300
  144. package/dist/commands/epic/ticket.js.map +0 -1
  145. package/dist/commands/epic/view.d.ts +0 -14
  146. package/dist/commands/epic/view.js +0 -141
  147. package/dist/commands/epic/view.js.map +0 -1
  148. package/dist/commands/execution/kill.d.ts +0 -12
  149. package/dist/commands/execution/kill.js +0 -18
  150. package/dist/commands/execution/kill.js.map +0 -1
  151. package/dist/commands/label/create.d.ts +0 -20
  152. package/dist/commands/label/create.js +0 -58
  153. package/dist/commands/label/create.js.map +0 -1
  154. package/dist/commands/label/delete.d.ts +0 -17
  155. package/dist/commands/label/delete.js +0 -33
  156. package/dist/commands/label/delete.js.map +0 -1
  157. package/dist/commands/label/group/create.d.ts +0 -20
  158. package/dist/commands/label/group/create.js +0 -56
  159. package/dist/commands/label/group/create.js.map +0 -1
  160. package/dist/commands/label/group/list.d.ts +0 -14
  161. package/dist/commands/label/group/list.js +0 -53
  162. package/dist/commands/label/group/list.js.map +0 -1
  163. package/dist/commands/label/index.d.ts +0 -15
  164. package/dist/commands/label/index.js +0 -59
  165. package/dist/commands/label/index.js.map +0 -1
  166. package/dist/commands/label/list.d.ts +0 -16
  167. package/dist/commands/label/list.js +0 -84
  168. package/dist/commands/label/list.js.map +0 -1
  169. package/dist/commands/link/create.d.ts +0 -16
  170. package/dist/commands/link/create.js +0 -136
  171. package/dist/commands/link/create.js.map +0 -1
  172. package/dist/commands/link/index.d.ts +0 -14
  173. package/dist/commands/link/index.js +0 -82
  174. package/dist/commands/link/index.js.map +0 -1
  175. package/dist/commands/link/list.d.ts +0 -18
  176. package/dist/commands/link/list.js +0 -178
  177. package/dist/commands/link/list.js.map +0 -1
  178. package/dist/commands/link/remove.d.ts +0 -16
  179. package/dist/commands/link/remove.js +0 -115
  180. package/dist/commands/link/remove.js.map +0 -1
  181. package/dist/commands/media/add.d.ts +0 -19
  182. package/dist/commands/media/add.js +0 -98
  183. package/dist/commands/media/add.js.map +0 -1
  184. package/dist/commands/media/index.d.ts +0 -14
  185. package/dist/commands/media/index.js +0 -93
  186. package/dist/commands/media/index.js.map +0 -1
  187. package/dist/commands/media/list.d.ts +0 -15
  188. package/dist/commands/media/list.js +0 -94
  189. package/dist/commands/media/list.js.map +0 -1
  190. package/dist/commands/media/preprocess.d.ts +0 -19
  191. package/dist/commands/media/preprocess.js +0 -91
  192. package/dist/commands/media/preprocess.js.map +0 -1
  193. package/dist/commands/media/remove.d.ts +0 -18
  194. package/dist/commands/media/remove.js +0 -105
  195. package/dist/commands/media/remove.js.map +0 -1
  196. package/dist/commands/media/show.d.ts +0 -17
  197. package/dist/commands/media/show.js +0 -122
  198. package/dist/commands/media/show.js.map +0 -1
  199. package/dist/commands/phase/create.d.ts +0 -22
  200. package/dist/commands/phase/create.js +0 -165
  201. package/dist/commands/phase/create.js.map +0 -1
  202. package/dist/commands/phase/delete.d.ts +0 -18
  203. package/dist/commands/phase/delete.js +0 -75
  204. package/dist/commands/phase/delete.js.map +0 -1
  205. package/dist/commands/phase/list.d.ts +0 -13
  206. package/dist/commands/phase/list.js +0 -75
  207. package/dist/commands/phase/list.js.map +0 -1
  208. package/dist/commands/phase/move.d.ts +0 -18
  209. package/dist/commands/phase/move.js +0 -124
  210. package/dist/commands/phase/move.js.map +0 -1
  211. package/dist/commands/phase/template/apply.d.ts +0 -26
  212. package/dist/commands/phase/template/apply.js +0 -15
  213. package/dist/commands/phase/template/apply.js.map +0 -1
  214. package/dist/commands/phase/template/create.d.ts +0 -23
  215. package/dist/commands/phase/template/create.js +0 -15
  216. package/dist/commands/phase/template/create.js.map +0 -1
  217. package/dist/commands/phase/template/delete.d.ts +0 -18
  218. package/dist/commands/phase/template/delete.js +0 -63
  219. package/dist/commands/phase/template/delete.js.map +0 -1
  220. package/dist/commands/phase/template/list.d.ts +0 -17
  221. package/dist/commands/phase/template/list.js +0 -90
  222. package/dist/commands/phase/template/list.js.map +0 -1
  223. package/dist/commands/phase/template/update.d.ts +0 -1
  224. package/dist/commands/phase/template/update.js +0 -2
  225. package/dist/commands/phase/template/update.js.map +0 -1
  226. package/dist/commands/phase/update.d.ts +0 -23
  227. package/dist/commands/phase/update.js +0 -209
  228. package/dist/commands/phase/update.js.map +0 -1
  229. package/dist/commands/priority/add.d.ts +0 -16
  230. package/dist/commands/priority/add.js +0 -71
  231. package/dist/commands/priority/add.js.map +0 -1
  232. package/dist/commands/priority/list.d.ts +0 -11
  233. package/dist/commands/priority/list.js +0 -35
  234. package/dist/commands/priority/list.js.map +0 -1
  235. package/dist/commands/priority/remove.d.ts +0 -14
  236. package/dist/commands/priority/remove.js +0 -55
  237. package/dist/commands/priority/remove.js.map +0 -1
  238. package/dist/commands/priority/set.d.ts +0 -15
  239. package/dist/commands/priority/set.js +0 -61
  240. package/dist/commands/priority/set.js.map +0 -1
  241. package/dist/commands/roadmap/add-project.d.ts +0 -19
  242. package/dist/commands/roadmap/add-project.js +0 -119
  243. package/dist/commands/roadmap/add-project.js.map +0 -1
  244. package/dist/commands/roadmap/create.d.ts +0 -22
  245. package/dist/commands/roadmap/create.js +0 -168
  246. package/dist/commands/roadmap/create.js.map +0 -1
  247. package/dist/commands/roadmap/delete.d.ts +0 -18
  248. package/dist/commands/roadmap/delete.js +0 -96
  249. package/dist/commands/roadmap/delete.js.map +0 -1
  250. package/dist/commands/roadmap/generate.d.ts +0 -24
  251. package/dist/commands/roadmap/generate.js +0 -201
  252. package/dist/commands/roadmap/generate.js.map +0 -1
  253. package/dist/commands/roadmap/index.d.ts +0 -14
  254. package/dist/commands/roadmap/index.js +0 -57
  255. package/dist/commands/roadmap/index.js.map +0 -1
  256. package/dist/commands/roadmap/list.d.ts +0 -14
  257. package/dist/commands/roadmap/list.js +0 -58
  258. package/dist/commands/roadmap/list.js.map +0 -1
  259. package/dist/commands/roadmap/remove-project.d.ts +0 -19
  260. package/dist/commands/roadmap/remove-project.js +0 -124
  261. package/dist/commands/roadmap/remove-project.js.map +0 -1
  262. package/dist/commands/roadmap/reorder.d.ts +0 -18
  263. package/dist/commands/roadmap/reorder.js +0 -147
  264. package/dist/commands/roadmap/reorder.js.map +0 -1
  265. package/dist/commands/roadmap/update.d.ts +0 -20
  266. package/dist/commands/roadmap/update.js +0 -134
  267. package/dist/commands/roadmap/update.js.map +0 -1
  268. package/dist/commands/roadmap/view.d.ts +0 -17
  269. package/dist/commands/roadmap/view.js +0 -93
  270. package/dist/commands/roadmap/view.js.map +0 -1
  271. package/dist/commands/staff/add.d.ts +0 -17
  272. package/dist/commands/staff/add.js +0 -275
  273. package/dist/commands/staff/add.js.map +0 -1
  274. package/dist/commands/staff/index.d.ts +0 -15
  275. package/dist/commands/staff/index.js +0 -88
  276. package/dist/commands/staff/index.js.map +0 -1
  277. package/dist/commands/staff/list.d.ts +0 -10
  278. package/dist/commands/staff/list.js +0 -104
  279. package/dist/commands/staff/list.js.map +0 -1
  280. package/dist/commands/staff/remove.d.ts +0 -18
  281. package/dist/commands/staff/remove.js +0 -144
  282. package/dist/commands/staff/remove.js.map +0 -1
  283. package/dist/commands/status/category.d.ts +0 -15
  284. package/dist/commands/status/category.js +0 -64
  285. package/dist/commands/status/category.js.map +0 -1
  286. package/dist/commands/status/create.d.ts +0 -21
  287. package/dist/commands/status/create.js +0 -161
  288. package/dist/commands/status/create.js.map +0 -1
  289. package/dist/commands/status/delete.d.ts +0 -14
  290. package/dist/commands/status/delete.js +0 -86
  291. package/dist/commands/status/delete.js.map +0 -1
  292. package/dist/commands/status/index.d.ts +0 -15
  293. package/dist/commands/status/index.js +0 -98
  294. package/dist/commands/status/index.js.map +0 -1
  295. package/dist/commands/status/list.d.ts +0 -13
  296. package/dist/commands/status/list.js +0 -97
  297. package/dist/commands/status/list.js.map +0 -1
  298. package/dist/commands/status/move.d.ts +0 -15
  299. package/dist/commands/status/move.js +0 -126
  300. package/dist/commands/status/move.js.map +0 -1
  301. package/dist/commands/status/update.d.ts +0 -20
  302. package/dist/commands/status/update.js +0 -214
  303. package/dist/commands/status/update.js.map +0 -1
  304. package/dist/commands/template/apply.d.ts +0 -28
  305. package/dist/commands/template/apply.js +0 -258
  306. package/dist/commands/template/apply.js.map +0 -1
  307. package/dist/commands/template/create.d.ts +0 -28
  308. package/dist/commands/template/create.js +0 -238
  309. package/dist/commands/template/create.js.map +0 -1
  310. package/dist/commands/template/delete.d.ts +0 -16
  311. package/dist/commands/template/delete.js +0 -141
  312. package/dist/commands/template/delete.js.map +0 -1
  313. package/dist/commands/template/index.d.ts +0 -11
  314. package/dist/commands/template/index.js +0 -65
  315. package/dist/commands/template/index.js.map +0 -1
  316. package/dist/commands/template/list.d.ts +0 -19
  317. package/dist/commands/template/list.js +0 -155
  318. package/dist/commands/template/list.js.map +0 -1
  319. package/dist/commands/template/save.d.ts +0 -17
  320. package/dist/commands/template/save.js +0 -101
  321. package/dist/commands/template/save.js.map +0 -1
  322. package/dist/commands/template/update.d.ts +0 -19
  323. package/dist/commands/template/update.js +0 -94
  324. package/dist/commands/template/update.js.map +0 -1
  325. package/dist/commands/ticket/template/apply.d.ts +0 -26
  326. package/dist/commands/ticket/template/apply.js +0 -15
  327. package/dist/commands/ticket/template/apply.js.map +0 -1
  328. package/dist/commands/ticket/template/delete.d.ts +0 -18
  329. package/dist/commands/ticket/template/delete.js +0 -63
  330. package/dist/commands/ticket/template/delete.js.map +0 -1
  331. package/dist/commands/ticket/template/list.d.ts +0 -17
  332. package/dist/commands/ticket/template/list.js +0 -79
  333. package/dist/commands/ticket/template/list.js.map +0 -1
  334. package/dist/commands/ticket/template/save.d.ts +0 -17
  335. package/dist/commands/ticket/template/save.js +0 -99
  336. package/dist/commands/ticket/template/save.js.map +0 -1
  337. package/dist/commands/work/spawn-all.d.ts +0 -19
  338. package/dist/commands/work/spawn-all.js +0 -64
  339. package/dist/commands/work/spawn-all.js.map +0 -1
  340. package/dist/commands/workflow/create.d.ts +0 -19
  341. package/dist/commands/workflow/create.js +0 -118
  342. package/dist/commands/workflow/create.js.map +0 -1
  343. package/dist/commands/workflow/delete.d.ts +0 -18
  344. package/dist/commands/workflow/delete.js +0 -108
  345. package/dist/commands/workflow/delete.js.map +0 -1
  346. package/dist/commands/workflow/index.d.ts +0 -16
  347. package/dist/commands/workflow/index.js +0 -73
  348. package/dist/commands/workflow/index.js.map +0 -1
  349. package/dist/commands/workflow/list.d.ts +0 -16
  350. package/dist/commands/workflow/list.js +0 -73
  351. package/dist/commands/workflow/list.js.map +0 -1
  352. package/dist/commands/workflow/setup.d.ts +0 -24
  353. package/dist/commands/workflow/setup.js +0 -351
  354. package/dist/commands/workflow/setup.js.map +0 -1
  355. package/dist/commands/workflow/show.d.ts +0 -13
  356. package/dist/commands/workflow/show.js +0 -17
  357. package/dist/commands/workflow/show.js.map +0 -1
  358. package/dist/commands/workflow/switch.js +0 -144
  359. package/dist/commands/workflow/switch.js.map +0 -1
  360. package/dist/commands/workflow/view.d.ts +0 -17
  361. package/dist/commands/workflow/view.js +0 -106
  362. package/dist/commands/workflow/view.js.map +0 -1
  363. package/dist/commands/workflow-rule/create.d.ts +0 -19
  364. package/dist/commands/workflow-rule/create.js +0 -141
  365. package/dist/commands/workflow-rule/create.js.map +0 -1
  366. package/dist/commands/workflow-rule/delete.d.ts +0 -18
  367. package/dist/commands/workflow-rule/delete.js +0 -72
  368. package/dist/commands/workflow-rule/delete.js.map +0 -1
  369. package/dist/commands/workflow-rule/index.d.ts +0 -16
  370. package/dist/commands/workflow-rule/index.js +0 -83
  371. package/dist/commands/workflow-rule/index.js.map +0 -1
  372. package/dist/commands/workflow-rule/list.d.ts +0 -17
  373. package/dist/commands/workflow-rule/list.js +0 -72
  374. package/dist/commands/workflow-rule/list.js.map +0 -1
  375. package/dist/commands/workflow-rule/update.d.ts +0 -22
  376. package/dist/commands/workflow-rule/update.js +0 -84
  377. package/dist/commands/workflow-rule/update.js.map +0 -1
@@ -1,33 +1,81 @@
1
1
  import Database from 'better-sqlite3';
2
+ import { eq, and, or, isNull, sql, asc, desc, like } from 'drizzle-orm';
2
3
  import * as fs from 'node:fs';
3
4
  import * as path from 'node:path';
4
5
  import { getThemePersistentDir, isEphemeralAgentName } from '../themes.js';
5
6
  import { throwIfNativeBindingError } from './native-validation.js';
6
7
  import { runDrizzleMigrations } from './migrator.js';
7
8
  import { ALL_MIGRATIONS } from './migrations/index.js';
9
+ import { createDrizzleConnection } from './drizzle.js';
10
+ import { workspace as workspaceTable, repositories as repositoriesTable, agents as agentsTable, agentThemes as agentThemesTable, agentThemeNames as agentThemeNamesTable, agentWorktrees as agentWorktreesTable, workspaceSettings as workspaceSettingsTable, mediaItems as mediaItemsTable, } from './drizzle-schema.js';
8
11
  // Re-export CREATE_TABLES_SQL from its canonical location
9
12
  export { CREATE_TABLES_SQL } from './workspace-schema.js';
10
- // CREATE_TABLES_SQL is now in workspace-schema.ts and re-exported above
13
+ // =============================================================================
14
+ // Internal helpers
15
+ // =============================================================================
16
+ /**
17
+ * Open the workspace database, wrap it with Drizzle, run a function,
18
+ * and close the connection. Handles the open/close lifecycle.
19
+ */
20
+ function withDrizzle(workspacePath, fn) {
21
+ const sqliteDb = openWorkspaceDatabase(workspacePath);
22
+ const ddb = createDrizzleConnection(sqliteDb);
23
+ try {
24
+ return fn(ddb, sqliteDb);
25
+ }
26
+ finally {
27
+ sqliteDb.close();
28
+ }
29
+ }
30
+ /**
31
+ * Map a Drizzle agent row to the Agent interface.
32
+ * Handles default values for backwards compatibility with old databases.
33
+ */
34
+ function toAgent(row) {
35
+ return {
36
+ name: row.name,
37
+ type: (row.type || 'persistent'),
38
+ status: (row.status || 'active'),
39
+ base_name: row.baseName,
40
+ theme_id: row.themeId,
41
+ worktree_path: row.worktreePath,
42
+ mount_mode: (row.mountMode || 'worktree'),
43
+ created_at: row.createdAt,
44
+ cleaned_at: row.cleanedAt,
45
+ };
46
+ }
47
+ /**
48
+ * Map a Drizzle theme row to the AgentTheme interface.
49
+ */
50
+ function toAgentTheme(row) {
51
+ return {
52
+ id: row.id,
53
+ name: row.name,
54
+ display_name: row.displayName,
55
+ description: row.description,
56
+ builtin: Boolean(row.builtin),
57
+ created_at: row.createdAt,
58
+ };
59
+ }
11
60
  /**
12
- * Ensure ephemeral agents are correctly typed based on their worktree path or naming pattern
61
+ * Ensure ephemeral agents are correctly typed based on their worktree path or naming pattern.
62
+ * Uses raw SQL because it relies on SQLite-specific GLOB operator and sqlite_master introspection.
13
63
  */
14
64
  function ensureEphemeralAgentTypes(db) {
15
- // Check if agents table exists
65
+ // Check if agents table exists (sqlite_master introspection — no Drizzle equivalent)
16
66
  const tableExists = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='agents'").get();
17
67
  if (!tableExists) {
18
68
  return;
19
69
  }
20
70
  // Agents in temp directory should be ephemeral
21
71
  db.exec("UPDATE agents SET type = 'ephemeral' WHERE worktree_path LIKE 'agents/temp/%' AND type != 'ephemeral'");
22
- // Detect ephemeral agents by naming pattern: adjective-name-number (e.g., blue-khosla-1)
23
- // Staff agents are single names like 'lecun', 'musk', 'gates'
72
+ // Detect ephemeral agents by naming pattern using SQLite GLOB (no Drizzle equivalent)
24
73
  db.exec(`
25
74
  UPDATE agents SET type = 'ephemeral'
26
75
  WHERE type != 'ephemeral'
27
76
  AND name GLOB '*-*-[0-9]*'
28
77
  `);
29
78
  // Also detect numberless ephemeral names (e.g., bold-bezos) using isEphemeralAgentName()
30
- // This catches agents that match the adjective-name pattern but don't have a number suffix
31
79
  const potentialEphemeral = db.prepare(`
32
80
  SELECT name FROM agents
33
81
  WHERE type != 'ephemeral'
@@ -73,142 +121,8 @@ export function openWorkspaceDatabase(workspacePath) {
73
121
  db.pragma('busy_timeout = 5000'); // Wait up to 5 seconds if database is locked
74
122
  // Run Drizzle migrations (creates tracking table, applies pending migrations)
75
123
  runDrizzleMigrations(db, ALL_MIGRATIONS);
76
- // Ensure ephemeral agents are correctly typed
124
+ // Ensure ephemeral agents are correctly typed (raw SQL — uses SQLite GLOB)
77
125
  ensureEphemeralAgentTypes(db);
78
- // Ensure theme tables exist
79
- db.exec(`
80
- CREATE TABLE IF NOT EXISTS agent_themes (
81
- id TEXT PRIMARY KEY,
82
- name TEXT NOT NULL UNIQUE,
83
- display_name TEXT NOT NULL,
84
- description TEXT,
85
- builtin BOOLEAN DEFAULT FALSE,
86
- created_at TEXT NOT NULL
87
- );
88
- CREATE TABLE IF NOT EXISTS agent_theme_names (
89
- theme_id TEXT NOT NULL,
90
- name TEXT NOT NULL,
91
- PRIMARY KEY (theme_id, name),
92
- FOREIGN KEY (theme_id) REFERENCES agent_themes(id) ON DELETE CASCADE
93
- );
94
- CREATE INDEX IF NOT EXISTS idx_theme_names_theme ON agent_theme_names(theme_id);
95
- CREATE INDEX IF NOT EXISTS idx_agents_theme ON agents(theme_id);
96
- `);
97
- // Migration: drop 'used' column if it exists (no longer needed)
98
- try {
99
- const tableInfo = db.prepare("PRAGMA table_info(agent_theme_names)").all();
100
- if (tableInfo.some(col => col.name === 'used')) {
101
- // SQLite doesn't support DROP COLUMN directly, so recreate the table
102
- db.exec(`
103
- CREATE TABLE IF NOT EXISTS agent_theme_names_new (
104
- theme_id TEXT NOT NULL,
105
- name TEXT NOT NULL,
106
- PRIMARY KEY (theme_id, name),
107
- FOREIGN KEY (theme_id) REFERENCES agent_themes(id) ON DELETE CASCADE
108
- );
109
- INSERT OR IGNORE INTO agent_theme_names_new (theme_id, name)
110
- SELECT theme_id, name FROM agent_theme_names;
111
- DROP TABLE agent_theme_names;
112
- ALTER TABLE agent_theme_names_new RENAME TO agent_theme_names;
113
- CREATE INDEX IF NOT EXISTS idx_theme_names_theme ON agent_theme_names(theme_id);
114
- `);
115
- }
116
- }
117
- catch {
118
- // Ignore migration errors - table might not exist yet
119
- }
120
- // Migration: add missing columns to agent_worktrees table (TKT-1014)
121
- try {
122
- const worktreesTableExists = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='agent_worktrees'").get();
123
- if (worktreesTableExists) {
124
- const worktreeTableInfo = db.prepare("PRAGMA table_info(agent_worktrees)").all();
125
- if (!worktreeTableInfo.some(col => col.name === 'commits_ahead')) {
126
- db.exec("ALTER TABLE agent_worktrees ADD COLUMN last_commit_hash TEXT");
127
- db.exec("ALTER TABLE agent_worktrees ADD COLUMN commits_ahead INTEGER NOT NULL DEFAULT 0");
128
- db.exec("ALTER TABLE agent_worktrees ADD COLUMN is_clean INTEGER NOT NULL DEFAULT 1");
129
- db.exec("ALTER TABLE agent_worktrees ADD COLUMN last_checked TEXT");
130
- }
131
- }
132
- }
133
- catch {
134
- // Ignore migration errors - table might not exist yet or columns already exist
135
- }
136
- // Migration: add mount_mode column to agents table (TKT-686)
137
- try {
138
- const agentsTableInfo = db.prepare("PRAGMA table_info(agents)").all();
139
- if (!agentsTableInfo.some(col => col.name === 'mount_mode')) {
140
- // Add mount_mode column with default 'worktree' for existing agents (backward compat)
141
- db.exec("ALTER TABLE agents ADD COLUMN mount_mode TEXT NOT NULL DEFAULT 'worktree' CHECK (mount_mode IN ('worktree', 'clone'))");
142
- }
143
- }
144
- catch {
145
- // Ignore migration errors - table might not exist yet or column already exists
146
- }
147
- // Migration: create media_items table (TKT-077)
148
- try {
149
- const mediaTableExists = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='media_items'").get();
150
- if (!mediaTableExists) {
151
- db.exec(`
152
- CREATE TABLE IF NOT EXISTS media_items (
153
- name TEXT PRIMARY KEY,
154
- path TEXT NOT NULL,
155
- source_path TEXT,
156
- media_type TEXT NOT NULL DEFAULT 'video' CHECK (media_type IN ('video', 'audio')),
157
- duration_seconds REAL,
158
- resolution TEXT,
159
- frame_count INTEGER NOT NULL DEFAULT 0,
160
- has_transcript INTEGER NOT NULL DEFAULT 0,
161
- frame_interval INTEGER NOT NULL DEFAULT 30,
162
- status TEXT NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'processing', 'ready', 'error')),
163
- error_message TEXT,
164
- added_at TEXT NOT NULL,
165
- processed_at TEXT
166
- )
167
- `);
168
- }
169
- }
170
- catch {
171
- // Ignore migration errors
172
- }
173
- // Migration: add position column to pmo_tickets table (TKT-965)
174
- try {
175
- const ticketsTableExists = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='pmo_tickets'").get();
176
- if (ticketsTableExists) {
177
- const ticketsTableInfo = db.prepare("PRAGMA table_info(pmo_tickets)").all();
178
- if (!ticketsTableInfo.some(col => col.name === 'position')) {
179
- db.exec("ALTER TABLE pmo_tickets ADD COLUMN position INTEGER NOT NULL DEFAULT 0");
180
- db.exec("CREATE INDEX IF NOT EXISTS idx_pmo_tickets_status_position ON pmo_tickets(status_id, position)");
181
- // Backfill existing tickets with gapped positions (1000, 2000, ...) per status,
182
- // ordered by priority then created_at
183
- const statuses = db.prepare("SELECT DISTINCT status_id FROM pmo_tickets WHERE status_id IS NOT NULL").all();
184
- const getTicketsForStatus = db.prepare(`
185
- SELECT id FROM pmo_tickets WHERE status_id = ?
186
- ORDER BY
187
- CASE priority
188
- WHEN 'P0' THEN 0
189
- WHEN 'P1' THEN 1
190
- WHEN 'P2' THEN 2
191
- WHEN 'P3' THEN 3
192
- ELSE 4
193
- END,
194
- created_at ASC
195
- `);
196
- const updatePosition = db.prepare("UPDATE pmo_tickets SET position = ? WHERE id = ?");
197
- const backfill = db.transaction(() => {
198
- for (const { status_id } of statuses) {
199
- const tickets = getTicketsForStatus.all(status_id);
200
- tickets.forEach((ticket, idx) => {
201
- updatePosition.run((idx + 1) * 1000, ticket.id);
202
- });
203
- }
204
- });
205
- backfill();
206
- }
207
- }
208
- }
209
- catch {
210
- // Ignore migration errors - table might not exist yet
211
- }
212
126
  return db;
213
127
  }
214
128
  /**
@@ -241,11 +155,15 @@ export function createWorkspaceDatabase(workspacePath, type, workspaceName, hasP
241
155
  db.pragma('foreign_keys = ON');
242
156
  // Run all migrations (baseline creates core workspace + PMO tables)
243
157
  runDrizzleMigrations(db, ALL_MIGRATIONS);
244
- // Insert workspace data (convert boolean to number for SQLite)
245
- db.prepare(`
246
- INSERT INTO workspace (id, type, workspace_name, has_pmo, created_at)
247
- VALUES (1, ?, ?, ?, ?)
248
- `).run(type, workspaceName, hasPMO ? 1 : 0, new Date().toISOString());
158
+ // Insert workspace data using Drizzle
159
+ const ddb = createDrizzleConnection(db);
160
+ ddb.insert(workspaceTable).values({
161
+ id: 1,
162
+ type,
163
+ workspaceName,
164
+ hasPmo: hasPMO,
165
+ createdAt: new Date().toISOString(),
166
+ }).run();
249
167
  return db;
250
168
  }
251
169
  /**
@@ -253,10 +171,19 @@ export function createWorkspaceDatabase(workspacePath, type, workspaceName, hasP
253
171
  */
254
172
  export function getWorkspaceConfig(workspacePath) {
255
173
  try {
256
- const db = openWorkspaceDatabase(workspacePath);
257
- const config = db.prepare('SELECT * FROM workspace LIMIT 1').get();
258
- db.close();
259
- return config || null;
174
+ return withDrizzle(workspacePath, (ddb) => {
175
+ const row = ddb.select().from(workspaceTable).limit(1).get();
176
+ if (!row)
177
+ return null;
178
+ return {
179
+ id: row.id ?? 1,
180
+ type: row.type,
181
+ workspace_name: row.workspaceName,
182
+ has_pmo: Boolean(row.hasPmo),
183
+ active_theme_id: row.activeThemeId,
184
+ created_at: row.createdAt,
185
+ };
186
+ });
260
187
  }
261
188
  catch {
262
189
  return null;
@@ -273,12 +200,12 @@ export function getActiveTheme(workspacePath) {
273
200
  return getTheme(workspacePath, config.active_theme_id);
274
201
  }
275
202
  // Auto-detect from existing agents
276
- const agents = getWorkspaceAgents(workspacePath);
277
- if (agents.length === 0) {
203
+ const agentList = getWorkspaceAgents(workspacePath);
204
+ if (agentList.length === 0) {
278
205
  return null;
279
206
  }
280
207
  // Check if any agent has a theme_id set
281
- const themedAgent = agents.find(a => a.theme_id);
208
+ const themedAgent = agentList.find(a => a.theme_id);
282
209
  if (themedAgent?.theme_id) {
283
210
  const theme = getTheme(workspacePath, themedAgent.theme_id);
284
211
  if (theme) {
@@ -293,7 +220,7 @@ export function getActiveTheme(workspacePath) {
293
220
  const themeNames = getThemeNames(workspacePath, theme.id);
294
221
  const themeNameSet = new Set(themeNames.map(n => n.name.toLowerCase()));
295
222
  // If any existing agent matches this theme's names
296
- const matchingAgent = agents.find(a => themeNameSet.has(a.name.toLowerCase()));
223
+ const matchingAgent = agentList.find(a => themeNameSet.has(a.name.toLowerCase()));
297
224
  if (matchingAgent) {
298
225
  // Auto-set it for future use
299
226
  setActiveTheme(workspacePath, theme.id);
@@ -306,82 +233,125 @@ export function getActiveTheme(workspacePath) {
306
233
  * Set the active theme for a workspace
307
234
  */
308
235
  export function setActiveTheme(workspacePath, themeId) {
309
- const db = openWorkspaceDatabase(workspacePath);
310
- if (themeId) {
311
- // Validate theme exists
312
- const theme = db.prepare('SELECT id FROM agent_themes WHERE id = ?').get(themeId);
313
- if (!theme) {
314
- db.close();
315
- throw new Error(`Theme "${themeId}" not found`);
236
+ withDrizzle(workspacePath, (ddb) => {
237
+ if (themeId) {
238
+ // Validate theme exists
239
+ const theme = ddb.select({ id: agentThemesTable.id })
240
+ .from(agentThemesTable)
241
+ .where(eq(agentThemesTable.id, themeId))
242
+ .get();
243
+ if (!theme) {
244
+ throw new Error(`Theme "${themeId}" not found`);
245
+ }
316
246
  }
317
- }
318
- db.prepare('UPDATE workspace SET active_theme_id = ? WHERE id = 1').run(themeId);
319
- db.close();
247
+ ddb.update(workspaceTable)
248
+ .set({ activeThemeId: themeId })
249
+ .where(eq(workspaceTable.id, 1))
250
+ .run();
251
+ });
320
252
  }
321
253
  /**
322
254
  * Add repositories to database
323
255
  */
324
256
  export function addRepositoriesToDatabase(workspacePath, repos) {
325
- const db = openWorkspaceDatabase(workspacePath);
326
- const insertRepo = db.prepare(`
327
- INSERT OR REPLACE INTO repositories (name, path, type, source_url, action, added_at)
328
- VALUES (?, ?, ?, ?, ?, ?)
329
- `);
330
- const transaction = db.transaction(() => {
257
+ withDrizzle(workspacePath, (ddb) => {
331
258
  for (const repo of repos) {
332
- insertRepo.run(repo.name, repo.path, 'main', repo.source_url || null, repo.action || null, new Date().toISOString());
259
+ ddb.insert(repositoriesTable)
260
+ .values({
261
+ name: repo.name,
262
+ path: repo.path,
263
+ type: 'main',
264
+ sourceUrl: repo.source_url || null,
265
+ action: repo.action || null,
266
+ addedAt: new Date().toISOString(),
267
+ })
268
+ .onConflictDoUpdate({
269
+ target: repositoriesTable.name,
270
+ set: {
271
+ path: repo.path,
272
+ type: 'main',
273
+ sourceUrl: repo.source_url || null,
274
+ action: repo.action || null,
275
+ addedAt: new Date().toISOString(),
276
+ },
277
+ })
278
+ .run();
333
279
  }
334
280
  });
335
- transaction();
336
- db.close();
337
281
  }
338
282
  /**
339
283
  * Add agents to database (case-insensitive uniqueness)
340
284
  */
341
285
  export function addAgentsToDatabase(workspacePath, agentNames, themeId, mountMode = 'worktree') {
342
- const db = openWorkspaceDatabase(workspacePath);
343
- // Check for existing agents (case-insensitive)
344
- const checkExisting = db.prepare('SELECT name FROM agents WHERE LOWER(name) = LOWER(?)');
345
- const insertAgent = db.prepare(`
346
- INSERT OR REPLACE INTO agents (name, type, base_name, theme_id, worktree_path, mount_mode, created_at)
347
- VALUES (?, ?, ?, ?, ?, ?, ?)
348
- `);
349
- const insertWorktree = db.prepare(`
350
- INSERT OR REPLACE INTO agent_worktrees (agent_name, repo_name, worktree_path, branch, created_at)
351
- VALUES (?, ?, ?, ?, ?)
352
- `);
353
- // Get workspace config to determine paths
354
- const workspace = db.prepare('SELECT * FROM workspace').get();
355
- // Get all repos for this workspace
356
- const repos = db.prepare('SELECT name FROM repositories').all();
357
- // Determine the effective theme ID (provided or active theme)
358
- const effectiveThemeId = themeId || workspace.active_theme_id || undefined;
359
- const persistentDir = getThemePersistentDir(effectiveThemeId);
360
- const transaction = db.transaction(() => {
361
- for (const agentName of agentNames) {
362
- // Skip if agent already exists (case-insensitive check)
363
- const existing = checkExisting.get(agentName);
364
- if (existing) {
365
- continue; // Agent already exists with same name (different case)
366
- }
367
- const now = new Date().toISOString();
368
- // Determine worktree path for the agent
369
- const agentWorktreePath = workspace.type === 'hq'
370
- ? `agents/${persistentDir}/${agentName}`
371
- : agentName;
372
- // Add agent (persistent type for manually added agents)
373
- insertAgent.run(agentName, 'persistent', null, effectiveThemeId || null, agentWorktreePath, mountMode, now);
374
- // Add worktrees for all repos
375
- for (const repo of repos) {
376
- const worktreePath = workspace.type === 'hq'
377
- ? `agents/${persistentDir}/${agentName}/${repo.name}`
378
- : `${agentName}/${repo.name}`;
379
- insertWorktree.run(agentName, repo.name, worktreePath, `agent-${agentName}`, now);
286
+ withDrizzle(workspacePath, (ddb, sqliteDb) => {
287
+ // Get workspace config to determine paths
288
+ const wsRow = ddb.select().from(workspaceTable).get();
289
+ if (!wsRow)
290
+ throw new Error('No workspace config found');
291
+ // Get all repos for this workspace
292
+ const repos = ddb.select({ name: repositoriesTable.name }).from(repositoriesTable).all();
293
+ // Determine the effective theme ID (provided or active theme)
294
+ const effectiveThemeId = themeId || wsRow.activeThemeId || undefined;
295
+ const persistentDir = getThemePersistentDir(effectiveThemeId);
296
+ const transaction = sqliteDb.transaction(() => {
297
+ for (const agentName of agentNames) {
298
+ // Check for existing agents (case-insensitive) via Drizzle sql
299
+ const existing = ddb.select({ name: agentsTable.name })
300
+ .from(agentsTable)
301
+ .where(sql `LOWER(${agentsTable.name}) = LOWER(${agentName})`)
302
+ .get();
303
+ if (existing) {
304
+ continue; // Agent already exists with same name (different case)
305
+ }
306
+ const now = new Date().toISOString();
307
+ // Determine worktree path for the agent
308
+ const agentWorktreePath = wsRow.type === 'hq'
309
+ ? `agents/${persistentDir}/${agentName}`
310
+ : agentName;
311
+ // Add agent (persistent type for manually added agents)
312
+ ddb.insert(agentsTable).values({
313
+ name: agentName,
314
+ type: 'persistent',
315
+ baseName: null,
316
+ themeId: effectiveThemeId || null,
317
+ worktreePath: agentWorktreePath,
318
+ mountMode,
319
+ createdAt: now,
320
+ }).onConflictDoUpdate({
321
+ target: agentsTable.name,
322
+ set: {
323
+ type: 'persistent',
324
+ baseName: null,
325
+ themeId: effectiveThemeId || null,
326
+ worktreePath: agentWorktreePath,
327
+ mountMode,
328
+ createdAt: now,
329
+ },
330
+ }).run();
331
+ // Add worktrees for all repos
332
+ for (const repo of repos) {
333
+ const worktreePath = wsRow.type === 'hq'
334
+ ? `agents/${persistentDir}/${agentName}/${repo.name}`
335
+ : `${agentName}/${repo.name}`;
336
+ ddb.insert(agentWorktreesTable).values({
337
+ agentName,
338
+ repoName: repo.name,
339
+ worktreePath,
340
+ branch: `agent-${agentName}`,
341
+ createdAt: now,
342
+ }).onConflictDoUpdate({
343
+ target: [agentWorktreesTable.agentName, agentWorktreesTable.repoName],
344
+ set: {
345
+ worktreePath,
346
+ branch: `agent-${agentName}`,
347
+ createdAt: now,
348
+ },
349
+ }).run();
350
+ }
380
351
  }
381
- }
352
+ });
353
+ transaction();
382
354
  });
383
- transaction();
384
- db.close();
385
355
  }
386
356
  /**
387
357
  * Add an ephemeral agent to the database.
@@ -403,26 +373,27 @@ export function addEphemeralAgentToDatabase(workspacePath, agentName, baseName,
403
373
  * simply gets null and can retry with a different name.
404
374
  */
405
375
  export function tryAddEphemeralAgentToDatabase(workspacePath, agentName, baseName, themeId, mountMode = 'worktree') {
406
- const db = openWorkspaceDatabase(workspacePath);
376
+ const sqliteDb = openWorkspaceDatabase(workspacePath);
377
+ const ddb = createDrizzleConnection(sqliteDb);
407
378
  try {
408
379
  const now = new Date().toISOString();
409
380
  const worktreePath = `agents/temp/${agentName}`;
410
- db.prepare(`
411
- INSERT INTO agents (name, type, status, base_name, theme_id, worktree_path, mount_mode, created_at)
412
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)
413
- `).run(agentName, 'ephemeral', 'active', baseName, themeId || null, worktreePath, mountMode, now);
414
- const agent = db.prepare('SELECT * FROM agents WHERE name = ?').get(agentName);
415
- return {
416
- name: agent.name,
417
- type: agent.type,
418
- status: agent.status,
419
- base_name: agent.base_name,
420
- theme_id: agent.theme_id,
421
- worktree_path: agent.worktree_path,
422
- mount_mode: (agent.mount_mode || 'clone'),
423
- created_at: agent.created_at,
424
- cleaned_at: agent.cleaned_at,
425
- };
381
+ ddb.insert(agentsTable).values({
382
+ name: agentName,
383
+ type: 'ephemeral',
384
+ status: 'active',
385
+ baseName,
386
+ themeId: themeId || null,
387
+ worktreePath,
388
+ mountMode,
389
+ createdAt: now,
390
+ }).run();
391
+ const agent = ddb.select().from(agentsTable)
392
+ .where(eq(agentsTable.name, agentName))
393
+ .get();
394
+ if (!agent)
395
+ return null;
396
+ return toAgent(agent);
426
397
  }
427
398
  catch (err) {
428
399
  const sqliteErr = err;
@@ -432,48 +403,50 @@ export function tryAddEphemeralAgentToDatabase(workspacePath, agentName, baseNam
432
403
  throw err;
433
404
  }
434
405
  finally {
435
- db.close();
406
+ sqliteDb.close();
436
407
  }
437
408
  }
438
409
  /**
439
410
  * Get all ephemeral agent names from the database
440
411
  */
441
412
  export function getEphemeralAgentNames(workspacePath) {
442
- const db = openWorkspaceDatabase(workspacePath);
443
- const agents = db.prepare("SELECT name FROM agents WHERE type = 'ephemeral'").all();
444
- db.close();
445
- return new Set(agents.map(a => a.name.toLowerCase()));
413
+ return withDrizzle(workspacePath, (ddb) => {
414
+ const rows = ddb.select({ name: agentsTable.name })
415
+ .from(agentsTable)
416
+ .where(eq(agentsTable.type, 'ephemeral'))
417
+ .all();
418
+ return new Set(rows.map(a => a.name.toLowerCase()));
419
+ });
446
420
  }
447
421
  /**
448
422
  * Remove an ephemeral agent from the database
449
423
  */
450
424
  export function removeEphemeralAgent(workspacePath, agentName) {
451
- const db = openWorkspaceDatabase(workspacePath);
452
- db.prepare("DELETE FROM agents WHERE name = ? AND type = 'ephemeral'").run(agentName);
453
- db.close();
425
+ withDrizzle(workspacePath, (ddb) => {
426
+ ddb.delete(agentsTable)
427
+ .where(and(eq(agentsTable.name, agentName), eq(agentsTable.type, 'ephemeral')))
428
+ .run();
429
+ });
454
430
  }
455
431
  /**
456
432
  * Get all agents in workspace
457
433
  */
458
434
  export function getWorkspaceAgents(workspacePath, includeCleanedUp = false) {
459
- const db = openWorkspaceDatabase(workspacePath);
460
- const query = includeCleanedUp
461
- ? 'SELECT * FROM agents ORDER BY created_at'
462
- : "SELECT * FROM agents WHERE status = 'active' OR status IS NULL ORDER BY created_at";
463
- const rows = db.prepare(query).all();
464
- db.close();
465
- // Map rows to Agent type, handling missing columns in old databases
466
- return rows.map(row => ({
467
- name: row.name,
468
- type: (row.type || 'persistent'),
469
- status: (row.status || 'active'),
470
- base_name: row.base_name,
471
- theme_id: row.theme_id,
472
- worktree_path: row.worktree_path,
473
- mount_mode: (row.mount_mode || 'worktree'),
474
- created_at: row.created_at,
475
- cleaned_at: row.cleaned_at,
476
- }));
435
+ return withDrizzle(workspacePath, (ddb) => {
436
+ let rows;
437
+ if (includeCleanedUp) {
438
+ rows = ddb.select().from(agentsTable)
439
+ .orderBy(asc(agentsTable.createdAt))
440
+ .all();
441
+ }
442
+ else {
443
+ rows = ddb.select().from(agentsTable)
444
+ .where(or(eq(agentsTable.status, 'active'), isNull(agentsTable.status)))
445
+ .orderBy(asc(agentsTable.createdAt))
446
+ .all();
447
+ }
448
+ return rows.map(toAgent);
449
+ });
477
450
  }
478
451
  /**
479
452
  * Get an agent by directory path.
@@ -490,38 +463,31 @@ export function getAgentByPath(workspacePath, absolutePath) {
490
463
  }
491
464
  // Get relative path from workspace root
492
465
  const relativePath = path.relative(normalizedWorkspace, normalizedPath);
493
- const db = openWorkspaceDatabase(workspacePath);
494
- const agents = db.prepare("SELECT * FROM agents WHERE status = 'active' OR status IS NULL").all();
495
- db.close();
496
- // Find agent whose worktree_path matches or contains the relative path
497
- for (const row of agents) {
498
- if (row.worktree_path) {
499
- // Check if relativePath starts with or equals the agent's worktree_path
500
- if (relativePath === row.worktree_path || relativePath.startsWith(row.worktree_path + '/')) {
501
- return {
502
- name: row.name,
503
- type: (row.type || 'persistent'),
504
- status: (row.status || 'active'),
505
- base_name: row.base_name,
506
- theme_id: row.theme_id,
507
- worktree_path: row.worktree_path,
508
- mount_mode: (row.mount_mode || 'worktree'),
509
- created_at: row.created_at,
510
- cleaned_at: row.cleaned_at,
511
- };
466
+ return withDrizzle(workspacePath, (ddb) => {
467
+ const rows = ddb.select().from(agentsTable)
468
+ .where(or(eq(agentsTable.status, 'active'), isNull(agentsTable.status)))
469
+ .all();
470
+ // Find agent whose worktree_path matches or contains the relative path
471
+ for (const row of rows) {
472
+ if (row.worktreePath) {
473
+ if (relativePath === row.worktreePath || relativePath.startsWith(row.worktreePath + '/')) {
474
+ return toAgent(row);
475
+ }
512
476
  }
513
477
  }
514
- }
515
- return null;
478
+ return null;
479
+ });
516
480
  }
517
481
  /**
518
482
  * Mark an agent as cleaned up (keeps the record for history)
519
483
  */
520
484
  export function markAgentCleaned(workspacePath, agentName) {
521
- const db = openWorkspaceDatabase(workspacePath);
522
- const now = new Date().toISOString();
523
- db.prepare("UPDATE agents SET status = 'cleaned', cleaned_at = ? WHERE name = ?").run(now, agentName);
524
- db.close();
485
+ withDrizzle(workspacePath, (ddb) => {
486
+ ddb.update(agentsTable)
487
+ .set({ status: 'cleaned', cleanedAt: new Date().toISOString() })
488
+ .where(eq(agentsTable.name, agentName))
489
+ .run();
490
+ });
525
491
  }
526
492
  /**
527
493
  * Sync agents in database with what exists on disk.
@@ -529,9 +495,9 @@ export function markAgentCleaned(workspacePath, agentName) {
529
495
  * Returns list of agents that were cleaned up.
530
496
  */
531
497
  export function syncAgentsWithDisk(workspacePath) {
532
- const agents = getWorkspaceAgents(workspacePath, false); // Only active agents
498
+ const agentList = getWorkspaceAgents(workspacePath, false); // Only active agents
533
499
  const cleanedAgents = [];
534
- for (const agent of agents) {
500
+ for (const agent of agentList) {
535
501
  // Determine expected directory path
536
502
  let agentDir;
537
503
  if (agent.worktree_path) {
@@ -565,9 +531,8 @@ export function discoverAgentsOnDisk(workspacePath) {
565
531
  const activeNames = new Set(activeAgents.map(a => a.name.toLowerCase()));
566
532
  // Get ALL agents including cleaned (for reactivation)
567
533
  const allAgents = getWorkspaceAgents(workspacePath, true);
568
- const cleanedAgents = new Map(allAgents.filter(a => a.status === 'cleaned').map(a => [a.name.toLowerCase(), a]));
569
- const db = openWorkspaceDatabase(workspacePath);
570
- try {
534
+ const cleanedAgentsMap = new Map(allAgents.filter(a => a.status === 'cleaned').map(a => [a.name.toLowerCase(), a]));
535
+ withDrizzle(workspacePath, (ddb) => {
571
536
  // Scan staff directory
572
537
  const staffDir = path.join(workspacePath, 'agents', 'staff');
573
538
  if (fs.existsSync(staffDir)) {
@@ -578,21 +543,24 @@ export function discoverAgentsOnDisk(workspacePath) {
578
543
  if (!activeNames.has(nameLower)) {
579
544
  const worktreePath = `agents/staff/${entry.name}`;
580
545
  const now = new Date().toISOString();
581
- // Check if this is a cleaned agent that should be reactivated
582
- const cleanedAgent = cleanedAgents.get(nameLower);
546
+ const cleanedAgent = cleanedAgentsMap.get(nameLower);
583
547
  if (cleanedAgent) {
584
548
  // Reactivate the cleaned agent
585
- db.prepare(`
586
- UPDATE agents SET status = 'active', cleaned_at = NULL, worktree_path = ?
587
- WHERE LOWER(name) = LOWER(?)
588
- `).run(worktreePath, entry.name);
549
+ ddb.update(agentsTable)
550
+ .set({ status: 'active', cleanedAt: null, worktreePath })
551
+ .where(sql `LOWER(${agentsTable.name}) = LOWER(${entry.name})`)
552
+ .run();
589
553
  }
590
554
  else {
591
- // Register new agent - discovered agents default to 'worktree' mode (legacy behavior)
592
- db.prepare(`
593
- INSERT INTO agents (name, type, status, worktree_path, mount_mode, created_at)
594
- VALUES (?, 'persistent', 'active', ?, 'worktree', ?)
595
- `).run(entry.name, worktreePath, now);
555
+ // Register new agent
556
+ ddb.insert(agentsTable).values({
557
+ name: entry.name,
558
+ type: 'persistent',
559
+ status: 'active',
560
+ worktreePath,
561
+ mountMode: 'worktree',
562
+ createdAt: now,
563
+ }).run();
596
564
  }
597
565
  result.discovered.push({ name: entry.name, type: 'persistent', path: worktreePath });
598
566
  activeNames.add(nameLower);
@@ -610,21 +578,24 @@ export function discoverAgentsOnDisk(workspacePath) {
610
578
  if (!activeNames.has(nameLower)) {
611
579
  const worktreePath = `agents/temp/${entry.name}`;
612
580
  const now = new Date().toISOString();
613
- // Check if this is a cleaned agent that should be reactivated
614
- const cleanedAgent = cleanedAgents.get(nameLower);
581
+ const cleanedAgent = cleanedAgentsMap.get(nameLower);
615
582
  if (cleanedAgent) {
616
583
  // Reactivate the cleaned agent
617
- db.prepare(`
618
- UPDATE agents SET status = 'active', cleaned_at = NULL, worktree_path = ?
619
- WHERE LOWER(name) = LOWER(?)
620
- `).run(worktreePath, entry.name);
584
+ ddb.update(agentsTable)
585
+ .set({ status: 'active', cleanedAt: null, worktreePath })
586
+ .where(sql `LOWER(${agentsTable.name}) = LOWER(${entry.name})`)
587
+ .run();
621
588
  }
622
589
  else {
623
- // Register new agent - discovered agents default to 'worktree' mode (legacy behavior)
624
- db.prepare(`
625
- INSERT INTO agents (name, type, status, worktree_path, mount_mode, created_at)
626
- VALUES (?, 'ephemeral', 'active', ?, 'worktree', ?)
627
- `).run(entry.name, worktreePath, now);
590
+ // Register new agent
591
+ ddb.insert(agentsTable).values({
592
+ name: entry.name,
593
+ type: 'ephemeral',
594
+ status: 'active',
595
+ worktreePath,
596
+ mountMode: 'worktree',
597
+ createdAt: now,
598
+ }).run();
628
599
  }
629
600
  result.discovered.push({ name: entry.name, type: 'ephemeral', path: worktreePath });
630
601
  activeNames.add(nameLower);
@@ -632,79 +603,123 @@ export function discoverAgentsOnDisk(workspacePath) {
632
603
  }
633
604
  }
634
605
  }
635
- }
636
- finally {
637
- db.close();
638
- }
606
+ });
639
607
  return result;
640
608
  }
641
609
  /**
642
610
  * Get all repositories in workspace
643
611
  */
644
612
  export function getWorkspaceRepositories(workspacePath) {
645
- const db = openWorkspaceDatabase(workspacePath);
646
- const repos = db.prepare('SELECT * FROM repositories ORDER BY added_at').all();
647
- db.close();
648
- return repos;
613
+ return withDrizzle(workspacePath, (ddb) => {
614
+ const rows = ddb.select().from(repositoriesTable)
615
+ .orderBy(asc(repositoriesTable.addedAt))
616
+ .all();
617
+ return rows.map(row => ({
618
+ name: row.name,
619
+ path: row.path,
620
+ type: (row.type || 'main'),
621
+ source_url: row.sourceUrl ?? undefined,
622
+ action: (row.action ?? undefined),
623
+ added_at: row.addedAt,
624
+ }));
625
+ });
649
626
  }
650
627
  /**
651
628
  * Get worktrees for a specific agent
652
629
  */
653
630
  export function getAgentWorktrees(workspacePath, agentName) {
654
- const db = openWorkspaceDatabase(workspacePath);
655
- const worktrees = db.prepare('SELECT * FROM agent_worktrees WHERE agent_name = ?').all(agentName);
656
- db.close();
657
- return worktrees;
631
+ return withDrizzle(workspacePath, (ddb) => {
632
+ const rows = ddb.select().from(agentWorktreesTable)
633
+ .where(eq(agentWorktreesTable.agentName, agentName))
634
+ .all();
635
+ return rows.map(row => ({
636
+ agent_name: row.agentName,
637
+ repo_name: row.repoName,
638
+ worktree_path: row.worktreePath,
639
+ branch: row.branch,
640
+ created_at: row.createdAt,
641
+ last_commit_hash: row.lastCommitHash ?? undefined,
642
+ commits_ahead: row.commitsAhead,
643
+ is_clean: Boolean(row.isClean),
644
+ last_checked: row.lastChecked ?? undefined,
645
+ }));
646
+ });
658
647
  }
659
648
  /**
660
649
  * Find agent worktrees matching a branch pattern (case-insensitive LIKE).
661
650
  */
662
651
  export function findWorktreesByBranch(workspacePath, branchPattern) {
663
- const db = openWorkspaceDatabase(workspacePath);
664
- const worktrees = db.prepare('SELECT * FROM agent_worktrees WHERE LOWER(branch) LIKE ?').all(branchPattern);
665
- db.close();
666
- return worktrees;
652
+ return withDrizzle(workspacePath, (ddb) => {
653
+ const rows = ddb.select().from(agentWorktreesTable)
654
+ .where(like(sql `LOWER(${agentWorktreesTable.branch})`, branchPattern))
655
+ .all();
656
+ return rows.map(row => ({
657
+ agent_name: row.agentName,
658
+ repo_name: row.repoName,
659
+ worktree_path: row.worktreePath,
660
+ branch: row.branch,
661
+ created_at: row.createdAt,
662
+ last_commit_hash: row.lastCommitHash ?? undefined,
663
+ commits_ahead: row.commitsAhead,
664
+ is_clean: Boolean(row.isClean),
665
+ last_checked: row.lastChecked ?? undefined,
666
+ }));
667
+ });
667
668
  }
668
669
  /**
669
670
  * Get agent worktrees for a specific repository.
670
671
  */
671
672
  export function getWorktreesForRepo(workspacePath, repoName) {
672
- const db = openWorkspaceDatabase(workspacePath);
673
- const worktrees = db.prepare('SELECT agent_name, is_clean, commits_ahead, branch FROM agent_worktrees WHERE repo_name = ?').all(repoName);
674
- db.close();
675
- return worktrees;
673
+ return withDrizzle(workspacePath, (ddb) => {
674
+ const rows = ddb.select({
675
+ agent_name: agentWorktreesTable.agentName,
676
+ is_clean: sql `${agentWorktreesTable.isClean}`,
677
+ commits_ahead: agentWorktreesTable.commitsAhead,
678
+ branch: agentWorktreesTable.branch,
679
+ }).from(agentWorktreesTable)
680
+ .where(eq(agentWorktreesTable.repoName, repoName))
681
+ .all();
682
+ return rows;
683
+ });
676
684
  }
677
685
  /**
678
686
  * Upsert a workspace setting (key-value pair).
679
687
  */
680
688
  export function upsertWorkspaceSetting(db, key, value) {
681
- db.prepare(`
682
- INSERT INTO workspace_settings (key, value)
683
- VALUES (?, ?)
684
- ON CONFLICT(key) DO UPDATE SET value = excluded.value
685
- `).run(key, value);
689
+ const ddb = createDrizzleConnection(db);
690
+ ddb.insert(workspaceSettingsTable)
691
+ .values({ key, value })
692
+ .onConflictDoUpdate({
693
+ target: workspaceSettingsTable.key,
694
+ set: { value },
695
+ })
696
+ .run();
686
697
  }
687
698
  /**
688
699
  * Remove agents from database
689
700
  */
690
701
  export function removeAgentsFromDatabase(workspacePath, agentNames) {
691
- const db = openWorkspaceDatabase(workspacePath);
692
- const deleteAgent = db.prepare('DELETE FROM agents WHERE name = ?');
693
- // Note: agent_worktrees will be deleted automatically due to CASCADE
694
- const transaction = db.transaction(() => {
695
- for (const agentName of agentNames) {
696
- deleteAgent.run(agentName);
697
- }
702
+ withDrizzle(workspacePath, (ddb, sqliteDb) => {
703
+ // Note: agent_worktrees will be deleted automatically due to CASCADE
704
+ const transaction = sqliteDb.transaction(() => {
705
+ for (const agentName of agentNames) {
706
+ ddb.delete(agentsTable)
707
+ .where(eq(agentsTable.name, agentName))
708
+ .run();
709
+ }
710
+ });
711
+ transaction();
698
712
  });
699
- transaction();
700
- db.close();
701
713
  }
702
714
  // =============================================================================
703
715
  // PMO Bootstrapping Operations
716
+ // Raw SQL is required here because these operate before migrations run
717
+ // or perform DDL operations that Drizzle doesn't support.
704
718
  // =============================================================================
705
719
  /**
706
720
  * Check if PMO tables exist and get basic stats.
707
721
  * Used by pmo init to detect existing PMO before storage layer is available.
722
+ * Raw SQL: uses sqlite_master introspection (pre-migration bootstrap).
708
723
  */
709
724
  export function checkPMOExists(dbPath) {
710
725
  let db;
@@ -735,6 +750,7 @@ export function checkPMOExists(dbPath) {
735
750
  /**
736
751
  * Get a PMO setting from the pmo_settings table.
737
752
  * Used for bootstrapping queries before storage layer is available.
753
+ * Raw SQL: pre-migration bootstrap query.
738
754
  */
739
755
  export function getPMOSetting(dbPath, key) {
740
756
  let db;
@@ -759,6 +775,7 @@ export function getPMOSetting(dbPath, key) {
759
775
  /**
760
776
  * Drop PMO tables from the database.
761
777
  * Used during PMO reinitialization.
778
+ * Raw SQL: DDL operations (DROP TABLE) are not supported by Drizzle.
762
779
  */
763
780
  export function dropPMOTables(dbPath, tables) {
764
781
  let db;
@@ -790,61 +807,79 @@ export function dropPMOTables(dbPath, tables) {
790
807
  * Get all themes
791
808
  */
792
809
  export function getThemes(workspacePath) {
793
- const db = openWorkspaceDatabase(workspacePath);
794
- const themes = db.prepare('SELECT * FROM agent_themes ORDER BY builtin DESC, name').all();
795
- db.close();
796
- return themes;
810
+ return withDrizzle(workspacePath, (ddb) => {
811
+ const rows = ddb.select().from(agentThemesTable)
812
+ .orderBy(desc(agentThemesTable.builtin), asc(agentThemesTable.name))
813
+ .all();
814
+ return rows.map(toAgentTheme);
815
+ });
797
816
  }
798
817
  /**
799
818
  * Get a theme by ID
800
819
  */
801
820
  export function getTheme(workspacePath, themeId) {
802
- const db = openWorkspaceDatabase(workspacePath);
803
- const theme = db.prepare('SELECT * FROM agent_themes WHERE id = ?').get(themeId);
804
- db.close();
805
- return theme || null;
821
+ return withDrizzle(workspacePath, (ddb) => {
822
+ const row = ddb.select().from(agentThemesTable)
823
+ .where(eq(agentThemesTable.id, themeId))
824
+ .get();
825
+ return row ? toAgentTheme(row) : null;
826
+ });
806
827
  }
807
828
  /**
808
829
  * Create a new theme
809
830
  */
810
831
  export function createTheme(workspacePath, theme) {
811
- const db = openWorkspaceDatabase(workspacePath);
812
- const now = new Date().toISOString();
813
- db.prepare(`
814
- INSERT INTO agent_themes (id, name, display_name, description, builtin, created_at)
815
- VALUES (?, ?, ?, ?, ?, ?)
816
- `).run(theme.id, theme.name, theme.displayName, theme.description || null, theme.builtin ? 1 : 0, now);
817
- const created = db.prepare('SELECT * FROM agent_themes WHERE id = ?').get(theme.id);
818
- db.close();
819
- return created;
832
+ return withDrizzle(workspacePath, (ddb) => {
833
+ const now = new Date().toISOString();
834
+ ddb.insert(agentThemesTable).values({
835
+ id: theme.id,
836
+ name: theme.name,
837
+ displayName: theme.displayName,
838
+ description: theme.description || null,
839
+ builtin: theme.builtin || false,
840
+ createdAt: now,
841
+ }).run();
842
+ const created = ddb.select().from(agentThemesTable)
843
+ .where(eq(agentThemesTable.id, theme.id))
844
+ .get();
845
+ return toAgentTheme(created);
846
+ });
820
847
  }
821
848
  /**
822
849
  * Delete a theme (cannot delete builtin themes)
823
850
  */
824
851
  export function deleteTheme(workspacePath, themeId) {
825
- const db = openWorkspaceDatabase(workspacePath);
826
- // Check if builtin
827
- const theme = db.prepare('SELECT builtin FROM agent_themes WHERE id = ?').get(themeId);
828
- if (!theme) {
829
- db.close();
830
- return false;
831
- }
832
- if (theme.builtin) {
833
- db.close();
834
- throw new Error('Cannot delete built-in themes');
835
- }
836
- db.prepare('DELETE FROM agent_themes WHERE id = ?').run(themeId);
837
- db.close();
838
- return true;
852
+ return withDrizzle(workspacePath, (ddb) => {
853
+ const theme = ddb.select({ builtin: agentThemesTable.builtin })
854
+ .from(agentThemesTable)
855
+ .where(eq(agentThemesTable.id, themeId))
856
+ .get();
857
+ if (!theme) {
858
+ return false;
859
+ }
860
+ if (theme.builtin) {
861
+ throw new Error('Cannot delete built-in themes');
862
+ }
863
+ ddb.delete(agentThemesTable)
864
+ .where(eq(agentThemesTable.id, themeId))
865
+ .run();
866
+ return true;
867
+ });
839
868
  }
840
869
  /**
841
870
  * Get names for a theme
842
871
  */
843
872
  export function getThemeNames(workspacePath, themeId) {
844
- const db = openWorkspaceDatabase(workspacePath);
845
- const names = db.prepare('SELECT * FROM agent_theme_names WHERE theme_id = ? ORDER BY name').all(themeId);
846
- db.close();
847
- return names;
873
+ return withDrizzle(workspacePath, (ddb) => {
874
+ const rows = ddb.select().from(agentThemeNamesTable)
875
+ .where(eq(agentThemeNamesTable.themeId, themeId))
876
+ .orderBy(asc(agentThemeNamesTable.name))
877
+ .all();
878
+ return rows.map(row => ({
879
+ theme_id: row.themeId,
880
+ name: row.name,
881
+ }));
882
+ });
848
883
  }
849
884
  /**
850
885
  * Get available names for a theme.
@@ -853,141 +888,183 @@ export function getThemeNames(workspacePath, themeId) {
853
888
  * 2. The agent exists but its worktree directory is missing (manually deleted)
854
889
  */
855
890
  export function getAvailableThemeNames(workspacePath, themeId) {
856
- const db = openWorkspaceDatabase(workspacePath);
857
- // Get all theme names
858
- const names = db.prepare('SELECT name FROM agent_theme_names WHERE theme_id = ? ORDER BY name').all(themeId);
859
- // Get existing staff agents with their worktree paths (persistent type only)
860
- const existingAgents = db.prepare(`
861
- SELECT LOWER(name) as name, worktree_path
862
- FROM agents
863
- WHERE type = 'persistent' AND (status = 'active' OR status IS NULL)
864
- `).all();
865
- db.close();
866
- // Build a set of names that are truly in use (agent exists AND worktree exists)
867
- const inUseNames = new Set();
868
- for (const agent of existingAgents) {
869
- if (agent.worktree_path) {
870
- const fullPath = path.join(workspacePath, agent.worktree_path);
871
- if (fs.existsSync(fullPath)) {
891
+ return withDrizzle(workspacePath, (ddb) => {
892
+ // Get all theme names
893
+ const names = ddb.select({ name: agentThemeNamesTable.name })
894
+ .from(agentThemeNamesTable)
895
+ .where(eq(agentThemeNamesTable.themeId, themeId))
896
+ .orderBy(asc(agentThemeNamesTable.name))
897
+ .all();
898
+ // Get existing staff agents with their worktree paths (persistent type only)
899
+ const existingAgents = ddb.select({
900
+ name: sql `LOWER(${agentsTable.name})`,
901
+ worktreePath: agentsTable.worktreePath,
902
+ })
903
+ .from(agentsTable)
904
+ .where(and(eq(agentsTable.type, 'persistent'), or(eq(agentsTable.status, 'active'), isNull(agentsTable.status))))
905
+ .all();
906
+ // Build a set of names that are truly in use (agent exists AND worktree exists)
907
+ const inUseNames = new Set();
908
+ for (const agent of existingAgents) {
909
+ if (agent.worktreePath) {
910
+ const fullPath = path.join(workspacePath, agent.worktreePath);
911
+ if (fs.existsSync(fullPath)) {
912
+ inUseNames.add(agent.name);
913
+ }
914
+ }
915
+ else {
916
+ // No worktree path means we can't verify - treat as in use to be safe
872
917
  inUseNames.add(agent.name);
873
918
  }
874
919
  }
875
- else {
876
- // No worktree path means we can't verify - treat as in use to be safe
877
- inUseNames.add(agent.name);
878
- }
879
- }
880
- // Filter out names that are truly in use
881
- return names
882
- .map(n => n.name)
883
- .filter(name => !inUseNames.has(name.toLowerCase()));
920
+ // Filter out names that are truly in use
921
+ return names
922
+ .map(n => n.name)
923
+ .filter(name => !inUseNames.has(name.toLowerCase()));
924
+ });
884
925
  }
885
926
  /**
886
927
  * Add names to a theme (case-insensitive uniqueness)
887
928
  */
888
929
  export function addThemeNames(workspacePath, themeId, names) {
889
- const db = openWorkspaceDatabase(workspacePath);
890
- // Check for existing name (case-insensitive)
891
- const checkExisting = db.prepare('SELECT name FROM agent_theme_names WHERE theme_id = ? AND LOWER(name) = LOWER(?)');
892
- const insertName = db.prepare(`
893
- INSERT INTO agent_theme_names (theme_id, name)
894
- VALUES (?, ?)
895
- `);
896
- const transaction = db.transaction(() => {
897
- for (const name of names) {
898
- // Skip if name already exists (case-insensitive)
899
- const existing = checkExisting.get(themeId, name);
900
- if (existing) {
901
- continue;
930
+ withDrizzle(workspacePath, (ddb, sqliteDb) => {
931
+ const transaction = sqliteDb.transaction(() => {
932
+ for (const name of names) {
933
+ // Check for existing name (case-insensitive)
934
+ const existing = ddb.select({ name: agentThemeNamesTable.name })
935
+ .from(agentThemeNamesTable)
936
+ .where(and(eq(agentThemeNamesTable.themeId, themeId), sql `LOWER(${agentThemeNamesTable.name}) = LOWER(${name})`))
937
+ .get();
938
+ if (existing) {
939
+ continue;
940
+ }
941
+ ddb.insert(agentThemeNamesTable).values({
942
+ themeId,
943
+ name,
944
+ }).run();
902
945
  }
903
- insertName.run(themeId, name);
904
- }
946
+ });
947
+ transaction();
905
948
  });
906
- transaction();
907
- db.close();
908
949
  }
909
950
  /**
910
951
  * Add a media item to the database
911
952
  */
912
953
  export function addMediaItemToDatabase(workspacePath, item) {
913
- const db = openWorkspaceDatabase(workspacePath);
914
- db.prepare(`
915
- INSERT OR REPLACE INTO media_items (name, path, source_path, media_type, frame_interval, added_at)
916
- VALUES (?, ?, ?, ?, ?, ?)
917
- `).run(item.name, item.path, item.source_path || null, item.media_type, item.frame_interval || 30, new Date().toISOString());
918
- db.close();
954
+ withDrizzle(workspacePath, (ddb) => {
955
+ const now = new Date().toISOString();
956
+ ddb.insert(mediaItemsTable)
957
+ .values({
958
+ name: item.name,
959
+ path: item.path,
960
+ sourcePath: item.source_path || null,
961
+ mediaType: item.media_type,
962
+ frameInterval: item.frame_interval || 30,
963
+ addedAt: now,
964
+ })
965
+ .onConflictDoUpdate({
966
+ target: mediaItemsTable.name,
967
+ set: {
968
+ path: item.path,
969
+ sourcePath: item.source_path || null,
970
+ mediaType: item.media_type,
971
+ frameInterval: item.frame_interval || 30,
972
+ addedAt: now,
973
+ },
974
+ })
975
+ .run();
976
+ });
919
977
  }
920
978
  /**
921
979
  * Update media item after preprocessing
922
980
  */
923
981
  export function updateMediaItemStatus(workspacePath, name, updates) {
924
- const db = openWorkspaceDatabase(workspacePath);
925
- const sets = ['status = ?'];
926
- const values = [updates.status];
927
- if (updates.duration_seconds !== undefined) {
928
- sets.push('duration_seconds = ?');
929
- values.push(updates.duration_seconds);
930
- }
931
- if (updates.resolution !== undefined) {
932
- sets.push('resolution = ?');
933
- values.push(updates.resolution);
934
- }
935
- if (updates.frame_count !== undefined) {
936
- sets.push('frame_count = ?');
937
- values.push(updates.frame_count);
938
- }
939
- if (updates.has_transcript !== undefined) {
940
- sets.push('has_transcript = ?');
941
- values.push(updates.has_transcript ? 1 : 0);
942
- }
943
- if (updates.error_message !== undefined) {
944
- sets.push('error_message = ?');
945
- values.push(updates.error_message);
946
- }
947
- if (updates.status === 'ready' || updates.status === 'error') {
948
- sets.push('processed_at = ?');
949
- values.push(new Date().toISOString());
950
- }
951
- values.push(name);
952
- db.prepare(`UPDATE media_items SET ${sets.join(', ')} WHERE name = ?`).run(...values);
953
- db.close();
982
+ withDrizzle(workspacePath, (ddb) => {
983
+ const setValues = { status: updates.status };
984
+ if (updates.duration_seconds !== undefined) {
985
+ setValues.durationSeconds = updates.duration_seconds;
986
+ }
987
+ if (updates.resolution !== undefined) {
988
+ setValues.resolution = updates.resolution;
989
+ }
990
+ if (updates.frame_count !== undefined) {
991
+ setValues.frameCount = updates.frame_count;
992
+ }
993
+ if (updates.has_transcript !== undefined) {
994
+ setValues.hasTranscript = updates.has_transcript;
995
+ }
996
+ if (updates.error_message !== undefined) {
997
+ setValues.errorMessage = updates.error_message;
998
+ }
999
+ if (updates.status === 'ready' || updates.status === 'error') {
1000
+ setValues.processedAt = new Date().toISOString();
1001
+ }
1002
+ ddb.update(mediaItemsTable)
1003
+ .set(setValues)
1004
+ .where(eq(mediaItemsTable.name, name))
1005
+ .run();
1006
+ });
954
1007
  }
955
1008
  /**
956
1009
  * Get all media items in workspace
957
1010
  */
958
1011
  export function getWorkspaceMediaItems(workspacePath) {
959
- const db = openWorkspaceDatabase(workspacePath);
960
- const rows = db.prepare('SELECT * FROM media_items ORDER BY added_at').all();
961
- db.close();
962
- return rows.map(row => ({
963
- ...row,
964
- media_type: row.media_type,
965
- has_transcript: Boolean(row.has_transcript),
966
- status: row.status,
967
- }));
1012
+ return withDrizzle(workspacePath, (ddb) => {
1013
+ const rows = ddb.select().from(mediaItemsTable)
1014
+ .orderBy(asc(mediaItemsTable.addedAt))
1015
+ .all();
1016
+ return rows.map(row => ({
1017
+ name: row.name,
1018
+ path: row.path,
1019
+ source_path: row.sourcePath,
1020
+ media_type: row.mediaType,
1021
+ duration_seconds: row.durationSeconds,
1022
+ resolution: row.resolution,
1023
+ frame_count: row.frameCount,
1024
+ has_transcript: Boolean(row.hasTranscript),
1025
+ frame_interval: row.frameInterval,
1026
+ status: row.status,
1027
+ error_message: row.errorMessage,
1028
+ added_at: row.addedAt,
1029
+ processed_at: row.processedAt,
1030
+ }));
1031
+ });
968
1032
  }
969
1033
  /**
970
1034
  * Get a single media item by name
971
1035
  */
972
1036
  export function getMediaItem(workspacePath, name) {
973
- const db = openWorkspaceDatabase(workspacePath);
974
- const row = db.prepare('SELECT * FROM media_items WHERE name = ?').get(name);
975
- db.close();
976
- if (!row)
977
- return null;
978
- return {
979
- ...row,
980
- media_type: row.media_type,
981
- has_transcript: Boolean(row.has_transcript),
982
- status: row.status,
983
- };
1037
+ return withDrizzle(workspacePath, (ddb) => {
1038
+ const row = ddb.select().from(mediaItemsTable)
1039
+ .where(eq(mediaItemsTable.name, name))
1040
+ .get();
1041
+ if (!row)
1042
+ return null;
1043
+ return {
1044
+ name: row.name,
1045
+ path: row.path,
1046
+ source_path: row.sourcePath,
1047
+ media_type: row.mediaType,
1048
+ duration_seconds: row.durationSeconds,
1049
+ resolution: row.resolution,
1050
+ frame_count: row.frameCount,
1051
+ has_transcript: Boolean(row.hasTranscript),
1052
+ frame_interval: row.frameInterval,
1053
+ status: row.status,
1054
+ error_message: row.errorMessage,
1055
+ added_at: row.addedAt,
1056
+ processed_at: row.processedAt,
1057
+ };
1058
+ });
984
1059
  }
985
1060
  /**
986
1061
  * Remove a media item from the database
987
1062
  */
988
1063
  export function removeMediaItemFromDatabase(workspacePath, name) {
989
- const db = openWorkspaceDatabase(workspacePath);
990
- db.prepare('DELETE FROM media_items WHERE name = ?').run(name);
991
- db.close();
1064
+ withDrizzle(workspacePath, (ddb) => {
1065
+ ddb.delete(mediaItemsTable)
1066
+ .where(eq(mediaItemsTable.name, name))
1067
+ .run();
1068
+ });
992
1069
  }
993
1070
  //# sourceMappingURL=index.js.map