@promptbook/cli 0.103.0-52 → 0.103.0-54

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 (142) 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/api-tokens/ApiTokensClient.tsx +186 -0
  8. package/apps/agents-server/src/app/admin/api-tokens/page.tsx +13 -0
  9. package/apps/agents-server/src/app/admin/chat-feedback/ChatFeedbackClient.tsx +541 -0
  10. package/apps/agents-server/src/app/admin/chat-feedback/page.tsx +22 -0
  11. package/apps/agents-server/src/app/admin/chat-history/ChatHistoryClient.tsx +532 -0
  12. package/apps/agents-server/src/app/admin/chat-history/page.tsx +21 -0
  13. package/apps/agents-server/src/app/admin/metadata/MetadataClient.tsx +241 -27
  14. package/apps/agents-server/src/app/admin/models/page.tsx +22 -0
  15. package/apps/agents-server/src/app/admin/users/[userId]/UserDetailClient.tsx +131 -0
  16. package/apps/agents-server/src/app/admin/users/[userId]/page.tsx +21 -0
  17. package/apps/agents-server/src/app/admin/users/page.tsx +18 -0
  18. package/apps/agents-server/src/app/agents/[agentName]/AgentChatWrapper.tsx +10 -2
  19. package/apps/agents-server/src/app/agents/[agentName]/ClearAgentChatFeedbackButton.tsx +63 -0
  20. package/apps/agents-server/src/app/agents/[agentName]/ClearAgentChatHistoryButton.tsx +63 -0
  21. package/apps/agents-server/src/app/agents/[agentName]/CloneAgentButton.tsx +41 -0
  22. package/apps/agents-server/src/app/agents/[agentName]/InstallPwaButton.tsx +74 -0
  23. package/apps/agents-server/src/app/agents/[agentName]/ServiceWorkerRegister.tsx +24 -0
  24. package/apps/agents-server/src/app/agents/[agentName]/_utils.ts +19 -0
  25. package/apps/agents-server/src/app/agents/[agentName]/api/agents/route.ts +67 -0
  26. package/apps/agents-server/src/app/agents/[agentName]/api/openai/chat/completions/route.ts +176 -0
  27. package/apps/agents-server/src/app/agents/[agentName]/api/profile/route.ts +3 -0
  28. package/apps/agents-server/src/app/agents/[agentName]/api/voice/route.ts +177 -0
  29. package/apps/agents-server/src/app/agents/[agentName]/book/page.tsx +3 -0
  30. package/apps/agents-server/src/app/agents/[agentName]/book+chat/AgentBookAndChat.tsx +53 -1
  31. package/apps/agents-server/src/app/agents/[agentName]/generateAgentMetadata.ts +11 -11
  32. package/apps/agents-server/src/app/agents/[agentName]/history/RestoreVersionButton.tsx +46 -0
  33. package/apps/agents-server/src/app/agents/[agentName]/history/actions.ts +12 -0
  34. package/apps/agents-server/src/app/agents/[agentName]/history/page.tsx +62 -0
  35. package/apps/agents-server/src/app/agents/[agentName]/images/icon-256.png/route.tsx +80 -0
  36. package/apps/agents-server/src/app/agents/[agentName]/images/screenshot-fullhd.png/route.tsx +92 -0
  37. package/apps/agents-server/src/app/agents/[agentName]/images/screenshot-phone.png/route.tsx +92 -0
  38. package/apps/agents-server/src/app/agents/[agentName]/integration/page.tsx +61 -0
  39. package/apps/agents-server/src/app/agents/[agentName]/opengraph-image.tsx +102 -0
  40. package/apps/agents-server/src/app/agents/[agentName]/page.tsx +64 -24
  41. package/apps/agents-server/src/app/api/agents/[agentName]/clone/route.ts +47 -0
  42. package/apps/agents-server/src/app/api/agents/[agentName]/route.ts +19 -0
  43. package/apps/agents-server/src/app/api/agents/route.ts +22 -13
  44. package/apps/agents-server/src/app/api/api-tokens/route.ts +76 -0
  45. package/apps/agents-server/src/app/api/auth/login/route.ts +6 -44
  46. package/apps/agents-server/src/app/api/chat-feedback/[id]/route.ts +38 -0
  47. package/apps/agents-server/src/app/api/chat-feedback/route.ts +157 -0
  48. package/apps/agents-server/src/app/api/chat-history/[id]/route.ts +37 -0
  49. package/apps/agents-server/src/app/api/chat-history/route.ts +147 -0
  50. package/apps/agents-server/src/app/api/federated-agents/route.ts +17 -0
  51. package/apps/agents-server/src/app/api/upload/route.ts +9 -1
  52. package/apps/agents-server/src/app/docs/[docId]/page.tsx +63 -0
  53. package/apps/agents-server/src/app/docs/page.tsx +34 -0
  54. package/apps/agents-server/src/app/layout.tsx +29 -3
  55. package/apps/agents-server/src/app/manifest.ts +109 -0
  56. package/apps/agents-server/src/app/page.tsx +8 -45
  57. package/apps/agents-server/src/app/recycle-bin/RestoreAgentButton.tsx +40 -0
  58. package/apps/agents-server/src/app/recycle-bin/actions.ts +27 -0
  59. package/apps/agents-server/src/app/recycle-bin/page.tsx +58 -0
  60. package/apps/agents-server/src/app/restricted/page.tsx +33 -0
  61. package/apps/agents-server/src/app/test/og-image/README.md +1 -0
  62. package/apps/agents-server/src/app/test/og-image/opengraph-image.tsx +37 -0
  63. package/apps/agents-server/src/app/test/og-image/page.tsx +22 -0
  64. package/apps/agents-server/src/components/Footer/Footer.tsx +175 -0
  65. package/apps/agents-server/src/components/Header/Header.tsx +450 -79
  66. package/apps/agents-server/src/components/Homepage/AgentCard.tsx +46 -14
  67. package/apps/agents-server/src/components/Homepage/AgentsList.tsx +58 -0
  68. package/apps/agents-server/src/components/Homepage/Card.tsx +1 -1
  69. package/apps/agents-server/src/components/Homepage/ExternalAgentsSection.tsx +21 -0
  70. package/apps/agents-server/src/components/Homepage/ExternalAgentsSectionClient.tsx +183 -0
  71. package/apps/agents-server/src/components/Homepage/ModelsSection.tsx +75 -0
  72. package/apps/agents-server/src/components/LayoutWrapper/LayoutWrapper.tsx +29 -3
  73. package/apps/agents-server/src/components/LoginDialog/LoginDialog.tsx +18 -17
  74. package/apps/agents-server/src/components/Portal/Portal.tsx +38 -0
  75. package/apps/agents-server/src/components/UsersList/UsersList.tsx +82 -131
  76. package/apps/agents-server/src/components/UsersList/useUsersAdmin.ts +139 -0
  77. package/apps/agents-server/src/database/metadataDefaults.ts +38 -6
  78. package/apps/agents-server/src/database/migrations/2025-12-0010-llm-cache.sql +12 -0
  79. package/apps/agents-server/src/database/migrations/2025-12-0060-api-tokens.sql +13 -0
  80. package/apps/agents-server/src/database/schema.ts +51 -0
  81. package/apps/agents-server/src/middleware.ts +193 -92
  82. package/apps/agents-server/src/tools/$provideCdnForServer.ts +3 -7
  83. package/apps/agents-server/src/tools/$provideExecutionToolsForServer.ts +10 -1
  84. package/apps/agents-server/src/tools/$provideServer.ts +2 -2
  85. package/apps/agents-server/src/utils/authenticateUser.ts +42 -0
  86. package/apps/agents-server/src/utils/cache/SupabaseCacheStorage.ts +55 -0
  87. package/apps/agents-server/src/utils/cdn/classes/VercelBlobStorage.ts +63 -0
  88. package/apps/agents-server/src/utils/chatFeedbackAdmin.ts +96 -0
  89. package/apps/agents-server/src/utils/chatHistoryAdmin.ts +96 -0
  90. package/apps/agents-server/src/utils/getEffectiveFederatedServers.ts +22 -0
  91. package/apps/agents-server/src/utils/getFederatedAgents.ts +31 -8
  92. package/apps/agents-server/src/utils/getFederatedServersFromMetadata.ts +10 -0
  93. package/apps/agents-server/src/utils/getVisibleCommitmentDefinitions.ts +12 -0
  94. package/apps/agents-server/src/utils/isUserAdmin.ts +2 -2
  95. package/apps/agents-server/vercel.json +7 -0
  96. package/esm/index.es.js +279 -2
  97. package/esm/index.es.js.map +1 -1
  98. package/esm/typings/servers.d.ts +8 -1
  99. package/esm/typings/src/_packages/components.index.d.ts +2 -0
  100. package/esm/typings/src/_packages/core.index.d.ts +6 -0
  101. package/esm/typings/src/_packages/types.index.d.ts +2 -0
  102. package/esm/typings/src/_packages/utils.index.d.ts +2 -0
  103. package/esm/typings/src/book-2.0/agent-source/AgentModelRequirements.d.ts +7 -0
  104. package/esm/typings/src/book-components/Chat/Chat/ChatProps.d.ts +4 -0
  105. package/esm/typings/src/book-components/_common/HamburgerMenu/HamburgerMenu.d.ts +12 -0
  106. package/esm/typings/src/book-components/icons/MicIcon.d.ts +8 -0
  107. package/esm/typings/src/collection/agent-collection/constructors/agent-collection-in-supabase/AgentCollectionInSupabase.d.ts +17 -0
  108. package/esm/typings/src/commitments/ACTION/ACTION.d.ts +4 -0
  109. package/esm/typings/src/commitments/DELETE/DELETE.d.ts +4 -0
  110. package/esm/typings/src/commitments/FORMAT/FORMAT.d.ts +4 -0
  111. package/esm/typings/src/commitments/GOAL/GOAL.d.ts +4 -0
  112. package/esm/typings/src/commitments/KNOWLEDGE/KNOWLEDGE.d.ts +4 -0
  113. package/esm/typings/src/commitments/MEMORY/MEMORY.d.ts +4 -0
  114. package/esm/typings/src/commitments/MESSAGE/AgentMessageCommitmentDefinition.d.ts +32 -0
  115. package/esm/typings/src/commitments/MESSAGE/InitialMessageCommitmentDefinition.d.ts +4 -0
  116. package/esm/typings/src/commitments/MESSAGE/MESSAGE.d.ts +4 -0
  117. package/esm/typings/src/commitments/MESSAGE/UserMessageCommitmentDefinition.d.ts +32 -0
  118. package/esm/typings/src/commitments/META/META.d.ts +4 -0
  119. package/esm/typings/src/commitments/META_COLOR/META_COLOR.d.ts +4 -0
  120. package/esm/typings/src/commitments/META_IMAGE/META_IMAGE.d.ts +4 -0
  121. package/esm/typings/src/commitments/META_LINK/META_LINK.d.ts +4 -0
  122. package/esm/typings/src/commitments/MODEL/MODEL.d.ts +4 -0
  123. package/esm/typings/src/commitments/NOTE/NOTE.d.ts +4 -0
  124. package/esm/typings/src/commitments/PERSONA/PERSONA.d.ts +4 -0
  125. package/esm/typings/src/commitments/RULE/RULE.d.ts +4 -0
  126. package/esm/typings/src/commitments/SAMPLE/SAMPLE.d.ts +4 -0
  127. package/esm/typings/src/commitments/SCENARIO/SCENARIO.d.ts +4 -0
  128. package/esm/typings/src/commitments/STYLE/STYLE.d.ts +4 -0
  129. package/esm/typings/src/commitments/_base/BaseCommitmentDefinition.d.ts +5 -0
  130. package/esm/typings/src/commitments/_base/CommitmentDefinition.d.ts +5 -0
  131. package/esm/typings/src/commitments/_base/NotYetImplementedCommitmentDefinition.d.ts +4 -0
  132. package/esm/typings/src/commitments/index.d.ts +20 -1
  133. package/esm/typings/src/execution/LlmExecutionTools.d.ts +9 -0
  134. package/esm/typings/src/llm-providers/agent/AgentLlmExecutionTools.d.ts +2 -1
  135. package/esm/typings/src/llm-providers/agent/RemoteAgent.d.ts +10 -1
  136. package/esm/typings/src/utils/normalization/normalizeMessageText.d.ts +9 -0
  137. package/esm/typings/src/utils/normalization/normalizeMessageText.test.d.ts +1 -0
  138. package/esm/typings/src/version.d.ts +1 -1
  139. package/package.json +2 -2
  140. package/umd/index.umd.js +279 -2
  141. package/umd/index.umd.js.map +1 -1
  142. package/apps/agents-server/src/utils/cdn/classes/DigitalOceanSpaces.ts +0 -119
