@xopcai/xopc 0.0.126 → 0.0.128

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 (366) hide show
  1. package/dist/browser-ext/manifest.json +1 -1
  2. package/dist/extensions/telegram/xopc.extension.json +1 -1
  3. package/dist/gateway/static/root/assets/{agent-avatar-dicebear-C70vgt60.js → agent-avatar-dicebear-Dx3HQkOk.js} +1 -1
  4. package/dist/gateway/static/root/assets/agents-Ceb_WmSZ.js +2 -0
  5. package/dist/gateway/static/root/assets/app-management-settings-panel-DGtoXVvI.js +1 -0
  6. package/dist/gateway/static/root/assets/{appearance-settings-BHlahIDL.js → appearance-settings-BQQf_iLu.js} +1 -1
  7. package/dist/gateway/static/root/assets/apps-page-D05daQF8.js +1 -0
  8. package/dist/gateway/static/root/assets/{archive-plugin-B-aoIt0w.js → archive-plugin-HuCFa2UL.js} +1 -1
  9. package/dist/gateway/static/root/assets/{attachment-load-BD0G0FD5.js → attachment-load-N3csiRso.js} +1 -1
  10. package/dist/gateway/static/root/assets/{automation-suggestion-card-iJRVI7aZ.js → automation-suggestion-card-DeYEW2YQ.js} +1 -1
  11. package/dist/gateway/static/root/assets/automations-page-Dxn5-hGx.js +4 -0
  12. package/dist/gateway/static/root/assets/{binary-plugins-B0mNlVSz.js → binary-plugins-DnJO0VG7.js} +1 -1
  13. package/dist/gateway/static/root/assets/{block-editor-CJY9Lu5G.js → block-editor-DOAZkqfs.js} +27 -27
  14. package/dist/gateway/static/root/assets/browser-settings-page-DSB-ZKIX.js +8 -0
  15. package/dist/gateway/static/root/assets/capability-presets-api-DRCu-s6d.js +1 -0
  16. package/dist/gateway/static/root/assets/capability-presets-settings-panel-M7jOJxZX.js +1 -0
  17. package/dist/gateway/static/root/assets/channel-recipient-api-BrW5Kcgc.js +1 -0
  18. package/dist/gateway/static/root/assets/channels-settings-C1iggCG3.js +1 -0
  19. package/dist/gateway/static/root/assets/{code-editor-gbRgS14t.js → code-editor--tMO38Tr.js} +1 -1
  20. package/dist/gateway/static/root/assets/connectors-page-Df0TJNgv.js +2 -0
  21. package/dist/gateway/static/root/assets/desktop-pet-MnqKnEgd.js +1 -0
  22. package/dist/gateway/static/root/assets/desktop-pet-settings-PRnTI9no.js +1 -0
  23. package/dist/gateway/static/root/assets/desktop-pet-sprite-DqZ6fbIj.js +1 -0
  24. package/dist/gateway/static/root/assets/directory-picker-path-field-DJNFtSWq.js +1 -0
  25. package/dist/gateway/static/root/assets/dreaming-settings-D0_2JavX.js +3 -0
  26. package/dist/gateway/static/root/assets/{extension-debug-page-BbyczVZ3.js → extension-debug-page-C4sQuETO.js} +1 -1
  27. package/dist/gateway/static/root/assets/{extension-page-DLN1I3DR.js → extension-page-DhnpWAUJ.js} +1 -1
  28. package/dist/gateway/static/root/assets/extension-settings-page-D7JDxFGh.js +1 -0
  29. package/dist/gateway/static/root/assets/fetch-BMjlW0g1.js +1 -0
  30. package/dist/gateway/static/root/assets/gateway-config-api-B_VyPdKh.js +1 -0
  31. package/dist/gateway/static/root/assets/gateway-settings-C0TutwM5.js +1 -0
  32. package/dist/gateway/static/root/assets/{gateway-startup-retry-DEMad2wt.js → gateway-startup-retry-CY5W2Pp4.js} +1 -1
  33. package/dist/gateway/static/root/assets/goal-create-dialog-DrDGK2L5.js +1 -0
  34. package/dist/gateway/static/root/assets/goal-detail-page-aaIMRRor.js +4 -0
  35. package/dist/gateway/static/root/assets/goals-config-api-CzTu01a1.js +1 -0
  36. package/dist/gateway/static/root/assets/goals-page-CBfCPiNK.js +2 -0
  37. package/dist/gateway/static/root/assets/goals-settings-4rKUC3EH.js +1 -0
  38. package/dist/gateway/static/root/assets/heartbeat-settings-DqJBKlza.js +1 -0
  39. package/dist/gateway/static/root/assets/{html-workspace-editor-DV90ddkE.js → html-workspace-editor-COqmtESs.js} +1 -1
  40. package/dist/gateway/static/root/assets/index-38TSE7ng.js +257 -0
  41. package/dist/gateway/static/root/assets/index-EKmpPvZj.css +1 -0
  42. package/dist/gateway/static/root/assets/{keyboard-shortcuts-settings-lKTg6St7.js → keyboard-shortcuts-settings-ClVhEihC.js} +1 -1
  43. package/dist/gateway/static/root/assets/logs-page-J8fw1O8x.js +2 -0
  44. package/dist/gateway/static/root/assets/{markdown-editor-BFOjBBla.js → markdown-editor-jD1LPiP-.js} +2 -2
  45. package/dist/gateway/static/root/assets/{markdown-split-Bv4k6iGJ.js → markdown-split-DQYDAL4r.js} +1 -1
  46. package/dist/gateway/static/root/assets/{media-plugins-Bbi8g3N8.js → media-plugins-o5fchHfb.js} +1 -1
  47. package/dist/gateway/static/root/assets/messages-YzjnURPJ.js +3 -0
  48. package/dist/gateway/static/root/assets/models-hub-panel-D9csuY5T.js +2 -0
  49. package/dist/gateway/static/root/assets/notes-page-CeWoUGI-.js +1 -0
  50. package/dist/gateway/static/root/assets/notes-workbench-D_oaF0Pq.js +2 -0
  51. package/dist/gateway/static/root/assets/{page-tabs-Cp395RYD.js → page-tabs-u0u41Gxz.js} +1 -1
  52. package/dist/gateway/static/root/assets/{preview-open-alternatives-DqV5OM4m.js → preview-open-alternatives-BaYRX55y.js} +1 -1
  53. package/dist/gateway/static/root/assets/project-detail-page-dkHzwhDL.js +1 -0
  54. package/dist/gateway/static/root/assets/projects-page-DBp2WExF.js +1 -0
  55. package/dist/gateway/static/root/assets/remote-access-hub-Cgzg4pNN.js +1 -0
  56. package/dist/gateway/static/root/assets/{save-bar-controls-BItmzAzI.js → save-bar-controls-8ebg7PIX.js} +1 -1
  57. package/dist/gateway/static/root/assets/{schedule-field-BQ8IjjCn.js → schedule-field-Cd7FPvsJ.js} +1 -1
  58. package/dist/gateway/static/root/assets/{schema-form-BRpD6x3d.js → schema-form-Bq9iqPeA.js} +1 -1
  59. package/dist/gateway/static/root/assets/sessions-page-vkORZvUB.js +1 -0
  60. package/dist/gateway/static/root/assets/settings-advanced-gate-BFJsnSvx.js +1 -0
  61. package/dist/gateway/static/root/assets/{settings-collapsible-section-BKSPQM4R.js → settings-collapsible-section-DaVKl6I7.js} +1 -1
  62. package/dist/gateway/static/root/assets/{settings-form-section-BmyfthNQ.js → settings-form-section-C92Shabe.js} +1 -1
  63. package/dist/gateway/static/root/assets/settings-loading-skeleton-EJwzElLr.js +1 -0
  64. package/dist/gateway/static/root/assets/settings-page-BLY6onMK.js +1 -0
  65. package/dist/gateway/static/root/assets/{settings-page-layout-Bls5tfbH.js → settings-page-layout-dTbdUfAB.js} +1 -1
  66. package/dist/gateway/static/root/assets/setup-status-panel-BU0JUWIA.js +1 -0
  67. package/dist/gateway/static/root/assets/share-preview-page-SgKIyNQ7.js +2 -0
  68. package/dist/gateway/static/root/assets/shares-settings-aiY_QGsl.js +1 -0
  69. package/dist/gateway/static/root/assets/skill-reload-api-C44tGvK7.js +1 -0
  70. package/dist/gateway/static/root/assets/skills-page-C1re61FQ.js +2 -0
  71. package/dist/gateway/static/root/assets/{system-settings-panel-DKDAojWM.js → system-settings-panel-CntJCZEg.js} +1 -1
  72. package/dist/gateway/static/root/assets/{typed-models-lib-BCCV4Kkp.js → typed-models-lib-CjULd6SF.js} +1 -1
  73. package/dist/gateway/static/root/assets/url-DFGNSBNT.js +1 -0
  74. package/dist/gateway/static/root/assets/use-channel-catalog-CPdJ0o0i.js +1 -0
  75. package/dist/gateway/static/root/assets/user-profile-settings-panel-DBgcbZfp.js +1 -0
  76. package/dist/gateway/static/root/assets/{utils-DPGVr3tS.js → utils-icmWvHl6.js} +1 -1
  77. package/dist/gateway/static/root/assets/vendor-codemirror-C1WKjCk5.js +45 -0
  78. package/dist/gateway/static/root/assets/voice-api-key-field-BfvNUIWI.js +1 -0
  79. package/dist/gateway/static/root/assets/work-item-detail-page-BYvZjBuJ.js +1 -0
  80. package/dist/gateway/static/root/assets/{workflow-run-setup-panel-BTkRVNAh.js → workflow-run-setup-panel-DMXd1znx.js} +2 -2
  81. package/dist/gateway/static/root/assets/{workflow-start-dialog-DAImVtdO.js → workflow-start-dialog-Dff4IHzz.js} +1 -1
  82. package/dist/gateway/static/root/assets/workflows-page-0-640fQi.js +18 -0
  83. package/dist/gateway/static/root/index.html +7 -6
  84. package/dist/package.js +1 -1
  85. package/dist/src/agent/agent-instance-gateway.d.ts +5 -0
  86. package/dist/src/agent/agent-manager.d.ts +17 -0
  87. package/dist/src/agent/agent-manager.js +125 -8
  88. package/dist/src/agent/agent-manager.js.map +1 -1
  89. package/dist/src/agent/capabilities/definitions.d.ts +167 -0
  90. package/dist/src/agent/capabilities/definitions.js +175 -0
  91. package/dist/src/agent/capabilities/definitions.js.map +1 -0
  92. package/dist/src/agent/capabilities/index.d.ts +2 -0
  93. package/dist/src/agent/capabilities/index.js +3 -0
  94. package/dist/src/agent/capabilities/registry.d.ts +10 -0
  95. package/dist/src/agent/capabilities/registry.js +51 -0
  96. package/dist/src/agent/capabilities/registry.js.map +1 -0
  97. package/dist/src/agent/goals/post-turn.js +23 -2
  98. package/dist/src/agent/goals/post-turn.js.map +1 -1
  99. package/dist/src/agent/messaging/command-handler.d.ts +4 -0
  100. package/dist/src/agent/messaging/command-handler.js +6 -0
  101. package/dist/src/agent/messaging/command-handler.js.map +1 -1
  102. package/dist/src/agent/service/process-direct-one-shot.js +5 -3
  103. package/dist/src/agent/service/process-direct-one-shot.js.map +1 -1
  104. package/dist/src/agent/service/process-direct-streaming.js +4 -3
  105. package/dist/src/agent/service/process-direct-streaming.js.map +1 -1
  106. package/dist/src/agent/service.d.ts +4 -0
  107. package/dist/src/agent/service.js +26 -0
  108. package/dist/src/agent/service.js.map +1 -1
  109. package/dist/src/agent/skills/hub-lock.d.ts +5 -5
  110. package/dist/src/agent/skills/hub-lock.js +12 -12
  111. package/dist/src/agent/skills/hub-lock.js.map +1 -1
  112. package/dist/src/agent/skills/hub-pull.d.ts +15 -0
  113. package/dist/src/agent/skills/hub-pull.js +299 -24
  114. package/dist/src/agent/skills/hub-pull.js.map +1 -1
  115. package/dist/src/agent/skills/index.js +4 -40
  116. package/dist/src/agent/skills/index.js.map +1 -1
  117. package/dist/src/agent/skills/install-target.d.ts +3 -0
  118. package/dist/src/agent/skills/install-target.js +11 -0
  119. package/dist/src/agent/skills/install-target.js.map +1 -0
  120. package/dist/src/agent/skills/managed-store.d.ts +5 -3
  121. package/dist/src/agent/skills/managed-store.js +10 -9
  122. package/dist/src/agent/skills/managed-store.js.map +1 -1
  123. package/dist/src/agent/skills/marketplace/adapters/store/adapter.js +5 -3
  124. package/dist/src/agent/skills/marketplace/adapters/store/adapter.js.map +1 -1
  125. package/dist/src/agent/skills/marketplace/adapters/store/store-api-client.d.ts +21 -1
  126. package/dist/src/agent/skills/marketplace/adapters/store/store-api-client.js +62 -6
  127. package/dist/src/agent/skills/marketplace/adapters/store/store-api-client.js.map +1 -1
  128. package/dist/src/agent/skills/marketplace/resolve-adapter.js +1 -1
  129. package/dist/src/agent/skills/marketplace/resolve-adapter.js.map +1 -1
  130. package/dist/src/agent/skills/parse-skill-metadata.js +9 -1
  131. package/dist/src/agent/skills/parse-skill-metadata.js.map +1 -1
  132. package/dist/src/agent/skills/skill-manage-ops.js +2 -2
  133. package/dist/src/agent/skills/skill-manage-ops.js.map +1 -1
  134. package/dist/src/agent/skills/skill-manager.d.ts +4 -0
  135. package/dist/src/agent/skills/skill-manager.js +18 -6
  136. package/dist/src/agent/skills/skill-manager.js.map +1 -1
  137. package/dist/src/agent/skills/types.d.ts +5 -0
  138. package/dist/src/agent/skills/workspace-skills-dir.d.ts +1 -0
  139. package/dist/src/agent/skills/workspace-skills-dir.js +5 -1
  140. package/dist/src/agent/skills/workspace-skills-dir.js.map +1 -1
  141. package/dist/src/agent/tools/automation-tool.d.ts +6 -6
  142. package/dist/src/agent/tools/browser/tool/schemas.d.ts +20 -20
  143. package/dist/src/agent/tools/composio-tool.d.ts +3 -3
  144. package/dist/src/agent/tools/desktop-pet-tool.d.ts +24 -0
  145. package/dist/src/agent/tools/desktop-pet-tool.js +90 -0
  146. package/dist/src/agent/tools/desktop-pet-tool.js.map +1 -0
  147. package/dist/src/agent/tools/factory.d.ts +5 -0
  148. package/dist/src/agent/tools/factory.js +22 -0
  149. package/dist/src/agent/tools/factory.js.map +1 -1
  150. package/dist/src/agent/tools/goal-tool.d.ts +16 -16
  151. package/dist/src/agent/tools/index.d.ts +2 -0
  152. package/dist/src/agent/tools/index.js +3 -1
  153. package/dist/src/agent/tools/skill-install-tool.d.ts +28 -0
  154. package/dist/src/agent/tools/skill-install-tool.js +81 -0
  155. package/dist/src/agent/tools/skill-install-tool.js.map +1 -0
  156. package/dist/src/agent/tools/skill-manage-tool.js +3 -6
  157. package/dist/src/agent/tools/skill-manage-tool.js.map +1 -1
  158. package/dist/src/agent/tools/tool-manual-tool.d.ts +2 -2
  159. package/dist/src/agent/tools/xopc-use-tool.d.ts +5 -5
  160. package/dist/src/automations/domain/types.d.ts +1 -0
  161. package/dist/src/automations/service/automation-service.js +9 -0
  162. package/dist/src/automations/service/automation-service.js.map +1 -1
  163. package/dist/src/browser/schemas.d.ts +52 -52
  164. package/dist/src/capabilities/store-connector.d.ts +17 -0
  165. package/dist/src/capabilities/store-connector.js +191 -0
  166. package/dist/src/capabilities/store-connector.js.map +1 -0
  167. package/dist/src/chat-commands/builtins/skill.d.ts +1 -0
  168. package/dist/src/chat-commands/builtins/skill.js +132 -0
  169. package/dist/src/chat-commands/builtins/skill.js.map +1 -0
  170. package/dist/src/chat-commands/context.d.ts +3 -0
  171. package/dist/src/chat-commands/context.js +2 -0
  172. package/dist/src/chat-commands/context.js.map +1 -1
  173. package/dist/src/chat-commands/index.d.ts +1 -0
  174. package/dist/src/chat-commands/index.js +3 -1
  175. package/dist/src/chat-commands/index.js.map +1 -1
  176. package/dist/src/chat-commands/types.d.ts +17 -0
  177. package/dist/src/cli/command-loaders.js +1 -0
  178. package/dist/src/cli/command-loaders.js.map +1 -1
  179. package/dist/src/cli/command-manifest.js +4 -0
  180. package/dist/src/cli/command-manifest.js.map +1 -1
  181. package/dist/src/cli/commands/connectors.d.ts +4 -0
  182. package/dist/src/cli/commands/connectors.js +96 -0
  183. package/dist/src/cli/commands/connectors.js.map +1 -0
  184. package/dist/src/cli/commands/doctor/checks/database-schema.js +1 -1
  185. package/dist/src/cli/commands/extension-marketplace.js +1 -1
  186. package/dist/src/cli/commands/skills.js +1 -1
  187. package/dist/src/config/agent-profile.js +1 -1
  188. package/dist/src/config/schema.js +2 -2
  189. package/dist/src/config/schema.js.map +1 -1
  190. package/dist/src/connectors/health.js +1 -1
  191. package/dist/src/connectors/install.js +3 -1
  192. package/dist/src/connectors/install.js.map +1 -1
  193. package/dist/src/connectors/materialize.js +2 -0
  194. package/dist/src/connectors/materialize.js.map +1 -1
  195. package/dist/src/connectors/types.d.ts +19 -1
  196. package/dist/src/extensions/install.js +1 -1
  197. package/dist/src/extensions/types/manifest.d.ts +1 -1
  198. package/dist/src/extensions/update.js +5 -3
  199. package/dist/src/extensions/update.js.map +1 -1
  200. package/dist/src/gateway/agent-builtin-tools.d.ts +1 -1
  201. package/dist/src/gateway/agent-builtin-tools.js +1 -0
  202. package/dist/src/gateway/agent-builtin-tools.js.map +1 -1
  203. package/dist/src/gateway/agents-admin.js +1 -1
  204. package/dist/src/gateway/chat-stream/mapper.js +5 -1
  205. package/dist/src/gateway/chat-stream/mapper.js.map +1 -1
  206. package/dist/src/gateway/chat-stream/protocol.d.ts +2 -0
  207. package/dist/src/gateway/hono/lib/config-payload.js +1 -1
  208. package/dist/src/gateway/hono/lib/config-payload.js.map +1 -1
  209. package/dist/src/gateway/hono/middleware/auth.d.ts +2 -2
  210. package/dist/src/gateway/hono/middleware/logger.d.ts +1 -1
  211. package/dist/src/gateway/hono/middleware/route-errors.d.ts +1 -1
  212. package/dist/src/gateway/hono/middleware/strict-rate-limit.d.ts +2 -2
  213. package/dist/src/gateway/hono/routes/capabilities.d.ts +3 -0
  214. package/dist/src/gateway/hono/routes/capabilities.js +79 -0
  215. package/dist/src/gateway/hono/routes/capabilities.js.map +1 -0
  216. package/dist/src/gateway/hono/routes/commands-skills.js +88 -3
  217. package/dist/src/gateway/hono/routes/commands-skills.js.map +1 -1
  218. package/dist/src/gateway/hono/routes/connectors.js +2 -2
  219. package/dist/src/gateway/hono/routes/goals.js +212 -3
  220. package/dist/src/gateway/hono/routes/goals.js.map +1 -1
  221. package/dist/src/gateway/hono/routes/index.js +2 -0
  222. package/dist/src/gateway/hono/routes/index.js.map +1 -1
  223. package/dist/src/gateway/hono/routes/lazy-bundles.js +10 -1
  224. package/dist/src/gateway/hono/routes/lazy-bundles.js.map +1 -1
  225. package/dist/src/gateway/hono/routes/mcp.js +1 -1
  226. package/dist/src/gateway/hono/routes/mobile.d.ts +3 -0
  227. package/dist/src/gateway/hono/routes/mobile.js +113 -0
  228. package/dist/src/gateway/hono/routes/mobile.js.map +1 -0
  229. package/dist/src/gateway/hono/routes/projects.js +45 -0
  230. package/dist/src/gateway/hono/routes/projects.js.map +1 -1
  231. package/dist/src/gateway/hono/routes/workspace.js +4 -1
  232. package/dist/src/gateway/hono/routes/workspace.js.map +1 -1
  233. package/dist/src/gateway/hono/sse.d.ts +3 -3
  234. package/dist/src/gateway/service/marketplace-service.d.ts +23 -1
  235. package/dist/src/gateway/service/marketplace-service.js +57 -6
  236. package/dist/src/gateway/service/marketplace-service.js.map +1 -1
  237. package/dist/src/gateway/service/run-gateway-agent.js +6 -2
  238. package/dist/src/gateway/service/run-gateway-agent.js.map +1 -1
  239. package/dist/src/gateway/service.d.ts +2 -0
  240. package/dist/src/gateway/service.js +12 -2
  241. package/dist/src/gateway/service.js.map +1 -1
  242. package/dist/src/goals/goal-contract-draft.d.ts +15 -0
  243. package/dist/src/goals/goal-contract-draft.js +114 -0
  244. package/dist/src/goals/goal-contract-draft.js.map +1 -0
  245. package/dist/src/goals/goal-evidence-review.d.ts +15 -0
  246. package/dist/src/goals/goal-evidence-review.js +91 -0
  247. package/dist/src/goals/goal-evidence-review.js.map +1 -0
  248. package/dist/src/goals/goal-runner.js +3 -1
  249. package/dist/src/goals/goal-runner.js.map +1 -1
  250. package/dist/src/goals/goal-service.d.ts +17 -1
  251. package/dist/src/goals/goal-service.js +44 -3
  252. package/dist/src/goals/goal-service.js.map +1 -1
  253. package/dist/src/goals/goal-store.d.ts +18 -1
  254. package/dist/src/goals/goal-store.js +190 -1
  255. package/dist/src/goals/goal-store.js.map +1 -1
  256. package/dist/src/goals/index.d.ts +4 -0
  257. package/dist/src/goals/index.js +3 -1
  258. package/dist/src/goals/types.d.ts +42 -0
  259. package/dist/src/mcp/channel-bridge.js +1 -1
  260. package/dist/src/mobile/notification-service.d.ts +8 -0
  261. package/dist/src/mobile/notification-service.js +139 -0
  262. package/dist/src/mobile/notification-service.js.map +1 -0
  263. package/dist/src/mobile/notification-store.d.ts +28 -0
  264. package/dist/src/mobile/notification-store.js +134 -0
  265. package/dist/src/mobile/notification-store.js.map +1 -0
  266. package/dist/src/mobile/notification-types.d.ts +37 -0
  267. package/dist/src/mobile/notification-types.js +11 -0
  268. package/dist/src/mobile/notification-types.js.map +1 -0
  269. package/dist/src/pets/factory.d.ts +25 -0
  270. package/dist/src/pets/factory.js +327 -0
  271. package/dist/src/pets/factory.js.map +1 -0
  272. package/dist/src/pets/manifest.d.ts +2 -0
  273. package/dist/src/pets/manifest.js +16 -0
  274. package/dist/src/pets/manifest.js.map +1 -0
  275. package/dist/src/pets/validator.d.ts +12 -0
  276. package/dist/src/pets/validator.js +175 -0
  277. package/dist/src/pets/validator.js.map +1 -0
  278. package/dist/src/projects/project-service.d.ts +2 -0
  279. package/dist/src/projects/project-service.js +6 -0
  280. package/dist/src/projects/project-service.js.map +1 -1
  281. package/dist/src/projects/project-store.d.ts +1 -0
  282. package/dist/src/projects/project-store.js +14 -7
  283. package/dist/src/projects/project-store.js.map +1 -1
  284. package/dist/src/projects/types.d.ts +2 -0
  285. package/dist/src/session/session-title.js +22 -2
  286. package/dist/src/session/session-title.js.map +1 -1
  287. package/dist/src/storage/sqlite/migrations/025_project_pinned_at.sql +5 -0
  288. package/dist/src/storage/sqlite/migrations/026_goal_contracts.sql +10 -0
  289. package/dist/src/storage/sqlite/migrations/027_mobile_notifications.sql +43 -0
  290. package/dist/src/storage/sqlite/migrations/028_goal_evidence_requirements.sql +47 -0
  291. package/dist/src/storage/sqlite/migrations/runner.d.ts +1 -1
  292. package/dist/src/storage/sqlite/migrations/runner.js +3 -3
  293. package/dist/src/storage/sqlite/migrations/runner.js.map +1 -1
  294. package/dist/src/storage/sqlite/migrations/runner.ts +1 -1
  295. package/dist/src/storage/sqlite/schema.js +1 -1
  296. package/dist/src/storage/sqlite/schema.sql +44 -0
  297. package/dist/src/tui/tui-startup-resources.js +1 -1
  298. package/package.json +1 -1
  299. package/skills/THIRD_PARTY_NOTICES.md +16 -0
  300. package/skills/creative/algorithmic-art/SKILL.md +37 -0
  301. package/skills/creative/algorithmic-art/templates/generator-template.js +27 -0
  302. package/skills/creative/algorithmic-art/templates/viewer.html +27 -0
  303. package/skills/creative/hatch-pet/SKILL.md +63 -0
  304. package/skills/documents/doc-coauthoring/SKILL.md +40 -0
  305. package/skills/documents/doc-coauthoring/references/reader-review.md +9 -0
  306. package/skills/documents/docx/SKILL.md +40 -0
  307. package/skills/documents/docx/references/editing-checklist.md +8 -0
  308. package/skills/documents/docx/scripts/inspect_ooxml.py +36 -0
  309. package/skills/documents/docx/scripts/render_docx.py +33 -0
  310. package/skills/documents/pdf/SKILL.md +47 -0
  311. package/skills/documents/pdf/references/operations.md +16 -0
  312. package/skills/documents/pdf/scripts/inspect_pdf.py +41 -0
  313. package/skills/documents/pdf/scripts/render_pdf.py +32 -0
  314. package/skills/documents/pptx/SKILL.md +41 -0
  315. package/skills/documents/pptx/references/qa-checklist.md +6 -0
  316. package/skills/documents/pptx/scripts/inspect_ooxml.py +34 -0
  317. package/skills/documents/pptx/scripts/render_pptx.py +33 -0
  318. package/skills/engineering/define-goal/SKILL.md +41 -0
  319. package/skills/engineering/define-goal/references/objective-rubric.md +12 -0
  320. package/dist/gateway/static/root/assets/agents-DRYVahI-.js +0 -2
  321. package/dist/gateway/static/root/assets/app-management-settings-panel-D9-HW6cy.js +0 -1
  322. package/dist/gateway/static/root/assets/apps-page-BdJ1eQg3.js +0 -1
  323. package/dist/gateway/static/root/assets/automations-page-D8hYQbkl.js +0 -4
  324. package/dist/gateway/static/root/assets/browser-settings-page-CiWEO-Iy.js +0 -8
  325. package/dist/gateway/static/root/assets/capability-presets-api-Bgc8ag55.js +0 -1
  326. package/dist/gateway/static/root/assets/capability-presets-settings-panel-B1nIi0TC.js +0 -1
  327. package/dist/gateway/static/root/assets/channel-recipient-api-0sauApoC.js +0 -1
  328. package/dist/gateway/static/root/assets/channels-settings-BddPdqVo.js +0 -1
  329. package/dist/gateway/static/root/assets/connectors-page-Ba2GDhSE.js +0 -2
  330. package/dist/gateway/static/root/assets/directory-picker-path-field-CiEc4KmY.js +0 -1
  331. package/dist/gateway/static/root/assets/dist-By6Rh2ee.js +0 -1
  332. package/dist/gateway/static/root/assets/dreaming-settings-4bvpk5wr.js +0 -3
  333. package/dist/gateway/static/root/assets/extension-settings-page-D9VGHmYJ.js +0 -1
  334. package/dist/gateway/static/root/assets/fetch-5V96dUz3.js +0 -1
  335. package/dist/gateway/static/root/assets/gateway-config-api-BtIzObwp.js +0 -1
  336. package/dist/gateway/static/root/assets/gateway-settings-KFsGGDhg.js +0 -1
  337. package/dist/gateway/static/root/assets/goal-create-dialog-4PcF6Sh6.js +0 -1
  338. package/dist/gateway/static/root/assets/goal-detail-page-B482PSaG.js +0 -1
  339. package/dist/gateway/static/root/assets/goals-config-api-CVN7-pdM.js +0 -1
  340. package/dist/gateway/static/root/assets/goals-page-CUdgJuDn.js +0 -2
  341. package/dist/gateway/static/root/assets/goals-settings-IYCH3-Pi.js +0 -1
  342. package/dist/gateway/static/root/assets/heartbeat-settings-Cf_a2Bmz.js +0 -1
  343. package/dist/gateway/static/root/assets/index-CA2tR2vm.js +0 -256
  344. package/dist/gateway/static/root/assets/index-Ds3u0LOk.css +0 -1
  345. package/dist/gateway/static/root/assets/logs-page-BCporcNH.js +0 -2
  346. package/dist/gateway/static/root/assets/messages-C22hxE1N.js +0 -3
  347. package/dist/gateway/static/root/assets/models-hub-panel-D68Pyow7.js +0 -2
  348. package/dist/gateway/static/root/assets/notes-page-CuUMOiyv.js +0 -1
  349. package/dist/gateway/static/root/assets/notes-workbench-Dx2N5zTk.js +0 -2
  350. package/dist/gateway/static/root/assets/project-detail-page-sB8AzZi3.js +0 -1
  351. package/dist/gateway/static/root/assets/projects-page-LBnJqWK2.js +0 -1
  352. package/dist/gateway/static/root/assets/remote-access-hub-Dbj0o6SC.js +0 -1
  353. package/dist/gateway/static/root/assets/sessions-page-DhDZKspB.js +0 -1
  354. package/dist/gateway/static/root/assets/settings-advanced-gate-BvXLMNL6.js +0 -1
  355. package/dist/gateway/static/root/assets/settings-page-Cfm9UxAr.js +0 -1
  356. package/dist/gateway/static/root/assets/setup-status-panel-kQEsTGAs.js +0 -1
  357. package/dist/gateway/static/root/assets/share-preview-page-DbYZNAPb.js +0 -2
  358. package/dist/gateway/static/root/assets/shares-settings-BtR4B0kn.js +0 -1
  359. package/dist/gateway/static/root/assets/skill-reload-api-CJNVOWoK.js +0 -1
  360. package/dist/gateway/static/root/assets/skills-page-DcWDHrt3.js +0 -2
  361. package/dist/gateway/static/root/assets/use-channel-catalog-NBqg3iFY.js +0 -1
  362. package/dist/gateway/static/root/assets/user-profile-settings-panel-suQ2x-LB.js +0 -1
  363. package/dist/gateway/static/root/assets/vendor-codemirror-BvNgyE1Q.js +0 -45
  364. package/dist/gateway/static/root/assets/voice-api-key-field-DaJASKFP.js +0 -1
  365. package/dist/gateway/static/root/assets/work-item-detail-page-BrCZpAWh.js +0 -1
  366. package/dist/gateway/static/root/assets/workflows-page-vyJ8wSXm.js +0 -18
