@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.
- package/bin/create-pylon.js +30 -7
- package/package.json +1 -1
- package/templates/agency/.env.example +7 -0
- package/templates/agency/README.md +28 -3
- package/templates/agency/app/(marketing)/contact-form.tsx +20 -18
- package/templates/agency/app/(marketing)/page.tsx +3 -3
- package/templates/agency/app/(marketing)/work/page.tsx +3 -3
- package/templates/agency/app/auth-form.tsx +4 -4
- package/templates/agency/app/dashboard/dashboard-client.tsx +110 -64
- package/templates/agency/app/dashboard/page.tsx +14 -16
- package/templates/agency/app/icon.svg +3 -0
- package/templates/agency/app/login/page.tsx +12 -14
- package/templates/agency/components/marketing.tsx +12 -5
- package/templates/agency/functions/seedStudioBackoffice.ts +42 -11
- package/templates/agency/lib/agency.ts +22 -0
- package/templates/agency/lib/demo.ts +118 -0
- package/templates/agency/lib/site.config.ts +19 -13
- package/templates/agency/public/favicon.ico +0 -0
- package/templates/agency/tests/agency.test.ts +57 -0
- package/templates/ai-chat/.env.example +13 -27
- package/templates/ai-chat/README.md +53 -60
- package/templates/ai-chat/app/(chat)/c/[id]/page.tsx +19 -0
- package/templates/ai-chat/app/(chat)/chat-client.tsx +657 -622
- package/templates/ai-chat/app/(chat)/layout.tsx +6 -43
- package/templates/ai-chat/app/(chat)/not-found.tsx +6 -10
- package/templates/ai-chat/app/(chat)/page.tsx +7 -11
- package/templates/ai-chat/app/auth-form.tsx +45 -38
- package/templates/ai-chat/app/error.tsx +9 -12
- package/templates/ai-chat/app/globals.css +464 -102
- package/templates/ai-chat/app/layout.tsx +11 -19
- package/templates/ai-chat/app/login/page.tsx +39 -24
- package/templates/ai-chat/app.ts +118 -43
- package/templates/ai-chat/components/chat/composer.tsx +193 -0
- package/templates/ai-chat/components/chat/empty-state.tsx +65 -0
- package/templates/ai-chat/components/chat/icons.tsx +198 -0
- package/templates/ai-chat/components/chat/markdown.tsx +164 -0
- package/templates/ai-chat/components/chat/message.tsx +183 -0
- package/templates/ai-chat/components/chat/setup.tsx +71 -0
- package/templates/ai-chat/components/chat/sidebar.tsx +288 -0
- package/templates/ai-chat/functions/_beginTurn.ts +145 -0
- package/templates/ai-chat/functions/_finishTurn.ts +55 -0
- package/templates/ai-chat/functions/assistantStatus.ts +13 -0
- package/templates/ai-chat/functions/claimGuestHistory.ts +39 -0
- package/templates/ai-chat/functions/createConversation.ts +28 -0
- package/templates/ai-chat/functions/deleteConversation.ts +15 -20
- package/templates/ai-chat/functions/prepareGuestClaim.ts +32 -0
- package/templates/ai-chat/functions/renameConversation.ts +19 -0
- package/templates/ai-chat/functions/respond.ts +88 -0
- package/templates/ai-chat/functions/seedExamples.ts +46 -0
- package/templates/ai-chat/functions/stopResponse.ts +30 -0
- package/templates/ai-chat/lib/brand-icon.ts +7 -0
- package/templates/ai-chat/lib/chat.ts +277 -0
- package/templates/ai-chat/lib/claim.ts +15 -0
- package/templates/ai-chat/lib/examples.ts +179 -0
- package/templates/ai-chat/lib/highlight.ts +107 -0
- package/templates/ai-chat/lib/markdown.ts +317 -0
- package/templates/ai-chat/lib/provider.ts +38 -0
- package/templates/ai-chat/lib/site.config.ts +55 -39
- package/templates/ai-chat/tests/chat.test.ts +174 -0
- package/templates/ai-chat/tests/components.test.tsx +57 -0
- package/templates/ai-chat/tests/markdown.test.ts +88 -0
- package/templates/ai-chat/tests/provider.test.ts +32 -0
- package/templates/backend/consumer/apps/api/app/layout.tsx +33 -0
- package/templates/backend/consumer/apps/api/app/page.tsx +54 -0
- package/templates/backend/consumer/apps/api/app.ts +166 -0
- package/templates/backend/consumer/apps/api/functions/addComment.ts +24 -0
- package/templates/backend/consumer/apps/api/functions/createPost.ts +19 -36
- package/templates/backend/consumer/apps/api/functions/deletePost.ts +15 -13
- package/templates/backend/consumer/apps/api/functions/myProfile.ts +5 -8
- package/templates/backend/consumer/apps/api/functions/seedDemo.ts +95 -0
- package/templates/backend/consumer/apps/api/functions/setFollow.ts +36 -0
- package/templates/backend/consumer/apps/api/functions/setLike.ts +36 -0
- package/templates/backend/consumer/apps/api/functions/upsertProfile.ts +43 -37
- package/templates/backend/consumer/apps/api/lib/seed.ts +89 -0
- package/templates/backend/consumer/apps/api/lib/social.ts +50 -0
- package/templates/backend/consumer/apps/api/package.json +8 -2
- package/templates/backend/consumer/apps/api/public/images/posts/arch.jpg +0 -0
- package/templates/backend/consumer/apps/api/public/images/posts/coffee.jpg +0 -0
- package/templates/backend/consumer/apps/api/public/images/posts/dog.jpg +0 -0
- package/templates/backend/consumer/apps/api/public/images/posts/lisbon.jpg +0 -0
- package/templates/backend/consumer/apps/api/public/images/posts/ramen.jpg +0 -0
- package/templates/backend/consumer/apps/api/public/images/posts/run.jpg +0 -0
- package/templates/backend/consumer/apps/api/public/images/posts/sunset.jpg +0 -0
- package/templates/backend/consumer/apps/api/tsconfig.json +12 -2
- package/templates/chat/README.md +43 -21
- package/templates/chat/app/chat-client.tsx +495 -85
- package/templates/chat/app/error.tsx +1 -1
- package/templates/chat/app/globals.css +170 -54
- package/templates/chat/app/layout.tsx +12 -9
- package/templates/chat/app/not-found.tsx +4 -2
- package/templates/chat/app/page.tsx +9 -19
- package/templates/chat/app.ts +96 -17
- package/templates/chat/components/chat/avatar.tsx +45 -0
- package/templates/chat/components/chat/composer.tsx +110 -0
- package/templates/chat/components/chat/message-list.tsx +285 -0
- package/templates/chat/components/chat/sidebar.tsx +190 -0
- package/templates/chat/functions/joinChat.ts +83 -0
- package/templates/chat/functions/renameMember.ts +25 -0
- package/templates/chat/lib/chat.ts +188 -0
- package/templates/chat/lib/seed.ts +78 -0
- package/templates/chat/tests/chat.test.ts +159 -0
- package/templates/chat/tests/composer.test.tsx +54 -0
- package/templates/consumer/README.md +3 -3
- package/templates/consumer/app/globals.css +10 -3
- package/templates/consumer/app/icon.svg +1 -0
- package/templates/consumer/components/social/avatar.tsx +25 -11
- package/templates/consumer/functions/seedFeed.ts +3 -3
- package/templates/consumer/lib/seed.ts +43 -85
- package/templates/consumer/public/favicon.ico +0 -0
- package/templates/consumer/public/images/posts/arch.jpg +0 -0
- package/templates/consumer/public/images/posts/coffee.jpg +0 -0
- package/templates/consumer/public/images/posts/dog.jpg +0 -0
- package/templates/consumer/public/images/posts/lisbon.jpg +0 -0
- package/templates/consumer/public/images/posts/ramen.jpg +0 -0
- package/templates/consumer/public/images/posts/run.jpg +0 -0
- package/templates/consumer/public/images/posts/sunset.jpg +0 -0
- package/templates/consumer/tests/seed.test.ts +39 -0
- package/templates/creator/.env.example +7 -0
- package/templates/creator/README.md +24 -5
- package/templates/creator/app/(marketing)/newsletter-signup.tsx +33 -12
- package/templates/creator/app/auth-form.tsx +7 -7
- package/templates/creator/app/dashboard/dashboard-client.tsx +137 -100
- package/templates/creator/app/dashboard/page.tsx +18 -18
- package/templates/creator/app/icon.svg +2 -0
- package/templates/creator/app/login/page.tsx +14 -14
- package/templates/creator/app.ts +4 -4
- package/templates/creator/functions/seedDemo.ts +32 -0
- package/templates/creator/functions/subscribe.ts +4 -15
- package/templates/creator/lib/demo.ts +94 -0
- package/templates/creator/lib/site.config.ts +15 -10
- package/templates/creator/lib/stats.ts +35 -0
- package/templates/creator/public/favicon.ico +0 -0
- package/templates/creator/tests/counter.test.ts +92 -0
- package/templates/creator/tests/live-count.test.tsx +45 -0
- package/templates/crm/README.md +22 -1
- package/templates/crm/app/apple-icon.png +0 -0
- package/templates/crm/app/companies/companies-view.tsx +32 -14
- package/templates/crm/app/companies/page.tsx +2 -1
- package/templates/crm/app/contacts/contacts-view.tsx +34 -10
- package/templates/crm/app/contacts/page.tsx +2 -1
- package/templates/crm/app/deals/[id]/deal-view.tsx +137 -98
- package/templates/crm/app/deals/[id]/page.tsx +2 -1
- package/templates/crm/app/globals.css +29 -14
- package/templates/crm/app/icon.svg +1 -0
- package/templates/crm/app/layout.tsx +7 -5
- package/templates/crm/app/page.tsx +2 -1
- package/templates/crm/app/pipeline-view.tsx +5 -5
- package/templates/crm/app/workspace.tsx +29 -17
- package/templates/crm/app.ts +5 -5
- package/templates/crm/components/activity-timeline.tsx +14 -4
- package/templates/crm/components/app-shell.tsx +94 -0
- package/templates/crm/components/auth-form.tsx +35 -5
- package/templates/crm/components/avatar.tsx +5 -1
- package/templates/crm/components/board-skeleton.tsx +5 -5
- package/templates/crm/components/brand-mark.tsx +25 -0
- package/templates/crm/components/command-palette.tsx +17 -17
- package/templates/crm/components/data-table.tsx +97 -36
- package/templates/crm/components/deal-card.tsx +23 -27
- package/templates/crm/components/filter-tabs.tsx +101 -0
- package/templates/crm/components/metrics-bar.tsx +6 -6
- package/templates/crm/components/nav.tsx +15 -0
- package/templates/crm/components/page-header.tsx +43 -7
- package/templates/crm/components/pipeline-board.tsx +16 -10
- package/templates/crm/components/require-auth.tsx +24 -26
- package/templates/crm/components/sidebar.tsx +54 -49
- package/templates/crm/components/stage-badge.tsx +4 -2
- package/templates/crm/components/ui/button.tsx +5 -4
- package/templates/crm/components/ui/dialog.tsx +1 -1
- package/templates/crm/components/ui/input.tsx +1 -1
- package/templates/crm/components/ui/select.tsx +1 -1
- package/templates/crm/components/ui/textarea.tsx +1 -1
- package/templates/crm/functions/seedWorkspace.ts +32 -17
- package/templates/crm/lib/brand.ts +9 -0
- package/templates/crm/lib/demo-team.ts +51 -0
- package/templates/crm/lib/pipeline.ts +37 -0
- package/templates/crm/lib/search.ts +4 -3
- package/templates/crm/lib/seed.ts +53 -8
- package/templates/crm/public/favicon.ico +0 -0
- package/templates/crm/tests/components.test.tsx +5 -5
- package/templates/crm/tests/demo-team.test.ts +18 -0
- package/templates/crm/tests/pipeline.test.ts +59 -3
- package/templates/directory/.env.example +7 -0
- package/templates/directory/README.md +22 -3
- package/templates/directory/app/(marketing)/directory-browse.tsx +30 -12
- package/templates/directory/app/(marketing)/page.tsx +6 -4
- package/templates/directory/app/auth-form.tsx +4 -4
- package/templates/directory/app/dashboard/dashboard-client.tsx +177 -90
- package/templates/directory/app/dashboard/page.tsx +16 -14
- package/templates/directory/app/icon.svg +4 -0
- package/templates/directory/app/login/page.tsx +16 -15
- package/templates/directory/functions/seedDemo.ts +43 -0
- package/templates/directory/lib/demo.ts +145 -0
- package/templates/directory/lib/directory.ts +27 -0
- package/templates/directory/lib/site.config.ts +21 -16
- package/templates/directory/public/favicon.ico +0 -0
- package/templates/directory/tests/directory.test.ts +46 -0
- package/templates/expo/consumer/apps/expo/App.tsx +360 -288
- package/templates/expo/consumer/apps/expo/app.json +9 -1
- package/templates/expo/consumer/apps/expo/package.json +2 -1
- package/templates/helpdesk/README.md +21 -0
- package/templates/helpdesk/app/apple-icon.png +0 -0
- package/templates/helpdesk/app/customers/customers-view.tsx +23 -7
- package/templates/helpdesk/app/customers/page.tsx +2 -1
- package/templates/helpdesk/app/globals.css +29 -14
- package/templates/helpdesk/app/icon.svg +1 -0
- package/templates/helpdesk/app/inbox-view.tsx +66 -47
- package/templates/helpdesk/app/layout.tsx +7 -5
- package/templates/helpdesk/app/page.tsx +4 -7
- package/templates/helpdesk/app/tickets/[id]/page.tsx +2 -1
- package/templates/helpdesk/app/tickets/[id]/ticket-view.tsx +134 -97
- package/templates/helpdesk/app/workspace.tsx +33 -22
- package/templates/helpdesk/app.ts +2 -2
- package/templates/helpdesk/components/app-shell.tsx +94 -0
- package/templates/helpdesk/components/auth-form.tsx +35 -5
- package/templates/helpdesk/components/avatar.tsx +5 -1
- package/templates/helpdesk/components/brand-mark.tsx +28 -0
- package/templates/helpdesk/components/command-palette.tsx +17 -17
- package/templates/helpdesk/components/data-table.tsx +97 -36
- package/templates/helpdesk/components/filter-tabs.tsx +101 -0
- package/templates/helpdesk/components/nav.tsx +14 -0
- package/templates/helpdesk/components/page-header.tsx +43 -7
- package/templates/helpdesk/components/priority-badge.tsx +49 -6
- package/templates/helpdesk/components/require-auth.tsx +24 -26
- package/templates/helpdesk/components/sidebar.tsx +54 -48
- package/templates/helpdesk/components/sla-indicator.tsx +5 -8
- package/templates/helpdesk/components/thread.tsx +7 -7
- package/templates/helpdesk/components/ticket-list.tsx +79 -19
- package/templates/helpdesk/components/ui/button.tsx +5 -4
- package/templates/helpdesk/components/ui/dialog.tsx +1 -1
- package/templates/helpdesk/components/ui/input.tsx +1 -1
- package/templates/helpdesk/components/ui/select.tsx +1 -1
- package/templates/helpdesk/components/ui/textarea.tsx +1 -1
- package/templates/helpdesk/functions/seedWorkspace.ts +25 -15
- package/templates/helpdesk/lib/brand.ts +9 -0
- package/templates/helpdesk/lib/demo-team.ts +51 -0
- package/templates/helpdesk/lib/search.ts +4 -3
- package/templates/helpdesk/lib/seed.ts +43 -2
- package/templates/helpdesk/lib/tickets.ts +68 -20
- package/templates/helpdesk/public/favicon.ico +0 -0
- package/templates/helpdesk/tests/components.test.tsx +10 -0
- package/templates/helpdesk/tests/demo-team.test.ts +18 -0
- package/templates/helpdesk/tests/tickets.test.ts +70 -0
- package/templates/imessage/.env.example +49 -0
- package/templates/imessage/AGENTS.md +153 -0
- package/templates/imessage/CLAUDE.md +7 -0
- package/templates/imessage/README.md +187 -0
- package/templates/imessage/app/console.tsx +382 -0
- package/templates/imessage/app/contacts/page.tsx +13 -0
- package/templates/imessage/app/error.tsx +16 -0
- package/templates/imessage/app/gate.tsx +31 -0
- package/templates/imessage/app/globals.css +183 -0
- package/templates/imessage/app/layout.tsx +17 -0
- package/templates/imessage/app/login/login-form.tsx +115 -0
- package/templates/imessage/app/login/page.tsx +30 -0
- package/templates/imessage/app/not-found.tsx +13 -0
- package/templates/imessage/app/page.tsx +14 -0
- package/templates/imessage/app/robots.ts +6 -0
- package/templates/imessage/app/setup/page.tsx +13 -0
- package/templates/imessage/app/sign-out.tsx +20 -0
- package/templates/imessage/app.ts +299 -0
- package/templates/imessage/bunfig +4 -0
- package/templates/imessage/components/avatar.tsx +46 -0
- package/templates/imessage/components/contact-panel.tsx +263 -0
- package/templates/imessage/components/contacts-view.tsx +172 -0
- package/templates/imessage/components/conversation-list.tsx +129 -0
- package/templates/imessage/components/nav-rail.tsx +67 -0
- package/templates/imessage/components/setup-view.tsx +408 -0
- package/templates/imessage/components/thread.tsx +229 -0
- package/templates/imessage/components/ui/badge.tsx +37 -0
- package/templates/imessage/components/ui/button.tsx +56 -0
- package/templates/imessage/components/ui/card.tsx +90 -0
- package/templates/imessage/components/ui/dialog.tsx +110 -0
- package/templates/imessage/components/ui/input.tsx +30 -0
- package/templates/imessage/components/ui/label.tsx +21 -0
- package/templates/imessage/components/ui/select.tsx +39 -0
- package/templates/imessage/components/ui/table.tsx +124 -0
- package/templates/imessage/components/ui/textarea.tsx +24 -0
- package/templates/imessage/components.json +20 -0
- package/templates/imessage/functions/answerNow.ts +34 -0
- package/templates/imessage/functions/assistant.ts +96 -0
- package/templates/imessage/functions/beginTurn.ts +101 -0
- package/templates/imessage/functions/cancelReminder.ts +13 -0
- package/templates/imessage/functions/claimOutbound.ts +44 -0
- package/templates/imessage/functions/clearDemo.ts +18 -0
- package/templates/imessage/functions/deleteNote.ts +11 -0
- package/templates/imessage/functions/deliverOutbound.ts +55 -0
- package/templates/imessage/functions/finishTurn.ts +84 -0
- package/templates/imessage/functions/fireReminder.ts +42 -0
- package/templates/imessage/functions/ingestInbound.ts +22 -0
- package/templates/imessage/functions/ownerAccount.ts +13 -0
- package/templates/imessage/functions/processTurn.ts +74 -0
- package/templates/imessage/functions/relayApply.ts +102 -0
- package/templates/imessage/functions/relaySync.ts +63 -0
- package/templates/imessage/functions/releaseOutbound.ts +18 -0
- package/templates/imessage/functions/resolveTurn.ts +25 -0
- package/templates/imessage/functions/saveContact.ts +27 -0
- package/templates/imessage/functions/seedDemo.ts +70 -0
- package/templates/imessage/functions/sendOwnerMessage.ts +28 -0
- package/templates/imessage/functions/sendblueWebhook.ts +49 -0
- package/templates/imessage/functions/setContactStatus.ts +17 -0
- package/templates/imessage/functions/settleOutbound.ts +35 -0
- package/templates/imessage/functions/setupStatus.ts +33 -0
- package/templates/imessage/functions/sweep.ts +58 -0
- package/templates/imessage/functions/toolCancelReminder.ts +17 -0
- package/templates/imessage/functions/toolForgetNote.ts +15 -0
- package/templates/imessage/functions/toolListNotes.ts +16 -0
- package/templates/imessage/functions/toolListReminders.ts +17 -0
- package/templates/imessage/functions/toolSaveNote.ts +22 -0
- package/templates/imessage/functions/toolSetReminder.ts +55 -0
- package/templates/imessage/functions/updateSettings.ts +48 -0
- package/templates/imessage/gitignore +11 -0
- package/templates/imessage/lib/agent-call.ts +251 -0
- package/templates/imessage/lib/assistant.ts +74 -0
- package/templates/imessage/lib/chunk.ts +135 -0
- package/templates/imessage/lib/demo.ts +68 -0
- package/templates/imessage/lib/format.ts +170 -0
- package/templates/imessage/lib/gate.ts +104 -0
- package/templates/imessage/lib/handles.ts +67 -0
- package/templates/imessage/lib/provider.ts +38 -0
- package/templates/imessage/lib/relay-protocol.ts +95 -0
- package/templates/imessage/lib/reminders.ts +41 -0
- package/templates/imessage/lib/secrets.ts +59 -0
- package/templates/imessage/lib/sendblue.ts +173 -0
- package/templates/imessage/lib/store.ts +407 -0
- package/templates/imessage/lib/transport.ts +135 -0
- package/templates/imessage/lib/utils.ts +10 -0
- package/templates/imessage/package.json +40 -0
- package/templates/imessage/relay/.env.example +14 -0
- package/templates/imessage/relay/applescript.ts +70 -0
- package/templates/imessage/relay/chatdb.ts +165 -0
- package/templates/imessage/relay/client.ts +41 -0
- package/templates/imessage/relay/main.ts +137 -0
- package/templates/imessage/relay/state.ts +35 -0
- package/templates/imessage/tests/agent-call.test.ts +112 -0
- package/templates/imessage/tests/applescript.test.ts +88 -0
- package/templates/imessage/tests/chatdb.test.ts +139 -0
- package/templates/imessage/tests/chunk.test.ts +61 -0
- package/templates/imessage/tests/fake-ctx.ts +88 -0
- package/templates/imessage/tests/ingest.test.ts +141 -0
- package/templates/imessage/tests/lib.test.ts +99 -0
- package/templates/imessage/tests/review-fixes.test.ts +138 -0
- package/templates/imessage/tests/secrets.test.ts +36 -0
- package/templates/imessage/tests/sendblue.test.ts +143 -0
- package/templates/imessage/tests/setup.ts +5 -0
- package/templates/imessage/tsconfig.json +18 -0
- package/templates/inventory/README.md +21 -0
- package/templates/inventory/app/apple-icon.png +0 -0
- package/templates/inventory/app/globals.css +29 -14
- package/templates/inventory/app/icon.svg +1 -0
- package/templates/inventory/app/layout.tsx +7 -5
- package/templates/inventory/app/movements/movements-view.tsx +32 -10
- package/templates/inventory/app/movements/page.tsx +2 -1
- package/templates/inventory/app/page.tsx +4 -6
- package/templates/inventory/app/products/[id]/page.tsx +2 -1
- package/templates/inventory/app/products/[id]/product-view.tsx +55 -25
- package/templates/inventory/app/products-view.tsx +74 -33
- package/templates/inventory/app/workspace.tsx +34 -16
- package/templates/inventory/app.ts +10 -2
- package/templates/inventory/components/app-shell.tsx +94 -0
- package/templates/inventory/components/auth-form.tsx +35 -5
- package/templates/inventory/components/avatar.tsx +5 -1
- package/templates/inventory/components/brand-mark.tsx +25 -0
- package/templates/inventory/components/category-tile.tsx +49 -0
- package/templates/inventory/components/command-palette.tsx +17 -17
- package/templates/inventory/components/data-table.tsx +97 -36
- package/templates/inventory/components/filter-tabs.tsx +101 -0
- package/templates/inventory/components/nav.tsx +14 -0
- package/templates/inventory/components/page-header.tsx +43 -7
- package/templates/inventory/components/require-auth.tsx +24 -26
- package/templates/inventory/components/sidebar.tsx +54 -48
- package/templates/inventory/components/stock-level.tsx +30 -14
- package/templates/inventory/components/summary-bar.tsx +8 -15
- package/templates/inventory/components/ui/button.tsx +5 -4
- package/templates/inventory/components/ui/dialog.tsx +1 -1
- package/templates/inventory/components/ui/input.tsx +1 -1
- package/templates/inventory/components/ui/select.tsx +1 -1
- package/templates/inventory/components/ui/textarea.tsx +1 -1
- package/templates/inventory/functions/seedWorkspace.ts +18 -8
- package/templates/inventory/lib/brand.ts +9 -0
- package/templates/inventory/lib/demo-team.ts +51 -0
- package/templates/inventory/lib/search.ts +4 -3
- package/templates/inventory/lib/seed.ts +29 -2
- package/templates/inventory/lib/stock.ts +32 -0
- package/templates/inventory/public/favicon.ico +0 -0
- package/templates/inventory/tests/demo-team.test.ts +18 -0
- package/templates/inventory/tests/stock.test.ts +29 -0
- package/templates/invoices/README.md +16 -0
- package/templates/invoices/app/apple-icon.png +0 -0
- package/templates/invoices/app/clients/clients-view.tsx +18 -6
- package/templates/invoices/app/clients/page.tsx +2 -1
- package/templates/invoices/app/globals.css +29 -14
- package/templates/invoices/app/icon.svg +1 -0
- package/templates/invoices/app/invoices/[id]/invoice-view.tsx +72 -48
- package/templates/invoices/app/invoices/[id]/page.tsx +2 -1
- package/templates/invoices/app/invoices-view.tsx +81 -32
- package/templates/invoices/app/layout.tsx +7 -5
- package/templates/invoices/app/page.tsx +4 -6
- package/templates/invoices/app/workspace.tsx +26 -16
- package/templates/invoices/app.ts +2 -2
- package/templates/invoices/components/app-shell.tsx +94 -0
- package/templates/invoices/components/auth-form.tsx +35 -5
- package/templates/invoices/components/avatar.tsx +5 -1
- package/templates/invoices/components/brand-mark.tsx +26 -0
- package/templates/invoices/components/command-palette.tsx +17 -17
- package/templates/invoices/components/data-table.tsx +97 -36
- package/templates/invoices/components/filter-tabs.tsx +101 -0
- package/templates/invoices/components/line-items.tsx +19 -14
- package/templates/invoices/components/nav.tsx +14 -0
- package/templates/invoices/components/page-header.tsx +43 -7
- package/templates/invoices/components/require-auth.tsx +24 -26
- package/templates/invoices/components/sidebar.tsx +54 -48
- package/templates/invoices/components/status-badge.tsx +7 -7
- package/templates/invoices/components/summary-bar.tsx +5 -5
- package/templates/invoices/components/ui/button.tsx +5 -4
- package/templates/invoices/components/ui/dialog.tsx +1 -1
- package/templates/invoices/components/ui/input.tsx +1 -1
- package/templates/invoices/components/ui/select.tsx +1 -1
- package/templates/invoices/components/ui/textarea.tsx +1 -1
- package/templates/invoices/functions/recordPayment.ts +3 -0
- package/templates/invoices/lib/billing.ts +40 -0
- package/templates/invoices/lib/brand.ts +9 -0
- package/templates/invoices/lib/search.ts +4 -3
- package/templates/invoices/public/favicon.ico +0 -0
- package/templates/invoices/tests/billing.test.ts +27 -0
- package/templates/ios/consumer/apps/ios/Package.swift +4 -3
- package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/ActivityView.swift +107 -0
- package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/AppModel.swift +171 -0
- package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/AuthView.swift +182 -0
- package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/CommentsView.swift +136 -0
- package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/ComposeView.swift +152 -0
- package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/ExploreView.swift +117 -0
- package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/FeedView.swift +175 -159
- package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/MainTabView.swift +54 -0
- package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/Media.swift +271 -0
- package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/Models.swift +101 -4
- package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/PostCard.swift +245 -0
- package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/ProfileEditorView.swift +208 -0
- package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/ProfileView.swift +183 -0
- package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/RootView.swift +25 -31
- package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/SocialStore.swift +285 -0
- package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/StoryViewer.swift +155 -0
- package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/Theme.swift +82 -0
- package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +4 -47
- package/templates/ios/consumer/apps/ios/project.yml +13 -9
- package/templates/local-service/.env.example +7 -0
- package/templates/local-service/README.md +26 -2
- package/templates/local-service/app/(marketing)/booking-widget.tsx +48 -9
- package/templates/local-service/app/(marketing)/page.tsx +66 -42
- package/templates/local-service/app/auth-form.tsx +8 -8
- package/templates/local-service/app/dashboard/dashboard-client.tsx +119 -92
- package/templates/local-service/app/dashboard/page.tsx +14 -14
- package/templates/local-service/app/icon.svg +2 -0
- package/templates/local-service/app/login/page.tsx +15 -15
- package/templates/local-service/functions/seedDemo.ts +49 -0
- package/templates/local-service/lib/demo.ts +135 -0
- package/templates/local-service/lib/hours.ts +44 -0
- package/templates/local-service/lib/site.config.ts +8 -3
- package/templates/local-service/lib/slots.ts +3 -0
- package/templates/local-service/public/favicon.ico +0 -0
- package/templates/local-service/tests/booking.test.ts +104 -0
- package/templates/marketplace/README.md +23 -15
- package/templates/marketplace/app/globals.css +172 -114
- package/templates/marketplace/app/layout.tsx +75 -30
- package/templates/marketplace/app/listing/[id]/page.tsx +210 -123
- package/templates/marketplace/app/me/page.tsx +4 -4
- package/templates/marketplace/app/page.tsx +221 -298
- package/templates/marketplace/app/sell/page.tsx +8 -11
- package/templates/marketplace/app.ts +32 -7
- package/templates/marketplace/client/ActivityFeed.tsx +311 -0
- package/templates/marketplace/client/AuthNav.tsx +24 -16
- package/templates/marketplace/client/ListingCard.tsx +118 -0
- package/templates/marketplace/client/LiveGrid.tsx +92 -0
- package/templates/marketplace/client/LiveListingStatus.tsx +32 -24
- package/templates/marketplace/client/LoginCard.tsx +114 -136
- package/templates/marketplace/client/MarketProvider.tsx +3 -5
- package/templates/marketplace/client/MyMarket.tsx +371 -185
- package/templates/marketplace/client/OfferPanel.tsx +421 -287
- package/templates/marketplace/client/SellForm.tsx +271 -192
- package/templates/marketplace/client/ThemeToggle.tsx +1 -0
- package/templates/marketplace/client/WatchButton.tsx +16 -16
- package/templates/marketplace/client/market.ts +61 -14
- package/templates/marketplace/functions/seedMarket.ts +143 -36
- package/templates/marketplace/functions/seedOffers.ts +101 -0
- package/templates/marketplace/lib/catalog.ts +72 -1
- package/templates/marketplace/public/images/listings/ceramic-lamp-640.webp +0 -0
- package/templates/marketplace/public/images/listings/ceramic-lamp.webp +0 -0
- package/templates/marketplace/public/images/listings/duffel-640.webp +0 -0
- package/templates/marketplace/public/images/listings/duffel.webp +0 -0
- package/templates/marketplace/public/images/listings/film-camera-640.webp +0 -0
- package/templates/marketplace/public/images/listings/film-camera.webp +0 -0
- package/templates/marketplace/public/images/listings/lounge-chair-640.webp +0 -0
- package/templates/marketplace/public/images/listings/lounge-chair.webp +0 -0
- package/templates/marketplace/public/images/listings/road-bike-640.webp +0 -0
- package/templates/marketplace/public/images/listings/road-bike.webp +0 -0
- package/templates/marketplace/public/images/listings/slipper-chair-640.webp +0 -0
- package/templates/marketplace/public/images/listings/slipper-chair.webp +0 -0
- package/templates/marketplace/public/images/listings/turntable-640.webp +0 -0
- package/templates/marketplace/public/images/listings/turntable.webp +0 -0
- package/templates/marketplace/public/images/listings/wardrobe-640.webp +0 -0
- package/templates/marketplace/public/images/listings/wardrobe.webp +0 -0
- package/templates/marketplace/public/images/listings/watch-640.webp +0 -0
- package/templates/marketplace/public/images/listings/watch.webp +0 -0
- package/templates/marketplace/public/images/listings/weekender-bag-640.webp +0 -0
- package/templates/marketplace/public/images/listings/weekender-bag.webp +0 -0
- package/templates/marketplace/tests/example.test.ts +64 -2
- package/templates/marketplace/tests/listing-card.test.tsx +61 -0
- package/templates/projects/README.md +24 -3
- package/templates/projects/app/apple-icon.png +0 -0
- package/templates/projects/app/clients/clients-view.tsx +20 -6
- package/templates/projects/app/clients/page.tsx +2 -1
- package/templates/projects/app/globals.css +29 -14
- package/templates/projects/app/icon.svg +1 -0
- package/templates/projects/app/layout.tsx +7 -5
- package/templates/projects/app/page.tsx +3 -5
- package/templates/projects/app/projects/[id]/page.tsx +2 -1
- package/templates/projects/app/projects/[id]/project-view.tsx +45 -30
- package/templates/projects/app/projects-view.tsx +105 -52
- package/templates/projects/app/workspace.tsx +41 -20
- package/templates/projects/app.ts +2 -2
- package/templates/projects/components/app-shell.tsx +94 -0
- package/templates/projects/components/auth-form.tsx +35 -5
- package/templates/projects/components/avatar.tsx +5 -1
- package/templates/projects/components/board-skeleton.tsx +3 -3
- package/templates/projects/components/brand-mark.tsx +25 -0
- package/templates/projects/components/command-palette.tsx +17 -17
- package/templates/projects/components/data-table.tsx +97 -36
- package/templates/projects/components/filter-tabs.tsx +101 -0
- package/templates/projects/components/meter.tsx +48 -0
- package/templates/projects/components/nav.tsx +14 -0
- package/templates/projects/components/page-header.tsx +43 -7
- package/templates/projects/components/require-auth.tsx +24 -26
- package/templates/projects/components/sidebar.tsx +54 -48
- package/templates/projects/components/summary-bar.tsx +49 -0
- package/templates/projects/components/task-board.tsx +10 -9
- package/templates/projects/components/time-dialog.tsx +29 -5
- package/templates/projects/components/ui/button.tsx +5 -4
- package/templates/projects/components/ui/dialog.tsx +1 -1
- package/templates/projects/components/ui/input.tsx +1 -1
- package/templates/projects/components/ui/select.tsx +1 -1
- package/templates/projects/components/ui/textarea.tsx +1 -1
- package/templates/projects/functions/seedWorkspace.ts +26 -12
- package/templates/projects/lib/brand.ts +9 -0
- package/templates/projects/lib/demo-team.ts +51 -0
- package/templates/projects/lib/search.ts +4 -3
- package/templates/projects/lib/seed.ts +35 -1
- package/templates/projects/lib/work.ts +63 -0
- package/templates/projects/public/favicon.ico +0 -0
- package/templates/projects/tests/demo-team.test.ts +18 -0
- package/templates/projects/tests/work.test.ts +65 -0
- package/templates/restaurant/.env.example +7 -0
- package/templates/restaurant/README.md +29 -3
- package/templates/restaurant/app/(marketing)/layout.tsx +4 -2
- package/templates/restaurant/app/(marketing)/page.tsx +47 -23
- package/templates/restaurant/app/(marketing)/reservation-widget.tsx +37 -7
- package/templates/restaurant/app/auth-form.tsx +8 -8
- package/templates/restaurant/app/dashboard/dashboard-client.tsx +241 -109
- package/templates/restaurant/app/dashboard/page.tsx +14 -14
- package/templates/restaurant/app/icon.svg +4 -0
- package/templates/restaurant/app/login/page.tsx +15 -15
- package/templates/restaurant/functions/seedDemo.ts +44 -0
- package/templates/restaurant/lib/demo.ts +144 -0
- package/templates/restaurant/lib/hours.ts +45 -0
- package/templates/restaurant/lib/site.config.ts +3 -3
- package/templates/restaurant/public/favicon.ico +0 -0
- package/templates/restaurant/tests/reservations.test.ts +67 -0
- package/templates/saas/README.md +54 -35
- package/templates/saas/app/(auth)/auth-form.tsx +25 -23
- package/templates/saas/app/(auth)/forgot-password/page.tsx +1 -1
- package/templates/saas/app/(auth)/layout.tsx +27 -33
- package/templates/saas/app/(auth)/login/page.tsx +1 -1
- package/templates/saas/app/(auth)/reset-password/page.tsx +1 -1
- package/templates/saas/app/(auth)/signup/page.tsx +1 -1
- package/templates/saas/app/(marketing)/layout.tsx +127 -253
- package/templates/saas/app/(marketing)/page.tsx +133 -372
- package/templates/saas/app/(marketing)/pricing/page.tsx +21 -19
- package/templates/saas/app/(marketing)/products/[slug]/page.tsx +40 -95
- package/templates/saas/app/dashboard/board-client.tsx +416 -0
- package/templates/saas/app/dashboard/dashboard-client.tsx +30 -615
- package/templates/saas/app/dashboard/layout.tsx +27 -13
- package/templates/saas/app/dashboard/overview-client.tsx +332 -0
- package/templates/saas/app/dashboard/page.tsx +29 -35
- package/templates/saas/app/dashboard/projects/[id]/page.tsx +29 -0
- package/templates/saas/app/dashboard/projects/page.tsx +11 -7
- package/templates/saas/app/dashboard/projects-client.tsx +398 -0
- package/templates/saas/app/globals.css +5 -4
- package/templates/saas/app/icon.svg +1 -0
- package/templates/saas/app/onboarding/onboarding-client.tsx +93 -26
- package/templates/saas/app/onboarding/page.tsx +6 -2
- package/templates/saas/app.ts +68 -13
- package/templates/saas/components/dashboard-shell.tsx +212 -132
- package/templates/saas/components/marketing.tsx +137 -274
- package/templates/saas/components/pricing-table.tsx +12 -11
- package/templates/saas/components/setup-checklist.tsx +31 -29
- package/templates/saas/components/task-ui.tsx +199 -0
- package/templates/saas/functions/createProject.ts +7 -13
- package/templates/saas/functions/createTask.ts +67 -0
- package/templates/saas/functions/deleteProject.ts +17 -0
- package/templates/saas/functions/deleteTask.ts +14 -0
- package/templates/saas/functions/seedWorkspace.ts +46 -0
- package/templates/saas/functions/setProjectStatus.ts +41 -0
- package/templates/saas/functions/updateProfile.ts +13 -0
- package/templates/saas/functions/updateProject.ts +4 -5
- package/templates/saas/functions/updateTask.ts +88 -0
- package/templates/saas/lib/members.ts +53 -0
- package/templates/saas/lib/names.ts +53 -0
- package/templates/saas/lib/plans.ts +11 -0
- package/templates/saas/lib/sample-workspace.ts +117 -0
- package/templates/saas/lib/site.config.ts +200 -463
- package/templates/saas/lib/tasks.ts +101 -0
- package/templates/saas/lib/use-live.ts +25 -0
- package/templates/saas/public/favicon.ico +0 -0
- package/templates/saas/public/screenshots/board.png +0 -0
- package/templates/saas/public/screenshots/members.png +0 -0
- package/templates/saas/public/screenshots/overview.png +0 -0
- package/templates/saas/public/screenshots/projects.png +0 -0
- package/templates/saas/scripts/capture-screenshots.mjs +156 -0
- package/templates/saas/tests/names.test.ts +35 -0
- package/templates/saas/tests/plans.test.ts +12 -0
- package/templates/saas/tests/sample-workspace.test.ts +46 -0
- package/templates/saas/tests/tasks.test.ts +81 -0
- package/templates/shop/README.md +25 -13
- package/templates/shop/app/(marketing)/layout.tsx +94 -67
- package/templates/shop/app/(marketing)/not-found.tsx +13 -12
- package/templates/shop/app/(marketing)/page.tsx +120 -59
- package/templates/shop/app/(marketing)/shop-client.tsx +364 -237
- package/templates/shop/app/(marketing)/success/page.tsx +75 -22
- package/templates/shop/app/globals.css +94 -1
- package/templates/shop/app/layout.tsx +4 -4
- package/templates/shop/app/login/page.tsx +4 -4
- package/templates/shop/app.ts +18 -6
- package/templates/shop/components/cart-button.tsx +33 -0
- package/templates/shop/components/marketing.tsx +17 -87
- package/templates/shop/components/shelf-count.tsx +36 -0
- package/templates/shop/functions/checkout.ts +17 -1
- package/templates/shop/lib/cart-store.ts +57 -0
- package/templates/shop/lib/shelf-store.ts +35 -0
- package/templates/shop/lib/shop.ts +27 -0
- package/templates/shop/lib/site.config.ts +108 -98
- package/templates/shop/public/images/hero.jpg +0 -0
- package/templates/shop/public/images/products/cedar-smoke.jpg +0 -0
- package/templates/shop/public/images/products/fig-leaf.jpg +0 -0
- package/templates/shop/public/images/products/linen.jpg +0 -0
- package/templates/shop/public/images/products/sea-salt.jpg +0 -0
- package/templates/shop/public/images/products/tobacco-oak.jpg +0 -0
- package/templates/shop/tests/shop.test.ts +64 -0
- package/templates/waitlist/.env.example +7 -0
- package/templates/waitlist/README.md +29 -13
- package/templates/waitlist/app/(marketing)/page.tsx +9 -7
- package/templates/waitlist/app/(marketing)/waitlist-hero.tsx +64 -31
- package/templates/waitlist/app/auth-form.tsx +8 -8
- package/templates/waitlist/app/dashboard/dashboard-client.tsx +132 -95
- package/templates/waitlist/app/dashboard/page.tsx +17 -16
- package/templates/waitlist/app/icon.svg +2 -0
- package/templates/waitlist/app/login/page.tsx +14 -14
- package/templates/waitlist/app.ts +6 -6
- package/templates/waitlist/functions/joinWaitlist.ts +6 -18
- package/templates/waitlist/functions/seedDemo.ts +32 -0
- package/templates/waitlist/lib/demo.ts +94 -0
- package/templates/waitlist/lib/launch.ts +19 -0
- package/templates/waitlist/lib/site.config.ts +14 -9
- package/templates/waitlist/lib/stats.ts +35 -0
- package/templates/waitlist/public/favicon.ico +0 -0
- package/templates/waitlist/tests/counter.test.ts +107 -0
- package/templates/waitlist/tests/live-count.test.tsx +48 -0
- package/templates/ai-chat/tests/example.test.ts +0 -12
- package/templates/backend/consumer/apps/api/functions/feed.ts +0 -57
- package/templates/backend/consumer/apps/api/functions/profilePosts.ts +0 -17
- package/templates/backend/consumer/apps/api/functions/toggleLike.ts +0 -48
- package/templates/backend/consumer/apps/api/schema.ts +0 -143
- package/templates/chat/functions/_keep.ts +0 -13
- package/templates/chat/tests/example.test.ts +0 -12
- package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/ProfileSetupView.swift +0 -68
- package/templates/marketplace/client/LiveTicker.tsx +0 -116
- package/templates/marketplace/client/ScrollToListingsLink.tsx +0 -29
- 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
|
+
}
|