@promptbook/cli 0.104.0-3 → 0.104.0-5

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 (44) 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]/api/profile/route.ts +4 -0
  7. package/apps/agents-server/src/app/agents/[agentName]/images/default-avatar.png/route.ts +139 -0
  8. package/apps/agents-server/src/app/api/messages/route.ts +102 -0
  9. package/apps/agents-server/src/components/Header/Header.tsx +4 -0
  10. package/apps/agents-server/src/database/$provideSupabaseForBrowser.ts +3 -3
  11. package/apps/agents-server/src/database/$provideSupabaseForServer.ts +1 -1
  12. package/apps/agents-server/src/database/$provideSupabaseForWorker.ts +3 -3
  13. package/apps/agents-server/src/database/migrate.ts +34 -1
  14. package/apps/agents-server/src/database/migrations/2025-11-0001-initial-schema.sql +1 -3
  15. package/apps/agents-server/src/database/migrations/2025-11-0002-metadata-table.sql +1 -3
  16. package/apps/agents-server/src/database/migrations/2025-12-0402-message-table.sql +42 -0
  17. package/apps/agents-server/src/database/schema.ts +95 -4
  18. package/apps/agents-server/src/message-providers/email/_common/Email.ts +73 -0
  19. package/apps/agents-server/src/message-providers/email/_common/utils/TODO.txt +1 -0
  20. package/apps/agents-server/src/message-providers/email/_common/utils/parseEmailAddress.test.ts.todo +108 -0
  21. package/apps/agents-server/src/message-providers/email/_common/utils/parseEmailAddress.ts +62 -0
  22. package/apps/agents-server/src/message-providers/email/_common/utils/parseEmailAddresses.test.ts.todo +117 -0
  23. package/apps/agents-server/src/message-providers/email/_common/utils/parseEmailAddresses.ts +19 -0
  24. package/apps/agents-server/src/message-providers/email/_common/utils/stringifyEmailAddress.test.ts.todo +119 -0
  25. package/apps/agents-server/src/message-providers/email/_common/utils/stringifyEmailAddress.ts +19 -0
  26. package/apps/agents-server/src/message-providers/email/_common/utils/stringifyEmailAddresses.test.ts.todo +74 -0
  27. package/apps/agents-server/src/message-providers/email/_common/utils/stringifyEmailAddresses.ts +14 -0
  28. package/apps/agents-server/src/message-providers/email/sendgrid/SendgridMessageProvider.ts +44 -0
  29. package/apps/agents-server/src/message-providers/email/zeptomail/ZeptomailMessageProvider.ts +51 -0
  30. package/apps/agents-server/src/message-providers/index.ts +13 -0
  31. package/apps/agents-server/src/message-providers/interfaces/MessageProvider.ts +11 -0
  32. package/apps/agents-server/src/utils/messages/sendMessage.ts +91 -0
  33. package/apps/agents-server/src/utils/messagesAdmin.ts +72 -0
  34. package/apps/agents-server/src/utils/normalization/filenameToPrompt.test.ts +36 -0
  35. package/apps/agents-server/src/utils/normalization/filenameToPrompt.ts +6 -2
  36. package/esm/index.es.js +8098 -8067
  37. package/esm/index.es.js.map +1 -1
  38. package/esm/typings/src/collection/agent-collection/constructors/agent-collection-in-supabase/AgentsDatabaseSchema.d.ts +18 -15
  39. package/esm/typings/src/llm-providers/_multiple/MultipleLlmExecutionTools.d.ts +6 -2
  40. package/esm/typings/src/llm-providers/remote/RemoteLlmExecutionTools.d.ts +1 -0
  41. package/esm/typings/src/version.d.ts +1 -1
  42. package/package.json +1 -1
  43. package/umd/index.umd.js +8114 -8083
  44. package/umd/index.umd.js.map +1 -1
