@agent-native/core 0.138.0 → 0.140.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/server/lib/dashboard-report.ts +2 -0
  3. package/corpus/templates/analytics/server/lib/emails.ts +51 -0
  4. package/corpus/templates/analytics/server/plugins/transactional-emails.ts +5 -0
  5. package/corpus/templates/calendar/server/lib/booking-emails.ts +154 -70
  6. package/corpus/templates/calendar/server/lib/emails.ts +167 -0
  7. package/corpus/templates/calendar/server/lib/event-guest-notifications.ts +64 -28
  8. package/corpus/templates/calendar/server/plugins/transactional-emails.ts +5 -0
  9. package/corpus/templates/clips/actions/invite-member.ts +39 -15
  10. package/corpus/templates/clips/server/lib/emails.ts +256 -0
  11. package/corpus/templates/clips/server/lib/transactional-email-templates.ts +24 -0
  12. package/corpus/templates/clips/server/plugins/transactional-email-catalog.ts +5 -0
  13. package/corpus/templates/content/server/lib/comment-notifications.ts +57 -26
  14. package/corpus/templates/content/server/lib/emails.ts +70 -0
  15. package/corpus/templates/content/server/plugins/transactional-emails.ts +5 -0
  16. package/corpus/templates/design/.agents/skills/design-systems/SKILL.md +44 -1
  17. package/corpus/templates/design/.generated/bridge/editor-chrome.generated.ts +265 -1
  18. package/corpus/templates/design/actions/create-design-system.ts +9 -10
  19. package/corpus/templates/design/actions/get-design-system.ts +53 -0
  20. package/corpus/templates/design/actions/import-design-tokens.ts +5 -71
  21. package/corpus/templates/design/actions/index-design-tokens.ts +61 -80
  22. package/corpus/templates/design/actions/update-design-system.ts +7 -10
  23. package/corpus/templates/design/app/components/design/EditPanel.tsx +45 -2
  24. package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +12 -0
  25. package/corpus/templates/design/app/components/design/edit-panel/design-system-swatches.ts +215 -0
  26. package/corpus/templates/design/app/components/design/edit-panel/document-colors.ts +10 -6
  27. package/corpus/templates/design/app/components/design/edit-panel/effects-properties.tsx +19 -3
  28. package/corpus/templates/design/app/components/design/edit-panel/fill-properties.tsx +36 -10
  29. package/corpus/templates/design/app/components/design/edit-panel/interaction-state-helpers.ts +15 -0
  30. package/corpus/templates/design/app/components/design/edit-panel/panel-primitives.tsx +19 -1
  31. package/corpus/templates/design/app/components/design/edit-panel/position-helpers.ts +6 -0
  32. package/corpus/templates/design/app/components/design/edit-panel/stroke-properties.tsx +53 -10
  33. package/corpus/templates/design/app/components/design/inspector/DesignColorPicker.tsx +142 -4
  34. package/corpus/templates/design/app/components/design/types.ts +6 -0
  35. package/corpus/templates/design/app/i18n-data.ts +20 -0
  36. package/corpus/templates/design/app/pages/DesignSystemSetup.tsx +1 -1
  37. package/corpus/templates/design/app/pages/DesignSystems.tsx +78 -1
  38. package/corpus/templates/design/app/pages/design-editor/html-layer-positioning.ts +6 -1
  39. package/corpus/templates/design/changelog/2026-08-04-design-system-tokens-show-by-name-in-the-editor.md +6 -0
  40. package/corpus/templates/design/e2e/helpers.ts +17 -0
  41. package/corpus/templates/design/server/lib/design-system-tokens.ts +35 -0
  42. package/corpus/templates/design/shared/authored-color-styles.ts +401 -0
  43. package/corpus/templates/design/shared/resolve-tweaks.ts +10 -18
  44. package/corpus/templates/dispatch/app/routes/transactional-email.$appId.$id.tsx +4 -0
  45. package/corpus/templates/dispatch/app/routes/transactional-email.tsx +4 -0
  46. package/corpus/templates/forms/server/lib/emails.ts +52 -0
  47. package/corpus/templates/forms/server/lib/response-email.ts +2 -0
  48. package/corpus/templates/forms/server/plugins/transactional-emails.ts +5 -0
  49. package/corpus/templates/plan/actions/request-plan-access.ts +33 -12
  50. package/corpus/templates/plan/server/lib/comment-notifications.ts +53 -27
  51. package/corpus/templates/plan/server/lib/emails.ts +67 -0
  52. package/corpus/templates/plan/server/plugins/transactional-emails.ts +5 -0
  53. package/corpus/templates/slides/server/lib/comment-notifications.ts +44 -21
  54. package/corpus/templates/slides/server/lib/emails.ts +40 -0
  55. package/corpus/templates/slides/server/plugins/transactional-emails.ts +5 -0
  56. package/dist/brand-kit/index.d.ts +1 -0
  57. package/dist/brand-kit/index.js +1 -0
  58. package/dist/brand-kit/tokens.d.ts +92 -0
  59. package/dist/brand-kit/tokens.js +280 -0
  60. package/dist/brand-kit/types.d.ts +25 -0
  61. package/dist/collab/awareness.d.ts +2 -2
  62. package/dist/collab/routes.d.ts +2 -2
  63. package/dist/email-catalog/actions/list-email-activity.d.ts +6 -0
  64. package/dist/email-catalog/actions/list-email-activity.js +15 -0
  65. package/dist/email-catalog/actions/list-email-engagement.d.ts +6 -0
  66. package/dist/email-catalog/actions/list-email-engagement.js +15 -0
  67. package/dist/email-catalog/actions/list-email-log.d.ts +8 -0
  68. package/dist/email-catalog/actions/list-email-log.js +22 -0
  69. package/dist/email-catalog/actions/list-transactional-emails.d.ts +23 -0
  70. package/dist/email-catalog/actions/list-transactional-emails.js +61 -0
  71. package/dist/email-catalog/actions/render-transactional-email-preview.d.ts +10 -0
  72. package/dist/email-catalog/actions/render-transactional-email-preview.js +24 -0
  73. package/dist/email-catalog/authorize.d.ts +2 -0
  74. package/dist/email-catalog/authorize.js +10 -0
  75. package/dist/email-catalog/log.d.ts +57 -0
  76. package/dist/email-catalog/log.js +117 -0
  77. package/dist/email-catalog/provider-metrics.d.ts +34 -0
  78. package/dist/email-catalog/provider-metrics.js +165 -0
  79. package/dist/email-catalog/registry.d.ts +79 -0
  80. package/dist/email-catalog/registry.js +71 -0
  81. package/dist/email-catalog/schema.d.ts +212 -0
  82. package/dist/email-catalog/schema.js +49 -0
  83. package/dist/email-catalog/system-emails.d.ts +14 -0
  84. package/dist/email-catalog/system-emails.js +68 -0
  85. package/dist/localization/default-messages.d.ts +45 -0
  86. package/dist/localization/default-messages.js +45 -0
  87. package/dist/notifications/routes.d.ts +6 -6
  88. package/dist/org/handlers.js +8 -1
  89. package/dist/progress/routes.d.ts +1 -1
  90. package/dist/provider-api/actions/custom-provider-registration.d.ts +7 -7
  91. package/dist/provider-api/actions/provider-api.d.ts +7 -7
  92. package/dist/provider-api/corpus-jobs.d.ts +2 -2
  93. package/dist/resources/handlers.d.ts +1 -1
  94. package/dist/server/action-discovery.js +22 -0
  95. package/dist/server/better-auth-instance.js +17 -2
  96. package/dist/server/email.d.ts +9 -0
  97. package/dist/server/email.js +44 -3
  98. package/dist/server/index.d.ts +1 -0
  99. package/dist/server/index.js +1 -0
  100. package/package.json +3 -1
