@promptbook/cli 0.103.0-51 → 0.103.0-53

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 (114) hide show
  1. package/apps/agents-server/README.md +1 -1
  2. package/apps/agents-server/config.ts +3 -3
  3. package/apps/agents-server/next.config.ts +1 -1
  4. package/apps/agents-server/public/sw.js +16 -0
  5. package/apps/agents-server/src/app/AddAgentButton.tsx +24 -4
  6. package/apps/agents-server/src/app/actions.ts +15 -13
  7. package/apps/agents-server/src/app/admin/chat-feedback/ChatFeedbackClient.tsx +541 -0
  8. package/apps/agents-server/src/app/admin/chat-feedback/page.tsx +22 -0
  9. package/apps/agents-server/src/app/admin/chat-history/ChatHistoryClient.tsx +532 -0
  10. package/apps/agents-server/src/app/admin/chat-history/page.tsx +21 -0
  11. package/apps/agents-server/src/app/admin/metadata/MetadataClient.tsx +241 -27
  12. package/apps/agents-server/src/app/admin/models/page.tsx +22 -0
  13. package/apps/agents-server/src/app/admin/users/[userId]/UserDetailClient.tsx +131 -0
  14. package/apps/agents-server/src/app/admin/users/[userId]/page.tsx +21 -0
  15. package/apps/agents-server/src/app/admin/users/page.tsx +18 -0
  16. package/apps/agents-server/src/app/agents/[agentName]/AgentQrCode.tsx +3 -3
  17. package/apps/agents-server/src/app/agents/[agentName]/ClearAgentChatFeedbackButton.tsx +63 -0
  18. package/apps/agents-server/src/app/agents/[agentName]/ClearAgentChatHistoryButton.tsx +63 -0
  19. package/apps/agents-server/src/app/agents/[agentName]/CloneAgentButton.tsx +41 -0
  20. package/apps/agents-server/src/app/agents/[agentName]/InstallPwaButton.tsx +74 -0
  21. package/apps/agents-server/src/app/agents/[agentName]/ServiceWorkerRegister.tsx +24 -0
  22. package/apps/agents-server/src/app/agents/[agentName]/_utils.ts +19 -0
  23. package/apps/agents-server/src/app/agents/[agentName]/api/agents/route.ts +67 -0
  24. package/apps/agents-server/src/app/agents/[agentName]/api/profile/route.ts +3 -0
  25. package/apps/agents-server/src/app/agents/[agentName]/api/voice/route.ts +177 -0
  26. package/apps/agents-server/src/app/agents/[agentName]/book/page.tsx +3 -0
  27. package/apps/agents-server/src/app/agents/[agentName]/book+chat/AgentBookAndChat.tsx +53 -1
  28. package/apps/agents-server/src/app/agents/[agentName]/generateAgentMetadata.ts +12 -12
  29. package/apps/agents-server/src/app/agents/[agentName]/history/RestoreVersionButton.tsx +46 -0
  30. package/apps/agents-server/src/app/agents/[agentName]/history/actions.ts +12 -0
  31. package/apps/agents-server/src/app/agents/[agentName]/history/page.tsx +62 -0
  32. package/apps/agents-server/src/app/agents/[agentName]/images/icon-256.png/route.tsx +80 -0
  33. package/apps/agents-server/src/app/agents/[agentName]/images/screenshot-fullhd.png/route.tsx +92 -0
  34. package/apps/agents-server/src/app/agents/[agentName]/images/screenshot-phone.png/route.tsx +92 -0
  35. package/apps/agents-server/src/app/agents/[agentName]/integration/page.tsx +61 -0
  36. package/apps/agents-server/src/app/agents/[agentName]/opengraph-image.tsx +102 -0
  37. package/apps/agents-server/src/app/agents/[agentName]/page.tsx +46 -24
  38. package/apps/agents-server/src/app/api/agents/[agentName]/clone/route.ts +47 -0
  39. package/apps/agents-server/src/app/api/agents/[agentName]/route.ts +19 -0
  40. package/apps/agents-server/src/app/api/agents/route.ts +22 -13
  41. package/apps/agents-server/src/app/api/auth/login/route.ts +6 -44
  42. package/apps/agents-server/src/app/api/chat-feedback/[id]/route.ts +38 -0
  43. package/apps/agents-server/src/app/api/chat-feedback/route.ts +157 -0
  44. package/apps/agents-server/src/app/api/chat-history/[id]/route.ts +37 -0
  45. package/apps/agents-server/src/app/api/chat-history/route.ts +147 -0
  46. package/apps/agents-server/src/app/api/federated-agents/route.ts +17 -0
  47. package/apps/agents-server/src/app/api/upload/route.ts +9 -1
  48. package/apps/agents-server/src/app/docs/[docId]/page.tsx +62 -0
  49. package/apps/agents-server/src/app/docs/page.tsx +33 -0
  50. package/apps/agents-server/src/app/layout.tsx +29 -3
  51. package/apps/agents-server/src/app/manifest.ts +109 -0
  52. package/apps/agents-server/src/app/page.tsx +8 -45
  53. package/apps/agents-server/src/app/recycle-bin/RestoreAgentButton.tsx +40 -0
  54. package/apps/agents-server/src/app/recycle-bin/actions.ts +27 -0
  55. package/apps/agents-server/src/app/recycle-bin/page.tsx +58 -0
  56. package/apps/agents-server/src/app/restricted/page.tsx +33 -0
  57. package/apps/agents-server/src/app/test/og-image/README.md +1 -0
  58. package/apps/agents-server/src/app/test/og-image/opengraph-image.tsx +37 -0
  59. package/apps/agents-server/src/app/test/og-image/page.tsx +22 -0
  60. package/apps/agents-server/src/components/Footer/Footer.tsx +175 -0
  61. package/apps/agents-server/src/components/Header/Header.tsx +445 -79
  62. package/apps/agents-server/src/components/Homepage/AgentCard.tsx +46 -14
  63. package/apps/agents-server/src/components/Homepage/AgentsList.tsx +58 -0
  64. package/apps/agents-server/src/components/Homepage/Card.tsx +1 -1
  65. package/apps/agents-server/src/components/Homepage/ExternalAgentsSection.tsx +21 -0
  66. package/apps/agents-server/src/components/Homepage/ExternalAgentsSectionClient.tsx +183 -0
  67. package/apps/agents-server/src/components/Homepage/ModelsSection.tsx +75 -0
  68. package/apps/agents-server/src/components/LayoutWrapper/LayoutWrapper.tsx +28 -3
  69. package/apps/agents-server/src/components/LoginDialog/LoginDialog.tsx +18 -17
  70. package/apps/agents-server/src/components/Portal/Portal.tsx +38 -0
  71. package/apps/agents-server/src/components/UsersList/UsersList.tsx +82 -131
  72. package/apps/agents-server/src/components/UsersList/useUsersAdmin.ts +139 -0
  73. package/apps/agents-server/src/database/metadataDefaults.ts +38 -6
  74. package/apps/agents-server/src/middleware.ts +146 -93
  75. package/apps/agents-server/src/tools/$provideServer.ts +2 -2
  76. package/apps/agents-server/src/utils/authenticateUser.ts +42 -0
  77. package/apps/agents-server/src/utils/chatFeedbackAdmin.ts +96 -0
  78. package/apps/agents-server/src/utils/chatHistoryAdmin.ts +96 -0
  79. package/apps/agents-server/src/utils/getEffectiveFederatedServers.ts +22 -0
  80. package/apps/agents-server/src/utils/getFederatedAgents.ts +31 -8
  81. package/apps/agents-server/src/utils/getFederatedServersFromMetadata.ts +10 -0
  82. package/apps/agents-server/src/utils/getVisibleCommitmentDefinitions.ts +12 -0
  83. package/apps/agents-server/src/utils/isUserAdmin.ts +2 -2
  84. package/apps/agents-server/vercel.json +7 -0
  85. package/esm/index.es.js +344 -11
  86. package/esm/index.es.js.map +1 -1
  87. package/esm/typings/servers.d.ts +8 -1
  88. package/esm/typings/src/_packages/components.index.d.ts +2 -0
  89. package/esm/typings/src/_packages/core.index.d.ts +6 -0
  90. package/esm/typings/src/_packages/types.index.d.ts +2 -0
  91. package/esm/typings/src/_packages/utils.index.d.ts +2 -0
  92. package/esm/typings/src/book-2.0/agent-source/AgentBasicInformation.d.ts +1 -0
  93. package/esm/typings/src/book-2.0/agent-source/AgentModelRequirements.d.ts +7 -0
  94. package/esm/typings/src/book-2.0/agent-source/createCommitmentRegex.d.ts +2 -2
  95. package/esm/typings/src/book-components/Chat/Chat/ChatProps.d.ts +10 -0
  96. package/esm/typings/src/book-components/PromptbookAgent/PromptbookAgent.d.ts +6 -0
  97. package/esm/typings/src/book-components/_common/HamburgerMenu/HamburgerMenu.d.ts +12 -0
  98. package/esm/typings/src/book-components/icons/MicIcon.d.ts +8 -0
  99. package/esm/typings/src/collection/agent-collection/constructors/agent-collection-in-supabase/AgentCollectionInSupabase.d.ts +17 -0
  100. package/esm/typings/src/commitments/MESSAGE/AgentMessageCommitmentDefinition.d.ts +28 -0
  101. package/esm/typings/src/commitments/MESSAGE/UserMessageCommitmentDefinition.d.ts +28 -0
  102. package/esm/typings/src/commitments/META_COLOR/META_COLOR.d.ts +38 -0
  103. package/esm/typings/src/commitments/_base/BaseCommitmentDefinition.d.ts +2 -1
  104. package/esm/typings/src/commitments/index.d.ts +22 -1
  105. package/esm/typings/src/execution/LlmExecutionTools.d.ts +9 -0
  106. package/esm/typings/src/llm-providers/agent/Agent.d.ts +1 -0
  107. package/esm/typings/src/llm-providers/agent/AgentLlmExecutionTools.d.ts +2 -1
  108. package/esm/typings/src/llm-providers/agent/RemoteAgent.d.ts +10 -1
  109. package/esm/typings/src/utils/normalization/normalizeMessageText.d.ts +9 -0
  110. package/esm/typings/src/utils/normalization/normalizeMessageText.test.d.ts +1 -0
  111. package/esm/typings/src/version.d.ts +1 -1
  112. package/package.json +1 -1
  113. package/umd/index.umd.js +344 -11
  114. package/umd/index.umd.js.map +1 -1