@@ -1,39 +1,51 @@
1
1
  'use server';
2
2
 
3
- import { $provideAgentCollectionForServer } from '@/src/tools/$provideAgentCollectionForServer';
4
3
  // import { BookEditor } from '@promptbook-local/components';
5
4
  import { $provideServer } from '@/src/tools/$provideServer';
6
- import { parseAgentSource } from '@promptbook-local/core';
7
- import { Columns2Icon, MessagesSquareIcon, NotebookPenIcon } from 'lucide-react';
5
+ import { PROMPTBOOK_COLOR } from '@promptbook-local/core';
6
+ import { CodeIcon, HistoryIcon, MessageCircleQuestionIcon, MessageSquareIcon, NotebookPenIcon } from 'lucide-react';
8
7
  import { headers } from 'next/headers';
9
8
  import { notFound } from 'next/navigation';
10
9
  import { Color } from '../../../../../../src/utils/color/Color';
11
10
  import { withAlpha } from '../../../../../../src/utils/color/operators/withAlpha';
12
11
  import { $sideEffect } from '../../../../../../src/utils/organization/$sideEffect';
12
+ import { AGENT_ACTIONS, getAgentName, getAgentProfile } from './_utils';
13
13
  import { AgentChatWrapper } from './AgentChatWrapper';
