@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
@@ -1,6 +1,8 @@
1
1
  import type { NextRequest } from 'next/server';
2
2
  import { NextResponse } from 'next/server';
3
3
  import { assertsError } from '../../../../../../../src/errors/assertsError';
4
+ import { assertSafeUrl } from '../../../../utils/assertSafeUrl';
5
+ import { getCurrentUser } from '../../../../utils/getCurrentUser';
4
6
  import { checkIfUrlCanBeEmbedded } from '../../../../utils/iframe/checkIfUrlCanBeEmbedded';
5
7
 
6
8
  /**
@@ -11,14 +13,30 @@ import { checkIfUrlCanBeEmbedded } from '../../../../utils/iframe/checkIfUrlCanB
11
13
  * - `url` — the fully-qualified HTTP(S) URL to check
12
14
  *
13
15
  * Returns `{ canEmbed: boolean }`.
16
+ *
17
+ * Requires authentication to prevent unauthenticated SSRF abuse.
18
+ * The destination URL is validated against private/internal IP ranges before fetching.
14
19
  */
15
20
  export async function GET(request: NextRequest): Promise<NextResponse> {
21
+ const currentUser = await getCurrentUser();
22
+ if (!currentUser) {
23
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
24
+ }
25
+
16
26
  const url = request.nextUrl.searchParams.get('url');
17
27
 
18
28
  if (!url) {
19
29
  return NextResponse.json({ error: 'Missing required query parameter: url' }, { status: 400 });
20
30
  }
21
31
 
32
+ // Guard against SSRF: reject private/internal IPs and non-HTTP(S) schemes
33
+ try {
34
+ assertSafeUrl(url);
35
+ } catch (error) {
36
+ assertsError(error);
37
+ return NextResponse.json({ error: error.message }, { status: 400 });
38
+ }
39
+
22
40
  try {
23
41
  const canEmbed = await checkIfUrlCanBeEmbedded(url);
24
42
  return NextResponse.json({ canEmbed });
@@ -3,6 +3,8 @@ import { serializeError } from '@promptbook-local/utils';
3
3
  import type { NextRequest } from 'next/server';
4
4
  import { NextResponse } from 'next/server';
5
5
  import { assertsError } from '../../../../../../../src/errors/assertsError';
6
+ import { assertSafeUrl } from '../../../../utils/assertSafeUrl';
7
+ import { getCurrentUser } from '../../../../utils/getCurrentUser';
6
8
 
7
9
  /**
8
10
  * Takes a screenshot of the given URL using a headless browser.
@@ -11,23 +13,28 @@ import { assertsError } from '../../../../../../../src/errors/assertsError';
11
13
  * - `url` — the fully-qualified HTTP(S) URL to screenshot
12
14
  *
13
15
  * Returns a PNG image.
16
+ *
17
+ * Requires authentication to prevent unauthenticated SSRF abuse.
18
+ * The destination URL is validated against private/internal IP ranges before fetching.
14
19
  */
15
20
  export async function GET(request: NextRequest): Promise<NextResponse> {
21
+ const currentUser = await getCurrentUser();
22
+ if (!currentUser) {
23
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
24
+ }
25
+
16
26
  const url = request.nextUrl.searchParams.get('url');
17
27
 
18
28
  if (!url) {
19
29
  return NextResponse.json({ error: 'Missing required query parameter: url' }, { status: 400 });
20
30
  }
21
31
 
22
- let parsedUrl: URL;
32
+ // Guard against SSRF: reject private/internal IPs and non-HTTP(S) schemes
23
33
  try {
24
- parsedUrl = new URL(url);
25
- } catch {
26
- return NextResponse.json({ error: 'Invalid URL' }, { status: 400 });
27
- }
28
-
29
- if (parsedUrl.protocol !== 'http:' && parsedUrl.protocol !== 'https:') {
30
- return NextResponse.json({ error: 'Only http and https URLs are supported' }, { status: 400 });
34
+ assertSafeUrl(url);
35
+ } catch (error) {
36
+ assertsError(error);
37
+ return NextResponse.json({ error: error.message }, { status: 400 });
31
38
  }
32
39
 
33
40
  try {
@@ -8,7 +8,10 @@ import {
8
8
  resolvePlaceholderImageUrl,
9
9
  resolveProfileImageUrl,
10
10
  } from '../../../../../../src/book-components/Chat/utils/loadAgentProfile';
11
+ import { assertsError } from '../../../../../../src/errors/assertsError';
11
12
  import { isValidAgentUrl } from '../../../../../../src/utils/validators/url/isValidAgentUrl';
13
+ import { assertSafeUrl } from '../../../utils/assertSafeUrl';
14
+ import { getCurrentUser } from '../../../utils/getCurrentUser';
12
15
 
13
16
  /**
14
17
  * Response for team agent profile.
@@ -88,8 +91,17 @@ async function fetchAgentProfile(agentUrl: string, localServerUrl: string): Prom
88
91
  *
89
92
  * The client uses this to render tool call chips without relying on direct cross-origin requests
90
93
  * to the teammate agent.
94
+ *
95
+ * Requires authentication to prevent unauthenticated SSRF abuse and exfiltration of the
96
+ * internal team access token. The destination URL is validated against private/internal
97
+ * IP ranges before any outbound request is made.
91
98
  */
92
99
  export async function GET(request: Request) {
100
+ const currentUser = await getCurrentUser();
101
+ if (!currentUser) {
102
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
103
+ }
104
+
93
105
  const requestUrl = new URL(request.url);
94
106
  const agentUrl = requestUrl.searchParams.get('url');
95
107
 
@@ -97,6 +109,14 @@ export async function GET(request: Request) {
97
109
  return NextResponse.json({ error: 'Invalid agent URL' }, { status: 400 });
98
110
  }
99
111
 
112
+ // Guard against SSRF: reject private/internal IPs and non-HTTP(S) schemes
113
+ try {
114
+ assertSafeUrl(agentUrl);
115
+ } catch (error) {
116
+ assertsError(error);
117
+ return NextResponse.json({ error: error.message }, { status: 400 });
118
+ }
119
+
100
120
  const normalizedUrl = agentUrl.replace(/\/$/, '');
101
121
  const profile = await fetchAgentProfile(normalizedUrl, requestUrl.origin);
102
122
 
@@ -149,7 +149,7 @@ async function getLayoutFederatedServers(options: {
149
149
  }
150
150
 
151
151
  try {
152
- const federatedServerUrls = await getFederatedServers({ excludeHiddenCoreServer: true });
152
+ const federatedServerUrls = await getFederatedServers();
153
153
  return Promise.all(
154
154
  federatedServerUrls.map(async (url) => ({
155
155
  title: `Federated: ${new URL(url).hostname}`,
@@ -300,6 +300,7 @@ function createManagementMenuItems(
300
300
  * @param formatText - Agent-aware text formatter.
301
301
  * @param handleRequestVisibilityUpdate - Visibility dialog action.
302
302
  * @param integrationLink - Generated integration link metadata.
303
+ * @param isAdmin - Whether the current user is an administrator (gates admin-only links).
303
304
  * @param isAuthenticated - Whether the current user is logged in.
304
305
  * @param shouldShowVisibilityAction - Whether visibility editing is available.
305
306
  * @param usageAnalyticsHref - Usage analytics URL for the current agent.
@@ -311,6 +312,7 @@ function createAdminMenuItems(
311
312
  formatText: FormatAgentContextMenuText,
312
313
  handleRequestVisibilityUpdate: () => Promise<void>,
313
314
  integrationLink: AgentContextMenuNavigationLink,
315
+ isAdmin: boolean,
314
316
  isAuthenticated: boolean,
315
317
  shouldShowVisibilityAction: boolean,
316
318
  usageAnalyticsHref: string,
@@ -333,26 +335,31 @@ function createAdminMenuItems(
333
335
  );
334
336
  }
335
337
 
338
+ if (isAdmin) {
339
+ menuItems.push(
340
+ {
341
+ type: 'link',
342
+ href: `/admin/chat-history?agentName=${encodeURIComponent(agentName)}`,
343
+ icon: MessageSquareIcon,
344
+ label: formatText('Chat History'),
345
+ },
346
+ {
347
+ type: 'link',
348
+ href: usageAnalyticsHref,
349
+ icon: BarChart3Icon,
350
+ label: formatText('Usage Analytics'),
351
+ },
352
+ {
353
+ type: 'link',
354
+ href: `/admin/chat-feedback?agentName=${encodeURIComponent(agentName)}`,
355
+ icon: MessageCircleQuestionIcon,
356
+ label: formatText('Chat Feedback'),
357
+ },
358
+ createDividerItem(),
359
+ );
360
+ }
361
+
336
362
  menuItems.push(
337
- {
338
- type: 'link',
339
- href: `/admin/chat-history?agentName=${encodeURIComponent(agentName)}`,
340
- icon: MessageSquareIcon,
341
- label: formatText('Chat History'),
342
- },
343
- {
344
- type: 'link',
345
- href: usageAnalyticsHref,
346
- icon: BarChart3Icon,
347
- label: formatText('Usage Analytics'),
348
- },
349
- {
350
- type: 'link',
351
- href: `/admin/chat-feedback?agentName=${encodeURIComponent(agentName)}`,
352
- icon: MessageCircleQuestionIcon,
353
- label: formatText('Chat Feedback'),
354
- },
355
- createDividerItem(),
356
363
  {
357
364
  type: 'link',
358
365
  href: integrationLink.href,
@@ -437,6 +444,7 @@ export function useAgentContextMenuItems(props: AgentContextMenuBaseProps): Cont
437
444
  formatText,
438
445
  handleRequestVisibilityUpdate,
439
446
  integrationLink,
447
+ isAdmin,
440
448
  isAuthenticated,
441
449
  shouldShowVisibilityAction,
442
450
  usageAnalyticsHref,
@@ -6,6 +6,7 @@ import type { AgentOrganizationAgent, AgentOrganizationFolder } from '../../util
6
6
  import { AgentsListDialogs } from './AgentsListDialogs';
7
7
  import { AgentsListHeader } from './AgentsListHeader';
8
8
  import { AgentsListViewContent } from './AgentsListViewContent';
9
+ import { AGENTS_IMPORT_FILE_INPUT_ACCEPT } from './useAgentsListImportExportState';
9
10
  import type { AgentWithVisibility } from './useFederatedAgents';
10
11
  import { useAgentsListState } from './useAgentsListState';
11
12
 
@@ -72,14 +73,43 @@ export function AgentsList(props: AgentsListProps) {
72
73
  isAdmin && contextMenuFolder ? () => state.handleRequestFolderVisibilityUpdate(contextMenuFolder.id) : undefined;
73
74
 
74
75
  return (
75
- <section className="mt-16 first:mt-4 mb-4">
76
+ <section
77
+ className={`relative mt-16 first:mt-4 mb-4 ${
78
+ state.isAgentsImportDragActive ? 'rounded-xl ring-2 ring-blue-400 ring-offset-4' : ''
79
+ }`}
80
+ onDragEnter={isAdmin ? state.handleAgentsFileDragEnter : undefined}
81
+ onDragLeave={isAdmin ? state.handleAgentsFileDragLeave : undefined}
82
+ onDragOver={isAdmin ? state.handleAgentsFileDragOver : undefined}
83
+ onDrop={isAdmin ? state.handleAgentsFileDrop : undefined}
84
+ >
85
+ {isAdmin && (
86
+ <input
87
+ ref={state.importFileInputRef}
88
+ type="file"
89
+ multiple
90
+ accept={AGENTS_IMPORT_FILE_INPUT_ACCEPT}
91
+ className="hidden"
92
+ onChange={(event) => void state.handleAgentsImportFileChange(event)}
93
+ disabled={state.isAgentsImporting}
94
+ />
95
+ )}
96
+ {isAdmin && state.isAgentsImportDragActive && (
97
+ <div className="pointer-events-none absolute inset-0 z-20 flex items-center justify-center rounded-xl border-2 border-dashed border-blue-400 bg-blue-50/85 text-sm font-medium text-blue-800 shadow-sm">
98
+ Drop books or ZIP archives to import agents
99
+ </div>
100
+ )}
76
101
  <AgentsListHeader
77
102
  agentCount={state.agentCount}
78
103
  allAgentsLabel={state.allAgentsLabel}
79
104
  breadcrumbFolders={state.breadcrumbFolders}
80
105
  canOrganize={canOrganize}
81
106
  headingTitle={state.headingTitle}
107
+ isAdmin={isAdmin}
108
+ isAgentsExporting={state.isAgentsExporting}
109
+ isAgentsImporting={state.isAgentsImporting}
82
110
  onCreateFolder={state.handleCreateFolder}
111
+ onExportAgents={() => void state.handleAgentsExport()}
112
+ onImportAgents={state.handleAgentsImportClick}
83
113
  onNavigateToFolder={state.navigateToFolder}
84
114
  onSetViewMode={state.setViewMode}
85
115
  viewMode={state.viewMode}
@@ -1,6 +1,16 @@
1
1
  'use client';
2
2
 
3
- import { Building2, FolderPlusIcon, Gamepad2, Grid, Map, Network, type LucideIcon } from 'lucide-react';
3
+ import {
4
+ Building2,
5
+ DownloadIcon,
6
+ FolderPlusIcon,
7
+ Gamepad2,
8
+ Grid,
9
+ Map,
10
+ Network,
11
+ UploadIcon,
12
+ type LucideIcon,
13
+ } from 'lucide-react';
4
14
  import type { AgentOrganizationFolder } from '../../utils/agentOrganization/types';
5
15
  import type { HomeViewMode } from './homeViewMode';
6
16
  import { BreadcrumbDropTarget } from './BreadcrumbDropTarget';
@@ -55,6 +65,18 @@ type AgentsListHeaderProps = {
55
65
  * Current heading title.
56
66
  */
57
67
  readonly headingTitle: string;
68
+ /**
69
+ * Whether agents import/export controls are available.
70
+ */
71
+ readonly isAdmin: boolean;
72
+ /**
73
+ * Downloads an agents export.
74
+ */
75
+ readonly onExportAgents: () => void;
76
+ /**
77
+ * Opens the agents import file picker.
78
+ */
79
+ readonly onImportAgents: () => void;
58
80
  /**
59
81
  * Opens the create-folder dialog.
60
82
  */
@@ -71,6 +93,14 @@ type AgentsListHeaderProps = {
71
93
  * Currently active homepage view mode.
72
94
  */
73
95
  readonly viewMode: HomeViewMode;
96
+ /**
97
+ * Whether agents export is currently running.
98
+ */
99
+ readonly isAgentsExporting: boolean;
100
+ /**
101
+ * Whether agents import is currently running.
102
+ */
103
+ readonly isAgentsImporting: boolean;
74
104
  };
75
105
 
76
106
  /**
@@ -100,7 +130,12 @@ export function AgentsListHeader({
100
130
  breadcrumbFolders,
101
131
  canOrganize,
102
132
  headingTitle,
133
+ isAdmin,
134
+ isAgentsExporting,
135
+ isAgentsImporting,
103
136
  onCreateFolder,
137
+ onExportAgents,
138
+ onImportAgents,
104
139
  onNavigateToFolder,
105
140
  onSetViewMode,
106
141
  viewMode,
@@ -138,6 +173,30 @@ export function AgentsListHeader({
138
173
  )}
139
174
  </div>
140
175
  <div className="flex items-center gap-2">
176
+ {isListView && isAdmin && (
177
+ <>
178
+ <button
179
+ type="button"
180
+ onClick={onExportAgents}
181
+ disabled={isAgentsExporting || isAgentsImporting}
182
+ className="flex items-center gap-2 px-3 py-2 rounded-lg border border-gray-200 bg-white text-gray-700 hover:bg-gray-50 transition-colors text-sm disabled:cursor-not-allowed disabled:opacity-60"
183
+ title="Export agents"
184
+ >
185
+ <DownloadIcon className="w-4 h-4" />
186
+ {isAgentsExporting ? 'Exporting...' : 'Export'}
187
+ </button>
188
+ <button
189
+ type="button"
190
+ onClick={onImportAgents}
191
+ disabled={isAgentsExporting || isAgentsImporting}
192
+ className="flex items-center gap-2 px-3 py-2 rounded-lg border border-gray-200 bg-white text-gray-700 hover:bg-gray-50 transition-colors text-sm disabled:cursor-not-allowed disabled:opacity-60"
193
+ title="Import agents"
194
+ >
195
+ <UploadIcon className="w-4 h-4" />
196
+ {isAgentsImporting ? 'Importing...' : 'Import'}
197
+ </button>
198
+ </>
199
+ )}
141
200
  {isListView && canOrganize && (
142
201
  <button
143
202
  type="button"