@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
@@ -22,7 +22,7 @@ import { heartbeatUserChatJob } from './heartbeatUserChatJob';
22
22
  import { persistUserChatJobTerminalState } from './persistUserChatJobTerminalState';
23
23
  import { persistUserChatJobProgressCard } from './persistUserChatJobProgressCard';
24
24
  import type { UserChatJobRecord } from './UserChatJobRecord';
25
- import type { UserChatProgressPhase } from './userChatProgressCard';
25
+ import type { UserChatProgressContext, UserChatProgressPhase } from './userChatProgressCard';
26
26
  import { createReplyAwareUserChatPromptContent, createReplyAwareUserChatPromptMessage } from './userChatReplies';
27
27
  import { resolvePromptThreadBeforeUserMessage } from './userChatMessageLifecycle';
28
28
  import { isUserChatNotFoundScopeError } from './UserChatScopeError';
@@ -132,14 +132,15 @@ export async function runUserChatJob(job: UserChatJobRecord): Promise<RunUserCha
132
132
  */
133
133
  function createRunUserChatJobProgressReporter(
134
134
  job: Pick<UserChatJobRecord, 'userId' | 'agentPermanentId' | 'chatId' | 'assistantMessageId' | 'id'>,
135
- ): (phase: UserChatProgressPhase) => Promise<void> {
136
- return async (phase) => {
135
+ ): (phase: UserChatProgressPhase, context?: UserChatProgressContext) => Promise<void> {
136
+ return async (phase, context) => {
137
137
  await persistUserChatJobProgressCard({
138
138
  userId: job.userId,
139
139
  agentPermanentId: job.agentPermanentId,
140
140
  chatId: job.chatId,
141
141
  assistantMessageId: job.assistantMessageId,
142
142
  phase,
143
+ context,
143
144
  }).catch((error) => {
144
145
  console.warn('[user-chat-job] progress_update_failed', {
145
146
  chatId: job.chatId,
@@ -1,4 +1,5 @@
1
1
  import type { ChatMessage } from '@promptbook-local/types';
2
+ import { spaceTrim } from '../../../../../src/_packages/utils.index';
2
3
 
3
4
  /**
4
5
  * User-facing phases shown while one durable chat answer is being prepared.
@@ -10,6 +11,70 @@ export type UserChatProgressPhase =
10
11
  | 'checking_capabilities'
11
12
  | 'starting_response';
12
13
 
14
+ /**
15
+ * Real runtime details surfaced into the user-facing progress card text.
16
+ *
17
+ * Only signals that are already known at the corresponding phase are filled in,
18
+ * so the produced markdown reflects what the worker is actually doing right now
19
+ * instead of a generic scripted summary.
20
+ */
21
+ export type UserChatProgressContext = {
22
+ /**
23
+ * Resolved human-readable agent name, when already loaded for this turn.
24
+ */
25
+ readonly agentName?: string;
26
+
27
+ /**
28
+ * Stable agent identifier shown when the human-readable name is not yet known.
29
+ */
30
+ readonly agentPermanentId?: string;
31
+
32
+ /**
33
+ * Resolved LLM provider title (e.g. `OpenAI`, `Anthropic`), when already known.
34
+ */
35
+ readonly provider?: string;
36
+
37
+ /**
38
+ * Number of attachments the user submitted with the current turn.
39
+ */
40
+ readonly attachmentCount?: number;
41
+
42
+ /**
43
+ * Number of agent-defined tools available to the model for this turn.
44
+ */
45
+ readonly toolCount?: number;
46
+
47
+ /**
48
+ * Concise user-facing tool labels (e.g. `web browser`, `calendar`) for the progress text.
49
+ */
50
+ readonly toolHighlights?: ReadonlyArray<string>;
51
+
52
+ /**
53
+ * Number of knowledge sources the agent will reference during the turn.
54
+ */
55
+ readonly knowledgeSourceCount?: number;
56
+
57
+ /**
58
+ * Whether the calendar integration is wired up for this turn.
59
+ */
60
+ readonly hasCalendarAccess?: boolean;
61
+
62
+ /**
63
+ * Whether the email-sending integration is wired up for this turn.
64
+ */
65
+ readonly hasEmailAccess?: boolean;
66
+
67
+ /**
68
+ * Whether one or more project repositories are linked to this turn.
69
+ */
70
+ readonly hasProjectAccess?: boolean;
71
+
72
+ /**
73
+ * Whether the AgentKit runtime for this agent came from the in-process cache.
74
+ */
75
+ readonly isAgentKitCached?: boolean;
76
+ };
77
+
13
78
  /**
14
79
  * Default title used by durable chat progress cards.
15
80
  */
@@ -26,39 +91,25 @@ const USER_CHAT_PROGRESS_ITEM_IDS = {
26
91
  } as const;
27
92
 
28
93
  /**
29
- * Static progress copy for one durable chat phase.
94
+ * Static checklist mapping for one durable chat phase.
30
95
  */
31
- type UserChatProgressPhasePresentation = {
32
- readonly now: string;
33
- readonly next: string;
96
+ type UserChatProgressPhaseChecklist = {
34
97
  readonly completedItemIds: ReadonlySet<string>;
35
98
  };
36
99
 
37
100
  /**
38
- * User-facing progress copy for durable chat phases.
101
+ * Static checklist progression shared across all durable chat phases.
39
102
  */
40
- const USER_CHAT_PROGRESS_PHASE_PRESENTATIONS: Record<UserChatProgressPhase, UserChatProgressPhasePresentation> = {
41
- queued: {
42
- now: 'Your message is queued and the agent will pick it up shortly.',
43
- next: 'The agent will read the conversation and prepare the answer.',
44
- completedItemIds: new Set([]),
45
- },
46
- reading_context: {
47
- now: 'Reading your message and the relevant conversation context.',
48
- next: 'Prepare the agent instructions and working context.',
49
- completedItemIds: new Set([USER_CHAT_PROGRESS_ITEM_IDS.RECEIVE_MESSAGE]),
50
- },
103
+ const USER_CHAT_PROGRESS_PHASE_CHECKLISTS: Record<UserChatProgressPhase, UserChatProgressPhaseChecklist> = {
104
+ queued: { completedItemIds: new Set([]) },
105
+ reading_context: { completedItemIds: new Set([USER_CHAT_PROGRESS_ITEM_IDS.RECEIVE_MESSAGE]) },
51
106
  preparing_agent: {
52
- now: 'Preparing the agent instructions and context needed for this request.',
53
- next: 'Check which tools and inputs are available.',
54
107
  completedItemIds: new Set([
55
108
  USER_CHAT_PROGRESS_ITEM_IDS.RECEIVE_MESSAGE,
56
109
  USER_CHAT_PROGRESS_ITEM_IDS.PREPARE_AGENT,
57
110
  ]),
58
111
  },
59
112
  checking_capabilities: {
60
- now: 'Checking available tools, attachments, and context before answering.',
61
- next: 'Start working through the response.',
62
113
  completedItemIds: new Set([
63
114
  USER_CHAT_PROGRESS_ITEM_IDS.RECEIVE_MESSAGE,
64
115
  USER_CHAT_PROGRESS_ITEM_IDS.PREPARE_AGENT,
@@ -66,8 +117,6 @@ const USER_CHAT_PROGRESS_PHASE_PRESENTATIONS: Record<UserChatProgressPhase, User
66
117
  ]),
67
118
  },
68
119
  starting_response: {
69
- now: 'Working through the response. Progress updates will stay focused on visible actions and results.',
70
- next: 'Stream the final answer as soon as it is ready.',
71
120
  completedItemIds: new Set([
72
121
  USER_CHAT_PROGRESS_ITEM_IDS.RECEIVE_MESSAGE,
73
122
  USER_CHAT_PROGRESS_ITEM_IDS.PREPARE_AGENT,
@@ -103,19 +152,215 @@ const USER_CHAT_PROGRESS_ITEMS: ReadonlyArray<{
103
152
 
104
153
  /**
105
154
  * Creates one user-facing durable chat progress card for the current job phase.
155
+ *
156
+ * The optional `context` carries already-resolved runtime details (agent name, provider,
157
+ * tool count, attachment count, integrations) so the rendered `now`/`next` text reflects
158
+ * what the worker is actually doing on this turn instead of a generic scripted summary.
106
159
  */
107
- export function createUserChatProgressCard(phase: UserChatProgressPhase): NonNullable<ChatMessage['progressCard']> {
108
- const presentation = USER_CHAT_PROGRESS_PHASE_PRESENTATIONS[phase];
160
+ export function createUserChatProgressCard(
161
+ phase: UserChatProgressPhase,
162
+ context?: UserChatProgressContext,
163
+ ): NonNullable<ChatMessage['progressCard']> {
164
+ const checklist = USER_CHAT_PROGRESS_PHASE_CHECKLISTS[phase];
165
+ const { now, next } = composeUserChatProgressPhaseText(phase, context);
109
166
 
110
167
  return {
111
168
  title: USER_CHAT_PROGRESS_TITLE,
112
- now: presentation.now,
113
- next: presentation.next,
169
+ now,
170
+ next,
114
171
  items: USER_CHAT_PROGRESS_ITEMS.map((item) => ({
115
172
  ...item,
116
- status: presentation.completedItemIds.has(item.id) ? 'completed' : 'pending',
173
+ status: checklist.completedItemIds.has(item.id) ? 'completed' : 'pending',
117
174
  })),
118
175
  updatedAt: new Date().toISOString() as NonNullable<ChatMessage['progressCard']>['updatedAt'],
119
176
  isVisible: true,
120
177
  };
121
178
  }
179
+
180
+ /**
181
+ * Composes the dynamic `now`/`next` markdown shown for one durable chat phase.
182
+ *
183
+ * @private internal helper of `createUserChatProgressCard`
184
+ */
185
+ function composeUserChatProgressPhaseText(
186
+ phase: UserChatProgressPhase,
187
+ context: UserChatProgressContext | undefined,
188
+ ): { now: string; next: string } {
189
+ const agentLabel = describeAgent(context);
190
+
191
+ switch (phase) {
192
+ case 'queued':
193
+ return {
194
+ now: spaceTrim(`
195
+ Your message is waiting in the queue for the next available worker to pick it up.
196
+ `),
197
+ next: spaceTrim(`
198
+ Read the conversation and load ${agentLabel}.
199
+ `),
200
+ };
201
+ case 'reading_context': {
202
+ const attachmentHint = describeAttachmentHint(context);
203
+ return {
204
+ now: spaceTrim(`
205
+ Reading your latest message${attachmentHint} together with the earlier conversation thread.
206
+ `),
207
+ next: spaceTrim(`
208
+ Resolve ${agentLabel} and load its instructions, persona, and rules.
209
+ `),
210
+ };
211
+ }
212
+ case 'preparing_agent': {
213
+ const knowledgeHint = describeKnowledgeHint(context);
214
+ return {
215
+ now: spaceTrim(`
216
+ Loading the instructions and configured commitments for ${agentLabel}${knowledgeHint}.
217
+ `),
218
+ next: spaceTrim(`
219
+ Check which tools, integrations, and attachments are usable for this turn.
220
+ `),
221
+ };
222
+ }
223
+ case 'checking_capabilities': {
224
+ const capabilitiesText = describeCapabilities(context);
225
+ return {
226
+ now: spaceTrim(`
227
+ Verifying that ${agentLabel} can use ${capabilitiesText} for this answer.
228
+ `),
229
+ next: spaceTrim(`
230
+ Connect to the language model and start streaming the response.
231
+ `),
232
+ };
233
+ }
234
+ case 'starting_response': {
235
+ const providerHint = describeProviderHint(context);
236
+ const cacheHint = context?.isAgentKitCached === false
237
+ ? ' Fresh runtime is being prepared because no cached agent was available.'
238
+ : '';
239
+ return {
240
+ now: spaceTrim(`
241
+ Calling the language model${providerHint} and waiting for the first tokens.${cacheHint}
242
+ `),
243
+ next: spaceTrim(`
244
+ Stream the response and update this panel with each user-relevant decision, action, or result.
245
+ `),
246
+ };
247
+ }
248
+ }
249
+ }
250
+
251
+ /**
252
+ * Returns the user-facing label for the active agent based on available context.
253
+ *
254
+ * @private internal helper of `composeUserChatProgressPhaseText`
255
+ */
256
+ function describeAgent(context: UserChatProgressContext | undefined): string {
257
+ if (context?.agentName) {
258
+ return `agent **${context.agentName}**`;
259
+ }
260
+
261
+ if (context?.agentPermanentId) {
262
+ return `agent \`${context.agentPermanentId}\``;
263
+ }
264
+
265
+ return 'the agent';
266
+ }
267
+
268
+ /**
269
+ * Returns a fragment describing attachment count when one or more attachments are present.
270
+ *
271
+ * @private internal helper of `composeUserChatProgressPhaseText`
272
+ */
273
+ function describeAttachmentHint(context: UserChatProgressContext | undefined): string {
274
+ const count = context?.attachmentCount ?? 0;
275
+ if (count <= 0) {
276
+ return '';
277
+ }
278
+
279
+ return count === 1 ? ' and 1 attachment' : ` and ${count} attachments`;
280
+ }
281
+
282
+ /**
283
+ * Returns a fragment describing knowledge sources when one or more are configured.
284
+ *
285
+ * @private internal helper of `composeUserChatProgressPhaseText`
286
+ */
287
+ function describeKnowledgeHint(context: UserChatProgressContext | undefined): string {
288
+ const count = context?.knowledgeSourceCount ?? 0;
289
+ if (count <= 0) {
290
+ return '';
291
+ }
292
+
293
+ return count === 1 ? ' (1 knowledge source)' : ` (${count} knowledge sources)`;
294
+ }
295
+
296
+ /**
297
+ * Returns a fragment describing the LLM provider when already known.
298
+ *
299
+ * @private internal helper of `composeUserChatProgressPhaseText`
300
+ */
301
+ function describeProviderHint(context: UserChatProgressContext | undefined): string {
302
+ if (!context?.provider) {
303
+ return '';
304
+ }
305
+
306
+ return ` via ${context.provider}`;
307
+ }
308
+
309
+ /**
310
+ * Builds the markdown fragment describing tools, knowledge, attachments, and integrations.
311
+ *
312
+ * @private internal helper of `composeUserChatProgressPhaseText`
313
+ */
314
+ function describeCapabilities(context: UserChatProgressContext | undefined): string {
315
+ if (!context) {
316
+ return 'its configured tools and inputs';
317
+ }
318
+
319
+ const fragments: Array<string> = [];
320
+
321
+ const toolCount = context.toolCount ?? 0;
322
+ if (toolCount > 0) {
323
+ const highlights = (context.toolHighlights ?? []).filter((entry) => entry.trim().length > 0);
324
+ if (highlights.length > 0) {
325
+ fragments.push(`${toolCount === 1 ? '1 tool' : `${toolCount} tools`} (${highlights.join(', ')})`);
326
+ } else {
327
+ fragments.push(toolCount === 1 ? '1 tool' : `${toolCount} tools`);
328
+ }
329
+ }
330
+
331
+ const knowledgeCount = context.knowledgeSourceCount ?? 0;
332
+ if (knowledgeCount > 0) {
333
+ fragments.push(knowledgeCount === 1 ? '1 knowledge source' : `${knowledgeCount} knowledge sources`);
334
+ }
335
+
336
+ const attachmentCount = context.attachmentCount ?? 0;
337
+ if (attachmentCount > 0) {
338
+ fragments.push(attachmentCount === 1 ? '1 attachment' : `${attachmentCount} attachments`);
339
+ }
340
+
341
+ const integrations: Array<string> = [];
342
+ if (context.hasCalendarAccess) {
343
+ integrations.push('calendar');
344
+ }
345
+ if (context.hasEmailAccess) {
346
+ integrations.push('email');
347
+ }
348
+ if (context.hasProjectAccess) {
349
+ integrations.push('project repository');
350
+ }
351
+ if (integrations.length > 0) {
352
+ fragments.push(`${integrations.join(' / ')} access`);
353
+ }
354
+
355
+ if (fragments.length === 0) {
356
+ return 'its configured instructions and inputs';
357
+ }
358
+
359
+ if (fragments.length === 1) {
360
+ return fragments[0]!;
361
+ }
362
+
363
+ const head = fragments.slice(0, -1).join(', ');
364
+ const tail = fragments[fragments.length - 1]!;
365
+ return `${head}, and ${tail}`;
366
+ }
@@ -1,6 +1,7 @@
1
1
  import { NextRequest } from 'next/server';
2
2
  import { $getTableName } from '../database/$getTableName';
3
3
  import { $provideSupabaseForServer } from '../database/$provideSupabaseForServer';
4
+ import { parseSessionToken, SESSION_COOKIE_NAME } from './session';
4
5
 
5
6
  /**
6
7
  * Result of Api key validation.
@@ -21,10 +22,13 @@ export type ApiKeyValidationResult = {
21
22
  export async function validateApiKey(request: NextRequest): Promise<ApiKeyValidationResult> {
22
23
  const authHeader = request.headers.get('authorization');
23
24
 
24
- // If no auth header, check if user has a session cookie (logged in via web)
25
+ // If no auth header, check if user has a valid signed session cookie (logged in via web).
26
+ // The cookie value must pass HMAC-signature verification — merely sending a `sessionToken`
27
+ // cookie with arbitrary content must NOT authenticate the request.
25
28
  if (!authHeader) {
26
- const hasSession = request.cookies.has('sessionToken');
27
- if (hasSession) {
29
+ const sessionToken = request.cookies.get(SESSION_COOKIE_NAME)?.value;
30
+ const session = parseSessionToken(sessionToken);
31
+ if (session !== null) {
28
32
  return { isValid: true };
29
33
  }
30
34
  return {
@@ -207,6 +207,10 @@ export type VpsSelfUpdateOverview = {
207
207
  * Number of commits the deployed checkout is behind the latest remote commit, or `null` when unknown.
208
208
  */
209
209
  readonly commitsBehindCount: number | null;
210
+ /**
211
+ * Commits that the deployed checkout is behind the latest remote commit (newest first).
212
+ */
213
+ readonly pendingCommits: ReadonlyArray<VpsSelfUpdatePendingCommit>;
210
214
  /**
211
215
  * Whether the remote branch contains a newer commit than the deployed checkout.
212
216
  */
@@ -465,6 +469,10 @@ export async function readVpsSelfUpdateOverview(): Promise<VpsSelfUpdateOverview
465
469
  currentCommitSha && latestRemoteCommitSha
466
470
  ? await countCommitsBetween(repositoryDirectory, currentCommitSha, latestRemoteCommitSha)
467
471
  : null;
472
+ const pendingCommits =
473
+ currentCommitSha && latestRemoteCommitSha
474
+ ? await listCommitsBetween(repositoryDirectory, currentCommitSha, latestRemoteCommitSha)
475
+ : [];
468
476
  const resolvedJob = resolveVpsSelfUpdateJobForOverview(job, {
469
477
  currentEnvironment,
470
478
  currentCommitSha,
@@ -484,6 +492,7 @@ export async function readVpsSelfUpdateOverview(): Promise<VpsSelfUpdateOverview
484
492
  latestRemoteCommitShortSha: abbreviateCommitSha(latestRemoteCommitSha),
485
493
  latestRemoteCommitDate,
486
494
  commitsBehindCount,
495
+ pendingCommits,
487
496
  isUpdateAvailable: Boolean(
488
497
  currentCommitSha && latestRemoteCommitSha && currentCommitSha !== latestRemoteCommitSha,
489
498
  ),
@@ -494,6 +503,33 @@ export async function readVpsSelfUpdateOverview(): Promise<VpsSelfUpdateOverview
494
503
  };
495
504
  }
496
505
 
506
+ /**
507
+ * Browser-safe summary of one commit that the deployed checkout is behind the latest remote commit.
508
+ */
509
+ export type VpsSelfUpdatePendingCommit = {
510
+ /**
511
+ * Full commit hash.
512
+ */
513
+ readonly commitSha: string;
514
+ /**
515
+ * Short commit hash (first 7 chars).
516
+ */
517
+ readonly shortCommitSha: string;
518
+ /**
519
+ * Single-line commit subject.
520
+ */
521
+ readonly subject: string;
522
+ /**
523
+ * Author timestamp in ISO format or `null` when unknown.
524
+ */
525
+ readonly authoredAt: string | null;
526
+ };
527
+
528
+ /**
529
+ * Hard ceiling for the pending-commits listing returned in the overview to avoid huge payloads on a long-stale server.
530
+ */
531
+ const VPS_SELF_UPDATE_MAX_PENDING_COMMITS = 100;
532
+
497
533
  /**
498
534
  * Browser-safe summary of one commit that the super admin can pick from the custom-target picker.
499
535
  */
@@ -790,6 +826,7 @@ function createUnavailableOverview(context: {
790
826
  latestRemoteCommitShortSha: null,
791
827
  latestRemoteCommitDate: null,
792
828
  commitsBehindCount: null,
829
+ pendingCommits: [],
793
830
  isUpdateAvailable: false,
794
831
  originRepositoryUrl: context.originRepositoryUrl,
795
832
  isOriginRepositoryDefault: context.originRepositoryUrl === VPS_SELF_UPDATE_DEFAULT_ORIGIN_REPOSITORY_URL,
@@ -1205,6 +1242,79 @@ async function countCommitsBetween(
1205
1242
  return Number.isFinite(parsedCount) ? parsedCount : null;
1206
1243
  }
1207
1244
 
1245
+ /**
1246
+ * Lists commits that separate two commits in the local repository so the admin UI can show subject/hash/date for each one.
1247
+ *
1248
+ * Returns an empty list when either commit cannot be resolved (typical for a shallow clone that has not been deepened yet)
1249
+ * or when both commits are identical.
1250
+ *
1251
+ * @param repositoryDirectory - Repository checkout path.
1252
+ * @param fromCommitSha - Older commit hash (deployed commit).
1253
+ * @param toCommitSha - Newer commit hash (latest remote commit).
1254
+ * @returns Browser-safe pending-commit list (newest first).
1255
+ */
1256
+ async function listCommitsBetween(
1257
+ repositoryDirectory: string,
1258
+ fromCommitSha: string,
1259
+ toCommitSha: string,
1260
+ ): Promise<ReadonlyArray<VpsSelfUpdatePendingCommit>> {
1261
+ if (fromCommitSha === toCommitSha) {
1262
+ return [];
1263
+ }
1264
+
1265
+ const output = await runGitInRepository(repositoryDirectory, [
1266
+ 'log',
1267
+ `--max-count=${VPS_SELF_UPDATE_MAX_PENDING_COMMITS}`,
1268
+ `--format=%H${GIT_LOG_FIELD_SEPARATOR}%aI${GIT_LOG_FIELD_SEPARATOR}%s`,
1269
+ `${fromCommitSha}..${toCommitSha}`,
1270
+ ]);
1271
+ if (!output) {
1272
+ return [];
1273
+ }
1274
+
1275
+ const pendingCommits: Array<VpsSelfUpdatePendingCommit> = [];
1276
+ for (const line of output.split('\n')) {
1277
+ if (!line) {
1278
+ continue;
1279
+ }
1280
+
1281
+ const fields = line.split(GIT_LOG_FIELD_SEPARATOR);
1282
+ const commitSha = fields[0] ?? '';
1283
+ if (!commitSha) {
1284
+ continue;
1285
+ }
1286
+
1287
+ const authoredAt = fields[1] || null;
1288
+ const subject = fields.slice(2).join(GIT_LOG_FIELD_SEPARATOR);
1289
+
1290
+ pendingCommits.push({
1291
+ commitSha,
1292
+ shortCommitSha: commitSha.slice(0, 7),
1293
+ subject,
1294
+ authoredAt,
1295
+ });
1296
+ }
1297
+
1298
+ return pendingCommits;
1299
+ }
1300
+
1301
+ /**
1302
+ * Reads the full persisted standalone VPS self-update log so the super admin can copy/download it for debugging.
1303
+ *
1304
+ * @returns Log file content or `null` when the file does not exist yet.
1305
+ */
1306
+ export async function readVpsSelfUpdateLogFileContent(): Promise<string | null> {
1307
+ const logFilePath = resolveVpsSelfUpdateLogFilePath();
1308
+ try {
1309
+ return await readFile(logFilePath, 'utf-8');
1310
+ } catch (error) {
1311
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
1312
+ return null;
1313
+ }
1314
+ throw error;
1315
+ }
1316
+ }
1317
+
1208
1318
  /**
1209
1319
  * Checks whether a detached update process is still alive.
1210
1320
  *