@promptbook/cli 0.104.0-4 → 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 (25) 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/migrations/2025-11-0001-initial-schema.sql +1 -3
  14. package/apps/agents-server/src/database/migrations/2025-11-0002-metadata-table.sql +1 -3
  15. package/apps/agents-server/src/database/schema.ts +95 -4
  16. package/apps/agents-server/src/message-providers/email/zeptomail/ZeptomailMessageProvider.ts +32 -24
  17. package/apps/agents-server/src/utils/messages/sendMessage.ts +7 -7
  18. package/apps/agents-server/src/utils/messagesAdmin.ts +72 -0
  19. package/esm/index.es.js +8146 -8145
  20. package/esm/index.es.js.map +1 -1
  21. package/esm/typings/src/collection/agent-collection/constructors/agent-collection-in-supabase/AgentsDatabaseSchema.d.ts +18 -15
  22. package/esm/typings/src/version.d.ts +1 -1
  23. package/package.json +1 -1
  24. package/umd/index.umd.js +8135 -8134
  25. package/umd/index.umd.js.map +1 -1
@@ -0,0 +1,102 @@
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+ import { $getTableName } from '../../../database/$getTableName';
3
+ import { $provideSupabase } from '../../../database/$provideSupabase';
4
+ import { isUserAdmin } from '../../../utils/isUserAdmin';
5
+
6
+ const DEFAULT_PAGE_SIZE = 20;
7
+ const MAX_PAGE_SIZE = 100;
8
+
9
+ function parsePositiveInt(value: string | null, fallback: number): number {
10
+ if (!value) return fallback;
11
+ const parsed = parseInt(value, 10);
12
+ if (Number.isNaN(parsed) || parsed <= 0) return fallback;
13
+ return parsed;
14
+ }
15
+
16
+ /**
17
+ * List messages with filters, search and pagination.
18
+ */
19
+ export async function GET(request: NextRequest) {
20
+ if (!(await isUserAdmin())) {
21
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
22
+ }
23
+
24
+ try {
25
+ const searchParams = request.nextUrl.searchParams;
26
+
27
+ const page = parsePositiveInt(searchParams.get('page'), 1);
28
+ const pageSize = Math.min(MAX_PAGE_SIZE, parsePositiveInt(searchParams.get('pageSize'), DEFAULT_PAGE_SIZE));
29
+ const search = searchParams.get('search')?.trim() || '';
30
+ const channel = searchParams.get('channel');
31
+ const direction = searchParams.get('direction');
32
+
33
+ const supabase = $provideSupabase();
34
+
35
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
36
+ let query = supabase.from(await $getTableName('Message')).select('*', { count: 'exact' });
37
+
38
+ if (channel) {
39
+ query = query.eq('channel', channel);
40
+ }
41
+
42
+ if (direction) {
43
+ query = query.eq('direction', direction);
44
+ }
45
+
46
+ if (search) {
47
+ // Search in content, subject (if in metadata?), sender/recipient emails
48
+ // Note: sender and recipients are JSONB, so ilike might not work directly on them unless cast to text
49
+ // Content is TEXT.
50
+ const escaped = search.replace(/%/g, '\\%').replace(/_/g, '\\_');
51
+ // Assuming simple search on content for now to avoid complexity with JSONB search in generic supabase client
52
+ query = query.ilike('content', `%${escaped}%`);
53
+ }
54
+
55
+ // Default sort by createdAt desc
56
+ query = query.order('createdAt', { ascending: false });
57
+
58
+ const from = (page - 1) * pageSize;
59
+ const to = from + pageSize - 1;
60
+
61
+ query = query.range(from, to);
62
+
63
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
64
+ const { data: messages, error, count } = (await query) as { data: any[]; error: any; count: number };
65
+
66
+ if (error) {
67
+ console.error('List messages error:', error);
68
+ return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
69
+ }
70
+
71
+ // Fetch attempts for these messages
72
+ if (messages && messages.length > 0) {
73
+ const messageIds = messages.map((m) => m.id);
74
+ const { data: attempts, error: attemptsError } = await supabase
75
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
76
+ .from(await $getTableName('MessageSendAttempt'))
77
+ .select('*')
78
+ .in('messageId', messageIds);
79
+
80
+ if (attemptsError) {
81
+ console.error('Fetch message attempts error:', attemptsError);
82
+ // We don't fail the whole request, just log it.
83
+ } else {
84
+ // Attach attempts to messages
85
+ for (const message of messages) {
86
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
87
+ (message as any).sendAttempts = attempts?.filter((a: any) => a.messageId === message.id) || [];
88
+ }
89
+ }
90
+ }
91
+
92
+ return NextResponse.json({
93
+ items: messages ?? [],
94
+ total: count ?? 0,
95
+ page,
96
+ pageSize,
97
+ });
98
+ } catch (error) {
99
+ console.error('List messages error:', error);
100
+ return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
101
+ }
102
+ }
@@ -297,6 +297,10 @@ export function Header(props: HeaderProps) {
297
297
  label: 'Chat history',
298
298
  href: '/admin/chat-history',
299
299
  },
