@promptbook/cli 0.112.0-102 → 0.112.0-104

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 (54) hide show
  1. package/apps/agents-server/README.md +0 -6
  2. package/apps/agents-server/src/app/AddAgentButton.tsx +0 -5
  3. package/apps/agents-server/src/app/actions.ts +50 -0
  4. package/apps/agents-server/src/app/admin/image-generator-test/ImageAttachmentsEditor.tsx +11 -6
  5. package/apps/agents-server/src/app/admin/metadata/MetadataClient.tsx +13 -15
  6. package/apps/agents-server/src/app/admin/servers/useCreateServerWizard.ts +13 -14
  7. package/apps/agents-server/src/app/agents/[agentName]/AgentProfileChat.tsx +3 -4
  8. package/apps/agents-server/src/app/api/health/route.ts +18 -0
  9. package/apps/agents-server/src/app/api/upload/route.ts +110 -383
  10. package/apps/agents-server/src/components/AgentProfile/AgentProfile.tsx +1 -4
  11. package/apps/agents-server/src/components/Header/Header.tsx +0 -11
  12. package/apps/agents-server/src/components/Header/useHeaderAgentMenus.tsx +0 -5
  13. package/apps/agents-server/src/components/NewAgentDialog/useNewAgentDialog.tsx +39 -16
  14. package/apps/agents-server/src/constants/defaultAgentAvatarVisual.ts +1 -1
  15. package/apps/agents-server/src/database/migrations/2026-06-0200-default-agent-avatar-visual-octopus3d3.sql +16 -0
  16. package/apps/agents-server/src/middleware.ts +2 -1
  17. package/apps/agents-server/src/tools/$provideCdnForServer.ts +87 -49
  18. package/apps/agents-server/src/utils/agentRouting/resolveAgentRouteTarget.ts +27 -4
  19. package/apps/agents-server/src/utils/cdn/classes/DigitalOceanSpaces.ts +17 -49
  20. package/apps/agents-server/src/utils/cdn/classes/TrackedFilesStorage.ts +5 -2
  21. package/apps/agents-server/src/utils/cdn/interfaces/IFilesStorage.ts +5 -0
  22. package/apps/agents-server/src/utils/defaultAgents/defaultAgents.ts +168 -0
  23. package/apps/agents-server/src/utils/defaultAgents/installDefaultAgents.ts +139 -0
  24. package/apps/agents-server/src/utils/shareTargetPayloads.ts +15 -63
  25. package/apps/agents-server/src/utils/upload/createBookEditorUploadHandler.ts +23 -150
  26. package/apps/agents-server/src/utils/upload/uploadFileToServer.ts +113 -0
  27. package/esm/index.es.js +711 -41
  28. package/esm/index.es.js.map +1 -1
  29. package/esm/scripts/run-codex-prompts/common/waitForPause.d.ts +12 -0
  30. package/esm/scripts/run-codex-prompts/main/runPromptRound.d.ts +2 -1
  31. package/esm/scripts/run-codex-prompts/ui/buildCoderRunUiFrame.d.ts +1 -0
  32. package/esm/scripts/run-codex-prompts/ui/buildRunUiFrameShared.d.ts +1 -1
  33. package/esm/src/avatars/types/AvatarVisualDefinition.d.ts +1 -1
  34. package/esm/src/avatars/visuals/octopus3d3AvatarVisual.d.ts +7 -0
  35. package/esm/src/version.d.ts +1 -1
  36. package/package.json +1 -1
  37. package/src/avatars/types/AvatarVisualDefinition.ts +1 -0
  38. package/src/avatars/visuals/avatarVisualRegistry.ts +2 -0
  39. package/src/avatars/visuals/octopus3d3AvatarVisual.ts +903 -0
  40. package/src/book-components/Chat/MarkdownContent/MarkdownContent.tsx +1 -3
  41. package/src/other/templates/getTemplatesPipelineCollection.ts +799 -809
  42. package/src/utils/agents/resolveAgentAvatarImageUrl.ts +1 -1
  43. package/src/version.ts +2 -2
  44. package/src/versions.txt +1 -0
  45. package/umd/index.umd.js +711 -41
  46. package/umd/index.umd.js.map +1 -1
  47. package/umd/scripts/run-codex-prompts/common/waitForPause.d.ts +12 -0
  48. package/umd/scripts/run-codex-prompts/main/runPromptRound.d.ts +2 -1
  49. package/umd/scripts/run-codex-prompts/ui/buildCoderRunUiFrame.d.ts +1 -0
  50. package/umd/scripts/run-codex-prompts/ui/buildRunUiFrameShared.d.ts +1 -1
  51. package/umd/src/avatars/types/AvatarVisualDefinition.d.ts +1 -1
  52. package/umd/src/avatars/visuals/octopus3d3AvatarVisual.d.ts +7 -0
  53. package/umd/src/version.d.ts +1 -1
  54. package/apps/agents-server/src/utils/cdn/resolveCdnStorageProvider.ts +0 -40