@@ -24,6 +24,7 @@ import { and, eq, sql } from "drizzle-orm";
24
24
  import { z } from "zod";
25
25
 
26
26
  import { getDb } from "../server/db/index.js";
27
+ import { CLIPS_ORGANIZATION_INVITE_EMAIL_ID } from "../server/lib/emails.js";
27
28
  import {
28
29
  getCurrentOwnerEmail,
29
30
  nanoid,
@@ -67,6 +68,36 @@ async function fetchOrgName(orgId: string): Promise<string> {
67
68
  return row?.name ?? "Organization";
68
69
  }
69
70
 
71
+ export function renderClipsInviteEmail({
72
+ appName,
73
+ orgName,
74
+ inviter,
75
+ role,
76
+ inviteUrl,
77
+ }: {
78
+ appName: string;
79
+ orgName: string;
80
+ inviter: string;
81
+ role: "admin" | "member";
82
+ inviteUrl: string;
83
+ }) {
84
+ return {
85
+ subject: `You're invited to ${orgName} on ${appName}`,
86
+ ...renderEmail({
87
+ brandName: appName,
88
+ preheader: `${inviter} invited you to ${orgName} on ${appName}.`,
89
+ heading: `You're invited to join ${orgName}`,
90
+ paragraphs: [
91
+ `${emailStrong(inviter)} invited you to the ${emailStrong(orgName)} organization on ${emailStrong(appName)} as ${emailStrong(role)}.`,
92
+ `Click the button below to accept the invite and start collaborating.`,
93
+ ],
94
+ cta: { label: "Accept invite", url: inviteUrl },
95
+ // guard:allow-raw-color — email HTML cannot reference CSS theme tokens; mail clients don't support custom properties.
96
+ brandColor: "#18181B",
97
+ }),
98
+ };
99
+ }
100
+
70
101
  export default defineAction({
71
102
  description:
72
103
  "Invite someone to the active organization by email. Creates a pending invitation. Role 'admin' maps to admin; all other Clips roles collapse to 'member'. Sends an email when a provider is configured.",
@@ -123,24 +154,17 @@ export default defineAction({
123
154
  const orgName = await fetchOrgName(organizationId);
124
155
  const inviteUrl = `${baseUrl()}/invite/${token}`;
125
156
 
126
- const appName = getAppName();
127
- const { html, text } = renderEmail({
128
- brandName: appName,
129
- preheader: `${inviter} invited you to ${orgName} on ${appName}.`,
130
- heading: `You're invited to join ${orgName}`,
131
- paragraphs: [
132
- `${emailStrong(inviter)} invited you to the ${emailStrong(orgName)} organization on ${emailStrong(appName)} as ${emailStrong(role)}.`,
133
- `Click the button below to accept the invite and start collaborating.`,
134
- ],
135
- cta: { label: "Accept invite", url: inviteUrl },
136
- brandColor: "#18181B",
137
- });
138
157
  try {
139
158
  await sendEmail({
159
+ ...renderClipsInviteEmail({
160
+ appName: getAppName(),
161
+ orgName,
162
+ inviter,
163
+ role,
164
+ inviteUrl,
165
+ }),
140
166
  to: args.email,
141
- subject: `You're invited to ${orgName} on ${appName}`,
142
- html,
143
- text,
167
+ templateId: CLIPS_ORGANIZATION_INVITE_EMAIL_ID,
144
168
  });
145
169
  } catch (err) {
146
170
  console.warn("[invite-member] email send failed:", err);
@@ -0,0 +1,256 @@
1
+ /**
2
+ * Catalog entries for the transactional emails Clips sends.
3
+ *
4
+ * Registered from `server/plugins/transactional-emails.ts` so Dispatch can list
5
+ * and preview them without the app having sent anything yet.
6
+ *
7
+ * Every entry except the organization invite renders through
8
+ * `renderClipsTransactionalEmail`, so a preview shows the real template rather
9
+ * than a copy of it.
10
+ */
11
+
12
+ import { defineTransactionalEmail } from "@agent-native/core/email-catalog";
13
+
14
+ import { renderClipsInviteEmail } from "../../actions/invite-member.js";
15
+ import {
16
+ CLIPS_ACTIVITY_COMMENT_EMAIL_ID,
17
+ CLIPS_ACTIVITY_REACTION_EMAIL_ID,
18
+ CLIPS_FIRST_AGENT_VIEW_EMAIL_ID,
19
+ CLIPS_FIRST_IMPORT_EMAIL_ID,
20
+ CLIPS_FIRST_VIEW_EMAIL_ID,
21
+ CLIPS_MONTHLY_RECAP_EMAIL_ID,
22
+ CLIPS_TWO_CLIPS_EMAIL_ID,
23
+ CLIPS_UNVIEWED_REMINDER_EMAIL_ID,
24
+ renderClipsTransactionalEmail,
25
+ type ClipsTransactionalEmailInput,
26
+ type ClipsTransactionalEmailRenderOptions,
27
+ } from "./transactional-email-templates.js";
28
+
29
+ /** Obviously-fake sample data — these render in a preview pane, never send. */
30
+ const PREVIEW_OPTIONS: ClipsTransactionalEmailRenderOptions = {
31
+ appUrl: "https://example.com",
32
+ };
33
+ const SAMPLE_RECORDING_ID = "rec_sample";
34
+ const SAMPLE_TITLE = "Onboarding walkthrough";
35
+ const SAMPLE_TO = "sam.rivera@example.com";
36
+
37
+ function preview(input: ClipsTransactionalEmailInput) {
38
+ return renderClipsTransactionalEmail(input, PREVIEW_OPTIONS);
39
+ }
40
+
41
+ export const CLIPS_ORGANIZATION_INVITE_EMAIL_ID = "clips.organization-invite";
42
+
43
+ /**
44
+ * How the shared Clips sender resolves From and Reply-To for every kind it
45
+ * renders, so each entry can say so without restating the mechanism.
46
+ */
47
+ const CLIPS_SENDER =
48
+ 'From is the configured EMAIL_FROM with the display name "Agent-Native Clips"; on first-party agent-native.com deployments it becomes clips@agent-native.com. Reply-to is hello@agent-native.com.';
49
+
50
+ let registered = false;
51
+
52
+ export function registerClipsEmails(): void {
53
+ if (registered) return;
54
+ registered = true;
55
+
56
+ defineTransactionalEmail({
57
+ id: CLIPS_FIRST_VIEW_EMAIL_ID,
58
+ name: "First view on a Clip",
59
+ trigger:
60
+ "The background transactional-email sweep finds the first counted view of a Clip by someone other than its owner, recorded after transactional email was switched on. One per Clip.",
61
+ recipientLabel: "Clip owner",
62
+ recipient:
63
+ "The Clip's owner. Suppressed recipient addresses are skipped, and the send is dropped if the owner is no longer the recording's owner at send time.",
64
+ senderLabel: "Agent-Native Clips",
65
+ sender: CLIPS_SENDER,
66
+ preview: () =>
67
+ preview({
68
+ kind: "first-view",
69
+ to: SAMPLE_TO,
70
+ recordingId: SAMPLE_RECORDING_ID,
71
+ title: SAMPLE_TITLE,
72
+ viewerEmail: "alex.chen@example.com",
73
+ }),
74
+ });
75
+
76
+ defineTransactionalEmail({
77
+ id: CLIPS_UNVIEWED_REMINDER_EMAIL_ID,
78
+ name: "Unviewed Clip reminder",
79
+ trigger:
80
+ "A direct share is 48 hours old and the recipient still has no counted view of that Clip. Re-checked at send time, so a view in the meantime cancels it.",
81
+ recipientLabel: "Shared-with address",
82
+ recipient:
83
+ "The address the Clip was directly shared with. Requires the share to still exist.",
84
+ senderLabel: "Sharer via Clips",
85
+ sender:
86
+ 'Same as the other Clips emails, except the display name becomes "<sharer> (via Agent-Native Clips)" and reply-to is the sharer\'s own address when it is a valid address, falling back to hello@agent-native.com.',
87
+ preview: () =>
88
+ preview({
89
+ kind: "unviewed-reminder",
90
+ to: SAMPLE_TO,
91
+ recordingId: SAMPLE_RECORDING_ID,
92
+ title: SAMPLE_TITLE,
93
+ senderEmail: "dana.hill@example.com",
94
+ senderName: "Dana Hill",
95
+ }),
96
+ });
97
+
98
+ defineTransactionalEmail({
99
+ id: CLIPS_FIRST_AGENT_VIEW_EMAIL_ID,
100
+ name: "First agent read of a Clip",
101
+ trigger:
102
+ "An AI agent reads one of an owner's Clips for the first time since transactional email was switched on. One per owner, not per Clip.",
103
+ recipientLabel: "Clip owner",
104
+ recipient:
105
+ "The owner of the Clip the agent read. Re-checked at send time against the owner's actual first agent view.",
106
+ senderLabel: "Agent-Native Clips",
107
+ sender: CLIPS_SENDER,
108
+ preview: () =>
109
+ preview({
110
+ kind: "first-agent-view",
111
+ to: SAMPLE_TO,
112
+ recordingId: SAMPLE_RECORDING_ID,
113
+ title: SAMPLE_TITLE,
114
+ agentName: "Claude Code",
115
+ }),
116
+ });
117
+
118
+ defineTransactionalEmail({
119
+ id: CLIPS_FIRST_IMPORT_EMAIL_ID,
120
+ name: "First imported video is ready",
121
+ trigger:
122
+ "An owner's first imported (rather than recorded) video finishes processing and becomes ready. One per owner.",
123
+ recipientLabel: "Clip owner",
124
+ recipient:
125
+ "The owner of the imported recording, re-checked at send time so a later import does not resend.",
126
+ senderLabel: "Agent-Native Clips",
127
+ sender: CLIPS_SENDER,
128
+ preview: () =>
129
+ preview({
130
+ kind: "first-import",
131
+ to: SAMPLE_TO,
132
+ recordingId: SAMPLE_RECORDING_ID,
133
+ title: "Quarterly demo recording",
134
+ }),
135
+ });
136
+
137
+ defineTransactionalEmail({
138
+ id: CLIPS_MONTHLY_RECAP_EMAIL_ID,
139
+ name: "Monthly Clips recap",
140
+ trigger:
141
+ "Once per owner per calendar month, from 14:00 UTC on the 1st, for owners whose Clips had any human views or agent reads in the closed month. Metrics and the top Clip are recomputed at send time.",
142
+ recipientLabel: "Clip owner",
143
+ recipient:
144
+ "The Clip owner whose audience the recap reports. Suppressed addresses are skipped.",
145
+ senderLabel: "Agent-Native Clips",
146
+ sender: CLIPS_SENDER,
147
+ preview: () =>
148
+ preview({
149
+ kind: "monthly-recap",
150
+ to: SAMPLE_TO,
151
+ month: "2025-02",
152
+ humanViews: 42,
153
+ agentSessions: 7,
154
+ topClip: {
155
+ recordingId: SAMPLE_RECORDING_ID,
156
+ title: SAMPLE_TITLE,
157
+ thumbnailUrl: "https://example.com/thumbnails/sample.png",
158
+ durationMs: 214_000,
159
+ recordedAt: "2025-02-11T16:00:00.000Z",
160
+ humanViews: 18,
161
+ agentSessions: 4,
162
+ },
163
+ copy: {
164
+ heroLine: "Your clips were watched 42 times. 7 agents read them.",
165
+ completionNote: "68% average completion · most stopped at 2:31",
166
+ agentBreakdown: "4 from Claude Code · 3 unidentified",
167
+ },
168
+ }),
169
+ });
170
+
171
+ defineTransactionalEmail({
172
+ id: CLIPS_TWO_CLIPS_EMAIL_ID,
173
+ name: "Two Clips received",
174
+ trigger:
175
+ "A one-time nudge once someone who owns no Clips of their own has had two distinct Clips directly shared with them. The summary line is AI-generated before the job is released to send.",
176
+ recipientLabel: "Shared-with address",
177
+ recipient:
178
+ "The shared-with address. Dropped at send time if they now own a Clip or no longer hold both shares.",
179
+ senderLabel: "Agent-Native Clips",
180
+ sender: CLIPS_SENDER,
181
+ preview: () =>
182
+ preview({
183
+ kind: "two-clips",
184
+ to: SAMPLE_TO,
185
+ generatedSummary:
186
+ "Two teammates walked you through the new onboarding flow.",
187
+ }),
188
+ });
189
+
190
+ defineTransactionalEmail({
191
+ id: CLIPS_ACTIVITY_COMMENT_EMAIL_ID,
192
+ name: "Clip comment",
193
+ trigger:
194
+ "Someone comments or replies on a Clip. Subject and copy differ slightly for a reply; both send under this id.",
195
+ recipientLabel: "Owner and thread authors",
196
+ recipient:
197
+ "The recording owner, plus every prior author in the thread when the comment is a reply. The list is re-checked against the recording's live ACL and filtered by each user's `emailNotifications` preference; the comment's own author never receives it.",
198
+ senderLabel: "Agent-Native Clips",
199
+ sender: CLIPS_SENDER,
200
+ preview: () =>
201
+ preview({
202
+ kind: "activity-comment",
203
+ to: SAMPLE_TO,
204
+ recordingId: SAMPLE_RECORDING_ID,
205
+ title: SAMPLE_TITLE,
206
+ authorEmail: "alex.chen@example.com",
207
+ authorName: "Alex Chen",
208
+ content: "The step at 1:20 is the part new hires always miss.",
209
+ videoTimestampMs: 80_000,
210
+ isReply: false,
211
+ }),
212
+ });
213
+
214
+ defineTransactionalEmail({
215
+ id: CLIPS_ACTIVITY_REACTION_EMAIL_ID,
216
+ name: "Clip reaction",
217
+ trigger: "A viewer reacts with an emoji on a Clip.",
218
+ recipientLabel: "Clip owner",
219
+ recipient:
220
+ "The recording owner plus any extra recipients the caller passes, re-checked against the recording's live ACL and filtered by each user's `emailNotifications` preference. The reacting viewer never receives it.",
221
+ senderLabel: "Agent-Native Clips",
222
+ sender: CLIPS_SENDER,
223
+ preview: () =>
224
+ preview({
225
+ kind: "activity-reaction",
226
+ to: SAMPLE_TO,
227
+ recordingId: SAMPLE_RECORDING_ID,
228
+ title: SAMPLE_TITLE,
229
+ emoji: "🎉",
230
+ authorEmail: "alex.chen@example.com",
231
+ authorName: "Alex Chen",
232
+ videoTimestampMs: 45_000,
233
+ }),
234
+ });
235
+
236
+ defineTransactionalEmail({
237
+ id: CLIPS_ORGANIZATION_INVITE_EMAIL_ID,
238
+ name: "Organization invitation",
239
+ trigger:
240
+ "An organization admin runs `invite-member`. Any earlier pending invite for the same address is cancelled first, so a re-invite sends a fresh email with a new token.",
241
+ recipientLabel: "Invited address",
242
+ recipient:
243
+ "The address passed to the action, exactly as typed. One email per invite.",
244
+ senderLabel: "Default sender",
245
+ sender:
246
+ "The configured default sender. This call site sets no `from`, `fromName`, `replyTo`, or `appSender`, so unlike the other Clips emails it does not send as Agent-Native Clips.",
247
+ preview: () =>
248
+ renderClipsInviteEmail({
249
+ appName: "Clips",
250
+ orgName: "Northwind Design",
251
+ inviter: "dana.hill@example.com",
252
+ role: "member",
253
+ inviteUrl: "https://example.com/invite/sample-token",
254
+ }),
255
+ });
256
+ }
@@ -86,6 +86,29 @@ export type ClipsTransactionalEmailInput =
86
86
  videoTimestampMs?: number | null;
87
87
  });
88
88
 
89
+ export const CLIPS_FIRST_VIEW_EMAIL_ID = "clips.first-view";
90
+ export const CLIPS_UNVIEWED_REMINDER_EMAIL_ID = "clips.unviewed-reminder";
91
+ export const CLIPS_FIRST_AGENT_VIEW_EMAIL_ID = "clips.first-agent-view";
92
+ export const CLIPS_FIRST_IMPORT_EMAIL_ID = "clips.first-import";
93
+ export const CLIPS_MONTHLY_RECAP_EMAIL_ID = "clips.monthly-recap";
94
+ export const CLIPS_TWO_CLIPS_EMAIL_ID = "clips.two-clips";
95
+ export const CLIPS_ACTIVITY_COMMENT_EMAIL_ID = "clips.activity-comment";
96
+ export const CLIPS_ACTIVITY_REACTION_EMAIL_ID = "clips.activity-reaction";
97
+
98
+ export const CLIPS_EMAIL_ID_BY_KIND: Record<
99
+ ClipsTransactionalEmailInput["kind"],
100
+ string
101
+ > = {
102
+ "first-view": CLIPS_FIRST_VIEW_EMAIL_ID,
103
+ "unviewed-reminder": CLIPS_UNVIEWED_REMINDER_EMAIL_ID,
104
+ "first-agent-view": CLIPS_FIRST_AGENT_VIEW_EMAIL_ID,
105
+ "first-import": CLIPS_FIRST_IMPORT_EMAIL_ID,
106
+ "monthly-recap": CLIPS_MONTHLY_RECAP_EMAIL_ID,
107
+ "two-clips": CLIPS_TWO_CLIPS_EMAIL_ID,
108
+ "activity-comment": CLIPS_ACTIVITY_COMMENT_EMAIL_ID,
109
+ "activity-reaction": CLIPS_ACTIVITY_REACTION_EMAIL_ID,
110
+ };
111
+
89
112
  export interface RecapTopClipInput {
90
113
  recordingId: string;
91
114
  title?: string | null;
@@ -613,6 +636,7 @@ export async function sendClipsTransactionalEmail(
613
636
  ? (validReplyTo(input.senderEmail) ?? FRIENDLY_REPLY_TO)
614
637
  : FRIENDLY_REPLY_TO,
615
638
  timeoutMs: EMAIL_SEND_TIMEOUT_MS,
639
+ templateId: CLIPS_EMAIL_ID_BY_KIND[input.kind],
616
640
  });
617
641
  }
618
642
 
@@ -0,0 +1,5 @@
1
+ import { registerClipsEmails } from "../lib/emails.js";
2
+
3
+ export default function registerTransactionalEmailCatalog(): void {
4
+ registerClipsEmails();
5
+ }
@@ -21,6 +21,10 @@ import { and, eq } from "drizzle-orm";
21
21
 
22
22
  import { CONTENT_USER_PREFS_KEY } from "../../shared/content-user-prefs.js";
23
23
  import { getDb, schema } from "../db/index.js";
24
+ import {
25
+ CONTENT_DOCUMENT_COMMENT_EMAIL_ID,
26
+ CONTENT_DOCUMENT_MENTION_EMAIL_ID,
27
+ } from "./emails.js";
24
28
 
25
29
  export type DocumentCommentNotificationResult = ActivityNotificationResult;
26
30
 
@@ -70,6 +74,48 @@ export interface DocumentCommentNotificationInput {
70
74
  isReply: boolean;
71
75
  }
72
76
 
77
+ export function renderDocumentCommentEmail({
78
+ actor,
79
+ title,
80
+ url,
81
+ content,
82
+ isReply,
83
+ wasMentioned,
84
+ }: {
85
+ actor: string;
86
+ title: string;
87
+ url: string;
88
+ content: string;
89
+ isReply: boolean;
90
+ wasMentioned: boolean;
91
+ }) {
92
+ const lead = wasMentioned
93
+ ? `${emailStrong(actor)} mentioned you in a comment on ${emailStrong(title)}.`
94
+ : isReply
95
+ ? `${emailStrong(actor)} replied in a comment thread on ${emailStrong(title)}.`
96
+ : `${emailStrong(actor)} commented on ${emailStrong(title)}.`;
97
+
98
+ return {
99
+ subject: wasMentioned
100
+ ? `${actor} mentioned you on "${title}"`
101
+ : isReply
102
+ ? `${actor} replied to a comment on "${title}"`
103
+ : `${actor} commented on "${title}"`,
104
+ ...renderEmail({
105
+ preheader: `${actor} commented on ${title}.`,
106
+ heading: wasMentioned
107
+ ? "You were mentioned"
108
+ : isReply
109
+ ? "New reply on your document"
110
+ : "New comment",
111
+ paragraphs: [lead, `"${excerpt(content)}"`],
112
+ cta: { label: "Open document", url },
113
+ footer:
114
+ "You received this because you own, were mentioned in, or participated in this thread. Turn these off in Documents settings.",
115
+ }),
116
+ };
117
+ }
118
+
73
119
  export async function notifyDocumentComment(
74
120
  input: DocumentCommentNotificationInput,
75
121
  ): Promise<DocumentCommentNotificationResult> {
@@ -112,34 +158,19 @@ async function deliverDocumentCommentEmails(
112
158
  logLabel: LOG_LABEL,
113
159
  send: async (to) => {
114
160
  const wasMentioned = mentioned.has(to);
115
- const lead = wasMentioned
116
- ? `${emailStrong(actor)} mentioned you in a comment on ${emailStrong(title)}.`
117
- : input.isReply
118
- ? `${emailStrong(actor)} replied in a comment thread on ${emailStrong(title)}.`
119
- : `${emailStrong(actor)} commented on ${emailStrong(title)}.`;
120
-
121
- const { html, text } = renderEmail({
122
- preheader: `${actor} commented on ${title}.`,
123
- heading: wasMentioned
124
- ? "You were mentioned"
125
- : input.isReply
126
- ? "New reply on your document"
127
- : "New comment",
128
- paragraphs: [lead, `"${excerpt(input.content)}"`],
129
- cta: { label: "Open document", url },
130
- footer:
131
- "You received this because you own, were mentioned in, or participated in this thread. Turn these off in Documents settings.",
132
- });
133
-
134
161
  await sendEmail({
162
+ ...renderDocumentCommentEmail({
163
+ actor,
164
+ title,
165
+ url,
166
+ content: input.content,
167
+ isReply: input.isReply,
168
+ wasMentioned,
169
+ }),
135
170
  to,
136
- subject: wasMentioned
137
- ? `${actor} mentioned you on "${title}"`
138
- : input.isReply
139
- ? `${actor} replied to a comment on "${title}"`
140
- : `${actor} commented on "${title}"`,
141
- html,
142
- text,
171
+ templateId: wasMentioned
172
+ ? CONTENT_DOCUMENT_MENTION_EMAIL_ID
173
+ : CONTENT_DOCUMENT_COMMENT_EMAIL_ID,
143
174
  });
144
175
  },
145
176
  });
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Catalog entries for the transactional emails Documents sends.
3
+ *
4
+ * Registered from `server/plugins/transactional-emails.ts` so Dispatch can list
5
+ * and preview them without the app having sent anything yet.
6
+ */
7
+
8
+ import { defineTransactionalEmail } from "@agent-native/core/email-catalog";
9
+
10
+ import { renderDocumentCommentEmail } from "./comment-notifications.js";
11
+
12
+ /** Obviously-fake sample data — these render in a preview pane, never send. */
13
+ const SAMPLE_TITLE = "Q3 launch brief";
14
+ const SAMPLE_URL = "https://example.com/page/doc_sample";
15
+ const SAMPLE_COMMENT =
16
+ "Can we tighten the second paragraph? It repeats the intro.";
17
+
18
+ export const CONTENT_DOCUMENT_COMMENT_EMAIL_ID = "content.document-comment";
19
+ export const CONTENT_DOCUMENT_MENTION_EMAIL_ID = "content.document-mention";
20
+
21
+ let registered = false;
22
+
23
+ export function registerContentEmails(): void {
24
+ if (registered) return;
25
+ registered = true;
26
+
27
+ defineTransactionalEmail({
28
+ id: CONTENT_DOCUMENT_COMMENT_EMAIL_ID,
29
+ name: "Document comment",
30
+ trigger:
31
+ "Someone posts a comment or a thread reply on a document. Sent to recipients who were not mentioned in it; the copy and subject differ slightly for a reply.",
32
+ recipientLabel: "Owner and thread authors",
33
+ recipient:
34
+ "The document owner, plus every prior author in the thread when the new comment is a reply. The list is re-checked against the document's live ACL and filtered by each user's `emailNotifications` preference; the comment's own author never receives it.",
35
+ senderLabel: "Default sender",
36
+ sender:
37
+ "The configured default sender. This call site sets no `from`, `fromName`, `replyTo`, or `appSender`.",
38
+ preview: () =>
39
+ renderDocumentCommentEmail({
40
+ actor: "Sam Rivera",
41
+ title: SAMPLE_TITLE,
42
+ url: SAMPLE_URL,
43
+ content: SAMPLE_COMMENT,
44
+ isReply: false,
45
+ wasMentioned: false,
46
+ }),
47
+ });
48
+
49
+ defineTransactionalEmail({
50
+ id: CONTENT_DOCUMENT_MENTION_EMAIL_ID,
51
+ name: "Document mention",
52
+ trigger:
53
+ "A comment or reply mentions someone by email. Sent instead of the plain comment notification to each mentioned recipient.",
54
+ recipientLabel: "Mentioned users",
55
+ recipient:
56
+ "The mentioned addresses supplied by the caller, re-checked against the document's live ACL and filtered by each user's `emailNotifications` preference.",
57
+ senderLabel: "Default sender",
58
+ sender:
59
+ "The configured default sender. This call site sets no `from`, `fromName`, `replyTo`, or `appSender`.",
60
+ preview: () =>
61
+ renderDocumentCommentEmail({
62
+ actor: "Sam Rivera",
63
+ title: SAMPLE_TITLE,
64
+ url: SAMPLE_URL,
65
+ content: SAMPLE_COMMENT,
66
+ isReply: false,
67
+ wasMentioned: true,
68
+ }),
69
+ });
70
+ }
@@ -0,0 +1,5 @@
1
+ import { registerContentEmails } from "../lib/emails.js";
2
+
3
+ export default function registerTransactionalEmails(): void {
4
+ registerContentEmails();
5
+ }
@@ -73,6 +73,14 @@ interface DesignSystemData {
73
73
  name: string;
74
74
  variant: "light" | "dark" | "auto";
75
75
  }[];
76
+ tokens?: {
77
+ name: string; // The source system's own name, e.g. "interactive-01"
78
+ cssVar: string; // e.g. "--cds-interactive-01"
79
+ value: string; // e.g. "#0F62FE" or "0.5rem"
80
+ type: "color" | "typography" | "spacing" | "radius" | "shadow" | "other";
81
+ group?: string; // Collection path, e.g. "Colors/Interactive"
82
+ source?: string; // e.g. "Carbon v11" or "globals.css"
83
+ }[];
76
84
  imageStyle?: {
77
85
  referenceUrls: string[];
78
86
  styleDescription: string; // e.g., "Clean, minimal product photography"
@@ -82,6 +90,29 @@ interface DesignSystemData {
82
90
  }
83
91
  ```
84
92
 
93
+ ### The seven color roles are a summary, not the vocabulary
94
+
95
+ `colors` exists so a generated page can be themed from any brand. It is **not**
96
+ where a real design system goes. Carbon has `interactive-01`, Material has
97
+ `md-sys-color-primary-container`; mapping those onto `secondary` throws away the
98
+ only name the design team uses, and the Brand Kit page then shows the wrong
99
+ label for a token the user recognises.
100
+
101
+ So when a source names its tokens, populate **both**: `colors` for generation,
102
+ and `tokens` for the real vocabulary. `tokens` is capped at 500 entries, every
103
+ `cssVar` must be a valid custom property, and values may not contain `;`, `{`,
104
+ `}`, `<`, `>`, or CSS comments — `create-design-system` and
105
+ `update-design-system` reject the whole write and name the offending entries
106
+ rather than storing a partial vocabulary that would read as a smaller system.
107
+
108
+ `update-design-system` replaces the entire `data` object, so carry the existing
109
+ `tokens` array through any edit or those names are lost.
110
+
111
+ A kit with no stored `tokens` falls back to whatever names its `customCSS`
112
+ declares, which is why the production templates already display real Carbon,
113
+ Material, and Primer token names. Prefer real stored `tokens` when a source
114
+ gives them to you.
115
+
85
116
  `imageStyle.styleDescription` is not just metadata — fold it into every
86
117
  `generate-asset` prompt for a design linked to this system, alongside the
87
118
  subject/composition/lighting direction described in the design-generation
@@ -456,7 +487,11 @@ When the user provides multiple sources, call all applicable import actions in p
456
487
  3. **Cross-reference with website** — validates colors/fonts are actually deployed
457
488
  4. **Documents supplement** — presentations may reveal brand colors not in code
458
489
  5. **Images inform mood** — color temperature, density, visual style
459
- 6. **Aggregate into DesignSystemData** — merge all extracted tokens, resolve conflicts
490
+ 6. **Aggregate into DesignSystemData** — merge all extracted tokens, resolve
491
+ conflicts, and carry every extracted name into `tokens`. The import actions
492
+ return `cssCustomProperties` keyed by the source's real variable names; those
493
+ keys are the vocabulary, so do not reduce them to the seven `colors` roles and
494
+ drop the rest.
460
495
  7. **Call `create-design-system`** with the combined result
461
496
  8. **Link to design** via `update-design --designSystemId`
462
497
 
@@ -509,6 +544,14 @@ When generating a design that has a linked design system, replace all default CS
509
544
  }
510
545
  ```
511
546
 
547
+ ### Reference named tokens, do not inline their values
548
+
549
+ `get-design-system` injects the kit's colour tokens and this requirement into
550
+ the generation context, so follow it there: write `var(--cds-link-primary,
551
+ #0f62fe)` rather than the colour. The fallback is required — exports and
552
+ kit-less designs define no custom properties, and a bare `var()` renders
553
+ transparent.
554
+
512
555
  Also update the Google Fonts `<link>` tag to include the design system's fonts:
513
556
 
514
557
  ```html