@promptbook/cli 0.112.0-135 → 0.112.0-137

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 (64) hide show
  1. package/README.md +20 -0
  2. package/apps/agents-server/README.md +0 -1
  3. package/apps/agents-server/src/app/admin/metadata/MetadataClient.tsx +246 -15
  4. package/apps/agents-server/src/app/admin/update/UpdateClient.tsx +237 -4
  5. package/apps/agents-server/src/app/api/admin/update/log/route.ts +42 -0
  6. package/apps/agents-server/src/app/api/agents/export/route.ts +41 -0
  7. package/apps/agents-server/src/app/api/agents/import/route.ts +168 -0
  8. package/apps/agents-server/src/app/api/federated-agents/route.ts +1 -1
  9. package/apps/agents-server/src/app/api/internal/agent-runner-limits/route.ts +2 -1
  10. package/apps/agents-server/src/app/api/internal/user-chat-jobs/run/route.ts +2 -1
  11. package/apps/agents-server/src/app/api/metadata/export/route.ts +34 -0
  12. package/apps/agents-server/src/app/api/metadata/import/route.ts +46 -0
  13. package/apps/agents-server/src/app/layout.tsx +1 -1
  14. package/apps/agents-server/src/components/Homepage/AgentsList.tsx +31 -1
  15. package/apps/agents-server/src/components/Homepage/AgentsListHeader.tsx +60 -1
  16. package/apps/agents-server/src/components/Homepage/useAgentsListImportExportState.ts +523 -0
  17. package/apps/agents-server/src/components/Homepage/useAgentsListState.ts +16 -0
  18. package/apps/agents-server/src/database/metadataDefaults.ts +0 -21
  19. package/apps/agents-server/src/database/seedCoreAgents.ts +235 -0
  20. package/apps/agents-server/src/database/seedDefaultAgents.ts +32 -1
  21. package/apps/agents-server/src/search/createDefaultServerSearchProviders/createFederatedAgentsSearchProvider.ts +1 -1
  22. package/apps/agents-server/src/tools/$provideAgentCollectionForServer.ts +2 -9
  23. package/apps/agents-server/src/utils/agentsTransfer/createAgentsExportZipStream.ts +68 -0
  24. package/apps/agents-server/src/utils/agentsTransfer/importAgentsFromFiles.ts +852 -0
  25. package/apps/agents-server/src/utils/backup/createBooksBackupZipStream.ts +15 -2
  26. package/apps/agents-server/src/utils/defaultAgents/loadDefaultAgentBooks.ts +46 -4
  27. package/apps/agents-server/src/utils/getFederatedServers.ts +3 -74
  28. package/apps/agents-server/src/utils/getWellKnownAgentUrl.ts +10 -4
  29. package/apps/agents-server/src/utils/localChatRunner/processLocalUserChatJob.ts +1 -1
  30. package/apps/agents-server/src/utils/metadataConfigurationTransfer.ts +426 -0
  31. package/apps/agents-server/src/utils/serverManagement/createManagedServer/bootstrapManagedServer.ts +2 -5
  32. package/apps/agents-server/src/utils/serverManagement/createManagedServer/seedServerCoreAgents.ts +162 -0
  33. package/apps/agents-server/src/utils/session.ts +2 -1
  34. package/apps/agents-server/src/utils/vpsSelfUpdate.ts +110 -0
  35. package/esm/index.es.js +1 -1
  36. package/esm/index.es.js.map +1 -1
  37. package/esm/src/_packages/core.index.d.ts +0 -2
  38. package/esm/src/utils/isTimingSafeEqualString.d.ts +25 -0
  39. package/esm/src/version.d.ts +1 -1
  40. package/package.json +1 -1
  41. package/servers.ts +1 -16
  42. package/src/_packages/core.index.ts +0 -2
  43. package/src/commitments/_common/teamInternalAgentAccess.ts +2 -1
  44. package/src/other/templates/getTemplatesPipelineCollection.ts +789 -716
  45. package/src/utils/isTimingSafeEqualString.ts +43 -0
  46. package/src/version.ts +2 -2
  47. package/src/versions.txt +2 -0
  48. package/umd/index.umd.js +1 -1
  49. package/umd/index.umd.js.map +1 -1
  50. package/umd/src/_packages/core.index.d.ts +0 -2
  51. package/umd/src/utils/isTimingSafeEqualString.d.ts +25 -0
  52. package/umd/src/version.d.ts +1 -1
  53. package/apps/agents-server/src/utils/defaultFederatedAgents/DefaultFederatedAgentsSyncOptions.ts +0 -9
  54. package/apps/agents-server/src/utils/defaultFederatedAgents/ensureDefaultFederatedAgentExists.ts +0 -277
  55. package/apps/agents-server/src/utils/defaultFederatedAgents/fetchCoreOrganizationPayload.ts +0 -39
  56. package/apps/agents-server/src/utils/defaultFederatedAgents/fetchFederatedAgentBook.ts +0 -43
  57. package/apps/agents-server/src/utils/defaultFederatedAgents/fetchWithDefaultFederatedAgentTimeout.ts +0 -28
  58. package/apps/agents-server/src/utils/defaultFederatedAgents/getDefaultFederatedAgentSyncPool.ts +0 -38
  59. package/apps/agents-server/src/utils/defaultFederatedAgents/loadActiveLocalAgentIdsByNormalizedName.ts +0 -41
  60. package/apps/agents-server/src/utils/defaultFederatedAgents/loadDefaultFederatedAgentSyncMetadata.ts +0 -76
  61. package/apps/agents-server/src/utils/defaultFederatedAgents/quoteIdentifier.ts +0 -11
  62. package/apps/agents-server/src/utils/defaultFederatedAgents/scheduleDefaultFederatedAgentsSync.ts +0 -88
  63. package/apps/agents-server/src/utils/defaultFederatedAgents/selectDefaultFederatedAgentsFromOrganizationPayload.ts +0 -181
  64. package/apps/agents-server/src/utils/defaultFederatedAgents/synchronizeDefaultFederatedAgents.ts +0 -77