@@ -44,7 +44,6 @@ type UseHeaderAgentMenusOptions = {
44
44
  readonly isAdmin: boolean;
45
45
  readonly isAuthenticated: boolean;
46
46
  readonly isInstalled: boolean;
47
- readonly navigateToHref: (href: string) => void;
48
47
  readonly namingPlural: string;
49
48
  readonly namingSingular: string;
50
49
  readonly onAgentRenamed: (payload: AgentContextMenuRenamePayload) => void;
@@ -160,7 +159,6 @@ export function useHeaderAgentMenus({
160
159
  isAdmin,
161
160
  isAuthenticated,
162
161
  isInstalled,
163
- navigateToHref,
164
162
  namingPlural,
165
163
  namingSingular,
166
164
  onAgentRenamed,
@@ -214,9 +212,6 @@ export function useHeaderAgentMenus({
214
212
  openNewAgentDialog,
215
213
  dialog: newAgentDialog,
216
214
  } = useNewAgentDialog({
217
- onCreated: ({ targetPath }) => {
218
- navigateToHref(targetPath);
219
- },
220
215
  onCreateFailed: async (error) => {
221
216
  await showNewAgentFailure('Failed to create agent:', error, namingSingular, translate);
222
217
  },
@@ -3,7 +3,9 @@
3
3
  import type { string_book } from '@promptbook-local/types';
4
4
  import type { ReactElement } from 'react';
5
5
  import { useCallback, useState } from 'react';
6
+ import { appendHeadlessParam, useIsHeadless } from '../_utils/headlessParam';
6
7
  import type { AgentVisibility } from '../../utils/agentVisibility';
8
+ import { buildFreshAgentChatHref } from '../../utils/agentRouting/agentRouteHrefs';
7
9
  import {
8
10
  $createAgentFromBookAction,
9
11
  $generateAgentBoilerplateAction,
@@ -51,9 +53,9 @@ type CreatedAgentPayload = {
51
53
  */
52
54
  type UseNewAgentDialogOptions = {
53
55
  /**
54
- * Called after a new agent is created successfully.
56
+ * Optional callback invoked after the new agent payload is prepared and before navigation starts.
55
57
  */
56
- readonly onCreated: (agent: CreatedAgentPayload) => Promise<void> | void;
58
+ readonly onCreated?: (agent: CreatedAgentPayload) => Promise<void> | void;
57
59
  /**
58
60
  * Optional callback invoked when creating an agent fails.
59
61
  */
@@ -120,11 +122,27 @@ function extractAgentNameFromBoilerplate(boilerplate: string_book): string {
120
122
  );
121
123
  }
122
124
 
125
+ /**
126
+ * Creates the navigation payload returned after one agent is persisted.
127
+ *
128
+ * @param agentName - Persisted display name of the agent.
129
+ * @param permanentId - Canonical immutable identifier of the agent.
130
+ * @returns Shared created-agent payload used by all creation surfaces.
131
+ */
132
+ function createCreatedAgentPayload(agentName: string, permanentId: string): CreatedAgentPayload {
133
+ return {
134
+ agentName,
135
+ permanentId,
136
+ targetPath: buildFreshAgentChatHref(permanentId),
137
+ };
138
+ }
139
+
123
140
  /**
124
141
  * Provides a shared "create new agent" workflow with boilerplate loading and a book-editing dialog.
125
142
  */
126
143
  export function useNewAgentDialog(options: UseNewAgentDialogOptions): UseNewAgentDialogResult {
127
144
  const { onCreated, onCreateFailed, onPrepareFailed } = options;
145
+ const isHeadless = useIsHeadless();
128
146
  const [isPreparingDialog, setIsPreparingDialog] = useState(false);
129
147
  const [dialogState, setDialogState] = useState<NewAgentDialogState | null>(null);
130
148
 
@@ -132,6 +150,21 @@ export function useNewAgentDialog(options: UseNewAgentDialogOptions): UseNewAgen
132
150
  setDialogState(null);
133
151
  }, []);
134
152
 
153
+ /**
154
+ * Finalizes one successful creation by hard-navigating to the new chat route.
155
+ *
156
+ * The App Router can transiently keep the just-created dynamic route in a stale not-found
157
+ * state, so new-agent creation intentionally uses a full navigation once the route is ready.
158
+ */
159
+ const handleCreatedAgent = useCallback(
160
+ async (agent: CreatedAgentPayload) => {
161
+ await onCreated?.(agent);
162
+ setDialogState(null);
163
+ window.location.assign(appendHeadlessParam(agent.targetPath, isHeadless));
164
+ },
165
+ [isHeadless, onCreated],
166
+ );
167
+
135
168
  const openNewAgentDialog = useCallback(
136
169
  async (openOptions?: OpenNewAgentDialogOptions) => {
137
170
  setIsPreparingDialog(true);
@@ -200,17 +233,12 @@ export function useNewAgentDialog(options: UseNewAgentDialogOptions): UseNewAgen
200
233
  surface: 'editor',
201
234
  folderId: dialogState.targetFolderId,
202
235
  });
203
- await onCreated({
204
- agentName,
205
- permanentId,
206
- targetPath: `/agents/${encodeURIComponent(permanentId)}`,
207
- });
208
- setDialogState(null);
236
+ await handleCreatedAgent(createCreatedAgentPayload(agentName, permanentId));
209
237
  } catch (error) {
210
238
  await onCreateFailed?.(error);
211
239
  }
212
240
  },
213
- [dialogState, onCreateFailed, onCreated],
241
+ [dialogState, handleCreatedAgent, onCreateFailed],
214
242
  );
215
243
 
216
244
  const handleCreateFromWizard = useCallback(
@@ -232,17 +260,12 @@ export function useNewAgentDialog(options: UseNewAgentDialogOptions): UseNewAgen
232
260
  knowledgeCount: request.knowledgeCount,
233
261
  });
234
262
 
235
- await onCreated({
236
- agentName,
237
- permanentId,
238
- targetPath: `/agents/${encodeURIComponent(permanentId)}`,
239
- });
240
- setDialogState(null);
263
+ await handleCreatedAgent(createCreatedAgentPayload(agentName, permanentId));
241
264
  } catch (error) {
242
265
  await onCreateFailed?.(error);
243
266
  }
244
267
  },
245
- [dialogState, onCreateFailed, onCreated],
268
+ [dialogState, handleCreatedAgent, onCreateFailed],
246
269
  );
