@promptbook/cli 0.113.0-0 → 0.113.0-1

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 (29) hide show
  1. package/apps/agents-server/src/app/admin/task-manager/TaskManagerTaskRow.tsx +7 -2
  2. package/apps/agents-server/src/app/api/page-preview/check/route.ts +5 -20
  3. package/apps/agents-server/src/app/api/page-preview/interact/route.ts +179 -0
  4. package/apps/agents-server/src/app/api/page-preview/live/route.ts +67 -0
  5. package/apps/agents-server/src/app/api/page-preview/screenshot/route.ts +13 -21
  6. package/apps/agents-server/src/constants/chatVisualMode.ts +1 -1
  7. package/apps/agents-server/src/constants/themeMode.ts +1 -1
  8. package/apps/agents-server/src/database/migrations/2026-05-2600-default-theme.sql +2 -2
  9. package/apps/agents-server/src/utils/chatTasksAdmin.ts +3 -1
  10. package/apps/agents-server/src/utils/getAdminChatTasksResponse/mapVpsSelfUpdateJobToAdminChatTask.ts +84 -0
  11. package/apps/agents-server/src/utils/getAdminChatTasksResponse.ts +195 -5
  12. package/apps/agents-server/src/utils/pagePreview/livePagePreviewSessions.ts +365 -0
  13. package/apps/agents-server/src/utils/pagePreview/resolvePagePreviewRequestUrl.ts +74 -0
  14. package/apps/agents-server/src/utils/vpsSelfUpdate.ts +12 -0
  15. package/esm/index.es.js +1 -1
  16. package/esm/src/book-components/Chat/Chat/CitationIframePreview.d.ts +2 -2
  17. package/esm/src/book-components/Chat/utils/livePagePreviewConstants.d.ts +12 -0
  18. package/esm/src/version.d.ts +1 -1
  19. package/package.json +1 -1
  20. package/src/book-components/Chat/Chat/Chat.module.css +14 -5
  21. package/src/book-components/Chat/Chat/CitationIframePreview.tsx +197 -22
  22. package/src/book-components/Chat/utils/livePagePreviewConstants.ts +17 -0
  23. package/src/other/templates/getTemplatesPipelineCollection.ts +764 -750
  24. package/src/version.ts +2 -2
  25. package/src/versions.txt +1 -0
  26. package/umd/index.umd.js +1 -1
  27. package/umd/src/book-components/Chat/Chat/CitationIframePreview.d.ts +2 -2
  28. package/umd/src/book-components/Chat/utils/livePagePreviewConstants.d.ts +12 -0
  29. package/umd/src/version.d.ts +1 -1
@@ -130,6 +130,10 @@ function formatTaskKind(kind: AdminChatTaskRecord['kind']): string {
130
130
  return 'Chat timeout';
131
131
  }
132
132
 
133
+ if (kind === 'VPS_SELF_UPDATE') {
134
+ return 'Self-update';
135
+ }
136
+
133
137
  return kind;
134
138
  }
135
139
 
@@ -260,8 +264,9 @@ function resolveTaskRowClassName(task: AdminChatTaskRecord, isStuck: boolean): s
260
264
  * @private function of TaskManagerTaskRow
261
265
  */
262
266
  function TaskManagerTaskActions({ busyAction, isBusy, onRunTaskAction, task }: TaskManagerTaskActionsProps) {
263
- const isCancelable = task.status === 'QUEUED' || task.status === 'RUNNING';
264
- const isRetryable = task.status === 'FAILED';
267
+ const isDurableChatTask = task.kind === 'CHAT_COMPLETION' || task.kind === 'CHAT_TIMEOUT';
268
+ const isCancelable = isDurableChatTask && (task.status === 'QUEUED' || task.status === 'RUNNING');
269
+ const isRetryable = isDurableChatTask && task.status === 'FAILED';
265
270
 
266
271
  return (
267
272
  <div className="flex justify-end gap-2">
@@ -1,9 +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';
6
4
  import { checkIfUrlCanBeEmbedded } from '../../../../utils/iframe/checkIfUrlCanBeEmbedded';
5
+ import { resolvePagePreviewRequestUrl } from '../../../../utils/pagePreview/resolvePagePreviewRequestUrl';
7
6
 
8
7
  /**
9
8
  * Checks whether a given URL can be embedded in an iframe by inspecting
@@ -18,27 +17,13 @@ import { checkIfUrlCanBeEmbedded } from '../../../../utils/iframe/checkIfUrlCanB
18
17
  * The destination URL is validated against private/internal IP ranges before fetching.
19
18
  */
20
19
  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
-
26
- const url = request.nextUrl.searchParams.get('url');
27
-
28
- if (!url) {
29
- return NextResponse.json({ error: 'Missing required query parameter: url' }, { status: 400 });
30
- }
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 });
20
+ const requestUrlResolution = await resolvePagePreviewRequestUrl(request);
21
+ if (requestUrlResolution.errorResponse !== null) {
22
+ return requestUrlResolution.errorResponse;
38
23
  }