@@ -15,6 +15,7 @@ import {
15
15
  } from './useAgentsListDragState';
16
16
  import { useAgentsListFolderPathRecovery } from './useAgentsListFolderPathRecovery';
17
17
  import { useAgentsListFolderState } from './useAgentsListFolderState';
18
+ import { useAgentsListImportExportState } from './useAgentsListImportExportState';
18
19
  import { useAgentsListNavigationState } from './useAgentsListNavigationState';
19
20
  import { useAgentsListOrganizationActions } from './useAgentsListOrganizationActions';
20
21
  import { useAgentsListOverlayDetailsState } from './useAgentsListOverlayDetailsState';
@@ -166,6 +167,10 @@ export function useAgentsListState(props: UseAgentsListStateProps) {
166
167
  setFolders,
167
168
  synchronizeAfterMutation,
168
169
  });
170
+ const importExportState = useAgentsListImportExportState({
171
+ currentFolderId,
172
+ synchronizeAfterMutation,
173
+ });
169
174
  const agentState = useAgentsListAgentState({
170
175
  agents,
171
176
  formatText,
@@ -255,6 +260,13 @@ export function useAgentsListState(props: UseAgentsListStateProps) {
255
260
  handleDragEnd: dragState.handleDragEnd,
256
261
  handleDragOver: dragState.handleDragOver,
257
262
  handleDragStart: dragState.handleDragStart,
263
+ handleAgentsExport: importExportState.handleAgentsExport,
264
+ handleAgentsFileDragEnter: importExportState.handleAgentsFileDragEnter,
265
+ handleAgentsFileDragLeave: importExportState.handleAgentsFileDragLeave,
266
+ handleAgentsFileDragOver: importExportState.handleAgentsFileDragOver,
267
+ handleAgentsFileDrop: importExportState.handleAgentsFileDrop,
268
+ handleAgentsImportClick: importExportState.handleAgentsImportClick,
269
+ handleAgentsImportFileChange: importExportState.handleAgentsImportFileChange,
258
270
  handleFolderContextMenu: overlayState.handleFolderContextMenu,
259
271
  handleRenameFolder: folderState.handleRenameFolder,
260
272
  handleRequestAgentVisibilityChange: agentState.handleRequestAgentVisibilityChange,
@@ -262,6 +274,10 @@ export function useAgentsListState(props: UseAgentsListStateProps) {
262
274
  handleShowQrCode: overlayState.handleShowQrCode,
263
275
  handleSubmitFolderEdit: folderState.handleSubmitFolderEdit,
264
276
  headingTitle,
277
+ importFileInputRef: importExportState.importFileInputRef,
278
+ isAgentsExporting: importExportState.isAgentsExporting,
279
+ isAgentsImportDragActive: importExportState.isAgentsImportDragActive,
280
+ isAgentsImporting: importExportState.isAgentsImporting,
265
281
  isFolderEditSubmitting: folderState.isFolderEditSubmitting,
266
282
  navigateToFolder,
267
283
  mazeAgents,
@@ -1,5 +1,3 @@
1
- import { spaceTrim } from 'spacetrim';
2
- import { CORE_AGENTS_SERVER } from '../../../../servers';
3
1
  import { DEFAULT_THINKING_MESSAGES } from '../../../../src/utils/DEFAULT_THINKING_MESSAGES';
4
2
  import { ANALYTICS_METADATA_KEYS, getAnalyticsMetadataDefinition } from '../constants/analyticsMetadata';
5
3
  import {
@@ -263,25 +261,6 @@ export const metadataDefaults: ReadonlyArray<MetadataDefinition> = [
263
261
  note: 'Comma separated list of allowed IPs or CIDR ranges. If set, only clients from these IPs are allowed to access the server.',
264
262
  type: 'IP_RANGE',
265
263
  },
266
- {
267
- key: 'CORE_SERVER',
268
- value: CORE_AGENTS_SERVER.url,
269
- note: spaceTrim(`
270
- Core Promptbook server URL used for agents that are used for common tasks, these agents are called well known agents:
271
-
272
- On the core server, the following well known agents should be hosted:
273
- - \`adam\`: The default ancestor agent for new agents
274
- - \`teacher\`: Agent that knows book syntax and can help with self-learning
275
-
276
- `),
277
- type: 'TEXT',
278
- },
279
- {
280
- key: 'IS_CORE_SERVER_HIDDEN',
281
- value: 'false',
282
- note: 'Hide the core federated server from homepage, navbar, search, and footer links while keeping it available for references when true.',
283
- type: 'BOOLEAN',
284
- },
285
264
  // <- TODO: [🆎] Allow to set well-known agent names via Metadata
286
265
 
287
266
  {
@@ -0,0 +1,235 @@
1
+ import type { SupabaseClient } from '@supabase/supabase-js';
2
+ import { spaceTrim } from 'spacetrim';
3
+ import { AgentCollectionInSupabase } from '../../../../src/collection/agent-collection/constructors/agent-collection-in-supabase/AgentCollectionInSupabase';
4
+ import type { AgentsDatabaseSchema } from '../../../../src/collection/agent-collection/constructors/agent-collection-in-supabase/AgentsDatabaseSchema';
5
+ import { parseAgentSource } from '../../../../src/book-2.0/agent-source/parseAgentSource';
6
+ import type { string_book } from '../../../../src/book-2.0/agent-source/string_book';
7
+ import { DatabaseError } from '../../../../src/errors/DatabaseError';
8
+ import { CORE_AGENT_DIRECTORY_NAME, loadCoreAgentBooks } from '../utils/defaultAgents/loadDefaultAgentBooks';
9
+ import { DEFAULT_AGENT_VISIBILITY } from '../utils/agentVisibility';
10
+ import { $getTableName } from './$getTableName';
11
+ import { $provideSupabaseForServer } from './$provideSupabaseForServer';
12
+
13
+ /**
14
+ * Logger surface used by the core-agent seeder.
15
+ *
16
+ * @private utility of `.core` folder seeding
17
+ */
18
+ type SeedCoreAgentsLogger = Pick<Console, 'error' | 'info' | 'warn'>;
19
+
20
+ /**
21
+ * Options for ensuring the bundled `.core` folder and core agents exist.
22
+ *
23
+ * @private utility of `.core` folder seeding
24
+ */
25
+ export type SeedCoreAgentsOptions = {
26
+ /**
27
+ * Optional explicit directory containing default `*.book` files.
28
+ */
29
+ readonly defaultAgentDirectory?: string | null;
30
+
31
+ /**
32
+ * Optional table prefix for the server namespace being seeded.
33
+ */
34
+ readonly tablePrefix?: string | null;
35
+
36
+ /**
37
+ * Optional logger for installer output.
38
+ */
39
+ readonly logger?: SeedCoreAgentsLogger;
40
+ };
41
+
42
+ /**
43
+ * Result of one `.core` agents seed attempt.
44
+ *
45
+ * @private utility of `.core` folder seeding
46
+ */
47
+ export type SeedCoreAgentsResult = {
48
+ /**
49
+ * Identifier of the `.core` folder, or `null` when no books were available and the folder was not created.
50
+ */
51
+ readonly coreFolderId: number | null;
52
+
53
+ /**
54
+ * Names of agents that were created during this run.
55
+ */
56
+ readonly createdAgentNames: ReadonlyArray<string>;
57
+ };
58
+
59
+ /**
60
+ * Ensures the `.core` folder and well-known core agents are present in the current server database.
61
+ *
62
+ * - Creates the `.core` folder when it is missing.
63
+ * - Creates each bundled `.core/*.book` agent when it is missing from the database.
64
+ * - Reuses the same persistence mechanism used by the default agent seeder.
65
+ *
66
+ * @param options - Optional seed controls.
67
+ * @returns Seed summary including the resolved `.core` folder id.
68
+ *
69
+ * @private utility of `.core` folder seeding
70
+ */
71
+ export async function seedCoreAgents(options: SeedCoreAgentsOptions = {}): Promise<SeedCoreAgentsResult> {
72
+ const logger = options.logger ?? console;
73
+ const tablePrefix = options.tablePrefix ?? process.env.SUPABASE_TABLE_PREFIX ?? '';
74
+ const collection = new AgentCollectionInSupabase(resolveAgentsDatabaseSupabaseClient(), { tablePrefix });
75
+
76
+ const coreAgentBooks = await loadCoreAgentBooks({
77
+ defaultAgentDirectory: options.defaultAgentDirectory,
78
+ });
79
+
80
+ if (coreAgentBooks.length === 0) {
81
+ logger.warn(
82
+ spaceTrim(`
83
+ Skipping \`.core\` agents because no bundled \`.core/*.book\` files were found.
84
+ `),
85
+ );
86
+
87
+ return { coreFolderId: null, createdAgentNames: [] };
88
+ }
89
+
90
+ const coreFolderId = await ensureCoreFolderExists();
91
+ const existingAgentNames = await loadExistingActiveAgentNames();
92
+ const createdAgentNames: Array<string> = [];
93
+
94
+ for (const [index, coreAgentBook] of coreAgentBooks.entries()) {
95
+ const agentName = parseCoreAgentName(coreAgentBook);
96
+
97
+ if (existingAgentNames.has(agentName)) {
98
+ continue;
99
+ }
100
+
101
+ const createdAgent = await collection.createAgent(coreAgentBook, {
102
+ folderId: coreFolderId,
103
+ sortOrder: index,
104
+ visibility: DEFAULT_AGENT_VISIBILITY,
105
+ });
106
+ createdAgentNames.push(createdAgent.agentName);
107
+ logger.info(`Created \`.core\` agent: ${createdAgent.agentName}`);
108
+ }
109
+
110
+ return { coreFolderId, createdAgentNames };
111
+ }
112
+
113
+ /**
114
+ * Reads the agent name from one bundled `.core` book source.
115
+ *
116
+ * @param coreAgentBook - Raw `.core` agent book source.
117
+ * @returns Persisted agent name parsed from the book.
118
+ *
119
+ * @private utility of `.core` folder seeding
120
+ */
121
+ function parseCoreAgentName(coreAgentBook: string_book): string {
122
+ return parseAgentSource(coreAgentBook).agentName;
123
+ }
124
+
125
+ /**
126
+ * Reads the set of active agent names already persisted on the current server.
127
+ *
128
+ * @returns Set of active agent names indexed for quick lookups.
129
+ *
130
+ * @private utility of `.core` folder seeding
131
+ */
132
+ async function loadExistingActiveAgentNames(): Promise<Set<string>> {
133
+ const supabase = $provideSupabaseForServer();
134
+ const agentTableName = await $getTableName('Agent');
135
+ const selectResult = await supabase
136
+ .from(agentTableName)
137
+ .select('agentName')
138
+ .is('deletedAt', null);
139
+
140
+ if (selectResult.error) {
141
+ throw new DatabaseError(
142
+ spaceTrim(
143
+ (block) => `
144
+ Failed to read active agents before seeding \`.core\` agents:
145
+
146
+ ${block(selectResult.error.message)}
147
+ `,
148
+ ),
149
+ );
150
+ }
151
+
152
+ const existingAgentNames = new Set<string>();
153
+ for (const row of (selectResult.data || []) as Array<{ agentName: string }>) {
154
+ existingAgentNames.add(row.agentName);
155
+ }
156
+ return existingAgentNames;
157
+ }
158
+
159
+ /**
160
+ * Ensures the `.core` folder exists at the root and returns its identifier.
161
+ *
162
+ * @returns Persisted `.core` folder identifier.
163
+ *
164
+ * @private utility of `.core` folder seeding
165
+ */
166
+ async function ensureCoreFolderExists(): Promise<number> {
167
+ const supabase = $provideSupabaseForServer();
168
+ const folderTableName = await $getTableName('AgentFolder');
169
+
170
+ const existingFolderResult = await supabase
171
+ .from(folderTableName)
172
+ .select('id')
173
+ .eq('name', CORE_AGENT_DIRECTORY_NAME)
174
+ .is('parentId', null)
175
+ .is('deletedAt', null)
176
+ .maybeSingle();
177
+
178
+ if (existingFolderResult.error) {
179
+ throw new DatabaseError(
180
+ spaceTrim(
181
+ (block) => `
182
+ Failed to read the \`${CORE_AGENT_DIRECTORY_NAME}\` folder before seeding core agents:
183
+
184
+ ${block(existingFolderResult.error.message)}
185
+ `,
186
+ ),
187
+ );
188
+ }
189
+
190
+ if (existingFolderResult.data) {
191
+ return (existingFolderResult.data as { id: number }).id;
192
+ }
193
+
194
+ const insertFolderResult = await supabase
195
+ .from(folderTableName)
196
+ .insert({
197
+ name: CORE_AGENT_DIRECTORY_NAME,
198
+ parentId: null,
199
+ sortOrder: 0,
200
+ icon: null,
201
+ color: null,
202
+ createdAt: new Date().toISOString(),
203
+ updatedAt: null,
204
+ } as never)
205
+ .select('id')
206
+ .maybeSingle();
207
+
208
+ if (insertFolderResult.error || !insertFolderResult.data) {
209
+ throw new DatabaseError(
210
+ spaceTrim(
211
+ (block) => `
212
+ Failed to create the \`${CORE_AGENT_DIRECTORY_NAME}\` folder while seeding core agents:
213
+
214
+ ${block(
215
+ insertFolderResult.error?.message ||
216
+ `The database did not return the created \`${CORE_AGENT_DIRECTORY_NAME}\` folder.`,
217
+ )}
218
+ `,
219
+ ),
220
+ );
221
+ }
222
+
223
+ return (insertFolderResult.data as { id: number }).id;
224
+ }
225
+
226
+ /**
227
+ * Resolves the Supabase-shaped client for the Agent collection subset.
228
+ *
229
+ * @returns Supabase client typed for agent collection persistence.
230
+ *
231
+ * @private utility of `.core` folder seeding
232
+ */
233
+ function resolveAgentsDatabaseSupabaseClient(): SupabaseClient<AgentsDatabaseSchema> {
234
+ return $provideSupabaseForServer() as unknown as SupabaseClient<AgentsDatabaseSchema>;
235
+ }
@@ -5,6 +5,7 @@ import { $provideSupabaseForServer } from './$provideSupabaseForServer';
5
5
  import { DEFAULT_AGENT_VISIBILITY } from '../utils/agentVisibility';
6
6
  import { loadDefaultAgentBooks } from '../utils/defaultAgents/loadDefaultAgentBooks';
7
7
  import { loadAgentsServerEnvFile } from './loadAgentsServerEnvFile';
8
+ import { seedCoreAgents } from './seedCoreAgents';
8
9
 
9
10
  /**
10
11
  * Environment variable with an explicit default-agent source directory.
@@ -93,6 +94,36 @@ export async function seedDefaultAgents(options: SeedDefaultAgentsOptions = {}):
93
94
  const logger = options.logger ?? console;
94
95
  const tablePrefix = options.tablePrefix ?? process.env[SUPABASE_TABLE_PREFIX_ENV_NAME] ?? '';
95
96
  const collection = new AgentCollectionInSupabase(resolveAgentsDatabaseSupabaseClient(), { tablePrefix });
97
+ const defaultAgentDirectory = options.defaultAgentDirectory ?? process.env[DEFAULT_AGENTS_DIRECTORY_ENV_NAME];
98
+
99
+ const result = await seedDefaultAgentBooks(collection, { defaultAgentDirectory, logger });
100
+
101
+ await seedCoreAgents({
102
+ defaultAgentDirectory,
103
+ tablePrefix,
104
+ logger,
105
+ });
106
+
107
+ return result;
108
+ }
109
+
110
+ /**
111
+ * Inserts the bundled top-level default agent books when the server is empty.
112
+ *
113
+ * @param collection - Agent collection bound to the current table prefix.
114
+ * @param options - Seeding context shared with `seedDefaultAgents`.
115
+ * @returns Seed summary for default agents only (core agents are tracked separately).
116
+ *
117
+ * @private utility of standalone default-agent seeding
118
+ */
119
+ async function seedDefaultAgentBooks(
120
+ collection: AgentCollectionInSupabase,
121
+ options: {
122
+ readonly defaultAgentDirectory: string | undefined;
123
+ readonly logger: SeedDefaultAgentsLogger;
124
+ },
125
+ ): Promise<SeedDefaultAgentsResult> {
126
+ const { defaultAgentDirectory, logger } = options;
96
127
  const existingAgentCount = await countExistingAgents(collection);
97
128
 
98
129
  if (existingAgentCount > 0) {
@@ -111,7 +142,7 @@ export async function seedDefaultAgents(options: SeedDefaultAgentsOptions = {}):
111
142
  }
112
143
 
113
144
  const defaultAgentBooks = await loadDefaultAgentBooks({
114
- defaultAgentDirectory: options.defaultAgentDirectory ?? process.env[DEFAULT_AGENTS_DIRECTORY_ENV_NAME],
145
+ defaultAgentDirectory,
115
146
  });
116
147
 
117
148
  if (defaultAgentBooks.length === 0) {
@@ -30,7 +30,7 @@ export function createFederatedAgentsSearchProvider(): ServerSearchProvider {
30
30
 
31
31
  let federatedServers: string[] = [];
32
32
  try {
33
- federatedServers = await getFederatedServers({ excludeHiddenCoreServer: true });
33
+ federatedServers = await getFederatedServers();
34
34
  } catch (error) {
35
35
  console.error('[search] Failed to load federated servers:', error);
36
36
  return [];
@@ -36,7 +36,7 @@ export async function $provideAgentCollectionForServer(): Promise<AgentCollectio
36
36
  */
37
37
 
38
38
  const supabase = $provideSupabaseForServer();
39
- const { publicUrl, tablePrefix } = await $provideServer();
39
+ const { tablePrefix } = await $provideServer();
40
40
  const cachedAgentCollection = agentCollectionsByTablePrefix.get(tablePrefix);
41
41
 
42
42
  if (cachedAgentCollection) {
@@ -50,16 +50,9 @@ export async function $provideAgentCollectionForServer(): Promise<AgentCollectio
50
50
 
51
51
  agentCollectionsByTablePrefix.set(tablePrefix, providedCollection);
52
52
 
53
- const [{ attachAgentPreparationScheduling }, { scheduleDefaultFederatedAgentsSync }] = await Promise.all([
54
- import('../utils/attachAgentPreparationScheduling'),
55
- import('../utils/defaultFederatedAgents/scheduleDefaultFederatedAgentsSync'),
56
- ]);
53
+ const { attachAgentPreparationScheduling } = await import('../utils/attachAgentPreparationScheduling');
57
54
 
58
55
  attachAgentPreparationScheduling(providedCollection, { tablePrefix });
59
- scheduleDefaultFederatedAgentsSync({
60
- tablePrefix,
61
- localServerUrl: publicUrl.href,
62
- });
63
56
 
64
57
  return providedCollection;
65
58
  }
@@ -0,0 +1,68 @@
1
+ import JSZip from 'jszip';
2
+ import { getMetadata } from '../../database/getMetadata';
3
+ import { appendBooksBackupEntriesToZip, type BooksBackupZipStream } from '../backup/createBooksBackupZipStream';
4
+
5
+ /**
6
+ * Metadata key used for the human-readable server name.
7
+ */
8
+ const SERVER_NAME_METADATA_KEY = 'SERVER_NAME';
9
+
10
+ /**
11
+ * Filename suffix used for agents-only exports.
12
+ */
13
+ const AGENTS_EXPORT_FILENAME_SUFFIX = '.agents.zip';
14
+
15
+ /**
16
+ * Fallback server filename stem when metadata does not provide a usable name.
17
+ */
18
+ const DEFAULT_AGENTS_EXPORT_FILENAME_STEM = 'promptbook-agents-server';
19
+
20
+ /**
21
+ * Pattern matching non-alphanumeric filename slug separators.
22
+ */
23
+ const FILENAME_SLUG_SEPARATOR_PATTERN = /[^a-z0-9]+/g;
24
+
25
+ /**
26
+ * Pattern matching leading or trailing filename slug separators.
27
+ */
28
+ const FILENAME_SLUG_EDGE_SEPARATOR_PATTERN = /^-+|-+$/g;
29
+
30
+ /**
31
+ * Builds a ZIP stream containing only agent books organized by folder structure.
32
+ *
33
+ * @returns Download filename and stream payload.
34
+ */
35
+ export async function createAgentsExportZipStream(): Promise<BooksBackupZipStream> {
36
+ const serverName = await getMetadata(SERVER_NAME_METADATA_KEY);
37
+ const zip = new JSZip();
38
+
39
+ await appendBooksBackupEntriesToZip(zip, '');
40
+
41
+ const stream = zip.generateNodeStream({
42
+ streamFiles: true,
43
+ compression: 'DEFLATE',
44
+ compressionOptions: { level: 6 },
45
+ });
46
+
47
+ return {
48
+ filename: `${createAgentsExportFilenameStem(serverName)}${AGENTS_EXPORT_FILENAME_SUFFIX}`,
49
+ stream,
50
+ };
51
+ }
52
+
53
+ /**
54
+ * Creates a safe lowercase filename stem from server metadata.
55
+ *
56
+ * @param serverName - Raw server name from metadata.
57
+ * @returns Filename stem for the agents export.
58
+ */
59
+ export function createAgentsExportFilenameStem(serverName: string | null | undefined): string {
60
+ const normalizedServerName = typeof serverName === 'string' ? serverName.trim().toLowerCase() : '';
61
+ const filenameStem = normalizedServerName
62
+ .normalize('NFKD')
63
+ .replace(/[\u0300-\u036f]/g, '')
64
+ .replace(FILENAME_SLUG_SEPARATOR_PATTERN, '-')
65
+ .replace(FILENAME_SLUG_EDGE_SEPARATOR_PATTERN, '');
66
+
67
+ return filenameStem || DEFAULT_AGENTS_EXPORT_FILENAME_STEM;
68
+ }