247
270
 
248
271
  const handleOpenEditorFromWizard = useCallback((request: NewAgentWizardOpenEditorRequest) => {
@@ -53,7 +53,7 @@ export const DEFAULT_AGENT_AVATAR_VISUAL_METADATA_VALUES = DEFAULT_AGENT_AVATAR_
53
53
  export const DEFAULT_AGENT_AVATAR_VISUAL_METADATA_VALUE =
54
54
  DEFAULT_AGENT_AVATAR_VISUAL_METADATA_OPTIONS.find(
55
55
  ({ visualId }) => visualId === SHARED_DEFAULT_AGENT_AVATAR_VISUAL_ID,
56
- )?.metadataValue || 'OCTOPUS3';
56
+ )?.metadataValue || 'OCTOPUS3D3';
57
57
 
58
58
  /**
59
59
  * Resolves one raw metadata value to a supported built-in avatar visual id.
@@ -0,0 +1,16 @@
1
+ INSERT INTO "prefix_Metadata" ("key", "value", "note", "createdAt", "updatedAt")
2
+ SELECT 'DEFAULT_AGENT_AVATAR_VISUAL',
3
+ 'OCTOPUS3D3',
4
+ 'Default built-in avatar visual used for agents without `META IMAGE` or `META AVATAR`. Allowed values: PIXEL_ART, OCTOPUS, OCTOPUS2, OCTOPUS3, OCTOPUS3D, OCTOPUS3D2, OCTOPUS3D3, ASCII_OCTOPUS, MINECRAFT, MINECRAFT2, FRACTAL, ORB.',
5
+ NOW(),
6
+ NOW()
7
+ WHERE NOT EXISTS (SELECT 1 FROM "prefix_Metadata" WHERE "key" = 'DEFAULT_AGENT_AVATAR_VISUAL');
8
+
9
+ UPDATE "prefix_Metadata"
10
+ SET "value" = CASE
11
+ WHEN UPPER(COALESCE("value", '')) = 'OCTOPUS3' THEN 'OCTOPUS3D3'
12
+ ELSE "value"
13
+ END,
14
+ "note" = 'Default built-in avatar visual used for agents without `META IMAGE` or `META AVATAR`. Allowed values: PIXEL_ART, OCTOPUS, OCTOPUS2, OCTOPUS3, OCTOPUS3D, OCTOPUS3D2, OCTOPUS3D3, ASCII_OCTOPUS, MINECRAFT, MINECRAFT2, FRACTAL, ORB.',
15
+ "updatedAt" = NOW()
16
+ WHERE "key" = 'DEFAULT_AGENT_AVATAR_VISUAL';
@@ -53,8 +53,9 @@ export const config = {
53
53
  * - favicon.ico (favicon file)
54
54
  * - robots.txt (should not block on middleware DB lookups)
55
55
  * - public folder
56
+ * - api/health (standalone VPS readiness probe)
56
57
  * - api/internal (worker/cron routes are authorized separately)
57
58
  */
58
- '/((?!_next/static|_next/image|favicon.ico|logo-|fonts/|robots.txt|api/internal).*)',
59
+ '/((?!_next/static|_next/image|favicon.ico|logo-|fonts/|robots.txt|api/health|api/internal).*)',
59
60
  ],
