@promptbook/cli 0.104.0-4 → 0.104.0-6

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 (45) hide show
  1. package/apps/agents-server/src/app/admin/messages/MessagesClient.tsx +294 -0
  2. package/apps/agents-server/src/app/admin/messages/page.tsx +13 -0
  3. package/apps/agents-server/src/app/admin/messages/send-email/SendEmailClient.tsx +104 -0
  4. package/apps/agents-server/src/app/admin/messages/send-email/actions.ts +35 -0
  5. package/apps/agents-server/src/app/admin/messages/send-email/page.tsx +13 -0
  6. package/apps/agents-server/src/app/agents/[agentName]/AgentProfileWrapper.tsx +3 -2
  7. package/apps/agents-server/src/app/agents/[agentName]/_utils.ts +6 -2
  8. package/apps/agents-server/src/app/agents/[agentName]/api/profile/route.ts +4 -0
  9. package/apps/agents-server/src/app/agents/[agentName]/code/page.tsx +14 -8
  10. package/apps/agents-server/src/app/agents/[agentName]/images/default-avatar.png/route.ts +139 -0
  11. package/apps/agents-server/src/app/agents/[agentName]/images/icon-256.png/route.tsx +5 -2
  12. package/apps/agents-server/src/app/agents/[agentName]/images/screenshot-fullhd.png/route.tsx +2 -2
  13. package/apps/agents-server/src/app/agents/[agentName]/images/screenshot-phone.png/route.tsx +2 -2
  14. package/apps/agents-server/src/app/agents/[agentName]/integration/page.tsx +2 -2
  15. package/apps/agents-server/src/app/agents/[agentName]/links/page.tsx +2 -2
  16. package/apps/agents-server/src/app/agents/[agentName]/opengraph-image.tsx +6 -2
  17. package/apps/agents-server/src/app/agents/[agentName]/page.tsx +7 -4
  18. package/apps/agents-server/src/app/agents/[agentName]/system-message/page.tsx +2 -1
  19. package/apps/agents-server/src/app/api/emails/incoming/sendgrid/route.ts +48 -0
  20. package/apps/agents-server/src/app/api/messages/route.ts +102 -0
  21. package/apps/agents-server/src/components/AgentProfile/AgentProfile.tsx +10 -2
  22. package/apps/agents-server/src/components/Header/Header.tsx +4 -0
  23. package/apps/agents-server/src/components/Homepage/AgentCard.tsx +2 -1
  24. package/apps/agents-server/src/database/$provideSupabaseForBrowser.ts +3 -3
  25. package/apps/agents-server/src/database/$provideSupabaseForServer.ts +1 -1
  26. package/apps/agents-server/src/database/$provideSupabaseForWorker.ts +3 -3
  27. package/apps/agents-server/src/database/migrations/2025-11-0001-initial-schema.sql +1 -3
  28. package/apps/agents-server/src/database/migrations/2025-11-0002-metadata-table.sql +1 -3
  29. package/apps/agents-server/src/database/schema.ts +95 -4
  30. package/apps/agents-server/src/message-providers/email/sendgrid/parseInboundSendgridEmail.ts +49 -0
  31. package/apps/agents-server/src/message-providers/email/zeptomail/ZeptomailMessageProvider.ts +32 -24
  32. package/apps/agents-server/src/utils/content/extractBodyContentFromHtml.ts +19 -0
  33. package/apps/agents-server/src/utils/messages/sendMessage.ts +7 -7
  34. package/apps/agents-server/src/utils/messagesAdmin.ts +72 -0
  35. package/esm/index.es.js +8021 -8062
  36. package/esm/index.es.js.map +1 -1
  37. package/esm/typings/src/book-2.0/agent-source/createAgentModelRequirements.d.ts +6 -6
  38. package/esm/typings/src/book-2.0/utils/generatePlaceholderAgentProfileImageUrl.d.ts +3 -3
  39. package/esm/typings/src/collection/agent-collection/constructors/agent-collection-in-supabase/AgentsDatabaseSchema.d.ts +18 -15
  40. package/esm/typings/src/types/typeAliases.d.ts +4 -0
  41. package/esm/typings/src/version.d.ts +1 -1
  42. package/package.json +1 -1
  43. package/umd/index.umd.js +8015 -8056
  44. package/umd/index.umd.js.map +1 -1
  45. package/esm/typings/src/book-2.0/utils/generateGravatarUrl.d.ts +0 -10
@@ -3,7 +3,7 @@
3
3
  * Source of truth: `schema.sql` *(do not edit table structure here manually)*
4
4
  *
5
5
  * [💽] Prompt:
6
- * Re-generate supabase typescript schema from the `./schema.sql`
6
+ * Re-generate supabase typescript schema from `./migrations/*.sql`
7
7
  */
8
8
 
