@promptbook/cli 0.104.0-1 → 0.104.0-2

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 (113) hide show
  1. package/apps/agents-server/next.config.ts +2 -2
  2. package/apps/agents-server/package.json +6 -1
  3. package/apps/agents-server/public/fonts/OpenMoji-color-cbdt.woff2 +0 -0
  4. package/apps/agents-server/scripts/generate-reserved-paths/generate-reserved-paths.ts +50 -0
  5. package/apps/agents-server/scripts/generate-reserved-paths/tsconfig.json +19 -0
  6. package/apps/agents-server/src/app/AddAgentButton.tsx +4 -3
  7. package/apps/agents-server/src/app/actions.ts +17 -5
  8. package/apps/agents-server/src/app/admin/metadata/MetadataClient.tsx +15 -11
  9. package/apps/agents-server/src/app/agents/[agentName]/AgentOptionsMenu.tsx +51 -7
  10. package/apps/agents-server/src/app/agents/[agentName]/AgentProfileChat.tsx +32 -2
  11. package/apps/agents-server/src/app/agents/[agentName]/AgentProfileWrapper.tsx +2 -0
  12. package/apps/agents-server/src/app/agents/[agentName]/_utils.ts +18 -0
  13. package/apps/agents-server/src/app/agents/[agentName]/agentLinks.tsx +8 -8
  14. package/apps/agents-server/src/app/agents/[agentName]/api/agents/route.ts +17 -26
  15. package/apps/agents-server/src/app/agents/[agentName]/api/chat/route.ts +17 -0
  16. package/apps/agents-server/src/app/agents/[agentName]/api/profile/route.ts +1 -1
  17. package/apps/agents-server/src/app/agents/[agentName]/api/voice/route.ts +1 -1
  18. package/apps/agents-server/src/app/agents/[agentName]/book/BookEditorWrapper.tsx +20 -16
  19. package/apps/agents-server/src/app/agents/[agentName]/book/page.tsx +15 -2
  20. package/apps/agents-server/src/app/agents/[agentName]/book+chat/page.tsx +15 -2
  21. package/apps/agents-server/src/app/agents/[agentName]/chat/page.tsx +12 -0
  22. package/apps/agents-server/src/app/agents/[agentName]/code/api/route.ts +66 -0
  23. package/apps/agents-server/src/app/agents/[agentName]/code/page.tsx +211 -0
  24. package/apps/agents-server/src/app/agents/[agentName]/generateAgentMetadata.ts +5 -0
  25. package/apps/agents-server/src/app/agents/[agentName]/history/actions.ts +2 -2
  26. package/apps/agents-server/src/app/agents/[agentName]/integration/page.tsx +1 -1
  27. package/apps/agents-server/src/app/agents/[agentName]/links/page.tsx +2 -2
  28. package/apps/agents-server/src/app/agents/[agentName]/page.tsx +12 -6
  29. package/apps/agents-server/src/app/agents/[agentName]/system-message/page.tsx +87 -0
  30. package/apps/agents-server/src/app/api/admin-email/route.ts +12 -0
  31. package/apps/agents-server/src/app/api/agents/[agentName]/restore/route.ts +19 -0
  32. package/apps/agents-server/src/app/api/agents/[agentName]/route.ts +42 -0
  33. package/apps/agents-server/src/app/api/agents/route.ts +29 -4
  34. package/apps/agents-server/src/app/api/docs/book.md/route.ts +58 -0
  35. package/apps/agents-server/src/app/api/federated-agents/route.ts +12 -0
  36. package/apps/agents-server/src/app/api/images/[filename]/route.ts +107 -0
  37. package/apps/agents-server/src/app/api/upload/route.ts +119 -45
  38. package/apps/agents-server/src/app/docs/[docId]/page.tsx +2 -3
  39. package/apps/agents-server/src/app/docs/page.tsx +12 -12
  40. package/apps/agents-server/src/app/globals.css +140 -33
  41. package/apps/agents-server/src/app/layout.tsx +27 -22
  42. package/apps/agents-server/src/app/page.tsx +50 -4
  43. package/apps/agents-server/src/app/recycle-bin/actions.ts +20 -14
  44. package/apps/agents-server/src/app/recycle-bin/page.tsx +25 -41
  45. package/apps/agents-server/src/app/sitemap.xml/route.ts +6 -3
  46. package/apps/agents-server/src/components/AgentProfile/AgentProfile.tsx +6 -97
  47. package/apps/agents-server/src/components/AgentProfile/useAgentBackground.ts +97 -0
  48. package/apps/agents-server/src/components/DeletedAgentBanner.tsx +26 -0
  49. package/apps/agents-server/src/components/DocsToolbar/DocsToolbar.tsx +38 -0
  50. package/apps/agents-server/src/components/DocumentationContent/DocumentationContent.tsx +11 -9
  51. package/apps/agents-server/src/components/Footer/Footer.tsx +5 -5
  52. package/apps/agents-server/src/components/ForgottenPasswordDialog/ForgottenPasswordDialog.tsx +61 -0
  53. package/apps/agents-server/src/components/Header/Header.tsx +79 -35
  54. package/apps/agents-server/src/components/Homepage/AgentCard.tsx +85 -20
  55. package/apps/agents-server/src/components/Homepage/AgentsList.tsx +72 -12
  56. package/apps/agents-server/src/components/Homepage/DeletedAgentsList.tsx +50 -0
  57. package/apps/agents-server/src/components/LayoutWrapper/LayoutWrapper.tsx +3 -2
  58. package/apps/agents-server/src/components/LoginForm/LoginForm.tsx +50 -1
  59. package/apps/agents-server/src/components/NotFoundPage/NotFoundPage.tsx +7 -2
  60. package/apps/agents-server/src/components/OpenMojiIcon/OpenMojiIcon.tsx +16 -7
  61. package/apps/agents-server/src/components/PrintHeader/PrintHeader.tsx +4 -4
  62. package/apps/agents-server/src/components/RegisterUserDialog/RegisterUserDialog.tsx +61 -0
  63. package/apps/agents-server/src/database/metadataDefaults.ts +19 -1
  64. package/apps/agents-server/src/database/migrations/2025-12-0240-agent-public-id.sql +3 -0
  65. package/apps/agents-server/src/database/migrations/2025-12-0360-agent-deleted-at.sql +1 -0
  66. package/apps/agents-server/src/database/migrations/2025-12-0370-image-table.sql +19 -0
  67. package/apps/agents-server/src/database/migrations/2025-12-0380-agent-visibility.sql +1 -0
  68. package/apps/agents-server/src/database/migrations/2025-12-0390-upload-tracking.sql +20 -0
  69. package/apps/agents-server/src/database/migrations/2025-12-0401-file-upload-status.sql +13 -0
  70. package/apps/agents-server/src/database/migrations/2025-12-0640-openai-assistant-cache.sql +12 -0
  71. package/apps/agents-server/src/database/schema.ts +109 -0
  72. package/apps/agents-server/src/generated/reservedPaths.ts +27 -0
  73. package/apps/agents-server/src/middleware.ts +7 -20
  74. package/apps/agents-server/src/tools/$provideCdnForServer.ts +6 -1
  75. package/apps/agents-server/src/utils/cdn/classes/TrackedFilesStorage.ts +57 -0
  76. package/apps/agents-server/src/utils/cdn/classes/VercelBlobStorage.ts +4 -0
  77. package/apps/agents-server/src/utils/cdn/interfaces/IFilesStorage.ts +18 -0
  78. package/apps/agents-server/src/utils/getUserIdFromRequest.ts +33 -0
  79. package/apps/agents-server/src/utils/handleChatCompletion.ts +60 -4
  80. package/apps/agents-server/src/utils/normalization/filenameToPrompt.ts +21 -0
  81. package/apps/agents-server/src/utils/validateApiKey.ts +2 -1
  82. package/esm/index.es.js +140 -27
  83. package/esm/index.es.js.map +1 -1
  84. package/esm/typings/src/_packages/types.index.d.ts +6 -2
  85. package/esm/typings/src/book-2.0/agent-source/AgentBasicInformation.d.ts +6 -1
  86. package/esm/typings/src/book-components/Chat/Chat/ChatMessageItem.d.ts +5 -1
  87. package/esm/typings/src/book-components/Chat/Chat/ChatProps.d.ts +5 -0
  88. package/esm/typings/src/book-components/Chat/CodeBlock/CodeBlock.d.ts +13 -0
  89. package/esm/typings/src/book-components/Chat/MarkdownContent/MarkdownContent.d.ts +1 -0
  90. package/esm/typings/src/book-components/_common/Dropdown/Dropdown.d.ts +2 -2
  91. package/esm/typings/src/book-components/_common/MenuHoisting/MenuHoistingContext.d.ts +56 -0
  92. package/esm/typings/src/collection/agent-collection/constructors/agent-collection-in-supabase/AgentCollectionInSupabase.d.ts +13 -7
  93. package/esm/typings/src/collection/agent-collection/constructors/agent-collection-in-supabase/AgentsDatabaseSchema.d.ts +6 -0
  94. package/esm/typings/src/commitments/DICTIONARY/DICTIONARY.d.ts +46 -0
  95. package/esm/typings/src/commitments/index.d.ts +2 -1
  96. package/esm/typings/src/llm-providers/ollama/OllamaExecutionTools.d.ts +1 -1
  97. package/esm/typings/src/llm-providers/openai/createOpenAiCompatibleExecutionTools.d.ts +1 -1
  98. package/esm/typings/src/types/typeAliases.d.ts +12 -0
  99. package/esm/typings/src/utils/environment/$detectRuntimeEnvironment.d.ts +4 -4
  100. package/esm/typings/src/utils/environment/$isRunningInBrowser.d.ts +1 -1
  101. package/esm/typings/src/utils/environment/$isRunningInJest.d.ts +1 -1
  102. package/esm/typings/src/utils/environment/$isRunningInNode.d.ts +1 -1
  103. package/esm/typings/src/utils/environment/$isRunningInWebWorker.d.ts +1 -1
  104. package/esm/typings/src/utils/markdown/extractAllBlocksFromMarkdown.d.ts +2 -2
  105. package/esm/typings/src/utils/markdown/extractOneBlockFromMarkdown.d.ts +2 -2
  106. package/esm/typings/src/utils/random/$randomBase58.d.ts +12 -0
  107. package/esm/typings/src/version.d.ts +1 -1
  108. package/package.json +1 -1
  109. package/umd/index.umd.js +146 -33
  110. package/umd/index.umd.js.map +1 -1
  111. package/apps/agents-server/package-lock.json +0 -27
  112. package/apps/agents-server/public/fonts/download-font.js +0 -22
  113. package/apps/agents-server/src/components/PrintButton/PrintButton.tsx +0 -18