60
61
  };
@@ -3,87 +3,125 @@ import { DigitalOceanSpaces } from '../utils/cdn/classes/DigitalOceanSpaces';
3
3
  import { TrackedFilesStorage } from '../utils/cdn/classes/TrackedFilesStorage';
4
4
  import { VercelBlobStorage } from '../utils/cdn/classes/VercelBlobStorage';
5
5
  import { IIFilesStorageWithCdn } from '../utils/cdn/interfaces/IFilesStorage';
6
- import { resolveCdnStorageProvider } from '../utils/cdn/resolveCdnStorageProvider';
7
6
 
8
7
  /**
9
- * Environment value that enables path-style S3 requests.
8
+ * Region expected by the bundled VersityGW S3-compatible storage.
10
9
  *
11
- * @private internal cache for `$provideCdnForServer`
10
+ * @private internal default for `$provideCdnForServer`
12
11
  */
13
- const TRUE_ENV_VALUE = 'true';
12
+ const SELF_CONTAINED_S3_DEFAULT_REGION = 'us-east-1';
14
13
 
15
14
  /**
16
- * Cache of raw CDN instance.
15
+ * Legacy fallback used by Cloudflare R2-style external S3 configuration.
17
16
  *
18
- * @private internal cache for `$provideCdnForServer`
17
+ * @private internal default for `$provideCdnForServer`
19
18
  */
20
- let rawCdn: IIFilesStorageWithCdn | null = null;
19
+ const EXTERNAL_S3_DEFAULT_REGION = 'auto';
21
20
 
22
21
  /**
23
- * Cache of tracked CDN instance.
22
+ * Cache of CDN instance
24
23
  *
25
24
  * @private internal cache for `$provideCdnForServer`
26
25
  */
27
- let trackedCdn: IIFilesStorageWithCdn | null = null;
26
+ let cdn: IIFilesStorageWithCdn | null = null;
28
27
 
29
28
  /**
30
- * Creates a CDN storage instance from environment variables.
29
+ * Provides a CDN storage interface for server-side file operations, with caching to reuse instances.
30
+ */
31
+ export function $provideCdnForServer(): IIFilesStorageWithCdn {
32
+ if (!cdn) {
33
+ const inner = createCdnStorageForServer();
34
+ const supabase = $provideSupabaseForServer();
35
+ cdn = new TrackedFilesStorage(inner, supabase);
36
+ }
37
+
38
+ return cdn;
39
+ }
40
+
41
+ /**
42
+ * Creates the configured CDN storage implementation for server-side file operations.
31
43
  *
32
- * @private internal factory for `$provideCdnForServer`
44
+ * @private helper of `$provideCdnForServer`
33
45
  */