39
24
 
40
25
  try {
41
- const canEmbed = await checkIfUrlCanBeEmbedded(url);
26
+ const canEmbed = await checkIfUrlCanBeEmbedded(requestUrlResolution.url);
42
27
  return NextResponse.json({ canEmbed });
43
28
  } catch (error) {
44
29
  assertsError(error);
@@ -0,0 +1,179 @@
1
+ import { serializeError } from '@promptbook-local/utils';
2
+ import type { NextRequest } from 'next/server';
3
+ import { NextResponse } from 'next/server';
4
+ import { assertsError } from '../../../../../../../src/errors/assertsError';
5
+ import { getCurrentUser } from '../../../../utils/getCurrentUser';
6
+ import {
7
+ applyLivePagePreviewInteraction,
8
+ isLivePagePreviewSessionId,
9
+ type LivePagePreviewInteraction,
10
+ } from '../../../../utils/pagePreview/livePagePreviewSessions';
11
+
12
+ /**
13
+ * Maximum wheel delta accepted from one live-preview UI event.
14
+ */
15
+ const MAX_LIVE_PAGE_PREVIEW_WHEEL_DELTA = 4_000;
16
+
17
+ /**
18
+ * Maximum keyboard key name length accepted from one live-preview UI event.
19
+ */
20
+ const MAX_LIVE_PAGE_PREVIEW_KEY_LENGTH = 40;
21
+
22
+ /**
23
+ * Parsed live-preview interaction request body.
24
+ */
25
+ type ParsedLivePagePreviewInteractionRequest = {
26
+ readonly sessionId: string;
27
+ readonly interaction: LivePagePreviewInteraction;
28
+ };
29
+
30
+ /**
31
+ * Constant for dynamic route rendering.
32
+ */
33
+ export const dynamic = 'force-dynamic';
34
+
35
+ /**
36
+ * Applies one UI interaction to an active live browser preview.
37
+ *
38
+ * Requires authentication because interaction targets are opened by the authenticated preview route.
39
+ */
40
+ export async function POST(request: NextRequest): Promise<NextResponse> {
41
+ const currentUser = await getCurrentUser();
42
+ if (!currentUser) {
43
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
44
+ }
45
+
46
+ const parsedRequest = await parseLivePagePreviewInteractionRequest(request);
47
+ if (!parsedRequest) {
48
+ return NextResponse.json({ error: 'Invalid live preview interaction request.' }, { status: 400 });
49
+ }
50
+
51
+ try {
52
+ const isApplied = await applyLivePagePreviewInteraction(parsedRequest);
53
+ if (!isApplied) {
54
+ return NextResponse.json({ error: 'Live preview session not found.' }, { status: 404 });
55
+ }
56
+
57
+ return NextResponse.json({ ok: true });
58
+ } catch (error) {
59
+ assertsError(error);
60
+ console.error('Error applying live page preview interaction:', error);
61
+ return NextResponse.json({ error: serializeError(error) }, { status: 500 });
62
+ }
63
+ }
64
+
65
+ /**
66
+ * Parses and validates a live-preview interaction request.
67
+ *
68
+ * @param request - Incoming interaction request.
69
+ * @returns Parsed request or null when the payload is invalid.
70
+ */
71
+ async function parseLivePagePreviewInteractionRequest(
72
+ request: NextRequest,
73
+ ): Promise<ParsedLivePagePreviewInteractionRequest | null> {
74
+ let body: unknown;
75
+
76
+ try {
77
+ body = await request.json();
78
+ } catch {
79
+ return null;
80
+ }
81
+
82
+ if (!isRecord(body)) {
83
+ return null;
84
+ }
85
+
86
+ const sessionId = typeof body.sessionId === 'string' ? body.sessionId : null;
87
+ if (!sessionId || !isLivePagePreviewSessionId(sessionId)) {
88
+ return null;
89
+ }
90
+
91
+ const interaction = parseLivePagePreviewInteraction(body);
92
+ if (!interaction) {
93
+ return null;
94
+ }
95
+
96
+ return {
97
+ sessionId,
98
+ interaction,
99
+ };
100
+ }
101
+
102
+ /**
103
+ * Parses one normalized live-preview interaction from an object payload.
104
+ *
105
+ * @param body - Request body record.
106
+ * @returns Interaction payload or null when invalid.
107
+ */
108
+ function parseLivePagePreviewInteraction(body: Record<string, unknown>): LivePagePreviewInteraction | null {
109
+ if (body.type === 'click') {
110
+ const x = readFiniteNumber(body.x);
111
+ const y = readFiniteNumber(body.y);
112
+
113
+ if (x === null || y === null) {
114
+ return null;
115
+ }
116
+
117
+ return { type: 'click', x, y };
118
+ }
119
+
120
+ if (body.type === 'wheel') {
121
+ const deltaX = readFiniteNumber(body.deltaX);
122
+ const deltaY = readFiniteNumber(body.deltaY);
123
+
124
+ if (deltaX === null || deltaY === null) {
125
+ return null;
126
+ }
127
+
128
+ return {
129
+ type: 'wheel',
130
+ deltaX: clampWheelDelta(deltaX),
131
+ deltaY: clampWheelDelta(deltaY),
132
+ };
133
+ }
134
+
135
+ if (body.type === 'keyDown') {
136
+ const key = typeof body.key === 'string' ? body.key : null;
137
+ if (!key || key.length > MAX_LIVE_PAGE_PREVIEW_KEY_LENGTH) {
138
+ return null;
139
+ }
140
+
141
+ return { type: 'keyDown', key };
142
+ }
143
+
144
+ return null;
145
+ }
146
+
147
+ /**
148
+ * Checks whether a value is a plain object record.
149
+ *
150
+ * @param value - Candidate value.
151
+ * @returns True when the value can be read as a JSON object.
152
+ */
153
+ function isRecord(value: unknown): value is Record<string, unknown> {
154
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
155
+ }
156
+
157
+ /**
158
+ * Reads one finite number from a JSON value.
159
+ *
160
+ * @param value - Candidate JSON value.
161
+ * @returns Number or null when invalid.
162
+ */
163
+ function readFiniteNumber(value: unknown): number | null {
164
+ if (typeof value !== 'number' || !Number.isFinite(value)) {
165
+ return null;
166
+ }
167
+
168
+ return value;
169
+ }
170
+
171
+ /**
172
+ * Clamps one wheel delta to a bounded value.
173
+ *
174
+ * @param value - Raw wheel delta.
175
+ * @returns Delta safe to forward to Playwright.
176
+ */
177
+ function clampWheelDelta(value: number): number {
178
+ return Math.min(Math.max(value, -MAX_LIVE_PAGE_PREVIEW_WHEEL_DELTA), MAX_LIVE_PAGE_PREVIEW_WHEEL_DELTA);
179
+ }
@@ -0,0 +1,67 @@
1
+ import { serializeError } from '@promptbook-local/utils';
2
+ import type { NextRequest } from 'next/server';
3
+ import { NextResponse } from 'next/server';
4
+ import { assertsError } from '../../../../../../../src/errors/assertsError';
5
+ import {
6
+ createLivePagePreviewStream,
7
+ getOrCreateLivePagePreviewSession,
8
+ isLivePagePreviewSessionId,
9
+ LIVE_PAGE_PREVIEW_STREAM_CONTENT_TYPE,
10
+ } from '../../../../utils/pagePreview/livePagePreviewSessions';
11
+ import { resolvePagePreviewRequestUrl } from '../../../../utils/pagePreview/resolvePagePreviewRequestUrl';
12
+
13
+ /**
14
+ * Query parameter containing the browser-side live preview session id.
15
+ */
16
+ const LIVE_PAGE_PREVIEW_SESSION_ID_QUERY_PARAMETER = 'sessionId';
17
+
18
+ /**
19
+ * Constant for dynamic route rendering.
20
+ */
21
+ export const dynamic = 'force-dynamic';
22
+
23
+ /**
24
+ * Streams a live browser preview for a knowledge source URL.
25
+ *
26
+ * Query parameters:
27
+ * - `url` — fully-qualified HTTP(S) URL to preview
28
+ * - `sessionId` — client-generated id used for forwarding interactions
29
+ *
30
+ * Requires authentication to prevent unauthenticated SSRF abuse.
31
+ */
32
+ export async function GET(request: NextRequest): Promise<NextResponse> {
33
+ const requestUrlResolution = await resolvePagePreviewRequestUrl(request);
34
+ if (requestUrlResolution.errorResponse !== null) {
35
+ return requestUrlResolution.errorResponse;
36
+ }
37
+
38
+ const sessionId = request.nextUrl.searchParams.get(LIVE_PAGE_PREVIEW_SESSION_ID_QUERY_PARAMETER);
39
+ if (!sessionId || !isLivePagePreviewSessionId(sessionId)) {
40
+ return NextResponse.json({ error: 'Missing or invalid live preview session id.' }, { status: 400 });
41
+ }
42
+
43
+ try {
44
+ const page = await getOrCreateLivePagePreviewSession({
45
+ sessionId,
46
+ url: requestUrlResolution.url,
47
+ signal: request.signal,
48
+ });
49
+ const stream = createLivePagePreviewStream({
50
+ sessionId,
51
+ page,
52
+ signal: request.signal,
53
+ });
54
+
55
+ return new NextResponse(stream, {
56
+ headers: {
57
+ 'Content-Type': LIVE_PAGE_PREVIEW_STREAM_CONTENT_TYPE,
58
+ 'Cache-Control': 'no-cache, no-store, max-age=0',
59
+ Connection: 'keep-alive',
60
+ },
61
+ });
62
+ } catch (error) {
63
+ assertsError(error);
64
+ console.error('Error streaming live page preview:', error);
65
+ return NextResponse.json({ error: serializeError(error) }, { status: 500 });
66
+ }
67
+ }
@@ -2,9 +2,12 @@ import { $provideBrowserForServer } from '@/src/tools/$provideBrowserForServer';
2
2
  import { serializeError } from '@promptbook-local/utils';
3
3
  import type { NextRequest } from 'next/server';
4
4
  import { NextResponse } from 'next/server';
5
+ import {
6
+ LIVE_PAGE_PREVIEW_VIEWPORT_HEIGHT,
7
+ LIVE_PAGE_PREVIEW_VIEWPORT_WIDTH,
8
+ } from '../../../../../../../src/book-components/Chat/utils/livePagePreviewConstants';
5
9
  import { assertsError } from '../../../../../../../src/errors/assertsError';
6
- import { assertSafeUrl } from '../../../../utils/assertSafeUrl';
7
- import { getCurrentUser } from '../../../../utils/getCurrentUser';
10
+ import { resolvePagePreviewRequestUrl } from '../../../../utils/pagePreview/resolvePagePreviewRequestUrl';
8
11
 
9
12
  /**
10
13
  * Takes a screenshot of the given URL using a headless browser.
@@ -18,23 +21,9 @@ import { getCurrentUser } from '../../../../utils/getCurrentUser';
18
21
  * The destination URL is validated against private/internal IP ranges before fetching.
19
22
  */
20
23
  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
-
26
- const url = request.nextUrl.searchParams.get('url');
27
-
28
- if (!url) {
29
- return NextResponse.json({ error: 'Missing required query parameter: url' }, { status: 400 });
30
- }
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 });
24
+ const requestUrlResolution = await resolvePagePreviewRequestUrl(request);
25
+ if (requestUrlResolution.errorResponse !== null) {
26
+ return requestUrlResolution.errorResponse;
38
27
  }