@@ -2,8 +2,8 @@ import type { NextConfig } from 'next';
2
2
  import path from 'path';
3
3
 
4
4
  const nextConfig: NextConfig = {
5
- output: 'standalone',
6
- // <- TODO: [🐱‍🚀][🧠] How to propperly build Next.js app
5
+ // output: 'standalone',
6
+ // <- TODO: [🐱‍🚀][🧠] How to propperly build Next.js app, for both Vercel and Doceker?
7
7
 
8
8
  experimental: {
9
9
  externalDir: true,
@@ -1,7 +1,12 @@
1
1
  {
2
2
  "name": "promptbook-agents-server",
3
3
  "scripts": {
4
- "migrate-database": "npx tsx src/database/migrate.ts",
4
+ "migrate-database": "npx tsx ./src/database/migrate.ts",
5
+ "generate-reserved-paths": "ts-node ./scripts/generate-reserved-paths/generate-reserved-paths.ts",
6
+ "prebuild": "npm run generate-reserved-paths",
7
+ "--postbuild": "echo {} > .next/export-detail.json && mkdir -p .next/export && echo \"Not found !!!!\" > .next/export/404.html && echo \"Server error !!!!\" > .next/export/500.html",
8
+ "--0": " <- !!!!",
9
+ "predev": "npm run generate-reserved-paths",
5
10
  "dev": "(npx kill-port 4440 || true) && next dev -p 4440",
6
11
  "test-build": "npm run build",
7
12
  "build": "(npx kill-port 4440 || true) && next build",
@@ -0,0 +1,50 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { GENERATOR_WARNING } from '../../../../src/config';
4
+
5
+ const appDir = path.join(__dirname, '..', '..', 'src', 'app');
6
+ const outputFile = path.join(__dirname, '..', '..', 'src', 'generated', 'reservedPaths.ts');
7
+
8
+ // Read the app directory
9
+ const entries = fs.readdirSync(appDir, { withFileTypes: true });
10
+
11
+ // Filter to get only directories (excluding dynamic routes like [agentName])
12
+ const reservedPaths = entries
13
+ .filter((entry) => entry.isDirectory())
14
+ .map((entry) => entry.name)
15
+ .filter((name) => !name.startsWith('[') && !name.endsWith(']'));
16
+
17
+ // Add additional paths that are not folders but should be reserved
18
+ const additionalPaths = [
19
+ '_next', // Next.js internal paths
20
+ 'manifest.webmanifest', // PWA manifest
21
+ 'sw.js', // Service Worker
22
+ 'favicon.ico', // Favicon
23
+ ];
24
+
25
+ const allReservedPaths = [...new Set([...reservedPaths, ...additionalPaths])].sort();
26
+
27
+ // Generate the TypeScript file
28
+ const content = `/**
29
+ * Reserved paths that should not be treated as agent names.
30
+ * This file is auto-generated by scripts/generate-reserved-paths.js
31
+ *
32
+ * ${GENERATOR_WARNING}
33
+ *
34
+ * @see /apps/agents-server/src/app - source directory
35
+ * @see /apps/agents-server/src/middleware.ts - where this is used
36
+ */
37
+ export const RESERVED_PATHS: readonly string[] = ${JSON.stringify(allReservedPaths, null, 4)} as const;
38
+ `;
39
+
40
+ // Ensure the generated directory exists
41
+ const generatedDir = path.dirname(outputFile);
42
+ if (!fs.existsSync(generatedDir)) {
43
+ fs.mkdirSync(generatedDir, { recursive: true });
44
+ }
45
+
46
+ // Write the file
47
+ fs.writeFileSync(outputFile, content, 'utf-8');
48
+
49
+ console.log(`Generated ${outputFile} with ${allReservedPaths.length} reserved paths:`);
50
+ console.log(allReservedPaths.join(', '));
@@ -0,0 +1,19 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "commonjs",
5
+ "downlevelIteration": true,
6
+ "allowJs": true,
7
+ "moduleResolution": "node",
8
+ "forceConsistentCasingInFileNames": true,
9
+ "noImplicitReturns": true,
10
+ "noImplicitThis": true,
11
+ "noImplicitAny": true,
12
+ "strictNullChecks": true,
13
+ "experimentalDecorators": true,
14
+ "noUnusedLocals": false,
15
+ "resolveJsonModule": true,
16
+ "esModuleInterop": true
17
+ },
18
+ "exclude": ["node_modules"]
19
+ }
@@ -1,3 +1,4 @@
1
+
1
2
  'use client';
2
3
 
3
4
  import { useRouter } from 'next/navigation';
@@ -11,10 +12,10 @@ export function AddAgentButton() {
11
12
 
12
13
  const handleAddAgent = async () => {
13
14
  setIsLoading(true);
14
- const agentName = await $createAgentAction();
15
+ const { agentName, permanentId } = await $createAgentAction();
15
16
  // TODO: Add proper error handling and UI feedback
16
- if (agentName) {
17
- router.push(`/agents/${agentName}`);
17
+ if (permanentId) {
18
+ router.push(`/agents/${permanentId}`);
18
19
  } else {
19
20
  router.refresh();
20
21
  setIsLoading(false);
@@ -1,16 +1,16 @@
1
1
  'use server';
2
2
 
3
3
  import { $generateBookBoilerplate } from '@promptbook-local/core';
4
- import { string_agent_name } from '@promptbook-local/types';
4
+ import { string_agent_name, string_book } from '@promptbook-local/types';
5
5
  import { revalidatePath } from 'next/cache';
6
- import { cookies } from 'next/headers';
6
+ import { string_agent_permanent_id } from '../../../../src/types/typeAliases';
7
7
  import { getMetadata } from '../database/getMetadata';
8
8
  import { $provideAgentCollectionForServer } from '../tools/$provideAgentCollectionForServer';
9
9
  import { authenticateUser } from '../utils/authenticateUser';
10
10
  import { isUserAdmin } from '../utils/isUserAdmin';
11
11
  import { clearSession, setSession } from '../utils/session';
12
12
 
13
- export async function $createAgentAction(): Promise<string_agent_name> {
13
+ export async function $createAgentAction(): Promise<{ agentName: string_agent_name; permanentId: string_agent_permanent_id }> {
14
14
  // TODO: [👹] Check permissions here
15
15
  if (!(await isUserAdmin())) {
16
16
  throw new Error('You are not authorized to create agents');
@@ -20,9 +20,21 @@ export async function $createAgentAction(): Promise<string_agent_name> {
20
20
  const namePool = (await getMetadata('NAME_POOL')) || 'ENGLISH';
21
21
  const agentSource = $generateBookBoilerplate({ namePool });
22
22
 
23
- const { agentName } = await collection.createAgent(agentSource);
23
+ const { agentName, permanentId } = await collection.createAgent(agentSource);
24
24
 
25
- return agentName;
25
+ return { agentName, permanentId };
26
+ }
27
+
28
+ export async function $createAgentFromBookAction(bookContent: string_book): Promise<{ agentName: string_agent_name; permanentId: string_agent_permanent_id }> {
29
+ // TODO: [👹] Check permissions here
30
+ if (!(await isUserAdmin())) {
31
+ throw new Error('You are not authorized to create agents');
32
+ }
33
+
34
+ const collection = await $provideAgentCollectionForServer();
35
+ const { agentName, permanentId } = await collection.createAgent(bookContent);
36
+
37
+ return { agentName, permanentId };
26
38
  }
27
39
 
28
40
  export async function loginAction(formData: FormData) {
@@ -1,5 +1,6 @@
1
1
  'use client';
2
2
 
3
+ import { upload } from '@vercel/blob/client';
3
4
  import { FileText, Hash, Image, Shield, ToggleLeft, Type, Upload } from 'lucide-react';
4
5
  import { useEffect, useRef, useState } from 'react';
5
6
  import { metadataDefaults, MetadataType } from '../../../database/metadataDefaults';
@@ -185,26 +186,29 @@ export function MetadataClient() {
185
186
 
186
187
  try {
187
188
  setIsUploading(true);
188
- const formData = new FormData();
189
- formData.append('file', file);
190
189
 
191
- const response = await fetch('/api/upload', {
192
- method: 'POST',
193
- body: formData,
190
+ // Build the full path including prefix and user/files directory
191
+ const pathPrefix = process.env.NEXT_PUBLIC_CDN_PATH_PREFIX || '';
192
+ const uploadPath = pathPrefix ? `${pathPrefix}/user/files/${file.name}` : `user/files/${file.name}`;
193
+
194
+ // Upload directly to Vercel Blob using client upload
195
+ const blob = await upload(uploadPath, file, {
196
+ access: 'public',
197
+ handleUploadUrl: '/api/upload',
198
+ clientPayload: JSON.stringify({
199
+ purpose: formState.key || 'METADATA_IMAGE',
200
+ contentType: file.type,
201
+ }),
194
202
  });
195
203
 
196
- if (!response.ok) {
197
- throw new Error(`Failed to upload file: ${response.statusText}`);
198
- }
199
-
200
- const { fileUrl: longFileUrl } = await response.json();
204
+ const fileUrl = blob.url;
201
205
 
202
206
  const LONG_URL = `${process.env.NEXT_PUBLIC_CDN_PUBLIC_URL!}/${process.env
203
207
  .NEXT_PUBLIC_CDN_PATH_PREFIX!}/user/files/`;
204
208
  const SHORT_URL = `https://ptbk.io/k/`;
205
209
  // <- TODO: [🌍] Unite this logic in one place
206
210
 
207
- const shortFileUrl = longFileUrl.split(LONG_URL).join(SHORT_URL);
211
+ const shortFileUrl = fileUrl.split(LONG_URL).join(SHORT_URL);
208
212
  setFormState((prev) => ({ ...prev, value: shortFileUrl }));
209
213
  } catch (err) {
210
214
  setError(err instanceof Error ? err.message : 'Failed to upload image');
@@ -2,9 +2,11 @@
2
2
 
3
3
  import { TODO_any } from '@promptbook-local/types';
4
4
  import {
5
+ CodeIcon,
5
6
  CopyIcon,
6
7
  CopyPlusIcon,
7
8
  DownloadIcon,
9
+ FileTextIcon,
8
10
  MailIcon,
9
11
  MessageCircleQuestionIcon,
10
12
  MessageSquareIcon,
@@ -13,11 +15,13 @@ import {
13
15
  QrCodeIcon,
14
16
  SmartphoneIcon,
15
17
  SquareSplitHorizontalIcon,
18
+ TrashIcon,
16
19
  } from 'lucide-react';
17
20
  import { Barlow_Condensed } from 'next/font/google';
18
21
  import { useCallback, useEffect, useRef, useState } from 'react';
19
- import { string_data_url, string_url_image } from '../../../../../../src/types/typeAliases';
22
+ import { string_agent_permanent_id, string_data_url, string_url_image } from '../../../../../../src/types/typeAliases';
20
23
  import { getAgentLinks } from './agentLinks';
24
+ import { deleteAgent } from '../../recycle-bin/actions';
21
25
 
22
26
  type BeforeInstallPromptEvent = Event & {
23
27
  prompt: () => Promise<void>;
@@ -33,6 +37,7 @@ const barlowCondensed = Barlow_Condensed({
33
37
  type AgentOptionsMenuProps = {
34
38
  agentName: string;
35
39
  derivedAgentName: string;
40
+ permanentId?: string_agent_permanent_id;
36
41
  agentUrl: string;
37
42
  agentEmail: string;
38
43
  brandColorHex: string;
@@ -44,6 +49,7 @@ type AgentOptionsMenuProps = {
44
49
  export function AgentOptionsMenu({
45
50
  agentName,
46
51
  derivedAgentName,
52
+ permanentId,
47
53
  agentUrl,
48
54
  agentEmail,
49
55
  brandColorHex,
@@ -115,7 +121,7 @@ export function AgentOptionsMenu({
115
121
  }
116
122
  };
117
123
 
118
- const links = getAgentLinks(agentName);
124
+ const links = getAgentLinks(permanentId || agentName);
119
125
  const editBookLink = links.find((l) => l.title === 'Edit Book')!;
120
126
  const integrationLink = links.find((l) => l.title === 'Integration')!;
121
127
  const historyLink = links.find((l) => l.title === 'History & Feedback')!;
@@ -128,13 +134,29 @@ export function AgentOptionsMenu({
128
134
  const handleUpdateUrl = () => {
129
135
  if (
130
136
  window.confirm(
131
- `Are you sure you want to change the agent URL from "/agents/${agentName}" to "/agents/${derivedAgentName}"?`
137
+ `Are you sure you want to change the agent URL from "/agents/${agentName}" to "/agents/${derivedAgentName}"?`,
132
138
  )
133
139
  ) {
134
140
  window.location.href = updateUrlHref;
135
141
  }
136
142
  };
137
143
 
144
+ const handleDeleteAgent = async () => {
145
+ if (
146
+ window.confirm(
147
+ `Are you sure you want to delete the agent "${agentName}"? This action can be undone by restoring it from the recycle bin.`,
148
+ )
149
+ ) {
150
+ try {
151
+ await deleteAgent(agentName);
152
+ window.location.href = '/';
153
+ } catch (error) {
154
+ console.error('Failed to delete agent:', error);
155
+ alert('Failed to delete agent. Please try again.');
156
+ }
157
+ }
158
+ };
159
+
138
160
  const menuItems = [
139
161
  ...(showUpdateUrl
140
162
  ? [
@@ -166,6 +188,18 @@ export function AgentOptionsMenu({
166
188
  icon: editBookLink.icon,
167
189
  label: editBookLink.title,
168
190
  },
191
+ {
192
+ type: 'link' as const,
193
+ href: `/agents/${encodeURIComponent(agentName)}/system-message`,
194
+ icon: FileTextIcon,
195
+ label: 'System Message',
196
+ },
197
+ {
198
+ type: 'link' as const,
199
+ href: `/agents/${encodeURIComponent(agentName)}/code`,
200
+ icon: CodeIcon,
201
+ label: 'View Code',
202
+ },
169
203
  { type: 'divider' as const },
170
204
  {
171
205
  type: 'link' as const,
@@ -243,6 +277,12 @@ export function AgentOptionsMenu({
243
277
  icon: DownloadIcon,
244
278
  label: 'Export Agent',
245
279
  },
280
+ {
281
+ type: 'action' as const,
282
+ icon: TrashIcon,
283
+ label: 'Delete Agent',
284
+ onClick: handleDeleteAgent,
285
+ },
246
286
  // {
247
287
  // type: 'link' as const,
248
288
  // href: backgroundImage,
@@ -297,12 +337,16 @@ export function AgentOptionsMenu({
297
337
  }
298
338
  }}
299
339
  className={`flex items-center gap-3 px-4 py-2.5 w-full text-left transition-colors
300
- ${item.highlight
301
- ? 'bg-yellow-100 text-yellow-900 font-bold hover:bg-yellow-200'
302
- : 'text-gray-700 hover:bg-gray-50'}
340
+ ${
341
+ item.highlight
342
+ ? 'bg-yellow-100 text-yellow-900 font-bold hover:bg-yellow-200'
343
+ : 'text-gray-700 hover:bg-gray-50'
344
+ }
303
345
  `}
304
346
  >
305
- <item.icon className={`w-4 h-4 ${item.highlight ? 'text-yellow-700' : 'text-gray-500'}`} />
347
+ <item.icon
348
+ className={`w-4 h-4 ${item.highlight ? 'text-yellow-700' : 'text-gray-500'}`}
349
+ />
306
350
  <span className="text-sm font-medium">{item.label}</span>
307
351
  </button>
308
352
  );
@@ -3,10 +3,13 @@
3
3
  import { usePromise } from '@common/hooks/usePromise';
4
4
  import { Chat } from '@promptbook-local/components';
5
5
  import { RemoteAgent } from '@promptbook-local/core';
6
+ import { string_book } from '@promptbook-local/types';
6
7
  import { useRouter } from 'next/navigation';
7
- import { useCallback, useMemo } from 'react';
8
+ import { useCallback, useMemo, useState } from 'react';
8
9
  import spaceTrim from 'spacetrim';
9
10
  import { string_agent_url, string_color } from '../../../../../../src/types/typeAliases';
11
+ import { $createAgentFromBookAction } from '../../../app/actions';
12
+ import { DeletedAgentBanner } from '../../../components/DeletedAgentBanner';
10
13
 
11
14
  type AgentProfileChatProps = {
12
15
  agentUrl: string_agent_url;
@@ -14,10 +17,12 @@ type AgentProfileChatProps = {
14
17
  fullname: string;
15
18
  brandColorHex: string_color;
16
19
  avatarSrc: string;
20
+ isDeleted?: boolean;
17
21
  };
18
22
 
19
- export function AgentProfileChat({ agentUrl, agentName, fullname, brandColorHex, avatarSrc }: AgentProfileChatProps) {
23
+ export function AgentProfileChat({ agentUrl, agentName, fullname, brandColorHex, avatarSrc, isDeleted = false }: AgentProfileChatProps) {
20
24
  const router = useRouter();
25
+ const [isCreatingAgent, setIsCreatingAgent] = useState(false);
21
26
 
22
27
  const agentPromise = useMemo(
23
28
  () =>
@@ -38,6 +43,21 @@ export function AgentProfileChat({ agentUrl, agentName, fullname, brandColorHex,
38
43
  [agentName, router],
39
44
  );
40
45
 
46
+ const handleCreateAgent = useCallback(async (bookContent: string) => {
47
+ setIsCreatingAgent(true);
48
+ try {
49
+ const { permanentId } = await $createAgentFromBookAction(bookContent as string_book);
50
+ if (permanentId) {
51
+ router.push(`/agents/${permanentId}`);
52
+ }
53
+ } catch (error) {
54
+ console.error('Failed to create agent:', error);
55
+ alert('Failed to create agent. Please try again.');
56
+ } finally {
57
+ setIsCreatingAgent(false);
58
+ }
59
+ }, [router]);
60
+
41
61
  const initialMessage = useMemo(() => {
42
62
  if (!agent) {
43
63
  return 'Loading...';
@@ -52,6 +72,15 @@ export function AgentProfileChat({ agentUrl, agentName, fullname, brandColorHex,
52
72
  );
53
73
  }, [agent, fullname, agentName]);
54
74
 
75
+ // If agent is deleted, show banner instead of chat
76
+ if (isDeleted) {
77
+ return (
78
+ <div className="w-full min-h-[350px] md:min-h-[500px] flex items-center justify-center">
79
+ <DeletedAgentBanner message="This agent has been deleted. You can restore it from the Recycle Bin." />
80
+ </div>
81
+ );
82
+ }
83
+
55
84
  // If agent is not loaded yet, we can show a skeleton or just the default Chat structure
56
85
  // But to match "same initial message", we need the agent loaded or at least the default fallback.
57
86
  // The fallback above matches AgentChat.tsx default.
@@ -80,6 +109,7 @@ export function AgentProfileChat({ agentUrl, agentName, fullname, brandColorHex,
80
109
  },
81
110
  ]}
82
111
  onMessage={handleMessage}
112
+ onCreateAgent={handleCreateAgent}
83
113
  isSaveButtonEnabled={false}
84
114
  isCopyButtonEnabled={false}
85
115
  className="bg-transparent"
@@ -21,6 +21,7 @@ export function AgentProfileWrapper(props: AgentProfileWrapperProps) {
21
21
 
22
22
  // Derived agentName from agent data
23
23
  const derivedAgentName = agent.agentName;
24
+ const permanentId = agent.permanentId;
24
25
 
25
26
  return (
26
27
  <AgentProfile
@@ -32,6 +33,7 @@ export function AgentProfileWrapper(props: AgentProfileWrapperProps) {
32
33
  <AgentOptionsMenu
33
34
  agentName={agentName}
34
35
  derivedAgentName={derivedAgentName}
36
+ permanentId={permanentId}
35
37
  agentUrl={agentUrl}
36
38
  agentEmail={agentEmail}
37
39
  brandColorHex={brandColorHex}
@@ -1,5 +1,7 @@
1
+ import { $getTableName } from '@/src/database/$getTableName';
1
2
  import { $provideAgentCollectionForServer } from '@/src/tools/$provideAgentCollectionForServer';
2
3
  import { parseAgentSource } from '@promptbook-local/core';
4
+ import { $provideSupabaseForServer } from '../../../database/$provideSupabaseForServer';
3
5
 
4
6
  export const AGENT_ACTIONS = ['Emails', 'Web chat', 'Read documents', 'Browser', 'WhatsApp', '<Coding/>'];
5
7
 
@@ -14,6 +16,22 @@ export async function getAgentProfile(agentName: string) {
14
16
  return parseAgentSource(agentSource);
15
17
  }
16
18
 
19
+ export async function isAgentDeleted(agentName: string): Promise<boolean> {
20
+ const supabase = $provideSupabaseForServer();
21
+
22
+ const result = await supabase
23
+ .from(await $getTableName(`Agent`))
24
+ .select('deletedAt')
25
+ .eq('agentName', agentName)
26
+ .single();
27
+
28
+ if (result.error || !result.data) {
29
+ return false; // If agent doesn't exist or error, consider not deleted
30
+ }
31
+
32
+ return result.data.deletedAt !== null;
33
+ }
34
+
17
35
  /**
18
36
  * TODO: Split to multiple files
19
37
  */
@@ -1,3 +1,4 @@
1
+ import { string_agent_name, string_agent_permanent_id } from '@promptbook-local/types';
1
2
  import {
2
3
  BookOpenIcon,
3
4
  CodeIcon,
@@ -18,42 +19,41 @@ type AgentLink = {
18
19
  rel?: string;
19
20
  };
20
21
 
21
- export const getAgentLinks = (agentName: string): AgentLink[] => {
22
- const encodedName = encodeURIComponent(agentName);
22
+ export const getAgentLinks = (permanentId: string_agent_permanent_id | string_agent_name): AgentLink[] => {
23
23
  return [
24
24
  {
25
25
  title: 'Chat with Agent',
26
- href: `/agents/${encodedName}`,
26
+ href: `/agents/${permanentId}`,
27
27
  icon: MessageSquareIcon,
28
28
  description: 'Direct interface to converse with the agent.',
29
29
  },
30
30
  {
31
31
  title: 'Edit Book',
32
- href: `/agents/${encodedName}/book`,
32
+ href: `/agents/${permanentId}/book`,
33
33
  icon: NotebookPenIcon,
34
34
  description: "Edit the agent's knowledge book.",
35
35
  },
36
36
  {
37
37
  title: 'Integration',
38
- href: `/agents/${encodedName}/integration`,
38
+ href: `/agents/${permanentId}/integration`,
39
39
  icon: CodeIcon,
40
40
  description: 'Learn how to integrate this agent into your applications.',
41
41
  },
42
42
  {
43
43
  title: 'History & Feedback',
44
- href: `/agents/${encodedName}/history`,
44
+ href: `/agents/${permanentId}/history`,
45
45
  icon: HistoryIcon,
46
46
  description: 'View past conversations and provide feedback.',
47
47
  },
48
48
  {
49
49
  title: 'All Links',
50
- href: `/agents/${encodedName}/links`,
50
+ href: `/agents/${permanentId}/links`,
51
51
  icon: LinkIcon,
52
52
  description: 'Signpost & Links',
53
53
  },
54
54
  {
55
55
  title: 'Website Integration',
56
- href: `/agents/${encodedName}/website-integration`,
56
+ href: `/agents/${permanentId}/website-integration`,
57
57
  icon: GlobeIcon,
58
58
  description: 'Embed the agent chat widget directly into your React application.',
59
59
  },
@@ -2,15 +2,12 @@ import { NextRequest, NextResponse } from 'next/server';
2
2
 
3
3
  export const dynamic = 'force-dynamic';
4
4
 
5
- export async function GET(
6
- request: NextRequest,
7
- { params }: { params: Promise<{ agentName: string }> }
8
- ) {
5
+ export async function GET(request: NextRequest, { params }: { params: Promise<{ agentName: string }> }) {
9
6
  try {
10
7
  const { agentName } = await params;
11
8
  // agentName is likely the federated server URL (e.g., "https://s6.ptbk.io")
12
9
  // It comes decoded from the URL params if it was encoded in the request path
13
-
10
+
14
11
  let serverUrl = agentName;
15
12
 
16
13
  // If the serverUrl doesn't look like a URL, it might be just a hostname or something else
@@ -18,20 +15,17 @@ export async function GET(
18
15
  // The client will likely pass the full URL or hostname.
19
16
  // We'll assume if it doesn't start with http, we might need to prepend it, or it's invalid.
20
17
  // However, the current federated servers list contains full URLs.
21
-
18
+
22
19
  // If it was somehow double encoded or something, we might need to handle it, but standard Next.js behavior is single decode.
23
-
20
+
24
21
  if (!serverUrl.startsWith('http')) {
25
- // Maybe it is just a hostname?
26
- // Let's try to assume https if missing
27
- if (serverUrl.includes('.')) {
28
- serverUrl = `https://${serverUrl}`;
29
- } else {
30
- return NextResponse.json(
31
- { error: 'Invalid federated server URL' },
32
- { status: 400 }
33
- );
34
- }
22
+ // Maybe it is just a hostname?
23
+ // Let's try to assume https if missing
24
+ if (serverUrl.includes('.')) {
25
+ serverUrl = `https://${serverUrl}`;
26
+ } else {
27
+ return NextResponse.json({ error: 'Invalid federated server URL' }, { status: 400 });
28
+ }
35
29
  }
36
30
 
37
31
  // Normalize URL (remove trailing slash)
@@ -48,20 +42,17 @@ export async function GET(
48
42
 
49
43
  if (!response.ok) {
50
44
  console.warn(`Proxy failed to fetch agents from ${serverUrl}: ${response.status} ${response.statusText}`);
51
- return NextResponse.json(
52
- { error: `Failed to fetch from ${serverUrl}` },
53
- { status: response.status }
54
- );
45
+ return NextResponse.json({ error: `Failed to fetch from ${serverUrl}` }, { status: response.status });
55
46
  }
56
47
 
57
48
  const data = await response.json();
58
49
  return NextResponse.json(data);
59
-
60
50
  } catch (error) {
61
51
  console.error('Proxy error fetching federated agents:', error);
62
- return NextResponse.json(
63
- { error: 'Internal Server Error' },
64
- { status: 500 }
65
- );
52
+ return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 });
66
53
  }
67
54
  }
55
+
56
+ /**
57
+ * TODO: !!!! Probbably remove this route
58
+ */
@@ -5,6 +5,7 @@ import { $provideOpenAiAssistantExecutionToolsForServer } from '@/src/tools/$pro
5
5
  import { Agent, computeAgentHash, PROMPTBOOK_ENGINE_VERSION } from '@promptbook-local/core';
6
6
  import { computeHash, serializeError } from '@promptbook-local/utils';
7
7
  import { assertsError } from '../../../../../../../../src/errors/assertsError';
8
+ import { isAgentDeleted } from '../../_utils';
8
9
 
9
10
  /**
10
11
  * Allow long-running streams: set to platform maximum (seconds)
@@ -26,6 +27,22 @@ export async function POST(request: Request, { params }: { params: Promise<{ age
26
27
  let { agentName } = await params;
27
28
  agentName = decodeURIComponent(agentName);
28
29
 
30
+ // Check if agent is deleted
31
+ if (await isAgentDeleted(agentName)) {
32
+ return new Response(
33
+ JSON.stringify({
34
+ error: {
35
+ message: 'This agent has been deleted. You can restore it from the Recycle Bin.',
36
+ type: 'agent_deleted',
37
+ },
38
+ }),
39
+ {
40
+ status: 410, // Gone - indicates the resource is no longer available
41
+ headers: { 'Content-Type': 'application/json' },
42
+ },
43
+ );
44
+ }
45
+
29
46
  const body = await request.json();
30
47
  const { message = 'Tell me more about yourself.', thread } = body;
31
48
  // <- TODO: [🐱‍🚀] To configuration DEFAULT_INITIAL_HIDDEN_MESSAGE
@@ -27,7 +27,7 @@ export async function GET(request: Request, { params }: { params: Promise<{ agen
27
27
  const agentSource = await collection.getAgentSource(agentName);
28
28
  const agentProfile = parseAgentSource(agentSource);
29
29
  const agentHash = computeAgentHash(agentSource);
30
- const isVoiceCallingEnabled = (await getMetadata('IS_VOICE_CALLING_ENABLED')) === 'true';
30
+ const isVoiceCallingEnabled = (await getMetadata('IS_EXPERIMENTAL_VOICE_CALLING_ENABLED')) === 'true';
31
31
 
32
32
  return new Response(
33
33
  JSON.stringify(
@@ -22,7 +22,7 @@ export async function OPTIONS(request: Request) {
22
22
 
23
23
  export async function POST(request: Request, { params }: { params: Promise<{ agentName: string }> }) {
24
24
  // Check if voice calling is enabled
25
- const isVoiceCallingEnabled = (await getMetadata('IS_VOICE_CALLING_ENABLED')) === 'true';
25
+ const isVoiceCallingEnabled = (await getMetadata('IS_EXPERIMENTAL_VOICE_CALLING_ENABLED')) === 'true';
26
26
  if (!isVoiceCallingEnabled) {
27
27
  return new Response(JSON.stringify({ error: 'Voice calling is disabled on this server' }), {
28
28
  status: 403,