34
- function createCdnForServer(): IIFilesStorageWithCdn {
35
- const provider = resolveCdnStorageProvider();
36
-
37
- switch (provider) {
38
- case 's3':
39
- return new DigitalOceanSpaces({
40
- bucket: process.env.CDN_BUCKET!,
41
- pathPrefix: process.env.NEXT_PUBLIC_CDN_PATH_PREFIX || '',
42
- endpoint: process.env.CDN_ENDPOINT!,
43
- region: process.env.CDN_REGION || 'auto',
44
- accessKeyId: process.env.CDN_ACCESS_KEY_ID!,
45
- secretAccessKey: process.env.CDN_SECRET_ACCESS_KEY!,
46
- cdnPublicUrl: new URL(process.env.NEXT_PUBLIC_CDN_PUBLIC_URL!),
47
- gzip: process.env.CDN_GZIP !== 'false',
48
- forcePathStyle: process.env.CDN_FORCE_PATH_STYLE === TRUE_ENV_VALUE,
49
- isPublicReadAclEnabled: process.env.CDN_ENABLE_PUBLIC_READ_ACL !== 'false',
50
- });
51
-
52
- case 'vercel':
53
- return new VercelBlobStorage({
54
- token: process.env.VERCEL_BLOB_READ_WRITE_TOKEN!,
55
- pathPrefix: process.env.NEXT_PUBLIC_CDN_PATH_PREFIX!,
56
- cdnPublicUrl: new URL(process.env.NEXT_PUBLIC_CDN_PUBLIC_URL!),
57
- });
46
+ function createCdnStorageForServer(): IIFilesStorageWithCdn {
47
+ if (isS3CompatibleStorageSelected()) {
48
+ return new DigitalOceanSpaces({
49
+ bucket: process.env.CDN_BUCKET!,
50
+ pathPrefix: process.env.NEXT_PUBLIC_CDN_PATH_PREFIX || '',
51
+ endpoint: process.env.CDN_ENDPOINT!,
52
+ accessKeyId: process.env.CDN_ACCESS_KEY_ID!,
53
+ secretAccessKey: process.env.CDN_SECRET_ACCESS_KEY!,
54
+ cdnPublicUrl: new URL(process.env.NEXT_PUBLIC_CDN_PUBLIC_URL!),
55
+ gzip: true,
56
+ forcePathStyle: process.env.CDN_FORCE_PATH_STYLE === 'true',
57
+ region: resolveS3CompatibleStorageRegion(),
58
+ });
58
59
  }
60
+
61
+ return new VercelBlobStorage({
62
+ token: process.env.VERCEL_BLOB_READ_WRITE_TOKEN!,
63
+ pathPrefix: process.env.NEXT_PUBLIC_CDN_PATH_PREFIX || '',
64
+ cdnPublicUrl: new URL(process.env.NEXT_PUBLIC_CDN_PUBLIC_URL!),
65
+ });
59
66
  }
60
67
 
61
68
  /**
62
- * Provides an untracked CDN storage interface for code paths that manage `File` rows themselves.
69
+ * Checks whether the current environment should use the S3-compatible storage implementation.
63
70
  *
64
- * @private internal cache for `$provideCdnForServer`
71
+ * @private helper of `$provideCdnForServer`
65
72
  */
66
- export function $provideUntrackedCdnForServer(): IIFilesStorageWithCdn {
67
- if (!rawCdn) {
68
- rawCdn = createCdnForServer();
73
+ function isS3CompatibleStorageSelected(): boolean {
74
+ const storageMode = getS3CompatibleStorageMode();
75
+ const isS3StorageMode =
76
+ storageMode === 's3' || storageMode === 'external-s3' || storageMode === 'self-contained-s3';
77
+
78
+ if (isS3StorageMode) {
79
+ return hasS3CompatibleStorageConfiguration();
69
80
  }
70
81
 
71
- return rawCdn;
82
+ return !process.env.VERCEL_BLOB_READ_WRITE_TOKEN && hasS3CompatibleStorageConfiguration();
72
83
  }
73
84
 
74
85
  /**
75
- * Provides a CDN storage interface for server-side file operations, with caching to reuse instances.
86
+ * Resolves the configured S3-compatible storage mode.
76
87
  *
77
- * @private internal cache for `$provideCdnForServer`
88
+ * @private helper of `$provideCdnForServer`
78
89
  */
79
- export function $provideCdnForServer(): IIFilesStorageWithCdn {
80
- if (!trackedCdn) {
81
- const inner = $provideUntrackedCdnForServer();
82
- const supabase = $provideSupabaseForServer();
83
- trackedCdn = new TrackedFilesStorage(inner, supabase);
90
+ function getS3CompatibleStorageMode(): string {
91
+ return (process.env.PTBK_FILE_STORAGE_MODE || process.env.CDN_PROVIDER || '').toLowerCase();
92
+ }
93
+
94
+ /**
95
+ * Resolves the S3 signing region used by AWS SDK requests.
96
+ *
97
+ * @private helper of `$provideCdnForServer`
98
+ */
99
+ function resolveS3CompatibleStorageRegion(): string {
100
+ const configuredRegion = process.env.CDN_REGION?.trim();
101
+ if (configuredRegion) {
102
+ return configuredRegion;
84
103
  }
85
104
 
86
- return trackedCdn;
105
+ if (getS3CompatibleStorageMode() === 'self-contained-s3') {
106
+ return SELF_CONTAINED_S3_DEFAULT_REGION;
107
+ }
108
+
109
+ return EXTERNAL_S3_DEFAULT_REGION;
110
+ }
111
+
112
+ /**
113
+ * Checks whether all S3-compatible storage environment variables are present.
114
+ *
115
+ * @private helper of `$provideCdnForServer`
116
+ */
117
+ function hasS3CompatibleStorageConfiguration(): boolean {
118
+ return Boolean(
119
+ process.env.CDN_BUCKET &&
120
+ process.env.CDN_ENDPOINT &&
121
+ process.env.CDN_ACCESS_KEY_ID &&
122
+ process.env.CDN_SECRET_ACCESS_KEY &&
123
+ process.env.NEXT_PUBLIC_CDN_PUBLIC_URL,
124
+ );
87
125
  }
88
126
 
89
127
  // TODO: [🏓] Unite `xxxForServer` and `xxxForNode` naming
@@ -56,15 +56,21 @@ type PseudoAgentRouteTarget = {
56
56
  export type AgentRouteTarget = LocalAgentRouteTarget | RemoteAgentRouteTarget | PseudoAgentRouteTarget;
57
57
 
58
58
  /**
59
- * Resolves any incoming `/agents/:agentId` token into a canonical target URL.
59
+ * Resolves any incoming `/agents/:agentId` token into a canonical target URL without memoization.
60
60
  *
61
61
  * Supported inputs include plain IDs/names, `@name`, `{name}`, `{id}`, and absolute `/agents/...` URLs.
62
62
  * Pseudo-agent tokens such as `{User}`, `{Void}`, or `{Null}` resolve to dedicated documentation pages.
63
63
  *
64
64
  * @param rawReference - Raw decoded route parameter value.
65
+ * @param options - Optional cache-bypass controls used by create-agent flows.
65
66
  * @returns Canonical local/remote route target or `null` when the reference cannot be resolved.
66
67
  */
67
- const getCachedAgentRouteTarget = cache(async (rawReference: string): Promise<AgentRouteTarget | null> => {
68
+ async function resolveAgentRouteTargetUncached(
69
+ rawReference: string,
70
+ options?: {
71
+ readonly forceRefresh?: boolean;
72
+ },
73
+ ): Promise<AgentRouteTarget | null> {
68
74
  const parsedBookScopedAgentIdentifier = parseBookScopedAgentIdentifier(rawReference);
69
75
  if (parsedBookScopedAgentIdentifier) {
70
76
  const { publicUrl } = await $provideServer();
@@ -96,7 +102,7 @@ const getCachedAgentRouteTarget = cache(async (rawReference: string): Promise<Ag
96
102
  };
97
103
  }
98
104
 
99
- const resolver = await $provideAgentReferenceResolver();
105
+ const resolver = await $provideAgentReferenceResolver({ forceRefresh: options?.forceRefresh });
100
106
  let resolvedUrlValue: string;
101
107
 
102
108
  try {
@@ -137,15 +143,32 @@ const getCachedAgentRouteTarget = cache(async (rawReference: string): Promise<Ag
137
143
  canonicalAgentId,
138
144
  canonicalUrl: `${localServerUrl}${AGENT_PATH_PREFIX}${encodeURIComponent(canonicalAgentId)}`,
139
145
  };
146
+ }
147
+
148
+ /**
149
+ * Memoized route-target resolver used for normal page rendering.
150
+ */
151
+ const getCachedAgentRouteTarget = cache(async (rawReference: string): Promise<AgentRouteTarget | null> => {
152
+ return resolveAgentRouteTargetUncached(rawReference);
140
153
  });
141
154
 
142
155
  /**
143
156
  * Resolves any incoming `/agents/:agentId` token into a canonical target URL.
144
157
  *
145
158
  * @param rawReference - Raw decoded route parameter value.
159
+ * @param options - Optional cache-bypass controls used by create-agent flows.
146
160
  * @returns Canonical local/remote route target or `null` when the reference cannot be resolved.
147
161
  */
148
- export async function resolveAgentRouteTarget(rawReference: string): Promise<AgentRouteTarget | null> {
162
+ export async function resolveAgentRouteTarget(
163
+ rawReference: string,
164
+ options?: {
165
+ readonly forceRefresh?: boolean;
166
+ },
167
+ ): Promise<AgentRouteTarget | null> {
168
+ if (options?.forceRefresh) {
169
+ return resolveAgentRouteTargetUncached(rawReference, options);
170
+ }
171
+
149
172
  return getCachedAgentRouteTarget(rawReference);
150
173
  }
151
174
 
@@ -12,45 +12,16 @@ type IDigitalOceanSpacesConfig = {
12
12
  readonly bucket: string;
13
13
  readonly pathPrefix: string;
14
14
  readonly endpoint: string;
15
- readonly region?: string;
16
15
  readonly accessKeyId: string;
17
16
  readonly secretAccessKey: string;
18
17
  readonly cdnPublicUrl: URL;
19
18
  readonly gzip: boolean;
20
19
  readonly forcePathStyle?: boolean;
21
- readonly isPublicReadAclEnabled?: boolean;
20
+ readonly region?: string;
22
21
 
23
22
  // TODO: [⛳️] Probbably prefix should be in this config not on the consumer side
24
23
  };
25
24
 
26
- /**
27
- * Resolves the S3 endpoint URL, preserving explicit `http` endpoints for local MinIO.
28
- *
29
- * @private internal helper for DigitalOceanSpaces.
30
- */
31
- function resolveS3Endpoint(endpoint: string): string {
32
- if (/^https?:\/\//i.test(endpoint)) {
33
- return endpoint;
34
- }
35
-
36
- return `https://${endpoint}`;
37
- }
38
-
39
- /**
40
- * Returns a URL object whose pathname ends with `/`.
41
- *
42
- * @private internal helper for DigitalOceanSpaces.
43
- */
44
- function ensureTrailingSlashUrl(url: URL): URL {
45
- const normalizedUrl = new URL(url.href);
46
-
47
- if (!normalizedUrl.pathname.endsWith('/')) {
48
- normalizedUrl.pathname = `${normalizedUrl.pathname}/`;
49
- }
50
-
51
- return normalizedUrl;
52
- }
53
-
54
25
  /**
55
26
  * Class implementing digital ocean spaces.
56
27
  */
@@ -64,7 +35,7 @@ export class DigitalOceanSpaces implements IIFilesStorageWithCdn {
64
35
  public constructor(private readonly config: IDigitalOceanSpacesConfig) {
65
36
  this.s3 = new S3Client({
66
37
  region: config.region || 'auto',
67
- endpoint: resolveS3Endpoint(config.endpoint),
38
+ endpoint: normalizeS3Endpoint(config.endpoint),
68
39
  forcePathStyle: config.forcePathStyle,
69
40
  credentials: {
70
41
  accessKeyId: config.accessKeyId,
@@ -74,13 +45,13 @@ export class DigitalOceanSpaces implements IIFilesStorageWithCdn {
74
45
  }
75
46
 
76
47
  public getItemUrl(key: string): URL {
77
- return new URL(this.getStorageKey(key), ensureTrailingSlashUrl(this.cdnPublicUrl));
48
+ return new URL(this.config.pathPrefix + '/' + key, this.cdnPublicUrl);
78
49
  }
79
50
 
80
51
  public async getItem(key: string): Promise<IFile | null> {
81
52
  const parameters = {
82
53
  Bucket: this.config.bucket,
83
- Key: this.getStorageKey(key),
54
+ Key: this.config.pathPrefix + '/' + key,
84
55
  };
85
56
 
86
57
  try {
@@ -136,12 +107,12 @@ export class DigitalOceanSpaces implements IIFilesStorageWithCdn {
136
107
  const uploadResult = await this.s3.send(
137
108
  new PutObjectCommand({
138
109
  Bucket: this.config.bucket,
139
- Key: this.getStorageKey(key),
110
+ Key: this.config.pathPrefix + '/' + key,
140
111
  ContentType: processedFile.type,
141
112
  ...putObjectRequestAdditional,
142
113
  Body: processedFile.data,
143
114
  // TODO: Public read access / just private to extending class
144
- ...(this.config.isPublicReadAclEnabled === false ? {} : { ACL: 'public-read' }),
115
+ ACL: 'public-read',
145
116
  }),
146
117
  );
147
118
 
@@ -149,22 +120,19 @@ export class DigitalOceanSpaces implements IIFilesStorageWithCdn {
149
120
  throw new Error(`Upload result does not contain ETag`);
150
121
  }
151
122
  }
123
+ }
152
124
 
153
- /**
154
- * Builds the final object key used in the S3 bucket.
155
- *
156
- * @private internal helper for DigitalOceanSpaces.
157
- */
158
- private getStorageKey(key: string): string {
159
- const normalizedKey = key.replace(/^\/+/g, '');
160
- const normalizedPathPrefix = this.config.pathPrefix.replace(/^\/+|\/+$/g, '');
161
-
162
- if (!normalizedPathPrefix) {
163
- return normalizedKey;
164
- }
165
-
166
- return `${normalizedPathPrefix}/${normalizedKey}`;
125
+ /**
126
+ * Normalizes endpoint values from legacy host-only configuration and full S3 URLs.
127
+ *
128
+ * @private helper of `DigitalOceanSpaces`
129
+ */
130
+ function normalizeS3Endpoint(endpoint: string): string {
131
+ if (/^https?:\/\//i.test(endpoint)) {
132
+ return endpoint;
167
133
  }
134
+
135
+ return `https://${endpoint}`;
168
136
  }
169
137
 
170
138
  // TODO: Implement Read-only mode
@@ -37,17 +37,20 @@ export class TrackedFilesStorage implements IIFilesStorageWithCdn {
37
37
 
38
38
  try {
39
39
  const { userId, purpose } = file;
40
- const storageUrl = this.getItemUrl(key).href;
40
+ const cdnUrl = this.getItemUrl(key).href;
41
+ const securityResult =
42
+ file.securityResult as AgentsServerDatabase['public']['Tables']['File']['Insert']['securityResult'];
41
43
 
42
44
  await this.supabase.from(await $getTableName('File')).insert({
43
45
  userId: userId || null,
44
46
  fileName: key,
45
47
  fileSize: file.fileSize ?? file.data.length,
46
48
  fileType: file.type,
47
- storageUrl,
49
+ storageUrl: cdnUrl,
48
50
  shortUrl: null,
49
51
  purpose: purpose || 'UNKNOWN',
50
52
  status: 'COMPLETED',
53
+ securityResult: securityResult || null,
51
54
  });
52
55
  } catch (error) {
53
56
  console.error('Failed to track upload:', error);
@@ -25,6 +25,11 @@ export type IFile = {
25
25
  * Note: This is optional, if not provided, the size of the buffer is used
26
26
  */
27
27
  fileSize?: number;
28
+
29
+ /**
30
+ * Optional file security result collected before the file is tracked.
31
+ */
32
+ securityResult?: Record<string, unknown>;
28
33
  };
29
34
 
30
35
  /**