@promptbook/cli 0.104.0-0 → 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 (118) 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/WebsiteIntegrationTabs.tsx +26 -0
  27. package/apps/agents-server/src/app/agents/[agentName]/integration/page.tsx +23 -6
  28. package/apps/agents-server/src/app/agents/[agentName]/links/page.tsx +2 -2
  29. package/apps/agents-server/src/app/agents/[agentName]/page.tsx +12 -6
  30. package/apps/agents-server/src/app/agents/[agentName]/system-message/page.tsx +87 -0
  31. package/apps/agents-server/src/app/agents/[agentName]/website-integration/page.tsx +35 -18
  32. package/apps/agents-server/src/app/api/admin-email/route.ts +12 -0
  33. package/apps/agents-server/src/app/api/agents/[agentName]/restore/route.ts +19 -0
  34. package/apps/agents-server/src/app/api/agents/[agentName]/route.ts +42 -0
  35. package/apps/agents-server/src/app/api/agents/route.ts +29 -4
  36. package/apps/agents-server/src/app/api/docs/book.md/route.ts +58 -0
  37. package/apps/agents-server/src/app/api/embed.js/route.ts +87 -67
  38. package/apps/agents-server/src/app/api/federated-agents/route.ts +12 -0
  39. package/apps/agents-server/src/app/api/images/[filename]/route.ts +107 -0
  40. package/apps/agents-server/src/app/api/upload/route.ts +119 -45
  41. package/apps/agents-server/src/app/docs/[docId]/page.tsx +2 -3
  42. package/apps/agents-server/src/app/docs/page.tsx +12 -12
  43. package/apps/agents-server/src/app/embed/layout.tsx +31 -0
  44. package/apps/agents-server/src/app/embed/page.tsx +22 -9
  45. package/apps/agents-server/src/app/globals.css +140 -33
  46. package/apps/agents-server/src/app/layout.tsx +27 -22
  47. package/apps/agents-server/src/app/page.tsx +50 -4
  48. package/apps/agents-server/src/app/recycle-bin/actions.ts +20 -14
  49. package/apps/agents-server/src/app/recycle-bin/page.tsx +25 -41
  50. package/apps/agents-server/src/app/sitemap.xml/route.ts +6 -3
  51. package/apps/agents-server/src/components/AgentProfile/AgentProfile.tsx +6 -97
  52. package/apps/agents-server/src/components/AgentProfile/useAgentBackground.ts +97 -0
  53. package/apps/agents-server/src/components/DeletedAgentBanner.tsx +26 -0
  54. package/apps/agents-server/src/components/DocsToolbar/DocsToolbar.tsx +38 -0
  55. package/apps/agents-server/src/components/DocumentationContent/DocumentationContent.tsx +11 -9
  56. package/apps/agents-server/src/components/Footer/Footer.tsx +5 -5
  57. package/apps/agents-server/src/components/ForgottenPasswordDialog/ForgottenPasswordDialog.tsx +61 -0
  58. package/apps/agents-server/src/components/Header/Header.tsx +79 -35
  59. package/apps/agents-server/src/components/Homepage/AgentCard.tsx +85 -20
  60. package/apps/agents-server/src/components/Homepage/AgentsList.tsx +72 -12
  61. package/apps/agents-server/src/components/Homepage/DeletedAgentsList.tsx +50 -0
  62. package/apps/agents-server/src/components/LayoutWrapper/LayoutWrapper.tsx +3 -2
  63. package/apps/agents-server/src/components/LoginForm/LoginForm.tsx +50 -1
  64. package/apps/agents-server/src/components/NotFoundPage/NotFoundPage.tsx +7 -2
  65. package/apps/agents-server/src/components/OpenMojiIcon/OpenMojiIcon.tsx +16 -7
  66. package/apps/agents-server/src/components/PrintHeader/PrintHeader.tsx +4 -4
  67. package/apps/agents-server/src/components/RegisterUserDialog/RegisterUserDialog.tsx +61 -0
  68. package/apps/agents-server/src/database/metadataDefaults.ts +19 -1
  69. package/apps/agents-server/src/database/migrations/2025-12-0240-agent-public-id.sql +3 -0
  70. package/apps/agents-server/src/database/migrations/2025-12-0360-agent-deleted-at.sql +1 -0
  71. package/apps/agents-server/src/database/migrations/2025-12-0370-image-table.sql +19 -0
  72. package/apps/agents-server/src/database/migrations/2025-12-0380-agent-visibility.sql +1 -0
  73. package/apps/agents-server/src/database/migrations/2025-12-0390-upload-tracking.sql +20 -0
  74. package/apps/agents-server/src/database/migrations/2025-12-0401-file-upload-status.sql +13 -0
  75. package/apps/agents-server/src/database/migrations/2025-12-0640-openai-assistant-cache.sql +12 -0
  76. package/apps/agents-server/src/database/schema.ts +109 -0
  77. package/apps/agents-server/src/generated/reservedPaths.ts +27 -0
  78. package/apps/agents-server/src/middleware.ts +7 -20
  79. package/apps/agents-server/src/tools/$provideCdnForServer.ts +6 -1
  80. package/apps/agents-server/src/utils/cdn/classes/TrackedFilesStorage.ts +57 -0
  81. package/apps/agents-server/src/utils/cdn/classes/VercelBlobStorage.ts +4 -0
  82. package/apps/agents-server/src/utils/cdn/interfaces/IFilesStorage.ts +18 -0
  83. package/apps/agents-server/src/utils/getUserIdFromRequest.ts +33 -0
  84. package/apps/agents-server/src/utils/handleChatCompletion.ts +60 -4
  85. package/apps/agents-server/src/utils/normalization/filenameToPrompt.ts +21 -0
  86. package/apps/agents-server/src/utils/validateApiKey.ts +2 -1
  87. package/esm/index.es.js +140 -27
  88. package/esm/index.es.js.map +1 -1
  89. package/esm/typings/src/_packages/types.index.d.ts +6 -2
  90. package/esm/typings/src/book-2.0/agent-source/AgentBasicInformation.d.ts +6 -1
  91. package/esm/typings/src/book-components/Chat/Chat/ChatMessageItem.d.ts +5 -1
  92. package/esm/typings/src/book-components/Chat/Chat/ChatProps.d.ts +5 -0
  93. package/esm/typings/src/book-components/Chat/CodeBlock/CodeBlock.d.ts +13 -0
  94. package/esm/typings/src/book-components/Chat/MarkdownContent/MarkdownContent.d.ts +1 -0
  95. package/esm/typings/src/book-components/_common/Dropdown/Dropdown.d.ts +2 -2
  96. package/esm/typings/src/book-components/_common/MenuHoisting/MenuHoistingContext.d.ts +56 -0
  97. package/esm/typings/src/collection/agent-collection/constructors/agent-collection-in-supabase/AgentCollectionInSupabase.d.ts +13 -7
  98. package/esm/typings/src/collection/agent-collection/constructors/agent-collection-in-supabase/AgentsDatabaseSchema.d.ts +6 -0
  99. package/esm/typings/src/commitments/DICTIONARY/DICTIONARY.d.ts +46 -0
  100. package/esm/typings/src/commitments/index.d.ts +2 -1
  101. package/esm/typings/src/llm-providers/ollama/OllamaExecutionTools.d.ts +1 -1
  102. package/esm/typings/src/llm-providers/openai/createOpenAiCompatibleExecutionTools.d.ts +1 -1
  103. package/esm/typings/src/types/typeAliases.d.ts +12 -0
  104. package/esm/typings/src/utils/environment/$detectRuntimeEnvironment.d.ts +4 -4
  105. package/esm/typings/src/utils/environment/$isRunningInBrowser.d.ts +1 -1
  106. package/esm/typings/src/utils/environment/$isRunningInJest.d.ts +1 -1
  107. package/esm/typings/src/utils/environment/$isRunningInNode.d.ts +1 -1
  108. package/esm/typings/src/utils/environment/$isRunningInWebWorker.d.ts +1 -1
  109. package/esm/typings/src/utils/markdown/extractAllBlocksFromMarkdown.d.ts +2 -2
  110. package/esm/typings/src/utils/markdown/extractOneBlockFromMarkdown.d.ts +2 -2
  111. package/esm/typings/src/utils/random/$randomBase58.d.ts +12 -0
  112. package/esm/typings/src/version.d.ts +1 -1
  113. package/package.json +1 -1
  114. package/umd/index.umd.js +146 -33
  115. package/umd/index.umd.js.map +1 -1
  116. package/apps/agents-server/package-lock.json +0 -27
  117. package/apps/agents-server/public/fonts/download-font.js +0 -22
  118. package/apps/agents-server/src/components/PrintButton/PrintButton.tsx +0 -18