300
+ {
301
+ label: 'Messages & Emails',
302
+ href: '/admin/messages',
303
+ },
300
304
  {
301
305
  label: 'Chat feedback',
302
306
  href: '/admin/chat-feedback',
@@ -1,6 +1,6 @@
1
- import { AgentsDatabaseSchema } from '@promptbook-local/types';
2
1
  import { $isRunningInBrowser } from '@promptbook-local/utils';
3
2
  import { createClient, SupabaseClient } from '@supabase/supabase-js';
3
+ import { AgentsServerDatabase } from './schema';
4
4
 
5
5
  /**
6
6
  * Internal cache for `$provideSupabaseForBrowser`
@@ -8,7 +8,7 @@ import { createClient, SupabaseClient } from '@supabase/supabase-js';
8
8
  * @private
9
9
  * @singleton
10
10
  */
11
- let supabase: SupabaseClient<AgentsDatabaseSchema>;
11
+ let supabase: SupabaseClient<AgentsServerDatabase>;
12
12
 
13
13
  /**
14
14
  * Get supabase client
@@ -27,7 +27,7 @@ export function $provideSupabaseForBrowser(): typeof supabase {
27
27
 
28
28
  if (!supabase) {
29
29
  // Create a single supabase client for interacting with your database
30
- supabase = createClient<AgentsDatabaseSchema>(
30
+ supabase = createClient<AgentsServerDatabase>(
31
31
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
32
32
  process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
33
33
  );
@@ -18,7 +18,7 @@ let supabase: SupabaseClient<AgentsServerDatabase>;
18
18
  *
19
19
  * @returns instance of supabase client
20
20
  */
21
- export function $provideSupabaseForServer(): typeof supabase {
21
+ export function $provideSupabaseForServer(): SupabaseClient<AgentsServerDatabase> {
22
22
  if (!$isRunningInNode()) {
23
23
  throw new Error(
24
24
  'Function `$provideSupabaseForServer` can not be used in browser, use `$provideSupabaseForBrowser` instead.',
@@ -1,6 +1,6 @@
1
- import { AgentsDatabaseSchema } from '@promptbook-local/types';
2
1
  import { $isRunningInWebWorker } from '@promptbook-local/utils';
3
2
  import { createClient, SupabaseClient } from '@supabase/supabase-js';
3
+ import { AgentsServerDatabase } from './schema';
4
4
 
5
5
  /**
6
6
  * Internal cache for `$provideSupabaseForWorker`
@@ -8,7 +8,7 @@ import { createClient, SupabaseClient } from '@supabase/supabase-js';
8
8
  * @private
9
9
  * @singleton
10
10
  */
11
- let supabase: SupabaseClient<AgentsDatabaseSchema>;
11
+ let supabase: SupabaseClient<AgentsServerDatabase>;
12
12
 
13
13
  /**
14
14
  * Get supabase client
@@ -27,7 +27,7 @@ export function $provideSupabaseForWorker(): typeof supabase {
27
27
 
28
28
  if (!supabase) {
29
29
  // Create a single supabase client for interacting with your database
30
- supabase = createClient<AgentsDatabaseSchema>(
30
+ supabase = createClient<AgentsServerDatabase>(
31
31
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
32
32
  process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
33
33
  );
@@ -1,6 +1,4 @@
1
- -- Note: This is primary source of truth for the database schema
2
- -- In future we want to be compatible with more then Supabase so we keep SQL as main schema definition
3
- -- To update, search for [💽]
1
+
4
2
 
5
3
 
6
4
  CREATE TABLE IF NOT EXISTS "prefix_Agent" (
@@ -1,6 +1,4 @@
1
- -- Note: This is primary source of truth for the database schema
2
- -- In future we want to be compatible with more then Supabase so we keep SQL as main schema definition
3
- -- To update, search for [💽]
1
+
4
2
 
5
3
 
6
4
  CREATE TABLE IF NOT EXISTS "prefix_Metadata" (
@@ -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>;
@@ -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
  }
@@ -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
+ }