@promptbook/cli 0.112.0-134 → 0.112.0-136

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 (94) hide show
  1. package/apps/agents-server/README.md +0 -1
  2. package/apps/agents-server/src/app/admin/chat-feedback/page.tsx +2 -4
  3. package/apps/agents-server/src/app/admin/chat-history/page.tsx +2 -4
  4. package/apps/agents-server/src/app/admin/metadata/MetadataClient.tsx +246 -15
  5. package/apps/agents-server/src/app/admin/update/UpdateClient.tsx +237 -4
  6. package/apps/agents-server/src/app/api/admin/update/log/route.ts +42 -0
  7. package/apps/agents-server/src/app/api/agents/[agentName]/route.ts +28 -9
  8. package/apps/agents-server/src/app/api/agents/export/route.ts +41 -0
  9. package/apps/agents-server/src/app/api/agents/import/route.ts +168 -0
  10. package/apps/agents-server/src/app/api/chat/route.ts +29 -7
  11. package/apps/agents-server/src/app/api/chat-feedback/export/route.ts +2 -2
  12. package/apps/agents-server/src/app/api/chat-feedback/route.ts +3 -3
  13. package/apps/agents-server/src/app/api/chat-history/[id]/route.ts +2 -2
  14. package/apps/agents-server/src/app/api/chat-history/export/route.ts +2 -2
  15. package/apps/agents-server/src/app/api/chat-history/route.ts +3 -3
  16. package/apps/agents-server/src/app/api/chat-streaming/route.ts +29 -0
  17. package/apps/agents-server/src/app/api/elevenlabs/tts/route.ts +60 -2
  18. package/apps/agents-server/src/app/api/federated-agents/route.ts +1 -1
  19. package/apps/agents-server/src/app/api/images/[filename]/route.ts +102 -0
  20. package/apps/agents-server/src/app/api/metadata/export/route.ts +34 -0
  21. package/apps/agents-server/src/app/api/metadata/import/route.ts +46 -0
  22. package/apps/agents-server/src/app/api/openai/v1/audio/transcriptions/route.ts +111 -2
  23. package/apps/agents-server/src/app/api/page-preview/check/route.ts +18 -0
  24. package/apps/agents-server/src/app/api/page-preview/screenshot/route.ts +15 -8
  25. package/apps/agents-server/src/app/api/team-agent-profile/route.ts +20 -0
  26. package/apps/agents-server/src/app/layout.tsx +1 -1
  27. package/apps/agents-server/src/components/AgentContextMenu/useAgentContextMenuItems.ts +27 -19
  28. package/apps/agents-server/src/components/Homepage/AgentsList.tsx +31 -1
  29. package/apps/agents-server/src/components/Homepage/AgentsListHeader.tsx +60 -1
  30. package/apps/agents-server/src/components/Homepage/useAgentsListImportExportState.ts +523 -0
  31. package/apps/agents-server/src/components/Homepage/useAgentsListState.ts +16 -0
  32. package/apps/agents-server/src/constants/defaultAgentAvatarVisual.ts +1 -1
  33. package/apps/agents-server/src/database/metadataDefaults.ts +0 -21
  34. package/apps/agents-server/src/database/migrations/2026-06-2700-default-agent-avatar-visual-octopus3d4.sql +16 -0
  35. package/apps/agents-server/src/database/seedCoreAgents.ts +235 -0
  36. package/apps/agents-server/src/database/seedDefaultAgents.ts +32 -1
  37. package/apps/agents-server/src/search/createDefaultServerSearchProviders/createFederatedAgentsSearchProvider.ts +1 -1
  38. package/apps/agents-server/src/tools/$provideAgentCollectionForServer.ts +2 -9
  39. package/apps/agents-server/src/utils/agentOwnership.ts +54 -5
  40. package/apps/agents-server/src/utils/agentsTransfer/createAgentsExportZipStream.ts +68 -0
  41. package/apps/agents-server/src/utils/agentsTransfer/importAgentsFromFiles.ts +852 -0
  42. package/apps/agents-server/src/utils/backup/createBooksBackupZipStream.ts +15 -2
  43. package/apps/agents-server/src/utils/defaultAgents/loadDefaultAgentBooks.ts +46 -4
  44. package/apps/agents-server/src/utils/findAgentForCallerWriteAccess.ts +36 -0
  45. package/apps/agents-server/src/utils/getFederatedServers.ts +3 -74
  46. package/apps/agents-server/src/utils/getWellKnownAgentUrl.ts +10 -4
  47. package/apps/agents-server/src/utils/metadataConfigurationTransfer.ts +426 -0
  48. package/apps/agents-server/src/utils/paidApiRequestGuard.ts +241 -0
  49. package/apps/agents-server/src/utils/serverManagement/createManagedServer/bootstrapManagedServer.ts +2 -5
  50. package/apps/agents-server/src/utils/serverManagement/createManagedServer/seedServerCoreAgents.ts +162 -0
  51. package/apps/agents-server/src/utils/userChat/createRunUserChatJobExecutionContext.ts +36 -8
  52. package/apps/agents-server/src/utils/userChat/persistUserChatJobProgressCard.ts +11 -2
  53. package/apps/agents-server/src/utils/userChat/resolveUserChatProgressToolHighlights.ts +100 -0
  54. package/apps/agents-server/src/utils/userChat/runUserChatJob.ts +4 -3
  55. package/apps/agents-server/src/utils/userChat/userChatProgressCard.ts +272 -27
  56. package/apps/agents-server/src/utils/validateApiKey.ts +7 -3
  57. package/apps/agents-server/src/utils/vpsSelfUpdate.ts +110 -0
  58. package/esm/index.es.js +795 -39
  59. package/esm/index.es.js.map +1 -1
  60. package/esm/src/_packages/core.index.d.ts +0 -2
  61. package/esm/src/avatars/types/AvatarVisualDefinition.d.ts +1 -1
  62. package/esm/src/avatars/visuals/octopus3d4AvatarVisual.d.ts +7 -0
  63. package/esm/src/utils/validators/url/isValidAgentUrl.d.ts +5 -0
  64. package/esm/src/version.d.ts +1 -1
  65. package/package.json +1 -1
  66. package/servers.ts +1 -16
  67. package/src/_packages/core.index.ts +0 -2
  68. package/src/avatars/types/AvatarVisualDefinition.ts +1 -0
  69. package/src/avatars/visuals/avatarVisualRegistry.ts +2 -0
  70. package/src/avatars/visuals/octopus3d4AvatarVisual.ts +1295 -0
  71. package/src/other/templates/getTemplatesPipelineCollection.ts +762 -883
  72. package/src/utils/agents/resolveAgentAvatarImageUrl.ts +1 -1
  73. package/src/utils/validators/url/isValidAgentUrl.ts +5 -7
  74. package/src/version.ts +2 -2
  75. package/src/versions.txt +2 -1
  76. package/umd/index.umd.js +795 -39
  77. package/umd/index.umd.js.map +1 -1
  78. package/umd/src/_packages/core.index.d.ts +0 -2
  79. package/umd/src/avatars/types/AvatarVisualDefinition.d.ts +1 -1
  80. package/umd/src/avatars/visuals/octopus3d4AvatarVisual.d.ts +7 -0
  81. package/umd/src/utils/validators/url/isValidAgentUrl.d.ts +5 -0
  82. package/umd/src/version.d.ts +1 -1
  83. package/apps/agents-server/src/utils/defaultFederatedAgents/DefaultFederatedAgentsSyncOptions.ts +0 -9
  84. package/apps/agents-server/src/utils/defaultFederatedAgents/ensureDefaultFederatedAgentExists.ts +0 -277
  85. package/apps/agents-server/src/utils/defaultFederatedAgents/fetchCoreOrganizationPayload.ts +0 -39
  86. package/apps/agents-server/src/utils/defaultFederatedAgents/fetchFederatedAgentBook.ts +0 -43
  87. package/apps/agents-server/src/utils/defaultFederatedAgents/fetchWithDefaultFederatedAgentTimeout.ts +0 -28
  88. package/apps/agents-server/src/utils/defaultFederatedAgents/getDefaultFederatedAgentSyncPool.ts +0 -38
  89. package/apps/agents-server/src/utils/defaultFederatedAgents/loadActiveLocalAgentIdsByNormalizedName.ts +0 -41
  90. package/apps/agents-server/src/utils/defaultFederatedAgents/loadDefaultFederatedAgentSyncMetadata.ts +0 -76
  91. package/apps/agents-server/src/utils/defaultFederatedAgents/quoteIdentifier.ts +0 -11
  92. package/apps/agents-server/src/utils/defaultFederatedAgents/scheduleDefaultFederatedAgentsSync.ts +0 -88
  93. package/apps/agents-server/src/utils/defaultFederatedAgents/selectDefaultFederatedAgentsFromOrganizationPayload.ts +0 -181
  94. package/apps/agents-server/src/utils/defaultFederatedAgents/synchronizeDefaultFederatedAgents.ts +0 -77