@@ -0,0 +1,12 @@
1
+ CREATE TABLE IF NOT EXISTS "prefix_OpenAiAssistantCache" (
2
+ "id" BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
3
+ "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
4
+ "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
5
+
6
+ "agentHash" TEXT NOT NULL,
7
+ "assistantId" TEXT NOT NULL
8
+ );
9
+
10
+ CREATE UNIQUE INDEX IF NOT EXISTS "prefix_OpenAiAssistantCache_agentHash_idx" ON "prefix_OpenAiAssistantCache" ("agentHash");
11
+
12
+ ALTER TABLE "prefix_OpenAiAssistantCache" ENABLE ROW LEVEL SECURITY;
@@ -47,6 +47,7 @@ export type AgentsServerDatabase = {
47
47
  agentName: string;
48
48
  createdAt: string;
49
49
  updatedAt: string | null;
50
+ permanentId: string | null;
50
51
  agentHash: string;
51
52
  agentSource: string;
52
53
  agentProfile: Json;
@@ -54,12 +55,15 @@ export type AgentsServerDatabase = {
54
55
  usage: Json | null;
55
56
  preparedModelRequirements: Json | null;
56
57
  preparedExternals: Json | null;
58
+ deletedAt: string | null;
59
+ visibility: 'PUBLIC' | 'PRIVATE';
57
60
  };
58
61
  Insert: {
59
62
  id?: number;
60
63
  agentName: string;
61
64
  createdAt: string;
62
65
  updatedAt?: string | null;
66
+ permanentId?: string | null;
63
67
  agentHash: string;
64
68
  agentSource: string;
65
69
  agentProfile: Json;
@@ -67,12 +71,15 @@ export type AgentsServerDatabase = {
67
71
  usage?: Json | null;
68
72
  preparedModelRequirements?: Json | null;
69
73
  preparedExternals?: Json | null;
74
+ deletedAt?: string | null;
75
+ visibility?: 'PUBLIC' | 'PRIVATE';
70
76
  };
71
77
  Update: {
72
78
  id?: number;
73
79
  agentName?: string;
74
80
  createdAt?: string;
75
81
  updatedAt?: string | null;
82
+ permanentId?: string | null;
76
83
  agentHash?: string;
77
84
  agentSource?: string;
78
85
  agentProfile?: Json;
@@ -80,6 +87,8 @@ export type AgentsServerDatabase = {
80
87
  usage?: Json | null;
81
88
  preparedModelRequirements?: Json | null;
82
89
  preparedExternals?: Json | null;
90
+ deletedAt?: string | null;
91
+ visibility?: 'PUBLIC' | 'PRIVATE';
83
92
  };
84
93
  Relationships: [];
85
94
  };
@@ -272,6 +281,30 @@ export type AgentsServerDatabase = {
272
281
  };
273
282
  Relationships: [];
274
283
  };
284
+ OpenAiAssistantCache: {
285
+ Row: {
286
+ id: number;
287
+ createdAt: string;
288
+ updatedAt: string;
289
+ agentHash: string;
290
+ assistantId: string;
291
+ };
292
+ Insert: {
293
+ id?: number;
294
+ createdAt?: string;
295
+ updatedAt?: string;
296
+ agentHash: string;
297
+ assistantId: string;
298
+ };
299
+ Update: {
300
+ id?: number;
301
+ createdAt?: string;
302
+ updatedAt?: string;
303
+ agentHash?: string;
304
+ assistantId?: string;
305
+ };
306
+ Relationships: [];
307
+ };
275
308
  ApiTokens: {
276
309
  Row: {
277
310
  id: number;
@@ -299,6 +332,82 @@ export type AgentsServerDatabase = {
299
332
  };
300
333
  Relationships: [];
301
334
  };
335
+ Image: {
336
+ Row: {
337
+ id: number;
338
+ createdAt: string;
339
+ updatedAt: string;
340
+ filename: string;
341
+ prompt: string;
342
+ cdnUrl: string;
343
+ cdnKey: string;
344
+ };
345
+ Insert: {
346
+ id?: number;
347
+ createdAt?: string;
348
+ updatedAt?: string;
349
+ filename: string;
350
+ prompt: string;
351
+ cdnUrl: string;
352
+ cdnKey: string;
353
+ };
354
+ Update: {
355
+ id?: number;
356
+ createdAt?: string;
357
+ updatedAt?: string;
358
+ filename?: string;
359
+ prompt?: string;
360
+ cdnUrl?: string;
361
+ cdnKey?: string;
362
+ };
363
+ Relationships: [];
364
+ };
365
+ File: {
366
+ Row: {
367
+ id: number;
368
+ createdAt: string;
369
+ userId: number | null;
370
+ fileName: string;
371
+ fileSize: number;
372
+ fileType: string;
373
+ storageUrl: string | null;
374
+ shortUrl: string | null;
375
+ purpose: string;
376
+ status: 'UPLOADING' | 'COMPLETED' | 'FAILED';
377
+ };
378
+ Insert: {
379
+ id?: number;
380
+ createdAt?: string;
381
+ userId?: number | null;
382
+ fileName: string;
383
+ fileSize: number;
384
+ fileType: string;
385
+ storageUrl?: string | null;
386
+ shortUrl?: string | null;
387
+ purpose: string;
388
+ status?: 'UPLOADING' | 'COMPLETED' | 'FAILED';
389
+ };
390
+ Update: {
391
+ id?: number;
392
+ createdAt?: string;
393
+ userId?: number | null;
394
+ fileName?: string;
395
+ fileSize?: number;
396
+ fileType?: string;
397
+ storageUrl?: string | null;
398
+ shortUrl?: string | null;
399
+ purpose?: string;
400
+ status?: 'UPLOADING' | 'COMPLETED' | 'FAILED';
401
+ };
402
+ Relationships: [
403
+ {
404
+ foreignKeyName: 'File_userId_fkey';
405
+ columns: ['userId'];
406
+ referencedRelation: 'User';
407
+ referencedColumns: ['id'];
408
+ },
409
+ ];
410
+ };
302
411
  };
303
412
  Views: Record<string, never>;
304
413
  Functions: Record<string, never>;
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Reserved paths that should not be treated as agent names.
3
+ * This file is auto-generated by scripts/generate-reserved-paths.js
4
+ *
5
+ * ⚠️ WARNING: This code has been generated so that any manual changes will be overwritten
6
+ *
7
+ * @see /apps/agents-server/src/app - source directory
8
+ * @see /apps/agents-server/src/middleware.ts - where this is used
9
+ */
10
+ export const RESERVED_PATHS: readonly string[] = [
11
+ "_next",
12
+ "admin",
13
+ "agents",
14
+ "api",
15
+ "docs",
16
+ "embed",
17
+ "favicon.ico",
18
+ "humans.txt",
19
+ "manifest.webmanifest",
20
+ "recycle-bin",
21
+ "restricted",
22
+ "robots.txt",
23
+ "security.txt",
24
+ "sitemap.xml",
25
+ "sw.js",
26
+ "test"
27
+ ] as const;
@@ -2,9 +2,11 @@ import { TODO_any } from '@promptbook-local/types';
2
2
  import { createClient } from '@supabase/supabase-js';
3
3
  import { NextRequest, NextResponse } from 'next/server';
4
4
  import { SERVERS, SUPABASE_TABLE_PREFIX } from '../config';
5
+ import { $getTableName } from './database/$getTableName';
6
+ import { RESERVED_PATHS } from './generated/reservedPaths';
5
7
  import { isIpAllowed } from './utils/isIpAllowed';
6
8
 
7
- // Note: Re-implementing normalizeTo_PascalCase to avoid importing from @promptbook-local/utils which might have Node.js dependencies
9
+ // Note: Re-implementing normalizeTo_PascalCase to avoid importing from @promptbook-local/utils which might have Node.js dependencies !!!!
8
10
  function normalizeTo_PascalCase(text: string): string {
9
11
  return text
10
12
  .replace(/(?:^\w|[A-Z]|\b\w)/g, (word, index) => {
@@ -58,7 +60,7 @@ export async function middleware(req: NextRequest) {
58
60
  });
59
61
 
60
62
  const { data } = await supabase
61
- .from(`${tablePrefix}Metadata`)
63
+ .from(await $getTableName(`Metadata`))
62
64
  .select('value')
63
65
  .eq('key', 'RESTRICT_IP')
64
66
  .single();
@@ -107,7 +109,7 @@ export async function middleware(req: NextRequest) {
107
109
  });
108
110
 
109
111
  const { data } = await supabase
110
- .from(`${tablePrefix}ApiTokens`)
112
+ .from(await $getTableName(`ApiTokens`))
111
113
  .select('id')
112
114
  .eq('token', token)
113
115
  .eq('isRevoked', false)
@@ -186,22 +188,7 @@ export async function middleware(req: NextRequest) {
186
188
 
187
189
  if (
188
190
  potentialAgentName &&
189
- ![
190
- 'agents',
191
- 'api',
192
- 'admin',
193
- 'docs',
194
- 'test',
195
- 'embed',
196
- '_next',
197
- 'manifest.webmanifest',
198
- 'sw.js',
199
- 'favicon.ico',
200
- 'sitemap.xml',
201
- 'robots.txt',
202
- 'security.txt',
203
- 'humans.txt',
204
- ].includes(potentialAgentName) &&
191
+ !RESERVED_PATHS.includes(potentialAgentName) &&
205
192
  !potentialAgentName.startsWith('.') &&
206
193
  // Note: Other static files are excluded by the matcher configuration below
207
194
  true
@@ -256,7 +243,7 @@ export async function middleware(req: NextRequest) {
256
243
 
257
244
  try {
258
245
  const { data } = await supabase
259
- .from(`${prefix}Agent`)
246
+ .from(await $getTableName(`Agent`))
260
247
  .select('agentName')
261
248
  .or(orFilter)
262
249
  .limit(1)
@@ -1,3 +1,5 @@
1
+ import { $provideSupabaseForServer } from '../database/$provideSupabaseForServer';
2
+ import { TrackedFilesStorage } from '../utils/cdn/classes/TrackedFilesStorage';
1
3
  import { VercelBlobStorage } from '../utils/cdn/classes/VercelBlobStorage';
2
4
  import { IIFilesStorageWithCdn } from '../utils/cdn/interfaces/IFilesStorage';
3
5
 
@@ -13,12 +15,15 @@ let cdn: IIFilesStorageWithCdn | null = null;
13
15
  */
14
16
  export function $provideCdnForServer(): IIFilesStorageWithCdn {
15
17
  if (!cdn) {
16
- cdn = new VercelBlobStorage({
18
+ const inner = new VercelBlobStorage({
17
19
  token: process.env.VERCEL_BLOB_READ_WRITE_TOKEN!,
18
20
  pathPrefix: process.env.NEXT_PUBLIC_CDN_PATH_PREFIX!,
19
21
  cdnPublicUrl: new URL(process.env.NEXT_PUBLIC_CDN_PUBLIC_URL!),
20
22
  });
21
23
 
24
+ const supabase = $provideSupabaseForServer();
25
+ cdn = new TrackedFilesStorage(inner, supabase);
26
+
22
27
  /*
23
28
  cdn = new DigitalOceanSpaces({
24
29
  bucket: process.env.CDN_BUCKET!,
@@ -0,0 +1,57 @@
1
+ import { SupabaseClient } from '@supabase/supabase-js';
2
+ import { $getTableName } from '../../../database/$getTableName';
3
+ import { AgentsServerDatabase } from '../../../database/schema';
4
+ import type { IFile, IIFilesStorageWithCdn } from '../interfaces/IFilesStorage';
5
+
6
+ export class TrackedFilesStorage implements IIFilesStorageWithCdn {
7
+ public constructor(
8
+ private readonly inner: IIFilesStorageWithCdn,
9
+ private readonly supabase: SupabaseClient<AgentsServerDatabase>,
10
+ ) {}
11
+
12
+ public get cdnPublicUrl(): URL {
13
+ return this.inner.cdnPublicUrl;
14
+ }
15
+
16
+ public get pathPrefix(): string | undefined {
17
+ return this.inner.pathPrefix;
18
+ }
19
+
20
+ public getItemUrl(key: string): URL {
21
+ return this.inner.getItemUrl(key);
22
+ }
23
+
24
+ public async getItem(key: string): Promise<IFile | null> {
25
+ return this.inner.getItem(key);
26
+ }
27
+
28
+ public async removeItem(key: string): Promise<void> {
29
+ return this.inner.removeItem(key);
30
+ }
31
+
32
+ public async setItem(key: string, file: IFile): Promise<void> {
33
+ console.log('!!! 0', { key, file });
34
+
35
+ await this.inner.setItem(key, file);
36
+
37
+ try {
38
+ const { userId, purpose } = file;
39
+ const cdnUrl = this.getItemUrl(key).href;
40
+
41
+ console.log('!!! 1', { userId, purpose, cdnUrl, key, file });
42
+
43
+ await this.supabase.from(await $getTableName('File')).insert({
44
+ userId: userId || null,
45
+ fileName: key,
46
+ fileSize: file.fileSize ?? file.data.length,
47
+ fileType: file.type,
48
+ cdnUrl,
49
+ purpose: purpose || 'UNKNOWN',
50
+ });
51
+
52
+ console.log('!!! 2', { userId, purpose, cdnUrl, key, file });
53
+ } catch (error) {
54
+ console.error('Failed to track upload:', error);
55
+ }
56
+ }
57
+ }
@@ -14,6 +14,10 @@ export class VercelBlobStorage implements IIFilesStorageWithCdn {
14
14
  return this.config.cdnPublicUrl;
15
15
  }
16
16
 
17
+ public get pathPrefix() {
18
+ return this.config.pathPrefix;
19
+ }
20
+
17
21
  public constructor(private readonly config: IVercelBlobStorageConfig) {}
18
22
 
19
23
  public getItemUrl(key: string): URL {
@@ -5,6 +5,23 @@ export type IFile = {
5
5
  // Maybe TODO name: string_name;
6
6
  type: string_mime_type;
7
7
  data: Buffer;
8
+
9
+ /**
10
+ * User who uploaded the file
11
+ */
12
+ userId?: number;
13
+
14
+ /**
15
+ * Purpose of the upload (e.g. KNOWLEDGE, SERVER_FAVICON_URL)
16
+ */
17
+ purpose?: string;
18
+
19
+ /**
20
+ * Size of the file in bytes
21
+ *
22
+ * Note: This is optional, if not provided, the size of the buffer is used
23
+ */
24
+ fileSize?: number;
8
25
  };
9
26
 
10
27
  /**
@@ -17,6 +34,7 @@ export type IFilesStorage = Omit<IStorage<IFile>, 'length' | 'clear' | 'key'>;
17
34
  */
18
35
  export type IIFilesStorageWithCdn = IFilesStorage & {
19
36
  readonly cdnPublicUrl: URL;
37
+ readonly pathPrefix?: string;
20
38
  getItemUrl(key: string): URL;
21
39
  };
22
40
 
@@ -0,0 +1,33 @@
1
+ import { NextRequest } from 'next/server';
2
+ import { $getTableName } from '../database/$getTableName';
3
+ import { $provideSupabaseForServer } from '../database/$provideSupabaseForServer';
4
+ import { getSession } from './session';
5
+
6
+ export async function getUserIdFromRequest(request: NextRequest): Promise<number | null> {
7
+ try {
8
+ // 1. Try to get user from session (cookie)
9
+ const session = await getSession();
10
+ if (session && session.username) {
11
+ const supabase = $provideSupabaseForServer();
12
+ const { data } = await supabase
13
+ .from(await $getTableName('User'))
14
+ .select('id')
15
+ .eq('username', session.username)
16
+ .single();
17
+
18
+ if (data) {
19
+ return data.id;
20
+ }
21
+ }
22
+
23
+ // 2. Try to get user from API key (Authorization header)
24
+ // TODO: [🧠] Implement linking API keys to users if needed
25
+ // const authHeader = request.headers.get('authorization');
26
+ // ...
27
+
28
+ } catch (error) {
29
+ console.error('Error getting user ID from request:', error);
30
+ }
31
+
32
+ return null;
33
+ }
@@ -2,11 +2,13 @@ import { $getTableName } from '@/src/database/$getTableName';
2
2
  import { $provideSupabaseForServer } from '@/src/database/$provideSupabaseForServer';
3
3
  import { $provideAgentCollectionForServer } from '@/src/tools/$provideAgentCollectionForServer';
4
4
  import { $provideOpenAiAssistantExecutionToolsForServer } from '@/src/tools/$provideOpenAiAssistantExecutionToolsForServer';
5
- import { Agent, computeAgentHash, PROMPTBOOK_ENGINE_VERSION } from '@promptbook-local/core';
5
+ import { Agent, computeAgentHash, parseAgentSource, PROMPTBOOK_ENGINE_VERSION } from '@promptbook-local/core';
6
+ import { OpenAiAssistantExecutionTools } from '@promptbook-local/openai';
6
7
  import { ChatMessage, ChatPromptResult, Prompt, string_book, TODO_any } from '@promptbook-local/types';
7
8
  import { computeHash } from '@promptbook-local/utils';
8
9
  import { NextRequest, NextResponse } from 'next/server';
9
10
  import { validateApiKey } from './validateApiKey';
11
+ import { isAgentDeleted } from '../app/agents/[agentName]/_utils';
10
12
 
11
13
  export async function handleChatCompletion(
12
14
  request: NextRequest,
@@ -60,6 +62,19 @@ export async function handleChatCompletion(
60
62
  );
61
63
  }
62
64
 
65
+ // Check if agent is deleted
66
+ if (await isAgentDeleted(agentName)) {
67
+ return NextResponse.json(
68
+ {
69
+ error: {
70
+ message: 'This agent has been deleted. You can restore it from the Recycle Bin.',
71
+ type: 'agent_deleted',
72
+ },
73
+ },
74
+ { status: 410 }, // Gone - indicates the resource is no longer available
75
+ );
76
+ }
77
+
63
78
  const collection = await $provideAgentCollectionForServer();
64
79
  let agentSource: string_book;
65
80
  try {
@@ -99,7 +114,50 @@ export async function handleChatCompletion(
99
114
  );
100
115
  }
101
116
 
102
- const openAiAssistantExecutionTools = await $provideOpenAiAssistantExecutionToolsForServer();
117
+ const agentHash = computeAgentHash(agentSource);
118
+ const supabase = $provideSupabaseForServer();
119
+ const { data: assistantCache } = await supabase
120
+ .from(await $getTableName('OpenAiAssistantCache'))
121
+ .select('assistantId')
122
+ .eq('agentHash', agentHash)
123
+ .single();
124
+
125
+ let openAiAssistantExecutionTools = await $provideOpenAiAssistantExecutionToolsForServer();
126
+
127
+ if (assistantCache?.assistantId) {
128
+ console.log(`[🐱‍🚀] Reusing assistant ${assistantCache.assistantId} for agent ${agentName} (hash: ${agentHash})`);
129
+ openAiAssistantExecutionTools = openAiAssistantExecutionTools.getAssistant(assistantCache.assistantId);
130
+ } else {
131
+ console.log(`[🐱‍🚀] Creating NEW assistant for agent ${agentName} (hash: ${agentHash})`);
132
+ // Parse to get instructions and name
133
+ const parsed = parseAgentSource(agentSource);
134
+ const name = parsed.agentName || agentName;
135
+ // Extract PERSONA
136
+ const baseInstructions = parsed.personaDescription || 'You are a helpful assistant.';
137
+
138
+ // Note: Append context to instructions
139
+ const contextLines = agentSource.split('\n').filter((line) => line.startsWith('CONTEXT '));
140
+ const contextInstructions = contextLines.join('\n');
141
+ const instructions = contextInstructions ? `${baseInstructions}\n\n${contextInstructions}` : baseInstructions;
142
+
143
+ // Create assistant
144
+ const newAssistantTools = await openAiAssistantExecutionTools.createNewAssistant({
145
+ name,
146
+ instructions,
147
+ // knowledgeSources?
148
+ });
149
+
150
+ // Save to cache
151
+ const newAssistantId = newAssistantTools.assistantId;
152
+ if (newAssistantId) {
153
+ await supabase.from(await $getTableName('OpenAiAssistantCache')).insert({
154
+ agentHash,
155
+ assistantId: newAssistantId,
156
+ });
157
+ openAiAssistantExecutionTools = newAssistantTools;
158
+ }
159
+ }
160
+
103
161
  const agent = new Agent({
104
162
  agentSource,
105
163
  executionTools: {
@@ -108,7 +166,6 @@ export async function handleChatCompletion(
108
166
  isVerbose: true, // or false
109
167
  });
110
168
 
111
- const agentHash = computeAgentHash(agentSource);
112
169
  const userAgent = request.headers.get('user-agent');
113
170
  const ip =
114
171
  request.headers.get('x-forwarded-for') ||
@@ -138,7 +195,6 @@ export async function handleChatCompletion(
138
195
  content: lastMessage.content,
139
196
  };
140
197
 
141
- const supabase = $provideSupabaseForServer();
142
198
  await supabase.from(await $getTableName('ChatHistory')).insert({
143
199
  createdAt: new Date().toISOString(),
144
200
  messageHash: computeHash(userMessageContent),
@@ -0,0 +1,21 @@
1
+ import { capitalize } from '../../../../../src/utils/normalization/capitalize';
2
+
3
+ /**
4
+ * Converts a filename like "cat-sitting-on-keyboard.png" to a prompt like "Cat sitting on keyboard"
5
+ *
6
+ * @param filename - The filename to convert
7
+ * @returns The normalized prompt
8
+ */
9
+ export function filenameToPrompt(filename: string): string {
10
+ // Remove file extension
11
+ const withoutExtension = filename.replace(/\.[^/.]+$/, '');
12
+
13
+ // Replace dashes and underscores with spaces
14
+ const withSpaces = withoutExtension.replace(/[-_]/g, ' ');
15
+
16
+ // Capitalize each word
17
+ const words = withSpaces.split(' ');
18
+ const capitalizedWords = words.map(word => capitalize(word));
19
+
20
+ return capitalizedWords.join(' ');
21
+ }
@@ -1,6 +1,7 @@
1
1
  import { createClient } from '@supabase/supabase-js';
2
2
  import { NextRequest } from 'next/server';
3
3
  import { SERVERS, SUPABASE_TABLE_PREFIX } from '../../config';
4
+ import { $getTableName } from '../database/$getTableName';
4
5
 
5
6
  // Note: Re-implementing normalizeTo_PascalCase to avoid importing from @promptbook-local/utils which might have Node.js dependencies
6
7
  function normalizeTo_PascalCase(text: string): string {
@@ -95,7 +96,7 @@ export async function validateApiKey(request: NextRequest): Promise<ApiKeyValida
95
96
  });
96
97
 
97
98
  const { data, error } = await supabase
98
- .from(`${tablePrefix}ApiTokens`)
99
+ .from(await $getTableName(`ApiTokens`))
99
100
  .select('id, isRevoked')
100
101
  .eq('token', token)
101
102
  .single();