14
14
  import { AgentQrCode } from './AgentQrCode';
15
15
  import { CopyField } from './CopyField';
16
16
  import { generateAgentMetadata } from './generateAgentMetadata';
17
+ import { InstallPwaButton } from './InstallPwaButton';
18
+ import { ServiceWorkerRegister } from './ServiceWorkerRegister';
19
+ import { ClearAgentChatHistoryButton } from './ClearAgentChatHistoryButton';
20
+ import { ClearAgentChatFeedbackButton } from './ClearAgentChatFeedbackButton';
21
+ import { CloneAgentButton } from './CloneAgentButton';
22
+ import { isUserAdmin } from '../../../utils/isUserAdmin';
17
23
  // import { Agent } from '@promptbook-local/core';
18
24
  // import { RemoteLlmExecutionTools } from '@promptbook-local/remote-client';
19
25
  // import { OpenAiAssistantExecutionTools } from '@promptbook-local/openai';
20
26
 
21
27
  export const generateMetadata = generateAgentMetadata;
22
28
 
23
- export default async function AgentPage({ params }: { params: Promise<{ agentName: string }> }) {
29
+ export default async function AgentPage({
30
+ params,
31
+ searchParams,
32
+ }: {
33
+ params: Promise<{ agentName: string }>;
34
+ searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
35
+ }) {
24
36
  // const [apiKey, setApiKey] = useStateInLocalStorage<string>('openai-apiKey', () => '');
25
37
  // const [isApiKeyVisible, setIsApiKeyVisible] = useState(false);
26
38
  // const [isApiKeySectionCollapsed, setIsApiKeySectionCollapsed] = useState(!!apiKey);
27
39
 
28
40
  $sideEffect(headers());
29
41
 
30
- let { agentName } = await params;
31
- agentName = decodeURIComponent(agentName);
42
+ const { message } = await searchParams;
43
+ const agentName = await getAgentName(params);
44
+ const isAdmin = await isUserAdmin();
32
45
 
33
- const collection = await $provideAgentCollectionForServer();
34
- let agentSource;
46
+ let agentProfile;
35
47
  try {
36
- agentSource = await collection.getAgentSource(agentName);
48
+ agentProfile = await getAgentProfile(agentName);
37
49
  } catch (error) {
38
50
  if (
39
51
  error instanceof Error &&
@@ -45,7 +57,6 @@ export default async function AgentPage({ params }: { params: Promise<{ agentNam
45
57
  }
46
58
  throw error;
47
59
  }
48
- const agentProfile = parseAgentSource(agentSource);
49
60
 
50
61
  const { publicUrl } = await $provideServer();
51
62
 
@@ -58,13 +69,15 @@ export default async function AgentPage({ params }: { params: Promise<{ agentNam
58
69
  console.log('[🐱‍🚀]', { pageUrl: agentUrl });
59
70
 
60
71
  // Extract brand color from meta
61
- const brandColor = Color.from(agentProfile.meta.color || '#3b82f6'); // Default to blue-600
72
+ const brandColor = Color.from(agentProfile.meta.color || PROMPTBOOK_COLOR);
62
73
 
63
74
  // Mock agent actions
64
- const agentActions = ['Emails', 'Web chat', 'Read documents', 'Browser', 'WhatsApp', '<Coding/>'];
75
+ const agentActions = AGENT_ACTIONS;
65
76
 
66
77
  return (
67
78
  <div className="flex flex-col md:flex-row h-[calc(100vh-60px)] w-full overflow-hidden">
79
+ <ServiceWorkerRegister scope={`/agents/${encodeURIComponent(agentName)}/`} />
80
+
68
81
  {/* Left sidebar: Profile info */}
69
82
  <div
70
83
  className="w-full md:w-[400px] flex flex-col gap-6 p-6 overflow-y-auto border-r bg-gray-50 flex-shrink-0"
@@ -114,33 +127,60 @@ export default async function AgentPage({ params }: { params: Promise<{ agentNam
114
127
  </div>
115
128
  </div>
116
129
 
117
- <div className="flex flex-col gap-2 mt-auto">
130
+ <div className="flex flex-col gap-3 mt-auto">
118
131
  <div className="flex gap-2">
119
132
  <a
120
- href={`/agents/${encodeURIComponent(agentName)}/chat`}
133
+ href={`/agents/${encodeURIComponent(agentName)}/book+chat`}
121
134
  // <- TODO: [🧠] Can I append path like this on current browser URL in href?
122
135
  className="flex-1 inline-flex items-center justify-center whitespace-nowrap bg-white hover:bg-gray-100 text-gray-800 px-4 py-2 rounded shadow font-semibold transition border border-gray-200"
123
136
  >
124
- <MessagesSquareIcon className="ml-2 w-4 h-4 mr-2" />
125
- Chat
137
+ <NotebookPenIcon className="ml-2 w-4 h-4 mr-2" />
138
+ Edit
126
139
  </a>
127
140
  <a
128
- href={`/agents/${encodeURIComponent(agentName)}/book+chat`}
141
+ href={`/agents/${encodeURIComponent(agentName)}/integration`}
129
142
  // <- TODO: [🧠] Can I append path like this on current browser URL in href?
130
143
  className="flex-1 inline-flex items-center justify-center whitespace-nowrap bg-white hover:bg-gray-100 text-gray-800 px-4 py-2 rounded shadow font-semibold transition border border-gray-200"
131
144
  >
132
- <Columns2Icon className="ml-2 w-4 h-4 mr-2" />
133
- Book + Chat
145
+ <CodeIcon className="ml-2 w-4 h-4 mr-2" />
146
+ Integration
134
147
  </a>
135
148
  <a
136
- href={`/agents/${encodeURIComponent(agentName)}/book`}
137
- // <- TODO: [🧠] Can I append path like this on current browser URL in href?
149
+ href={`/agents/${encodeURIComponent(agentName)}/history`}
138
150
  className="flex-1 inline-flex items-center justify-center whitespace-nowrap bg-white hover:bg-gray-100 text-gray-800 px-4 py-2 rounded shadow font-semibold transition border border-gray-200"
139
151
  >
140
- <NotebookPenIcon className="ml-2 w-4 h-4 mr-2" />
141
- Edit
152
+ <HistoryIcon className="ml-2 w-4 h-4 mr-2" />
153
+ History
142
154
  </a>
155
+ {isAdmin && <CloneAgentButton agentName={agentName} />}
156
+ <InstallPwaButton />
143
157
  </div>
158
+
159
+ {isAdmin && (
160
+ <div className="border-t border-dashed border-gray-300 pt-3">
161
+ <h2 className="mb-2 text-xs font-semibold uppercase tracking-wide text-gray-600">
162
+ Maintenance
163
+ </h2>
164
+ <div className="flex flex-col gap-2">
165
+ <a
166
+ href={`/admin/chat-history?agentName=${encodeURIComponent(agentName)}`}
167
+ className="inline-flex items-center justify-center whitespace-nowrap rounded-md border border-gray-300 bg-white px-3 py-1.5 text-xs font-semibold text-gray-700 shadow-sm hover:bg-gray-50"
168
+ >
169
+ <MessageSquareIcon className="mr-2 w-3 h-3" />
170
+ Chat history
171
+ </a>
172
+ <a
173
+ href={`/admin/chat-feedback?agentName=${encodeURIComponent(agentName)}`}
174
+ className="inline-flex items-center justify-center whitespace-nowrap rounded-md border border-gray-300 bg-white px-3 py-1.5 text-xs font-semibold text-gray-700 shadow-sm hover:bg-gray-50"
175
+ >
176
+ <MessageCircleQuestionIcon className="mr-2 w-3 h-3" />
177
+ Chat feedback
178
+ </a>
179
+ <ClearAgentChatHistoryButton agentName={agentName} />
180
+ <ClearAgentChatFeedbackButton agentName={agentName} />
181
+ </div>
182
+ </div>
183
+ )}
144
184
  </div>
145
185
 
146
186
  <div className="flex flex-col items-center gap-4 pt-6 border-t border-gray-200 w-full">
@@ -162,7 +202,7 @@ export default async function AgentPage({ params }: { params: Promise<{ agentNam
162
202
 
163
203
  {/* Main content: Chat */}
164
204
  <div className="flex-1 relative h-full bg-white">
165
- <AgentChatWrapper agentUrl={agentUrl} />
205
+ <AgentChatWrapper agentUrl={agentUrl} defaultMessage={message as string} />
166
206
  </div>
167
207
  </div>
168
208
  );
@@ -0,0 +1,47 @@
1
+ // POST /api/agents/[agentName]/clone
2
+ import { $provideAgentCollectionForServer } from '@/src/tools/$provideAgentCollectionForServer';
3
+ import { AgentBasicInformation } from '../../../../../../../../src/book-2.0/agent-source/AgentBasicInformation';
4
+ import { string_book } from '../../../../../../../../src/book-2.0/agent-source/string_book';
5
+ import { TODO_any } from '@promptbook-local/types';
6
+ import { NextResponse } from 'next/server';
7
+
8
+ export async function POST(request: Request, { params }: { params: Promise<{ agentName: string }> }) {
9
+ const { agentName } = await params;
10
+ const collection = await $provideAgentCollectionForServer();
11
+
12
+ try {
13
+ const source = await collection.getAgentSource(agentName);
14
+
15
+ // Generate new name
16
+ // TODO: [🧠] Better naming strategy, maybe check for collisions
17
+ let newAgentName = `${agentName} (Copy)`;
18
+ let counter = 1;
19
+
20
+ // eslint-disable-next-line no-constant-condition
21
+ while (true) {
22
+ try {
23
+ await collection.getAgentSource(newAgentName);
24
+ // If success, it means it exists, so we try next one
25
+ counter++;
26
+ newAgentName = `${agentName} (Copy ${counter})`;
27
+ } catch (error) {
28
+ // If error, it likely means it does not exist (NotFoundError), so we can use it
29
+ // TODO: [🧠] Check if it is really NotFoundError
30
+ break;
31
+ }
32
+ }
33
+
34
+ const lines = source.split('\n');
35
+ lines[0] = newAgentName;
36
+ const newSource = lines.join('\n') as string_book;
37
+
38
+ const newAgent = await collection.createAgent(newSource);
39
+
40
+ return NextResponse.json(newAgent);
41
+ } catch (error) {
42
+ return NextResponse.json(
43
+ { success: false, error: (error as TODO_any)?.message || 'Failed to clone agent' },
44
+ { status: 500 },
45
+ );
46
+ }
47
+ }
@@ -0,0 +1,19 @@
1
+ // DELETE /api/agents/[agentName]
2
+ import { $provideAgentCollectionForServer } from '@/src/tools/$provideAgentCollectionForServer';
3
+ import { TODO_any } from '@promptbook-local/types';
4
+ import { NextResponse } from 'next/server';
5
+
6
+ export async function DELETE(request: Request, { params }: { params: Promise<{ agentName: string }> }) {
7
+ const { agentName } = await params;
8
+ const collection = await $provideAgentCollectionForServer();
9
+
10
+ try {
11
+ await collection.deleteAgent(agentName);
12
+ return NextResponse.json({ success: true });
13
+ } catch (error) {
14
+ return NextResponse.json(
15
+ { success: false, error: (error as TODO_any)?.message || 'Failed to delete agent' },
16
+ { status: 500 },
17
+ );
18
+ }
19
+ }
@@ -1,6 +1,7 @@
1
+ import { $provideServer } from '@/src/tools/$provideServer';
1
2
  import { NextResponse } from 'next/server';
2
- import { getMetadata } from '../../../database/getMetadata';
3
3
  import { $provideAgentCollectionForServer } from '../../../tools/$provideAgentCollectionForServer';
4
+ import { getFederatedServersFromMetadata } from '../../../utils/getFederatedServersFromMetadata';
4
5
 
5
6
  export const dynamic = 'force-dynamic';
6
7
 
@@ -8,27 +9,35 @@ export async function GET() {
8
9
  try {
9
10
  const collection = await $provideAgentCollectionForServer();
10
11
  const agents = await collection.listAgents();
11
- const serverUrl = (await getMetadata('SERVER_URL')) || '';
12
- const federatedServersString = (await getMetadata('FEDERATED_SERVERS')) || '';
13
- const federatedServers = federatedServersString
14
- .split(',')
15
- .map((s) => s.trim())
16
- .filter((s) => s !== '');
12
+ const federatedServers = await getFederatedServersFromMetadata();
13
+ const { publicUrl } = await $provideServer();
17
14
 
18
15
  const agentsWithUrl = agents.map((agent) => ({
19
16
  ...agent,
20
- url: `${serverUrl}/${agent.agentName}`,
17
+ url: `${publicUrl.href}agents/${encodeURIComponent(agent.agentName)}`,
21
18
  }));
22
19
 
23
- return NextResponse.json({
20
+ const response = NextResponse.json({
24
21
  agents: agentsWithUrl,
25
22
  federatedServers,
26
23
  });
24
+
25
+ // Add CORS headers
26
+ response.headers.set('Access-Control-Allow-Origin', '*');
27
+ response.headers.set('Access-Control-Allow-Methods', 'GET, OPTIONS');
28
+ response.headers.set('Access-Control-Allow-Headers', 'Content-Type, Authorization');
29
+
30
+ return response;
27
31
  } catch (error) {
28
32
  console.error('Error fetching agents:', error);
29
- return NextResponse.json(
30
- { error: 'Failed to fetch agents' },
31
- { status: 500 },
32
- );
33
+ return NextResponse.json({ error: 'Failed to fetch agents' }, { status: 500 });
33
34
  }
34
35
  }
36
+
37
+ export async function OPTIONS() {
38
+ const response = new NextResponse(null, { status: 200 });
39
+ response.headers.set('Access-Control-Allow-Origin', '*');
40
+ response.headers.set('Access-Control-Allow-Methods', 'GET, OPTIONS');
41
+ response.headers.set('Access-Control-Allow-Headers', 'Content-Type, Authorization');
42
+ return response;
43
+ }
@@ -0,0 +1,76 @@
1
+ import { $getTableName } from '../../../database/$getTableName';
2
+ import { $provideSupabase } from '../../../database/$provideSupabase';
3
+ import { isUserAdmin } from '../../../utils/isUserAdmin';
4
+ import { randomUUID } from 'crypto';
5
+ import { NextRequest, NextResponse } from 'next/server';
6
+
7
+ export async function GET(request: NextRequest) {
8
+ if (!(await isUserAdmin())) {
9
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
10
+ }
11
+
12
+ const supabase = $provideSupabase();
13
+ const table = await $getTableName('ApiTokens');
14
+
15
+ const { data, error } = await supabase.from(table).select('*').order('createdAt', { ascending: false });
16
+
17
+ if (error) {
18
+ return NextResponse.json({ error: error.message }, { status: 500 });
19
+ }
20
+
21
+ return NextResponse.json(data);
22
+ }
23
+
24
+ export async function POST(request: NextRequest) {
25
+ if (!(await isUserAdmin())) {
26
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
27
+ }
28
+
29
+ try {
30
+ const body = await request.json();
31
+ const { note } = body;
32
+
33
+ const token = `ptbk_${randomUUID().replace(/-/g, '')}`;
34
+
35
+ const supabase = $provideSupabase();
36
+ const table = await $getTableName('ApiTokens');
37
+
38
+ const { data, error } = await supabase
39
+ .from(table)
40
+ .insert({ token, note })
41
+ .select()
42
+ .single();
43
+
44
+ if (error) {
45
+ return NextResponse.json({ error: error.message }, { status: 500 });
46
+ }
47
+
48
+ return NextResponse.json(data);
49
+ } catch (e) {
50
+ return NextResponse.json({ error: 'Invalid request body' }, { status: 400 });
51
+ }
52
+ }
53
+
54
+ export async function DELETE(request: NextRequest) {
55
+ if (!(await isUserAdmin())) {
56
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
57
+ }
58
+
59
+ const searchParams = request.nextUrl.searchParams;
60
+ const id = searchParams.get('id');
61
+
62
+ if (!id) {
63
+ return NextResponse.json({ error: 'ID is required' }, { status: 400 });
64
+ }
65
+
66
+ const supabase = $provideSupabase();
67
+ const table = await $getTableName('ApiTokens');
68
+
69
+ const { error } = await supabase.from(table).delete().eq('id', parseInt(id, 10));
70
+
71
+ if (error) {
72
+ return NextResponse.json({ error: error.message }, { status: 500 });
73
+ }
74
+
75
+ return NextResponse.json({ success: true });
76
+ }
@@ -1,7 +1,4 @@
1
- import { $getTableName } from '@/src/database/$getTableName';
2
- import { $provideSupabaseForServer } from '../../../../database/$provideSupabaseForServer';
3
- import { AgentsServerDatabase } from '../../../../database/schema';
4
- import { verifyPassword } from '../../../../utils/auth';
1
+ import { authenticateUser } from '../../../../utils/authenticateUser';
5
2
  import { setSession } from '../../../../utils/session';
6
3
  import { NextResponse } from 'next/server';
7
4
 
@@ -14,50 +11,15 @@ export async function POST(request: Request) {
14
11
  return NextResponse.json({ error: 'Username and password are required' }, { status: 400 });
15
12
  }
16
13
 
17
- // 1. Check if it's the environment admin
18
- if (process.env.ADMIN_PASSWORD && password === process.env.ADMIN_PASSWORD && username === 'admin') {
19
- // Or maybe allow any username if password matches admin password?
20
- // The task says "process.env.ADMIN_PASSWORD is like one of the admin users"
21
- // Assuming username 'admin' for environment password login.
22
- await setSession({ username: 'admin', isAdmin: true });
23
- return NextResponse.json({ success: true });
24
- }
25
-
26
- // 2. Check DB users
27
- const supabase = $provideSupabaseForServer();
28
- const { data: user, error } = await supabase
29
- .from(await $getTableName('User'))
30
- .select('*')
31
- .eq('username', username)
32
- .single();
33
-
34
- if (error || !user) {
35
- // Check if password matches ADMIN_PASSWORD even if user doesn't exist?
36
- // "The table User should work together with the process.env.ADMIN_PASSWORD"
37
- // If the user enters a password that matches process.env.ADMIN_PASSWORD, should they get admin access regardless of username?
38
- // "process.env.ADMIN_PASSWORD is like one of the admin users" implies it's a specific credential.
39
- // Let's stick to: if username is 'admin' and password is ADMIN_PASSWORD, it works.
40
- // Or if the password matches ADMIN_PASSWORD, maybe we grant admin access?
41
- // "Non-admin users can only log in... cannot see list of users"
42
-
43
- // Re-reading: "process.env.ADMIN_PASSWORD is like one of the admin users in the User table"
44
- // This suggests it's treated as a user.
45
-
46
- // If I login with a valid user from DB, I check password hash.
47
- // If I login with 'admin' and ADMIN_PASSWORD, I get admin.
14
+ const user = await authenticateUser(username, password);
48
15
 
16
+ if (user) {
17
+ await setSession(user);
18
+ return NextResponse.json({ success: true });
19
+ } else {
49
20
  return NextResponse.json({ error: 'Invalid credentials' }, { status: 401 });
50
21
  }
51
22
 
52
- const isValid = await verifyPassword(password, (user as AgentsServerDatabase['public']['Tables']['User']['Row']).passwordHash);
53
-
54
- if (!isValid) {
55
- return NextResponse.json({ error: 'Invalid credentials' }, { status: 401 });
56
- }
57
-
58
- await setSession({ username: (user as AgentsServerDatabase['public']['Tables']['User']['Row']).username, isAdmin: (user as AgentsServerDatabase['public']['Tables']['User']['Row']).isAdmin });
59
- return NextResponse.json({ success: true });
60
-
61
23
  } catch (error) {
62
24
  console.error('Login error:', error);
63
25
  return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
@@ -0,0 +1,38 @@
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
+ /**
7
+ * Delete a single chat feedback entry by ID.
8
+ */
9
+ export async function DELETE(request: NextRequest, context: { params: Promise<{ id: string }> }) {
10
+ if (!(await isUserAdmin())) {
11
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
12
+ }
13
+
14
+
15
+ const rawId = (await context.params).id;
16
+ const id = Number.parseInt(rawId, 10);
17
+
18
+ if (!Number.isFinite(id) || id <= 0) {
19
+ return NextResponse.json({ error: 'Invalid id' }, { status: 400 });
20
+ }
21
+
22
+ try {
23
+ const supabase = $provideSupabase();
24
+ const table = await $getTableName('ChatFeedback');
25
+
26
+ const { error } = await supabase.from(table).delete().eq('id', id);
27
+
28
+ if (error) {
29
+ console.error('Delete chat feedback row error:', error);
30
+ return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
31
+ }
32
+
33
+ return NextResponse.json({ success: true });
34
+ } catch (error) {
35
+ console.error('Delete chat feedback row error:', error);
36
+ return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
37
+ }
38
+ }
@@ -0,0 +1,157 @@
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
+ type SortField = 'createdAt' | 'agentName' | 'id';
10
+ type SortOrder = 'asc' | 'desc';
11
+
12
+ function parsePositiveInt(value: string | null, fallback: number): number {
13
+ if (!value) return fallback;
14
+ const parsed = parseInt(value, 10);
15
+ if (Number.isNaN(parsed) || parsed <= 0) return fallback;
16
+ return parsed;
17
+ }
18
+
19
+ function parseSortField(value: string | null): SortField {
20
+ if (value === 'agentName' || value === 'id') return value;
21
+ return 'createdAt';
22
+ }
23
+
24
+ function parseSortOrder(value: string | null): SortOrder {
25
+ return value === 'asc' ? 'asc' : 'desc';
26
+ }
27
+
28
+ /**
29
+ * List chat feedback with filters, search and pagination.
30
+ *
31
+ * Query params:
32
+ * - page: number (1-based)
33
+ * - pageSize: number (items per page)
34
+ * - agentName: filter by agent name
35
+ * - search: free-text search across agentName, url, ip, textRating, userNote and expectedAnswer
36
+ * - sortBy: createdAt | agentName | id (default: createdAt)
37
+ * - sortOrder: asc | desc (default: desc)
38
+ */
39
+ export async function GET(request: NextRequest) {
40
+ if (!(await isUserAdmin())) {
41
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
42
+ }
43
+
44
+ try {
45
+ const searchParams = request.nextUrl.searchParams;
46
+
47
+ const page = parsePositiveInt(searchParams.get('page'), 1);
48
+ const pageSize = Math.min(
49
+ MAX_PAGE_SIZE,
50
+ parsePositiveInt(searchParams.get('pageSize'), DEFAULT_PAGE_SIZE),
51
+ );
52
+ const agentName = searchParams.get('agentName');
53
+ const search = searchParams.get('search')?.trim() || '';
54
+ const sortBy = parseSortField(searchParams.get('sortBy'));
55
+ const sortOrder = parseSortOrder(searchParams.get('sortOrder'));
56
+
57
+ const supabase = $provideSupabase();
58
+ const table = await $getTableName('ChatFeedback');
59
+
60
+ let query = supabase
61
+ .from(table)
62
+ .select('*', { count: 'exact' });
63
+
64
+ if (agentName) {
65
+ query = query.eq('agentName', agentName);
66
+ }
67
+
68
+ if (search) {
69
+ // Note: We intentionally limit search to simple text columns
70
+ // to keep the query portable and efficient.
71
+ //
72
+ // This searches across:
73
+ // - agentName
74
+ // - url
75
+ // - ip
76
+ // - textRating
77
+ // - userNote
78
+ // - expectedAnswer
79
+ const escaped = search.replace(/%/g, '\\%').replace(/_/g, '\\_');
80
+ query = query.or(
81
+ [
82
+ `agentName.ilike.%${escaped}%`,
83
+ `url.ilike.%${escaped}%`,
84
+ `ip.ilike.%${escaped}%`,
85
+ `textRating.ilike.%${escaped}%`,
86
+ `userNote.ilike.%${escaped}%`,
87
+ `expectedAnswer.ilike.%${escaped}%`,
88
+ ].join(','),
89
+ );
90
+ }
91
+
92
+ query = query.order(sortBy, { ascending: sortOrder === 'asc' });
93
+
94
+ const from = (page - 1) * pageSize;
95
+ const to = from + pageSize - 1;
96
+
97
+ query = query.range(from, to);
98
+
99
+ const { data, error, count } = await query;
100
+
101
+ if (error) {
102
+ console.error('List chat feedback error:', error);
103
+ return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
104
+ }
105
+
106
+ return NextResponse.json({
107
+ items: data ?? [],
108
+ total: count ?? 0,
109
+ page,
110
+ pageSize,
111
+ sortBy,
112
+ sortOrder,
113
+ });
114
+ } catch (error) {
115
+ console.error('List chat feedback error:', error);
116
+ return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
117
+ }
118
+ }
119
+
120
+ /**
121
+ * Delete chat feedback for a specific agent.
122
+ *
123
+ * Query params:
124
+ * - agentName: name of the agent whose feedback should be removed
125
+ */
126
+ export async function DELETE(request: NextRequest) {
127
+ if (!(await isUserAdmin())) {
128
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
129
+ }
130
+
131
+ const searchParams = request.nextUrl.searchParams;
132
+ const agentName = searchParams.get('agentName');
133
+
134
+ if (!agentName) {
135
+ return NextResponse.json({ error: 'agentName is required' }, { status: 400 });
136
+ }
137
+
138
+ try {
139
+ const supabase = $provideSupabase();
140
+ const table = await $getTableName('ChatFeedback');
141
+
142
+ const { error } = await supabase
143
+ .from(table)
144
+ .delete()
145
+ .eq('agentName', agentName);
146
+
147
+ if (error) {
148
+ console.error('Clear chat feedback error:', error);
149
+ return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
150
+ }
151
+
152
+ return NextResponse.json({ success: true });
153
+ } catch (error) {
154
+ console.error('Clear chat feedback error:', error);
155
+ return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
156
+ }
157
+ }
@@ -0,0 +1,37 @@
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
+ /**
7
+ * Delete a single chat history entry by ID.
8
+ */
9
+ export async function DELETE(request: NextRequest, context: { params: Promise<{ id: string }> }) {
10
+ if (!(await isUserAdmin())) {
11
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
12
+ }
13
+
14
+ const rawId = (await context.params).id;
15
+ const id = Number.parseInt(rawId, 10);
16
+
17
+ if (!Number.isFinite(id) || id <= 0) {
18
+ return NextResponse.json({ error: 'Invalid id' }, { status: 400 });
19
+ }
20
+
21
+ try {
22
+ const supabase = $provideSupabase();
23
+ const table = await $getTableName('ChatHistory');
24
+
25
+ const { error } = await supabase.from(table).delete().eq('id', id);
26
+
27
+ if (error) {
28
+ console.error('Delete chat history row error:', error);
29
+ return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
30
+ }
31
+
32
+ return NextResponse.json({ success: true });
33
+ } catch (error) {
34
+ console.error('Delete chat history row error:', error);
35
+ return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
36
+ }
37
+ }