@@ -0,0 +1,117 @@
1
+ import { describe, expect, it } from '@jest/globals';
2
+ import { parseEmailAddresses } from './parseEmailAddresses';
3
+
4
+ describe('how parseEmailAddresses works', () => {
5
+ it('should work with single email', () => {
6
+ expect(parseEmailAddresses('pavol@webgpt.cz')).toEqual([
7
+ {
8
+ fullName: null,
9
+ baseEmail: 'pavol@webgpt.cz',
10
+ fullEmail: 'pavol@webgpt.cz',
11
+ plus: [],
12
+ },
13
+ ]);
14
+ });
15
+
16
+ it('should work with simple emails', () => {
17
+ expect(parseEmailAddresses('pavol@webgpt.cz, jirka@webgpt.cz, tomas@webgpt.cz')).toEqual([
18
+ {
19
+ fullName: null,
20
+ baseEmail: 'pavol@webgpt.cz',
21
+ fullEmail: 'pavol@webgpt.cz',
22
+ plus: [],
23
+ },
24
+ {
25
+ fullName: null,
26
+ baseEmail: 'jirka@webgpt.cz',
27
+ fullEmail: 'jirka@webgpt.cz',
28
+ plus: [],
29
+ },
30
+ {
31
+ fullName: null,
32
+ baseEmail: 'tomas@webgpt.cz',
33
+ fullEmail: 'tomas@webgpt.cz',
34
+ plus: [],
35
+ },
36
+ ]);
37
+ });
38
+
39
+ it('should work with fullname', () => {
40
+ expect(
41
+ parseEmailAddresses(
42
+ 'Pavol Hejný <pavol@webgpt.cz>, Jirka <jirka@webgpt.cz>, "Tomáš Studeník" <tomas@webgpt.cz>',
43
+ ),
44
+ ).toEqual([
45
+ {
46
+ fullName: 'Pavol Hejný',
47
+ baseEmail: 'pavol@webgpt.cz',
48
+ fullEmail: 'pavol@webgpt.cz',
49
+ plus: [],
50
+ },
51
+ {
52
+ fullName: 'Jirka',
53
+ baseEmail: 'jirka@webgpt.cz',
54
+ fullEmail: 'jirka@webgpt.cz',
55
+ plus: [],
56
+ },
57
+ {
58
+ fullName: 'Tomáš Studeník',
59
+ baseEmail: 'tomas@webgpt.cz',
60
+ fullEmail: 'tomas@webgpt.cz',
61
+ plus: [],
62
+ },
63
+ ]);
64
+ });
65
+
66
+ it('not confused by comma', () => {
67
+ expect(parseEmailAddresses(', pavol@webgpt.cz, ')).toEqual([
68
+ {
69
+ fullName: null,
70
+ fullEmail: 'pavol@webgpt.cz',
71
+ baseEmail: 'pavol@webgpt.cz',
72
+ plus: [],
73
+ },
74
+ ]);
75
+ });
76
+
77
+ it('works on real-life example', () => {
78
+ expect(
79
+ parseEmailAddresses(
80
+ '"bob" <bob@bot.webgpt.cz>, "pavolto" <pavol+to@ptbk.io>, "Pavol" <pavol@collboard.com>',
81
+ ),
82
+ ).toEqual([
83
+ {
84
+ fullName: 'bob',
85
+ fullEmail: 'bob@bot.webgpt.cz',
86
+ baseEmail: 'bob@bot.webgpt.cz',
87
+ plus: [],
88
+ },
89
+ {
90
+ fullName: 'pavolto',
91
+ fullEmail: 'pavol+to@ptbk.io',
92
+ baseEmail: 'pavol@ptbk.io',
93
+ plus: ['to'],
94
+ },
95
+ {
96
+ fullName: 'Pavol',
97
+ fullEmail: 'pavol@collboard.com',
98
+ baseEmail: 'pavol@collboard.com',
99
+ plus: [],
100
+ },
101
+ ]);
102
+ });
103
+
104
+ it('throws on invalid email adresses', () => {
105
+ expect(() => parseEmailAddresses('Pavol, Hejný')).toThrowError(/Invalid email address/);
106
+ expect(() => parseEmailAddresses('Pavol Hejný <>')).toThrowError(/Invalid email address/);
107
+ expect(() => parseEmailAddresses('Pavol Hejný, <@webgpt.cz>')).toThrowError(/Invalid email address/);
108
+ expect(() => parseEmailAddresses('Pavol Hejný <webgpt.cz>')).toThrowError(/Invalid email address/);
109
+ expect(() => parseEmailAddresses('Pavol Hejný <pavol@>')).toThrowError(/Invalid email address/);
110
+ expect(() => parseEmailAddresses('Pavol Hejný <a@b>,')).toThrowError(/Invalid email address/);
111
+ });
112
+ });
113
+
114
+
115
+ /**
116
+ * TODO: [🐫] This test fails because of aliased imports `import type { string_emails } from '@promptbook-local/types';`, fix it
117
+ */
@@ -0,0 +1,19 @@
1
+ import type { string_emails } from '@promptbook-local/types';
2
+ import { spaceTrim } from 'spacetrim';
3
+ import type { EmailAddress } from '../Email';
4
+ import { parseEmailAddress } from './parseEmailAddress';
5
+
6
+ /**
7
+ * Parses the email addresses into its components
8
+ */
9
+ export function parseEmailAddresses(value: string_emails): Array<EmailAddress> {
10
+ const emailAddresses = value
11
+ .split(',')
12
+ .map((email) => spaceTrim(email))
13
+ .filter((email) => email !== '')
14
+ .map((email) => parseEmailAddress(email));
15
+
16
+ // console.log('parseEmailAddresses', value, '->', emailAddresses);
17
+
18
+ return emailAddresses;
19
+ }
@@ -0,0 +1,119 @@
1
+ import { describe, expect, it } from '@jest/globals';
2
+ import { stringifyEmailAddress } from './stringifyEmailAddress';
3
+
4
+ describe('how stringifyEmailAddress works', () => {
5
+ it('should work with simple email', () => {
6
+ expect(
7
+ stringifyEmailAddress({
8
+ fullName: null,
9
+ baseEmail: 'pavol@webgpt.cz',
10
+ fullEmail: 'pavol@webgpt.cz',
11
+ plus: [],
12
+ }),
13
+ ).toBe('pavol@webgpt.cz');
14
+ expect(
15
+ stringifyEmailAddress({
16
+ fullName: null,
17
+ baseEmail: 'jirka@webgpt.cz',
18
+ fullEmail: 'jirka@webgpt.cz',
19
+ plus: [],
20
+ }),
21
+ ).toBe('jirka@webgpt.cz');
22
+ expect(
23
+ stringifyEmailAddress({
24
+ fullName: null,
25
+ baseEmail: 'tomas@webgpt.cz',
26
+ fullEmail: 'tomas@webgpt.cz',
27
+ plus: [],
28
+ }),
29
+ ).toBe('tomas@webgpt.cz');
30
+ });
31
+
32
+ it('should work with fullname', () => {
33
+ expect(
34
+ stringifyEmailAddress({
35
+ fullName: 'Pavol Hejný',
36
+ baseEmail: 'pavol@webgpt.cz',
37
+ fullEmail: 'pavol@webgpt.cz',
38
+ plus: [],
39
+ }),
40
+ ).toBe('"Pavol Hejný" <pavol@webgpt.cz>');
41
+ expect(
42
+ stringifyEmailAddress({
43
+ fullName: 'Jirka',
44
+ baseEmail: 'jirka@webgpt.cz',
45
+ fullEmail: 'jirka@webgpt.cz',
46
+ plus: [],
47
+ }),
48
+ ).toBe('"Jirka" <jirka@webgpt.cz>');
49
+ expect(
50
+ stringifyEmailAddress({
51
+ fullName: 'Tomáš Studeník',
52
+ baseEmail: 'tomas@webgpt.cz',
53
+ fullEmail: 'tomas@webgpt.cz',
54
+ plus: [],
55
+ }),
56
+ ).toBe('"Tomáš Studeník" <tomas@webgpt.cz>');
57
+ });
58
+
59
+ it('should work with plus', () => {
60
+ expect(
61
+ stringifyEmailAddress({
62
+ fullName: null,
63
+ baseEmail: 'pavol@webgpt.cz',
64
+ fullEmail: 'pavol+test@webgpt.cz',
65
+ plus: ['test'],
66
+ }),
67
+ ).toBe('pavol+test@webgpt.cz');
68
+ expect(
69
+ stringifyEmailAddress({
70
+ fullName: null,
71
+ baseEmail: 'jirka@webgpt.cz',
72
+ fullEmail: 'jirka+test@webgpt.cz',
73
+ plus: ['test'],
74
+ }),
75
+ ).toBe('jirka+test@webgpt.cz');
76
+ expect(
77
+ stringifyEmailAddress({
78
+ fullName: null,
79
+ baseEmail: 'tomas@webgpt.cz',
80
+ fullEmail: 'tomas+test+ainautes@webgpt.cz',
81
+ plus: ['test', 'ainautes'],
82
+ }),
83
+ ).toBe('tomas+test+ainautes@webgpt.cz');
84
+ });
85
+
86
+ it('should work with both fullname and plus', () => {
87
+ expect(
88
+ stringifyEmailAddress({
89
+ fullName: 'Pavol Hejný',
90
+ baseEmail: 'pavol@webgpt.cz',
91
+ fullEmail: 'pavol+test@webgpt.cz',
92
+ plus: ['test'],
93
+ }),
94
+ ).toBe('"Pavol Hejný" <pavol+test@webgpt.cz>');
95
+ expect(
96
+ stringifyEmailAddress({
97
+ fullName: 'Jirka',
98
+ baseEmail: 'jirka@webgpt.cz',
99
+ fullEmail: 'jirka+test@webgpt.cz',
100
+ plus: ['test'],
101
+ }),
102
+ ).toBe('"Jirka" <jirka+test@webgpt.cz>');
103
+ expect(
104
+ stringifyEmailAddress({
105
+ fullName: 'Tomáš Studeník',
106
+ baseEmail: 'tomas@webgpt.cz',
107
+ fullEmail: 'tomas+test+ainautes@webgpt.cz',
108
+ plus: ['test', 'ainautes'],
109
+ }),
110
+ ).toBe('"Tomáš Studeník" <tomas+test+ainautes@webgpt.cz>');
111
+ });
112
+
113
+ // TODO: [🎾] Implement and test here escaping
114
+ });
115
+
116
+
117
+ /**
118
+ * TODO: [🐫] This test fails because of aliased imports `import type { string_emails } from '@promptbook-local/types';`, fix it
119
+ */
@@ -0,0 +1,19 @@
1
+ import type { string_email } from '@promptbook-local/types';
2
+ import type { EmailAddress } from '../Email';
3
+
4
+ /**
5
+ * Makes string email from EmailAddress
6
+ */
7
+ export function stringifyEmailAddress(emailAddress: EmailAddress): string_email {
8
+ const { fullEmail, fullName } = emailAddress;
9
+
10
+ if (fullName !== null) {
11
+ return `"${fullName}" <${fullEmail}>`;
12
+ }
13
+
14
+ return fullEmail;
15
+ }
16
+
17
+ /**
18
+ * TODO: [🎾] Implement and test here escaping
19
+ */
@@ -0,0 +1,74 @@
1
+ import { describe, expect, it } from '@jest/globals';
2
+ import { stringifyEmailAddresses } from './stringifyEmailAddresses';
3
+
4
+ describe('how stringifyEmailAddresses works', () => {
5
+ it('should work with single email', () => {
6
+ expect(
7
+ stringifyEmailAddresses([
8
+ {
9
+ fullName: null,
10
+ baseEmail: 'pavol@webgpt.cz',
11
+ fullEmail: 'pavol@webgpt.cz',
12
+ plus: [],
13
+ },
14
+ ]),
15
+ ).toEqual('pavol@webgpt.cz');
16
+ });
17
+
18
+ it('should work with simple emails', () => {
19
+ expect(
20
+ stringifyEmailAddresses([
21
+ {
22
+ fullName: null,
23
+ baseEmail: 'pavol@webgpt.cz',
24
+ fullEmail: 'pavol@webgpt.cz',
25
+ plus: [],
26
+ },
27
+ {
28
+ fullName: null,
29
+ baseEmail: 'jirka@webgpt.cz',
30
+ fullEmail: 'jirka@webgpt.cz',
31
+ plus: [],
32
+ },
33
+ {
34
+ fullName: null,
35
+ baseEmail: 'tomas@webgpt.cz',
36
+ fullEmail: 'tomas@webgpt.cz',
37
+ plus: [],
38
+ },
39
+ ]),
40
+ ).toEqual('pavol@webgpt.cz, jirka@webgpt.cz, tomas@webgpt.cz');
41
+ });
42
+
43
+ it('should work with fullname', () => {
44
+ expect(
45
+ stringifyEmailAddresses([
46
+ {
47
+ fullName: 'Pavol Hejný',
48
+ baseEmail: 'pavol@webgpt.cz',
49
+ fullEmail: 'pavol@webgpt.cz',
50
+ plus: [],
51
+ },
52
+ {
53
+ fullName: 'Jiří Jahn',
54
+ baseEmail: 'jirka@webgpt.cz',
55
+ fullEmail: 'jirka@webgpt.cz',
56
+ plus: [],
57
+ },
58
+ {
59
+ fullName: 'Tomáš Studeník',
60
+ baseEmail: 'tomas@webgpt.cz',
61
+ fullEmail: 'tomas@webgpt.cz',
62
+ plus: [],
63
+ },
64
+ ]),
65
+ ).toEqual('"Pavol Hejný" <pavol@webgpt.cz>, "Jiří Jahn" <jirka@webgpt.cz>, "Tomáš Studeník" <tomas@webgpt.cz>');
66
+ });
67
+
68
+ // TODO: [🎾] Implement and test here escaping
69
+ });
70
+
71
+
72
+ /**
73
+ * TODO: [🐫] This test fails because of aliased imports `import type { string_emails } from '@promptbook-local/types';`, fix it
74
+ */
@@ -0,0 +1,14 @@
1
+ import type { string_emails } from '@promptbook-local/types';
2
+ import type { EmailAddress } from '../Email';
3
+ import { stringifyEmailAddress } from './stringifyEmailAddress';
4
+
5
+ /**
6
+ * Makes string email from multiple EmailAddress
7
+ */
8
+ export function stringifyEmailAddresses(emailAddresses: Array<EmailAddress>): string_emails {
9
+ return emailAddresses.map((emailAddress) => stringifyEmailAddress(emailAddress)).join(', ');
10
+ }
11
+
12
+ /**
13
+ * TODO: [🎾] Implement and test here escaping
14
+ */
@@ -0,0 +1,44 @@
1
+ import { removeMarkdownFormatting } from '@promptbook-local/markdown-utils';
2
+ import type { really_any } from '@promptbook-local/types';
3
+ import sendgridEmailClient from '@sendgrid/mail';
4
+ import { marked } from 'marked';
5
+ import { MessageProvider } from '../../interfaces/MessageProvider';
6
+ import { OutboundEmail } from '../_common/Email';
7
+ import { parseEmailAddress } from '../_common/utils/parseEmailAddress';
8
+
9
+ export class SendgridMessageProvider implements MessageProvider {
10
+ constructor(private readonly apiKey: string) {
11
+ sendgridEmailClient.setApiKey(this.apiKey);
12
+ }
13
+
14
+ public async send(message: OutboundEmail): Promise<really_any> {
15
+ const sender = message.sender;
16
+ const recipients = (Array.isArray(message.recipients) ? message.recipients : [message.recipients]).filter(
17
+ Boolean,
18
+ ) as really_any[];
19
+
20
+ const text = removeMarkdownFormatting(message.content);
21
+ const html = await marked.parse(message.content);
22
+
23
+ const { fullEmail, fullName } = parseEmailAddress(sender);
24
+
25
+ const response = await sendgridEmailClient.send({
26
+ from: {
27
+ email: fullEmail,
28
+ name: fullName || undefined,
29
+ },
30
+ to: recipients.map((r) => {
31
+ const { fullEmail, fullName } = parseEmailAddress(r.email || r.baseEmail || r);
32
+ return {
33
+ email: fullEmail,
34
+ name: r.name || fullName || undefined,
35
+ };
36
+ }),
37
+ subject: message.metadata?.subject || 'No Subject',
38
+ text,
39
+ html,
40
+ });
41
+
42
+ return response;
43
+ }
44
+ }
@@ -0,0 +1,51 @@
1
+ import { removeMarkdownFormatting } from '@promptbook-local/markdown-utils';
2
+ import type { really_any } from '@promptbook-local/types';
3
+ import { marked } from 'marked';
4
+ // @ts-expect-error: Zeptomail types are not resolving correctly
5
+ import { SendMailClient } from 'zeptomail';
6
+ import { MessageProvider } from '../../interfaces/MessageProvider';
7
+ import { OutboundEmail } from '../_common/Email';
8
+
9
+ export class ZeptomailMessageProvider implements MessageProvider {
10
+ constructor(private readonly apiKey: string) {}
11
+
12
+ public async send(message: OutboundEmail): Promise<really_any> {
13
+ try {
14
+ const client = new SendMailClient({ url: 'api.zeptomail.com/', token: this.apiKey });
15
+
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[];
20
+
21
+ const textbody = removeMarkdownFormatting(message.content);
22
+ const htmlbody = await marked.parse(message.content);
23
+
24
+ const response = await client.sendMail({
25
+ from: {
26
+ address: sender.email || sender.baseEmail || sender,
27
+ name: sender.name || sender.fullName || undefined,
28
+ },
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
+ });
41
+
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
+ }
50
+ }
51
+ }
@@ -0,0 +1,13 @@
1
+ import { SendgridMessageProvider } from './email/sendgrid/SendgridMessageProvider';
2
+ import { ZeptomailMessageProvider } from './email/zeptomail/ZeptomailMessageProvider';
3
+ import { MessageProvider } from './interfaces/MessageProvider';
4
+
5
+ export const EMAIL_PROVIDERS: Record<string, MessageProvider> = {};
6
+
7
+ if (process.env.ZEPTOMAIL_API_KEY) {
8
+ EMAIL_PROVIDERS['ZEPTOMAIL'] = new ZeptomailMessageProvider(process.env.ZEPTOMAIL_API_KEY);
9
+ }
10
+
11
+ if (process.env.SENDGRID_API_KEY) {
12
+ EMAIL_PROVIDERS['SENDGRID'] = new SendgridMessageProvider(process.env.SENDGRID_API_KEY);
13
+ }
@@ -0,0 +1,11 @@
1
+ import type { Message, really_any, string_email } from '@promptbook-local/types';
2
+
3
+ export type MessageProvider = {
4
+ /**
5
+ * Sends a message through the provider
6
+ *
7
+ * @param message The message to send
8
+ * @returns Raw response from the provider
9
+ */
10
+ send(message: Message<string_email>): Promise<really_any>;
11
+ };
@@ -0,0 +1,91 @@
1
+ import type { really_any } from '@promptbook-local/types';
2
+ import { serializeError } from '@promptbook-local/utils';
3
+ import { assertsError } from '../../../../../src/errors/assertsError';
4
+ import { $getTableName } from '../../database/$getTableName';
5
+ import { $provideSupabaseForServer } from '../../database/$provideSupabaseForServer';
6
+ import { EMAIL_PROVIDERS } from '../../message-providers';
7
+ import { OutboundEmail } from '../../message-providers/email/_common/Email';
8
+
9
+ /**
10
+ * Sends a message
11
+ */
12
+ export async function sendMessage(message: OutboundEmail): Promise<void> {
13
+ const supabase = await $provideSupabaseForServer();
14
+
15
+ // 1. Insert message
16
+ const { data: insertedMessage, error: insertError } = await supabase
17
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
18
+ .from(await $getTableName('Message'))
19
+ .insert({
20
+ channel: message.channel || 'UNKNOWN',
21
+ direction: message.direction || 'OUTBOUND',
22
+ sender: message.sender,
23
+ recipients: message.recipients,
24
+ content: message.content,
25
+ threadId: message.threadId,
26
+ metadata: message.metadata,
27
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
28
+ } as any)
29
+ .select()
30
+ .single();
31
+
32
+ if (insertError) {
33
+ throw new Error(`Failed to insert message: ${insertError.message}`);
34
+ }
35
+
36
+ if (!insertedMessage) {
37
+ throw new Error('Failed to insert message: No data returned');
38
+ }
39
+
40
+ // 2. If outbound and email, try to send
41
+ if (message.direction === 'OUTBOUND' && message.channel === 'EMAIL') {
42
+ const providers = Object.keys(EMAIL_PROVIDERS);
43
+
44
+ if (providers.length === 0) {
45
+ console.warn('No email providers configured');
46
+ return;
47
+ }
48
+
49
+ let isSent = false;
50
+
51
+ for (const providerName of providers) {
52
+ const provider = EMAIL_PROVIDERS[providerName];
53
+ let isSuccessful = false;
54
+ let raw: really_any = null;
55
+
56
+ try {
57
+ console.log(`📤 Sending email via ${providerName}`);
58
+ raw = await provider.send(message);
59
+ isSuccessful = true;
60
+ isSent = true;
61
+ } catch (error) {
62
+ assertsError(error);
63
+ console.error(`Failed to send email via ${providerName}`, error);
64
+ raw = { error: serializeError(error) };
65
+ }
66
+
67
+ // 3. Log attempt
68
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
69
+ await supabase.from(await $getTableName('MessageSendAttempt')).insert({
70
+ // @ts-expect-error: insertedMessage is any
71
+ messageId: insertedMessage.id,
72
+ providerName,
73
+ isSuccessful,
74
+ raw,
75
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
76
+ } as any);
77
+
78
+ if (isSuccessful) {
79
+ break;
80
+ }
81
+ }
82
+
83
+ if (!isSent) {
84
+ throw new Error('Failed to send email via any provider');
85
+ }
86
+ }
87
+ }
88
+
89
+ /**
90
+ * TODO: !!!! Move to `message-providers` and rename `message-providers` -> `messages`
91
+ */
@@ -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
+ }