@pylonsync/create-pylon 0.19.0 → 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,187 @@
1
+ # __APP_NAME__
2
+
3
+ An AI assistant people text over iMessage, built on Pylon. You choose who it
4
+ answers; everything it says and does shows up live in your dashboard.
5
+
6
+ ```bash
7
+ cp .env.example .env # set PYLON_ADMIN_EMAILS, a model key, and a transport
8
+ bun install
9
+ bun run dev # http://localhost:4321
10
+ ```
11
+
12
+ Sign in at `/login` with the email in `PYLON_ADMIN_EMAILS`. In `pylon dev` the
13
+ six-digit code appears on the page; in production it is emailed.
14
+
15
+ `pylon dev` never sends texts: replies are stored and marked "Dry run". A dev
16
+ server listens on your local network and returns sign-in codes in its
17
+ responses, so anyone on the network who knows your email could sign in as you.
18
+ Set `IMESSAGE_ALLOW_DEV_SENDS=1` only on a network you trust. The dashboard
19
+ opens with three sample threads (marked "Sample"); remove them from Setup once
20
+ real texts arrive.
21
+
22
+ ## How it works
23
+
24
+ ```
25
+ iPhone ──iMessage──▶ Sendblue ──webhook──────────▶ sendblueWebhook ─┐
26
+ your Mac (relay/) ──relaySync─▶ relaySync ──────┤
27
+ ▼
28
+ ingestInbound: dedupe → allowlist → rate limit → store
29
+ │
30
+ processTurn (job) ◀─────┘
31
+ │ runs agent() as the owner
32
+ ▼
33
+ assistant: history + tools (notes, reminders)
34
+ │
35
+ reply → split into bubbles → queued → transport → iPhone
36
+ ```
37
+
38
+ - **Transports** (`lib/transport.ts`). One `MessageTransport` interface, two
39
+ adapters. `IMESSAGE_TRANSPORT=sendblue` uses Sendblue's API
40
+ (`lib/sendblue.ts`). `IMESSAGE_TRANSPORT=relay` uses the Bun script in
41
+ `relay/` on a Mac you own.
42
+ - **Allowlist** (`lib/gate.ts`). Only contacts marked Allowed get answers.
43
+ Anyone else is stored under Requests and ignored, or gets one fixed reply if
44
+ you turn that on. Blocked contacts are never answered. STOP and START work
45
+ for everyone.
46
+ - **Rate limit.** Each contact gets at most N answered texts per rolling hour
47
+ (Setup, default 30). A number that is not on the allowlist can store at most
48
+ 20 texts per hour, and at most 50 new numbers are stored per hour; the rest
49
+ are dropped. STOP and START confirmations go out at most once a day each.
50
+ The assistant can create at most 10 reminders per contact per day, each at
51
+ least five minutes out, and none fire while the assistant is paused.
52
+ - **The agent** (`functions/assistant.ts`) is Pylon's `agent()`: the tool loop,
53
+ the transcript in the synced `AgentRun`/`AgentMessage` entities, and token
54
+ streaming come from the framework. One run per conversation holds its
55
+ history; after 80 turns a new run starts with the last few texts as context.
56
+ - **Tools.** `remember_note`, `recall_notes`, `forget_note`, `set_reminder`,
57
+ `list_reminders`, `cancel_reminder`. They act only on the person being
58
+ answered: the contact comes from the server (`functions/resolveTurn.ts`),
59
+ never from the model. Reminders run on Pylon's job scheduler
60
+ (`ctx.scheduler.runAt` → `functions/fireReminder.ts`).
61
+ - **Replies** are converted from Markdown to plain text and split into up to
62
+ four bubbles along paragraph breaks (`lib/chunk.ts`).
63
+ - **Dashboard.** Owner-only. Every entity policy is `auth.isAdmin`, and the
64
+ owner is the account whose verified email is in `PYLON_ADMIN_EMAILS`.
65
+ Anonymous visitors and other accounts read nothing.
66
+
67
+ ### Why the server calls itself
68
+
69
+ `agent()` stores each run under the signed-in user who called it. A webhook
70
+ has no user, so `processTurn` opens a session for the owner
71
+ (`POST /api/auth/session`) and calls `/api/fn/assistant` over HTTP
72
+ (`lib/agent-call.ts`). The session token stays in server memory. In
73
+ `pylon dev` this needs nothing. In production set `PYLON_ADMIN_TOKEN` (only the
74
+ server uses it, to open that session). The call goes to `127.0.0.1:$PYLON_PORT`
75
+ when the port is known, otherwise to `APP_URL`, which must be https.
76
+
77
+ ## Transport 1: Sendblue
78
+
79
+ No Mac needed. Sendblue hosts the iMessage line.
80
+
81
+ 1. Create an account and a number at [dashboard.sendblue.com](https://dashboard.sendblue.com).
82
+ 2. Set in `.env` (or `pylon secrets set` on Stack0 Cloud):
83
+ ```
84
+ IMESSAGE_TRANSPORT=sendblue
85
+ SENDBLUE_API_KEY=...
86
+ SENDBLUE_API_SECRET=...
87
+ SENDBLUE_FROM_NUMBER=+15125550100
88
+ SENDBLUE_WEBHOOK_SECRET=<openssl rand -hex 32>
89
+ ```
90
+ 3. In Sendblue, set the receive webhook to
91
+ `https://<your app>/api/webhooks/sendblueWebhook` with the same secret.
92
+ Sendblue sends it back in the `sb-signing-secret` header on every delivery;
93
+ the webhook compares it in constant time and rejects anything else. Because
94
+ that secret is not bound to the body, Sendblue's `message_handle` is also
95
+ stored as a unique key, so a replayed or retried delivery is stored once.
96
+ 4. Add your own number on the Allowlist page and text the Sendblue number.
97
+
98
+ ## Transport 2: Mac relay
99
+
100
+ Uses your Mac and Apple ID. The relay reads new messages from Messages'
101
+ database and sends replies through Messages.app.
102
+
103
+ 1. On the server: `IMESSAGE_TRANSPORT=relay` and `RELAY_TOKEN=<openssl rand -hex 32>`.
104
+ 2. On a Mac that stays on and is signed in to Messages, copy this project and
105
+ run `bun install`. Create `relay/.env` from `relay/.env.example`:
106
+ ```
107
+ PYLON_URL=https://<your app>
108
+ RELAY_TOKEN=<same value as the server>
109
+ ```
110
+ 3. **Full Disk Access.** System Settings → Privacy & Security → Full Disk
111
+ Access → add the app that runs the relay (Terminal, iTerm, or the `bun`
112
+ binary if you run it under launchd). The relay opens
113
+ `~/Library/Messages/chat.db` read-only and never writes to it.
114
+ 4. **Automation.** Start it with `bun run relay`. The first reply makes macOS
115
+ ask whether it may control Messages; allow it. You can change this later
116
+ under Privacy & Security → Automation.
117
+ 5. Keep it running. With launchd, a `~/Library/LaunchAgents` plist that runs
118
+ `bun relay/main.ts` from the project directory with `KeepAlive` works well.
119
+
120
+ `RELAY_TOKEN` is as sensitive as your Apple ID: whoever holds it can post
121
+ texts as any sender and read every reply waiting to go out. Keep it only in
122
+ the server environment and in `relay/.env` on the Mac.
123
+
124
+ The relay forwards every one-to-one text your Apple ID receives, including
125
+ texts from numbers not on the allowlist (for example sign-in codes from
126
+ services that text from a full phone number). They are stored under Requests
127
+ and never answered. Short-code senders are dropped.
128
+
129
+ How the relay works (`relay/main.ts`):
130
+
131
+ - It starts at the newest message, so your existing history is never sent to
132
+ the server. Its position (a chat.db ROWID) is saved in
133
+ `~/.pylon-imessage-relay/state.json` and only advances past messages the
134
+ server accepted.
135
+ - Every `POLL_MS` (default 3s, minimum 2.5s) it makes one call to `/api/fn/relaySync` with
136
+ `Authorization: Bearer <RELAY_TOKEN>`: new messages in, acks for sent replies,
137
+ and the next replies to send back. The server compares the token in constant
138
+ time.
139
+ - Group chats, your own messages, and reactions are skipped. Messages that
140
+ macOS stores only in `attributedBody` (common since macOS 13) are decoded.
141
+ - Replies are sent with `osascript`. The message text and the recipient are
142
+ passed as arguments to a fixed AppleScript (`on run argv`), never inserted
143
+ into script source, so a message cannot change what the script does.
144
+ - A reply is recorded as sent on disk before it is acknowledged, so a crash
145
+ does not send it twice. Unacknowledged replies return to the queue after 60
146
+ seconds and are marked failed after 5 attempts.
147
+
148
+ ## Environment
149
+
150
+ | Variable | Needed for |
151
+ |---|---|
152
+ | `PYLON_ADMIN_EMAILS` | The owner's email. Required. |
153
+ | `ANTHROPIC_API_KEY` (or `PYLON_LLM_PROVIDER=openai` + `OPENAI_API_KEY`) | Replies. Without it texts are stored and marked "waiting for setup". The model id is `ASSISTANT_MODEL` in `lib/assistant.ts`. |
154
+ | `IMESSAGE_TRANSPORT` | `sendblue` or `relay`. |
155
+ | `SENDBLUE_*` | The Sendblue transport. |
156
+ | `RELAY_TOKEN` | The relay transport. At least 24 characters. |
157
+ | `IMESSAGE_DRY_RUN=1` | Store replies without sending them (both transports). Always on in `pylon dev` unless `IMESSAGE_ALLOW_DEV_SENDS=1`. |
158
+ | `PYLON_ADMIN_TOKEN`, `APP_URL` | Production: how the server reaches its own API to run the agent. `APP_URL` must be https and is used only when `PYLON_PORT` is not set. |
159
+
160
+ The Setup page shows which of these are set (never their values), when the
161
+ last text came in, and when the relay last checked in.
162
+
163
+ ## Tests
164
+
165
+ ```bash
166
+ bun test tests
167
+ ```
168
+
169
+ Covers webhook verification (valid, wrong, missing, unconfigured secret),
170
+ idempotency, the allowlist and rate limit, STOP/START, relay token auth,
171
+ AppleScript argument passing (including a real `osascript` echo run on macOS
172
+ with quotes, backslashes, newlines, and unicode), the chat.db reader against a
173
+ fixture database built in a temp directory, the agent call over SSE, and
174
+ reply chunking. Nothing in the suite sends a message or calls Sendblue.
175
+
176
+ ## Deploy
177
+
178
+ ```bash
179
+ pylon deploy
180
+ pylon secrets set PYLON_ADMIN_EMAILS=you@example.com ANTHROPIC_API_KEY=... \
181
+ IMESSAGE_TRANSPORT=sendblue SENDBLUE_API_KEY=... SENDBLUE_API_SECRET=... \
182
+ SENDBLUE_FROM_NUMBER=+1... SENDBLUE_WEBHOOK_SECRET=... \
183
+ APP_URL=https://<your app> PYLON_ADMIN_TOKEN=<openssl rand -hex 32>
184
+ ```
185
+
186
+ Sign-in codes in production need an email provider (`PYLON_EMAIL_PROVIDER`);
187
+ Stack0 Cloud has one configured.
@@ -0,0 +1,382 @@
1
+ "use client";
2
+
3
+ import React, { useEffect, useMemo, useRef, useState } from "react";
4
+ import { callFn, db, useRouter } from "@pylonsync/react";
5
+ import { useAuth } from "@pylonsync/client";
6
+ import { ContactPanel, type PanelActivity } from "@/components/contact-panel";
7
+ import { ContactsView, type ContactRowView } from "@/components/contacts-view";
8
+ import { ConversationList, type ConversationSummary } from "@/components/conversation-list";
9
+ import { NavRail, type View } from "@/components/nav-rail";
10
+ import { setupIssueCount, SetupView, type SetupStatus, type SettingsView, type TransportStateView } from "@/components/setup-view";
11
+ import { Thread, type Notice } from "@/components/thread";
12
+ import { describeToolCall, toolRefusal, type ThreadMessage } from "@/lib/format";
13
+ import { DEFAULT_SETTINGS } from "@/lib/gate";
14
+ import { cn } from "@/lib/utils";
15
+
16
+ interface ContactRow {
17
+ id: string;
18
+ handle: string;
19
+ displayName?: string | null;
20
+ status: string;
21
+ optedOut?: boolean | null;
22
+ lastMessageAt?: string | null;
23
+ demo?: boolean | null;
24
+ }
25
+ interface ConversationRow {
26
+ id: string;
27
+ contactId: string;
28
+ handle: string;
29
+ lastMessageAt?: string | null;
30
+ lastPreview?: string | null;
31
+ agentRunId?: string | null;
32
+ turnState?: string | null;
33
+ lastError?: string | null;
34
+ demo?: boolean | null;
35
+ }
36
+ interface MessageRow extends ThreadMessage {
37
+ conversationId: string;
38
+ }
39
+ interface NoteRow {
40
+ id: string;
41
+ contactId: string;
42
+ text: string;
43
+ createdAt: string;
44
+ }
45
+ interface ReminderRow {
46
+ id: string;
47
+ contactId: string;
48
+ text: string;
49
+ dueAt: string;
50
+ status: string;
51
+ }
52
+ interface AgentRunRow {
53
+ id: string;
54
+ status: string;
55
+ steps?: number | null;
56
+ error?: string | null;
57
+ updatedAt: string;
58
+ title?: string | null;
59
+ }
60
+ interface AgentMessageRow {
61
+ id: string;
62
+ runId: string;
63
+ role: string;
64
+ content: unknown;
65
+ createdAt: string;
66
+ }
67
+ interface SettingsRow extends Partial<SettingsView> {
68
+ id: string;
69
+ key: string;
70
+ }
71
+
72
+ const UNANSWERED = new Set(["awaiting_setup", "failed", "rate_limited"]);
73
+
74
+ function errorMessage(err: unknown): string {
75
+ if (err && typeof err === "object" && "message" in err) return String((err as { message: unknown }).message);
76
+ return "Something went wrong";
77
+ }
78
+
79
+ /**
80
+ * The dashboard, and the only module that touches `db` / `callFn`. Every
81
+ * `db.useQuery` below is a live subscription: a text that arrives through the
82
+ * webhook or the relay shows up in the list and the open thread without a
83
+ * reload, and so does the assistant's reply.
84
+ */
85
+ export function Console({ view, initialConversationId }: { view: View; initialConversationId: string | null }) {
86
+ const router = useRouter();
87
+ const { signOut } = useAuth();
88
+ const { data: contacts } = db.useQuery<ContactRow>("Contact");
89
+ const { data: conversations, loading } = db.useQuery<ConversationRow>("Conversation");
90
+ const { data: messages } = db.useQuery<MessageRow>("Message");
91
+ const { data: notes } = db.useQuery<NoteRow>("Note");
92
+ const { data: reminders } = db.useQuery<ReminderRow>("Reminder");
93
+ const { data: runs } = db.useQuery<AgentRunRow>("AgentRun");
94
+ const { data: agentMessages } = db.useQuery<AgentMessageRow>("AgentMessage");
95
+ const { data: settingsRows } = db.useQuery<SettingsRow>("Settings");
96
+ const { data: transportRows } = db.useQuery<TransportStateView & { id: string }>("TransportState");
97
+
98
+ const [status, setStatus] = useState<SetupStatus | null>(null);
99
+ const [selectedId, setSelectedId] = useState<string | null>(initialConversationId);
100
+ const [query, setQuery] = useState("");
101
+ const [detailsOpen, setDetailsOpen] = useState(false);
102
+ const [toast, setToast] = useState<string | null>(null);
103
+ const seeded = useRef(false);
104
+
105
+ useEffect(() => {
106
+ callFn<SetupStatus>("setupStatus", {}).then(setStatus, () => setStatus(null));
107
+ }, []);
108
+
109
+ // A fresh install gets the sample threads once, so the dashboard never opens empty.
110
+ useEffect(() => {
111
+ if (loading || seeded.current) return;
112
+ seeded.current = true;
113
+ if ((conversations ?? []).length > 0) return;
114
+ callFn("seedDemo", {}).catch(() => {
115
+ seeded.current = false;
116
+ });
117
+ }, [loading, conversations]);
118
+
119
+ const contactById = useMemo(() => new Map((contacts ?? []).map((c) => [c.id, c] as const)), [contacts]);
120
+
121
+ const summaries: ConversationSummary[] = useMemo(() => {
122
+ const unansweredByConversation = new Map<string, number>();
123
+ for (const m of messages ?? []) {
124
+ if (m.direction === "in" && UNANSWERED.has(m.status)) {
125
+ unansweredByConversation.set(m.conversationId, (unansweredByConversation.get(m.conversationId) ?? 0) + 1);
126
+ }
127
+ }
128
+ const q = query.trim().toLowerCase();
129
+ return (conversations ?? [])
130
+ .map((c) => {
131
+ const contact = contactById.get(c.contactId);
132
+ const status = (contact?.status ?? "unknown") as ConversationSummary["status"];
133
+ return {
134
+ id: c.id,
135
+ name: contact?.displayName ?? null,
136
+ handle: c.handle,
137
+ preview: c.lastPreview ?? "",
138
+ lastMessageAt: c.lastMessageAt ?? null,
139
+ status,
140
+ thinking: c.turnState === "running",
141
+ needsAttention: status === "allowed" && (unansweredByConversation.get(c.id) ?? 0) > 0,
142
+ demo: Boolean(c.demo),
143
+ };
144
+ })
145
+ .filter((c) => !q || (c.name ?? "").toLowerCase().includes(q) || c.handle.includes(q) || c.preview.toLowerCase().includes(q))
146
+ .sort((a, b) => ((a.lastMessageAt ?? "") < (b.lastMessageAt ?? "") ? 1 : -1));
147
+ }, [conversations, contactById, messages, query]);
148
+
149
+ // Desktop opens the newest conversation; phones start on the list.
150
+ useEffect(() => {
151
+ if (view !== "messages" || selectedId || summaries.length === 0) return;
152
+ if (typeof window !== "undefined" && window.matchMedia("(min-width: 768px)").matches) {
153
+ setSelectedId(summaries[0].id);
154
+ }
155
+ }, [view, selectedId, summaries]);
156
+
157
+ function select(id: string | null) {
158
+ setSelectedId(id);
159
+ setDetailsOpen(false);
160
+ // Keep the URL shareable without a navigation round trip.
161
+ window.history.replaceState(null, "", id ? `/?c=${encodeURIComponent(id)}` : "/");
162
+ }
163
+
164
+ async function run<T>(fn: string, args: Record<string, unknown>): Promise<T> {
165
+ try {
166
+ return await callFn<T>(fn, args);
167
+ } catch (err) {
168
+ const message = errorMessage(err);
169
+ setToast(message);
170
+ setTimeout(() => setToast(null), 4000);
171
+ throw new Error(message);
172
+ }
173
+ }
174
+
175
+ const settingsRow = (settingsRows ?? []).find((s) => s.key === "main");
176
+ const settings: SettingsView = {
177
+ ownerName: settingsRow?.ownerName ?? "",
178
+ timezone: settingsRow?.timezone ?? DEFAULT_SETTINGS.timezone,
179
+ unknownSenderPolicy: settingsRow?.unknownSenderPolicy ?? DEFAULT_SETTINGS.unknownSenderPolicy,
180
+ unknownSenderReply: settingsRow?.unknownSenderReply ?? DEFAULT_SETTINGS.unknownSenderReply,
181
+ rateLimitPerHour: settingsRow?.rateLimitPerHour ?? DEFAULT_SETTINGS.rateLimitPerHour,
182
+ paused: Boolean(settingsRow?.paused),
183
+ };
184
+ const transport = (transportRows ?? []).find((t) => t.name === status?.transport) ?? null;
185
+ const requests = (contacts ?? []).filter((c) => c.status === "unknown").length;
186
+
187
+ const nav = (
188
+ <NavRail
189
+ view={view}
190
+ setupIssues={setupIssueCount(status)}
191
+ requests={requests}
192
+ onSignOut={() => {
193
+ void signOut();
194
+ window.location.assign("/login");
195
+ }}
196
+ />
197
+ );
198
+
199
+ let body: React.ReactNode;
200
+ if (view === "contacts") {
201
+ const rows: ContactRowView[] = (contacts ?? [])
202
+ .map((c) => ({
203
+ id: c.id,
204
+ name: c.displayName ?? null,
205
+ handle: c.handle,
206
+ status: (c.status ?? "unknown") as ContactRowView["status"],
207
+ optedOut: Boolean(c.optedOut),
208
+ lastMessageAt: c.lastMessageAt ?? null,
209
+ demo: Boolean(c.demo),
210
+ }))
211
+ .sort((a, b) => ((a.lastMessageAt ?? "") < (b.lastMessageAt ?? "") ? 1 : -1));
212
+ body = (
213
+ <ContactsView
214
+ contacts={rows}
215
+ onAdd={async (handle, name) => {
216
+ await run("saveContact", { handle, displayName: name, status: "allowed" });
217
+ }}
218
+ onStatus={(id, s) => void run("setContactStatus", { contactId: id, status: s }).catch(() => {})}
219
+ onOpen={(contactId) => {
220
+ const conv = (conversations ?? []).find((c) => c.contactId === contactId);
221
+ router.push(conv ? `/?c=${encodeURIComponent(conv.id)}` : "/");
222
+ }}
223
+ />
224
+ );
225
+ } else if (view === "setup") {
226
+ body = (
227
+ <SetupView
228
+ status={status}
229
+ transport={transport}
230
+ settings={settings}
231
+ hasDemo={(conversations ?? []).some((c) => c.demo)}
232
+ onSaveSettings={async (patch) => {
233
+ await run("updateSettings", patch);
234
+ }}
235
+ onClearDemo={async () => {
236
+ await run("clearDemo", {});
237
+ }}
238
+ />
239
+ );
240
+ } else {
241
+ const conversation = (conversations ?? []).find((c) => c.id === selectedId) ?? null;
242
+ const contact = conversation ? contactById.get(conversation.contactId) : undefined;
243
+ const threadMessages = conversation ? (messages ?? []).filter((m) => m.conversationId === conversation.id) : [];
244
+ const unanswered = threadMessages.filter((m) => m.direction === "in" && UNANSWERED.has(m.status)).length;
245
+ const runRow = conversation ? (runs ?? []).find((r) => r.id === conversation.agentRunId || r.title === conversation.id) : undefined;
246
+ const activity: PanelActivity[] = [];
247
+ if (conversation) {
248
+ const runIds = new Set((runs ?? []).filter((r) => r.title === conversation.id).map((r) => r.id));
249
+ const refusals = new Map<string, string | null>();
250
+ for (const m of agentMessages ?? []) {
251
+ if (!runIds.has(m.runId) || m.role !== "user" || !Array.isArray(m.content)) continue;
252
+ for (const block of m.content as { type?: string; tool_use_id?: string; content?: unknown; is_error?: boolean }[]) {
253
+ if (block.type === "tool_result" && block.tool_use_id) {
254
+ refusals.set(block.tool_use_id, toolRefusal(block.content, block.is_error === true));
255
+ }
256
+ }
257
+ }
258
+ for (const m of agentMessages ?? []) {
259
+ if (!runIds.has(m.runId) || m.role !== "assistant" || !Array.isArray(m.content)) continue;
260
+ for (const block of m.content as { type?: string; id?: string; name?: string; input?: Record<string, unknown> }[]) {
261
+ if (block.type === "tool_use" && block.name) {
262
+ const refused = block.id ? refusals.get(block.id) : null;
263
+ const label = describeToolCall(block.name, block.input ?? {});
264
+ activity.push({
265
+ id: block.id ?? `${m.id}-${activity.length}`,
266
+ label: refused ? `Refused: ${label.charAt(0).toLowerCase()}${label.slice(1)}. ${refused}` : label,
267
+ at: m.createdAt,
268
+ });
269
+ }
270
+ }
271
+ }
272
+ activity.sort((a, b) => (a.at < b.at ? 1 : -1)).splice(8);
273
+ }
274
+
275
+ let notice: Notice | null = null;
276
+ if (conversation && contact) {
277
+ if (status && !status.transport) {
278
+ notice = { tone: "warn", title: "No transport", body: <>Set IMESSAGE_TRANSPORT to sendblue or relay. See Setup.</> };
279
+ } else if (status && !status.provider.configured && contact.status === "allowed") {
280
+ notice = {
281
+ tone: "warn",
282
+ title: "Replies are off",
283
+ body: <>No model key is set, so new texts wait here unanswered. Add ANTHROPIC_API_KEY to the server environment, then answer them from the contact details.</>,
284
+ };
285
+ } else if (settings.paused && contact.status === "allowed") {
286
+ notice = { tone: "muted", title: "Assistant paused", body: <>Texts are stored. Turn replies back on in Setup.</> };
287
+ } else if (conversation.lastError && unanswered > 0) {
288
+ notice = { tone: "warn", title: "Last reply failed", body: conversation.lastError };
289
+ }
290
+ }
291
+
292
+ const panel = (onClose?: () => void) =>
293
+ conversation && contact ? (
294
+ <ContactPanel
295
+ name={contact.displayName ?? null}
296
+ handle={contact.handle}
297
+ status={(contact.status ?? "unknown") as "allowed" | "unknown" | "blocked"}
298
+ optedOut={Boolean(contact.optedOut)}
299
+ unanswered={unanswered}
300
+ notes={(notes ?? []).filter((n) => n.contactId === contact.id).sort((a, b) => (a.createdAt < b.createdAt ? 1 : -1))}
301
+ reminders={(reminders ?? [])
302
+ .filter((r) => r.contactId === contact.id && r.status !== "cancelled")
303
+ .sort((a, b) => (a.dueAt < b.dueAt ? -1 : 1))}
304
+ run={runRow ? { status: runRow.status, steps: Number(runRow.steps) || 0, updatedAt: runRow.updatedAt, error: runRow.error ?? null } : null}
305
+ activity={activity}
306
+ onClose={onClose}
307
+ onStatus={(s) => void run("setContactStatus", { contactId: contact.id, status: s }).catch(() => {})}
308
+ onRename={(name) =>
309
+ void run("saveContact", { handle: contact.handle, displayName: name, status: contact.status }).catch(() => {})
310
+ }
311
+ onAnswerNow={() => void run("answerNow", { conversationId: conversation.id }).catch(() => {})}
312
+ onDeleteNote={(id) => void run("deleteNote", { noteId: id }).catch(() => {})}
313
+ onCancelReminder={(id) => void run("cancelReminder", { reminderId: id }).catch(() => {})}
314
+ />
315
+ ) : null;
316
+
317
+ body = (
318
+ <div className="flex h-full min-h-0">
319
+ <div
320
+ className={cn(
321
+ "h-full min-h-0 w-full flex-col border-r bg-rail/50 pt-3 md:flex md:w-[320px] md:shrink-0",
322
+ conversation ? "hidden" : "flex",
323
+ )}
324
+ >
325
+ <div className="flex items-baseline justify-between px-4 pb-2">
326
+ <h1 className="text-[22px] font-semibold tracking-[-0.025em]">Messages</h1>
327
+ <span className="text-[12px] text-muted-foreground">{summaries.length} threads</span>
328
+ </div>
329
+ <ConversationList conversations={summaries} selectedId={selectedId} query={query} onQuery={setQuery} onSelect={select} />
330
+ </div>
331
+ {conversation && contact ? (
332
+ <>
333
+ <div className="h-full min-h-0 min-w-0 flex-1">
334
+ <Thread
335
+ key={conversation.id}
336
+ contact={{
337
+ name: contact.displayName ?? null,
338
+ handle: contact.handle,
339
+ status: (contact.status ?? "unknown") as "allowed" | "unknown" | "blocked",
340
+ optedOut: Boolean(contact.optedOut),
341
+ }}
342
+ messages={threadMessages}
343
+ thinking={conversation.turnState === "running"}
344
+ notice={notice}
345
+ demo={Boolean(conversation.demo)}
346
+ onBack={() => select(null)}
347
+ onToggleDetails={() => setDetailsOpen((o) => !o)}
348
+ onSend={async (text) => {
349
+ await run("sendOwnerMessage", { conversationId: conversation.id, text });
350
+ }}
351
+ />
352
+ </div>
353
+ <div className="hidden h-full w-[300px] shrink-0 border-l xl:block">{panel()}</div>
354
+ {detailsOpen ? (
355
+ <div className="fixed inset-0 z-30 flex justify-end bg-black/20 xl:hidden" onClick={() => setDetailsOpen(false)}>
356
+ <div className="h-full w-[min(340px,92vw)] bg-background shadow-2xl" onClick={(e) => e.stopPropagation()}>
357
+ {panel(() => setDetailsOpen(false))}
358
+ </div>
359
+ </div>
360
+ ) : null}
361
+ </>
362
+ ) : (
363
+ <div className="hidden flex-1 items-center justify-center bg-background text-[14px] text-muted-foreground md:flex">
364
+ {loading ? "Loading…" : summaries.length ? "Select a conversation." : "No conversations yet."}
365
+ </div>
366
+ )}
367
+ </div>
368
+ );
369
+ }
370
+
371
+ return (
372
+ <div className="flex h-[100dvh] flex-col overflow-hidden md:flex-row">
373
+ {nav}
374
+ <main className="min-h-0 min-w-0 flex-1">{body}</main>
375
+ {toast ? (
376
+ <div role="alert" className="fixed bottom-20 left-1/2 z-40 -translate-x-1/2 rounded-full bg-foreground px-4 py-2 text-[13px] text-background shadow-lg md:bottom-6">
377
+ {toast}
378
+ </div>
379
+ ) : null}
380
+ </div>
381
+ );
382
+ }
@@ -0,0 +1,13 @@
1
+ import React from "react";
2
+ import type { Metadata, PageProps } from "@pylonsync/react";
3
+ import { Console } from "../console";
4
+ import { ownerGate } from "../gate";
5
+
6
+ export const metadata: Metadata = { title: "Allowlist", robots: "noindex" };
7
+
8
+ // `/contacts` — who the assistant answers.
9
+ export default function ContactsPage({ auth, response }: PageProps) {
10
+ const blocked = ownerGate({ auth, response });
11
+ if (blocked) return blocked;
12
+ return <Console view="contacts" initialConversationId={null} />;
13
+ }
@@ -0,0 +1,16 @@
1
+ "use client";
2
+
3
+ import React from "react";
4
+ import type { ErrorBoundaryProps } from "@pylonsync/react";
5
+
6
+ export default function ErrorPage({ error, reset }: ErrorBoundaryProps) {
7
+ return (
8
+ <main className="flex min-h-[100dvh] flex-col items-center justify-center gap-3 px-4 text-center">
9
+ <h1 className="text-[22px] font-semibold tracking-[-0.02em]">Something went wrong</h1>
10
+ <p className="max-w-[48ch] text-[14px] text-muted-foreground">{error.message}</p>
11
+ <button type="button" onClick={reset} className="rounded-full bg-foreground px-5 py-2 text-[13px] font-medium text-background">
12
+ Try again
13
+ </button>
14
+ </main>
15
+ );
16
+ }
@@ -0,0 +1,31 @@
1
+ import React from "react";
2
+ import type { PageProps } from "@pylonsync/react";
3
+ import { SignOutAndRetry } from "./sign-out";
4
+
5
+ /**
6
+ * Server-side gate shared by the dashboard pages. Anonymous visitors and guest
7
+ * sessions go to /login. A signed-in account that is not the owner (not an
8
+ * admin via PYLON_ADMIN_EMAILS) gets an explanation and no data: every entity
9
+ * policy denies non-admins as well, so this is presentation, not the fence.
10
+ */
11
+ export function ownerGate({ auth, response }: Pick<PageProps, "auth" | "response">): React.ReactNode | null {
12
+ if (!auth.user_id || auth.user_id.startsWith("guest_")) {
13
+ response.redirect("/login");
14
+ return <></>;
15
+ }
16
+ if (!auth.is_admin) {
17
+ return (
18
+ <main className="flex min-h-[100dvh] items-center justify-center px-4">
19
+ <div className="max-w-[420px] text-center">
20
+ <h1 className="text-[22px] font-semibold tracking-[-0.02em]">This dashboard belongs to its owner</h1>
21
+ <p className="mt-2 text-[14px] leading-relaxed text-muted-foreground">
22
+ You are signed in, but your email is not the owner email set in PYLON_ADMIN_EMAILS on this server, or it has
23
+ not been verified with a sign-in code yet.
24
+ </p>
25
+ <SignOutAndRetry />
26
+ </div>
27
+ </main>
28
+ );
29
+ }
30
+ return null;
31
+ }