@@ -30,6 +30,8 @@ export declare class ProjectService {
30
30
  autoCreate?: boolean;
31
31
  }): WorkspaceProjectMatch | null;
32
32
  update(id: string, input: UpdateProjectInput): Project;
33
+ pin(id: string): Project;
34
+ unpin(id: string): Project;
33
35
  delete(id: string): void;
34
36
  getWithDetails(id: string): ProjectWithDetails | null;
35
37
  attachSession(sessionKey: string, projectId: string): void;
@@ -177,6 +177,12 @@ var ProjectService = class {
177
177
  });
178
178
  return project;
179
179
  }
180
+ pin(id) {
181
+ return this.update(id, { pinnedAt: Date.now() });
182
+ }
183
+ unpin(id) {
184
+ return this.update(id, { pinnedAt: null });
185
+ }
180
186
  delete(id) {
181
187
  this.store.delete(id);
182
188
  }
@@ -1 +1 @@
1
- {"version":3,"file":"project-service.js","names":[],"sources":["../../../src/projects/project-service.ts"],"sourcesContent":["import { changedFieldsFromPatch, emitActivity, systemActivityActor, systemActivitySource } from '../activity/emitter.js';\nimport { getSessionMetadata } from '../storage/sqlite/index.js';\nimport { ProjectStore } from './project-store.js';\nimport { bindGoalToProject, listProjectGoalIds, unbindGoalFromProject } from './goal-bind.js';\nimport { bindSessionToProject, listProjectSessionKeys, unbindSessionFromProject } from './session-bind.js';\nimport type { CreateProjectInput, Project, ProjectListQuery, ProjectListResult, ProjectWithDetails, SidebarProjectListQuery, UpdateProjectInput } from './types.js';\nimport {\n canonicalWorkspacePath,\n ensureWorkspaceDirectory,\n inferProjectNameFromWorkspaceRoot,\n isPathSameOrInsideWorkspace,\n isSafeAutoCreateWorkspaceRoot,\n ProjectWorkspaceConflictError,\n ProjectWorkspaceMissingError,\n resolveWorkspaceProjectRoot,\n workspaceDirectoryExists,\n type WorkspaceProjectMatch,\n} from './workspace-project.js';\n\nexport type ProjectSuggestion = {\n projectId: string;\n projectName: string;\n score: number;\n reason: string;\n};\n\nexport class ProjectService {\n constructor(private readonly store = new ProjectStore()) {}\n\n private listAllProjects(): Project[] {\n const items: Project[] = [];\n let offset = 0;\n const limit = 500;\n while (true) {\n const page = this.list({ limit, offset });\n items.push(...page.items);\n if (!page.hasMore || page.items.length === 0) break;\n offset += page.items.length;\n }\n return items;\n }\n\n create(input: CreateProjectInput): Project {\n const rawWorkspaceRoot = input.workspaceRoot?.trim();\n const workspaceRoot = rawWorkspaceRoot ? canonicalWorkspacePath(rawWorkspaceRoot) : undefined;\n if (rawWorkspaceRoot && !workspaceRoot) {\n throw new Error('Invalid workspace root');\n }\n if (workspaceRoot) {\n const existing = this.findByWorkspaceRoot(workspaceRoot);\n if (existing) throw new ProjectWorkspaceConflictError(existing);\n if (!workspaceDirectoryExists(workspaceRoot)) {\n if (input.createWorkspaceRoot === true) {\n ensureWorkspaceDirectory(workspaceRoot);\n } else {\n throw new ProjectWorkspaceMissingError(workspaceRoot);\n }\n }\n }\n const name = input.name?.trim() || inferProjectNameFromWorkspaceRoot(workspaceRoot) || '';\n const slug = input.slug?.trim() || this.store.generateSlug(name);\n const project = this.store.create({ ...input, name, slug, workspaceRoot });\n emitActivity({\n type: 'project.created',\n primaryObject: { kind: 'project', id: project.id, title: project.name },\n actor: systemActivityActor(),\n source: systemActivitySource(),\n payload: {\n name: project.name,\n workspaceRoot: project.workspaceRoot,\n brief: project.brief,\n },\n scopes: [{ scopeKind: 'project', scopeId: project.id, reason: 'object_owner' }],\n nowMs: project.createdAt,\n });\n return project;\n }\n\n get(id: string): Project | null {\n return this.store.get(id);\n }\n\n getBySlug(slug: string): Project | null {\n return this.store.findBySlug(slug);\n }\n\n list(query?: ProjectListQuery): ProjectListResult {\n return this.store.list(query);\n }\n\n listWithSidebarSessions(query?: SidebarProjectListQuery): ProjectListResult {\n return this.store.listWithSidebarSessions(query);\n }\n\n findByWorkspaceRoot(workspaceRoot: string, options: { excludeProjectId?: string } = {}): Project | null {\n const target = canonicalWorkspacePath(workspaceRoot);\n if (!target) return null;\n for (const project of this.listAllProjects()) {\n if (options.excludeProjectId && project.id === options.excludeProjectId) continue;\n const projectRoot = canonicalWorkspacePath(project.workspaceRoot);\n if (projectRoot && isPathSameOrInsideWorkspace(projectRoot, target) && isPathSameOrInsideWorkspace(target, projectRoot)) {\n return project;\n }\n }\n return null;\n }\n\n resolveForWorkspacePath(workspacePath: string): { project: Project; reason: 'exact' | 'contained' } | null {\n const target = canonicalWorkspacePath(workspacePath);\n if (!target) return null;\n let best: { project: Project; root: string; reason: 'exact' | 'contained' } | null = null;\n for (const project of this.listAllProjects()) {\n const root = canonicalWorkspacePath(project.workspaceRoot);\n if (!root || !isPathSameOrInsideWorkspace(root, target)) continue;\n const reason = isPathSameOrInsideWorkspace(root, target) && isPathSameOrInsideWorkspace(target, root) ? 'exact' : 'contained';\n if (!best || root.length > best.root.length) {\n best = { project, root, reason };\n }\n }\n return best ? { project: best.project, reason: best.reason } : null;\n }\n\n resolveOrCreateForWorkspacePath(input: {\n workspacePath: string;\n agentId?: string;\n defaultAgentId?: string;\n autoCreate?: boolean;\n }): WorkspaceProjectMatch | null {\n const existing = this.resolveForWorkspacePath(input.workspacePath);\n if (existing) return { project: existing.project, reason: existing.reason, created: false };\n if (!input.autoCreate) return null;\n const workspaceRoot = resolveWorkspaceProjectRoot(input.workspacePath) ?? canonicalWorkspacePath(input.workspacePath);\n if (!workspaceRoot || !isSafeAutoCreateWorkspaceRoot(workspaceRoot)) return null;\n const rootExisting = this.resolveForWorkspacePath(workspaceRoot);\n if (rootExisting) return { project: rootExisting.project, reason: rootExisting.reason, created: false };\n const project = this.create({\n name: inferProjectNameFromWorkspaceRoot(workspaceRoot) ?? undefined,\n workspaceRoot,\n defaultAgentId: Object.hasOwn(input, 'defaultAgentId') ? input.defaultAgentId : input.agentId,\n });\n return { project, reason: 'auto_created', created: true };\n }\n\n update(id: string, input: UpdateProjectInput): Project {\n const patch = { ...input };\n if (input.workspaceRoot !== undefined && input.workspaceRoot !== null && input.workspaceRoot.trim()) {\n const workspaceRoot = canonicalWorkspacePath(input.workspaceRoot);\n if (!workspaceRoot) {\n throw new Error('Invalid workspace root');\n }\n const existing = this.findByWorkspaceRoot(workspaceRoot, { excludeProjectId: id });\n if (existing) throw new ProjectWorkspaceConflictError(existing);\n if (!workspaceDirectoryExists(workspaceRoot)) {\n if (input.createWorkspaceRoot === true) {\n ensureWorkspaceDirectory(workspaceRoot);\n } else {\n throw new ProjectWorkspaceMissingError(workspaceRoot);\n }\n }\n patch.workspaceRoot = workspaceRoot;\n }\n const before = this.store.get(id);\n const project = this.store.update(id, patch);\n const changes = changedFieldsFromPatch(patch as Record<string, unknown>, ['createWorkspaceRoot']);\n const type = before?.status !== project.status\n ? 'project.status_changed'\n : before?.workspaceRoot !== project.workspaceRoot\n ? 'project.workspace_changed'\n : 'project.updated';\n emitActivity({\n type,\n primaryObject: { kind: 'project', id: project.id, title: project.name },\n actor: systemActivityActor(),\n source: systemActivitySource(),\n payload: {\n changes,\n ...(type === 'project.status_changed' ? { from: before?.status, to: project.status } : {}),\n ...(type === 'project.workspace_changed' ? { from: before?.workspaceRoot, to: project.workspaceRoot } : {}),\n },\n scopes: [{ scopeKind: 'project', scopeId: project.id, reason: 'object_owner' }],\n nowMs: project.updatedAt,\n });\n return project;\n }\n\n delete(id: string): void {\n this.store.delete(id);\n }\n\n getWithDetails(id: string): ProjectWithDetails | null {\n return this.store.getWithDetails(id);\n }\n\n attachSession(sessionKey: string, projectId: string): void {\n bindSessionToProject(sessionKey, projectId);\n }\n\n detachSession(sessionKey: string): void {\n unbindSessionFromProject(sessionKey);\n }\n\n listSessionKeys(projectId: string, limit?: number, offset?: number): string[] {\n return listProjectSessionKeys(projectId, limit, offset);\n }\n\n attachGoal(goalId: string, projectId: string): void {\n bindGoalToProject(goalId, projectId);\n }\n\n detachGoal(goalId: string): void {\n unbindGoalFromProject(goalId);\n }\n\n listGoalIds(projectId: string, limit?: number, offset?: number): string[] {\n return listProjectGoalIds(projectId, limit, offset);\n }\n\n suggestProjectsForSession(sessionKey: string): ProjectSuggestion[] {\n const session = getSessionMetadata(sessionKey);\n if (!session) return [];\n const projects = this.list({ status: 'active', limit: 500 }).items;\n const haystack = [session.key, session.name, session.routing?.agentId, session.sourceChannel, ...(session.tags ?? [])]\n .filter(Boolean)\n .join(' ')\n .toLowerCase();\n const suggestions: ProjectSuggestion[] = [];\n for (const project of projects) {\n let score = 0;\n const reasons: string[] = [];\n if (haystack.includes(project.slug.toLowerCase())) {\n score += 10;\n reasons.push('session metadata matches project slug');\n }\n for (const token of project.name.toLowerCase().split(/\\s+/).filter((t) => t.length >= 3)) {\n if (haystack.includes(token)) {\n score += 3;\n reasons.push(`matches ${token}`);\n break;\n }\n }\n if (score > 0) {\n suggestions.push({ projectId: project.id, projectName: project.name, score, reason: reasons.join(', ') });\n }\n }\n return suggestions.sort((a, b) => b.score - a.score).slice(0, 5);\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"project-service.js","names":[],"sources":["../../../src/projects/project-service.ts"],"sourcesContent":["import { changedFieldsFromPatch, emitActivity, systemActivityActor, systemActivitySource } from '../activity/emitter.js';\nimport { getSessionMetadata } from '../storage/sqlite/index.js';\nimport { ProjectStore } from './project-store.js';\nimport { bindGoalToProject, listProjectGoalIds, unbindGoalFromProject } from './goal-bind.js';\nimport { bindSessionToProject, listProjectSessionKeys, unbindSessionFromProject } from './session-bind.js';\nimport type { CreateProjectInput, Project, ProjectListQuery, ProjectListResult, ProjectWithDetails, SidebarProjectListQuery, UpdateProjectInput } from './types.js';\nimport {\n canonicalWorkspacePath,\n ensureWorkspaceDirectory,\n inferProjectNameFromWorkspaceRoot,\n isPathSameOrInsideWorkspace,\n isSafeAutoCreateWorkspaceRoot,\n ProjectWorkspaceConflictError,\n ProjectWorkspaceMissingError,\n resolveWorkspaceProjectRoot,\n workspaceDirectoryExists,\n type WorkspaceProjectMatch,\n} from './workspace-project.js';\n\nexport type ProjectSuggestion = {\n projectId: string;\n projectName: string;\n score: number;\n reason: string;\n};\n\nexport class ProjectService {\n constructor(private readonly store = new ProjectStore()) {}\n\n private listAllProjects(): Project[] {\n const items: Project[] = [];\n let offset = 0;\n const limit = 500;\n while (true) {\n const page = this.list({ limit, offset });\n items.push(...page.items);\n if (!page.hasMore || page.items.length === 0) break;\n offset += page.items.length;\n }\n return items;\n }\n\n create(input: CreateProjectInput): Project {\n const rawWorkspaceRoot = input.workspaceRoot?.trim();\n const workspaceRoot = rawWorkspaceRoot ? canonicalWorkspacePath(rawWorkspaceRoot) : undefined;\n if (rawWorkspaceRoot && !workspaceRoot) {\n throw new Error('Invalid workspace root');\n }\n if (workspaceRoot) {\n const existing = this.findByWorkspaceRoot(workspaceRoot);\n if (existing) throw new ProjectWorkspaceConflictError(existing);\n if (!workspaceDirectoryExists(workspaceRoot)) {\n if (input.createWorkspaceRoot === true) {\n ensureWorkspaceDirectory(workspaceRoot);\n } else {\n throw new ProjectWorkspaceMissingError(workspaceRoot);\n }\n }\n }\n const name = input.name?.trim() || inferProjectNameFromWorkspaceRoot(workspaceRoot) || '';\n const slug = input.slug?.trim() || this.store.generateSlug(name);\n const project = this.store.create({ ...input, name, slug, workspaceRoot });\n emitActivity({\n type: 'project.created',\n primaryObject: { kind: 'project', id: project.id, title: project.name },\n actor: systemActivityActor(),\n source: systemActivitySource(),\n payload: {\n name: project.name,\n workspaceRoot: project.workspaceRoot,\n brief: project.brief,\n },\n scopes: [{ scopeKind: 'project', scopeId: project.id, reason: 'object_owner' }],\n nowMs: project.createdAt,\n });\n return project;\n }\n\n get(id: string): Project | null {\n return this.store.get(id);\n }\n\n getBySlug(slug: string): Project | null {\n return this.store.findBySlug(slug);\n }\n\n list(query?: ProjectListQuery): ProjectListResult {\n return this.store.list(query);\n }\n\n listWithSidebarSessions(query?: SidebarProjectListQuery): ProjectListResult {\n return this.store.listWithSidebarSessions(query);\n }\n\n findByWorkspaceRoot(workspaceRoot: string, options: { excludeProjectId?: string } = {}): Project | null {\n const target = canonicalWorkspacePath(workspaceRoot);\n if (!target) return null;\n for (const project of this.listAllProjects()) {\n if (options.excludeProjectId && project.id === options.excludeProjectId) continue;\n const projectRoot = canonicalWorkspacePath(project.workspaceRoot);\n if (projectRoot && isPathSameOrInsideWorkspace(projectRoot, target) && isPathSameOrInsideWorkspace(target, projectRoot)) {\n return project;\n }\n }\n return null;\n }\n\n resolveForWorkspacePath(workspacePath: string): { project: Project; reason: 'exact' | 'contained' } | null {\n const target = canonicalWorkspacePath(workspacePath);\n if (!target) return null;\n let best: { project: Project; root: string; reason: 'exact' | 'contained' } | null = null;\n for (const project of this.listAllProjects()) {\n const root = canonicalWorkspacePath(project.workspaceRoot);\n if (!root || !isPathSameOrInsideWorkspace(root, target)) continue;\n const reason = isPathSameOrInsideWorkspace(root, target) && isPathSameOrInsideWorkspace(target, root) ? 'exact' : 'contained';\n if (!best || root.length > best.root.length) {\n best = { project, root, reason };\n }\n }\n return best ? { project: best.project, reason: best.reason } : null;\n }\n\n resolveOrCreateForWorkspacePath(input: {\n workspacePath: string;\n agentId?: string;\n defaultAgentId?: string;\n autoCreate?: boolean;\n }): WorkspaceProjectMatch | null {\n const existing = this.resolveForWorkspacePath(input.workspacePath);\n if (existing) return { project: existing.project, reason: existing.reason, created: false };\n if (!input.autoCreate) return null;\n const workspaceRoot = resolveWorkspaceProjectRoot(input.workspacePath) ?? canonicalWorkspacePath(input.workspacePath);\n if (!workspaceRoot || !isSafeAutoCreateWorkspaceRoot(workspaceRoot)) return null;\n const rootExisting = this.resolveForWorkspacePath(workspaceRoot);\n if (rootExisting) return { project: rootExisting.project, reason: rootExisting.reason, created: false };\n const project = this.create({\n name: inferProjectNameFromWorkspaceRoot(workspaceRoot) ?? undefined,\n workspaceRoot,\n defaultAgentId: Object.hasOwn(input, 'defaultAgentId') ? input.defaultAgentId : input.agentId,\n });\n return { project, reason: 'auto_created', created: true };\n }\n\n update(id: string, input: UpdateProjectInput): Project {\n const patch = { ...input };\n if (input.workspaceRoot !== undefined && input.workspaceRoot !== null && input.workspaceRoot.trim()) {\n const workspaceRoot = canonicalWorkspacePath(input.workspaceRoot);\n if (!workspaceRoot) {\n throw new Error('Invalid workspace root');\n }\n const existing = this.findByWorkspaceRoot(workspaceRoot, { excludeProjectId: id });\n if (existing) throw new ProjectWorkspaceConflictError(existing);\n if (!workspaceDirectoryExists(workspaceRoot)) {\n if (input.createWorkspaceRoot === true) {\n ensureWorkspaceDirectory(workspaceRoot);\n } else {\n throw new ProjectWorkspaceMissingError(workspaceRoot);\n }\n }\n patch.workspaceRoot = workspaceRoot;\n }\n const before = this.store.get(id);\n const project = this.store.update(id, patch);\n const changes = changedFieldsFromPatch(patch as Record<string, unknown>, ['createWorkspaceRoot']);\n const type = before?.status !== project.status\n ? 'project.status_changed'\n : before?.workspaceRoot !== project.workspaceRoot\n ? 'project.workspace_changed'\n : 'project.updated';\n emitActivity({\n type,\n primaryObject: { kind: 'project', id: project.id, title: project.name },\n actor: systemActivityActor(),\n source: systemActivitySource(),\n payload: {\n changes,\n ...(type === 'project.status_changed' ? { from: before?.status, to: project.status } : {}),\n ...(type === 'project.workspace_changed' ? { from: before?.workspaceRoot, to: project.workspaceRoot } : {}),\n },\n scopes: [{ scopeKind: 'project', scopeId: project.id, reason: 'object_owner' }],\n nowMs: project.updatedAt,\n });\n return project;\n }\n\n pin(id: string): Project {\n return this.update(id, { pinnedAt: Date.now() });\n }\n\n unpin(id: string): Project {\n return this.update(id, { pinnedAt: null });\n }\n\n delete(id: string): void {\n this.store.delete(id);\n }\n\n getWithDetails(id: string): ProjectWithDetails | null {\n return this.store.getWithDetails(id);\n }\n\n attachSession(sessionKey: string, projectId: string): void {\n bindSessionToProject(sessionKey, projectId);\n }\n\n detachSession(sessionKey: string): void {\n unbindSessionFromProject(sessionKey);\n }\n\n listSessionKeys(projectId: string, limit?: number, offset?: number): string[] {\n return listProjectSessionKeys(projectId, limit, offset);\n }\n\n attachGoal(goalId: string, projectId: string): void {\n bindGoalToProject(goalId, projectId);\n }\n\n detachGoal(goalId: string): void {\n unbindGoalFromProject(goalId);\n }\n\n listGoalIds(projectId: string, limit?: number, offset?: number): string[] {\n return listProjectGoalIds(projectId, limit, offset);\n }\n\n suggestProjectsForSession(sessionKey: string): ProjectSuggestion[] {\n const session = getSessionMetadata(sessionKey);\n if (!session) return [];\n const projects = this.list({ status: 'active', limit: 500 }).items;\n const haystack = [session.key, session.name, session.routing?.agentId, session.sourceChannel, ...(session.tags ?? [])]\n .filter(Boolean)\n .join(' ')\n .toLowerCase();\n const suggestions: ProjectSuggestion[] = [];\n for (const project of projects) {\n let score = 0;\n const reasons: string[] = [];\n if (haystack.includes(project.slug.toLowerCase())) {\n score += 10;\n reasons.push('session metadata matches project slug');\n }\n for (const token of project.name.toLowerCase().split(/\\s+/).filter((t) => t.length >= 3)) {\n if (haystack.includes(token)) {\n score += 3;\n reasons.push(`matches ${token}`);\n break;\n }\n }\n if (score > 0) {\n suggestions.push({ projectId: project.id, projectName: project.name, score, reason: reasons.join(', ') });\n }\n }\n return suggestions.sort((a, b) => b.score - a.score).slice(0, 5);\n }\n}\n"],"mappings":"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"}
@@ -12,6 +12,7 @@ export type ProjectRow = {
12
12
  created_at: number;
13
13
  updated_at: number;
14
14
  last_active_at: number | null;
15
+ pinned_at: number | null;
15
16
  };
16
17
  export declare function slugifyProjectName(name: string): string;
17
18
  export declare class ProjectStore {
@@ -23,7 +23,8 @@ function projectFromRow(row) {
23
23
  instructions: row.instructions ?? void 0,
24
24
  createdAt: row.created_at,
25
25
  updatedAt: row.updated_at,
26
- lastActiveAt: row.last_active_at ?? void 0
26
+ lastActiveAt: row.last_active_at ?? void 0,
27
+ pinnedAt: row.pinned_at ?? void 0
27
28
  };
28
29
  }
29
30
  function clampLimit(limit, fallback = 50) {
@@ -80,13 +81,14 @@ var ProjectStore = class {
80
81
  instructions: trimOptional(input.instructions),
81
82
  createdAt: now,
82
83
  updatedAt: now,
83
- lastActiveAt: now
84
+ lastActiveAt: now,
85
+ pinnedAt: void 0
84
86
  };
85
87
  runSqliteWriteTransaction((db) => {
86
88
  db.prepare(`INSERT INTO projects (
87
89
  project_id, name, slug, description, status, default_agent_id, workspace_root,
88
- brief, instructions, created_at, updated_at, last_active_at
89
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(project.id, project.name, project.slug, project.description ?? null, project.status, project.defaultAgentId ?? null, project.workspaceRoot ?? null, project.brief ?? null, project.instructions ?? null, project.createdAt, project.updatedAt, project.lastActiveAt ?? null);
90
+ brief, instructions, created_at, updated_at, last_active_at, pinned_at
91
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(project.id, project.name, project.slug, project.description ?? null, project.status, project.defaultAgentId ?? null, project.workspaceRoot ?? null, project.brief ?? null, project.instructions ?? null, project.createdAt, project.updatedAt, project.lastActiveAt ?? null, project.pinnedAt ?? null);
90
92
  syncProjectFts(db, project);
91
93
  });
92
94
  return project;
@@ -163,7 +165,11 @@ var ProjectStore = class {
163
165
  JOIN sessions s ON s.project_id = p.project_id
164
166
  ${where}
165
167
  GROUP BY p.project_id
166
- ORDER BY latest_session_at DESC, p.updated_at DESC
168
+ ORDER BY
169
+ CASE WHEN p.pinned_at IS NULL THEN 1 ELSE 0 END ASC,
170
+ p.pinned_at DESC,
171
+ latest_session_at DESC,
172
+ p.updated_at DESC
167
173
  LIMIT ? OFFSET ?`).all(...params, limit, offset).map(projectFromRow),
168
174
  total,
169
175
  limit,
@@ -183,12 +189,13 @@ var ProjectStore = class {
183
189
  ...input.workspaceRoot !== void 0 ? { workspaceRoot: nullableText(input.workspaceRoot) ?? void 0 } : {},
184
190
  ...input.brief !== void 0 ? { brief: nullableText(input.brief) ?? void 0 } : {},
185
191
  ...input.instructions !== void 0 ? { instructions: nullableText(input.instructions) ?? void 0 } : {},
192
+ ...input.pinnedAt !== void 0 ? { pinnedAt: input.pinnedAt ?? void 0 } : {},
186
193
  updatedAt: Date.now()
187
194
  };
188
195
  runSqliteWriteTransaction((db) => {
189
196
  db.prepare(`UPDATE projects SET
190
- name = ?, description = ?, status = ?, default_agent_id = ?, workspace_root = ?, brief = ?, instructions = ?, updated_at = ?
191
- WHERE project_id = ?`).run(next.name, next.description ?? null, next.status, next.defaultAgentId ?? null, next.workspaceRoot ?? null, next.brief ?? null, next.instructions ?? null, next.updatedAt, id);
197
+ name = ?, description = ?, status = ?, default_agent_id = ?, workspace_root = ?, brief = ?, instructions = ?, pinned_at = ?, updated_at = ?
198
+ WHERE project_id = ?`).run(next.name, next.description ?? null, next.status, next.defaultAgentId ?? null, next.workspaceRoot ?? null, next.brief ?? null, next.instructions ?? null, next.pinnedAt ?? null, next.updatedAt, id);
192
199
  syncProjectFts(db, next);
193
200
  });
194
201
  return this.get(id);
@@ -1 +1 @@
1
- {"version":3,"file":"project-store.js","names":[],"sources":["../../../src/projects/project-store.ts"],"sourcesContent":["import { randomUUID } from 'node:crypto';\n\nimport { escapeFts5Query } from '../storage/sqlite/fts.js';\nimport { getSqliteDatabase, runSqliteWriteTransaction } from '../storage/sqlite/transaction.js';\nimport type {\n CreateProjectInput,\n Project,\n ProjectListQuery,\n ProjectListResult,\n ProjectStatus,\n SidebarProjectListQuery,\n ProjectWithDetails,\n UpdateProjectInput,\n} from './types.js';\n\nexport type ProjectRow = {\n project_id: string;\n name: string;\n slug: string;\n description: string | null;\n status: string;\n default_agent_id: string | null;\n workspace_root: string | null;\n brief: string | null;\n instructions: string | null;\n created_at: number;\n updated_at: number;\n last_active_at: number | null;\n};\n\nfunction trimOptional(value: string | null | undefined): string | undefined {\n if (value == null) return undefined;\n const trimmed = value.trim();\n return trimmed || undefined;\n}\n\nfunction nullableText(value: string | null | undefined): string | null {\n if (value == null) return null;\n const trimmed = value.trim();\n return trimmed || null;\n}\n\nfunction projectFromRow(row: ProjectRow): Project {\n return {\n id: row.project_id,\n name: row.name,\n slug: row.slug,\n description: row.description ?? undefined,\n status: row.status as ProjectStatus,\n defaultAgentId: row.default_agent_id ?? undefined,\n workspaceRoot: row.workspace_root ?? undefined,\n brief: row.brief ?? undefined,\n instructions: row.instructions ?? undefined,\n createdAt: row.created_at,\n updatedAt: row.updated_at,\n lastActiveAt: row.last_active_at ?? undefined,\n };\n}\n\nfunction clampLimit(limit: number | undefined, fallback = 50): number {\n return Math.min(500, Math.max(1, Math.floor(limit ?? fallback)));\n}\n\nfunction normalizeName(name: string): string {\n const trimmed = name.trim();\n if (!trimmed) throw new Error('Project name is required');\n return trimmed;\n}\n\nexport function slugifyProjectName(name: string): string {\n const slug = name\n .trim()\n .toLowerCase()\n .normalize('NFKD')\n .replace(/[\\u0300-\\u036f]/g, '')\n .replace(/[^\\p{L}\\p{N}]+/gu, '-')\n .replace(/^-+|-+$/g, '')\n .slice(0, 80);\n return slug || `project-${Date.now()}`;\n}\n\nfunction projectSortColumn(sortBy: ProjectListQuery['sortBy']): string {\n switch (sortBy) {\n case 'createdAt':\n return 'created_at';\n case 'name':\n return 'LOWER(name)';\n case 'updatedAt':\n default:\n return 'updated_at';\n }\n}\n\nfunction projectSortExpression(sortBy: ProjectListQuery['sortBy']): string {\n const column = projectSortColumn(sortBy);\n return column === 'LOWER(name)' ? 'LOWER(p.name)' : `p.${column}`;\n}\n\nfunction projectFtsContent(project: Project): string {\n return [\n project.name,\n project.slug,\n project.description,\n project.workspaceRoot,\n project.brief,\n project.instructions,\n project.defaultAgentId,\n ]\n .filter((value): value is string => Boolean(value?.trim()))\n .join('\\n');\n}\n\nfunction syncProjectFts(\n db: ReturnType<typeof getSqliteDatabase>,\n project: Project,\n): void {\n db.prepare(`DELETE FROM projects_fts WHERE project_id = ?`).run(project.id);\n db.prepare(`INSERT INTO projects_fts (content, project_id) VALUES (?, ?)`).run(\n projectFtsContent(project),\n project.id,\n );\n}\n\nexport class ProjectStore {\n create(input: CreateProjectInput): Project {\n const now = Date.now();\n const name = normalizeName(input.name);\n const slug = input.slug?.trim() || slugifyProjectName(name);\n const project: Project = {\n id: randomUUID(),\n name,\n slug,\n description: trimOptional(input.description),\n status: 'active',\n defaultAgentId: trimOptional(input.defaultAgentId),\n workspaceRoot: trimOptional(input.workspaceRoot),\n brief: trimOptional(input.brief),\n instructions: trimOptional(input.instructions),\n createdAt: now,\n updatedAt: now,\n lastActiveAt: now,\n };\n\n runSqliteWriteTransaction((db) => {\n db.prepare(\n `INSERT INTO projects (\n project_id, name, slug, description, status, default_agent_id, workspace_root,\n brief, instructions, created_at, updated_at, last_active_at\n ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,\n ).run(\n project.id,\n project.name,\n project.slug,\n project.description ?? null,\n project.status,\n project.defaultAgentId ?? null,\n project.workspaceRoot ?? null,\n project.brief ?? null,\n project.instructions ?? null,\n project.createdAt,\n project.updatedAt,\n project.lastActiveAt ?? null,\n );\n syncProjectFts(db, project);\n });\n return project;\n }\n\n get(id: string): Project | null {\n const row = getSqliteDatabase()\n .prepare(`SELECT * FROM projects WHERE project_id = ?`)\n .get(id) as ProjectRow | undefined;\n return row ? projectFromRow(row) : null;\n }\n\n findBySlug(slug: string): Project | null {\n const row = getSqliteDatabase()\n .prepare(`SELECT * FROM projects WHERE slug = ?`)\n .get(slug.trim()) as ProjectRow | undefined;\n return row ? projectFromRow(row) : null;\n }\n\n list(query: ProjectListQuery = {}): ProjectListResult {\n const conditions: string[] = [];\n const params: Array<string | number> = [];\n const search = query.search?.trim();\n const ftsQuery = search ? escapeFts5Query(search) : '';\n if (query.status) {\n const statuses = Array.isArray(query.status) ? query.status : [query.status];\n conditions.push(`p.status IN (${statuses.map(() => '?').join(', ')})`);\n params.push(...statuses);\n }\n if (ftsQuery) {\n conditions.push(`p.project_id IN (SELECT project_id FROM projects_fts WHERE projects_fts MATCH ?)`);\n params.push(ftsQuery);\n }\n const where = conditions.length ? `WHERE ${conditions.join(' AND ')}` : '';\n const limit = clampLimit(query.limit, 50);\n const offset = Math.max(0, Math.floor(query.offset ?? 0));\n const sortOrder = query.sortOrder === 'asc' ? 'ASC' : 'DESC';\n const sortColumn = projectSortExpression(query.sortBy);\n const db = getSqliteDatabase();\n const total = (db.prepare(`SELECT COUNT(*) AS total FROM projects p ${where}`).get(...params) as { total: number }).total;\n const rows = db\n .prepare(`SELECT p.* FROM projects p ${where} ORDER BY ${sortColumn} ${sortOrder} LIMIT ? OFFSET ?`)\n .all(...params, limit, offset) as ProjectRow[];\n return { items: rows.map(projectFromRow), total, limit, offset, hasMore: offset + limit < total };\n }\n\n listWithSidebarSessions(query: SidebarProjectListQuery = {}): ProjectListResult {\n const projectConditions: string[] = [];\n const sessionConditions = [\n `s.hidden_from_session_list = 0`,\n `s.session_type = 'chat'`,\n ];\n const params: Array<string | number> = [];\n\n if (query.status) {\n const statuses = Array.isArray(query.status) ? query.status : [query.status];\n projectConditions.push(`p.status IN (${statuses.map(() => '?').join(', ')})`);\n params.push(...statuses);\n }\n\n if (query.updatedAfter !== undefined) {\n const clauses = [`s.updated_at >= ?`];\n params.push(query.updatedAfter);\n if (query.includePinned) {\n clauses.push(`s.status = 'pinned'`);\n }\n const includeSessionKey = query.includeSessionKey?.trim();\n if (includeSessionKey) {\n clauses.push(`s.session_key = ?`);\n params.push(includeSessionKey);\n }\n sessionConditions.push(`(${clauses.join(' OR ')})`);\n }\n\n const whereParts = [...projectConditions, ...sessionConditions];\n const where = whereParts.length ? `WHERE ${whereParts.join(' AND ')}` : '';\n const limit = clampLimit(query.limit, 50);\n const offset = Math.max(0, Math.floor(query.offset ?? 0));\n const db = getSqliteDatabase();\n const total = (db\n .prepare(\n `SELECT COUNT(DISTINCT p.project_id) AS total\n FROM projects p\n JOIN sessions s ON s.project_id = p.project_id\n ${where}`,\n )\n .get(...params) as { total: number }).total;\n const rows = db\n .prepare(\n `SELECT p.*, MAX(s.updated_at) AS latest_session_at\n FROM projects p\n JOIN sessions s ON s.project_id = p.project_id\n ${where}\n GROUP BY p.project_id\n ORDER BY latest_session_at DESC, p.updated_at DESC\n LIMIT ? OFFSET ?`,\n )\n .all(...params, limit, offset) as ProjectRow[];\n return { items: rows.map(projectFromRow), total, limit, offset, hasMore: offset + limit < total };\n }\n\n update(id: string, input: UpdateProjectInput): Project {\n const before = this.get(id);\n if (!before) throw new Error(`Project not found: ${id}`);\n const next = {\n ...before,\n ...(input.name !== undefined ? { name: normalizeName(input.name) } : {}),\n ...(input.description !== undefined ? { description: nullableText(input.description) ?? undefined } : {}),\n ...(input.status !== undefined ? { status: input.status } : {}),\n ...(input.defaultAgentId !== undefined ? { defaultAgentId: nullableText(input.defaultAgentId) ?? undefined } : {}),\n ...(input.workspaceRoot !== undefined ? { workspaceRoot: nullableText(input.workspaceRoot) ?? undefined } : {}),\n ...(input.brief !== undefined ? { brief: nullableText(input.brief) ?? undefined } : {}),\n ...(input.instructions !== undefined ? { instructions: nullableText(input.instructions) ?? undefined } : {}),\n updatedAt: Date.now(),\n } satisfies Project;\n\n runSqliteWriteTransaction((db) => {\n db.prepare(\n `UPDATE projects SET\n name = ?, description = ?, status = ?, default_agent_id = ?, workspace_root = ?, brief = ?, instructions = ?, updated_at = ?\n WHERE project_id = ?`,\n ).run(\n next.name,\n next.description ?? null,\n next.status,\n next.defaultAgentId ?? null,\n next.workspaceRoot ?? null,\n next.brief ?? null,\n next.instructions ?? null,\n next.updatedAt,\n id,\n );\n syncProjectFts(db, next);\n });\n return this.get(id)!;\n }\n\n delete(id: string): void {\n runSqliteWriteTransaction((db) => {\n db.prepare(`UPDATE sessions SET project_id = NULL WHERE project_id = ?`).run(id);\n db.prepare(`UPDATE goals SET project_id = NULL WHERE project_id = ?`).run(id);\n db.prepare(`UPDATE workflow_runs SET project_id = NULL WHERE project_id = ?`).run(id);\n db.prepare(`UPDATE automations SET project_id = NULL WHERE project_id = ?`).run(id);\n db.prepare(`UPDATE memory_records SET project_id = NULL WHERE project_id = ?`).run(id);\n db.prepare(`DELETE FROM projects_fts WHERE project_id = ?`).run(id);\n db.prepare(`DELETE FROM projects WHERE project_id = ?`).run(id);\n });\n }\n\n getSessionCount(id: string): number {\n return (getSqliteDatabase()\n .prepare(`SELECT COUNT(*) AS total FROM sessions WHERE project_id = ? AND hidden_from_session_list = 0`)\n .get(id) as { total: number }).total;\n }\n\n getGoalCount(id: string): number {\n return (getSqliteDatabase().prepare(`SELECT COUNT(*) AS total FROM goals WHERE project_id = ?`).get(id) as { total: number }).total;\n }\n\n getActiveGoalCount(id: string): number {\n return (getSqliteDatabase()\n .prepare(`SELECT COUNT(*) AS total FROM goals WHERE project_id = ? AND status IN ('active', 'paused', 'blocked', 'needs_input')`)\n .get(id) as { total: number }).total;\n }\n\n getRecentSessions(id: string, limit = 5): ProjectWithDetails['recentSessions'] {\n const rows = getSqliteDatabase()\n .prepare(\n `SELECT session_key, name, updated_at, agent_id FROM sessions\n WHERE project_id = ? AND hidden_from_session_list = 0 ORDER BY updated_at DESC LIMIT ?`,\n )\n .all(id, clampLimit(limit, 5)) as Array<{ session_key: string; name: string | null; updated_at: number; agent_id: string }>;\n return rows.map((row) => ({\n key: row.session_key,\n name: row.name ?? undefined,\n updatedAt: new Date(row.updated_at).toISOString(),\n agentId: row.agent_id,\n }));\n }\n\n getRecentWorkflowRuns(id: string, limit = 5): ProjectWithDetails['recentWorkflowRuns'] {\n const rows = getSqliteDatabase()\n .prepare(\n `SELECT run_id, definition_id, status, created_at_ms FROM workflow_runs\n WHERE project_id = ? ORDER BY created_at_ms DESC LIMIT ?`,\n )\n .all(id, clampLimit(limit, 5)) as Array<{ run_id: string; definition_id: string; status: string; created_at_ms: number }>;\n return rows.map((row) => ({ runId: row.run_id, definitionId: row.definition_id, status: row.status, createdAt: row.created_at_ms }));\n }\n\n getWithDetails(id: string): ProjectWithDetails | null {\n const project = this.get(id);\n if (!project) return null;\n return {\n ...project,\n sessionCount: this.getSessionCount(id),\n goalCount: this.getGoalCount(id),\n activeGoalCount: this.getActiveGoalCount(id),\n recentSessions: this.getRecentSessions(id),\n recentWorkflowRuns: this.getRecentWorkflowRuns(id),\n };\n }\n\n generateSlug(name: string): string {\n const base = slugifyProjectName(name);\n let candidate = base;\n let counter = 2;\n while (this.findBySlug(candidate)) {\n candidate = `${base}-${counter}`;\n counter += 1;\n }\n return candidate;\n }\n}\n"],"mappings":";;;;AA8BA,SAAS,aAAa,OAAsD;AAC1E,KAAI,SAAS,KAAM,QAAO,KAAA;AAE1B,QADgB,MAAM,MACR,IAAI,KAAA;;AAGpB,SAAS,aAAa,OAAiD;AACrE,KAAI,SAAS,KAAM,QAAO;AAE1B,QADgB,MAAM,MACR,IAAI;;AAGpB,SAAS,eAAe,KAA0B;AAChD,QAAO;EACL,IAAI,IAAI;EACR,MAAM,IAAI;EACV,MAAM,IAAI;EACV,aAAa,IAAI,eAAe,KAAA;EAChC,QAAQ,IAAI;EACZ,gBAAgB,IAAI,oBAAoB,KAAA;EACxC,eAAe,IAAI,kBAAkB,KAAA;EACrC,OAAO,IAAI,SAAS,KAAA;EACpB,cAAc,IAAI,gBAAgB,KAAA;EAClC,WAAW,IAAI;EACf,WAAW,IAAI;EACf,cAAc,IAAI,kBAAkB,KAAA;EACrC;;AAGH,SAAS,WAAW,OAA2B,WAAW,IAAY;AACpE,QAAO,KAAK,IAAI,KAAK,KAAK,IAAI,GAAG,KAAK,MAAM,SAAS,SAAS,CAAC,CAAC;;AAGlE,SAAS,cAAc,MAAsB;CAC3C,MAAM,UAAU,KAAK,MAAM;AAC3B,KAAI,CAAC,QAAS,OAAM,IAAI,MAAM,2BAA2B;AACzD,QAAO;;AAGT,SAAgB,mBAAmB,MAAsB;AASvD,QARa,KACV,MAAM,CACN,aAAa,CACb,UAAU,OAAO,CACjB,QAAQ,oBAAoB,GAAG,CAC/B,QAAQ,oBAAoB,IAAI,CAChC,QAAQ,YAAY,GAAG,CACvB,MAAM,GAAG,GACD,IAAI,WAAW,KAAK,KAAK;;AAGtC,SAAS,kBAAkB,QAA4C;AACrE,SAAQ,QAAR;EACE,KAAK,YACH,QAAO;EACT,KAAK,OACH,QAAO;EAET,QACE,QAAO;;;AAIb,SAAS,sBAAsB,QAA4C;CACzE,MAAM,SAAS,kBAAkB,OAAO;AACxC,QAAO,WAAW,gBAAgB,kBAAkB,KAAK;;AAG3D,SAAS,kBAAkB,SAA0B;AACnD,QAAO;EACL,QAAQ;EACR,QAAQ;EACR,QAAQ;EACR,QAAQ;EACR,QAAQ;EACR,QAAQ;EACR,QAAQ;EACT,CACE,QAAQ,UAA2B,QAAQ,OAAO,MAAM,CAAC,CAAC,CAC1D,KAAK,KAAK;;AAGf,SAAS,eACP,IACA,SACM;AACN,IAAG,QAAQ,gDAAgD,CAAC,IAAI,QAAQ,GAAG;AAC3E,IAAG,QAAQ,+DAA+D,CAAC,IACzE,kBAAkB,QAAQ,EAC1B,QAAQ,GACT;;AAGH,IAAa,eAAb,MAA0B;CACxB,OAAO,OAAoC;EACzC,MAAM,MAAM,KAAK,KAAK;EACtB,MAAM,OAAO,cAAc,MAAM,KAAK;EACtC,MAAM,OAAO,MAAM,MAAM,MAAM,IAAI,mBAAmB,KAAK;EAC3D,MAAM,UAAmB;GACvB,IAAI,YAAY;GAChB;GACA;GACA,aAAa,aAAa,MAAM,YAAY;GAC5C,QAAQ;GACR,gBAAgB,aAAa,MAAM,eAAe;GAClD,eAAe,aAAa,MAAM,cAAc;GAChD,OAAO,aAAa,MAAM,MAAM;GAChC,cAAc,aAAa,MAAM,aAAa;GAC9C,WAAW;GACX,WAAW;GACX,cAAc;GACf;AAED,6BAA2B,OAAO;AAChC,MAAG,QACD;;;uDAID,CAAC,IACA,QAAQ,IACR,QAAQ,MACR,QAAQ,MACR,QAAQ,eAAe,MACvB,QAAQ,QACR,QAAQ,kBAAkB,MAC1B,QAAQ,iBAAiB,MACzB,QAAQ,SAAS,MACjB,QAAQ,gBAAgB,MACxB,QAAQ,WACR,QAAQ,WACR,QAAQ,gBAAgB,KACzB;AACD,kBAAe,IAAI,QAAQ;IAC3B;AACF,SAAO;;CAGT,IAAI,IAA4B;EAC9B,MAAM,MAAM,mBAAmB,CAC5B,QAAQ,8CAA8C,CACtD,IAAI,GAAG;AACV,SAAO,MAAM,eAAe,IAAI,GAAG;;CAGrC,WAAW,MAA8B;EACvC,MAAM,MAAM,mBAAmB,CAC5B,QAAQ,wCAAwC,CAChD,IAAI,KAAK,MAAM,CAAC;AACnB,SAAO,MAAM,eAAe,IAAI,GAAG;;CAGrC,KAAK,QAA0B,EAAE,EAAqB;EACpD,MAAM,aAAuB,EAAE;EAC/B,MAAM,SAAiC,EAAE;EACzC,MAAM,SAAS,MAAM,QAAQ,MAAM;EACnC,MAAM,WAAW,SAAS,gBAAgB,OAAO,GAAG;AACpD,MAAI,MAAM,QAAQ;GAChB,MAAM,WAAW,MAAM,QAAQ,MAAM,OAAO,GAAG,MAAM,SAAS,CAAC,MAAM,OAAO;AAC5E,cAAW,KAAK,gBAAgB,SAAS,UAAU,IAAI,CAAC,KAAK,KAAK,CAAC,GAAG;AACtE,UAAO,KAAK,GAAG,SAAS;;AAE1B,MAAI,UAAU;AACZ,cAAW,KAAK,mFAAmF;AACnG,UAAO,KAAK,SAAS;;EAEvB,MAAM,QAAQ,WAAW,SAAS,SAAS,WAAW,KAAK,QAAQ,KAAK;EACxE,MAAM,QAAQ,WAAW,MAAM,OAAO,GAAG;EACzC,MAAM,SAAS,KAAK,IAAI,GAAG,KAAK,MAAM,MAAM,UAAU,EAAE,CAAC;EACzD,MAAM,YAAY,MAAM,cAAc,QAAQ,QAAQ;EACtD,MAAM,aAAa,sBAAsB,MAAM,OAAO;EACtD,MAAM,KAAK,mBAAmB;EAC9B,MAAM,QAAS,GAAG,QAAQ,4CAA4C,QAAQ,CAAC,IAAI,GAAG,OAAO,CAAuB;AAIpH,SAAO;GAAE,OAHI,GACV,QAAQ,8BAA8B,MAAM,YAAY,WAAW,GAAG,UAAU,mBAAmB,CACnG,IAAI,GAAG,QAAQ,OAAO,OACL,CAAC,IAAI,eAAe;GAAE;GAAO;GAAO;GAAQ,SAAS,SAAS,QAAQ;GAAO;;CAGnG,wBAAwB,QAAiC,EAAE,EAAqB;EAC9E,MAAM,oBAA8B,EAAE;EACtC,MAAM,oBAAoB,CACxB,kCACA,0BACD;EACD,MAAM,SAAiC,EAAE;AAEzC,MAAI,MAAM,QAAQ;GAChB,MAAM,WAAW,MAAM,QAAQ,MAAM,OAAO,GAAG,MAAM,SAAS,CAAC,MAAM,OAAO;AAC5E,qBAAkB,KAAK,gBAAgB,SAAS,UAAU,IAAI,CAAC,KAAK,KAAK,CAAC,GAAG;AAC7E,UAAO,KAAK,GAAG,SAAS;;AAG1B,MAAI,MAAM,iBAAiB,KAAA,GAAW;GACpC,MAAM,UAAU,CAAC,oBAAoB;AACrC,UAAO,KAAK,MAAM,aAAa;AAC/B,OAAI,MAAM,cACR,SAAQ,KAAK,sBAAsB;GAErC,MAAM,oBAAoB,MAAM,mBAAmB,MAAM;AACzD,OAAI,mBAAmB;AACrB,YAAQ,KAAK,oBAAoB;AACjC,WAAO,KAAK,kBAAkB;;AAEhC,qBAAkB,KAAK,IAAI,QAAQ,KAAK,OAAO,CAAC,GAAG;;EAGrD,MAAM,aAAa,CAAC,GAAG,mBAAmB,GAAG,kBAAkB;EAC/D,MAAM,QAAQ,WAAW,SAAS,SAAS,WAAW,KAAK,QAAQ,KAAK;EACxE,MAAM,QAAQ,WAAW,MAAM,OAAO,GAAG;EACzC,MAAM,SAAS,KAAK,IAAI,GAAG,KAAK,MAAM,MAAM,UAAU,EAAE,CAAC;EACzD,MAAM,KAAK,mBAAmB;EAC9B,MAAM,QAAS,GACZ,QACC;;;WAGG,QACJ,CACA,IAAI,GAAG,OAAO,CAAuB;AAYxC,SAAO;GAAE,OAXI,GACV,QACC;;;WAGG,MAAM;;;2BAIV,CACA,IAAI,GAAG,QAAQ,OAAO,OACL,CAAC,IAAI,eAAe;GAAE;GAAO;GAAO;GAAQ,SAAS,SAAS,QAAQ;GAAO;;CAGnG,OAAO,IAAY,OAAoC;EACrD,MAAM,SAAS,KAAK,IAAI,GAAG;AAC3B,MAAI,CAAC,OAAQ,OAAM,IAAI,MAAM,sBAAsB,KAAK;EACxD,MAAM,OAAO;GACX,GAAG;GACH,GAAI,MAAM,SAAS,KAAA,IAAY,EAAE,MAAM,cAAc,MAAM,KAAK,EAAE,GAAG,EAAE;GACvE,GAAI,MAAM,gBAAgB,KAAA,IAAY,EAAE,aAAa,aAAa,MAAM,YAAY,IAAI,KAAA,GAAW,GAAG,EAAE;GACxG,GAAI,MAAM,WAAW,KAAA,IAAY,EAAE,QAAQ,MAAM,QAAQ,GAAG,EAAE;GAC9D,GAAI,MAAM,mBAAmB,KAAA,IAAY,EAAE,gBAAgB,aAAa,MAAM,eAAe,IAAI,KAAA,GAAW,GAAG,EAAE;GACjH,GAAI,MAAM,kBAAkB,KAAA,IAAY,EAAE,eAAe,aAAa,MAAM,cAAc,IAAI,KAAA,GAAW,GAAG,EAAE;GAC9G,GAAI,MAAM,UAAU,KAAA,IAAY,EAAE,OAAO,aAAa,MAAM,MAAM,IAAI,KAAA,GAAW,GAAG,EAAE;GACtF,GAAI,MAAM,iBAAiB,KAAA,IAAY,EAAE,cAAc,aAAa,MAAM,aAAa,IAAI,KAAA,GAAW,GAAG,EAAE;GAC3G,WAAW,KAAK,KAAK;GACtB;AAED,6BAA2B,OAAO;AAChC,MAAG,QACD;;+BAGD,CAAC,IACA,KAAK,MACL,KAAK,eAAe,MACpB,KAAK,QACL,KAAK,kBAAkB,MACvB,KAAK,iBAAiB,MACtB,KAAK,SAAS,MACd,KAAK,gBAAgB,MACrB,KAAK,WACL,GACD;AACD,kBAAe,IAAI,KAAK;IACxB;AACF,SAAO,KAAK,IAAI,GAAG;;CAGrB,OAAO,IAAkB;AACvB,6BAA2B,OAAO;AAChC,MAAG,QAAQ,6DAA6D,CAAC,IAAI,GAAG;AAChF,MAAG,QAAQ,0DAA0D,CAAC,IAAI,GAAG;AAC7E,MAAG,QAAQ,kEAAkE,CAAC,IAAI,GAAG;AACrF,MAAG,QAAQ,gEAAgE,CAAC,IAAI,GAAG;AACnF,MAAG,QAAQ,mEAAmE,CAAC,IAAI,GAAG;AACtF,MAAG,QAAQ,gDAAgD,CAAC,IAAI,GAAG;AACnE,MAAG,QAAQ,4CAA4C,CAAC,IAAI,GAAG;IAC/D;;CAGJ,gBAAgB,IAAoB;AAClC,SAAQ,mBAAmB,CACxB,QAAQ,+FAA+F,CACvG,IAAI,GAAG,CAAuB;;CAGnC,aAAa,IAAoB;AAC/B,SAAQ,mBAAmB,CAAC,QAAQ,2DAA2D,CAAC,IAAI,GAAG,CAAuB;;CAGhI,mBAAmB,IAAoB;AACrC,SAAQ,mBAAmB,CACxB,QAAQ,wHAAwH,CAChI,IAAI,GAAG,CAAuB;;CAGnC,kBAAkB,IAAY,QAAQ,GAAyC;AAO7E,SANa,mBAAmB,CAC7B,QACC;iGAED,CACA,IAAI,IAAI,WAAW,OAAO,EAAE,CACpB,CAAC,KAAK,SAAS;GACxB,KAAK,IAAI;GACT,MAAM,IAAI,QAAQ,KAAA;GAClB,WAAW,IAAI,KAAK,IAAI,WAAW,CAAC,aAAa;GACjD,SAAS,IAAI;GACd,EAAE;;CAGL,sBAAsB,IAAY,QAAQ,GAA6C;AAOrF,SANa,mBAAmB,CAC7B,QACC;mEAED,CACA,IAAI,IAAI,WAAW,OAAO,EAAE,CACpB,CAAC,KAAK,SAAS;GAAE,OAAO,IAAI;GAAQ,cAAc,IAAI;GAAe,QAAQ,IAAI;GAAQ,WAAW,IAAI;GAAe,EAAE;;CAGtI,eAAe,IAAuC;EACpD,MAAM,UAAU,KAAK,IAAI,GAAG;AAC5B,MAAI,CAAC,QAAS,QAAO;AACrB,SAAO;GACL,GAAG;GACH,cAAc,KAAK,gBAAgB,GAAG;GACtC,WAAW,KAAK,aAAa,GAAG;GAChC,iBAAiB,KAAK,mBAAmB,GAAG;GAC5C,gBAAgB,KAAK,kBAAkB,GAAG;GAC1C,oBAAoB,KAAK,sBAAsB,GAAG;GACnD;;CAGH,aAAa,MAAsB;EACjC,MAAM,OAAO,mBAAmB,KAAK;EACrC,IAAI,YAAY;EAChB,IAAI,UAAU;AACd,SAAO,KAAK,WAAW,UAAU,EAAE;AACjC,eAAY,GAAG,KAAK,GAAG;AACvB,cAAW;;AAEb,SAAO"}
1
+ {"version":3,"file":"project-store.js","names":[],"sources":["../../../src/projects/project-store.ts"],"sourcesContent":["import { randomUUID } from 'node:crypto';\n\nimport { escapeFts5Query } from '../storage/sqlite/fts.js';\nimport { getSqliteDatabase, runSqliteWriteTransaction } from '../storage/sqlite/transaction.js';\nimport type {\n CreateProjectInput,\n Project,\n ProjectListQuery,\n ProjectListResult,\n ProjectStatus,\n SidebarProjectListQuery,\n ProjectWithDetails,\n UpdateProjectInput,\n} from './types.js';\n\nexport type ProjectRow = {\n project_id: string;\n name: string;\n slug: string;\n description: string | null;\n status: string;\n default_agent_id: string | null;\n workspace_root: string | null;\n brief: string | null;\n instructions: string | null;\n created_at: number;\n updated_at: number;\n last_active_at: number | null;\n pinned_at: number | null;\n};\n\nfunction trimOptional(value: string | null | undefined): string | undefined {\n if (value == null) return undefined;\n const trimmed = value.trim();\n return trimmed || undefined;\n}\n\nfunction nullableText(value: string | null | undefined): string | null {\n if (value == null) return null;\n const trimmed = value.trim();\n return trimmed || null;\n}\n\nfunction projectFromRow(row: ProjectRow): Project {\n return {\n id: row.project_id,\n name: row.name,\n slug: row.slug,\n description: row.description ?? undefined,\n status: row.status as ProjectStatus,\n defaultAgentId: row.default_agent_id ?? undefined,\n workspaceRoot: row.workspace_root ?? undefined,\n brief: row.brief ?? undefined,\n instructions: row.instructions ?? undefined,\n createdAt: row.created_at,\n updatedAt: row.updated_at,\n lastActiveAt: row.last_active_at ?? undefined,\n pinnedAt: row.pinned_at ?? undefined,\n };\n}\n\nfunction clampLimit(limit: number | undefined, fallback = 50): number {\n return Math.min(500, Math.max(1, Math.floor(limit ?? fallback)));\n}\n\nfunction normalizeName(name: string): string {\n const trimmed = name.trim();\n if (!trimmed) throw new Error('Project name is required');\n return trimmed;\n}\n\nexport function slugifyProjectName(name: string): string {\n const slug = name\n .trim()\n .toLowerCase()\n .normalize('NFKD')\n .replace(/[\\u0300-\\u036f]/g, '')\n .replace(/[^\\p{L}\\p{N}]+/gu, '-')\n .replace(/^-+|-+$/g, '')\n .slice(0, 80);\n return slug || `project-${Date.now()}`;\n}\n\nfunction projectSortColumn(sortBy: ProjectListQuery['sortBy']): string {\n switch (sortBy) {\n case 'createdAt':\n return 'created_at';\n case 'name':\n return 'LOWER(name)';\n case 'updatedAt':\n default:\n return 'updated_at';\n }\n}\n\nfunction projectSortExpression(sortBy: ProjectListQuery['sortBy']): string {\n const column = projectSortColumn(sortBy);\n return column === 'LOWER(name)' ? 'LOWER(p.name)' : `p.${column}`;\n}\n\nfunction projectFtsContent(project: Project): string {\n return [\n project.name,\n project.slug,\n project.description,\n project.workspaceRoot,\n project.brief,\n project.instructions,\n project.defaultAgentId,\n ]\n .filter((value): value is string => Boolean(value?.trim()))\n .join('\\n');\n}\n\nfunction syncProjectFts(\n db: ReturnType<typeof getSqliteDatabase>,\n project: Project,\n): void {\n db.prepare(`DELETE FROM projects_fts WHERE project_id = ?`).run(project.id);\n db.prepare(`INSERT INTO projects_fts (content, project_id) VALUES (?, ?)`).run(\n projectFtsContent(project),\n project.id,\n );\n}\n\nexport class ProjectStore {\n create(input: CreateProjectInput): Project {\n const now = Date.now();\n const name = normalizeName(input.name);\n const slug = input.slug?.trim() || slugifyProjectName(name);\n const project: Project = {\n id: randomUUID(),\n name,\n slug,\n description: trimOptional(input.description),\n status: 'active',\n defaultAgentId: trimOptional(input.defaultAgentId),\n workspaceRoot: trimOptional(input.workspaceRoot),\n brief: trimOptional(input.brief),\n instructions: trimOptional(input.instructions),\n createdAt: now,\n updatedAt: now,\n lastActiveAt: now,\n pinnedAt: undefined,\n };\n\n runSqliteWriteTransaction((db) => {\n db.prepare(\n `INSERT INTO projects (\n project_id, name, slug, description, status, default_agent_id, workspace_root,\n brief, instructions, created_at, updated_at, last_active_at, pinned_at\n ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,\n ).run(\n project.id,\n project.name,\n project.slug,\n project.description ?? null,\n project.status,\n project.defaultAgentId ?? null,\n project.workspaceRoot ?? null,\n project.brief ?? null,\n project.instructions ?? null,\n project.createdAt,\n project.updatedAt,\n project.lastActiveAt ?? null,\n project.pinnedAt ?? null,\n );\n syncProjectFts(db, project);\n });\n return project;\n }\n\n get(id: string): Project | null {\n const row = getSqliteDatabase()\n .prepare(`SELECT * FROM projects WHERE project_id = ?`)\n .get(id) as ProjectRow | undefined;\n return row ? projectFromRow(row) : null;\n }\n\n findBySlug(slug: string): Project | null {\n const row = getSqliteDatabase()\n .prepare(`SELECT * FROM projects WHERE slug = ?`)\n .get(slug.trim()) as ProjectRow | undefined;\n return row ? projectFromRow(row) : null;\n }\n\n list(query: ProjectListQuery = {}): ProjectListResult {\n const conditions: string[] = [];\n const params: Array<string | number> = [];\n const search = query.search?.trim();\n const ftsQuery = search ? escapeFts5Query(search) : '';\n if (query.status) {\n const statuses = Array.isArray(query.status) ? query.status : [query.status];\n conditions.push(`p.status IN (${statuses.map(() => '?').join(', ')})`);\n params.push(...statuses);\n }\n if (ftsQuery) {\n conditions.push(`p.project_id IN (SELECT project_id FROM projects_fts WHERE projects_fts MATCH ?)`);\n params.push(ftsQuery);\n }\n const where = conditions.length ? `WHERE ${conditions.join(' AND ')}` : '';\n const limit = clampLimit(query.limit, 50);\n const offset = Math.max(0, Math.floor(query.offset ?? 0));\n const sortOrder = query.sortOrder === 'asc' ? 'ASC' : 'DESC';\n const sortColumn = projectSortExpression(query.sortBy);\n const db = getSqliteDatabase();\n const total = (db.prepare(`SELECT COUNT(*) AS total FROM projects p ${where}`).get(...params) as { total: number }).total;\n const rows = db\n .prepare(`SELECT p.* FROM projects p ${where} ORDER BY ${sortColumn} ${sortOrder} LIMIT ? OFFSET ?`)\n .all(...params, limit, offset) as ProjectRow[];\n return { items: rows.map(projectFromRow), total, limit, offset, hasMore: offset + limit < total };\n }\n\n listWithSidebarSessions(query: SidebarProjectListQuery = {}): ProjectListResult {\n const projectConditions: string[] = [];\n const sessionConditions = [\n `s.hidden_from_session_list = 0`,\n `s.session_type = 'chat'`,\n ];\n const params: Array<string | number> = [];\n\n if (query.status) {\n const statuses = Array.isArray(query.status) ? query.status : [query.status];\n projectConditions.push(`p.status IN (${statuses.map(() => '?').join(', ')})`);\n params.push(...statuses);\n }\n\n if (query.updatedAfter !== undefined) {\n const clauses = [`s.updated_at >= ?`];\n params.push(query.updatedAfter);\n if (query.includePinned) {\n clauses.push(`s.status = 'pinned'`);\n }\n const includeSessionKey = query.includeSessionKey?.trim();\n if (includeSessionKey) {\n clauses.push(`s.session_key = ?`);\n params.push(includeSessionKey);\n }\n sessionConditions.push(`(${clauses.join(' OR ')})`);\n }\n\n const whereParts = [...projectConditions, ...sessionConditions];\n const where = whereParts.length ? `WHERE ${whereParts.join(' AND ')}` : '';\n const limit = clampLimit(query.limit, 50);\n const offset = Math.max(0, Math.floor(query.offset ?? 0));\n const db = getSqliteDatabase();\n const total = (db\n .prepare(\n `SELECT COUNT(DISTINCT p.project_id) AS total\n FROM projects p\n JOIN sessions s ON s.project_id = p.project_id\n ${where}`,\n )\n .get(...params) as { total: number }).total;\n const rows = db\n .prepare(\n `SELECT p.*, MAX(s.updated_at) AS latest_session_at\n FROM projects p\n JOIN sessions s ON s.project_id = p.project_id\n ${where}\n GROUP BY p.project_id\n ORDER BY\n CASE WHEN p.pinned_at IS NULL THEN 1 ELSE 0 END ASC,\n p.pinned_at DESC,\n latest_session_at DESC,\n p.updated_at DESC\n LIMIT ? OFFSET ?`,\n )\n .all(...params, limit, offset) as ProjectRow[];\n return { items: rows.map(projectFromRow), total, limit, offset, hasMore: offset + limit < total };\n }\n\n update(id: string, input: UpdateProjectInput): Project {\n const before = this.get(id);\n if (!before) throw new Error(`Project not found: ${id}`);\n const next = {\n ...before,\n ...(input.name !== undefined ? { name: normalizeName(input.name) } : {}),\n ...(input.description !== undefined ? { description: nullableText(input.description) ?? undefined } : {}),\n ...(input.status !== undefined ? { status: input.status } : {}),\n ...(input.defaultAgentId !== undefined ? { defaultAgentId: nullableText(input.defaultAgentId) ?? undefined } : {}),\n ...(input.workspaceRoot !== undefined ? { workspaceRoot: nullableText(input.workspaceRoot) ?? undefined } : {}),\n ...(input.brief !== undefined ? { brief: nullableText(input.brief) ?? undefined } : {}),\n ...(input.instructions !== undefined ? { instructions: nullableText(input.instructions) ?? undefined } : {}),\n ...(input.pinnedAt !== undefined ? { pinnedAt: input.pinnedAt ?? undefined } : {}),\n updatedAt: Date.now(),\n } satisfies Project;\n\n runSqliteWriteTransaction((db) => {\n db.prepare(\n `UPDATE projects SET\n name = ?, description = ?, status = ?, default_agent_id = ?, workspace_root = ?, brief = ?, instructions = ?, pinned_at = ?, updated_at = ?\n WHERE project_id = ?`,\n ).run(\n next.name,\n next.description ?? null,\n next.status,\n next.defaultAgentId ?? null,\n next.workspaceRoot ?? null,\n next.brief ?? null,\n next.instructions ?? null,\n next.pinnedAt ?? null,\n next.updatedAt,\n id,\n );\n syncProjectFts(db, next);\n });\n return this.get(id)!;\n }\n\n delete(id: string): void {\n runSqliteWriteTransaction((db) => {\n db.prepare(`UPDATE sessions SET project_id = NULL WHERE project_id = ?`).run(id);\n db.prepare(`UPDATE goals SET project_id = NULL WHERE project_id = ?`).run(id);\n db.prepare(`UPDATE workflow_runs SET project_id = NULL WHERE project_id = ?`).run(id);\n db.prepare(`UPDATE automations SET project_id = NULL WHERE project_id = ?`).run(id);\n db.prepare(`UPDATE memory_records SET project_id = NULL WHERE project_id = ?`).run(id);\n db.prepare(`DELETE FROM projects_fts WHERE project_id = ?`).run(id);\n db.prepare(`DELETE FROM projects WHERE project_id = ?`).run(id);\n });\n }\n\n getSessionCount(id: string): number {\n return (getSqliteDatabase()\n .prepare(`SELECT COUNT(*) AS total FROM sessions WHERE project_id = ? AND hidden_from_session_list = 0`)\n .get(id) as { total: number }).total;\n }\n\n getGoalCount(id: string): number {\n return (getSqliteDatabase().prepare(`SELECT COUNT(*) AS total FROM goals WHERE project_id = ?`).get(id) as { total: number }).total;\n }\n\n getActiveGoalCount(id: string): number {\n return (getSqliteDatabase()\n .prepare(`SELECT COUNT(*) AS total FROM goals WHERE project_id = ? AND status IN ('active', 'paused', 'blocked', 'needs_input')`)\n .get(id) as { total: number }).total;\n }\n\n getRecentSessions(id: string, limit = 5): ProjectWithDetails['recentSessions'] {\n const rows = getSqliteDatabase()\n .prepare(\n `SELECT session_key, name, updated_at, agent_id FROM sessions\n WHERE project_id = ? AND hidden_from_session_list = 0 ORDER BY updated_at DESC LIMIT ?`,\n )\n .all(id, clampLimit(limit, 5)) as Array<{ session_key: string; name: string | null; updated_at: number; agent_id: string }>;\n return rows.map((row) => ({\n key: row.session_key,\n name: row.name ?? undefined,\n updatedAt: new Date(row.updated_at).toISOString(),\n agentId: row.agent_id,\n }));\n }\n\n getRecentWorkflowRuns(id: string, limit = 5): ProjectWithDetails['recentWorkflowRuns'] {\n const rows = getSqliteDatabase()\n .prepare(\n `SELECT run_id, definition_id, status, created_at_ms FROM workflow_runs\n WHERE project_id = ? ORDER BY created_at_ms DESC LIMIT ?`,\n )\n .all(id, clampLimit(limit, 5)) as Array<{ run_id: string; definition_id: string; status: string; created_at_ms: number }>;\n return rows.map((row) => ({ runId: row.run_id, definitionId: row.definition_id, status: row.status, createdAt: row.created_at_ms }));\n }\n\n getWithDetails(id: string): ProjectWithDetails | null {\n const project = this.get(id);\n if (!project) return null;\n return {\n ...project,\n sessionCount: this.getSessionCount(id),\n goalCount: this.getGoalCount(id),\n activeGoalCount: this.getActiveGoalCount(id),\n recentSessions: this.getRecentSessions(id),\n recentWorkflowRuns: this.getRecentWorkflowRuns(id),\n };\n }\n\n generateSlug(name: string): string {\n const base = slugifyProjectName(name);\n let candidate = base;\n let counter = 2;\n while (this.findBySlug(candidate)) {\n candidate = `${base}-${counter}`;\n counter += 1;\n }\n return candidate;\n }\n}\n"],"mappings":"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"}
@@ -15,6 +15,7 @@ export interface Project {
15
15
  createdAt: number;
16
16
  updatedAt: number;
17
17
  lastActiveAt?: number;
18
+ pinnedAt?: number;
18
19
  }
19
20
  export interface ProjectWithDetails extends Project {
20
21
  sessionCount: number;
@@ -69,5 +70,6 @@ export interface UpdateProjectInput {
69
70
  createWorkspaceRoot?: boolean;
70
71
  brief?: string | null;
71
72
  instructions?: string | null;
73
+ pinnedAt?: number | null;
72
74
  }
73
75
  export type ProjectListResult = PaginatedResult<Project>;
@@ -32,12 +32,32 @@ function extractTextFromMessage(m) {
32
32
  function firstUserText(messages) {
33
33
  const u = messages.find((m) => m.role === "user");
34
34
  if (!u) return "";
35
- return stripMediaClaimChecks(stripEnvelopeTimestampPrefix(stripSessionStartupContextFromUserText(extractTextFromMessage(u))));
35
+ return normalizeUserTextForTitle(extractTextFromMessage(u));
36
36
  }
37
37
  function stripMediaClaimChecks(text) {
38
38
  if (!text.includes("[media attached:") && !text.includes("xopc-media-uri:")) return text;
39
39
  return text.replace(/\s*\[media attached:[^\]]+\]\s*\r?\nxopc-media-uri:[^\r\n]+\r?\n\s*xopc-media-path:[^\r\n]+(?:\r?\n\s*Use the read_media tool[^\r\n]*)?/g, "").replace(/\s*\[media attached:[^\]]+\]\s*/g, " ").replace(/\s*xopc-media-uri:[^\r\n]+/g, "").replace(/\s*xopc-media-path:[^\r\n]+/g, "").replace(/\s*Use the read_media tool[^\r\n]*/g, "").replace(/\n{3,}/g, "\n\n").trim();
40
40
  }
41
+ function stripExpandedSkillBlockForTitle(text) {
42
+ const trimmed = text.trimStart();
43
+ if (!/^##\s+Skill:\s*\S+/i.test(trimmed)) return text;
44
+ const markerRe = /(?:^|\r?\n)\*\*Arguments\*\*:\s*/gi;
45
+ let argsStart = -1;
46
+ while (markerRe.exec(trimmed) !== null) argsStart = markerRe.lastIndex;
47
+ if (argsStart !== -1) {
48
+ const args = trimmed.slice(argsStart).trim();
49
+ if (args) return args;
50
+ }
51
+ return (trimmed.split(/\r?\n/)[0]?.trim() ?? "").replace(/^##\s+Skill:\s*/i, "").trim();
52
+ }
53
+ function stripSkillCommandForTitle(text) {
54
+ const match = text.trimStart().match(/^\/skill:([^\s]+)(?:\s+([\s\S]*))?$/i);
55
+ if (!match) return text;
56
+ return match[2]?.trim() || match[1]?.trim() || text;
57
+ }
58
+ function normalizeUserTextForTitle(raw) {
59
+ return stripSkillCommandForTitle(stripExpandedSkillBlockForTitle(stripMediaClaimChecks(stripEnvelopeTimestampPrefix(stripSessionStartupContextFromUserText(raw))))).trim();
60
+ }
41
61
  /** First assistant message that has visible text (skips tool-only assistant rows). */
42
62
  function firstAssistantText(messages) {
43
63
  for (const m of messages) if (m.role === "assistant") {
@@ -73,7 +93,7 @@ function getSessionTitleSource(meta) {
73
93
  }
74
94
  /** Title from a single user message (first line), for immediate sidebar labels. */
75
95
  function provisionalTitleFromUserText(raw) {
76
- const text = stripEnvelopeTimestampPrefix(stripSessionStartupContextFromUserText((raw ?? "").trim()));
96
+ const text = normalizeUserTextForTitle((raw ?? "").trim());
77
97
  if (!text) return null;
78
98
  const line = text.split(/\n/)[0]?.trim();
79
99
  if (!line) return null;
@@ -1 +1 @@
1
- {"version":3,"file":"session-title.js","names":[],"sources":["../../../src/session/session-title.ts"],"sourcesContent":["/**\n * LLM-generated session titles (webchat and any path using SessionStore).\n */\n\nimport type { AgentMessage } from '@earendil-works/pi-agent-core';\nimport { complete, type UserMessage } from '@earendil-works/pi-ai/compat';\n\nimport { stripSessionStartupContextFromUserText } from '../agent/reply/startup-context.js';\nimport { stripEnvelopeTimestampPrefix } from '../channels/envelope-timestamp.js';\nimport { isCronSessionKey, parseSessionKey } from '../routing/session-key.js';\nimport { resolveModel } from '../providers/index.js';\nimport { createLogger } from '../utils/logger.js';\nimport { readAgentMessageContent } from '../agent/memory/agent-message-access.js';\nimport type { SessionStore } from './store.js';\n\nconst log = createLogger('SessionAutoTitle');\n\nconst MAX_TITLE_LEN = 80;\n\n/** Collect visible text from any content block that exposes `text` (pi-ai / OpenAI / Anthropic shapes). */\nfunction extractTextFromMessage(m: AgentMessage): string {\n const raw = readAgentMessageContent(m);\n if (typeof raw === 'string') return raw.trim();\n if (Array.isArray(raw)) {\n const parts: string[] = [];\n for (const c of raw) {\n if (c && typeof c === 'object') {\n const o = c as unknown as Record<string, unknown>;\n const type = typeof o.type === 'string' ? o.type : '';\n if (type === 'toolCall' || type === 'tool_use' || type === 'tool_result') continue;\n if (typeof o.text === 'string' && o.text.trim()) {\n parts.push(o.text.trim());\n }\n }\n }\n return parts.join(' ').trim();\n }\n return '';\n}\n\nfunction firstUserText(messages: AgentMessage[]): string {\n const u = messages.find((m) => m.role === 'user');\n if (!u) return '';\n const raw = extractTextFromMessage(u);\n return stripMediaClaimChecks(stripEnvelopeTimestampPrefix(stripSessionStartupContextFromUserText(raw)));\n}\n\nfunction stripMediaClaimChecks(text: string): string {\n if (!text.includes('[media attached:') && !text.includes('xopc-media-uri:')) {\n return text;\n }\n return text\n .replace(\n /\\s*\\[media attached:[^\\]]+\\]\\s*\\r?\\nxopc-media-uri:[^\\r\\n]+\\r?\\n\\s*xopc-media-path:[^\\r\\n]+(?:\\r?\\n\\s*Use the read_media tool[^\\r\\n]*)?/g,\n '',\n )\n .replace(/\\s*\\[media attached:[^\\]]+\\]\\s*/g, ' ')\n .replace(/\\s*xopc-media-uri:[^\\r\\n]+/g, '')\n .replace(/\\s*xopc-media-path:[^\\r\\n]+/g, '')\n .replace(/\\s*Use the read_media tool[^\\r\\n]*/g, '')\n .replace(/\\n{3,}/g, '\\n\\n')\n .trim();\n}\n\n/** First assistant message that has visible text (skips tool-only assistant rows). */\nfunction firstAssistantText(messages: AgentMessage[]): string {\n for (const m of messages) {\n if (m.role === 'assistant') {\n const t = extractTextFromMessage(m);\n if (t.length > 0) return t;\n }\n }\n return '';\n}\n\nexport function isWebchatSessionKey(sessionKey: string): boolean {\n const p = parseSessionKey(sessionKey);\n if (p?.source === 'webchat') return true;\n return sessionKey.includes(':webchat:');\n}\n\n/** Whether to run LLM/fallback session naming for this key (excludes cron, heartbeat). */\nexport function shouldAutoTitleSessionKey(sessionKey: string): boolean {\n const raw = (sessionKey ?? '').trim();\n if (!raw) return false;\n if (isCronSessionKey(raw)) return false;\n if (raw.toLowerCase().startsWith('heartbeat:')) return false;\n return true;\n}\n\nexport function sanitizeSessionTitle(raw: string): string {\n let s = raw.trim();\n if ((s.startsWith('\"') && s.endsWith('\"')) || (s.startsWith(\"'\") && s.endsWith(\"'\"))) {\n s = s.slice(1, -1).trim();\n }\n const lineBreak = s.indexOf('\\n');\n if (lineBreak !== -1) s = s.slice(0, lineBreak).trim();\n if (s.length > MAX_TITLE_LEN) s = s.slice(0, MAX_TITLE_LEN - 1).trimEnd() + '…';\n return s;\n}\n\nexport type SessionTitleSource = 'provisional' | 'llm' | 'user';\n\nexport function getSessionTitleSource(\n meta: { customData?: Record<string, unknown> } | null | undefined,\n): SessionTitleSource | null {\n const raw = meta?.customData?.titleSource;\n if (raw === 'provisional' || raw === 'llm' || raw === 'user') return raw;\n return null;\n}\n\n/** Title from a single user message (first line), for immediate sidebar labels. */\nexport function provisionalTitleFromUserText(raw: string): string | null {\n const text = stripEnvelopeTimestampPrefix(\n stripSessionStartupContextFromUserText((raw ?? '').trim()),\n );\n if (!text) return null;\n const line = text.split(/\\n/)[0]?.trim();\n if (!line) return null;\n return sanitizeSessionTitle(line);\n}\n\n/** Non-LLM title: first line of first user text, else first assistant line. */\nexport function fallbackTitleFromMessages(messages: AgentMessage[]): string | null {\n const u = firstUserText(messages);\n if (u) {\n const line = u.split(/\\n/)[0]?.trim();\n if (line) return sanitizeSessionTitle(line);\n }\n const a = firstAssistantText(messages);\n if (a) {\n const line = a.split(/\\n/)[0]?.trim();\n if (line) return sanitizeSessionTitle(line);\n }\n return null;\n}\n\n/**\n * Returns a title string, or null if generation should be skipped or failed.\n */\nexport async function generateSessionTitleFromMessages(\n modelRef: string,\n messages: AgentMessage[],\n signal?: AbortSignal,\n): Promise<string | null> {\n const userText = firstUserText(messages);\n const assistantText = firstAssistantText(messages);\n if (!userText && !assistantText) return null;\n\n let model: ReturnType<typeof resolveModel>;\n try {\n model = resolveModel(modelRef);\n } catch (err) {\n log.warn({ err, modelRef }, 'Cannot resolve model for session title');\n return null;\n }\n\n const prompt =\n userText && assistantText\n ? `You label chat sessions. Given the first user message and the start of the assistant reply, output ONE short title (max 8 words). No quotes. No punctuation at the end. Use the same language as the user when possible.\n\nUser: ${userText.slice(0, 2000)}\n\nAssistant: ${assistantText.slice(0, 2000)}\n\nTitle:`\n : userText\n ? `The assistant reply only used tools (no visible text yet). Output ONE short title (max 8 words) based only on the user's first message. No quotes. No punctuation at the end. Use the same language as the user.\n\nUser: ${userText.slice(0, 2000)}\n\nTitle:`\n : `Output ONE short title (max 8 words) for this assistant reply. No quotes. No punctuation at the end.\n\nAssistant: ${assistantText!.slice(0, 2000)}\n\nTitle:`;\n\n const userMsg: UserMessage = { role: 'user', content: prompt, timestamp: Date.now() };\n\n try {\n const result = await complete(\n model,\n { messages: [userMsg] },\n {\n maxTokens: 64,\n temperature: 0.35,\n signal: signal as AbortSignal,\n },\n );\n\n let text = '';\n if (Array.isArray(result.content)) {\n for (const c of result.content) {\n if (c && typeof c === 'object' && (c as { type?: string }).type === 'text') {\n text += String((c as { text?: string }).text || '');\n }\n }\n }\n\n const cleaned = sanitizeSessionTitle(text);\n return cleaned.length > 0 ? cleaned : null;\n } catch (err) {\n log.warn({ err }, 'Session title LLM call failed');\n return null;\n }\n}\n\nexport type SessionTitleUpdatedHook = (sessionKey: string, name: string) => void | Promise<void>;\n\n/** Whether post-turn LLM refine may run (provisional or still unnamed; not user-locked). */\nexport function shouldRefineSessionTitleWithLlm(\n meta: { name?: string; customData?: Record<string, unknown> } | null | undefined,\n): boolean {\n if (!meta) return false;\n const source = getSessionTitleSource(meta);\n if (source === 'user') return false;\n if (meta.name?.trim()) return source === 'provisional';\n return true;\n}\n\nfunction canAutoWriteTitle(meta: { name?: string; customData?: Record<string, unknown> } | null): boolean {\n if (!meta) return false;\n const source = getSessionTitleSource(meta);\n if (source === 'user') return false;\n return !meta.name?.trim();\n}\n\n/**\n * Set provisional title from first user text when session is still unnamed.\n * Skips cron/heartbeat keys and user-locked titles.\n */\nexport async function maybeSetProvisionalSessionTitle(\n sessionStore: SessionStore,\n sessionKey: string,\n userText?: string,\n onUpdated?: SessionTitleUpdatedHook,\n): Promise<void> {\n if (!shouldAutoTitleSessionKey(sessionKey)) return;\n\n let meta = await sessionStore.getMetadata(sessionKey);\n if (!meta) return;\n if (!canAutoWriteTitle(meta)) return;\n\n let title: string | null = null;\n if (userText?.trim()) {\n title = provisionalTitleFromUserText(userText);\n }\n if (!title) {\n const messages = await sessionStore.load(sessionKey);\n if (!messages.length) return;\n title = fallbackTitleFromMessages(messages);\n }\n if (!title) return;\n\n try {\n await sessionStore.updateMetadata(sessionKey, {\n name: title,\n customData: { ...(meta.customData ?? {}), titleSource: 'provisional' },\n });\n await onUpdated?.(sessionKey, title);\n } catch (err) {\n log.warn({ err, sessionKey }, 'Session title: provisional updateMetadata failed');\n }\n}\n\n/**\n * LLM refine when title is empty or still provisional (not user-locked).\n */\nexport async function maybeRefineSessionTitleWithLlm(\n sessionStore: SessionStore,\n sessionKey: string,\n modelRef: string | undefined,\n onUpdated?: SessionTitleUpdatedHook,\n): Promise<void> {\n if (!shouldAutoTitleSessionKey(sessionKey)) return;\n\n let messages = await sessionStore.load(sessionKey);\n if (!messages.length) return;\n\n let meta = await sessionStore.getMetadata(sessionKey);\n if (!meta) {\n await sessionStore.saveMessages(sessionKey, messages);\n meta = await sessionStore.getMetadata(sessionKey);\n }\n if (!meta) {\n log.warn({ sessionKey }, 'Session title: metadata missing after save');\n return;\n }\n if (!shouldRefineSessionTitleWithLlm(meta)) return;\n\n const source = getSessionTitleSource(meta);\n let title: string | null = null;\n const ref = modelRef?.trim();\n if (ref) {\n const controller = new AbortController();\n const timeout = setTimeout(() => controller.abort(), 25_000);\n try {\n title = await generateSessionTitleFromMessages(ref, messages, controller.signal);\n } finally {\n clearTimeout(timeout);\n }\n }\n if (!title) {\n title = fallbackTitleFromMessages(messages);\n }\n if (!title) return;\n\n const existing = meta.name?.trim();\n if (existing === title) {\n if (source !== 'llm') {\n try {\n await sessionStore.updateMetadata(sessionKey, {\n customData: { ...(meta.customData ?? {}), titleSource: 'llm' },\n });\n } catch {\n /* ignore */\n }\n }\n return;\n }\n\n try {\n await sessionStore.updateMetadata(sessionKey, {\n name: title,\n customData: { ...(meta.customData ?? {}), titleSource: 'llm' },\n });\n await onUpdated?.(sessionKey, title);\n } catch (err) {\n log.warn({ err, sessionKey }, 'Session title: refine updateMetadata failed');\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"session-title.js","names":[],"sources":["../../../src/session/session-title.ts"],"sourcesContent":["/**\n * LLM-generated session titles (webchat and any path using SessionStore).\n */\n\nimport type { AgentMessage } from '@earendil-works/pi-agent-core';\nimport { complete, type UserMessage } from '@earendil-works/pi-ai/compat';\n\nimport { stripSessionStartupContextFromUserText } from '../agent/reply/startup-context.js';\nimport { stripEnvelopeTimestampPrefix } from '../channels/envelope-timestamp.js';\nimport { isCronSessionKey, parseSessionKey } from '../routing/session-key.js';\nimport { resolveModel } from '../providers/index.js';\nimport { createLogger } from '../utils/logger.js';\nimport { readAgentMessageContent } from '../agent/memory/agent-message-access.js';\nimport type { SessionStore } from './store.js';\n\nconst log = createLogger('SessionAutoTitle');\n\nconst MAX_TITLE_LEN = 80;\n\n/** Collect visible text from any content block that exposes `text` (pi-ai / OpenAI / Anthropic shapes). */\nfunction extractTextFromMessage(m: AgentMessage): string {\n const raw = readAgentMessageContent(m);\n if (typeof raw === 'string') return raw.trim();\n if (Array.isArray(raw)) {\n const parts: string[] = [];\n for (const c of raw) {\n if (c && typeof c === 'object') {\n const o = c as unknown as Record<string, unknown>;\n const type = typeof o.type === 'string' ? o.type : '';\n if (type === 'toolCall' || type === 'tool_use' || type === 'tool_result') continue;\n if (typeof o.text === 'string' && o.text.trim()) {\n parts.push(o.text.trim());\n }\n }\n }\n return parts.join(' ').trim();\n }\n return '';\n}\n\nfunction firstUserText(messages: AgentMessage[]): string {\n const u = messages.find((m) => m.role === 'user');\n if (!u) return '';\n const raw = extractTextFromMessage(u);\n return normalizeUserTextForTitle(raw);\n}\n\nfunction stripMediaClaimChecks(text: string): string {\n if (!text.includes('[media attached:') && !text.includes('xopc-media-uri:')) {\n return text;\n }\n return text\n .replace(\n /\\s*\\[media attached:[^\\]]+\\]\\s*\\r?\\nxopc-media-uri:[^\\r\\n]+\\r?\\n\\s*xopc-media-path:[^\\r\\n]+(?:\\r?\\n\\s*Use the read_media tool[^\\r\\n]*)?/g,\n '',\n )\n .replace(/\\s*\\[media attached:[^\\]]+\\]\\s*/g, ' ')\n .replace(/\\s*xopc-media-uri:[^\\r\\n]+/g, '')\n .replace(/\\s*xopc-media-path:[^\\r\\n]+/g, '')\n .replace(/\\s*Use the read_media tool[^\\r\\n]*/g, '')\n .replace(/\\n{3,}/g, '\\n\\n')\n .trim();\n}\n\nfunction stripExpandedSkillBlockForTitle(text: string): string {\n const trimmed = text.trimStart();\n if (!/^##\\s+Skill:\\s*\\S+/i.test(trimmed)) {\n return text;\n }\n\n const markerRe = /(?:^|\\r?\\n)\\*\\*Arguments\\*\\*:\\s*/gi;\n let argsStart = -1;\n while (markerRe.exec(trimmed) !== null) {\n argsStart = markerRe.lastIndex;\n }\n\n if (argsStart !== -1) {\n const args = trimmed.slice(argsStart).trim();\n if (args) return args;\n }\n\n const firstLine = trimmed.split(/\\r?\\n/)[0]?.trim() ?? '';\n return firstLine.replace(/^##\\s+Skill:\\s*/i, '').trim();\n}\n\nfunction stripSkillCommandForTitle(text: string): string {\n const trimmed = text.trimStart();\n const match = trimmed.match(/^\\/skill:([^\\s]+)(?:\\s+([\\s\\S]*))?$/i);\n if (!match) {\n return text;\n }\n const args = match[2]?.trim();\n return args || match[1]?.trim() || text;\n}\n\nfunction normalizeUserTextForTitle(raw: string): string {\n return stripSkillCommandForTitle(\n stripExpandedSkillBlockForTitle(\n stripMediaClaimChecks(\n stripEnvelopeTimestampPrefix(stripSessionStartupContextFromUserText(raw)),\n ),\n ),\n ).trim();\n}\n\n/** First assistant message that has visible text (skips tool-only assistant rows). */\nfunction firstAssistantText(messages: AgentMessage[]): string {\n for (const m of messages) {\n if (m.role === 'assistant') {\n const t = extractTextFromMessage(m);\n if (t.length > 0) return t;\n }\n }\n return '';\n}\n\nexport function isWebchatSessionKey(sessionKey: string): boolean {\n const p = parseSessionKey(sessionKey);\n if (p?.source === 'webchat') return true;\n return sessionKey.includes(':webchat:');\n}\n\n/** Whether to run LLM/fallback session naming for this key (excludes cron, heartbeat). */\nexport function shouldAutoTitleSessionKey(sessionKey: string): boolean {\n const raw = (sessionKey ?? '').trim();\n if (!raw) return false;\n if (isCronSessionKey(raw)) return false;\n if (raw.toLowerCase().startsWith('heartbeat:')) return false;\n return true;\n}\n\nexport function sanitizeSessionTitle(raw: string): string {\n let s = raw.trim();\n if ((s.startsWith('\"') && s.endsWith('\"')) || (s.startsWith(\"'\") && s.endsWith(\"'\"))) {\n s = s.slice(1, -1).trim();\n }\n const lineBreak = s.indexOf('\\n');\n if (lineBreak !== -1) s = s.slice(0, lineBreak).trim();\n if (s.length > MAX_TITLE_LEN) s = s.slice(0, MAX_TITLE_LEN - 1).trimEnd() + '…';\n return s;\n}\n\nexport type SessionTitleSource = 'provisional' | 'llm' | 'user';\n\nexport function getSessionTitleSource(\n meta: { customData?: Record<string, unknown> } | null | undefined,\n): SessionTitleSource | null {\n const raw = meta?.customData?.titleSource;\n if (raw === 'provisional' || raw === 'llm' || raw === 'user') return raw;\n return null;\n}\n\n/** Title from a single user message (first line), for immediate sidebar labels. */\nexport function provisionalTitleFromUserText(raw: string): string | null {\n const text = normalizeUserTextForTitle((raw ?? '').trim());\n if (!text) return null;\n const line = text.split(/\\n/)[0]?.trim();\n if (!line) return null;\n return sanitizeSessionTitle(line);\n}\n\n/** Non-LLM title: first line of first user text, else first assistant line. */\nexport function fallbackTitleFromMessages(messages: AgentMessage[]): string | null {\n const u = firstUserText(messages);\n if (u) {\n const line = u.split(/\\n/)[0]?.trim();\n if (line) return sanitizeSessionTitle(line);\n }\n const a = firstAssistantText(messages);\n if (a) {\n const line = a.split(/\\n/)[0]?.trim();\n if (line) return sanitizeSessionTitle(line);\n }\n return null;\n}\n\n/**\n * Returns a title string, or null if generation should be skipped or failed.\n */\nexport async function generateSessionTitleFromMessages(\n modelRef: string,\n messages: AgentMessage[],\n signal?: AbortSignal,\n): Promise<string | null> {\n const userText = firstUserText(messages);\n const assistantText = firstAssistantText(messages);\n if (!userText && !assistantText) return null;\n\n let model: ReturnType<typeof resolveModel>;\n try {\n model = resolveModel(modelRef);\n } catch (err) {\n log.warn({ err, modelRef }, 'Cannot resolve model for session title');\n return null;\n }\n\n const prompt =\n userText && assistantText\n ? `You label chat sessions. Given the first user message and the start of the assistant reply, output ONE short title (max 8 words). No quotes. No punctuation at the end. Use the same language as the user when possible.\n\nUser: ${userText.slice(0, 2000)}\n\nAssistant: ${assistantText.slice(0, 2000)}\n\nTitle:`\n : userText\n ? `The assistant reply only used tools (no visible text yet). Output ONE short title (max 8 words) based only on the user's first message. No quotes. No punctuation at the end. Use the same language as the user.\n\nUser: ${userText.slice(0, 2000)}\n\nTitle:`\n : `Output ONE short title (max 8 words) for this assistant reply. No quotes. No punctuation at the end.\n\nAssistant: ${assistantText!.slice(0, 2000)}\n\nTitle:`;\n\n const userMsg: UserMessage = { role: 'user', content: prompt, timestamp: Date.now() };\n\n try {\n const result = await complete(\n model,\n { messages: [userMsg] },\n {\n maxTokens: 64,\n temperature: 0.35,\n signal: signal as AbortSignal,\n },\n );\n\n let text = '';\n if (Array.isArray(result.content)) {\n for (const c of result.content) {\n if (c && typeof c === 'object' && (c as { type?: string }).type === 'text') {\n text += String((c as { text?: string }).text || '');\n }\n }\n }\n\n const cleaned = sanitizeSessionTitle(text);\n return cleaned.length > 0 ? cleaned : null;\n } catch (err) {\n log.warn({ err }, 'Session title LLM call failed');\n return null;\n }\n}\n\nexport type SessionTitleUpdatedHook = (sessionKey: string, name: string) => void | Promise<void>;\n\n/** Whether post-turn LLM refine may run (provisional or still unnamed; not user-locked). */\nexport function shouldRefineSessionTitleWithLlm(\n meta: { name?: string; customData?: Record<string, unknown> } | null | undefined,\n): boolean {\n if (!meta) return false;\n const source = getSessionTitleSource(meta);\n if (source === 'user') return false;\n if (meta.name?.trim()) return source === 'provisional';\n return true;\n}\n\nfunction canAutoWriteTitle(meta: { name?: string; customData?: Record<string, unknown> } | null): boolean {\n if (!meta) return false;\n const source = getSessionTitleSource(meta);\n if (source === 'user') return false;\n return !meta.name?.trim();\n}\n\n/**\n * Set provisional title from first user text when session is still unnamed.\n * Skips cron/heartbeat keys and user-locked titles.\n */\nexport async function maybeSetProvisionalSessionTitle(\n sessionStore: SessionStore,\n sessionKey: string,\n userText?: string,\n onUpdated?: SessionTitleUpdatedHook,\n): Promise<void> {\n if (!shouldAutoTitleSessionKey(sessionKey)) return;\n\n let meta = await sessionStore.getMetadata(sessionKey);\n if (!meta) return;\n if (!canAutoWriteTitle(meta)) return;\n\n let title: string | null = null;\n if (userText?.trim()) {\n title = provisionalTitleFromUserText(userText);\n }\n if (!title) {\n const messages = await sessionStore.load(sessionKey);\n if (!messages.length) return;\n title = fallbackTitleFromMessages(messages);\n }\n if (!title) return;\n\n try {\n await sessionStore.updateMetadata(sessionKey, {\n name: title,\n customData: { ...(meta.customData ?? {}), titleSource: 'provisional' },\n });\n await onUpdated?.(sessionKey, title);\n } catch (err) {\n log.warn({ err, sessionKey }, 'Session title: provisional updateMetadata failed');\n }\n}\n\n/**\n * LLM refine when title is empty or still provisional (not user-locked).\n */\nexport async function maybeRefineSessionTitleWithLlm(\n sessionStore: SessionStore,\n sessionKey: string,\n modelRef: string | undefined,\n onUpdated?: SessionTitleUpdatedHook,\n): Promise<void> {\n if (!shouldAutoTitleSessionKey(sessionKey)) return;\n\n let messages = await sessionStore.load(sessionKey);\n if (!messages.length) return;\n\n let meta = await sessionStore.getMetadata(sessionKey);\n if (!meta) {\n await sessionStore.saveMessages(sessionKey, messages);\n meta = await sessionStore.getMetadata(sessionKey);\n }\n if (!meta) {\n log.warn({ sessionKey }, 'Session title: metadata missing after save');\n return;\n }\n if (!shouldRefineSessionTitleWithLlm(meta)) return;\n\n const source = getSessionTitleSource(meta);\n let title: string | null = null;\n const ref = modelRef?.trim();\n if (ref) {\n const controller = new AbortController();\n const timeout = setTimeout(() => controller.abort(), 25_000);\n try {\n title = await generateSessionTitleFromMessages(ref, messages, controller.signal);\n } finally {\n clearTimeout(timeout);\n }\n }\n if (!title) {\n title = fallbackTitleFromMessages(messages);\n }\n if (!title) return;\n\n const existing = meta.name?.trim();\n if (existing === title) {\n if (source !== 'llm') {\n try {\n await sessionStore.updateMetadata(sessionKey, {\n customData: { ...(meta.customData ?? {}), titleSource: 'llm' },\n });\n } catch {\n /* ignore */\n }\n }\n return;\n }\n\n try {\n await sessionStore.updateMetadata(sessionKey, {\n name: title,\n customData: { ...(meta.customData ?? {}), titleSource: 'llm' },\n });\n await onUpdated?.(sessionKey, title);\n } catch (err) {\n log.warn({ err, sessionKey }, 'Session title: refine updateMetadata failed');\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,5 @@
1
+ ALTER TABLE projects ADD COLUMN pinned_at INTEGER;
2
+
3
+ CREATE INDEX IF NOT EXISTS idx_projects_pinned
4
+ ON projects(pinned_at DESC)
5
+ WHERE pinned_at IS NOT NULL;
@@ -0,0 +1,10 @@
1
+ CREATE TABLE IF NOT EXISTS goal_contracts (
2
+ goal_id TEXT PRIMARY KEY,
3
+ version INTEGER NOT NULL DEFAULT 1,
4
+ objective TEXT NOT NULL,
5
+ scope_boundary TEXT,
6
+ evidence_plan_json TEXT NOT NULL DEFAULT '[]',
7
+ created_at INTEGER NOT NULL,
8
+ updated_at INTEGER NOT NULL,
9
+ FOREIGN KEY (goal_id) REFERENCES goals(goal_id) ON DELETE CASCADE
10
+ );
@@ -0,0 +1,43 @@
1
+ CREATE TABLE IF NOT EXISTS mobile_devices (
2
+ device_id TEXT PRIMARY KEY,
3
+ platform TEXT NOT NULL,
4
+ push_token TEXT NOT NULL,
5
+ push_provider TEXT NOT NULL DEFAULT 'expo',
6
+ enabled INTEGER NOT NULL DEFAULT 1,
7
+ permissions TEXT NOT NULL DEFAULT 'unknown',
8
+ preferences_json TEXT NOT NULL DEFAULT '{}',
9
+ app_version TEXT,
10
+ last_seen_at INTEGER NOT NULL,
11
+ created_at INTEGER NOT NULL,
12
+ updated_at INTEGER NOT NULL
13
+ );
14
+
15
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_mobile_devices_push_token
16
+ ON mobile_devices(push_provider, push_token);
17
+ CREATE INDEX IF NOT EXISTS idx_mobile_devices_enabled
18
+ ON mobile_devices(enabled, updated_at DESC);
19
+
20
+ CREATE TABLE IF NOT EXISTS mobile_activity_events (
21
+ event_id TEXT PRIMARY KEY,
22
+ event_type TEXT NOT NULL,
23
+ entity_kind TEXT NOT NULL,
24
+ entity_id TEXT NOT NULL,
25
+ priority TEXT NOT NULL,
26
+ title TEXT NOT NULL,
27
+ body TEXT,
28
+ deep_link TEXT NOT NULL,
29
+ payload_json TEXT NOT NULL DEFAULT '{}',
30
+ created_at INTEGER NOT NULL
31
+ );
32
+
33
+ CREATE INDEX IF NOT EXISTS idx_mobile_activity_events_created
34
+ ON mobile_activity_events(created_at DESC);
35
+
36
+ CREATE TABLE IF NOT EXISTS mobile_activity_acknowledgements (
37
+ event_id TEXT NOT NULL,
38
+ device_id TEXT NOT NULL,
39
+ acknowledged_at INTEGER NOT NULL,
40
+ PRIMARY KEY (event_id, device_id),
41
+ FOREIGN KEY (event_id) REFERENCES mobile_activity_events(event_id) ON DELETE CASCADE,
42
+ FOREIGN KEY (device_id) REFERENCES mobile_devices(device_id) ON DELETE CASCADE
43
+ );
@@ -0,0 +1,47 @@
1
+ CREATE TABLE IF NOT EXISTS goal_evidence_requirements (
2
+ requirement_id TEXT PRIMARY KEY,
3
+ goal_id TEXT NOT NULL,
4
+ text TEXT NOT NULL,
5
+ status TEXT NOT NULL DEFAULT 'pending',
6
+ review_reason TEXT,
7
+ review_confidence REAL,
8
+ reviewed_by TEXT,
9
+ reviewed_at INTEGER,
10
+ requires_human_approval INTEGER NOT NULL DEFAULT 1,
11
+ created_at INTEGER NOT NULL,
12
+ updated_at INTEGER NOT NULL,
13
+ sort_order INTEGER NOT NULL,
14
+ UNIQUE(goal_id, text),
15
+ FOREIGN KEY (goal_id) REFERENCES goals(goal_id) ON DELETE CASCADE
16
+ );
17
+
18
+ CREATE INDEX IF NOT EXISTS idx_goal_evidence_requirements_goal_order
19
+ ON goal_evidence_requirements(goal_id, sort_order ASC);
20
+
21
+ CREATE TABLE IF NOT EXISTS goal_evidence_requirement_links (
22
+ requirement_id TEXT NOT NULL,
23
+ evidence_id TEXT NOT NULL,
24
+ linked_by TEXT NOT NULL,
25
+ created_at INTEGER NOT NULL,
26
+ PRIMARY KEY (requirement_id, evidence_id),
27
+ FOREIGN KEY (requirement_id) REFERENCES goal_evidence_requirements(requirement_id) ON DELETE CASCADE,
28
+ FOREIGN KEY (evidence_id) REFERENCES goal_evidence(evidence_id) ON DELETE CASCADE
29
+ );
30
+
31
+ CREATE INDEX IF NOT EXISTS idx_goal_evidence_requirement_links_evidence
32
+ ON goal_evidence_requirement_links(evidence_id);
33
+
34
+ INSERT OR IGNORE INTO goal_evidence_requirements (
35
+ requirement_id, goal_id, text, status, requires_human_approval, created_at, updated_at, sort_order
36
+ )
37
+ SELECT
38
+ lower(hex(randomblob(16))),
39
+ goal_contracts.goal_id,
40
+ json_each.value,
41
+ 'pending',
42
+ 1,
43
+ goal_contracts.created_at,
44
+ goal_contracts.updated_at,
45
+ CAST(json_each.key AS INTEGER) + 1
46
+ FROM goal_contracts, json_each(goal_contracts.evidence_plan_json)
47
+ WHERE json_valid(goal_contracts.evidence_plan_json) AND json_each.type = 'text';
@@ -3,7 +3,7 @@ import type { ApplyMigrationsOptions } from './types.js';
3
3
  /** Baseline schema version applied from schema.sql on first open. */
4
4
  export declare const XOPC_DB_BASELINE_SCHEMA_VERSION = 11;
5
5
  /** Latest schema version this release supports (increment when adding migrations). */
6
- export declare const XOPC_DB_SCHEMA_VERSION = 24;
6
+ export declare const XOPC_DB_SCHEMA_VERSION = 28;
7
7
  export declare function resolveMigrationsDir(override?: string): string;
8
8
  /**
9
9
  * Apply sequential SQL migrations from (currentVersion + 1) through targetVersion.
@@ -12,7 +12,7 @@ const log = createLogger("Sqlite:Migrations");
12
12
  /** Baseline schema version applied from schema.sql on first open. */
13
13
  const XOPC_DB_BASELINE_SCHEMA_VERSION = 11;
14
14
  /** Latest schema version this release supports (increment when adding migrations). */
15
- const XOPC_DB_SCHEMA_VERSION = 24;
15
+ const XOPC_DB_SCHEMA_VERSION = 28;
16
16
  const MODULE_DIR = dirname(fileURLToPath(import.meta.url));
17
17
  function resolveMigrationsDir(override) {
18
18
  if (override) return override;
@@ -46,7 +46,7 @@ function applySingleMigration(db, migration) {
46
46
  * Each step runs in its own transaction and updates schema_meta on success.
47
47
  */
48
48
  function applyPendingMigrations(db, options = {}) {
49
- const targetVersion = options.targetVersion ?? 24;
49
+ const targetVersion = options.targetVersion ?? 28;
50
50
  let currentVersion = readSchemaVersion(db);
51
51
  if (currentVersion > targetVersion) throw new DatabaseSchemaTooNewError(currentVersion, targetVersion);
52
52
  if (currentVersion >= targetVersion) return currentVersion;
@@ -62,7 +62,7 @@ function applyPendingMigrations(db, options = {}) {
62
62
  }
63
63
  /** Inspect schema version without mutating the database. */
64
64
  function inspectSchemaMigrationStatus(db, options = {}) {
65
- const appVersion = options.targetVersion ?? 24;
65
+ const appVersion = options.targetVersion ?? 28;
66
66
  const dbVersion = readSchemaVersion(db);
67
67
  if (dbVersion > appVersion) return {
68
68
  dbVersion,
@@ -1 +1 @@
1
- {"version":3,"file":"runner.js","names":[],"sources":["../../../../../src/storage/sqlite/migrations/runner.ts"],"sourcesContent":["import { existsSync } from 'node:fs';\nimport { dirname, join } from 'node:path';\nimport { fileURLToPath } from 'node:url';\n\nimport type { DatabaseSync } from 'node:sqlite';\n\nimport { createLogger } from '../../../utils/logger.js';\nimport { readSchemaVersion, setSchemaVersion } from '../schema-version.js';\nimport { discoverSqlMigrations } from './discover.js';\nimport {\n DatabaseSchemaMigrationGapError,\n DatabaseSchemaTooNewError,\n} from './errors.js';\nimport type { ApplyMigrationsOptions, SqlMigration } from './types.js';\n\nconst log = createLogger('Sqlite:Migrations');\n\n/** Baseline schema version applied from schema.sql on first open. */\nexport const XOPC_DB_BASELINE_SCHEMA_VERSION = 11;\n\n/** Latest schema version this release supports (increment when adding migrations). */\nexport const XOPC_DB_SCHEMA_VERSION = 24;\n\nconst MODULE_DIR = dirname(fileURLToPath(import.meta.url));\n\nexport function resolveMigrationsDir(override?: string): string {\n if (override) {\n return override;\n }\n // Packaged Electron gateway bundle: `out/server/index.js` + `out/server/migrations/`.\n const siblingDir = join(MODULE_DIR, 'migrations');\n if (existsSync(siblingDir)) {\n return siblingDir;\n }\n // Dev / dist: SQL files live next to `migrations/runner.js`.\n return MODULE_DIR;\n}\n\nfunction migrationByTarget(\n migrations: SqlMigration[],\n targetVersion: number,\n): SqlMigration | undefined {\n return migrations.find((migration) => migration.targetVersion === targetVersion);\n}\n\nfunction applySingleMigration(db: DatabaseSync, migration: SqlMigration): void {\n log.info({ targetVersion: migration.targetVersion, file: migration.filename }, 'Applying SQLite migration');\n db.exec('BEGIN IMMEDIATE');\n try {\n db.exec(migration.sql);\n setSchemaVersion(db, migration.targetVersion);\n db.exec('COMMIT');\n } catch (error) {\n try {\n db.exec('ROLLBACK');\n } catch {\n /* preserve original error */\n }\n const em = error instanceof Error ? error.message : String(error);\n throw new Error(\n `SQLite migration to v${migration.targetVersion} (${migration.filename}) failed: ${em}`,\n { cause: error },\n );\n }\n}\n\n/**\n * Apply sequential SQL migrations from (currentVersion + 1) through targetVersion.\n * Each step runs in its own transaction and updates schema_meta on success.\n */\nexport function applyPendingMigrations(\n db: DatabaseSync,\n options: ApplyMigrationsOptions = {},\n): number {\n const targetVersion = options.targetVersion ?? XOPC_DB_SCHEMA_VERSION;\n let currentVersion = readSchemaVersion(db);\n\n if (currentVersion > targetVersion) {\n throw new DatabaseSchemaTooNewError(currentVersion, targetVersion);\n }\n\n if (currentVersion >= targetVersion) {\n return currentVersion;\n }\n\n const migrations = discoverSqlMigrations(resolveMigrationsDir(options.migrationsDir));\n\n while (currentVersion < targetVersion) {\n const nextVersion = currentVersion + 1;\n const migration = migrationByTarget(migrations, nextVersion);\n if (!migration) {\n throw new DatabaseSchemaMigrationGapError(currentVersion, targetVersion, nextVersion);\n }\n applySingleMigration(db, migration);\n currentVersion = nextVersion;\n }\n\n return currentVersion;\n}\n\nexport type SchemaMigrationStatus = {\n dbVersion: number;\n appVersion: number;\n pendingVersions: number[];\n isTooNew: boolean;\n hasMigrationGap: boolean;\n missingVersion: number | null;\n};\n\n/** Inspect schema version without mutating the database. */\nexport function inspectSchemaMigrationStatus(\n db: DatabaseSync,\n options: Pick<ApplyMigrationsOptions, 'migrationsDir' | 'targetVersion'> = {},\n): SchemaMigrationStatus {\n const appVersion = options.targetVersion ?? XOPC_DB_SCHEMA_VERSION;\n const dbVersion = readSchemaVersion(db);\n const isTooNew = dbVersion > appVersion;\n\n if (isTooNew) {\n return {\n dbVersion,\n appVersion,\n pendingVersions: [],\n isTooNew: true,\n hasMigrationGap: false,\n missingVersion: null,\n };\n }\n\n const migrations = discoverSqlMigrations(resolveMigrationsDir(options.migrationsDir));\n const pendingVersions: number[] = [];\n let hasMigrationGap = false;\n let missingVersion: number | null = null;\n\n for (let version = dbVersion + 1; version <= appVersion; version++) {\n if (migrationByTarget(migrations, version)) {\n pendingVersions.push(version);\n } else {\n hasMigrationGap = true;\n missingVersion = version;\n break;\n }\n }\n\n return {\n dbVersion,\n appVersion,\n pendingVersions,\n isTooNew: false,\n hasMigrationGap,\n missingVersion,\n };\n}\n"],"mappings":";;;;;;;;;aAMwD;AASxD,MAAM,MAAM,aAAa,oBAAoB;;AAG7C,MAAa,kCAAkC;;AAG/C,MAAa,yBAAyB;AAEtC,MAAM,aAAa,QAAQ,cAAc,OAAO,KAAK,IAAI,CAAC;AAE1D,SAAgB,qBAAqB,UAA2B;AAC9D,KAAI,SACF,QAAO;CAGT,MAAM,aAAa,KAAK,YAAY,aAAa;AACjD,KAAI,WAAW,WAAW,CACxB,QAAO;AAGT,QAAO;;AAGT,SAAS,kBACP,YACA,eAC0B;AAC1B,QAAO,WAAW,MAAM,cAAc,UAAU,kBAAkB,cAAc;;AAGlF,SAAS,qBAAqB,IAAkB,WAA+B;AAC7E,KAAI,KAAK;EAAE,eAAe,UAAU;EAAe,MAAM,UAAU;EAAU,EAAE,4BAA4B;AAC3G,IAAG,KAAK,kBAAkB;AAC1B,KAAI;AACF,KAAG,KAAK,UAAU,IAAI;AACtB,mBAAiB,IAAI,UAAU,cAAc;AAC7C,KAAG,KAAK,SAAS;UACV,OAAO;AACd,MAAI;AACF,MAAG,KAAK,WAAW;UACb;EAGR,MAAM,KAAK,iBAAiB,QAAQ,MAAM,UAAU,OAAO,MAAM;AACjE,QAAM,IAAI,MACR,wBAAwB,UAAU,cAAc,IAAI,UAAU,SAAS,YAAY,MACnF,EAAE,OAAO,OAAO,CACjB;;;;;;;AAQL,SAAgB,uBACd,IACA,UAAkC,EAAE,EAC5B;CACR,MAAM,gBAAgB,QAAQ,iBAAA;CAC9B,IAAI,iBAAiB,kBAAkB,GAAG;AAE1C,KAAI,iBAAiB,cACnB,OAAM,IAAI,0BAA0B,gBAAgB,cAAc;AAGpE,KAAI,kBAAkB,cACpB,QAAO;CAGT,MAAM,aAAa,sBAAsB,qBAAqB,QAAQ,cAAc,CAAC;AAErF,QAAO,iBAAiB,eAAe;EACrC,MAAM,cAAc,iBAAiB;EACrC,MAAM,YAAY,kBAAkB,YAAY,YAAY;AAC5D,MAAI,CAAC,UACH,OAAM,IAAI,gCAAgC,gBAAgB,eAAe,YAAY;AAEvF,uBAAqB,IAAI,UAAU;AACnC,mBAAiB;;AAGnB,QAAO;;;AAaT,SAAgB,6BACd,IACA,UAA2E,EAAE,EACtD;CACvB,MAAM,aAAa,QAAQ,iBAAA;CAC3B,MAAM,YAAY,kBAAkB,GAAG;AAGvC,KAFiB,YAAY,WAG3B,QAAO;EACL;EACA;EACA,iBAAiB,EAAE;EACnB,UAAU;EACV,iBAAiB;EACjB,gBAAgB;EACjB;CAGH,MAAM,aAAa,sBAAsB,qBAAqB,QAAQ,cAAc,CAAC;CACrF,MAAM,kBAA4B,EAAE;CACpC,IAAI,kBAAkB;CACtB,IAAI,iBAAgC;AAEpC,MAAK,IAAI,UAAU,YAAY,GAAG,WAAW,YAAY,UACvD,KAAI,kBAAkB,YAAY,QAAQ,CACxC,iBAAgB,KAAK,QAAQ;MACxB;AACL,oBAAkB;AAClB,mBAAiB;AACjB;;AAIJ,QAAO;EACL;EACA;EACA;EACA,UAAU;EACV;EACA;EACD"}
1
+ {"version":3,"file":"runner.js","names":[],"sources":["../../../../../src/storage/sqlite/migrations/runner.ts"],"sourcesContent":["import { existsSync } from 'node:fs';\nimport { dirname, join } from 'node:path';\nimport { fileURLToPath } from 'node:url';\n\nimport type { DatabaseSync } from 'node:sqlite';\n\nimport { createLogger } from '../../../utils/logger.js';\nimport { readSchemaVersion, setSchemaVersion } from '../schema-version.js';\nimport { discoverSqlMigrations } from './discover.js';\nimport {\n DatabaseSchemaMigrationGapError,\n DatabaseSchemaTooNewError,\n} from './errors.js';\nimport type { ApplyMigrationsOptions, SqlMigration } from './types.js';\n\nconst log = createLogger('Sqlite:Migrations');\n\n/** Baseline schema version applied from schema.sql on first open. */\nexport const XOPC_DB_BASELINE_SCHEMA_VERSION = 11;\n\n/** Latest schema version this release supports (increment when adding migrations). */\nexport const XOPC_DB_SCHEMA_VERSION = 28;\n\nconst MODULE_DIR = dirname(fileURLToPath(import.meta.url));\n\nexport function resolveMigrationsDir(override?: string): string {\n if (override) {\n return override;\n }\n // Packaged Electron gateway bundle: `out/server/index.js` + `out/server/migrations/`.\n const siblingDir = join(MODULE_DIR, 'migrations');\n if (existsSync(siblingDir)) {\n return siblingDir;\n }\n // Dev / dist: SQL files live next to `migrations/runner.js`.\n return MODULE_DIR;\n}\n\nfunction migrationByTarget(\n migrations: SqlMigration[],\n targetVersion: number,\n): SqlMigration | undefined {\n return migrations.find((migration) => migration.targetVersion === targetVersion);\n}\n\nfunction applySingleMigration(db: DatabaseSync, migration: SqlMigration): void {\n log.info({ targetVersion: migration.targetVersion, file: migration.filename }, 'Applying SQLite migration');\n db.exec('BEGIN IMMEDIATE');\n try {\n db.exec(migration.sql);\n setSchemaVersion(db, migration.targetVersion);\n db.exec('COMMIT');\n } catch (error) {\n try {\n db.exec('ROLLBACK');\n } catch {\n /* preserve original error */\n }\n const em = error instanceof Error ? error.message : String(error);\n throw new Error(\n `SQLite migration to v${migration.targetVersion} (${migration.filename}) failed: ${em}`,\n { cause: error },\n );\n }\n}\n\n/**\n * Apply sequential SQL migrations from (currentVersion + 1) through targetVersion.\n * Each step runs in its own transaction and updates schema_meta on success.\n */\nexport function applyPendingMigrations(\n db: DatabaseSync,\n options: ApplyMigrationsOptions = {},\n): number {\n const targetVersion = options.targetVersion ?? XOPC_DB_SCHEMA_VERSION;\n let currentVersion = readSchemaVersion(db);\n\n if (currentVersion > targetVersion) {\n throw new DatabaseSchemaTooNewError(currentVersion, targetVersion);\n }\n\n if (currentVersion >= targetVersion) {\n return currentVersion;\n }\n\n const migrations = discoverSqlMigrations(resolveMigrationsDir(options.migrationsDir));\n\n while (currentVersion < targetVersion) {\n const nextVersion = currentVersion + 1;\n const migration = migrationByTarget(migrations, nextVersion);\n if (!migration) {\n throw new DatabaseSchemaMigrationGapError(currentVersion, targetVersion, nextVersion);\n }\n applySingleMigration(db, migration);\n currentVersion = nextVersion;\n }\n\n return currentVersion;\n}\n\nexport type SchemaMigrationStatus = {\n dbVersion: number;\n appVersion: number;\n pendingVersions: number[];\n isTooNew: boolean;\n hasMigrationGap: boolean;\n missingVersion: number | null;\n};\n\n/** Inspect schema version without mutating the database. */\nexport function inspectSchemaMigrationStatus(\n db: DatabaseSync,\n options: Pick<ApplyMigrationsOptions, 'migrationsDir' | 'targetVersion'> = {},\n): SchemaMigrationStatus {\n const appVersion = options.targetVersion ?? XOPC_DB_SCHEMA_VERSION;\n const dbVersion = readSchemaVersion(db);\n const isTooNew = dbVersion > appVersion;\n\n if (isTooNew) {\n return {\n dbVersion,\n appVersion,\n pendingVersions: [],\n isTooNew: true,\n hasMigrationGap: false,\n missingVersion: null,\n };\n }\n\n const migrations = discoverSqlMigrations(resolveMigrationsDir(options.migrationsDir));\n const pendingVersions: number[] = [];\n let hasMigrationGap = false;\n let missingVersion: number | null = null;\n\n for (let version = dbVersion + 1; version <= appVersion; version++) {\n if (migrationByTarget(migrations, version)) {\n pendingVersions.push(version);\n } else {\n hasMigrationGap = true;\n missingVersion = version;\n break;\n }\n }\n\n return {\n dbVersion,\n appVersion,\n pendingVersions,\n isTooNew: false,\n hasMigrationGap,\n missingVersion,\n };\n}\n"],"mappings":";;;;;;;;;aAMwD;AASxD,MAAM,MAAM,aAAa,oBAAoB;;AAG7C,MAAa,kCAAkC;;AAG/C,MAAa,yBAAyB;AAEtC,MAAM,aAAa,QAAQ,cAAc,OAAO,KAAK,IAAI,CAAC;AAE1D,SAAgB,qBAAqB,UAA2B;AAC9D,KAAI,SACF,QAAO;CAGT,MAAM,aAAa,KAAK,YAAY,aAAa;AACjD,KAAI,WAAW,WAAW,CACxB,QAAO;AAGT,QAAO;;AAGT,SAAS,kBACP,YACA,eAC0B;AAC1B,QAAO,WAAW,MAAM,cAAc,UAAU,kBAAkB,cAAc;;AAGlF,SAAS,qBAAqB,IAAkB,WAA+B;AAC7E,KAAI,KAAK;EAAE,eAAe,UAAU;EAAe,MAAM,UAAU;EAAU,EAAE,4BAA4B;AAC3G,IAAG,KAAK,kBAAkB;AAC1B,KAAI;AACF,KAAG,KAAK,UAAU,IAAI;AACtB,mBAAiB,IAAI,UAAU,cAAc;AAC7C,KAAG,KAAK,SAAS;UACV,OAAO;AACd,MAAI;AACF,MAAG,KAAK,WAAW;UACb;EAGR,MAAM,KAAK,iBAAiB,QAAQ,MAAM,UAAU,OAAO,MAAM;AACjE,QAAM,IAAI,MACR,wBAAwB,UAAU,cAAc,IAAI,UAAU,SAAS,YAAY,MACnF,EAAE,OAAO,OAAO,CACjB;;;;;;;AAQL,SAAgB,uBACd,IACA,UAAkC,EAAE,EAC5B;CACR,MAAM,gBAAgB,QAAQ,iBAAA;CAC9B,IAAI,iBAAiB,kBAAkB,GAAG;AAE1C,KAAI,iBAAiB,cACnB,OAAM,IAAI,0BAA0B,gBAAgB,cAAc;AAGpE,KAAI,kBAAkB,cACpB,QAAO;CAGT,MAAM,aAAa,sBAAsB,qBAAqB,QAAQ,cAAc,CAAC;AAErF,QAAO,iBAAiB,eAAe;EACrC,MAAM,cAAc,iBAAiB;EACrC,MAAM,YAAY,kBAAkB,YAAY,YAAY;AAC5D,MAAI,CAAC,UACH,OAAM,IAAI,gCAAgC,gBAAgB,eAAe,YAAY;AAEvF,uBAAqB,IAAI,UAAU;AACnC,mBAAiB;;AAGnB,QAAO;;;AAaT,SAAgB,6BACd,IACA,UAA2E,EAAE,EACtD;CACvB,MAAM,aAAa,QAAQ,iBAAA;CAC3B,MAAM,YAAY,kBAAkB,GAAG;AAGvC,KAFiB,YAAY,WAG3B,QAAO;EACL;EACA;EACA,iBAAiB,EAAE;EACnB,UAAU;EACV,iBAAiB;EACjB,gBAAgB;EACjB;CAGH,MAAM,aAAa,sBAAsB,qBAAqB,QAAQ,cAAc,CAAC;CACrF,MAAM,kBAA4B,EAAE;CACpC,IAAI,kBAAkB;CACtB,IAAI,iBAAgC;AAEpC,MAAK,IAAI,UAAU,YAAY,GAAG,WAAW,YAAY,UACvD,KAAI,kBAAkB,YAAY,QAAQ,CACxC,iBAAgB,KAAK,QAAQ;MACxB;AACL,oBAAkB;AAClB,mBAAiB;AACjB;;AAIJ,QAAO;EACL;EACA;EACA;EACA,UAAU;EACV;EACA;EACD"}
@@ -19,7 +19,7 @@ const log = createLogger('Sqlite:Migrations');
19
19
  export const XOPC_DB_BASELINE_SCHEMA_VERSION = 11;
20
20
 
21
21
  /** Latest schema version this release supports (increment when adding migrations). */
22
- export const XOPC_DB_SCHEMA_VERSION = 24;
22
+ export const XOPC_DB_SCHEMA_VERSION = 28;
23
23
 
24
24
  const MODULE_DIR = dirname(fileURLToPath(import.meta.url));
25
25
 
@@ -31,7 +31,7 @@ function ensureXopcDatabaseSchema(db) {
31
31
  bootstrapFreshDatabase(db);
32
32
  currentVersion = 11;
33
33
  }
34
- applyPendingMigrations(db, { targetVersion: 24 });
34
+ applyPendingMigrations(db, { targetVersion: 28 });
35
35
  }
36
36
  //#endregion
37
37
  export { SCHEMA_META_SCHEMA_VERSION_KEY, XOPC_DB_BASELINE_SCHEMA_VERSION, XOPC_DB_SCHEMA_VERSION, ensureSchemaMetaTable, ensureXopcDatabaseSchema, readSchemaVersion, readSchemaVersionForTest, setSchemaVersion };