@@ -75,114 +75,167 @@ export async function middleware(req: NextRequest) {
75
75
  const allowedIps =
76
76
  allowedIpsMetadata !== null && allowedIpsMetadata !== undefined ? allowedIpsMetadata : allowedIpsEnv;
77
77
 
78
- if (isIpAllowed(ip, allowedIps)) {
79
- // Handle OPTIONS (preflight) requests before any redirects
80
- // to avoid CORS issues ("Redirect is not allowed for a preflight request")
81
- if (req.method === 'OPTIONS') {
82
- return new NextResponse(null, {
83
- status: 200,
84
- headers: {
85
- 'Access-Control-Allow-Origin': '*',
86
- 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
87
- 'Access-Control-Allow-Headers': 'Content-Type',
88
- },
89
- });
78
+ const isIpAllowedResult = isIpAllowed(ip, allowedIps);
79
+ const isLoggedIn = req.cookies.has('sessionToken');
80
+ const isAccessRestricted = !isIpAllowedResult && !isLoggedIn;
81
+
82
+ // Handle OPTIONS (preflight) requests globally
83
+ if (req.method === 'OPTIONS') {
84
+ return new NextResponse(null, {
85
+ status: 200,
86
+ headers: {
87
+ 'Access-Control-Allow-Origin': '*',
88
+ 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
89
+ 'Access-Control-Allow-Headers': 'Content-Type',
90
+ },
91
+ });
92
+ }
93
+
94
+ if (isAccessRestricted) {
95
+ const path = req.nextUrl.pathname;
96
+
97
+ // Allow specific paths for restricted users
98
+ // - /: Homepage / Agent List
99
+ // - /agents: Agent List
100
+ // - /api/agents: Agent List API
101
+ // - /api/federated-agents: Federated Agent List API
102
+ // - /api/auth/*: Auth endpoints
103
+ // - /restricted: Restricted Access Page
104
+ // - /docs: Documentation
105
+ // - /manifest.webmanifest: Manifest
106
+ // - /sw.js: Service Worker
107
+ const isAllowedPath =
108
+ path === '/' ||
109
+ path === '/agents' ||
110
+ path.startsWith('/api/agents') ||
111
+ path.startsWith('/api/federated-agents') ||
112
+ path.startsWith('/api/auth') ||
113
+ path === '/restricted' ||
114
+ path.startsWith('/docs') ||
115
+ path === '/manifest.webmanifest' ||
116
+ path === '/sw.js';
117
+
118
+ if (isAllowedPath) {
119
+ return NextResponse.next();
90
120
  }
91
121
 
92
- // 3. Redirect /:agentName/* to /agents/:agentName/*
93
- // This enables accessing agents from the root path
94
- const pathParts = req.nextUrl.pathname.split('/');
95
- const potentialAgentName = pathParts[1];
96
-
97
- if (
98
- potentialAgentName &&
99
- !['agents', 'api', 'admin', 'embed', '_next', 'favicon.ico'].includes(potentialAgentName) &&
100
- !potentialAgentName.startsWith('.') &&
101
- // Note: Other static files are excluded by the matcher configuration below
102
- true
103
- ) {
122
+ // Block access to other paths (e.g. Chat)
123
+ if (req.headers.get('accept')?.includes('text/html')) {
104
124
  const url = req.nextUrl.clone();
105
- url.pathname = `/agents${req.nextUrl.pathname}`;
106
- const response = NextResponse.redirect(url);
125
+ url.pathname = '/restricted';
126
+ return NextResponse.rewrite(url);
127
+ }
128
+ return new NextResponse('Forbidden', { status: 403 });
129
+ }
107
130
 
108
- // Enable CORS for the redirect
109
- response.headers.set('Access-Control-Allow-Origin', '*');
110
- response.headers.set('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
111
- response.headers.set('Access-Control-Allow-Headers', 'Content-Type');
131
+ // If we are here, the user is allowed (either by IP or session)
132
+ // Proceed with normal logic
133
+
134
+ // 3. Redirect /:agentName/* to /agents/:agentName/*
135
+ // This enables accessing agents from the root path
136
+ const pathParts = req.nextUrl.pathname.split('/');
137
+ const potentialAgentName = pathParts[1];
138
+
139
+ if (
140
+ potentialAgentName &&
141
+ ![
142
+ 'agents',
143
+ 'api',
144
+ 'admin',
145
+ 'docs',
146
+ 'manifest.webmanifest',
147
+ 'sw.js',
148
+ 'test',
149
+ 'embed',
150
+ '_next',
151
+ 'favicon.ico',
152
+ ].includes(potentialAgentName) &&
153
+ !potentialAgentName.startsWith('.') &&
154
+ // Note: Other static files are excluded by the matcher configuration below
155
+ true
156
+ ) {
157
+ const url = req.nextUrl.clone();
158
+ url.pathname = `/agents${req.nextUrl.pathname}`;
159
+ const response = NextResponse.redirect(url);
160
+
161
+ // Enable CORS for the redirect
162
+ response.headers.set('Access-Control-Allow-Origin', '*');
163
+ response.headers.set('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
164
+ response.headers.set('Access-Control-Allow-Headers', 'Content-Type');
165
+
166
+ return response;
167
+ }
112
168
 
113
- return response;
114
- }
169
+ // 4. Custom Domain Routing
170
+ // If the host is not one of the configured SERVERS, try to find an agent with a matching META LINK
115
171
 
116
- // 4. Custom Domain Routing
117
- // If the host is not one of the configured SERVERS, try to find an agent with a matching META LINK
172
+ if (host && SERVERS && !SERVERS.some((server) => server === host)) {
173
+ const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
174
+ const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY || process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
118
175
 
119
- if (host && SERVERS && !SERVERS.some((server) => server === host)) {
120
- const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
121
- const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY || process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
176
+ if (supabaseUrl && supabaseKey) {
177
+ const supabase = createClient(supabaseUrl, supabaseKey, {
178
+ auth: {
179
+ persistSession: false,
180
+ autoRefreshToken: false,
181
+ },
182
+ });
122
183
 
123
- if (supabaseUrl && supabaseKey) {
124
- const supabase = createClient(supabaseUrl, supabaseKey, {
125
- auth: {
126
- persistSession: false,
127
- autoRefreshToken: false,
128
- },
129
- });
184
+ // Determine prefixes to check
185
+ // We check all configured servers because the custom domain could point to any of them
186
+ // (or if they share the database, we need to check the relevant tables)
187
+ const serversToCheck = SERVERS;
130
188
 
131
- // Determine prefixes to check
132
- // We check all configured servers because the custom domain could point to any of them
133
- // (or if they share the database, we need to check the relevant tables)
134
- const serversToCheck = SERVERS;
135
-
136
- // TODO: [🧠] If there are many servers, this loop might be slow. Optimize if needed.
137
- for (const serverHost of serversToCheck) {
138
- let serverName = serverHost;
139
- serverName = serverName.replace(/\.ptbk\.io$/, '');
140
- serverName = normalizeTo_PascalCase(serverName);
141
- const prefix = `server_${serverName}_`;
142
-
143
- // Search for agent with matching META LINK
144
- // agentProfile->links is an array of strings
145
- // We check if it contains the host, or https://host, or http://host
146
-
147
- const searchLinks = [host, `https://${host}`, `http://${host}`];
148
-
149
- // Construct OR filter: agentProfile.cs.{"links":["link1"]},agentProfile.cs.{"links":["link2"]},...
150
- const orFilter = searchLinks.map((link) => `agentProfile.cs.{"links":["${link}"]}`).join(',');
151
-
152
- try {
153
- const { data } = await supabase
154
- .from(`${prefix}Agent`)
155
- .select('agentName')
156
- .or(orFilter)
157
- .limit(1)
158
- .single();
159
-
160
- if (data && data.agentName) {
161
- // Found the agent!
162
- const url = req.nextUrl.clone();
163
- url.pathname = `/${data.agentName}`;
164
-
165
- // Pass the server context to the app via header
166
- const requestHeaders = new Headers(req.headers);
167
- requestHeaders.set('x-promptbook-server', serverHost);
168
-
169
- return NextResponse.rewrite(url, {
170
- request: {
171
- headers: requestHeaders,
172
- },
173
- });
174
- }
175
- } catch (error) {
176
- // Ignore error (e.g. table not found, or agent not found) and continue to next server
177
- // console.error(`Error checking server ${serverHost} for custom domain ${host}:`, error);
189
+ // TODO: [🧠] If there are many servers, this loop might be slow. Optimize if needed.
190
+ for (const serverHost of serversToCheck) {
191
+ let serverName = serverHost;
192
+ serverName = serverName.replace(/\.ptbk\.io$/, '');
193
+ serverName = normalizeTo_PascalCase(serverName);
194
+ const prefix = `server_${serverName}_`;
195
+
196
+ // Search for agent with matching META LINK
197
+ // agentProfile->links is an array of strings
198
+ // We check if it contains the host, or https://host, or http://host
199
+
200
+ const searchLinks = [host, `https://${host}`, `http://${host}`];
201
+
202
+ // Construct OR filter: agentProfile.cs.{"links":["link1"]},agentProfile.cs.{"links":["link2"]},...
203
+ const orFilter = searchLinks.map((link) => `agentProfile.cs.{"links":["${link}"]}`).join(',');
204
+
205
+ try {
206
+ const { data } = await supabase
207
+ .from(`${prefix}Agent`)
208
+ .select('agentName')
209
+ .or(orFilter)
210
+ .limit(1)
211
+ .single();
212
+
213
+ if (data && data.agentName) {
214
+ // Found the agent!
215
+ const url = req.nextUrl.clone();
216
+ url.pathname = `/${data.agentName}`;
217
+
218
+ // Pass the server context to the app via header
219
+ const requestHeaders = new Headers(req.headers);
220
+ requestHeaders.set('x-promptbook-server', serverHost);
221
+
222
+ return NextResponse.rewrite(url, {
223
+ request: {
224
+ headers: requestHeaders,
225
+ },
226
+ });
178
227
  }
228
+ } catch (error) {
229
+ // Ignore error (e.g. table not found, or agent not found) and continue to next server
230
+ // console.error(`Error checking server ${serverHost} for custom domain ${host}:`, error);
179
231
  }
180
232
  }
181
233
  }
182
-
183
- return NextResponse.next();
184
234
  }
185
235
 
236
+ return NextResponse.next();
237
+
238
+ // This part should be unreachable due to logic above, but keeping as fallback
186
239
  return new NextResponse('Forbidden', { status: 403 });
187
240
  }
188
241
 
@@ -1,11 +1,11 @@
1
- import { NEXT_PUBLIC_URL, SERVERS, SUPABASE_TABLE_PREFIX } from '@/config';
1
+ import { NEXT_PUBLIC_SITE_URL, SERVERS, SUPABASE_TABLE_PREFIX } from '@/config';
2
2
  import { normalizeTo_PascalCase } from '@promptbook-local/utils';
3
3
  import { headers } from 'next/headers';
4
4
 
5
5
  export async function $provideServer() {
6
6
  if (!SERVERS) {
7
7
  return {
8
- publicUrl: NEXT_PUBLIC_URL || new URL(`https://${(await headers()).get('host') || 'localhost:4440'}`),
8
+ publicUrl: NEXT_PUBLIC_SITE_URL || new URL(`https://${(await headers()).get('host') || 'localhost:4440'}`),
9
9
  tablePrefix: SUPABASE_TABLE_PREFIX,
10
10
  };
11
11
  }
@@ -0,0 +1,42 @@
1
+ import { $getTableName } from '../database/$getTableName';
2
+ import { $provideSupabaseForServer } from '../database/$provideSupabaseForServer';
3
+ import { AgentsServerDatabase } from '../database/schema';
4
+ import { verifyPassword } from './auth';
5
+
6
+ export type AuthenticatedUser = {
7
+ username: string;
8
+ isAdmin: boolean;
9
+ };
10
+
11
+ export async function authenticateUser(username: string, password: string): Promise<AuthenticatedUser | null> {
12
+ // 1. Check if it's the environment admin
13
+ if (username === 'admin' && process.env.ADMIN_PASSWORD && password === process.env.ADMIN_PASSWORD) {
14
+ return { username: 'admin', isAdmin: true };
15
+ }
16
+
17
+ // 2. Check DB users
18
+ try {
19
+ const supabase = $provideSupabaseForServer();
20
+ const { data: user, error } = await supabase
21
+ .from(await $getTableName('User'))
22
+ .select('*')
23
+ .eq('username', username)
24
+ .single();
25
+
26
+ if (error || !user) {
27
+ return null;
28
+ }
29
+
30
+ const userRow = user as AgentsServerDatabase['public']['Tables']['User']['Row'];
31
+ const isValid = await verifyPassword(password, userRow.passwordHash);
32
+
33
+ if (!isValid) {
34
+ return null;
35
+ }
36
+
37
+ return { username: userRow.username, isAdmin: userRow.isAdmin };
38
+ } catch (error) {
39
+ console.error('Authentication error:', error);
40
+ return null;
41
+ }
42
+ }
@@ -0,0 +1,96 @@
1
+ import type { AgentsServerDatabase } from '../database/schema';
2
+
3
+ export type ChatFeedbackRow = AgentsServerDatabase['public']['Tables']['ChatFeedback']['Row'];
4
+ export type ChatFeedbackSortField = 'createdAt' | 'agentName' | 'id';
5
+ export type ChatFeedbackSortOrder = 'asc' | 'desc';
6
+
7
+ export type ChatFeedbackListResponse = {
8
+ items: ChatFeedbackRow[];
9
+ total: number;
10
+ page: number;
11
+ pageSize: number;
12
+ sortBy: ChatFeedbackSortField;
13
+ sortOrder: ChatFeedbackSortOrder;
14
+ };
15
+
16
+ export type ChatFeedbackListParams = {
17
+ page?: number;
18
+ pageSize?: number;
19
+ agentName?: string;
20
+ search?: string;
21
+ sortBy?: ChatFeedbackSortField;
22
+ sortOrder?: ChatFeedbackSortOrder;
23
+ };
24
+
25
+ /**
26
+ * Build query string for chat feedback listing.
27
+ *
28
+ * Kept in a dedicated helper so it can be shared between the
29
+ * admin feedback page and other admin UIs (per-agent tools, etc.).
30
+ */
31
+ function buildQuery(params: ChatFeedbackListParams): string {
32
+ const searchParams = new URLSearchParams();
33
+
34
+ if (params.page && params.page > 0) searchParams.set('page', String(params.page));
35
+ if (params.pageSize && params.pageSize > 0) searchParams.set('pageSize', String(params.pageSize));
36
+ if (params.agentName) searchParams.set('agentName', params.agentName);
37
+ if (params.search) searchParams.set('search', params.search);
38
+ if (params.sortBy) searchParams.set('sortBy', params.sortBy);
39
+ if (params.sortOrder) searchParams.set('sortOrder', params.sortOrder);
40
+
41
+ const qs = searchParams.toString();
42
+ return qs ? `?${qs}` : '';
43
+ }
44
+
45
+ /**
46
+ * Fetch chat feedback from the admin API.
47
+ */
48
+ export async function $fetchChatFeedback(params: ChatFeedbackListParams = {}): Promise<ChatFeedbackListResponse> {
49
+ const qs = buildQuery(params);
50
+ const response = await fetch(`/api/chat-feedback${qs}`, {
51
+ method: 'GET',
52
+ });
53
+
54
+ if (!response.ok) {
55
+ const data = await response.json().catch(() => ({}));
56
+ throw new Error(data.error || 'Failed to load chat feedback');
57
+ }
58
+
59
+ return (await response.json()) as ChatFeedbackListResponse;
60
+ }
61
+
62
+ /**
63
+ * Clear chat feedback for a specific agent.
64
+ */
65
+ export async function $clearAgentChatFeedback(agentName: string): Promise<void> {
66
+ if (!agentName) {
67
+ throw new Error('agentName is required to clear chat feedback');
68
+ }
69
+
70
+ const response = await fetch(`/api/chat-feedback?agentName=${encodeURIComponent(agentName)}`, {
71
+ method: 'DELETE',
72
+ });
73
+
74
+ if (!response.ok) {
75
+ const data = await response.json().catch(() => ({}));
76
+ throw new Error(data.error || 'Failed to clear chat feedback');
77
+ }
78
+ }
79
+
80
+ /**
81
+ * Delete a single chat feedback row by ID.
82
+ */
83
+ export async function $deleteChatFeedbackRow(id: number): Promise<void> {
84
+ if (!id || id <= 0) {
85
+ throw new Error('Valid id is required to delete chat feedback row');
86
+ }
87
+
88
+ const response = await fetch(`/api/chat-feedback/${encodeURIComponent(String(id))}`, {
89
+ method: 'DELETE',
90
+ });
91
+
92
+ if (!response.ok) {
93
+ const data = await response.json().catch(() => ({}));
94
+ throw new Error(data.error || 'Failed to delete chat feedback row');
95
+ }
96
+ }
@@ -0,0 +1,96 @@
1
+ import type { AgentsServerDatabase } from '../database/schema';
2
+
3
+ export type ChatHistoryRow = AgentsServerDatabase['public']['Tables']['ChatHistory']['Row'];
4
+ export type ChatHistorySortField = 'createdAt' | 'agentName' | 'id';
5
+ export type ChatHistorySortOrder = 'asc' | 'desc';
6
+
7
+ export type ChatHistoryListResponse = {
8
+ items: ChatHistoryRow[];
9
+ total: number;
10
+ page: number;
11
+ pageSize: number;
12
+ sortBy: ChatHistorySortField;
13
+ sortOrder: ChatHistorySortOrder;
14
+ };
15
+
16
+ export type ChatHistoryListParams = {
17
+ page?: number;
18
+ pageSize?: number;
19
+ agentName?: string;
20
+ search?: string;
21
+ sortBy?: ChatHistorySortField;
22
+ sortOrder?: ChatHistorySortOrder;
23
+ };
24
+
25
+ /**
26
+ * Build query string for chat history listing.
27
+ */
28
+ function buildQuery(params: ChatHistoryListParams): string {
29
+ const searchParams = new URLSearchParams();
30
+
31
+ if (params.page && params.page > 0) searchParams.set('page', String(params.page));
32
+ if (params.pageSize && params.pageSize > 0) searchParams.set('pageSize', String(params.pageSize));
33
+ if (params.agentName) searchParams.set('agentName', params.agentName);
34
+ if (params.search) searchParams.set('search', params.search);
35
+ if (params.sortBy) searchParams.set('sortBy', params.sortBy);
36
+ if (params.sortOrder) searchParams.set('sortOrder', params.sortOrder);
37
+
38
+ const qs = searchParams.toString();
39
+ return qs ? `?${qs}` : '';
40
+ }
41
+
42
+ /**
43
+ * Fetch chat history from the admin API.
44
+ *
45
+ * This is shared between the admin page and other admin UIs (e.g. per-agent tools)
46
+ * to keep the API surface DRY.
47
+ */
48
+ export async function $fetchChatHistory(params: ChatHistoryListParams = {}): Promise<ChatHistoryListResponse> {
49
+ const qs = buildQuery(params);
50
+ const response = await fetch(`/api/chat-history${qs}`, {
51
+ method: 'GET',
52
+ });
53
+
54
+ if (!response.ok) {
55
+ const data = await response.json().catch(() => ({}));
56
+ throw new Error(data.error || 'Failed to load chat history');
57
+ }
58
+
59
+ return (await response.json()) as ChatHistoryListResponse;
60
+ }
61
+
62
+ /**
63
+ * Clear chat history for a specific agent.
64
+ */
65
+ export async function $clearAgentChatHistory(agentName: string): Promise<void> {
66
+ if (!agentName) {
67
+ throw new Error('agentName is required to clear chat history');
68
+ }
69
+
70
+ const response = await fetch(`/api/chat-history?agentName=${encodeURIComponent(agentName)}`, {
71
+ method: 'DELETE',
72
+ });
73
+
74
+ if (!response.ok) {
75
+ const data = await response.json().catch(() => ({}));
76
+ throw new Error(data.error || 'Failed to clear chat history');
77
+ }
78
+ }
79
+
80
+ /**
81
+ * Delete a single chat history row by ID.
82
+ */
83
+ export async function $deleteChatHistoryRow(id: number): Promise<void> {
84
+ if (!id || id <= 0) {
85
+ throw new Error('Valid id is required to delete chat history row');
86
+ }
87
+
88
+ const response = await fetch(`/api/chat-history/${encodeURIComponent(String(id))}`, {
89
+ method: 'DELETE',
90
+ });
91
+
92
+ if (!response.ok) {
93
+ const data = await response.json().catch(() => ({}));
94
+ throw new Error(data.error || 'Failed to delete chat history row');
95
+ }
96
+ }
@@ -0,0 +1,22 @@
1
+ import type { string_promptbook_server_url } from '../../../../src/types/typeAliases';
2
+ import { AUTO_FEDERATED_AGENT_SERVER_URLS } from '../../../../servers';
3
+
4
+ /**
5
+ * Computes the effective list of federated servers for the current Agents Server.
6
+ *
7
+ * - Combines servers listed in the FEDERATED_SERVERS metadata (comma-separated)
8
+ * with a set of auto-federated servers defined in the root servers.ts config.
9
+ * - Ensures uniqueness and trims whitespace.
10
+ */
11
+ export function getEffectiveFederatedServers(federatedServersString: string): Array<string_promptbook_server_url> {
12
+ const manualFederatedServers = federatedServersString
13
+ .split(',')
14
+ .map((s) => s.trim())
15
+ .filter((s): s is string_promptbook_server_url => s !== '');
16
+
17
+ const merged = Array.from(
18
+ new Set<string_promptbook_server_url>([...manualFederatedServers, ...AUTO_FEDERATED_AGENT_SERVER_URLS]),
19
+ );
20
+
21
+ return merged;
22
+ }
@@ -11,13 +11,18 @@ type AgentsApiResponse = {
11
11
  federatedServers: string[];
12
12
  };
13
13
 
14
+ export type AgentsByServer = {
15
+ serverUrl: string;
16
+ agents: AgentWithUrl[];
17
+ };
18
+
14
19
  /**
15
20
  * Fetches agents from federated servers recursively
16
21
  */
17
- export async function getFederatedAgents(initialServers: string[]): Promise<AgentWithUrl[]> {
22
+ export async function getFederatedAgents(initialServers: string[]): Promise<AgentsByServer[]> {
18
23
  const visited = new Set<string>();
19
24
  const queue = [...initialServers];
20
- const externalAgentsMap = new Map<string, AgentWithUrl>();
25
+ const agentsByServer = new Map<string, AgentWithUrl[]>();
21
26
  const MAX_SERVERS = 20;
22
27
 
23
28
  while (queue.length > 0 && visited.size < MAX_SERVERS) {
@@ -30,10 +35,10 @@ export async function getFederatedAgents(initialServers: string[]): Promise<Agen
30
35
 
31
36
  try {
32
37
  // TODO: [🧠] Should we use some shorter timeout?
33
- const response = await fetch(`${normalizedUrl}/api/agents`, {
34
- next: { revalidate: 600 } // Cache for 10 minutes
38
+ const response = await fetch(`${normalizedUrl}/api/agents`, {
39
+ next: { revalidate: 600 }, // Cache for 10 minutes
35
40
  });
36
-
41
+
37
42
  if (!response.ok) {
38
43
  console.warn(`Failed to fetch agents from ${normalizedUrl}: ${response.status} ${response.statusText}`);
39
44
  continue;
@@ -42,11 +47,26 @@ export async function getFederatedAgents(initialServers: string[]): Promise<Agen
42
47
  const data: AgentsApiResponse = await response.json();
43
48
 
44
49
  if (data.agents && Array.isArray(data.agents)) {
50
+ const serverAgents: AgentWithUrl[] = [];
51
+ const existingAgentUrls = new Set<string>();
52
+
53
+ // Collect all existing agent URLs across all servers
54
+ for (const agents of agentsByServer.values()) {
55
+ for (const agent of agents) {
56
+ existingAgentUrls.add(agent.url);
57
+ }
58
+ }
59
+
45
60
  for (const agent of data.agents) {
46
- if (agent.url && !externalAgentsMap.has(agent.url)) {
47
- externalAgentsMap.set(agent.url, agent);
61
+ if (agent.url && !existingAgentUrls.has(agent.url)) {
62
+ serverAgents.push(agent);
63
+ existingAgentUrls.add(agent.url);
48
64
  }
49
65
  }
66
+
67
+ if (serverAgents.length > 0) {
68
+ agentsByServer.set(normalizedUrl, serverAgents);
69
+ }
50
70
  }
51
71
 
52
72
  if (data.federatedServers && Array.isArray(data.federatedServers)) {
@@ -62,5 +82,8 @@ export async function getFederatedAgents(initialServers: string[]): Promise<Agen
62
82
  }
63
83
  }
64
84
 
65
- return Array.from(externalAgentsMap.values());
85
+ return Array.from(agentsByServer.entries()).map(([serverUrl, agents]) => ({
86
+ serverUrl,
87
+ agents,
88
+ }));
66
89
  }
@@ -0,0 +1,10 @@
1
+ import { getMetadata } from '../database/getMetadata';
2
+ import { getEffectiveFederatedServers } from './getEffectiveFederatedServers';
3
+
4
+ /**
5
+ * Reads FEDERATED_SERVERS metadata and returns a normalized list of server URLs.
6
+ */
7
+ export async function getFederatedServersFromMetadata(): Promise<string[]> {
8
+ const federatedServersString = (await getMetadata('FEDERATED_SERVERS')) || '';
9
+ return getEffectiveFederatedServers(federatedServersString);
10
+ }
@@ -0,0 +1,12 @@
1
+ import { getGroupedCommitmentDefinitions } from '../../../../src/commitments';
2
+ import { NotYetImplementedCommitmentDefinition } from '../../../../src/commitments/_base/NotYetImplementedCommitmentDefinition';
3
+
4
+ /**
5
+ * Gets visible commitment definitions for documentation
6
+ * Excluding those that are not yet implemented
7
+ */
8
+ export function getVisibleCommitmentDefinitions() {
9
+ return getGroupedCommitmentDefinitions().filter(
10
+ (group) => !(group.primary instanceof NotYetImplementedCommitmentDefinition),
11
+ );
12
+ }
@@ -4,13 +4,13 @@ import { getSession } from './session';
4
4
  /**
5
5
  * Checks if the current user is an admin
6
6
  *
7
- * Note: If `process.env.ADMIN_PASSWORD` is not set, everyone is admin
7
+ * Note: If `process.env.ADMIN_PASSWORD` is not set, no one is admin
8
8
  *
9
9
  * @returns true if the user is admin
10
10
  */
11
11
  export async function isUserAdmin(): Promise<boolean> {
12
12
  if (!process.env.ADMIN_PASSWORD) {
13
- return true;
13
+ return false;
14
14
  }
15
15
 
16
16
  // Check legacy admin token
@@ -0,0 +1,7 @@
1
+ {
2
+ "build": {
3
+ "env": {
4
+ "VERCEL_FORCE_NO_BUILD_CACHE": "1"
5
+ }
6
+ }
7
+ }