@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.
- package/README.md +20 -0
- package/apps/agents-server/README.md +0 -1
- package/apps/agents-server/src/app/admin/metadata/MetadataClient.tsx +246 -15
- package/apps/agents-server/src/app/admin/update/UpdateClient.tsx +237 -4
- package/apps/agents-server/src/app/api/admin/update/log/route.ts +42 -0
- package/apps/agents-server/src/app/api/agents/export/route.ts +41 -0
- package/apps/agents-server/src/app/api/agents/import/route.ts +168 -0
- package/apps/agents-server/src/app/api/federated-agents/route.ts +1 -1
- package/apps/agents-server/src/app/api/internal/agent-runner-limits/route.ts +2 -1
- package/apps/agents-server/src/app/api/internal/user-chat-jobs/run/route.ts +2 -1
- package/apps/agents-server/src/app/api/metadata/export/route.ts +34 -0
- package/apps/agents-server/src/app/api/metadata/import/route.ts +46 -0
- package/apps/agents-server/src/app/layout.tsx +1 -1
- package/apps/agents-server/src/components/Homepage/AgentsList.tsx +31 -1
- package/apps/agents-server/src/components/Homepage/AgentsListHeader.tsx +60 -1
- package/apps/agents-server/src/components/Homepage/useAgentsListImportExportState.ts +523 -0
- package/apps/agents-server/src/components/Homepage/useAgentsListState.ts +16 -0
- package/apps/agents-server/src/database/metadataDefaults.ts +0 -21
- package/apps/agents-server/src/database/seedCoreAgents.ts +235 -0
- package/apps/agents-server/src/database/seedDefaultAgents.ts +32 -1
- package/apps/agents-server/src/search/createDefaultServerSearchProviders/createFederatedAgentsSearchProvider.ts +1 -1
- package/apps/agents-server/src/tools/$provideAgentCollectionForServer.ts +2 -9
- package/apps/agents-server/src/utils/agentsTransfer/createAgentsExportZipStream.ts +68 -0
- package/apps/agents-server/src/utils/agentsTransfer/importAgentsFromFiles.ts +852 -0
- package/apps/agents-server/src/utils/backup/createBooksBackupZipStream.ts +15 -2
- package/apps/agents-server/src/utils/defaultAgents/loadDefaultAgentBooks.ts +46 -4
- package/apps/agents-server/src/utils/getFederatedServers.ts +3 -74
- package/apps/agents-server/src/utils/getWellKnownAgentUrl.ts +10 -4
- package/apps/agents-server/src/utils/localChatRunner/processLocalUserChatJob.ts +1 -1
- package/apps/agents-server/src/utils/metadataConfigurationTransfer.ts +426 -0
- package/apps/agents-server/src/utils/serverManagement/createManagedServer/bootstrapManagedServer.ts +2 -5
- package/apps/agents-server/src/utils/serverManagement/createManagedServer/seedServerCoreAgents.ts +162 -0
- package/apps/agents-server/src/utils/session.ts +2 -1
- package/apps/agents-server/src/utils/vpsSelfUpdate.ts +110 -0
- package/esm/index.es.js +1 -1
- package/esm/index.es.js.map +1 -1
- package/esm/src/_packages/core.index.d.ts +0 -2
- package/esm/src/utils/isTimingSafeEqualString.d.ts +25 -0
- package/esm/src/version.d.ts +1 -1
- package/package.json +1 -1
- package/servers.ts +1 -16
- package/src/_packages/core.index.ts +0 -2
- package/src/commitments/_common/teamInternalAgentAccess.ts +2 -1
- package/src/other/templates/getTemplatesPipelineCollection.ts +789 -716
- package/src/utils/isTimingSafeEqualString.ts +43 -0
- package/src/version.ts +2 -2
- package/src/versions.txt +2 -0
- package/umd/index.umd.js +1 -1
- package/umd/index.umd.js.map +1 -1
- package/umd/src/_packages/core.index.d.ts +0 -2
- package/umd/src/utils/isTimingSafeEqualString.d.ts +25 -0
- package/umd/src/version.d.ts +1 -1
- package/apps/agents-server/src/utils/defaultFederatedAgents/DefaultFederatedAgentsSyncOptions.ts +0 -9
- package/apps/agents-server/src/utils/defaultFederatedAgents/ensureDefaultFederatedAgentExists.ts +0 -277
- package/apps/agents-server/src/utils/defaultFederatedAgents/fetchCoreOrganizationPayload.ts +0 -39
- package/apps/agents-server/src/utils/defaultFederatedAgents/fetchFederatedAgentBook.ts +0 -43
- package/apps/agents-server/src/utils/defaultFederatedAgents/fetchWithDefaultFederatedAgentTimeout.ts +0 -28
- package/apps/agents-server/src/utils/defaultFederatedAgents/getDefaultFederatedAgentSyncPool.ts +0 -38
- package/apps/agents-server/src/utils/defaultFederatedAgents/loadActiveLocalAgentIdsByNormalizedName.ts +0 -41
- package/apps/agents-server/src/utils/defaultFederatedAgents/loadDefaultFederatedAgentSyncMetadata.ts +0 -76
- package/apps/agents-server/src/utils/defaultFederatedAgents/quoteIdentifier.ts +0 -11
- package/apps/agents-server/src/utils/defaultFederatedAgents/scheduleDefaultFederatedAgentsSync.ts +0 -88
- package/apps/agents-server/src/utils/defaultFederatedAgents/selectDefaultFederatedAgentsFromOrganizationPayload.ts +0 -181
- package/apps/agents-server/src/utils/defaultFederatedAgents/synchronizeDefaultFederatedAgents.ts +0 -77
|
@@ -129,7 +129,9 @@ export async function appendBooksBackupEntriesToZip(zip: JSZip, archiveRootPath:
|
|
|
129
129
|
const resolveFolderPath = createFolderPathResolver(folders, folderSegmentById);
|
|
130
130
|
|
|
131
131
|
// Keep the root directory explicit even when there are no agents.
|
|
132
|
-
|
|
132
|
+
if (archiveRootPath) {
|
|
133
|
+
zip.folder(archiveRootPath);
|
|
134
|
+
}
|
|
133
135
|
|
|
134
136
|
const relativeFilePaths = new Set<string>();
|
|
135
137
|
const folderPathByAgentId = new Map<number, string>();
|
|
@@ -152,10 +154,21 @@ export async function appendBooksBackupEntriesToZip(zip: JSZip, archiveRootPath:
|
|
|
152
154
|
const folderSegments = resolveFolderPath(agent.folderId ?? null);
|
|
153
155
|
const initialBookFilename = createInitialBookFilename(agent);
|
|
154
156
|
const relativePath = createUniqueBookRelativePath(relativeFilePaths, folderSegments, initialBookFilename, agent.id);
|
|
155
|
-
zip.file(
|
|
157
|
+
zip.file(createArchivePath(archiveRootPath, relativePath), agent.agentSource || '');
|
|
156
158
|
}
|
|
157
159
|
}
|
|
158
160
|
|
|
161
|
+
/**
|
|
162
|
+
* Joins an optional archive root with one relative path.
|
|
163
|
+
*
|
|
164
|
+
* @param archiveRootPath - Optional ZIP folder root.
|
|
165
|
+
* @param relativePath - Book path relative to the selected root.
|
|
166
|
+
* @returns ZIP-safe path without a leading slash.
|
|
167
|
+
*/
|
|
168
|
+
function createArchivePath(archiveRootPath: string, relativePath: string): string {
|
|
169
|
+
return archiveRootPath ? `${archiveRootPath}/${relativePath}` : relativePath;
|
|
170
|
+
}
|
|
171
|
+
|
|
159
172
|
/**
|
|
160
173
|
* Builds a download-ready stream with all books organized by the Agents Server folder tree.
|
|
161
174
|
*
|
|
@@ -4,6 +4,13 @@ import type { string_book } from '../../../../../src/book-2.0/agent-source/strin
|
|
|
4
4
|
import { DatabaseError } from '../../../../../src/errors/DatabaseError';
|
|
5
5
|
import { spaceTrim } from 'spacetrim';
|
|
6
6
|
|
|
7
|
+
/**
|
|
8
|
+
* Folder name on disk that contains the bundled `.core` agent books.
|
|
9
|
+
*
|
|
10
|
+
* @private utility of Agents Server default-agent seeding
|
|
11
|
+
*/
|
|
12
|
+
export const CORE_AGENT_DIRECTORY_NAME = '.core';
|
|
13
|
+
|
|
7
14
|
/**
|
|
8
15
|
* Options for loading bundled default agent books.
|
|
9
16
|
*
|
|
@@ -42,14 +49,49 @@ export async function loadDefaultAgentBooks(
|
|
|
42
49
|
options: LoadDefaultAgentBooksOptions = {},
|
|
43
50
|
): Promise<ReadonlyArray<string_book>> {
|
|
44
51
|
const defaultAgentDirectory = await resolveDefaultAgentDirectory(options);
|
|
45
|
-
|
|
46
|
-
|
|
52
|
+
return loadAgentBooksFromDirectory(defaultAgentDirectory);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Loads all bundled `.core` agent books in deterministic filename order.
|
|
57
|
+
*
|
|
58
|
+
* @param options - Optional explicit default-agent directory.
|
|
59
|
+
* @returns Sorted core agent sources, or an empty list when the `.core` subfolder is absent.
|
|
60
|
+
*
|
|
61
|
+
* @private utility of Agents Server default-agent seeding
|
|
62
|
+
*/
|
|
63
|
+
export async function loadCoreAgentBooks(
|
|
64
|
+
options: LoadDefaultAgentBooksOptions = {},
|
|
65
|
+
): Promise<ReadonlyArray<string_book>> {
|
|
66
|
+
const defaultAgentDirectory = await resolveDefaultAgentDirectory(options);
|
|
67
|
+
const coreAgentDirectory = resolve(defaultAgentDirectory, CORE_AGENT_DIRECTORY_NAME);
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
await access(coreAgentDirectory);
|
|
71
|
+
} catch {
|
|
72
|
+
return [];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return loadAgentBooksFromDirectory(coreAgentDirectory);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Reads `.book` files from one directory in deterministic filename order.
|
|
80
|
+
*
|
|
81
|
+
* @param agentDirectory - Absolute path to the directory containing `.book` files.
|
|
82
|
+
* @returns Sorted agent sources.
|
|
83
|
+
*
|
|
84
|
+
* @private utility of Agents Server default-agent seeding
|
|
85
|
+
*/
|
|
86
|
+
async function loadAgentBooksFromDirectory(agentDirectory: string): Promise<ReadonlyArray<string_book>> {
|
|
87
|
+
const directoryEntries = await readdir(agentDirectory);
|
|
88
|
+
const agentFilenames = directoryEntries
|
|
47
89
|
.filter((entry) => entry.toLowerCase().endsWith('.book'))
|
|
48
90
|
.sort((leftFilename, rightFilename) => leftFilename.localeCompare(rightFilename));
|
|
49
91
|
|
|
50
92
|
return Promise.all(
|
|
51
|
-
|
|
52
|
-
const filePath = resolve(
|
|
93
|
+
agentFilenames.map(async (filename) => {
|
|
94
|
+
const filePath = resolve(agentDirectory, filename);
|
|
53
95
|
return (await readFile(filePath, 'utf-8')) as string_book;
|
|
54
96
|
}),
|
|
55
97
|
);
|
|
@@ -1,27 +1,6 @@
|
|
|
1
1
|
import { string_promptbook_server_url } from '@promptbook-local/types';
|
|
2
2
|
import { getMetadataMap } from '../database/getMetadata';
|
|
3
3
|
|
|
4
|
-
/**
|
|
5
|
-
* Options that tweak the behavior of `getFederatedServers`.
|
|
6
|
-
*
|
|
7
|
-
* @public exported from `apps/agents-server`
|
|
8
|
-
*/
|
|
9
|
-
export type GetFederatedServersOptions = {
|
|
10
|
-
/**
|
|
11
|
-
* When set, hide the core server from the resulting list if the metadata flag requests it.
|
|
12
|
-
*
|
|
13
|
-
* @private Internal toggle for UI surfaces that should hide the core server.
|
|
14
|
-
*/
|
|
15
|
-
readonly excludeHiddenCoreServer?: boolean;
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Metadata key storing the core server URL.
|
|
20
|
-
*
|
|
21
|
-
* @private
|
|
22
|
-
*/
|
|
23
|
-
const CORE_SERVER_METADATA_KEY = 'CORE_SERVER';
|
|
24
|
-
|
|
25
4
|
/**
|
|
26
5
|
* Metadata key storing the comma-separated federated servers list.
|
|
27
6
|
*
|
|
@@ -29,67 +8,17 @@ const CORE_SERVER_METADATA_KEY = 'CORE_SERVER';
|
|
|
29
8
|
*/
|
|
30
9
|
const FEDERATED_SERVERS_METADATA_KEY = 'FEDERATED_SERVERS';
|
|
31
10
|
|
|
32
|
-
/**
|
|
33
|
-
* Metadata key indicating whether the core server should be hidden from UI lists.
|
|
34
|
-
*
|
|
35
|
-
* @private
|
|
36
|
-
*/
|
|
37
|
-
const IS_CORE_SERVER_HIDDEN_METADATA_KEY = 'IS_CORE_SERVER_HIDDEN';
|
|
38
|
-
|
|
39
11
|
/**
|
|
40
12
|
* Reads FEDERATED_SERVERS metadata and returns a normalized list of server URLs.
|
|
41
13
|
*
|
|
42
14
|
* @public exported from `apps/agents-server`
|
|
43
15
|
*/
|
|
44
|
-
export async function getFederatedServers(
|
|
45
|
-
const metadata = await getMetadataMap([
|
|
46
|
-
FEDERATED_SERVERS_METADATA_KEY,
|
|
47
|
-
CORE_SERVER_METADATA_KEY,
|
|
48
|
-
IS_CORE_SERVER_HIDDEN_METADATA_KEY,
|
|
49
|
-
]);
|
|
16
|
+
export async function getFederatedServers(): Promise<string_promptbook_server_url[]> {
|
|
17
|
+
const metadata = await getMetadataMap([FEDERATED_SERVERS_METADATA_KEY]);
|
|
50
18
|
const federatedServersString = metadata[FEDERATED_SERVERS_METADATA_KEY] || '';
|
|
51
|
-
const rawCoreServer = metadata[CORE_SERVER_METADATA_KEY] || '';
|
|
52
|
-
const coreServer = rawCoreServer.trim() as string_promptbook_server_url;
|
|
53
19
|
|
|
54
|
-
|
|
55
|
-
throw new Error('Missing CORE_SERVER metadata value');
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
const federatedServers = federatedServersString
|
|
20
|
+
return federatedServersString
|
|
59
21
|
.split(',')
|
|
60
22
|
.map((server) => server.trim())
|
|
61
23
|
.filter((server): server is string_promptbook_server_url => server !== '');
|
|
62
|
-
|
|
63
|
-
const allServers = [coreServer, ...federatedServers];
|
|
64
|
-
if (options?.excludeHiddenCoreServer && isCoreServerHidden(metadata)) {
|
|
65
|
-
const normalizedCoreServer = normalizeServerUrl(coreServer);
|
|
66
|
-
return allServers.filter((server) => normalizeServerUrl(server) !== normalizedCoreServer);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
return allServers;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* Determines if the metadata flag explicitly hides the core server from UI lists.
|
|
74
|
-
*
|
|
75
|
-
* @param metadata - Metadata map containing IS_CORE_SERVER_HIDDEN value.
|
|
76
|
-
* @returns Whether the core server should be hidden.
|
|
77
|
-
*
|
|
78
|
-
* @private Internal helper for filtering UI-friendly lists.
|
|
79
|
-
*/
|
|
80
|
-
function isCoreServerHidden(metadata: Record<string, string | null>): boolean {
|
|
81
|
-
const rawValue = metadata[IS_CORE_SERVER_HIDDEN_METADATA_KEY] || '';
|
|
82
|
-
return rawValue.trim().toLowerCase() === 'true';
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* Normalizes a server URL by trimming trailing slashes for comparison purposes.
|
|
87
|
-
*
|
|
88
|
-
* @param value - Raw server URL.
|
|
89
|
-
* @returns Normalized server URL.
|
|
90
|
-
*
|
|
91
|
-
* @private Internal helper used when filtering hidden servers.
|
|
92
|
-
*/
|
|
93
|
-
function normalizeServerUrl(value: string): string {
|
|
94
|
-
return value.replace(/\/+$/, '');
|
|
95
24
|
}
|
|
@@ -1,14 +1,20 @@
|
|
|
1
1
|
import { CORE_AGENTS_SERVER_WELL_KNOWN_AGENT_NAMES } from '../../../../servers';
|
|
2
2
|
import { string_agent_url } from '../../../../src/_packages/types.index'; // <- [🚾]
|
|
3
|
-
import {
|
|
3
|
+
import { resolveCurrentOrInternalServerOrigin } from './resolveCurrentOrInternalServerOrigin';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
*
|
|
6
|
+
* Resolves the URL of one well-known agent (Adam, Teacher, ...) hosted locally in the `.core` folder of the current Agents Server.
|
|
7
|
+
*
|
|
8
|
+
* @param agentName - Logical well-known agent identifier.
|
|
9
|
+
* @returns Absolute URL of the well-known agent on the current server.
|
|
10
|
+
*
|
|
11
|
+
* @private utility of Agents Server
|
|
7
12
|
*/
|
|
8
13
|
export async function getWellKnownAgentUrl(
|
|
9
14
|
agentName: keyof typeof CORE_AGENTS_SERVER_WELL_KNOWN_AGENT_NAMES,
|
|
10
15
|
): Promise<string_agent_url> {
|
|
11
|
-
const
|
|
16
|
+
const localServerOrigin = await resolveCurrentOrInternalServerOrigin();
|
|
17
|
+
const normalizedServerOrigin = localServerOrigin.replace(/\/+$/g, '');
|
|
12
18
|
|
|
13
|
-
return `${
|
|
19
|
+
return `${normalizedServerOrigin}/agents/${CORE_AGENTS_SERVER_WELL_KNOWN_AGENT_NAMES[agentName]}` as string_agent_url;
|
|
14
20
|
}
|
|
@@ -223,7 +223,7 @@ async function synchronizeLocalUserChatJob(
|
|
|
223
223
|
}
|
|
224
224
|
|
|
225
225
|
const failedFileContent = await readOptionalTextFile(join(agentDirectoryPath, metadata.failedPath));
|
|
226
|
-
if (failedFileContent !== null) {
|
|
226
|
+
if (failedFileContent !== null && job.status !== 'FAILED') {
|
|
227
227
|
const failureReason = parseLocalChatFailedMessageBook({
|
|
228
228
|
bookContent: failedFileContent,
|
|
229
229
|
expectedMessagesBeforeAnswer: metadata.expectedMessagesBeforeAnswer,
|
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
import { spaceTrim } from 'spacetrim';
|
|
2
|
+
import { DatabaseError } from '../../../../src/errors/DatabaseError';
|
|
3
|
+
import { ParseError } from '../../../../src/errors/ParseError';
|
|
4
|
+
import { normalizeToKebabCase } from '../../../../src/utils/normalization/normalize-to-kebab-case';
|
|
5
|
+
import { removeEmojis } from '../../../../src/utils/normalization/removeEmojis';
|
|
6
|
+
import { PROMPTBOOK_ENGINE_VERSION, type string_promptbook_version } from '../../../../src/version';
|
|
7
|
+
import { $getTableName } from '../database/$getTableName';
|
|
8
|
+
import { $provideSupabaseForServer } from '../database/$provideSupabaseForServer';
|
|
9
|
+
import { invalidateMetadataCache } from '../database/getMetadata';
|
|
10
|
+
import { getMetadataDefinition, metadataDefaults, validateMetadataValue } from '../database/metadataDefaults';
|
|
11
|
+
import type { AgentsServerDatabase } from '../database/schema';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Metadata key containing the human-readable server name.
|
|
15
|
+
*/
|
|
16
|
+
const SERVER_NAME_METADATA_KEY = 'SERVER_NAME';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Fallback filename stem used when the server name cannot produce a safe name.
|
|
20
|
+
*/
|
|
21
|
+
const DEFAULT_METADATA_EXPORT_FILENAME_STEM = 'promptbook-agents-server';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Filename suffix required for standalone metadata exports.
|
|
25
|
+
*/
|
|
26
|
+
const METADATA_EXPORT_FILENAME_SUFFIX = '.metadata.json';
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Minimal metadata row shape needed by import/export helpers.
|
|
30
|
+
*/
|
|
31
|
+
type MetadataConfigurationRow = Pick<
|
|
32
|
+
AgentsServerDatabase['public']['Tables']['Metadata']['Row'],
|
|
33
|
+
'key' | 'value' | 'note'
|
|
34
|
+
>;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Metadata row persisted during import.
|
|
38
|
+
*/
|
|
39
|
+
type MetadataConfigurationImportRow = {
|
|
40
|
+
/**
|
|
41
|
+
* Metadata key.
|
|
42
|
+
*/
|
|
43
|
+
readonly key: string;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Metadata value to persist.
|
|
47
|
+
*/
|
|
48
|
+
readonly value: string;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Metadata note to persist.
|
|
52
|
+
*/
|
|
53
|
+
readonly note: string | null;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* One entry inside the standalone metadata export JSON.
|
|
58
|
+
*/
|
|
59
|
+
export type MetadataConfigurationExportEntry = {
|
|
60
|
+
/**
|
|
61
|
+
* Metadata key.
|
|
62
|
+
*/
|
|
63
|
+
readonly key: string;
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Metadata value, omitted when it matches the built-in default.
|
|
67
|
+
*/
|
|
68
|
+
readonly value?: string;
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Metadata note, omitted when it matches the built-in default note.
|
|
72
|
+
*/
|
|
73
|
+
readonly note?: string | null;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Standalone metadata export JSON payload.
|
|
78
|
+
*/
|
|
79
|
+
export type MetadataConfigurationExportPayload = {
|
|
80
|
+
/**
|
|
81
|
+
* Promptbook engine version that generated the export.
|
|
82
|
+
*/
|
|
83
|
+
readonly promptbookVersion: string_promptbook_version;
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Metadata entries with default fields omitted.
|
|
87
|
+
*/
|
|
88
|
+
readonly metadata: ReadonlyArray<MetadataConfigurationExportEntry>;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Prepared metadata import plan before it touches the database.
|
|
93
|
+
*/
|
|
94
|
+
export type MetadataConfigurationImportPlan = {
|
|
95
|
+
/**
|
|
96
|
+
* Rows that should be inserted or updated.
|
|
97
|
+
*/
|
|
98
|
+
readonly rowsToUpsert: ReadonlyArray<MetadataConfigurationImportRow>;
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Default-backed keys absent from the import and therefore reset by deleting persisted rows.
|
|
102
|
+
*/
|
|
103
|
+
readonly defaultKeysToReset: ReadonlyArray<string>;
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Summary returned after importing metadata.
|
|
108
|
+
*/
|
|
109
|
+
export type MetadataConfigurationImportSummary = {
|
|
110
|
+
/**
|
|
111
|
+
* Number of metadata rows inserted or updated from the import file.
|
|
112
|
+
*/
|
|
113
|
+
readonly importedCount: number;
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Number of built-in metadata keys reset to default.
|
|
117
|
+
*/
|
|
118
|
+
readonly resetCount: number;
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Full metadata export result used by the API route.
|
|
123
|
+
*/
|
|
124
|
+
export type MetadataConfigurationExportResult = {
|
|
125
|
+
/**
|
|
126
|
+
* Browser download filename.
|
|
127
|
+
*/
|
|
128
|
+
readonly filename: string;
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* JSON payload to send to the browser.
|
|
132
|
+
*/
|
|
133
|
+
readonly payload: MetadataConfigurationExportPayload;
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Creates a standalone metadata export from the current server database.
|
|
138
|
+
*
|
|
139
|
+
* @returns Export filename and payload.
|
|
140
|
+
*/
|
|
141
|
+
export async function createMetadataConfigurationExport(): Promise<MetadataConfigurationExportResult> {
|
|
142
|
+
const metadataRows = await loadMetadataConfigurationRows();
|
|
143
|
+
const serverName = resolveServerNameFromMetadataRows(metadataRows);
|
|
144
|
+
|
|
145
|
+
return {
|
|
146
|
+
filename: createMetadataConfigurationExportFilename(serverName),
|
|
147
|
+
payload: createMetadataConfigurationExportPayload(metadataRows),
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Imports standalone metadata JSON into the current server database.
|
|
153
|
+
*
|
|
154
|
+
* @param payload - Parsed JSON import payload.
|
|
155
|
+
* @returns Import summary for the UI.
|
|
156
|
+
*/
|
|
157
|
+
export async function importMetadataConfigurationPayload(
|
|
158
|
+
payload: unknown,
|
|
159
|
+
): Promise<MetadataConfigurationImportSummary> {
|
|
160
|
+
const importPlan = createMetadataConfigurationImportPlan(payload);
|
|
161
|
+
const supabase = $provideSupabaseForServer();
|
|
162
|
+
const tableName = await $getTableName('Metadata');
|
|
163
|
+
const nowIso = new Date().toISOString();
|
|
164
|
+
|
|
165
|
+
if (importPlan.defaultKeysToReset.length > 0) {
|
|
166
|
+
const { error } = await supabase.from(tableName).delete().in('key', [...importPlan.defaultKeysToReset]);
|
|
167
|
+
|
|
168
|
+
if (error) {
|
|
169
|
+
throw new DatabaseError(
|
|
170
|
+
spaceTrim(`
|
|
171
|
+
Failed to reset metadata keys to their defaults.
|
|
172
|
+
|
|
173
|
+
${error.message}
|
|
174
|
+
`),
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (importPlan.rowsToUpsert.length > 0) {
|
|
180
|
+
const { error } = await supabase.from(tableName).upsert(
|
|
181
|
+
importPlan.rowsToUpsert.map((row) => ({
|
|
182
|
+
...row,
|
|
183
|
+
updatedAt: nowIso,
|
|
184
|
+
})),
|
|
185
|
+
{
|
|
186
|
+
onConflict: 'key',
|
|
187
|
+
},
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
if (error) {
|
|
191
|
+
throw new DatabaseError(
|
|
192
|
+
spaceTrim(`
|
|
193
|
+
Failed to import metadata configuration.
|
|
194
|
+
|
|
195
|
+
${error.message}
|
|
196
|
+
`),
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
invalidateMetadataCache();
|
|
202
|
+
|
|
203
|
+
return {
|
|
204
|
+
importedCount: importPlan.rowsToUpsert.length,
|
|
205
|
+
resetCount: importPlan.defaultKeysToReset.length,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Creates the standalone export payload from metadata rows.
|
|
211
|
+
*
|
|
212
|
+
* @param metadataRows - Persisted metadata rows.
|
|
213
|
+
* @param promptbookVersion - Promptbook version to include in the export.
|
|
214
|
+
* @returns JSON payload with default fields omitted.
|
|
215
|
+
*/
|
|
216
|
+
export function createMetadataConfigurationExportPayload(
|
|
217
|
+
metadataRows: ReadonlyArray<MetadataConfigurationRow>,
|
|
218
|
+
promptbookVersion: string_promptbook_version = PROMPTBOOK_ENGINE_VERSION,
|
|
219
|
+
): MetadataConfigurationExportPayload {
|
|
220
|
+
const metadata = metadataRows
|
|
221
|
+
.flatMap((metadataRow) => {
|
|
222
|
+
const exportEntry = createMetadataConfigurationExportEntry(metadataRow);
|
|
223
|
+
return exportEntry ? [exportEntry] : [];
|
|
224
|
+
})
|
|
225
|
+
.sort((leftEntry, rightEntry) => leftEntry.key.localeCompare(rightEntry.key));
|
|
226
|
+
|
|
227
|
+
return {
|
|
228
|
+
promptbookVersion,
|
|
229
|
+
metadata,
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Creates a browser-safe metadata export filename from the server name.
|
|
235
|
+
*
|
|
236
|
+
* @param serverName - Human-readable server name.
|
|
237
|
+
* @returns Kebab-case metadata export filename.
|
|
238
|
+
*/
|
|
239
|
+
export function createMetadataConfigurationExportFilename(serverName: string | null | undefined): string {
|
|
240
|
+
const normalizedServerName = normalizeToKebabCase(removeEmojis(serverName || ''));
|
|
241
|
+
const filenameStem = normalizedServerName || DEFAULT_METADATA_EXPORT_FILENAME_STEM;
|
|
242
|
+
|
|
243
|
+
return `${filenameStem}${METADATA_EXPORT_FILENAME_SUFFIX}`;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Validates and normalizes a standalone metadata import payload.
|
|
248
|
+
*
|
|
249
|
+
* @param payload - Parsed JSON import payload.
|
|
250
|
+
* @returns Import plan ready for persistence.
|
|
251
|
+
*/
|
|
252
|
+
export function createMetadataConfigurationImportPlan(payload: unknown): MetadataConfigurationImportPlan {
|
|
253
|
+
if (!payload || typeof payload !== 'object') {
|
|
254
|
+
throw new ParseError('Metadata import must be a JSON object.');
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const payloadRecord = payload as Record<string, unknown>;
|
|
258
|
+
|
|
259
|
+
if (typeof payloadRecord.promptbookVersion !== 'string' || payloadRecord.promptbookVersion.trim() === '') {
|
|
260
|
+
throw new ParseError('Metadata import is missing `promptbookVersion`.');
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (!Array.isArray(payloadRecord.metadata)) {
|
|
264
|
+
throw new ParseError('Metadata import must contain a `metadata` array.');
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const importedMetadataKeys = new Set<string>();
|
|
268
|
+
const rowsToUpsert = payloadRecord.metadata.map((rawMetadataEntry, index) =>
|
|
269
|
+
normalizeMetadataConfigurationImportEntry(rawMetadataEntry, index, importedMetadataKeys),
|
|
270
|
+
);
|
|
271
|
+
const defaultKeysToReset = metadataDefaults
|
|
272
|
+
.map((metadataDefinition) => metadataDefinition.key)
|
|
273
|
+
.filter((metadataKey) => !importedMetadataKeys.has(metadataKey))
|
|
274
|
+
.sort((leftKey, rightKey) => leftKey.localeCompare(rightKey));
|
|
275
|
+
|
|
276
|
+
return {
|
|
277
|
+
rowsToUpsert: rowsToUpsert.sort((leftRow, rightRow) => leftRow.key.localeCompare(rightRow.key)),
|
|
278
|
+
defaultKeysToReset,
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Loads all persisted metadata rows from the current server database.
|
|
284
|
+
*
|
|
285
|
+
* @returns Metadata rows sorted by key.
|
|
286
|
+
*/
|
|
287
|
+
async function loadMetadataConfigurationRows(): Promise<Array<MetadataConfigurationRow>> {
|
|
288
|
+
const supabase = $provideSupabaseForServer();
|
|
289
|
+
const tableName = await $getTableName('Metadata');
|
|
290
|
+
const { data, error } = await supabase.from(tableName).select('key, value, note').order('key');
|
|
291
|
+
|
|
292
|
+
if (error) {
|
|
293
|
+
throw new DatabaseError(
|
|
294
|
+
spaceTrim(`
|
|
295
|
+
Failed to load metadata configuration.
|
|
296
|
+
|
|
297
|
+
${error.message}
|
|
298
|
+
`),
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
return ((data ?? []) as Array<MetadataConfigurationRow>).sort((leftRow, rightRow) =>
|
|
303
|
+
leftRow.key.localeCompare(rightRow.key),
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Creates one export entry while omitting fields that match defaults.
|
|
309
|
+
*
|
|
310
|
+
* @param metadataRow - Persisted metadata row.
|
|
311
|
+
* @returns Export entry or `null` when both value and note are default.
|
|
312
|
+
*/
|
|
313
|
+
function createMetadataConfigurationExportEntry(
|
|
314
|
+
metadataRow: MetadataConfigurationRow,
|
|
315
|
+
): MetadataConfigurationExportEntry | null {
|
|
316
|
+
const metadataDefinition = getMetadataDefinition(metadataRow.key);
|
|
317
|
+
const defaultNote = metadataDefinition?.note ?? null;
|
|
318
|
+
const isValueDefault = Boolean(metadataDefinition) && metadataRow.value === metadataDefinition!.value;
|
|
319
|
+
const isNoteDefault = metadataRow.note === defaultNote;
|
|
320
|
+
const exportEntry: MetadataConfigurationExportEntry = {
|
|
321
|
+
key: metadataRow.key,
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
if (!isValueDefault) {
|
|
325
|
+
(exportEntry as { value: string }).value = metadataRow.value;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
if (!isNoteDefault) {
|
|
329
|
+
(exportEntry as { note: string | null }).note = metadataRow.note;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const isValueExported = Object.prototype.hasOwnProperty.call(exportEntry, 'value');
|
|
333
|
+
const isNoteExported = Object.prototype.hasOwnProperty.call(exportEntry, 'note');
|
|
334
|
+
|
|
335
|
+
if (!isValueExported && !isNoteExported) {
|
|
336
|
+
return null;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
return exportEntry;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Normalizes one metadata import entry.
|
|
344
|
+
*
|
|
345
|
+
* @param rawMetadataEntry - Untrusted entry from the import JSON.
|
|
346
|
+
* @param index - Zero-based entry index for error messages.
|
|
347
|
+
* @param importedMetadataKeys - Mutable set used to reject duplicates and compute resets.
|
|
348
|
+
* @returns Metadata row ready for persistence.
|
|
349
|
+
*/
|
|
350
|
+
function normalizeMetadataConfigurationImportEntry(
|
|
351
|
+
rawMetadataEntry: unknown,
|
|
352
|
+
index: number,
|
|
353
|
+
importedMetadataKeys: Set<string>,
|
|
354
|
+
): MetadataConfigurationImportRow {
|
|
355
|
+
if (!rawMetadataEntry || typeof rawMetadataEntry !== 'object') {
|
|
356
|
+
throw new ParseError(`Metadata entry #${index + 1} must be an object.`);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const rawMetadataEntryRecord = rawMetadataEntry as Record<string, unknown>;
|
|
360
|
+
const rawKey = rawMetadataEntryRecord.key;
|
|
361
|
+
if (typeof rawKey !== 'string' || rawKey.trim() === '') {
|
|
362
|
+
throw new ParseError(`Metadata entry #${index + 1} is missing a non-empty \`key\`.`);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
const key = rawKey.trim();
|
|
366
|
+
const isDuplicateKey = importedMetadataKeys.has(key);
|
|
367
|
+
if (isDuplicateKey) {
|
|
368
|
+
throw new ParseError(`Metadata import contains duplicate key \`${key}\`.`);
|
|
369
|
+
}
|
|
370
|
+
importedMetadataKeys.add(key);
|
|
371
|
+
|
|
372
|
+
const metadataDefinition = getMetadataDefinition(key);
|
|
373
|
+
const isValueProvided = Object.prototype.hasOwnProperty.call(rawMetadataEntryRecord, 'value');
|
|
374
|
+
const isNoteProvided = Object.prototype.hasOwnProperty.call(rawMetadataEntryRecord, 'note');
|
|
375
|
+
|
|
376
|
+
if (isValueProvided && typeof rawMetadataEntryRecord.value !== 'string') {
|
|
377
|
+
throw new ParseError(`Metadata entry \`${key}\` has invalid \`value\`; expected a string.`);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
if (
|
|
381
|
+
isNoteProvided &&
|
|
382
|
+
typeof rawMetadataEntryRecord.note !== 'string' &&
|
|
383
|
+
rawMetadataEntryRecord.note !== null
|
|
384
|
+
) {
|
|
385
|
+
throw new ParseError(`Metadata entry \`${key}\` has invalid \`note\`; expected a string or null.`);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
if (!isValueProvided && !metadataDefinition) {
|
|
389
|
+
throw new ParseError(
|
|
390
|
+
spaceTrim(`
|
|
391
|
+
Metadata entry \`${key}\` is missing \`value\`.
|
|
392
|
+
|
|
393
|
+
Only built-in metadata keys can omit default values.
|
|
394
|
+
`),
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
const value = isValueProvided ? (rawMetadataEntryRecord.value as string) : metadataDefinition!.value;
|
|
399
|
+
const note = isNoteProvided
|
|
400
|
+
? (rawMetadataEntryRecord.note as string | null)
|
|
401
|
+
: metadataDefinition?.note ?? null;
|
|
402
|
+
const validationError = validateMetadataValue(key, value);
|
|
403
|
+
|
|
404
|
+
if (validationError) {
|
|
405
|
+
throw new ParseError(validationError);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
return {
|
|
409
|
+
key,
|
|
410
|
+
value,
|
|
411
|
+
note,
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Resolves server name for the export filename from persisted metadata or defaults.
|
|
417
|
+
*
|
|
418
|
+
* @param metadataRows - Persisted metadata rows.
|
|
419
|
+
* @returns Server name candidate.
|
|
420
|
+
*/
|
|
421
|
+
function resolveServerNameFromMetadataRows(metadataRows: ReadonlyArray<MetadataConfigurationRow>): string {
|
|
422
|
+
const metadataRow = metadataRows.find((candidateRow) => candidateRow.key === SERVER_NAME_METADATA_KEY);
|
|
423
|
+
const defaultServerName = getMetadataDefinition(SERVER_NAME_METADATA_KEY)?.value ?? DEFAULT_METADATA_EXPORT_FILENAME_STEM;
|
|
424
|
+
|
|
425
|
+
return metadataRow?.value.trim() || defaultServerName;
|
|
426
|
+
}
|
package/apps/agents-server/src/utils/serverManagement/createManagedServer/bootstrapManagedServer.ts
CHANGED
|
@@ -5,7 +5,6 @@ import {
|
|
|
5
5
|
acquireMigrationExecutionLock,
|
|
6
6
|
releaseMigrationExecutionLock,
|
|
7
7
|
} from '../../../database/acquireMigrationExecutionLock';
|
|
8
|
-
import { scheduleDefaultFederatedAgentsSync } from '../../defaultFederatedAgents/scheduleDefaultFederatedAgentsSync';
|
|
9
8
|
import { createServerPublicUrl, invalidateRegisteredServersCache } from '../../serverRegistry';
|
|
10
9
|
import type { CreateServerResult } from '../createManagedServer';
|
|
11
10
|
import { resolveManagedServerConnectionString } from '../resolveManagedServerConnectionString';
|
|
@@ -14,6 +13,7 @@ import { createFailedServerResult } from './createFailedServerResult';
|
|
|
14
13
|
import { createSqlRecorder } from './createSqlRecorder';
|
|
15
14
|
import { insertManagedServerRegistryRow } from './insertManagedServerRegistryRow';
|
|
16
15
|
import type { NormalizedCreateServerInput } from './normalizeCreateServerInput';
|
|
16
|
+
import { seedServerCoreAgents } from './seedServerCoreAgents';
|
|
17
17
|
import { seedServerDefaultAgents } from './seedServerDefaultAgents';
|
|
18
18
|
import { seedServerMetadata } from './seedServerMetadata';
|
|
19
19
|
import { seedServerUsers } from './seedServerUsers';
|
|
@@ -50,15 +50,12 @@ export async function bootstrapManagedServer(input: NormalizedCreateServerInput)
|
|
|
50
50
|
if (input.isDefaultAgentsInstalled) {
|
|
51
51
|
await seedServerDefaultAgents(client, input, sqlRecorder);
|
|
52
52
|
}
|
|
53
|
+
await seedServerCoreAgents(client, input, sqlRecorder);
|
|
53
54
|
|
|
54
55
|
await client.query('COMMIT');
|
|
55
56
|
sqlRecorder.addStatement('COMMIT');
|
|
56
57
|
|
|
57
58
|
invalidateRegisteredServersCache();
|
|
58
|
-
scheduleDefaultFederatedAgentsSync({
|
|
59
|
-
tablePrefix: server.tablePrefix,
|
|
60
|
-
localServerUrl: createServerPublicUrl(server.domain).href,
|
|
61
|
-
});
|
|
62
59
|
|
|
63
60
|
return {
|
|
64
61
|
ok: true,
|