@pylonsync/create-pylon 0.19.1 → 0.20.0

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 (675) hide show
  1. package/bin/create-pylon.js +30 -7
  2. package/package.json +1 -1
  3. package/templates/agency/.env.example +7 -0
  4. package/templates/agency/README.md +28 -3
  5. package/templates/agency/app/(marketing)/contact-form.tsx +20 -18
  6. package/templates/agency/app/(marketing)/page.tsx +3 -3
  7. package/templates/agency/app/(marketing)/work/page.tsx +3 -3
  8. package/templates/agency/app/auth-form.tsx +4 -4
  9. package/templates/agency/app/dashboard/dashboard-client.tsx +110 -64
  10. package/templates/agency/app/dashboard/page.tsx +14 -16
  11. package/templates/agency/app/icon.svg +3 -0
  12. package/templates/agency/app/login/page.tsx +12 -14
  13. package/templates/agency/components/marketing.tsx +12 -5
  14. package/templates/agency/functions/seedStudioBackoffice.ts +42 -11
  15. package/templates/agency/lib/agency.ts +22 -0
  16. package/templates/agency/lib/demo.ts +118 -0
  17. package/templates/agency/lib/site.config.ts +19 -13
  18. package/templates/agency/public/favicon.ico +0 -0
  19. package/templates/agency/tests/agency.test.ts +57 -0
  20. package/templates/ai-chat/.env.example +13 -27
  21. package/templates/ai-chat/README.md +53 -60
  22. package/templates/ai-chat/app/(chat)/c/[id]/page.tsx +19 -0
  23. package/templates/ai-chat/app/(chat)/chat-client.tsx +657 -622
  24. package/templates/ai-chat/app/(chat)/layout.tsx +6 -43
  25. package/templates/ai-chat/app/(chat)/not-found.tsx +6 -10
  26. package/templates/ai-chat/app/(chat)/page.tsx +7 -11
  27. package/templates/ai-chat/app/auth-form.tsx +45 -38
  28. package/templates/ai-chat/app/error.tsx +9 -12
  29. package/templates/ai-chat/app/globals.css +464 -102
  30. package/templates/ai-chat/app/layout.tsx +11 -19
  31. package/templates/ai-chat/app/login/page.tsx +39 -24
  32. package/templates/ai-chat/app.ts +118 -43
  33. package/templates/ai-chat/components/chat/composer.tsx +193 -0
  34. package/templates/ai-chat/components/chat/empty-state.tsx +65 -0
  35. package/templates/ai-chat/components/chat/icons.tsx +198 -0
  36. package/templates/ai-chat/components/chat/markdown.tsx +164 -0
  37. package/templates/ai-chat/components/chat/message.tsx +183 -0
  38. package/templates/ai-chat/components/chat/setup.tsx +71 -0
  39. package/templates/ai-chat/components/chat/sidebar.tsx +288 -0
  40. package/templates/ai-chat/functions/_beginTurn.ts +145 -0
  41. package/templates/ai-chat/functions/_finishTurn.ts +55 -0
  42. package/templates/ai-chat/functions/assistantStatus.ts +13 -0
  43. package/templates/ai-chat/functions/claimGuestHistory.ts +39 -0
  44. package/templates/ai-chat/functions/createConversation.ts +28 -0
  45. package/templates/ai-chat/functions/deleteConversation.ts +15 -20
  46. package/templates/ai-chat/functions/prepareGuestClaim.ts +32 -0
  47. package/templates/ai-chat/functions/renameConversation.ts +19 -0
  48. package/templates/ai-chat/functions/respond.ts +88 -0
  49. package/templates/ai-chat/functions/seedExamples.ts +46 -0
  50. package/templates/ai-chat/functions/stopResponse.ts +30 -0
  51. package/templates/ai-chat/lib/brand-icon.ts +7 -0
  52. package/templates/ai-chat/lib/chat.ts +277 -0
  53. package/templates/ai-chat/lib/claim.ts +15 -0
  54. package/templates/ai-chat/lib/examples.ts +179 -0
  55. package/templates/ai-chat/lib/highlight.ts +107 -0
  56. package/templates/ai-chat/lib/markdown.ts +317 -0
  57. package/templates/ai-chat/lib/provider.ts +38 -0
  58. package/templates/ai-chat/lib/site.config.ts +55 -39
  59. package/templates/ai-chat/tests/chat.test.ts +174 -0
  60. package/templates/ai-chat/tests/components.test.tsx +57 -0
  61. package/templates/ai-chat/tests/markdown.test.ts +88 -0
  62. package/templates/ai-chat/tests/provider.test.ts +32 -0
  63. package/templates/backend/consumer/apps/api/app/layout.tsx +33 -0
  64. package/templates/backend/consumer/apps/api/app/page.tsx +54 -0
  65. package/templates/backend/consumer/apps/api/app.ts +166 -0
  66. package/templates/backend/consumer/apps/api/functions/addComment.ts +24 -0
  67. package/templates/backend/consumer/apps/api/functions/createPost.ts +19 -36
  68. package/templates/backend/consumer/apps/api/functions/deletePost.ts +15 -13
  69. package/templates/backend/consumer/apps/api/functions/myProfile.ts +5 -8
  70. package/templates/backend/consumer/apps/api/functions/seedDemo.ts +95 -0
  71. package/templates/backend/consumer/apps/api/functions/setFollow.ts +36 -0
  72. package/templates/backend/consumer/apps/api/functions/setLike.ts +36 -0
  73. package/templates/backend/consumer/apps/api/functions/upsertProfile.ts +43 -37
  74. package/templates/backend/consumer/apps/api/lib/seed.ts +89 -0
  75. package/templates/backend/consumer/apps/api/lib/social.ts +50 -0
  76. package/templates/backend/consumer/apps/api/package.json +8 -2
  77. package/templates/backend/consumer/apps/api/public/images/posts/arch.jpg +0 -0
  78. package/templates/backend/consumer/apps/api/public/images/posts/coffee.jpg +0 -0
  79. package/templates/backend/consumer/apps/api/public/images/posts/dog.jpg +0 -0
  80. package/templates/backend/consumer/apps/api/public/images/posts/lisbon.jpg +0 -0
  81. package/templates/backend/consumer/apps/api/public/images/posts/ramen.jpg +0 -0
  82. package/templates/backend/consumer/apps/api/public/images/posts/run.jpg +0 -0
  83. package/templates/backend/consumer/apps/api/public/images/posts/sunset.jpg +0 -0
  84. package/templates/backend/consumer/apps/api/tsconfig.json +12 -2
  85. package/templates/chat/README.md +43 -21
  86. package/templates/chat/app/chat-client.tsx +495 -85
  87. package/templates/chat/app/error.tsx +1 -1
  88. package/templates/chat/app/globals.css +170 -54
  89. package/templates/chat/app/layout.tsx +12 -9
  90. package/templates/chat/app/not-found.tsx +4 -2
  91. package/templates/chat/app/page.tsx +9 -19
  92. package/templates/chat/app.ts +96 -17
  93. package/templates/chat/components/chat/avatar.tsx +45 -0
  94. package/templates/chat/components/chat/composer.tsx +110 -0
  95. package/templates/chat/components/chat/message-list.tsx +285 -0
  96. package/templates/chat/components/chat/sidebar.tsx +190 -0
  97. package/templates/chat/functions/joinChat.ts +83 -0
  98. package/templates/chat/functions/renameMember.ts +25 -0
  99. package/templates/chat/lib/chat.ts +188 -0
  100. package/templates/chat/lib/seed.ts +78 -0
  101. package/templates/chat/tests/chat.test.ts +159 -0
  102. package/templates/chat/tests/composer.test.tsx +54 -0
  103. package/templates/consumer/README.md +3 -3
  104. package/templates/consumer/app/globals.css +10 -3
  105. package/templates/consumer/app/icon.svg +1 -0
  106. package/templates/consumer/components/social/avatar.tsx +25 -11
  107. package/templates/consumer/functions/seedFeed.ts +3 -3
  108. package/templates/consumer/lib/seed.ts +43 -85
  109. package/templates/consumer/public/favicon.ico +0 -0
  110. package/templates/consumer/public/images/posts/arch.jpg +0 -0
  111. package/templates/consumer/public/images/posts/coffee.jpg +0 -0
  112. package/templates/consumer/public/images/posts/dog.jpg +0 -0
  113. package/templates/consumer/public/images/posts/lisbon.jpg +0 -0
  114. package/templates/consumer/public/images/posts/ramen.jpg +0 -0
  115. package/templates/consumer/public/images/posts/run.jpg +0 -0
  116. package/templates/consumer/public/images/posts/sunset.jpg +0 -0
  117. package/templates/consumer/tests/seed.test.ts +39 -0
  118. package/templates/creator/.env.example +7 -0
  119. package/templates/creator/README.md +24 -5
  120. package/templates/creator/app/(marketing)/newsletter-signup.tsx +33 -12
  121. package/templates/creator/app/auth-form.tsx +7 -7
  122. package/templates/creator/app/dashboard/dashboard-client.tsx +137 -100
  123. package/templates/creator/app/dashboard/page.tsx +18 -18
  124. package/templates/creator/app/icon.svg +2 -0
  125. package/templates/creator/app/login/page.tsx +14 -14
  126. package/templates/creator/app.ts +4 -4
  127. package/templates/creator/functions/seedDemo.ts +32 -0
  128. package/templates/creator/functions/subscribe.ts +4 -15
  129. package/templates/creator/lib/demo.ts +94 -0
  130. package/templates/creator/lib/site.config.ts +15 -10
  131. package/templates/creator/lib/stats.ts +35 -0
  132. package/templates/creator/public/favicon.ico +0 -0
  133. package/templates/creator/tests/counter.test.ts +92 -0
  134. package/templates/creator/tests/live-count.test.tsx +45 -0
  135. package/templates/crm/README.md +22 -1
  136. package/templates/crm/app/apple-icon.png +0 -0
  137. package/templates/crm/app/companies/companies-view.tsx +32 -14
  138. package/templates/crm/app/companies/page.tsx +2 -1
  139. package/templates/crm/app/contacts/contacts-view.tsx +34 -10
  140. package/templates/crm/app/contacts/page.tsx +2 -1
  141. package/templates/crm/app/deals/[id]/deal-view.tsx +137 -98
  142. package/templates/crm/app/deals/[id]/page.tsx +2 -1
  143. package/templates/crm/app/globals.css +29 -14
  144. package/templates/crm/app/icon.svg +1 -0
  145. package/templates/crm/app/layout.tsx +7 -5
  146. package/templates/crm/app/page.tsx +2 -1
  147. package/templates/crm/app/pipeline-view.tsx +5 -5
  148. package/templates/crm/app/workspace.tsx +29 -17
  149. package/templates/crm/app.ts +5 -5
  150. package/templates/crm/components/activity-timeline.tsx +14 -4
  151. package/templates/crm/components/app-shell.tsx +94 -0
  152. package/templates/crm/components/auth-form.tsx +35 -5
  153. package/templates/crm/components/avatar.tsx +5 -1
  154. package/templates/crm/components/board-skeleton.tsx +5 -5
  155. package/templates/crm/components/brand-mark.tsx +25 -0
  156. package/templates/crm/components/command-palette.tsx +17 -17
  157. package/templates/crm/components/data-table.tsx +97 -36
  158. package/templates/crm/components/deal-card.tsx +23 -27
  159. package/templates/crm/components/filter-tabs.tsx +101 -0
  160. package/templates/crm/components/metrics-bar.tsx +6 -6
  161. package/templates/crm/components/nav.tsx +15 -0
  162. package/templates/crm/components/page-header.tsx +43 -7
  163. package/templates/crm/components/pipeline-board.tsx +16 -10
  164. package/templates/crm/components/require-auth.tsx +24 -26
  165. package/templates/crm/components/sidebar.tsx +54 -49
  166. package/templates/crm/components/stage-badge.tsx +4 -2
  167. package/templates/crm/components/ui/button.tsx +5 -4
  168. package/templates/crm/components/ui/dialog.tsx +1 -1
  169. package/templates/crm/components/ui/input.tsx +1 -1
  170. package/templates/crm/components/ui/select.tsx +1 -1
  171. package/templates/crm/components/ui/textarea.tsx +1 -1
  172. package/templates/crm/functions/seedWorkspace.ts +32 -17
  173. package/templates/crm/lib/brand.ts +9 -0
  174. package/templates/crm/lib/demo-team.ts +51 -0
  175. package/templates/crm/lib/pipeline.ts +37 -0
  176. package/templates/crm/lib/search.ts +4 -3
  177. package/templates/crm/lib/seed.ts +53 -8
  178. package/templates/crm/public/favicon.ico +0 -0
  179. package/templates/crm/tests/components.test.tsx +5 -5
  180. package/templates/crm/tests/demo-team.test.ts +18 -0
  181. package/templates/crm/tests/pipeline.test.ts +59 -3
  182. package/templates/directory/.env.example +7 -0
  183. package/templates/directory/README.md +22 -3
  184. package/templates/directory/app/(marketing)/directory-browse.tsx +30 -12
  185. package/templates/directory/app/(marketing)/page.tsx +6 -4
  186. package/templates/directory/app/auth-form.tsx +4 -4
  187. package/templates/directory/app/dashboard/dashboard-client.tsx +177 -90
  188. package/templates/directory/app/dashboard/page.tsx +16 -14
  189. package/templates/directory/app/icon.svg +4 -0
  190. package/templates/directory/app/login/page.tsx +16 -15
  191. package/templates/directory/functions/seedDemo.ts +43 -0
  192. package/templates/directory/lib/demo.ts +145 -0
  193. package/templates/directory/lib/directory.ts +27 -0
  194. package/templates/directory/lib/site.config.ts +21 -16
  195. package/templates/directory/public/favicon.ico +0 -0
  196. package/templates/directory/tests/directory.test.ts +46 -0
  197. package/templates/expo/consumer/apps/expo/App.tsx +360 -288
  198. package/templates/expo/consumer/apps/expo/app.json +9 -1
  199. package/templates/expo/consumer/apps/expo/package.json +2 -1
  200. package/templates/helpdesk/README.md +21 -0
  201. package/templates/helpdesk/app/apple-icon.png +0 -0
  202. package/templates/helpdesk/app/customers/customers-view.tsx +23 -7
  203. package/templates/helpdesk/app/customers/page.tsx +2 -1
  204. package/templates/helpdesk/app/globals.css +29 -14
  205. package/templates/helpdesk/app/icon.svg +1 -0
  206. package/templates/helpdesk/app/inbox-view.tsx +66 -47
  207. package/templates/helpdesk/app/layout.tsx +7 -5
  208. package/templates/helpdesk/app/page.tsx +4 -7
  209. package/templates/helpdesk/app/tickets/[id]/page.tsx +2 -1
  210. package/templates/helpdesk/app/tickets/[id]/ticket-view.tsx +134 -97
  211. package/templates/helpdesk/app/workspace.tsx +33 -22
  212. package/templates/helpdesk/app.ts +2 -2
  213. package/templates/helpdesk/components/app-shell.tsx +94 -0
  214. package/templates/helpdesk/components/auth-form.tsx +35 -5
  215. package/templates/helpdesk/components/avatar.tsx +5 -1
  216. package/templates/helpdesk/components/brand-mark.tsx +28 -0
  217. package/templates/helpdesk/components/command-palette.tsx +17 -17
  218. package/templates/helpdesk/components/data-table.tsx +97 -36
  219. package/templates/helpdesk/components/filter-tabs.tsx +101 -0
  220. package/templates/helpdesk/components/nav.tsx +14 -0
  221. package/templates/helpdesk/components/page-header.tsx +43 -7
  222. package/templates/helpdesk/components/priority-badge.tsx +49 -6
  223. package/templates/helpdesk/components/require-auth.tsx +24 -26
  224. package/templates/helpdesk/components/sidebar.tsx +54 -48
  225. package/templates/helpdesk/components/sla-indicator.tsx +5 -8
  226. package/templates/helpdesk/components/thread.tsx +7 -7
  227. package/templates/helpdesk/components/ticket-list.tsx +79 -19
  228. package/templates/helpdesk/components/ui/button.tsx +5 -4
  229. package/templates/helpdesk/components/ui/dialog.tsx +1 -1
  230. package/templates/helpdesk/components/ui/input.tsx +1 -1
  231. package/templates/helpdesk/components/ui/select.tsx +1 -1
  232. package/templates/helpdesk/components/ui/textarea.tsx +1 -1
  233. package/templates/helpdesk/functions/seedWorkspace.ts +25 -15
  234. package/templates/helpdesk/lib/brand.ts +9 -0
  235. package/templates/helpdesk/lib/demo-team.ts +51 -0
  236. package/templates/helpdesk/lib/search.ts +4 -3
  237. package/templates/helpdesk/lib/seed.ts +43 -2
  238. package/templates/helpdesk/lib/tickets.ts +68 -20
  239. package/templates/helpdesk/public/favicon.ico +0 -0
  240. package/templates/helpdesk/tests/components.test.tsx +10 -0
  241. package/templates/helpdesk/tests/demo-team.test.ts +18 -0
  242. package/templates/helpdesk/tests/tickets.test.ts +70 -0
  243. package/templates/imessage/.env.example +49 -0
  244. package/templates/imessage/AGENTS.md +153 -0
  245. package/templates/imessage/CLAUDE.md +7 -0
  246. package/templates/imessage/README.md +187 -0
  247. package/templates/imessage/app/console.tsx +382 -0
  248. package/templates/imessage/app/contacts/page.tsx +13 -0
  249. package/templates/imessage/app/error.tsx +16 -0
  250. package/templates/imessage/app/gate.tsx +31 -0
  251. package/templates/imessage/app/globals.css +183 -0
  252. package/templates/imessage/app/layout.tsx +17 -0
  253. package/templates/imessage/app/login/login-form.tsx +115 -0
  254. package/templates/imessage/app/login/page.tsx +30 -0
  255. package/templates/imessage/app/not-found.tsx +13 -0
  256. package/templates/imessage/app/page.tsx +14 -0
  257. package/templates/imessage/app/robots.ts +6 -0
  258. package/templates/imessage/app/setup/page.tsx +13 -0
  259. package/templates/imessage/app/sign-out.tsx +20 -0
  260. package/templates/imessage/app.ts +299 -0
  261. package/templates/imessage/bunfig +4 -0
  262. package/templates/imessage/components/avatar.tsx +46 -0
  263. package/templates/imessage/components/contact-panel.tsx +263 -0
  264. package/templates/imessage/components/contacts-view.tsx +172 -0
  265. package/templates/imessage/components/conversation-list.tsx +129 -0
  266. package/templates/imessage/components/nav-rail.tsx +67 -0
  267. package/templates/imessage/components/setup-view.tsx +408 -0
  268. package/templates/imessage/components/thread.tsx +229 -0
  269. package/templates/imessage/components/ui/badge.tsx +37 -0
  270. package/templates/imessage/components/ui/button.tsx +56 -0
  271. package/templates/imessage/components/ui/card.tsx +90 -0
  272. package/templates/imessage/components/ui/dialog.tsx +110 -0
  273. package/templates/imessage/components/ui/input.tsx +30 -0
  274. package/templates/imessage/components/ui/label.tsx +21 -0
  275. package/templates/imessage/components/ui/select.tsx +39 -0
  276. package/templates/imessage/components/ui/table.tsx +124 -0
  277. package/templates/imessage/components/ui/textarea.tsx +24 -0
  278. package/templates/imessage/components.json +20 -0
  279. package/templates/imessage/functions/answerNow.ts +34 -0
  280. package/templates/imessage/functions/assistant.ts +96 -0
  281. package/templates/imessage/functions/beginTurn.ts +101 -0
  282. package/templates/imessage/functions/cancelReminder.ts +13 -0
  283. package/templates/imessage/functions/claimOutbound.ts +44 -0
  284. package/templates/imessage/functions/clearDemo.ts +18 -0
  285. package/templates/imessage/functions/deleteNote.ts +11 -0
  286. package/templates/imessage/functions/deliverOutbound.ts +55 -0
  287. package/templates/imessage/functions/finishTurn.ts +84 -0
  288. package/templates/imessage/functions/fireReminder.ts +42 -0
  289. package/templates/imessage/functions/ingestInbound.ts +22 -0
  290. package/templates/imessage/functions/ownerAccount.ts +13 -0
  291. package/templates/imessage/functions/processTurn.ts +74 -0
  292. package/templates/imessage/functions/relayApply.ts +102 -0
  293. package/templates/imessage/functions/relaySync.ts +63 -0
  294. package/templates/imessage/functions/releaseOutbound.ts +18 -0
  295. package/templates/imessage/functions/resolveTurn.ts +25 -0
  296. package/templates/imessage/functions/saveContact.ts +27 -0
  297. package/templates/imessage/functions/seedDemo.ts +70 -0
  298. package/templates/imessage/functions/sendOwnerMessage.ts +28 -0
  299. package/templates/imessage/functions/sendblueWebhook.ts +49 -0
  300. package/templates/imessage/functions/setContactStatus.ts +17 -0
  301. package/templates/imessage/functions/settleOutbound.ts +35 -0
  302. package/templates/imessage/functions/setupStatus.ts +33 -0
  303. package/templates/imessage/functions/sweep.ts +58 -0
  304. package/templates/imessage/functions/toolCancelReminder.ts +17 -0
  305. package/templates/imessage/functions/toolForgetNote.ts +15 -0
  306. package/templates/imessage/functions/toolListNotes.ts +16 -0
  307. package/templates/imessage/functions/toolListReminders.ts +17 -0
  308. package/templates/imessage/functions/toolSaveNote.ts +22 -0
  309. package/templates/imessage/functions/toolSetReminder.ts +55 -0
  310. package/templates/imessage/functions/updateSettings.ts +48 -0
  311. package/templates/imessage/gitignore +11 -0
  312. package/templates/imessage/lib/agent-call.ts +251 -0
  313. package/templates/imessage/lib/assistant.ts +74 -0
  314. package/templates/imessage/lib/chunk.ts +135 -0
  315. package/templates/imessage/lib/demo.ts +68 -0
  316. package/templates/imessage/lib/format.ts +170 -0
  317. package/templates/imessage/lib/gate.ts +104 -0
  318. package/templates/imessage/lib/handles.ts +67 -0
  319. package/templates/imessage/lib/provider.ts +38 -0
  320. package/templates/imessage/lib/relay-protocol.ts +95 -0
  321. package/templates/imessage/lib/reminders.ts +41 -0
  322. package/templates/imessage/lib/secrets.ts +59 -0
  323. package/templates/imessage/lib/sendblue.ts +173 -0
  324. package/templates/imessage/lib/store.ts +407 -0
  325. package/templates/imessage/lib/transport.ts +135 -0
  326. package/templates/imessage/lib/utils.ts +10 -0
  327. package/templates/imessage/package.json +40 -0
  328. package/templates/imessage/relay/.env.example +14 -0
  329. package/templates/imessage/relay/applescript.ts +70 -0
  330. package/templates/imessage/relay/chatdb.ts +165 -0
  331. package/templates/imessage/relay/client.ts +41 -0
  332. package/templates/imessage/relay/main.ts +137 -0
  333. package/templates/imessage/relay/state.ts +35 -0
  334. package/templates/imessage/tests/agent-call.test.ts +112 -0
  335. package/templates/imessage/tests/applescript.test.ts +88 -0
  336. package/templates/imessage/tests/chatdb.test.ts +139 -0
  337. package/templates/imessage/tests/chunk.test.ts +61 -0
  338. package/templates/imessage/tests/fake-ctx.ts +88 -0
  339. package/templates/imessage/tests/ingest.test.ts +141 -0
  340. package/templates/imessage/tests/lib.test.ts +99 -0
  341. package/templates/imessage/tests/review-fixes.test.ts +138 -0
  342. package/templates/imessage/tests/secrets.test.ts +36 -0
  343. package/templates/imessage/tests/sendblue.test.ts +143 -0
  344. package/templates/imessage/tests/setup.ts +5 -0
  345. package/templates/imessage/tsconfig.json +18 -0
  346. package/templates/inventory/README.md +21 -0
  347. package/templates/inventory/app/apple-icon.png +0 -0
  348. package/templates/inventory/app/globals.css +29 -14
  349. package/templates/inventory/app/icon.svg +1 -0
  350. package/templates/inventory/app/layout.tsx +7 -5
  351. package/templates/inventory/app/movements/movements-view.tsx +32 -10
  352. package/templates/inventory/app/movements/page.tsx +2 -1
  353. package/templates/inventory/app/page.tsx +4 -6
  354. package/templates/inventory/app/products/[id]/page.tsx +2 -1
  355. package/templates/inventory/app/products/[id]/product-view.tsx +55 -25
  356. package/templates/inventory/app/products-view.tsx +74 -33
  357. package/templates/inventory/app/workspace.tsx +34 -16
  358. package/templates/inventory/app.ts +10 -2
  359. package/templates/inventory/components/app-shell.tsx +94 -0
  360. package/templates/inventory/components/auth-form.tsx +35 -5
  361. package/templates/inventory/components/avatar.tsx +5 -1
  362. package/templates/inventory/components/brand-mark.tsx +25 -0
  363. package/templates/inventory/components/category-tile.tsx +49 -0
  364. package/templates/inventory/components/command-palette.tsx +17 -17
  365. package/templates/inventory/components/data-table.tsx +97 -36
  366. package/templates/inventory/components/filter-tabs.tsx +101 -0
  367. package/templates/inventory/components/nav.tsx +14 -0
  368. package/templates/inventory/components/page-header.tsx +43 -7
  369. package/templates/inventory/components/require-auth.tsx +24 -26
  370. package/templates/inventory/components/sidebar.tsx +54 -48
  371. package/templates/inventory/components/stock-level.tsx +30 -14
  372. package/templates/inventory/components/summary-bar.tsx +8 -15
  373. package/templates/inventory/components/ui/button.tsx +5 -4
  374. package/templates/inventory/components/ui/dialog.tsx +1 -1
  375. package/templates/inventory/components/ui/input.tsx +1 -1
  376. package/templates/inventory/components/ui/select.tsx +1 -1
  377. package/templates/inventory/components/ui/textarea.tsx +1 -1
  378. package/templates/inventory/functions/seedWorkspace.ts +18 -8
  379. package/templates/inventory/lib/brand.ts +9 -0
  380. package/templates/inventory/lib/demo-team.ts +51 -0
  381. package/templates/inventory/lib/search.ts +4 -3
  382. package/templates/inventory/lib/seed.ts +29 -2
  383. package/templates/inventory/lib/stock.ts +32 -0
  384. package/templates/inventory/public/favicon.ico +0 -0
  385. package/templates/inventory/tests/demo-team.test.ts +18 -0
  386. package/templates/inventory/tests/stock.test.ts +29 -0
  387. package/templates/invoices/README.md +16 -0
  388. package/templates/invoices/app/apple-icon.png +0 -0
  389. package/templates/invoices/app/clients/clients-view.tsx +18 -6
  390. package/templates/invoices/app/clients/page.tsx +2 -1
  391. package/templates/invoices/app/globals.css +29 -14
  392. package/templates/invoices/app/icon.svg +1 -0
  393. package/templates/invoices/app/invoices/[id]/invoice-view.tsx +72 -48
  394. package/templates/invoices/app/invoices/[id]/page.tsx +2 -1
  395. package/templates/invoices/app/invoices-view.tsx +81 -32
  396. package/templates/invoices/app/layout.tsx +7 -5
  397. package/templates/invoices/app/page.tsx +4 -6
  398. package/templates/invoices/app/workspace.tsx +26 -16
  399. package/templates/invoices/app.ts +2 -2
  400. package/templates/invoices/components/app-shell.tsx +94 -0
  401. package/templates/invoices/components/auth-form.tsx +35 -5
  402. package/templates/invoices/components/avatar.tsx +5 -1
  403. package/templates/invoices/components/brand-mark.tsx +26 -0
  404. package/templates/invoices/components/command-palette.tsx +17 -17
  405. package/templates/invoices/components/data-table.tsx +97 -36
  406. package/templates/invoices/components/filter-tabs.tsx +101 -0
  407. package/templates/invoices/components/line-items.tsx +19 -14
  408. package/templates/invoices/components/nav.tsx +14 -0
  409. package/templates/invoices/components/page-header.tsx +43 -7
  410. package/templates/invoices/components/require-auth.tsx +24 -26
  411. package/templates/invoices/components/sidebar.tsx +54 -48
  412. package/templates/invoices/components/status-badge.tsx +7 -7
  413. package/templates/invoices/components/summary-bar.tsx +5 -5
  414. package/templates/invoices/components/ui/button.tsx +5 -4
  415. package/templates/invoices/components/ui/dialog.tsx +1 -1
  416. package/templates/invoices/components/ui/input.tsx +1 -1
  417. package/templates/invoices/components/ui/select.tsx +1 -1
  418. package/templates/invoices/components/ui/textarea.tsx +1 -1
  419. package/templates/invoices/functions/recordPayment.ts +3 -0
  420. package/templates/invoices/lib/billing.ts +40 -0
  421. package/templates/invoices/lib/brand.ts +9 -0
  422. package/templates/invoices/lib/search.ts +4 -3
  423. package/templates/invoices/public/favicon.ico +0 -0
  424. package/templates/invoices/tests/billing.test.ts +27 -0
  425. package/templates/ios/consumer/apps/ios/Package.swift +4 -3
  426. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/ActivityView.swift +107 -0
  427. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/AppModel.swift +171 -0
  428. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/AuthView.swift +182 -0
  429. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/CommentsView.swift +136 -0
  430. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/ComposeView.swift +152 -0
  431. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/ExploreView.swift +117 -0
  432. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/FeedView.swift +175 -159
  433. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/MainTabView.swift +54 -0
  434. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/Media.swift +271 -0
  435. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/Models.swift +101 -4
  436. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/PostCard.swift +245 -0
  437. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/ProfileEditorView.swift +208 -0
  438. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/ProfileView.swift +183 -0
  439. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/RootView.swift +25 -31
  440. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/SocialStore.swift +285 -0
  441. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/StoryViewer.swift +155 -0
  442. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/Theme.swift +82 -0
  443. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +4 -47
  444. package/templates/ios/consumer/apps/ios/project.yml +13 -9
  445. package/templates/local-service/.env.example +7 -0
  446. package/templates/local-service/README.md +26 -2
  447. package/templates/local-service/app/(marketing)/booking-widget.tsx +48 -9
  448. package/templates/local-service/app/(marketing)/page.tsx +66 -42
  449. package/templates/local-service/app/auth-form.tsx +8 -8
  450. package/templates/local-service/app/dashboard/dashboard-client.tsx +119 -92
  451. package/templates/local-service/app/dashboard/page.tsx +14 -14
  452. package/templates/local-service/app/icon.svg +2 -0
  453. package/templates/local-service/app/login/page.tsx +15 -15
  454. package/templates/local-service/functions/seedDemo.ts +49 -0
  455. package/templates/local-service/lib/demo.ts +135 -0
  456. package/templates/local-service/lib/hours.ts +44 -0
  457. package/templates/local-service/lib/site.config.ts +8 -3
  458. package/templates/local-service/lib/slots.ts +3 -0
  459. package/templates/local-service/public/favicon.ico +0 -0
  460. package/templates/local-service/tests/booking.test.ts +104 -0
  461. package/templates/marketplace/README.md +23 -15
  462. package/templates/marketplace/app/globals.css +172 -114
  463. package/templates/marketplace/app/layout.tsx +75 -30
  464. package/templates/marketplace/app/listing/[id]/page.tsx +210 -123
  465. package/templates/marketplace/app/me/page.tsx +4 -4
  466. package/templates/marketplace/app/page.tsx +221 -298
  467. package/templates/marketplace/app/sell/page.tsx +8 -11
  468. package/templates/marketplace/app.ts +32 -7
  469. package/templates/marketplace/client/ActivityFeed.tsx +311 -0
  470. package/templates/marketplace/client/AuthNav.tsx +24 -16
  471. package/templates/marketplace/client/ListingCard.tsx +118 -0
  472. package/templates/marketplace/client/LiveGrid.tsx +92 -0
  473. package/templates/marketplace/client/LiveListingStatus.tsx +32 -24
  474. package/templates/marketplace/client/LoginCard.tsx +114 -136
  475. package/templates/marketplace/client/MarketProvider.tsx +3 -5
  476. package/templates/marketplace/client/MyMarket.tsx +371 -185
  477. package/templates/marketplace/client/OfferPanel.tsx +421 -287
  478. package/templates/marketplace/client/SellForm.tsx +271 -192
  479. package/templates/marketplace/client/ThemeToggle.tsx +1 -0
  480. package/templates/marketplace/client/WatchButton.tsx +16 -16
  481. package/templates/marketplace/client/market.ts +61 -14
  482. package/templates/marketplace/functions/seedMarket.ts +143 -36
  483. package/templates/marketplace/functions/seedOffers.ts +101 -0
  484. package/templates/marketplace/lib/catalog.ts +72 -1
  485. package/templates/marketplace/public/images/listings/ceramic-lamp-640.webp +0 -0
  486. package/templates/marketplace/public/images/listings/ceramic-lamp.webp +0 -0
  487. package/templates/marketplace/public/images/listings/duffel-640.webp +0 -0
  488. package/templates/marketplace/public/images/listings/duffel.webp +0 -0
  489. package/templates/marketplace/public/images/listings/film-camera-640.webp +0 -0
  490. package/templates/marketplace/public/images/listings/film-camera.webp +0 -0
  491. package/templates/marketplace/public/images/listings/lounge-chair-640.webp +0 -0
  492. package/templates/marketplace/public/images/listings/lounge-chair.webp +0 -0
  493. package/templates/marketplace/public/images/listings/road-bike-640.webp +0 -0
  494. package/templates/marketplace/public/images/listings/road-bike.webp +0 -0
  495. package/templates/marketplace/public/images/listings/slipper-chair-640.webp +0 -0
  496. package/templates/marketplace/public/images/listings/slipper-chair.webp +0 -0
  497. package/templates/marketplace/public/images/listings/turntable-640.webp +0 -0
  498. package/templates/marketplace/public/images/listings/turntable.webp +0 -0
  499. package/templates/marketplace/public/images/listings/wardrobe-640.webp +0 -0
  500. package/templates/marketplace/public/images/listings/wardrobe.webp +0 -0
  501. package/templates/marketplace/public/images/listings/watch-640.webp +0 -0
  502. package/templates/marketplace/public/images/listings/watch.webp +0 -0
  503. package/templates/marketplace/public/images/listings/weekender-bag-640.webp +0 -0
  504. package/templates/marketplace/public/images/listings/weekender-bag.webp +0 -0
  505. package/templates/marketplace/tests/example.test.ts +64 -2
  506. package/templates/marketplace/tests/listing-card.test.tsx +61 -0
  507. package/templates/projects/README.md +24 -3
  508. package/templates/projects/app/apple-icon.png +0 -0
  509. package/templates/projects/app/clients/clients-view.tsx +20 -6
  510. package/templates/projects/app/clients/page.tsx +2 -1
  511. package/templates/projects/app/globals.css +29 -14
  512. package/templates/projects/app/icon.svg +1 -0
  513. package/templates/projects/app/layout.tsx +7 -5
  514. package/templates/projects/app/page.tsx +3 -5
  515. package/templates/projects/app/projects/[id]/page.tsx +2 -1
  516. package/templates/projects/app/projects/[id]/project-view.tsx +45 -30
  517. package/templates/projects/app/projects-view.tsx +105 -52
  518. package/templates/projects/app/workspace.tsx +41 -20
  519. package/templates/projects/app.ts +2 -2
  520. package/templates/projects/components/app-shell.tsx +94 -0
  521. package/templates/projects/components/auth-form.tsx +35 -5
  522. package/templates/projects/components/avatar.tsx +5 -1
  523. package/templates/projects/components/board-skeleton.tsx +3 -3
  524. package/templates/projects/components/brand-mark.tsx +25 -0
  525. package/templates/projects/components/command-palette.tsx +17 -17
  526. package/templates/projects/components/data-table.tsx +97 -36
  527. package/templates/projects/components/filter-tabs.tsx +101 -0
  528. package/templates/projects/components/meter.tsx +48 -0
  529. package/templates/projects/components/nav.tsx +14 -0
  530. package/templates/projects/components/page-header.tsx +43 -7
  531. package/templates/projects/components/require-auth.tsx +24 -26
  532. package/templates/projects/components/sidebar.tsx +54 -48
  533. package/templates/projects/components/summary-bar.tsx +49 -0
  534. package/templates/projects/components/task-board.tsx +10 -9
  535. package/templates/projects/components/time-dialog.tsx +29 -5
  536. package/templates/projects/components/ui/button.tsx +5 -4
  537. package/templates/projects/components/ui/dialog.tsx +1 -1
  538. package/templates/projects/components/ui/input.tsx +1 -1
  539. package/templates/projects/components/ui/select.tsx +1 -1
  540. package/templates/projects/components/ui/textarea.tsx +1 -1
  541. package/templates/projects/functions/seedWorkspace.ts +26 -12
  542. package/templates/projects/lib/brand.ts +9 -0
  543. package/templates/projects/lib/demo-team.ts +51 -0
  544. package/templates/projects/lib/search.ts +4 -3
  545. package/templates/projects/lib/seed.ts +35 -1
  546. package/templates/projects/lib/work.ts +63 -0
  547. package/templates/projects/public/favicon.ico +0 -0
  548. package/templates/projects/tests/demo-team.test.ts +18 -0
  549. package/templates/projects/tests/work.test.ts +65 -0
  550. package/templates/restaurant/.env.example +7 -0
  551. package/templates/restaurant/README.md +29 -3
  552. package/templates/restaurant/app/(marketing)/layout.tsx +4 -2
  553. package/templates/restaurant/app/(marketing)/page.tsx +47 -23
  554. package/templates/restaurant/app/(marketing)/reservation-widget.tsx +37 -7
  555. package/templates/restaurant/app/auth-form.tsx +8 -8
  556. package/templates/restaurant/app/dashboard/dashboard-client.tsx +241 -109
  557. package/templates/restaurant/app/dashboard/page.tsx +14 -14
  558. package/templates/restaurant/app/icon.svg +4 -0
  559. package/templates/restaurant/app/login/page.tsx +15 -15
  560. package/templates/restaurant/functions/seedDemo.ts +44 -0
  561. package/templates/restaurant/lib/demo.ts +144 -0
  562. package/templates/restaurant/lib/hours.ts +45 -0
  563. package/templates/restaurant/lib/site.config.ts +3 -3
  564. package/templates/restaurant/public/favicon.ico +0 -0
  565. package/templates/restaurant/tests/reservations.test.ts +67 -0
  566. package/templates/saas/README.md +54 -35
  567. package/templates/saas/app/(auth)/auth-form.tsx +25 -23
  568. package/templates/saas/app/(auth)/forgot-password/page.tsx +1 -1
  569. package/templates/saas/app/(auth)/layout.tsx +27 -33
  570. package/templates/saas/app/(auth)/login/page.tsx +1 -1
  571. package/templates/saas/app/(auth)/reset-password/page.tsx +1 -1
  572. package/templates/saas/app/(auth)/signup/page.tsx +1 -1
  573. package/templates/saas/app/(marketing)/layout.tsx +127 -253
  574. package/templates/saas/app/(marketing)/page.tsx +133 -372
  575. package/templates/saas/app/(marketing)/pricing/page.tsx +21 -19
  576. package/templates/saas/app/(marketing)/products/[slug]/page.tsx +40 -95
  577. package/templates/saas/app/dashboard/board-client.tsx +416 -0
  578. package/templates/saas/app/dashboard/dashboard-client.tsx +30 -615
  579. package/templates/saas/app/dashboard/layout.tsx +27 -13
  580. package/templates/saas/app/dashboard/overview-client.tsx +332 -0
  581. package/templates/saas/app/dashboard/page.tsx +29 -35
  582. package/templates/saas/app/dashboard/projects/[id]/page.tsx +29 -0
  583. package/templates/saas/app/dashboard/projects/page.tsx +11 -7
  584. package/templates/saas/app/dashboard/projects-client.tsx +398 -0
  585. package/templates/saas/app/globals.css +5 -4
  586. package/templates/saas/app/icon.svg +1 -0
  587. package/templates/saas/app/onboarding/onboarding-client.tsx +93 -26
  588. package/templates/saas/app/onboarding/page.tsx +6 -2
  589. package/templates/saas/app.ts +68 -13
  590. package/templates/saas/components/dashboard-shell.tsx +212 -132
  591. package/templates/saas/components/marketing.tsx +137 -274
  592. package/templates/saas/components/pricing-table.tsx +12 -11
  593. package/templates/saas/components/setup-checklist.tsx +31 -29
  594. package/templates/saas/components/task-ui.tsx +199 -0
  595. package/templates/saas/functions/createProject.ts +7 -13
  596. package/templates/saas/functions/createTask.ts +67 -0
  597. package/templates/saas/functions/deleteProject.ts +17 -0
  598. package/templates/saas/functions/deleteTask.ts +14 -0
  599. package/templates/saas/functions/seedWorkspace.ts +46 -0
  600. package/templates/saas/functions/setProjectStatus.ts +41 -0
  601. package/templates/saas/functions/updateProfile.ts +13 -0
  602. package/templates/saas/functions/updateProject.ts +4 -5
  603. package/templates/saas/functions/updateTask.ts +88 -0
  604. package/templates/saas/lib/members.ts +53 -0
  605. package/templates/saas/lib/names.ts +53 -0
  606. package/templates/saas/lib/plans.ts +11 -0
  607. package/templates/saas/lib/sample-workspace.ts +117 -0
  608. package/templates/saas/lib/site.config.ts +200 -463
  609. package/templates/saas/lib/tasks.ts +101 -0
  610. package/templates/saas/lib/use-live.ts +25 -0
  611. package/templates/saas/public/favicon.ico +0 -0
  612. package/templates/saas/public/screenshots/board.png +0 -0
  613. package/templates/saas/public/screenshots/members.png +0 -0
  614. package/templates/saas/public/screenshots/overview.png +0 -0
  615. package/templates/saas/public/screenshots/projects.png +0 -0
  616. package/templates/saas/scripts/capture-screenshots.mjs +156 -0
  617. package/templates/saas/tests/names.test.ts +35 -0
  618. package/templates/saas/tests/plans.test.ts +12 -0
  619. package/templates/saas/tests/sample-workspace.test.ts +46 -0
  620. package/templates/saas/tests/tasks.test.ts +81 -0
  621. package/templates/shop/README.md +25 -13
  622. package/templates/shop/app/(marketing)/layout.tsx +94 -67
  623. package/templates/shop/app/(marketing)/not-found.tsx +13 -12
  624. package/templates/shop/app/(marketing)/page.tsx +120 -59
  625. package/templates/shop/app/(marketing)/shop-client.tsx +364 -237
  626. package/templates/shop/app/(marketing)/success/page.tsx +75 -22
  627. package/templates/shop/app/globals.css +94 -1
  628. package/templates/shop/app/layout.tsx +4 -4
  629. package/templates/shop/app/login/page.tsx +4 -4
  630. package/templates/shop/app.ts +18 -6
  631. package/templates/shop/components/cart-button.tsx +33 -0
  632. package/templates/shop/components/marketing.tsx +17 -87
  633. package/templates/shop/components/shelf-count.tsx +36 -0
  634. package/templates/shop/functions/checkout.ts +17 -1
  635. package/templates/shop/lib/cart-store.ts +57 -0
  636. package/templates/shop/lib/shelf-store.ts +35 -0
  637. package/templates/shop/lib/shop.ts +27 -0
  638. package/templates/shop/lib/site.config.ts +108 -98
  639. package/templates/shop/public/images/hero.jpg +0 -0
  640. package/templates/shop/public/images/products/cedar-smoke.jpg +0 -0
  641. package/templates/shop/public/images/products/fig-leaf.jpg +0 -0
  642. package/templates/shop/public/images/products/linen.jpg +0 -0
  643. package/templates/shop/public/images/products/sea-salt.jpg +0 -0
  644. package/templates/shop/public/images/products/tobacco-oak.jpg +0 -0
  645. package/templates/shop/tests/shop.test.ts +64 -0
  646. package/templates/waitlist/.env.example +7 -0
  647. package/templates/waitlist/README.md +29 -13
  648. package/templates/waitlist/app/(marketing)/page.tsx +9 -7
  649. package/templates/waitlist/app/(marketing)/waitlist-hero.tsx +64 -31
  650. package/templates/waitlist/app/auth-form.tsx +8 -8
  651. package/templates/waitlist/app/dashboard/dashboard-client.tsx +132 -95
  652. package/templates/waitlist/app/dashboard/page.tsx +17 -16
  653. package/templates/waitlist/app/icon.svg +2 -0
  654. package/templates/waitlist/app/login/page.tsx +14 -14
  655. package/templates/waitlist/app.ts +6 -6
  656. package/templates/waitlist/functions/joinWaitlist.ts +6 -18
  657. package/templates/waitlist/functions/seedDemo.ts +32 -0
  658. package/templates/waitlist/lib/demo.ts +94 -0
  659. package/templates/waitlist/lib/launch.ts +19 -0
  660. package/templates/waitlist/lib/site.config.ts +14 -9
  661. package/templates/waitlist/lib/stats.ts +35 -0
  662. package/templates/waitlist/public/favicon.ico +0 -0
  663. package/templates/waitlist/tests/counter.test.ts +107 -0
  664. package/templates/waitlist/tests/live-count.test.tsx +48 -0
  665. package/templates/ai-chat/tests/example.test.ts +0 -12
  666. package/templates/backend/consumer/apps/api/functions/feed.ts +0 -57
  667. package/templates/backend/consumer/apps/api/functions/profilePosts.ts +0 -17
  668. package/templates/backend/consumer/apps/api/functions/toggleLike.ts +0 -48
  669. package/templates/backend/consumer/apps/api/schema.ts +0 -143
  670. package/templates/chat/functions/_keep.ts +0 -13
  671. package/templates/chat/tests/example.test.ts +0 -12
  672. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/ProfileSetupView.swift +0 -68
  673. package/templates/marketplace/client/LiveTicker.tsx +0 -116
  674. package/templates/marketplace/client/ScrollToListingsLink.tsx +0 -29
  675. package/templates/marketplace/public/images/marketplace-hero.webp +0 -0