9
9
  // Json helper (Supabase style)
@@ -120,7 +120,14 @@ export type AgentsServerDatabase = {
120
120
  agentSource?: string;
121
121
  promptbookEngineVersion?: string;
122
122
  };
123
- Relationships: [];
123
+ Relationships: [
124
+ {
125
+ foreignKeyName: 'AgentHistory_agentName_fkey';
126
+ columns: ['agentName'];
127
+ referencedRelation: 'Agent';
128
+ referencedColumns: ['agentName'];
129
+ },
130
+ ];
124
131
  };
125
132
  ChatHistory: {
126
133
  Row: {
@@ -174,7 +181,14 @@ export type AgentsServerDatabase = {
174
181
  source?: 'AGENT_PAGE_CHAT' | 'OPENAI_API_COMPATIBILITY' | null;
175
182
  apiKey?: string | null;
176
183
  };
177
- Relationships: [];
184
+ Relationships: [
185
+ {
186
+ foreignKeyName: 'ChatHistory_agentName_fkey';
187
+ columns: ['agentName'];
188
+ referencedRelation: 'Agent';
189
+ referencedColumns: ['agentName'];
190
+ },
191
+ ];
178
192
  };
179
193
  ChatFeedback: {
180
194
  Row: {
@@ -228,7 +242,14 @@ export type AgentsServerDatabase = {
228
242
  language?: string | null;
229
243
  platform?: string | null;
230
244
  };
231
- Relationships: [];
245
+ Relationships: [
246
+ {
247
+ foreignKeyName: 'ChatFeedback_agentName_fkey';
248
+ columns: ['agentName'];
249
+ referencedRelation: 'Agent';
250
+ referencedColumns: ['agentName'];
251
+ },
252
+ ];
232
253
  };
233
254
  User: {
234
255
  Row: {
@@ -408,6 +429,76 @@ export type AgentsServerDatabase = {
408
429
  },
409
430
  ];
410
431
  };
432
+ Message: {
433
+ Row: {
434
+ id: number;
435
+ createdAt: string;
436
+ channel: string;
437
+ direction: string;
438
+ sender: Json;
439
+ recipients: Json | null;
440
+ content: string;
441
+ threadId: string | null;
442
+ metadata: Json | null;
443
+ };
444
+ Insert: {
445
+ id?: number;
446
+ createdAt?: string;
447
+ channel: string;
448
+ direction: string;
449
+ sender: Json;
450
+ recipients?: Json | null;
451
+ content: string;
452
+ threadId?: string | null;
453
+ metadata?: Json | null;
454
+ };
455
+ Update: {
456
+ id?: number;
457
+ createdAt?: string;
458
+ channel?: string;
459
+ direction?: string;
460
+ sender?: Json;
461
+ recipients?: Json | null;
462
+ content?: string;
463
+ threadId?: string | null;
464
+ metadata?: Json | null;
465
+ };
466
+ Relationships: [];
467
+ };
468
+ MessageSendAttempt: {
469
+ Row: {
470
+ id: number;
471
+ createdAt: string;
472
+ messageId: number;
473
+ providerName: string;
474
+ isSuccessful: boolean;
475
+ raw: Json | null;
476
+ };
477
+ Insert: {
478
+ id?: number;
479
+ createdAt?: string;
480
+ messageId: number;
481
+ providerName: string;
482
+ isSuccessful: boolean;
483
+ raw?: Json | null;
484
+ };
485
+ Update: {
486
+ id?: number;
487
+ createdAt?: string;
488
+ messageId?: number;
489
+ providerName?: string;
490
+ isSuccessful?: boolean;
491
+ raw?: Json | null;
492
+ };
493
+ Relationships: [
494
+ {
495
+ foreignKeyName: 'MessageSendAttempt_messageId_fkey';
496
+ columns: ['messageId'];
497
+ referencedRelation: 'Message';
498
+ referencedColumns: ['id'];
499
+ },
500
+ ];
501
+ };
411
502
  };
412
503
  Views: Record<string, never>;
413
504
  Functions: Record<string, never>;
@@ -0,0 +1,49 @@
1
+ import { string_markdown } from '@promptbook-local/types';
2
+ import { simpleParser } from 'mailparser';
3
+ import TurndownService from 'turndown';
4
+ import { extractBodyContentFromHtml } from '../../../utils/content/extractBodyContentFromHtml';
5
+ import type { InboundEmail } from '../_common/Email';
6
+ import { parseEmailAddress } from '../_common/utils/parseEmailAddress';
7
+ import { parseEmailAddresses } from '../_common/utils/parseEmailAddresses';
8
+
9
+ /**
10
+ * Function parseInboundSendgridEmail will parse raw inbound email from Sendgrid and return Email object
11
+ */
12
+ export async function parseInboundSendgridEmail(rawEmail: string): Promise<InboundEmail> {
13
+ const parsedEmail = await simpleParser(rawEmail);
14
+
15
+ const toArray = !Array.isArray(parsedEmail.to)
16
+ ? parsedEmail.to === undefined
17
+ ? []
18
+ : [parsedEmail.to]
19
+ : parsedEmail.to;
20
+ const to = toArray.flatMap((_) => parseEmailAddresses(_.text));
21
+
22
+ const ccArray = !Array.isArray(parsedEmail.cc)
23
+ ? parsedEmail.cc === undefined
24
+ ? []
25
+ : [parsedEmail.cc]
26
+ : parsedEmail.cc;
27
+ const cc = ccArray.flatMap((_) => parseEmailAddresses(_.text));
28
+
29
+ const turndownService = new TurndownService();
30
+
31
+ const content = (parsedEmail.html
32
+ ? turndownService.turndown(extractBodyContentFromHtml(parsedEmail.html))
33
+ : parsedEmail.text || '') as string_markdown;
34
+
35
+ const email: InboundEmail = {
36
+ channel: 'EMAIL',
37
+ direction: 'INBOUND',
38
+ sender: parseEmailAddress(parsedEmail.from?.text || '').fullEmail,
39
+ recipients: to.map((_) => _.fullEmail),
40
+ cc,
41
+ subject: parsedEmail.subject || '',
42
+ content,
43
+ attachments: [
44
+ /* <- TODO: [📯] Parse attachments */
45
+ ],
46
+ };
47
+
48
+ return email;
49
+ }
@@ -10,34 +10,42 @@ export class ZeptomailMessageProvider implements MessageProvider {
10
10
  constructor(private readonly apiKey: string) {}
11
11
 
12
12
  public async send(message: OutboundEmail): Promise<really_any> {
13
- const client = new SendMailClient({ url: 'api.zeptomail.com/', token: this.apiKey });
13
+ try {
14
+ const client = new SendMailClient({ url: 'api.zeptomail.com/', token: this.apiKey });
14
15
 
15
- const sender = message.sender as really_any;
16
- const recipients = (Array.isArray(message.recipients) ? message.recipients : [message.recipients]).filter(
17
- Boolean,
18
- ) as really_any[];
16
+ const sender = message.sender as really_any;
17
+ const recipients = (Array.isArray(message.recipients) ? message.recipients : [message.recipients]).filter(
18
+ Boolean,
19
+ ) as really_any[];
19
20
 
20
- const textbody = removeMarkdownFormatting(message.content);
21
- const htmlbody = await marked.parse(message.content);
21
+ const textbody = removeMarkdownFormatting(message.content);
22
+ const htmlbody = await marked.parse(message.content);
22
23
 
23
- const response = await client.sendMail({
24
- from: {
25
- address: sender.email || sender.baseEmail || sender,
26
- name: sender.name || sender.fullName || undefined,
27
- },
28
- to: recipients.map((r) => ({
29
- email_address: {
30
- address: r.email || r.baseEmail || r,
31
- name: r.name || r.fullName || undefined,
24
+ const response = await client.sendMail({
25
+ from: {
26
+ address: sender.email || sender.baseEmail || sender,
27
+ name: sender.name || sender.fullName || undefined,
32
28
  },
33
- })),
34
- subject: message.metadata?.subject || 'No Subject',
35
- textbody,
36
- htmlbody,
37
- track_clicks: true,
38
- track_opens: true,
39
- });
29
+ to: recipients.map((r) => ({
30
+ email_address: {
31
+ address: r.email || r.baseEmail || r,
32
+ name: r.name || r.fullName || undefined,
33
+ },
34
+ })),
35
+ subject: message.metadata?.subject || 'No Subject',
36
+ textbody,
37
+ htmlbody,
38
+ track_clicks: true,
39
+ track_opens: true,
40
+ });
40
41
 
41
- return response;
42
+ return response;
43
+ } catch (raw: really_any) {
44
+ if (!('error' in raw)) {
45
+ throw raw;
46
+ }
47
+
48
+ throw new Error(raw.error.message, raw.error.details);
49
+ }
42
50
  }
43
51
  }
@@ -0,0 +1,19 @@
1
+ import type { string_html } from '@promptbook-local/types';
2
+
3
+ /**
4
+ * Extract the first heading from HTML
5
+ *
6
+ * @param contentText HTML
7
+ * @returns heading
8
+ */
9
+ export function extractBodyContentFromHtml(html: string_html): string_html {
10
+ // Note: Not using DOMParser, because it's overkill for this simple task
11
+
12
+ const match = html.match(/<body[^>]*>(?<bodyContent>[\s\S]*)<\/body>/s);
13
+
14
+ if (!match) {
15
+ return html;
16
+ }
17
+
18
+ return match.groups!.bodyContent!;
19
+ }
@@ -1,4 +1,6 @@
1
1
  import type { really_any } from '@promptbook-local/types';
2
+ import { serializeError } from '@promptbook-local/utils';
3
+ import { assertsError } from '../../../../../src/errors/assertsError';
2
4
  import { $getTableName } from '../../database/$getTableName';
3
5
  import { $provideSupabaseForServer } from '../../database/$provideSupabaseForServer';
4
6
  import { EMAIL_PROVIDERS } from '../../message-providers';
@@ -9,15 +11,11 @@ import { OutboundEmail } from '../../message-providers/email/_common/Email';
9
11
  */
10
12
  export async function sendMessage(message: OutboundEmail): Promise<void> {
11
13
  const supabase = await $provideSupabaseForServer();
12
- // @ts-expect-error: Tables are not yet in types
13
- const messageTable = await $getTableName('Message');
14
- // @ts-expect-error: Tables are not yet in types
15
- const messageSendAttemptTable = await $getTableName('MessageSendAttempt');
16
14
 
17
15
  // 1. Insert message
18
16
  const { data: insertedMessage, error: insertError } = await supabase
19
17
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
20
- .from(messageTable as any)
18
+ .from(await $getTableName('Message'))
21
19
  .insert({
22
20
  channel: message.channel || 'UNKNOWN',
23
21
  direction: message.direction || 'OUTBOUND',
@@ -56,17 +54,19 @@ export async function sendMessage(message: OutboundEmail): Promise<void> {
56
54
  let raw: really_any = null;
57
55
 
58
56
  try {
57
+ console.log(`📤 Sending email via ${providerName}`);
59
58
  raw = await provider.send(message);
60
59
  isSuccessful = true;
61
60
  isSent = true;
62
61
  } catch (error) {
62
+ assertsError(error);
63
63
  console.error(`Failed to send email via ${providerName}`, error);
64
- raw = { error: error instanceof Error ? error.message : String(error) };
64
+ raw = { error: serializeError(error) };
65
65
  }
66
66
 
67
67
  // 3. Log attempt
68
68
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
69
- await supabase.from(messageSendAttemptTable as any).insert({
69
+ await supabase.from(await $getTableName('MessageSendAttempt')).insert({
70
70
  // @ts-expect-error: insertedMessage is any
71
71
  messageId: insertedMessage.id,
72
72
  providerName,
@@ -0,0 +1,72 @@
1
+ import type { Json } from '../database/schema';
2
+
3
+ export type MessageRow = {
4
+ id: number;
5
+ createdAt: string;
6
+ channel: string;
7
+ direction: string;
8
+ sender: Json;
9
+ recipients: Json;
10
+ content: string;
11
+ threadId: string | null;
12
+ metadata: Json;
13
+ // Joined fields
14
+ sendAttempts?: MessageSendAttemptRow[];
15
+ };
16
+
17
+ export type MessageSendAttemptRow = {
18
+ id: number;
19
+ createdAt: string;
20
+ messageId: number;
21
+ providerName: string;
22
+ isSuccessful: boolean;
23
+ raw: Json;
24
+ };
25
+
26
+ export type MessagesListResponse = {
27
+ items: MessageRow[];
28
+ total: number;
29
+ page: number;
30
+ pageSize: number;
31
+ };
32
+
33
+ export type MessagesListParams = {
34
+ page?: number;
35
+ pageSize?: number;
36
+ search?: string;
37
+ channel?: string;
38
+ direction?: string;
39
+ };
40
+
41
+ /**
42
+ * Build query string for messages listing.
43
+ */
44
+ function buildQuery(params: MessagesListParams): string {
45
+ const searchParams = new URLSearchParams();
46
+
47
+ if (params.page && params.page > 0) searchParams.set('page', String(params.page));
48
+ if (params.pageSize && params.pageSize > 0) searchParams.set('pageSize', String(params.pageSize));
49
+ if (params.search) searchParams.set('search', params.search);
50
+ if (params.channel) searchParams.set('channel', params.channel);
51
+ if (params.direction) searchParams.set('direction', params.direction);
52
+
53
+ const qs = searchParams.toString();
54
+ return qs ? `?${qs}` : '';
55
+ }
56
+
57
+ /**
58
+ * Fetch messages from the admin API.
59
+ */
60
+ export async function $fetchMessages(params: MessagesListParams = {}): Promise<MessagesListResponse> {
61
+ const qs = buildQuery(params);
62
+ const response = await fetch(`/api/messages${qs}`, {
63
+ method: 'GET',
64
+ });
65
+
66
+ if (!response.ok) {
67
+ const data = await response.json().catch(() => ({}));
68
+ throw new Error(data.error || 'Failed to load messages');
69
+ }
70
+
71
+ return (await response.json()) as MessagesListResponse;
72
+ }