@@ -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
  }
@@ -69,9 +69,7 @@ export async function findOwnedAgentByIdentifier(userId: number, identifier: str
69
69
  const tableName = await $getTableName('Agent');
70
70
  const { data, error } = await supabase
71
71
  .from(tableName)
72
- .select(
73
- 'id, agentName, createdAt, updatedAt, permanentId, agentHash, agentSource, agentProfile, promptbookEngineVersion, folderId, sortOrder, deletedAt, visibility, userId',
74
- )
72
+ .select(OWNED_AGENT_ROW_COLUMNS)
75
73
  .eq('userId', userId as never)
76
74
  .or(buildAgentNameOrIdFilter(identifier))
77
75
  .limit(10);
@@ -80,7 +78,58 @@ export async function findOwnedAgentByIdentifier(userId: number, identifier: str
80
78
  throw new Error(`Failed to load agent "${identifier}": ${error.message}`);
81
79
  }
82
80
 
83
- const rows = (data || []) as unknown as OwnedAgentRow[];
81
+ return pickAgentRowByIdentifier((data || []) as unknown as OwnedAgentRow[], identifier, 'for this owner');
82
+ }
83
+
84
+ /**
85
+ * Loads one agent by permanent id or agent name without an owner restriction.
86
+ *
87
+ * Owner-aware callers should use {@link findOwnedAgentByIdentifier}; this helper is reserved for administrative
88
+ * overrides where the caller has been independently authorized to read or mutate any agent.
89
+ *
90
+ * @param identifier - Permanent id or agent name.
91
+ * @returns Matching agent row or `null` when nothing matches.
92
+ */
93
+ export async function findAgentByIdentifier(identifier: string): Promise<OwnedAgentRow | null> {
94
+ const supabase = $provideSupabaseForServer();
95
+ const tableName = await $getTableName('Agent');
96
+ const { data, error } = await supabase
97
+ .from(tableName)
98
+ .select(OWNED_AGENT_ROW_COLUMNS)
99
+ .or(buildAgentNameOrIdFilter(identifier))
100
+ .limit(10);
101
+
102
+ if (error) {
103
+ throw new Error(`Failed to load agent "${identifier}": ${error.message}`);
104
+ }
105
+
106
+ return pickAgentRowByIdentifier((data || []) as unknown as OwnedAgentRow[], identifier, 'across agents');
107
+ }
108
+
109
+ /**
110
+ * Database columns selected for every agent row exposed by ownership-aware helpers.
111
+ */
112
+ const OWNED_AGENT_ROW_COLUMNS =
113
+ 'id, agentName, createdAt, updatedAt, permanentId, agentHash, agentSource, agentProfile, promptbookEngineVersion, folderId, sortOrder, deletedAt, visibility, userId';
114
+
115
+ /**
116
+ * Disambiguates a list of agent rows that all matched the same identifier.
117
+ *
118
+ * Resolution order:
119
+ * 1. Exact `permanentId` match — always preferred because permanent ids are unique.
120
+ * 2. Single `agentName` match — preserves the legacy lookup by human-readable name.
121
+ * 3. Single remaining row — accepts the only candidate found.
122
+ *
123
+ * @param rows - Candidate agent rows returned by Supabase.
124
+ * @param identifier - Identifier that was searched.
125
+ * @param ambiguityScope - Human-readable scope used in the ambiguity error message.
126
+ * @returns Best-matching row or `null` when no candidate matches.
127
+ */
128
+ function pickAgentRowByIdentifier(
129
+ rows: OwnedAgentRow[],
130
+ identifier: string,
131
+ ambiguityScope: string,
132
+ ): OwnedAgentRow | null {
84
133
  if (rows.length === 0) {
85
134
  return null;
86
135
  }
@@ -100,7 +149,7 @@ export async function findOwnedAgentByIdentifier(userId: number, identifier: str
100
149
  }
101
150
 
102
151
  throw new Error(
103
- `Agent identifier "${identifier}" is ambiguous for this owner. Use the stable \`permanentId\` returned by the API.`,
152
+ `Agent identifier "${identifier}" is ambiguous ${ambiguityScope}. Use the stable \`permanentId\` returned by the API.`,
104
153
  );
105
154
  }
106
155
 
@@ -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
+ }