@@ -0,0 +1,101 @@
1
+ import { mutation, v } from "@pylonsync/functions";
2
+ import { buildTurnInput, MAX_RUN_MESSAGES } from "../lib/assistant";
3
+ import { loadSettings, toContact, toConversation, TURN_STALE_MS } from "../lib/store";
4
+
5
+ const MAX_TEXTS_PER_TURN = 20;
6
+ const CONTEXT_LINES = 12;
7
+
8
+ // Claim the next agent turn for a conversation, in one transaction:
9
+ // - a turn already running → flag the conversation so that turn runs again
10
+ // when it finishes, and return "busy"
11
+ // - nothing waiting → "idle"
12
+ // - otherwise mark the waiting texts "processing" under a new turn id and
13
+ // return the agent input built from them.
14
+ // Internal: called by processTurn only.
15
+ export default mutation({
16
+ internal: true,
17
+ auth: "admin",
18
+ args: { conversationId: v.string() },
19
+ async handler(ctx, args) {
20
+ const row = await ctx.db.get("Conversation", args.conversationId);
21
+ if (!row) return { state: "idle" as const };
22
+ const conversation = toConversation(row);
23
+ const now = new Date();
24
+
25
+ if (conversation.turnState === "running") {
26
+ const started = Date.parse(conversation.turnStartedAt ?? "") || 0;
27
+ if (now.getTime() - started < TURN_STALE_MS) {
28
+ await ctx.db.update("Conversation", conversation.id, { needsAnotherTurn: true });
29
+ return { state: "busy" as const };
30
+ }
31
+ }
32
+
33
+ const pending = await ctx.db.query("Message", {
34
+ conversationId: conversation.id,
35
+ status: "received",
36
+ $order: { createdAt: "asc" },
37
+ $limit: MAX_TEXTS_PER_TURN,
38
+ });
39
+ if (pending.length === 0) {
40
+ await ctx.db.update("Conversation", conversation.id, { turnState: "idle", needsAnotherTurn: false });
41
+ return { state: "idle" as const };
42
+ }
43
+
44
+ const turnId = crypto.randomUUID();
45
+ for (const m of pending) {
46
+ await ctx.db.update("Message", String(m.id), { status: "processing", turnId });
47
+ }
48
+ await ctx.db.update("Conversation", conversation.id, {
49
+ turnState: "running",
50
+ turnId,
51
+ turnStartedAt: now.toISOString(),
52
+ needsAnotherTurn: false,
53
+ });
54
+
55
+ const contactRow = await ctx.db.get("Contact", conversation.contactId);
56
+ const contact = contactRow ? toContact(contactRow) : null;
57
+ const settings = await loadSettings(ctx);
58
+
59
+ // One AgentRun holds the model transcript. When it grows past the cap, the
60
+ // next turn starts a new run and carries recent texts as plain context.
61
+ let runId = conversation.agentRunId;
62
+ let context: string[] = [];
63
+ if (runId) {
64
+ const transcript = await ctx.db.query("AgentMessage", { runId, $limit: MAX_RUN_MESSAGES + 1 });
65
+ if (transcript.length > MAX_RUN_MESSAGES) runId = null;
66
+ }
67
+ if (!runId) {
68
+ const recent = await ctx.db.query("Message", {
69
+ conversationId: conversation.id,
70
+ $order: { createdAt: "desc" },
71
+ $limit: CONTEXT_LINES + pending.length,
72
+ });
73
+ const pendingIds = new Set(pending.map((m) => String(m.id)));
74
+ context = recent
75
+ .filter((m) => !pendingIds.has(String(m.id)) && (m.direction === "out" || m.status === "answered"))
76
+ .slice(0, CONTEXT_LINES)
77
+ .reverse()
78
+ .map((m) => `${m.direction === "in" ? "Them" : "You"}: ${String(m.text).replace(/\s+/g, " ").slice(0, 300)}`);
79
+ }
80
+
81
+ const input = buildTurnInput(
82
+ {
83
+ ownerName: settings.ownerName || null,
84
+ contactName: contact?.displayName ?? null,
85
+ handle: conversation.handle,
86
+ timezone: settings.timezone,
87
+ now,
88
+ },
89
+ pending.map((m) => String(m.text)),
90
+ context,
91
+ );
92
+
93
+ return {
94
+ state: "started" as const,
95
+ turnId,
96
+ conversationId: conversation.id,
97
+ runId,
98
+ input,
99
+ };
100
+ },
101
+ });
@@ -0,0 +1,13 @@
1
+ import { mutation, v } from "@pylonsync/functions";
2
+
3
+ // Cancel an upcoming reminder from the dashboard. Owner only.
4
+ export default mutation({
5
+ auth: "admin",
6
+ args: { reminderId: v.string() },
7
+ async handler(ctx, args) {
8
+ const reminder = await ctx.db.get("Reminder", args.reminderId);
9
+ if (!reminder || reminder.status !== "scheduled") throw ctx.error("NOT_FOUND", "No upcoming reminder with that id");
10
+ await ctx.db.update("Reminder", args.reminderId, { status: "cancelled", detail: "Cancelled by the owner" });
11
+ return { ok: true };
12
+ },
13
+ });
@@ -0,0 +1,44 @@
1
+ import { mutation, v } from "@pylonsync/functions";
2
+
3
+ /** Longer than a full batch can take: 5 sends × 20 s fetch timeout, plus settling. */
4
+ const LEASE_MS = 3 * 60 * 1000;
5
+ const BATCH = 5;
6
+
7
+ // Lease a conversation's queued Sendblue messages for deliverOutbound, oldest
8
+ // first. The lease carries a random token; settleOutbound accepts a result
9
+ // only with that token, so a job whose lease expired and was taken over
10
+ // cannot settle (or be followed by a resend of) a message it no longer holds.
11
+ // Internal.
12
+ export default mutation({
13
+ internal: true,
14
+ auth: "admin",
15
+ args: { conversationId: v.string() },
16
+ async handler(ctx, args) {
17
+ const conversation = await ctx.db.get("Conversation", args.conversationId);
18
+ if (!conversation) return { leaseToken: null, messages: [] };
19
+ const now = new Date();
20
+ const rows = await ctx.db.query("Message", {
21
+ conversationId: args.conversationId,
22
+ status: { $in: ["queued", "leased"] },
23
+ $order: { createdAt: "asc" },
24
+ $limit: 50,
25
+ });
26
+ const leaseToken = crypto.randomUUID();
27
+ const leaseUntil = new Date(now.getTime() + LEASE_MS).toISOString();
28
+ const messages: { id: string; handle: string; text: string }[] = [];
29
+ for (const row of rows) {
30
+ if (messages.length >= BATCH) break;
31
+ if (row.direction !== "out" || row.transport !== "sendblue") continue;
32
+ // A live lease belongs to another delivery job; stop so order holds.
33
+ if (row.status === "leased" && String(row.leaseUntil ?? "") > now.toISOString()) break;
34
+ await ctx.db.update("Message", String(row.id), {
35
+ status: "leased",
36
+ leaseUntil,
37
+ leaseToken,
38
+ attempts: (Number(row.attempts) || 0) + 1,
39
+ });
40
+ messages.push({ id: String(row.id), handle: String(conversation.handle), text: String(row.text) });
41
+ }
42
+ return { leaseToken: messages.length > 0 ? leaseToken : null, messages };
43
+ },
44
+ });
@@ -0,0 +1,18 @@
1
+ import { mutation } from "@pylonsync/functions";
2
+
3
+ // Delete every sample row (demo: true). Owner only.
4
+ export default mutation({
5
+ auth: "admin",
6
+ args: {},
7
+ async handler(ctx) {
8
+ let removed = 0;
9
+ for (const entity of ["Message", "Note", "Reminder", "Conversation", "Contact"]) {
10
+ const rows = await ctx.db.query(entity, { demo: true });
11
+ for (const row of rows) {
12
+ await ctx.db.delete(entity, String(row.id));
13
+ removed += 1;
14
+ }
15
+ }
16
+ return { removed };
17
+ },
18
+ });
@@ -0,0 +1,11 @@
1
+ import { mutation, v } from "@pylonsync/functions";
2
+
3
+ // Remove a saved fact from the dashboard. Owner only.
4
+ export default mutation({
5
+ auth: "admin",
6
+ args: { noteId: v.string() },
7
+ async handler(ctx, args) {
8
+ const deleted = await ctx.db.delete("Note", args.noteId);
9
+ return { deleted };
10
+ },
11
+ });
@@ -0,0 +1,55 @@
1
+ import { action, v } from "@pylonsync/functions";
2
+ import { sendblueConfig, sendblueTransport } from "../lib/sendblue";
3
+ import { isDryRun } from "../lib/transport";
4
+
5
+ interface Claimed {
6
+ leaseToken: string | null;
7
+ messages: { id: string; handle: string; text: string }[];
8
+ }
9
+
10
+ // Send a conversation's queued Sendblue messages, oldest first, one at a time
11
+ // so bubbles arrive in order. claimOutbound leases the batch in a transaction
12
+ // under a token only this job holds, so two concurrent jobs for the same
13
+ // conversation never send a message twice.
14
+ // Scheduled by queueOutbound (lib/store.ts); internal.
15
+ export default action({
16
+ internal: true,
17
+ auth: "admin",
18
+ timeout: 120,
19
+ args: { conversationId: v.string() },
20
+ async handler(ctx, args) {
21
+ const claim = await ctx.runMutation<Claimed>("claimOutbound", { conversationId: args.conversationId });
22
+ const batch = claim.messages;
23
+ if (batch.length === 0 || !claim.leaseToken) return { sent: 0 };
24
+ let failed = false;
25
+ const transport = sendblueTransport(sendblueConfig(ctx.env, isDryRun(ctx.env)));
26
+ let sent = 0;
27
+ for (const message of batch) {
28
+ const result = await transport.send({ messageId: message.id, to: message.handle, text: message.text });
29
+ const settled = await ctx.runMutation<{ ok: boolean }>("settleOutbound", {
30
+ messageId: message.id,
31
+ leaseToken: claim.leaseToken,
32
+ status: result.status === "sent" || result.status === "queued" ? "sent" : result.status,
33
+ externalId: result.status === "sent" ? result.externalId : null,
34
+ error: result.status === "failed" ? result.error : null,
35
+ });
36
+ // The lease was lost (expired and taken over): stop, the new holder owns the rest.
37
+ if (!settled.ok) break;
38
+ if (result.status === "failed") {
39
+ failed = true;
40
+ // Release the rest so a later attempt keeps the order intact.
41
+ await ctx.runMutation("releaseOutbound", {
42
+ leaseToken: claim.leaseToken,
43
+ messageIds: batch.slice(batch.indexOf(message) + 1).map((m) => m.id),
44
+ });
45
+ break;
46
+ }
47
+ sent += 1;
48
+ }
49
+ // More may be waiting behind this batch.
50
+ if (!failed && batch.length === 5) {
51
+ await ctx.scheduler.runAfter(0, "deliverOutbound", { conversationId: args.conversationId });
52
+ }
53
+ return { sent };
54
+ },
55
+ });
@@ -0,0 +1,84 @@
1
+ import { mutation, v } from "@pylonsync/functions";
2
+ import { queueOutbound, toContact, toConversation } from "../lib/store";
3
+
4
+ // Close a turn claimed by beginTurn: settle its inbound texts, store the
5
+ // reply bubbles and queue them for delivery, record the agent run, and
6
+ // schedule another turn when texts arrived meanwhile. Internal: processTurn only.
7
+ export default mutation({
8
+ internal: true,
9
+ auth: "admin",
10
+ args: {
11
+ conversationId: v.string(),
12
+ turnId: v.string(),
13
+ outcome: v.union(v.literal("answered"), v.literal("awaiting_setup"), v.literal("failed")),
14
+ detail: v.optional(v.string()),
15
+ runId: v.optional(v.string()),
16
+ chunks: v.optional(v.array(v.string())),
17
+ },
18
+ async handler(ctx, args) {
19
+ const row = await ctx.db.get("Conversation", args.conversationId);
20
+ if (!row) return { ok: false };
21
+ const conversation = toConversation(row);
22
+ // A sweep may have taken the turn over; a stale finisher must not clobber it.
23
+ if (conversation.turnId !== args.turnId) return { ok: false };
24
+
25
+ const turnMessages = await ctx.db.query("Message", {
26
+ conversationId: conversation.id,
27
+ turnId: args.turnId,
28
+ });
29
+ const now = new Date().toISOString();
30
+ for (const m of turnMessages) {
31
+ if (m.direction !== "in" || m.status !== "processing") continue;
32
+ await ctx.db.update("Message", String(m.id), {
33
+ status: args.outcome,
34
+ detail: args.outcome === "answered" ? null : (args.detail ?? null),
35
+ agentRunId: args.runId ?? null,
36
+ });
37
+ }
38
+
39
+ const contactRow = await ctx.db.get("Contact", conversation.contactId);
40
+ const contact = contactRow ? toContact(contactRow) : null;
41
+ // Re-check at send time: the owner may have blocked the contact, or the
42
+ // contact may have texted STOP, while the model was thinking.
43
+ const maySend = contact !== null && contact.status === "allowed" && !contact.optedOut;
44
+ if (args.outcome === "answered" && maySend) {
45
+ for (const text of args.chunks ?? []) {
46
+ await queueOutbound(ctx, {
47
+ conversationId: conversation.id,
48
+ contactId: conversation.contactId,
49
+ handle: conversation.handle,
50
+ text,
51
+ kind: "agent",
52
+ turnId: args.turnId,
53
+ agentRunId: args.runId ?? null,
54
+ });
55
+ }
56
+ }
57
+
58
+ const patch: Record<string, unknown> = {
59
+ turnState: "idle",
60
+ turnId: null,
61
+ turnStartedAt: null,
62
+ needsAnotherTurn: false,
63
+ };
64
+ if (args.runId) patch.agentRunId = args.runId;
65
+ if (args.outcome === "answered") {
66
+ patch.lastError = null;
67
+ patch.lastErrorAt = null;
68
+ } else {
69
+ patch.lastError = args.detail ?? args.outcome;
70
+ patch.lastErrorAt = now;
71
+ }
72
+ await ctx.db.update("Conversation", conversation.id, patch);
73
+
74
+ const waiting = await ctx.db.query("Message", {
75
+ conversationId: conversation.id,
76
+ status: "received",
77
+ $limit: 1,
78
+ });
79
+ if (conversation.needsAnotherTurn || waiting.length > 0) {
80
+ await ctx.scheduler.runAfter(0, "processTurn", { conversationId: conversation.id });
81
+ }
82
+ return { ok: true };
83
+ },
84
+ });
@@ -0,0 +1,42 @@
1
+ import { mutation, v } from "@pylonsync/functions";
2
+ import { loadSettings, queueOutbound, toContact } from "../lib/store";
3
+
4
+ // Send a due reminder. Scheduled with ctx.scheduler.runAt by toolSetReminder;
5
+ // internal. A cancelled or already-sent reminder is a no-op, so a job retry
6
+ // never texts twice.
7
+ export default mutation({
8
+ internal: true,
9
+ auth: "admin",
10
+ args: { reminderId: v.string() },
11
+ async handler(ctx, args) {
12
+ const reminder = await ctx.db.get("Reminder", args.reminderId);
13
+ if (!reminder || reminder.status !== "scheduled") return { sent: false };
14
+ const contactRow = await ctx.db.get("Contact", String(reminder.contactId));
15
+ const conversation = await ctx.db.get("Conversation", String(reminder.conversationId));
16
+ const contact = contactRow ? toContact(contactRow) : null;
17
+ if ((await loadSettings(ctx)).paused) {
18
+ await ctx.db.update("Reminder", args.reminderId, { status: "cancelled", detail: "The assistant was paused" });
19
+ return { sent: false };
20
+ }
21
+ if (!contact || !conversation || contact.status !== "allowed" || contact.optedOut) {
22
+ await ctx.db.update("Reminder", args.reminderId, {
23
+ status: "cancelled",
24
+ detail: "The contact is no longer on the allowlist",
25
+ });
26
+ return { sent: false };
27
+ }
28
+ const messageId = await queueOutbound(ctx, {
29
+ conversationId: String(conversation.id),
30
+ contactId: contact.id,
31
+ handle: contact.handle,
32
+ text: String(reminder.text),
33
+ kind: "reminder",
34
+ });
35
+ await ctx.db.update("Reminder", args.reminderId, {
36
+ status: "sent",
37
+ sentAt: new Date().toISOString(),
38
+ detail: null,
39
+ });
40
+ return { sent: true, messageId };
41
+ },
42
+ });
@@ -0,0 +1,22 @@
1
+ import { mutation, v } from "@pylonsync/functions";
2
+ import { ingestInbound } from "../lib/store";
3
+
4
+ // Store one verified inbound message and act on it (lib/store.ts ingestInbound).
5
+ // Internal: reachable only from sendblueWebhook after its secret check.
6
+ export default mutation({
7
+ internal: true,
8
+ auth: "admin",
9
+ args: {
10
+ transport: v.union(v.literal("sendblue"), v.literal("relay")),
11
+ message: v.object({
12
+ externalId: v.string(),
13
+ handle: v.string(),
14
+ text: v.string(),
15
+ sentAt: v.string(),
16
+ service: v.string(),
17
+ }),
18
+ },
19
+ async handler(ctx, args) {
20
+ return ingestInbound(ctx, args.message, args.transport);
21
+ },
22
+ });
@@ -0,0 +1,13 @@
1
+ import { query } from "@pylonsync/functions";
2
+ import { findOwner } from "../lib/store";
3
+
4
+ // The owner's user id (lib/store.ts findOwner). Internal: the agent runs as
5
+ // this account, so the id never leaves the server.
6
+ export default query({
7
+ internal: true,
8
+ auth: "admin",
9
+ args: {},
10
+ async handler(ctx) {
11
+ return findOwner(ctx);
12
+ },
13
+ });
@@ -0,0 +1,74 @@
1
+ import { action, v } from "@pylonsync/functions";
2
+ import { runAgentAsOwner } from "../lib/agent-call";
3
+ import { chunkReply } from "../lib/chunk";
4
+ import { providerStatus } from "../lib/provider";
5
+
6
+ type Begin =
7
+ | { state: "idle" | "busy" }
8
+ | { state: "started"; turnId: string; conversationId: string; runId: string | null; input: string };
9
+
10
+ // One agent turn for a conversation: claim the waiting texts, run the
11
+ // `assistant` agent as the owner, split the reply into bubbles, and hand them
12
+ // to finishTurn for delivery. Scheduled by ingestInbound; internal.
13
+ //
14
+ // Every failure is recorded on the texts and the conversation rather than
15
+ // thrown, so the job queue does not retry a turn that already reached the
16
+ // model.
17
+ export default action({
18
+ internal: true,
19
+ auth: "admin",
20
+ timeout: 300,
21
+ args: { conversationId: v.string() },
22
+ async handler(ctx, args) {
23
+ const begin = await ctx.runMutation<Begin>("beginTurn", { conversationId: args.conversationId });
24
+ if (begin.state !== "started") return { state: begin.state };
25
+
26
+ const finish = (fields: Record<string, unknown>) =>
27
+ ctx.runMutation("finishTurn", {
28
+ conversationId: begin.conversationId,
29
+ turnId: begin.turnId,
30
+ ...fields,
31
+ });
32
+
33
+ try {
34
+ if (!providerStatus(ctx.env).configured) {
35
+ await finish({
36
+ outcome: "awaiting_setup",
37
+ detail: "No model provider key is set. Add ANTHROPIC_API_KEY or OPENAI_API_KEY, then answer from the contact details.",
38
+ });
39
+ return { state: "awaiting_setup" };
40
+ }
41
+ const owner = await ctx.runQuery<{ userId: string } | null>("ownerAccount", {});
42
+ if (!owner) {
43
+ await finish({
44
+ outcome: "awaiting_setup",
45
+ detail: "No owner account yet. Sign in at /login with the email in PYLON_ADMIN_EMAILS.",
46
+ });
47
+ return { state: "awaiting_setup" };
48
+ }
49
+
50
+ const outcome = await runAgentAsOwner(ctx.env, owner.userId, {
51
+ input: begin.input,
52
+ ...(begin.runId ? { runId: begin.runId } : {}),
53
+ // The run's title carries the conversation id. The agent's tools read
54
+ // it back (functions/resolveTurn.ts) to know which contact they act on.
55
+ title: begin.conversationId,
56
+ });
57
+ if (!outcome.ok) {
58
+ await finish({ outcome: "failed", detail: `${outcome.code}: ${outcome.message}`.slice(0, 500) });
59
+ return { state: "failed" };
60
+ }
61
+ const chunks = chunkReply(outcome.result.text);
62
+ if (chunks.length === 0) {
63
+ await finish({ outcome: "failed", detail: "The model returned an empty reply", runId: outcome.result.runId });
64
+ return { state: "failed" };
65
+ }
66
+ await finish({ outcome: "answered", runId: outcome.result.runId, chunks });
67
+ return { state: "answered", bubbles: chunks.length };
68
+ } catch (err) {
69
+ const message = err instanceof Error ? err.message : String(err);
70
+ await finish({ outcome: "failed", detail: message.slice(0, 500) });
71
+ return { state: "failed" };
72
+ }
73
+ },
74
+ });
@@ -0,0 +1,102 @@
1
+ import { mutation, v } from "@pylonsync/functions";
2
+ import { MAX_OUTBOUND_PER_SYNC, MAX_RELAY_ATTEMPTS, OUTBOUND_LEASE_MS } from "../lib/relay-protocol";
3
+ import { ingestInbound, touchTransport } from "../lib/store";
4
+
5
+ // One relay sync, in one transaction: record the heartbeat, settle the acks
6
+ // for replies the relay sent, store new inbound messages, and lease the next
7
+ // batch of queued replies. Internal: reachable only from relaySync after its
8
+ // token check.
9
+ export default mutation({
10
+ internal: true,
11
+ auth: "admin",
12
+ args: {
13
+ relayVersion: v.string(),
14
+ host: v.string(),
15
+ messages: v.array(
16
+ v.object({
17
+ externalId: v.string(),
18
+ handle: v.string(),
19
+ text: v.string(),
20
+ sentAt: v.string(),
21
+ service: v.string(),
22
+ }),
23
+ ),
24
+ skipped: v.array(v.string()),
25
+ acks: v.array(
26
+ v.object({ id: v.string(), ok: v.boolean(), error: v.optional(v.string()), dryRun: v.optional(v.boolean()) }),
27
+ ),
28
+ },
29
+ async handler(ctx, args) {
30
+ const now = new Date();
31
+ const nowIso = now.toISOString();
32
+ const statePatch: Record<string, unknown> = {
33
+ relayLastSeenAt: nowIso,
34
+ relayVersion: args.relayVersion,
35
+ relayHost: args.host,
36
+ };
37
+
38
+ for (const ack of args.acks) {
39
+ const row = await ctx.db.get("Message", ack.id);
40
+ // Only a leased relay reply can be settled; anything else is stale.
41
+ if (!row || row.direction !== "out" || row.transport !== "relay" || row.status !== "leased") continue;
42
+ if (ack.ok && ack.dryRun) {
43
+ await ctx.db.update("Message", ack.id, {
44
+ status: "dry_run",
45
+ sentAt: nowIso,
46
+ leaseUntil: null,
47
+ detail: "RELAY_DRY_RUN is on; the relay did not send it.",
48
+ });
49
+ } else if (ack.ok) {
50
+ await ctx.db.update("Message", ack.id, { status: "sent", sentAt: nowIso, leaseUntil: null, detail: null });
51
+ statePatch.lastOutboundAt = nowIso;
52
+ } else {
53
+ const error = ack.error || "The relay could not send this message";
54
+ await ctx.db.update("Message", ack.id, { status: "failed", detail: error, leaseUntil: null });
55
+ statePatch.lastError = error;
56
+ statePatch.lastErrorAt = nowIso;
57
+ }
58
+ }
59
+
60
+ const accepted: string[] = [...args.skipped];
61
+ for (const message of args.messages) {
62
+ await ingestInbound(ctx, message, "relay");
63
+ // "dropped" is accepted too: the server decided not to store it, and
64
+ // resending the same row would get the same answer.
65
+ accepted.push(message.externalId.replace(/^relay:/, ""));
66
+ }
67
+
68
+ // Queued replies plus leases the relay let expire, oldest first.
69
+ const queued = await ctx.db.query("Message", {
70
+ status: { $in: ["queued", "leased"] },
71
+ $order: { createdAt: "asc" },
72
+ $limit: 200,
73
+ });
74
+ const outbound: { id: string; handle: string; text: string }[] = [];
75
+ const leaseUntil = new Date(now.getTime() + OUTBOUND_LEASE_MS).toISOString();
76
+ for (const row of queued) {
77
+ if (outbound.length >= MAX_OUTBOUND_PER_SYNC) break;
78
+ if (row.transport !== "relay" || row.direction !== "out") continue;
79
+ if (row.status === "leased" && String(row.leaseUntil ?? "") > nowIso) continue;
80
+ const attempts = Number(row.attempts) || 0;
81
+ if (attempts >= MAX_RELAY_ATTEMPTS) {
82
+ await ctx.db.update("Message", String(row.id), {
83
+ status: "failed",
84
+ leaseUntil: null,
85
+ detail: `The relay did not confirm delivery after ${attempts} attempts`,
86
+ });
87
+ continue;
88
+ }
89
+ const conversation = await ctx.db.get("Conversation", String(row.conversationId));
90
+ if (!conversation) continue;
91
+ await ctx.db.update("Message", String(row.id), {
92
+ status: "leased",
93
+ leaseUntil,
94
+ attempts: attempts + 1,
95
+ });
96
+ outbound.push({ id: String(row.id), handle: String(conversation.handle), text: String(row.text) });
97
+ }
98
+
99
+ await touchTransport(ctx, "relay", statePatch);
100
+ return { accepted, outbound };
101
+ },
102
+ });
@@ -0,0 +1,63 @@
1
+ import { action, v } from "@pylonsync/functions";
2
+ import {
3
+ MAX_INBOUND_PER_SYNC,
4
+ parseRelayInbound,
5
+ type RelaySyncResponse,
6
+ } from "../lib/relay-protocol";
7
+ import { bearerToken, checkSecret, headerValue } from "../lib/secrets";
8
+ import { selectedTransport, type InboundMessage } from "../lib/transport";
9
+
10
+ // The Mac relay's single endpoint (see lib/relay-protocol.ts for the contract).
11
+ // Public by necessity; the handler rejects the call unless the bearer token
12
+ // matches RELAY_TOKEN (constant-time), and only then elevates to write.
13
+ export default action({
14
+ auth: "public",
15
+ args: {
16
+ relayVersion: v.string(),
17
+ host: v.string(),
18
+ inbound: v.array(v.any()),
19
+ acks: v.array(
20
+ v.object({
21
+ id: v.string(),
22
+ ok: v.boolean(),
23
+ error: v.optional(v.string()),
24
+ dryRun: v.optional(v.boolean()),
25
+ }),
26
+ ),
27
+ },
28
+ async handler(ctx, args): Promise<RelaySyncResponse> {
29
+ if (selectedTransport(ctx.env) !== "relay") {
30
+ throw ctx.error("TRANSPORT_DISABLED", "The relay transport is not enabled on this server");
31
+ }
32
+ const presented = bearerToken(headerValue(ctx.request?.headers, "authorization"));
33
+ const check = checkSecret(presented, ctx.env.RELAY_TOKEN);
34
+ if (!check.ok) {
35
+ if (check.reason === "not_configured") throw ctx.error("NOT_CONFIGURED", "RELAY_TOKEN is not set");
36
+ throw ctx.error("UNAUTHORIZED", "Relay token missing or invalid");
37
+ }
38
+ if (args.inbound.length > MAX_INBOUND_PER_SYNC || args.acks.length > 100) {
39
+ throw ctx.error("TOO_LARGE", `Send at most ${MAX_INBOUND_PER_SYNC} messages per sync`);
40
+ }
41
+
42
+ const messages: InboundMessage[] = [];
43
+ // Items the server deliberately skips (group chats, attachments without
44
+ // text) still count as accepted, so the relay's watermark moves past them.
45
+ const skipped: string[] = [];
46
+ for (const item of args.inbound) {
47
+ const parsed = parseRelayInbound(item);
48
+ if ("message" in parsed) messages.push(parsed.message);
49
+ else if (typeof (item as { guid?: unknown })?.guid === "string") {
50
+ skipped.push(String((item as { guid: string }).guid).slice(0, 128));
51
+ }
52
+ }
53
+
54
+ await ctx.auth.elevate({ admin: true, reason: "relay token verified" });
55
+ return ctx.runMutation<RelaySyncResponse>("relayApply", {
56
+ relayVersion: args.relayVersion.slice(0, 32),
57
+ host: args.host.slice(0, 64),
58
+ messages,
59
+ skipped,
60
+ acks: args.acks.map((a) => ({ id: a.id, ok: a.ok, error: a.error?.slice(0, 300), dryRun: a.dryRun === true })),
61
+ });
62
+ },
63
+ });
@@ -0,0 +1,18 @@
1
+ import { mutation, v } from "@pylonsync/functions";
2
+
3
+ // Return leased Sendblue messages to the queue after an earlier message in the
4
+ // same batch failed. Internal: deliverOutbound only.
5
+ export default mutation({
6
+ internal: true,
7
+ auth: "admin",
8
+ args: { leaseToken: v.string(), messageIds: v.array(v.string()) },
9
+ async handler(ctx, args) {
10
+ for (const id of args.messageIds) {
11
+ const row = await ctx.db.get("Message", id);
12
+ if (row?.status === "leased" && row.leaseToken === args.leaseToken) {
13
+ await ctx.db.update("Message", id, { status: "queued", leaseUntil: null, leaseToken: null });
14
+ }
15
+ }
16
+ return { released: args.messageIds.length };
17
+ },
18
+ });