@pylonsync/create-pylon 0.19.1 → 0.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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,135 @@
|
|
|
1
|
+
// The one interface both iMessage transports implement. Server code talks to a
|
|
2
|
+
// `MessageTransport`; which adapter it gets depends on IMESSAGE_TRANSPORT.
|
|
3
|
+
//
|
|
4
|
+
// sendblue Sendblue's hosted iMessage API. Inbound: signed webhook to
|
|
5
|
+
// /api/webhooks/sendblueWebhook. Outbound: their REST API.
|
|
6
|
+
// relay A Bun script on an always-on Mac (relay/). Inbound: the relay
|
|
7
|
+
// reads ~/Library/Messages/chat.db and posts new rows to
|
|
8
|
+
// /api/fn/relaySync. Outbound: replies wait in the Message table
|
|
9
|
+
// until the relay's next sync picks them up and sends them with
|
|
10
|
+
// Messages.app.
|
|
11
|
+
|
|
12
|
+
import { normalizeHandle } from "./handles";
|
|
13
|
+
|
|
14
|
+
export type TransportName = "sendblue" | "relay";
|
|
15
|
+
|
|
16
|
+
/** A message a transport received, in the app's shape. */
|
|
17
|
+
export interface InboundMessage {
|
|
18
|
+
/** The transport's own message id. Webhooks retry, so this is the dedupe key. */
|
|
19
|
+
externalId: string;
|
|
20
|
+
/** Normalized sender handle (E.164 phone or lowercase email). */
|
|
21
|
+
handle: string;
|
|
22
|
+
text: string;
|
|
23
|
+
/** ISO-8601 time the sender sent it. */
|
|
24
|
+
sentAt: string;
|
|
25
|
+
/** "iMessage" | "SMS" | … as reported by the transport. */
|
|
26
|
+
service: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** A message the app wants delivered. */
|
|
30
|
+
export interface OutboundMessage {
|
|
31
|
+
/** Id of the outbound Message row this send belongs to. */
|
|
32
|
+
messageId: string;
|
|
33
|
+
to: string;
|
|
34
|
+
text: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export type SendResult =
|
|
38
|
+
/** The transport accepted the message. */
|
|
39
|
+
| { status: "sent"; externalId: string | null }
|
|
40
|
+
/** The message waits for a pull-based transport (the Mac relay) to collect it. */
|
|
41
|
+
| { status: "queued" }
|
|
42
|
+
/** IMESSAGE_DRY_RUN is on: nothing left the server. */
|
|
43
|
+
| { status: "dry_run" }
|
|
44
|
+
| { status: "failed"; error: string };
|
|
45
|
+
|
|
46
|
+
export interface MessageTransport {
|
|
47
|
+
readonly name: TransportName;
|
|
48
|
+
/** Turn one raw inbound payload into an InboundMessage, or say why it is skipped. */
|
|
49
|
+
normalizeInbound(payload: unknown): { message: InboundMessage } | { skip: string };
|
|
50
|
+
send(message: OutboundMessage): Promise<SendResult>;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
type Env = Record<string, string | undefined>;
|
|
54
|
+
|
|
55
|
+
function present(env: Env, key: string): boolean {
|
|
56
|
+
const v = env[key];
|
|
57
|
+
return typeof v === "string" && v.trim() !== "";
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** The transport named by IMESSAGE_TRANSPORT, or null when unset or unknown. */
|
|
61
|
+
export function selectedTransport(env: Env): TransportName | null {
|
|
62
|
+
const v = (env.IMESSAGE_TRANSPORT ?? "").trim().toLowerCase();
|
|
63
|
+
return v === "sendblue" || v === "relay" ? v : null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function flag(env: Env, key: string): boolean {
|
|
67
|
+
const v = (env[key] ?? "").trim().toLowerCase();
|
|
68
|
+
return v === "1" || v === "true" || v === "yes";
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** `pylon dev` sets PYLON_DEV_MODE=1. */
|
|
72
|
+
export function isDevMode(env: Env): boolean {
|
|
73
|
+
return flag(env, "PYLON_DEV_MODE");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Whether outbound messages are recorded without being sent. True when
|
|
78
|
+
* IMESSAGE_DRY_RUN is set, and always in `pylon dev` unless
|
|
79
|
+
* IMESSAGE_ALLOW_DEV_SENDS=1: a development server is reachable from the local
|
|
80
|
+
* network and hands out sign-in codes in its responses, so it must not be able
|
|
81
|
+
* to text real people by default.
|
|
82
|
+
*/
|
|
83
|
+
export function isDryRun(env: Env): boolean {
|
|
84
|
+
if (flag(env, "IMESSAGE_DRY_RUN")) return true;
|
|
85
|
+
return isDevMode(env) && !flag(env, "IMESSAGE_ALLOW_DEV_SENDS");
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export interface SetupItem {
|
|
89
|
+
key: string;
|
|
90
|
+
label: string;
|
|
91
|
+
ok: boolean;
|
|
92
|
+
/** One sentence on what to do when `ok` is false. */
|
|
93
|
+
hint: string;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** What each transport needs, as a checklist. Reports presence only, never values. */
|
|
97
|
+
export function transportChecklist(env: Env, name: TransportName): SetupItem[] {
|
|
98
|
+
if (name === "sendblue") {
|
|
99
|
+
const from = normalizeHandle(env.SENDBLUE_FROM_NUMBER ?? "");
|
|
100
|
+
return [
|
|
101
|
+
{
|
|
102
|
+
key: "SENDBLUE_API_KEY",
|
|
103
|
+
label: "Sendblue API key id",
|
|
104
|
+
ok: present(env, "SENDBLUE_API_KEY"),
|
|
105
|
+
hint: "Copy the API key id from the Sendblue dashboard.",
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
key: "SENDBLUE_API_SECRET",
|
|
109
|
+
label: "Sendblue API secret",
|
|
110
|
+
ok: present(env, "SENDBLUE_API_SECRET"),
|
|
111
|
+
hint: "Copy the API secret from the Sendblue dashboard.",
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
key: "SENDBLUE_FROM_NUMBER",
|
|
115
|
+
label: "Sendblue number",
|
|
116
|
+
ok: from?.kind === "phone",
|
|
117
|
+
hint: "Your Sendblue line in E.164 form, for example +15125550100.",
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
key: "SENDBLUE_WEBHOOK_SECRET",
|
|
121
|
+
label: "Webhook secret",
|
|
122
|
+
ok: (env.SENDBLUE_WEBHOOK_SECRET ?? "").trim().length >= 24,
|
|
123
|
+
hint: "At least 24 random characters. Set the same value as the webhook secret in Sendblue.",
|
|
124
|
+
},
|
|
125
|
+
];
|
|
126
|
+
}
|
|
127
|
+
return [
|
|
128
|
+
{
|
|
129
|
+
key: "RELAY_TOKEN",
|
|
130
|
+
label: "Relay token",
|
|
131
|
+
ok: (env.RELAY_TOKEN ?? "").trim().length >= 24,
|
|
132
|
+
hint: "At least 24 random characters. The relay on your Mac sends the same value.",
|
|
133
|
+
},
|
|
134
|
+
];
|
|
135
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type ClassValue, clsx } from "clsx";
|
|
2
|
+
import { twMerge } from "tailwind-merge";
|
|
3
|
+
|
|
4
|
+
// `cn` — the shadcn class merger. clsx resolves conditional/array class
|
|
5
|
+
// inputs; tailwind-merge then dedupes conflicting Tailwind utilities so
|
|
6
|
+
// the last one wins (e.g. `cn("px-2", "px-4")` → "px-4"). Every shadcn
|
|
7
|
+
// component routes its className through this.
|
|
8
|
+
export function cn(...inputs: ClassValue[]) {
|
|
9
|
+
return twMerge(clsx(inputs));
|
|
10
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "__APP_NAME_KEBAB__",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "pylon dev",
|
|
8
|
+
"deploy": "pylon deploy",
|
|
9
|
+
"check": "tsc --noEmit",
|
|
10
|
+
"test": "pylon test",
|
|
11
|
+
"relay": "bun relay/main.ts"
|
|
12
|
+
},
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"@pylonsync/react": "^__PYLON_VERSION__",
|
|
15
|
+
"@pylonsync/sdk": "^__PYLON_VERSION__",
|
|
16
|
+
"@pylonsync/functions": "^__PYLON_VERSION__",
|
|
17
|
+
"@pylonsync/client": "^__PYLON_VERSION__",
|
|
18
|
+
"react": "^19.0.0",
|
|
19
|
+
"react-dom": "^19.0.0",
|
|
20
|
+
"tailwindcss": "^4.3.0",
|
|
21
|
+
"@tailwindcss/cli": "^4.3.0",
|
|
22
|
+
"tw-animate-css": "^1.2.0",
|
|
23
|
+
"class-variance-authority": "^0.7.1",
|
|
24
|
+
"clsx": "^2.1.1",
|
|
25
|
+
"tailwind-merge": "^2.5.0",
|
|
26
|
+
"lucide-react": "^0.460.0",
|
|
27
|
+
"@radix-ui/react-dialog": "^1.1.4",
|
|
28
|
+
"@radix-ui/react-slot": "^1.1.0"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@pylonsync/cli": "^__PYLON_VERSION__",
|
|
32
|
+
"@happy-dom/global-registrator": "^20.10.0",
|
|
33
|
+
"@testing-library/dom": "^10.4.0",
|
|
34
|
+
"@testing-library/react": "^16.3.0",
|
|
35
|
+
"@types/node": "^22.0.0",
|
|
36
|
+
"@types/react": "^19.0.0",
|
|
37
|
+
"@types/react-dom": "^19.0.0",
|
|
38
|
+
"typescript": "^5.6.0"
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Copy to relay/.env on the Mac that runs the relay. Bun loads it automatically
|
|
2
|
+
# when you start the relay from this directory.
|
|
3
|
+
|
|
4
|
+
# Your app's public origin (https). The relay calls <PYLON_URL>/api/fn/relaySync.
|
|
5
|
+
PYLON_URL=https://your-app.example.com
|
|
6
|
+
|
|
7
|
+
# Same value as RELAY_TOKEN on the server. Generate with: openssl rand -hex 32
|
|
8
|
+
RELAY_TOKEN=
|
|
9
|
+
|
|
10
|
+
# Optional
|
|
11
|
+
# CHAT_DB=/Users/you/Library/Messages/chat.db
|
|
12
|
+
# RELAY_STATE=/Users/you/.pylon-imessage-relay/state.json
|
|
13
|
+
# POLL_MS=3000 (minimum 2500)
|
|
14
|
+
# RELAY_DRY_RUN=1
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// Send an iMessage through Messages.app with osascript.
|
|
2
|
+
//
|
|
3
|
+
// The message text and the recipient are passed as ARGUMENTS to a fixed
|
|
4
|
+
// AppleScript (`on run argv`), never spliced into script source, so quotes,
|
|
5
|
+
// backslashes, newlines, or AppleScript keywords in a message cannot change
|
|
6
|
+
// what the script does. osascript is spawned directly, without a shell.
|
|
7
|
+
|
|
8
|
+
import { isNormalizedHandle } from "../lib/handles";
|
|
9
|
+
|
|
10
|
+
export const SEND_SCRIPT = `on run argv
|
|
11
|
+
set theHandle to item 2 of argv
|
|
12
|
+
set theText to item 3 of argv
|
|
13
|
+
tell application "Messages"
|
|
14
|
+
set theService to 1st account whose service type = iMessage
|
|
15
|
+
set theBuddy to participant theHandle of theService
|
|
16
|
+
send theText to theBuddy
|
|
17
|
+
end tell
|
|
18
|
+
end run`;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Fixed first argument. osascript stops option parsing at the first
|
|
22
|
+
* non-option argument, so this keeps a message that starts with "-" from
|
|
23
|
+
* being read as an osascript flag.
|
|
24
|
+
*/
|
|
25
|
+
export const ARGV_SENTINEL = "pylon-imessage";
|
|
26
|
+
|
|
27
|
+
export const MAX_SEND_CHARS = 20_000;
|
|
28
|
+
|
|
29
|
+
export function buildOsascriptArgv(handle: string, text: string): string[] {
|
|
30
|
+
if (!isNormalizedHandle(handle)) throw new Error("Refusing to send: recipient is not a phone number or email");
|
|
31
|
+
if (typeof text !== "string" || text.length === 0) throw new Error("Refusing to send an empty message");
|
|
32
|
+
if (text.length > MAX_SEND_CHARS) throw new Error("Refusing to send: message too long");
|
|
33
|
+
if (text.includes("\u0000")) throw new Error("Refusing to send: message contains a NUL byte");
|
|
34
|
+
return ["osascript", "-e", SEND_SCRIPT, ARGV_SENTINEL, handle, text];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface SpawnResult {
|
|
38
|
+
exitCode: number;
|
|
39
|
+
stderr: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export type Spawner = (argv: string[]) => Promise<SpawnResult>;
|
|
43
|
+
|
|
44
|
+
/** Default spawner: Bun.spawn with an argv array (no shell). */
|
|
45
|
+
export const bunSpawner: Spawner = async (argv) => {
|
|
46
|
+
const proc = Bun.spawn(argv, { stdout: "ignore", stderr: "pipe", stdin: "ignore" });
|
|
47
|
+
const timer = setTimeout(() => proc.kill(), 30_000);
|
|
48
|
+
const [exitCode, stderr] = await Promise.all([proc.exited, new Response(proc.stderr).text()]);
|
|
49
|
+
clearTimeout(timer);
|
|
50
|
+
return { exitCode, stderr };
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export async function sendIMessage(
|
|
54
|
+
handle: string,
|
|
55
|
+
text: string,
|
|
56
|
+
options: { dryRun: boolean; spawn?: Spawner },
|
|
57
|
+
): Promise<{ ok: true } | { ok: false; error: string }> {
|
|
58
|
+
let argv: string[];
|
|
59
|
+
try {
|
|
60
|
+
argv = buildOsascriptArgv(handle, text);
|
|
61
|
+
} catch (err) {
|
|
62
|
+
return { ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
63
|
+
}
|
|
64
|
+
if (options.dryRun) return { ok: true };
|
|
65
|
+
const result = await (options.spawn ?? bunSpawner)(argv);
|
|
66
|
+
if (result.exitCode !== 0) {
|
|
67
|
+
return { ok: false, error: `osascript exited ${result.exitCode}: ${result.stderr.trim().slice(0, 300)}` };
|
|
68
|
+
}
|
|
69
|
+
return { ok: true };
|
|
70
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
// Read new messages from macOS Messages' database (~/Library/Messages/chat.db).
|
|
2
|
+
//
|
|
3
|
+
// The database is opened READ-ONLY. Messages.app owns it; this script never
|
|
4
|
+
// writes to it. New rows are found by ROWID watermark: every message row gets
|
|
5
|
+
// a larger ROWID than the ones before it.
|
|
6
|
+
|
|
7
|
+
import { Database } from "bun:sqlite";
|
|
8
|
+
|
|
9
|
+
/** Seconds between 1970-01-01 and 2001-01-01 (Apple's reference date). */
|
|
10
|
+
const APPLE_EPOCH_OFFSET_S = 978_307_200;
|
|
11
|
+
|
|
12
|
+
export interface ChatDbMessage {
|
|
13
|
+
rowId: number;
|
|
14
|
+
guid: string;
|
|
15
|
+
handle: string;
|
|
16
|
+
text: string;
|
|
17
|
+
sentAt: string;
|
|
18
|
+
service: string;
|
|
19
|
+
/** Set when the row belongs to a group chat; the relay skips those. */
|
|
20
|
+
isGroup: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function openChatDb(path: string): Database {
|
|
24
|
+
return new Database(path, { readonly: true });
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** message.date: nanoseconds since 2001 on macOS 10.13+, seconds before that. */
|
|
28
|
+
export function appleDateToIso(value: number | bigint | null): string {
|
|
29
|
+
if (value === null || value === undefined) return new Date().toISOString();
|
|
30
|
+
const n = Number(value);
|
|
31
|
+
if (!Number.isFinite(n) || n === 0) return new Date().toISOString();
|
|
32
|
+
const seconds = n > 1e12 ? n / 1e9 : n;
|
|
33
|
+
return new Date((seconds + APPLE_EPOCH_OFFSET_S) * 1000).toISOString();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Pull the plain text out of `message.attributedBody`, an NSAttributedString
|
|
38
|
+
* archived with NSArchiver (typedstream). Since macOS 13 many rows have a
|
|
39
|
+
* null `text` column and carry the text only here.
|
|
40
|
+
*
|
|
41
|
+
* Layout around the string: the class name "NSString", five bytes of
|
|
42
|
+
* typedstream framing ending in 0x2B ('+'), then a length and the UTF-8
|
|
43
|
+
* bytes. The length is one byte, or 0x81 followed by a little-endian uint16,
|
|
44
|
+
* or 0x82 followed by a little-endian uint32.
|
|
45
|
+
*/
|
|
46
|
+
export function decodeAttributedBody(blob: Uint8Array | null | undefined): string | null {
|
|
47
|
+
if (!blob || blob.length === 0) return null;
|
|
48
|
+
const bytes = blob instanceof Uint8Array ? blob : new Uint8Array(blob);
|
|
49
|
+
const marker = new TextEncoder().encode("NSString");
|
|
50
|
+
const start = indexOf(bytes, marker);
|
|
51
|
+
if (start === -1) return null;
|
|
52
|
+
let i = start + marker.length;
|
|
53
|
+
// Find the '+' that introduces the string payload within the framing bytes.
|
|
54
|
+
const plus = bytes.indexOf(0x2b, i);
|
|
55
|
+
if (plus === -1 || plus - i > 8) return null;
|
|
56
|
+
i = plus + 1;
|
|
57
|
+
if (i >= bytes.length) return null;
|
|
58
|
+
let length = bytes[i];
|
|
59
|
+
i += 1;
|
|
60
|
+
if (length === 0x81) {
|
|
61
|
+
if (i + 2 > bytes.length) return null;
|
|
62
|
+
length = bytes[i] | (bytes[i + 1] << 8);
|
|
63
|
+
i += 2;
|
|
64
|
+
} else if (length === 0x82) {
|
|
65
|
+
if (i + 4 > bytes.length) return null;
|
|
66
|
+
length = (bytes[i] | (bytes[i + 1] << 8) | (bytes[i + 2] << 16) | (bytes[i + 3] << 24)) >>> 0;
|
|
67
|
+
i += 4;
|
|
68
|
+
}
|
|
69
|
+
if (length === 0 || i + length > bytes.length) return null;
|
|
70
|
+
const text = new TextDecoder("utf-8", { fatal: false }).decode(bytes.subarray(i, i + length));
|
|
71
|
+
return text.replace(//g, "").trim() || null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function indexOf(haystack: Uint8Array, needle: Uint8Array): number {
|
|
75
|
+
outer: for (let i = 0; i <= haystack.length - needle.length; i++) {
|
|
76
|
+
for (let j = 0; j < needle.length; j++) {
|
|
77
|
+
if (haystack[i + j] !== needle[j]) continue outer;
|
|
78
|
+
}
|
|
79
|
+
return i;
|
|
80
|
+
}
|
|
81
|
+
return -1;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** The newest ROWID, used as the starting watermark so old history is never replayed. */
|
|
85
|
+
export function latestRowId(db: Database): number {
|
|
86
|
+
const row = db.query("SELECT COALESCE(MAX(ROWID), 0) AS max FROM message").get() as { max: number };
|
|
87
|
+
return Number(row.max) || 0;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
interface RawRow {
|
|
91
|
+
rowId: number;
|
|
92
|
+
guid: string;
|
|
93
|
+
text: string | null;
|
|
94
|
+
attributedBody: Uint8Array | null;
|
|
95
|
+
date: number | bigint | null;
|
|
96
|
+
service: string | null;
|
|
97
|
+
handle: string | null;
|
|
98
|
+
chatStyle: number | null;
|
|
99
|
+
participants: number | null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Inbound messages after `afterRowId`, oldest first. Skips the owner's own
|
|
104
|
+
* sent messages, tapbacks and other reactions (associated_message_type != 0),
|
|
105
|
+
* and rows with no sender handle. Group-chat rows are returned with
|
|
106
|
+
* `isGroup: true` so the caller can advance past them.
|
|
107
|
+
*/
|
|
108
|
+
export function readNewMessages(db: Database, afterRowId: number, limit = 100): ChatDbMessage[] {
|
|
109
|
+
const rows = db
|
|
110
|
+
.query(
|
|
111
|
+
`SELECT m.ROWID AS rowId, m.guid AS guid, m.text AS text, m.attributedBody AS attributedBody,
|
|
112
|
+
m.date AS date, m.service AS service, h.id AS handle,
|
|
113
|
+
c.style AS chatStyle,
|
|
114
|
+
(SELECT COUNT(*) FROM chat_handle_join chj WHERE chj.chat_id = c.ROWID) AS participants
|
|
115
|
+
FROM message m
|
|
116
|
+
LEFT JOIN handle h ON h.ROWID = m.handle_id
|
|
117
|
+
LEFT JOIN chat_message_join cmj ON cmj.message_id = m.ROWID
|
|
118
|
+
LEFT JOIN chat c ON c.ROWID = cmj.chat_id
|
|
119
|
+
WHERE m.ROWID > ?1
|
|
120
|
+
AND m.is_from_me = 0
|
|
121
|
+
AND COALESCE(m.associated_message_type, 0) = 0
|
|
122
|
+
GROUP BY m.ROWID
|
|
123
|
+
ORDER BY m.ROWID ASC
|
|
124
|
+
LIMIT ?2`,
|
|
125
|
+
)
|
|
126
|
+
.all(afterRowId, limit) as RawRow[];
|
|
127
|
+
|
|
128
|
+
const out: ChatDbMessage[] = [];
|
|
129
|
+
for (const r of rows) {
|
|
130
|
+
const text = r.text && r.text.trim() !== "" ? r.text : decodeAttributedBody(r.attributedBody);
|
|
131
|
+
out.push({
|
|
132
|
+
rowId: Number(r.rowId),
|
|
133
|
+
guid: String(r.guid),
|
|
134
|
+
handle: r.handle ?? "",
|
|
135
|
+
text: (text ?? "").replace(//g, "").trim(),
|
|
136
|
+
sentAt: appleDateToIso(r.date),
|
|
137
|
+
service: r.service ?? "iMessage",
|
|
138
|
+
// chat.style 43 is a group chat, 45 a one-to-one chat.
|
|
139
|
+
isGroup: r.chatStyle === 43 || Number(r.participants ?? 0) > 1,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
return out;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* The new watermark after a sync. Walks the batch in ROWID order and stops at
|
|
147
|
+
* the first message that was sent to the server but not accepted, so that
|
|
148
|
+
* message is read again next time. Rows the relay skipped (group chats,
|
|
149
|
+
* reactions without text) are passed over.
|
|
150
|
+
*/
|
|
151
|
+
export function advanceWatermark(
|
|
152
|
+
current: number,
|
|
153
|
+
rows: { rowId: number; guid: string }[],
|
|
154
|
+
sent: { guid: string }[],
|
|
155
|
+
accepted: string[],
|
|
156
|
+
): number {
|
|
157
|
+
const sentGuids = new Set(sent.map((m) => m.guid));
|
|
158
|
+
const acceptedGuids = new Set(accepted);
|
|
159
|
+
let watermark = current;
|
|
160
|
+
for (const r of rows) {
|
|
161
|
+
if (sentGuids.has(r.guid) && !acceptedGuids.has(r.guid)) break;
|
|
162
|
+
if (r.rowId > watermark) watermark = r.rowId;
|
|
163
|
+
}
|
|
164
|
+
return watermark;
|
|
165
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// HTTP client for the server's relaySync endpoint.
|
|
2
|
+
|
|
3
|
+
import type { RelaySyncRequest, RelaySyncResponse } from "../lib/relay-protocol";
|
|
4
|
+
|
|
5
|
+
export type FetchLike = (url: string, init: RequestInit) => Promise<Response>;
|
|
6
|
+
|
|
7
|
+
export class RelayHttpError extends Error {
|
|
8
|
+
constructor(
|
|
9
|
+
readonly status: number,
|
|
10
|
+
readonly code: string,
|
|
11
|
+
message: string,
|
|
12
|
+
) {
|
|
13
|
+
super(message);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function relaySync(
|
|
18
|
+
serverUrl: string,
|
|
19
|
+
token: string,
|
|
20
|
+
body: RelaySyncRequest,
|
|
21
|
+
fetchImpl: FetchLike = fetch,
|
|
22
|
+
): Promise<RelaySyncResponse> {
|
|
23
|
+
const url = new URL("/api/fn/relaySync", serverUrl);
|
|
24
|
+
const res = await fetchImpl(url.toString(), {
|
|
25
|
+
method: "POST",
|
|
26
|
+
headers: { "content-type": "application/json", authorization: `Bearer ${token}` },
|
|
27
|
+
body: JSON.stringify(body),
|
|
28
|
+
signal: AbortSignal.timeout(20_000),
|
|
29
|
+
});
|
|
30
|
+
const json = (await res.json().catch(() => null)) as
|
|
31
|
+
| (RelaySyncResponse & { error?: { code?: string; message?: string } })
|
|
32
|
+
| null;
|
|
33
|
+
if (!res.ok || !json || json.error) {
|
|
34
|
+
const code = json?.error?.code ?? `HTTP_${res.status}`;
|
|
35
|
+
throw new RelayHttpError(res.status, code, json?.error?.message ?? `relaySync failed with HTTP ${res.status}`);
|
|
36
|
+
}
|
|
37
|
+
return {
|
|
38
|
+
accepted: Array.isArray(json.accepted) ? json.accepted.map(String) : [],
|
|
39
|
+
outbound: Array.isArray(json.outbound) ? json.outbound : [],
|
|
40
|
+
};
|
|
41
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
// iMessage relay for a Mac that stays on. Run with `bun relay/main.ts` (or
|
|
2
|
+
// `bun run relay` from the project root). See the README's "Mac relay" section
|
|
3
|
+
// for the Full Disk Access and Automation permissions it needs.
|
|
4
|
+
//
|
|
5
|
+
// Every POLL_MS it:
|
|
6
|
+
// 1. reads chat.db rows newer than its watermark (read-only),
|
|
7
|
+
// 2. sends them to the server along with acks for replies it sent,
|
|
8
|
+
// 3. advances the watermark past the rows the server accepted,
|
|
9
|
+
// 4. sends the replies the server handed back, through Messages.app.
|
|
10
|
+
//
|
|
11
|
+
// Configuration (environment, or relay/.env which Bun loads automatically):
|
|
12
|
+
// PYLON_URL your app's origin, e.g. https://assistant.example.com
|
|
13
|
+
// RELAY_TOKEN same value as RELAY_TOKEN on the server
|
|
14
|
+
// CHAT_DB default ~/Library/Messages/chat.db
|
|
15
|
+
// RELAY_STATE default ~/.pylon-imessage-relay/state.json
|
|
16
|
+
// POLL_MS default 3000, minimum 2500 (the server allows 30 syncs a minute)
|
|
17
|
+
// RELAY_DRY_RUN 1 to log replies instead of sending them
|
|
18
|
+
|
|
19
|
+
import { hostname, homedir } from "node:os";
|
|
20
|
+
import { join } from "node:path";
|
|
21
|
+
import { normalizeHandle } from "../lib/handles";
|
|
22
|
+
import { MAX_INBOUND_PER_SYNC, type RelayAck, type RelayInboundItem } from "../lib/relay-protocol";
|
|
23
|
+
import { sendIMessage } from "./applescript";
|
|
24
|
+
import { advanceWatermark, latestRowId, openChatDb, readNewMessages } from "./chatdb";
|
|
25
|
+
import { RelayHttpError, relaySync } from "./client";
|
|
26
|
+
import { loadState, saveState, type RelayState } from "./state";
|
|
27
|
+
|
|
28
|
+
const VERSION = "1.0.0";
|
|
29
|
+
|
|
30
|
+
function config() {
|
|
31
|
+
const serverUrl = (process.env.PYLON_URL ?? "").trim();
|
|
32
|
+
const token = (process.env.RELAY_TOKEN ?? "").trim();
|
|
33
|
+
if (!/^https?:\/\//.test(serverUrl)) throw new Error("Set PYLON_URL to your app's origin, e.g. https://assistant.example.com");
|
|
34
|
+
if (serverUrl.startsWith("http://") && !/^http:\/\/(localhost|127\.0\.0\.1)(:|\/|$)/.test(serverUrl)) {
|
|
35
|
+
throw new Error("PYLON_URL must use https:// (http:// is allowed only for localhost)");
|
|
36
|
+
}
|
|
37
|
+
if (token.length < 24) throw new Error("Set RELAY_TOKEN (at least 24 characters, same value as the server)");
|
|
38
|
+
return {
|
|
39
|
+
serverUrl,
|
|
40
|
+
token,
|
|
41
|
+
chatDb: process.env.CHAT_DB || join(homedir(), "Library", "Messages", "chat.db"),
|
|
42
|
+
statePath: process.env.RELAY_STATE || join(homedir(), ".pylon-imessage-relay", "state.json"),
|
|
43
|
+
pollMs: Math.max(2500, Number(process.env.POLL_MS) || 3000),
|
|
44
|
+
dryRun: ["1", "true", "yes"].includes((process.env.RELAY_DRY_RUN ?? "").toLowerCase()),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const log = (msg: string) => console.log(`[relay ${new Date().toISOString()}] ${msg}`);
|
|
49
|
+
|
|
50
|
+
async function main() {
|
|
51
|
+
const cfg = config();
|
|
52
|
+
let db;
|
|
53
|
+
try {
|
|
54
|
+
db = openChatDb(cfg.chatDb);
|
|
55
|
+
} catch (err) {
|
|
56
|
+
throw new Error(
|
|
57
|
+
`Cannot open ${cfg.chatDb}. Grant Full Disk Access to the app running this script (see README). ${err instanceof Error ? err.message : err}`,
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
let state: RelayState = loadState(cfg.statePath) ?? { lastRowId: latestRowId(db), sentUnacked: [] };
|
|
62
|
+
saveState(cfg.statePath, state);
|
|
63
|
+
log(`started v${VERSION}; watermark ROWID ${state.lastRowId}${cfg.dryRun ? "; dry run, replies are not sent" : ""}`);
|
|
64
|
+
|
|
65
|
+
// Acks for replies sent in a previous loop (or before a restart).
|
|
66
|
+
let acks: RelayAck[] = state.sentUnacked.map((id) => ({ id, ok: true, dryRun: cfg.dryRun }));
|
|
67
|
+
let stopping = false;
|
|
68
|
+
process.on("SIGINT", () => (stopping = true));
|
|
69
|
+
process.on("SIGTERM", () => (stopping = true));
|
|
70
|
+
|
|
71
|
+
while (!stopping) {
|
|
72
|
+
try {
|
|
73
|
+
const rows = readNewMessages(db, state.lastRowId, MAX_INBOUND_PER_SYNC);
|
|
74
|
+
const inbound: RelayInboundItem[] = [];
|
|
75
|
+
for (const r of rows) {
|
|
76
|
+
const handle = normalizeHandle(r.handle)?.handle;
|
|
77
|
+
if (r.isGroup || !handle || r.text === "") continue;
|
|
78
|
+
inbound.push({ guid: r.guid, handle, text: r.text, sentAt: r.sentAt, service: r.service });
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const res = await relaySync(cfg.serverUrl, cfg.token, {
|
|
82
|
+
relayVersion: VERSION,
|
|
83
|
+
host: hostname().slice(0, 64),
|
|
84
|
+
inbound,
|
|
85
|
+
acks,
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// The server stored the acks; forget them.
|
|
89
|
+
const ackedIds = new Set(acks.map((a) => a.id));
|
|
90
|
+
state = { ...state, sentUnacked: state.sentUnacked.filter((id) => !ackedIds.has(id)) };
|
|
91
|
+
acks = [];
|
|
92
|
+
|
|
93
|
+
state = { ...state, lastRowId: advanceWatermark(state.lastRowId, rows, inbound, res.accepted) };
|
|
94
|
+
saveState(cfg.statePath, state);
|
|
95
|
+
if (inbound.length > 0) log(`delivered ${inbound.length} inbound message(s)`);
|
|
96
|
+
|
|
97
|
+
for (const item of res.outbound) {
|
|
98
|
+
if (state.sentUnacked.includes(item.id)) {
|
|
99
|
+
acks.push({ id: item.id, ok: true, dryRun: cfg.dryRun });
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
const handle = normalizeHandle(item.handle)?.handle;
|
|
103
|
+
const result = handle
|
|
104
|
+
? await sendIMessage(handle, item.text, { dryRun: cfg.dryRun })
|
|
105
|
+
: ({ ok: false, error: "invalid recipient" } as const);
|
|
106
|
+
if (result.ok) {
|
|
107
|
+
// Persist before acking so a crash here never sends twice.
|
|
108
|
+
state = { ...state, sentUnacked: [...state.sentUnacked, item.id] };
|
|
109
|
+
saveState(cfg.statePath, state);
|
|
110
|
+
acks.push({ id: item.id, ok: true, dryRun: cfg.dryRun });
|
|
111
|
+
} else {
|
|
112
|
+
acks.push({ id: item.id, ok: false, error: result.error });
|
|
113
|
+
log(`send failed: ${result.error}`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
if (res.outbound.length > 0) log(`${cfg.dryRun ? "dry run: skipped" : "sent"} ${res.outbound.length} repl${res.outbound.length === 1 ? "y" : "ies"}`);
|
|
117
|
+
} catch (err) {
|
|
118
|
+
if (err instanceof RelayHttpError && (err.code === "UNAUTHORIZED" || err.code === "NOT_CONFIGURED")) {
|
|
119
|
+
log(`server rejected the relay (${err.code}). Check RELAY_TOKEN on both sides.`);
|
|
120
|
+
} else if (err instanceof RelayHttpError && err.code === "TRANSPORT_DISABLED") {
|
|
121
|
+
log("server is not using the relay transport. Set IMESSAGE_TRANSPORT=relay on the server.");
|
|
122
|
+
} else {
|
|
123
|
+
log(`sync failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
await Bun.sleep(cfg.pollMs);
|
|
127
|
+
}
|
|
128
|
+
db.close();
|
|
129
|
+
log("stopped");
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (import.meta.main) {
|
|
133
|
+
main().catch((err) => {
|
|
134
|
+
console.error(`[relay] ${err instanceof Error ? err.message : String(err)}`);
|
|
135
|
+
process.exit(1);
|
|
136
|
+
});
|
|
137
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// The relay's small on-disk state: the chat.db ROWID watermark and the ids of
|
|
2
|
+
// replies it already sent but has not had acknowledged yet. Written with a
|
|
3
|
+
// temp file + rename, so a crash mid-write leaves the previous state intact.
|
|
4
|
+
|
|
5
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
6
|
+
import { dirname } from "node:path";
|
|
7
|
+
|
|
8
|
+
export interface RelayState {
|
|
9
|
+
/** Highest chat.db ROWID the server has accepted. */
|
|
10
|
+
lastRowId: number;
|
|
11
|
+
/** Replies sent through Messages.app whose ack the server has not received. */
|
|
12
|
+
sentUnacked: string[];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function loadState(path: string): RelayState | null {
|
|
16
|
+
if (!existsSync(path)) return null;
|
|
17
|
+
try {
|
|
18
|
+
const raw = JSON.parse(readFileSync(path, "utf8")) as Partial<RelayState>;
|
|
19
|
+
const lastRowId = Number(raw.lastRowId);
|
|
20
|
+
if (!Number.isFinite(lastRowId) || lastRowId < 0) return null;
|
|
21
|
+
return {
|
|
22
|
+
lastRowId,
|
|
23
|
+
sentUnacked: Array.isArray(raw.sentUnacked) ? raw.sentUnacked.filter((s) => typeof s === "string") : [],
|
|
24
|
+
};
|
|
25
|
+
} catch {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function saveState(path: string, state: RelayState): void {
|
|
31
|
+
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
|
32
|
+
const tmp = `${path}.${process.pid}.tmp`;
|
|
33
|
+
writeFileSync(tmp, JSON.stringify(state), { mode: 0o600 });
|
|
34
|
+
renameSync(tmp, path);
|
|
35
|
+
}
|