39
28
 
40
29
  try {
@@ -42,8 +31,11 @@ export async function GET(request: NextRequest): Promise<NextResponse> {
42
31
  const page = await browserContext.newPage();
43
32
 
44
33
  try {
45
- await page.setViewportSize({ width: 1280, height: 800 });
46
- await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 30_000 });
34
+ await page.setViewportSize({
35
+ width: LIVE_PAGE_PREVIEW_VIEWPORT_WIDTH,
36
+ height: LIVE_PAGE_PREVIEW_VIEWPORT_HEIGHT,
37
+ });
38
+ await page.goto(requestUrlResolution.url, { waitUntil: 'domcontentloaded', timeout: 30_000 });
47
39
  const screenshotBuffer = await page.screenshot({ type: 'png' });
48
40
 
49
41
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -46,7 +46,7 @@ export const CHAT_VISUAL_MODE_COOKIE_NAME = 'promptbook_agents_chat_visual_mode'
46
46
  /**
47
47
  * Fallback visual mode when metadata/browser values are missing or invalid.
48
48
  */
49
- export const DEFAULT_CHAT_VISUAL_MODE: ChatVisualMode = CHAT_VISUAL_MODES.BUBBLE_MODE;
49
+ export const DEFAULT_CHAT_VISUAL_MODE: ChatVisualMode = CHAT_VISUAL_MODES.ARTICLE_MODE;
50
50
 
51
51
  /**
52
52
  * Resolves one raw visual mode value to a supported mode.
@@ -46,7 +46,7 @@ export const THEME_MODE_OPTIONS: ReadonlyArray<{
46
46
  /**
47
47
  * Built-in fallback theme mode applied when metadata/browser values are missing or invalid.
48
48
  */
49
- export const DEFAULT_THEME_MODE: ThemeMode = THEME_MODES.SYSTEM;
49
+ export const DEFAULT_THEME_MODE: ThemeMode = THEME_MODES.LIGHT;
50
50
 
51
51
  /**
52
52
  * Local storage key used to keep the latest browser theme mode in sync.
@@ -1,6 +1,6 @@
1
1
  INSERT INTO "prefix_Metadata" ("key", "value", "note", "createdAt", "updatedAt")
2
2
  SELECT 'DEFAULT_THEME',
3
- 'SYSTEM',
3
+ 'LIGHT',
4
4
  'Default theme mode for browsers without a saved preference. Allowed values: SYSTEM, LIGHT, DARK.',
5
5
  NOW(),
6
6
  NOW()
@@ -9,7 +9,7 @@ WHERE NOT EXISTS (SELECT 1 FROM "prefix_Metadata" WHERE "key" = 'DEFAULT_THEME')
9
9
  UPDATE "prefix_Metadata"
10
10
  SET "value" = CASE
11
11
  WHEN UPPER(COALESCE("value", '')) IN ('SYSTEM', 'LIGHT', 'DARK') THEN UPPER("value")
12
- ELSE 'SYSTEM'
12
+ ELSE 'LIGHT'
13
13
  END,
14
14
  "note" = 'Default theme mode for browsers without a saved preference. Allowed values: SYSTEM, LIGHT, DARK.',
15
15
  "updatedAt" = NOW()
@@ -10,9 +10,11 @@ export type AdminChatTaskView = 'active' | 'running' | 'queued' | 'failed' | 'al
10
10
  /**
11
11
  * Durable task kinds currently surfaced by the admin task manager.
12
12
  *
13
+ * `VPS_SELF_UPDATE` represents the standalone VPS self-update session triggered from `/admin/update`.
14
+ *
13
15
  * @private internal admin utility of Agents Server
14
16
  */
15
- export type AdminChatTaskKind = 'CHAT_COMPLETION' | 'CHAT_TIMEOUT';
17
+ export type AdminChatTaskKind = 'CHAT_COMPLETION' | 'CHAT_TIMEOUT' | 'VPS_SELF_UPDATE';
16
18
 
17
19
  /**
18
20
  * One row shown in the admin task-manager table.
@@ -0,0 +1,84 @@
1
+ import type { AdminChatTaskRecord } from '../chatTasksAdmin';
2
+ import type { UserChatJobStatus } from '../userChat/UserChatJobRecord';
3
+ import type { VpsSelfUpdateJobSnapshot } from '../vpsSelfUpdate';
4
+
5
+ /**
6
+ * Stable synthetic task id used for the singleton standalone VPS self-update task row.
7
+ *
8
+ * @private internal admin utility of Agents Server
9
+ */
10
+ export const VPS_SELF_UPDATE_ADMIN_CHAT_TASK_ID = 'vps-self-update';
11
+
12
+ /**
13
+ * Synthetic queue name used for the standalone VPS self-update task row.
14
+ *
15
+ * @private internal admin utility of Agents Server
16
+ */
17
+ export const VPS_SELF_UPDATE_ADMIN_CHAT_TASK_QUEUE_NAME = 'vps-self-update';
18
+
19
+ /**
20
+ * Maps a persisted VPS self-update job snapshot to the admin task manager row shape.
21
+ *
22
+ * @param job - Latest persisted self-update job snapshot.
23
+ * @returns Task manager row or `null` when no self-update has ever been triggered on this server.
24
+ */
25
+ export function mapVpsSelfUpdateJobToAdminChatTask(job: VpsSelfUpdateJobSnapshot): AdminChatTaskRecord | null {
26
+ const status = toAdminChatTaskStatus(job);
27
+ if (!status) {
28
+ return null;
29
+ }
30
+
31
+ const isTerminalStatus = status === 'COMPLETED' || status === 'FAILED';
32
+ const startedAt = job.startedAt;
33
+ const finishedAt = isTerminalStatus ? job.finishedAt || startedAt : null;
34
+ const updatedAt = finishedAt || startedAt || new Date().toISOString();
35
+ const errorSummary = job.errorMessage || null;
36
+ const currentStepDetails = job.currentStep || null;
37
+
38
+ return {
39
+ id: VPS_SELF_UPDATE_ADMIN_CHAT_TASK_ID,
40
+ kind: 'VPS_SELF_UPDATE',
41
+ status,
42
+ createdAt: startedAt || updatedAt,
43
+ queuedAt: startedAt || updatedAt,
44
+ startedAt,
45
+ updatedAt,
46
+ finishedAt,
47
+ cancelRequestedAt: null,
48
+ pausedAt: null,
49
+ lastHeartbeatAt: null,
50
+ leaseExpiresAt: null,
51
+ recurrenceIntervalMs: null,
52
+ priority: null,
53
+ attemptCount: 1,
54
+ retryCount: 0,
55
+ lastErrorSummary: errorSummary,
56
+ lastErrorDetails: currentStepDetails,
57
+ userId: 0,
58
+ username: null,
59
+ agentPermanentId: VPS_SELF_UPDATE_ADMIN_CHAT_TASK_ID,
60
+ agentName: job.targetEnvironment.label,
61
+ chatId: job.targetBranch || job.targetEnvironment.branch || VPS_SELF_UPDATE_ADMIN_CHAT_TASK_ID,
62
+ workerId: job.pid !== null ? String(job.pid) : null,
63
+ queueName: VPS_SELF_UPDATE_ADMIN_CHAT_TASK_QUEUE_NAME,
64
+ };
65
+ }
66
+
67
+ /**
68
+ * Maps the shell-owned self-update status to the durable chat-task status enum used by the admin task manager.
69
+ *
70
+ * @param job - Persisted self-update job snapshot.
71
+ * @returns Equivalent durable chat-job status, or `null` when the job is idle (no run has been triggered yet).
72
+ */
73
+ function toAdminChatTaskStatus(job: VpsSelfUpdateJobSnapshot): UserChatJobStatus | null {
74
+ switch (job.status) {
75
+ case 'running':
76
+ return 'RUNNING';
77
+ case 'succeeded':
78
+ return 'COMPLETED';
79
+ case 'failed':
80
+ return 'FAILED';
81
+ case 'idle':
82
+ return null;
83
+ }
84
+ }