pi-background-tasks 0.6.0 → 0.7.2

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.
@@ -1,306 +1,432 @@
1
- import { statSync } from "node:fs";
2
- import { open } from "node:fs/promises";
3
- import { DEFAULT_MAX_BYTES } from "@earendil-works/pi-coding-agent";
4
-
5
- export type TaskStatus = "running" | "completed" | "failed" | "killed";
6
- export type KillKind = "user" | "timeout" | "output_cap" | "shutdown";
7
-
8
- export type TaskContextUsage = {
9
- tokens: number | null;
10
- contextWindow: number;
11
- percent: number | null;
12
- };
13
-
14
- export type TaskTokenUsage = {
15
- input: number;
16
- output: number;
17
- cacheRead: number;
18
- cacheWrite: number;
19
- totalTokens: number;
20
- costTotal?: number;
21
- };
22
-
23
- export type TaskToolUsage = {
24
- total: number;
25
- failed: number;
26
- byName: Record<string, number>;
27
- };
28
-
29
- export type BgTaskSnapshot = {
30
- id: string;
31
- name?: string | undefined;
32
- command: string;
33
- description?: string | undefined;
34
- status: TaskStatus;
35
- outputPath: string;
36
- cwd: string;
37
- startTime: number;
38
- endTime?: number | undefined;
39
- exitCode?: number | null | undefined;
40
- signal?: string | null | undefined;
41
- pid?: number | undefined;
42
- bytesWritten: number;
43
- isAgent: boolean;
44
- error?: string | undefined;
45
- notified: boolean;
46
- notifyOnCompletion: boolean;
47
- triggerOnCompletion: boolean;
48
- timeoutSeconds?: number | undefined;
49
- contextUsage?: TaskContextUsage | undefined;
50
- tokenUsage?: TaskTokenUsage | undefined;
51
- toolUsage?: TaskToolUsage | undefined;
52
- model?: string | undefined;
53
- };
54
-
55
- export type BgTask = Omit<BgTaskSnapshot, "name"> & {
56
- name: string;
57
- outputAbsPath: string;
58
- metadataAbsPath: string;
59
- child?: import("./registry.js").BackgroundTaskChildProcess | undefined;
60
- stream?: import("node:fs").WriteStream | undefined;
61
- timeoutHandle?: NodeJS.Timeout | undefined;
62
- killKind?: KillKind | undefined;
63
- killSignalSent?: boolean | undefined;
64
- capExceeded?: boolean | undefined;
65
- finalized?: boolean | undefined;
66
- contextUsageBuffer?: string | undefined;
67
- /** True when this task launched a telemetry-wrapped Pi agent; its stdout carries control lines, not raw output. */
68
- telemetryWrapped?: boolean | undefined;
69
- /** Partial trailing stdout line held between chunks while reconstructing wrapped-agent control lines. */
70
- agentStdoutBuffer?: string | undefined;
71
- waiters: Array<() => void>;
72
- };
73
-
74
- export type BgRunDetails = {
75
- task: BgTaskSnapshot;
76
- };
77
-
78
- export type BgStatusDetails = {
79
- tasks: BgTaskSnapshot[];
80
- };
81
-
82
- export type BgLogsDetails = {
83
- task: BgTaskSnapshot;
84
- path: string;
85
- bytesRead: number;
86
- truncated: boolean;
87
- tail: boolean;
88
- };
89
-
90
- export type BgKillDetails = {
91
- task: BgTaskSnapshot;
92
- message: string;
93
- };
94
-
95
- export type StartTaskOptions = {
96
- name?: string | undefined;
97
- description?: string | undefined;
98
- isAgent?: boolean | undefined;
99
- timeoutSeconds?: number | undefined;
100
- notifyOnCompletion?: boolean | undefined;
101
- triggerOnCompletion?: boolean | undefined;
102
- };
1
+ import { statSync, type WriteStream } from 'node:fs';
2
+ import { open } from 'node:fs/promises';
3
+ import { DEFAULT_MAX_BYTES } from '@earendil-works/pi-coding-agent';
4
+ import type { BackgroundTaskChildProcess } from './registry.js';
5
+
6
+ export const TASK_STATUS_VALUES = ['running', 'completed', 'failed', 'killed'] as const;
7
+ export const TERMINAL_TASK_STATUS_VALUES = ['completed', 'failed', 'killed'] as const;
8
+
9
+ export type TaskStatus = (typeof TASK_STATUS_VALUES)[number];
10
+ export type TerminalTaskStatus = (typeof TERMINAL_TASK_STATUS_VALUES)[number];
11
+ export type KillKind = 'user' | 'timeout' | 'output_cap' | 'shutdown';
12
+
13
+ export type JsonObject = Readonly<Record<PropertyKey, unknown>>;
14
+
15
+ export interface TaskContextUsage {
16
+ tokens: number | null;
17
+ contextWindow: number;
18
+ percent: number | null;
19
+ }
20
+
21
+ export interface TaskTokenUsage {
22
+ input: number;
23
+ output: number;
24
+ cacheRead: number;
25
+ cacheWrite: number;
26
+ totalTokens: number;
27
+ costTotal?: number;
28
+ }
29
+
30
+ export interface TaskToolUsage {
31
+ total: number;
32
+ failed: number;
33
+ byName: Record<string, number>;
34
+ }
35
+
36
+ export interface BgTaskSnapshot {
37
+ id: string;
38
+ name?: string | undefined;
39
+ command: string;
40
+ description?: string | undefined;
41
+ status: TaskStatus;
42
+ outputPath: string;
43
+ cwd: string;
44
+ startTime: number;
45
+ endTime?: number | undefined;
46
+ exitCode?: number | null | undefined;
47
+ signal?: string | null | undefined;
48
+ pid?: number | undefined;
49
+ bytesWritten: number;
50
+ isAgent: boolean;
51
+ error?: string | undefined;
52
+ notified: boolean;
53
+ notifyOnCompletion: boolean;
54
+ triggerOnCompletion: boolean;
55
+ timeoutSeconds?: number | undefined;
56
+ contextUsage?: TaskContextUsage | undefined;
57
+ tokenUsage?: TaskTokenUsage | undefined;
58
+ toolUsage?: TaskToolUsage | undefined;
59
+ model?: string | undefined;
60
+ attestationPath?: string | undefined;
61
+ }
62
+
63
+ export interface AttestedPiTaskFiles {
64
+ eventsPath: string;
65
+ stderrPath: string;
66
+ wrapperPath: string;
67
+ attestationPath: string;
68
+ }
69
+
70
+ export interface AttestedPiTaskSnapshot extends BgTaskSnapshot {
71
+ attestedPi?: AttestedPiTaskFiles | undefined;
72
+ }
73
+
74
+ export interface BgTask extends Omit<BgTaskSnapshot, 'name'> {
75
+ name: string;
76
+ outputAbsPath: string;
77
+ metadataAbsPath: string;
78
+ eventsAbsPath?: string | undefined;
79
+ stderrAbsPath?: string | undefined;
80
+ wrapperAbsPath?: string | undefined;
81
+ attestationAbsPath?: string | undefined;
82
+ child?: BackgroundTaskChildProcess | undefined;
83
+ stream?: WriteStream | undefined;
84
+ timeoutHandle?: NodeJS.Timeout | undefined;
85
+ killKind?: KillKind | undefined;
86
+ killSignalSent?: boolean | undefined;
87
+ capExceeded?: boolean | undefined;
88
+ finalized?: boolean | undefined;
89
+ terminalPublished?: boolean | undefined;
90
+ terminalPublishInFlight?: boolean | undefined;
91
+ terminalPublishRetryHandle?: NodeJS.Timeout | undefined;
92
+ /** Optional protocol barrier used by EventBus run requests so early child exits cannot publish before the run response is observable. */
93
+ terminalPublicationGate?: Promise<void> | undefined;
94
+ contextUsageBuffer?: string | undefined;
95
+ /** True when this task launched a telemetry-wrapped Pi agent; its stdout carries control lines, not raw output. */
96
+ telemetryWrapped?: boolean | undefined;
97
+ /** Partial trailing stdout line held between chunks while reconstructing wrapped-agent control lines. */
98
+ agentStdoutBuffer?: string | undefined;
99
+ attestationPath?: string | undefined;
100
+ attestedPi?: AttestedPiTaskFiles | undefined;
101
+ metadataWriteChain?: Promise<void> | undefined;
102
+ waiters: Array<() => void>;
103
+ }
104
+
105
+ export type CompletionDeliveryMode =
106
+ | 'notification-and-wake'
107
+ | 'notification-only'
108
+ | 'manual-monitoring';
109
+
110
+ export interface CompletionDeliveryGuidance {
111
+ readonly mode: CompletionDeliveryMode;
112
+ readonly notificationEnabled: boolean;
113
+ readonly automaticWakeEnabled: boolean;
114
+ readonly text: string;
115
+ }
116
+
117
+ /**
118
+ * Describe the actual parent-agent completion path for one bg_run launch.
119
+ * A wake request cannot take effect without the notification that carries it.
120
+ */
121
+ export function deriveCompletionDeliveryGuidance(
122
+ notifyOnCompletion: boolean,
123
+ triggerOnCompletion: boolean,
124
+ ): CompletionDeliveryGuidance {
125
+ if (notifyOnCompletion && triggerOnCompletion) {
126
+ return {
127
+ mode: 'notification-and-wake',
128
+ notificationEnabled: true,
129
+ automaticWakeEnabled: true,
130
+ text: [
131
+ 'Terminal notification: enabled.',
132
+ 'Automatic follow-up turn: enabled.',
133
+ 'Next action: do not poll or sleep merely to wait; continue only independent useful work, otherwise end this turn and wait for <background-task-notification>.',
134
+ ].join('\n'),
135
+ };
136
+ }
137
+
138
+ if (notifyOnCompletion) {
139
+ return {
140
+ mode: 'notification-only',
141
+ notificationEnabled: true,
142
+ automaticWakeEnabled: false,
143
+ text: [
144
+ 'Terminal notification: enabled.',
145
+ 'Automatic follow-up turn: disabled. The terminal notification will be delivered, but it will not start an agent turn.',
146
+ 'Next action: automatic wake-up was explicitly disabled; use bg_status/bg_logs only when deliberate monitoring is required, without tight polling.',
147
+ ].join('\n'),
148
+ };
149
+ }
150
+
151
+ return {
152
+ mode: 'manual-monitoring',
153
+ notificationEnabled: false,
154
+ automaticWakeEnabled: false,
155
+ text: [
156
+ 'Terminal notification: disabled.',
157
+ triggerOnCompletion
158
+ ? 'Automatic follow-up turn: disabled because terminal notifications are disabled. triggerOnCompletion has no effect while notifyOnCompletion is false.'
159
+ : 'Automatic follow-up turn: disabled.',
160
+ 'Next action: completion delivery was explicitly disabled; use bg_status/bg_logs only for deliberate manual monitoring, without tight polling.',
161
+ ].join('\n'),
162
+ };
163
+ }
164
+
165
+ export interface BgRunDetails {
166
+ task: BgTaskSnapshot;
167
+ }
168
+
169
+ export interface BgStatusDetails {
170
+ tasks: BgTaskSnapshot[];
171
+ }
172
+
173
+ export interface BgLogsDetails {
174
+ task: BgTaskSnapshot;
175
+ path: string;
176
+ bytesRead: number;
177
+ truncated: boolean;
178
+ tail: boolean;
179
+ }
180
+
181
+ export interface BgKillDetails {
182
+ task: BgTaskSnapshot;
183
+ message: string;
184
+ }
185
+
186
+ export interface StartTaskOptions {
187
+ name?: string | undefined;
188
+ description?: string | undefined;
189
+ isAgent?: boolean | undefined;
190
+ timeoutSeconds?: number | undefined;
191
+ notifyOnCompletion?: boolean | undefined;
192
+ triggerOnCompletion?: boolean | undefined;
193
+ /** @internal EventBus protocol barrier; callers should not set this outside the extension service. */
194
+ terminalPublicationGate?: Promise<void> | undefined;
195
+ }
196
+
197
+ export interface StartAttestedPiTaskOptions {
198
+ name: string;
199
+ provider: string;
200
+ model: string;
201
+ prompt: string;
202
+ reportPath: string;
203
+ extraPiArgs?: string[] | undefined;
204
+ thinking?: string | undefined;
205
+ timeoutSeconds?: number | undefined;
206
+ }
103
207
 
104
208
  export const DEFAULT_LOG_BYTES = Math.min(DEFAULT_MAX_BYTES, 50 * 1024);
105
209
  export const MAX_LOG_BYTES = Math.min(DEFAULT_MAX_BYTES, 50 * 1024);
106
210
  export const COMMAND_PREVIEW_CHARS = 90;
211
+ const parseJsonValue: (text: string) => unknown = globalThis.JSON.parse;
212
+
213
+ export function isJsonObject(value: unknown): value is JsonObject {
214
+ return typeof value === 'object' && value !== null;
215
+ }
216
+
217
+ export function parseJsonText(text: string): unknown {
218
+ return parseJsonValue(text);
219
+ }
107
220
 
108
221
  export function sanitizePathSegment(value: string): string {
109
- const sanitized = value.replace(/[^a-zA-Z0-9_.-]+/g, "-").replace(/^-+|-+$/g, "");
110
- return sanitized || "session";
222
+ const sanitized = value.replace(/[^a-zA-Z0-9_.-]+/g, '-').replace(/^-+|-+$/g, '');
223
+ return sanitized || 'session';
111
224
  }
112
225
 
113
226
  export function stripMatchingQuotes(value: string): string {
114
- const trimmed = value.trim();
115
- if (trimmed.length >= 2) {
116
- const first = trimmed[0];
117
- const last = trimmed[trimmed.length - 1];
118
- if ((first === '"' && last === '"') || (first === "'" && last === "'")) {
119
- return trimmed.slice(1, -1);
120
- }
121
- }
122
- return trimmed;
227
+ const trimmed = value.trim();
228
+ if (trimmed.length >= 2) {
229
+ const first = trimmed[0];
230
+ const last = trimmed[trimmed.length - 1];
231
+ if ((first === '"' && last === '"') || (first === "'" && last === "'")) {
232
+ return trimmed.slice(1, -1);
233
+ }
234
+ }
235
+ return trimmed;
123
236
  }
124
237
 
125
238
  export function compactWhitespace(value: string): string {
126
- return value.replace(/\s+/g, " ").trim();
239
+ return value.replace(/\s+/g, ' ').trim();
127
240
  }
128
241
 
129
242
  export function truncateChars(value: string, maxChars: number): string {
130
- if (value.length <= maxChars) return value;
131
- return `${value.slice(0, Math.max(0, maxChars - 1))}…`;
243
+ if (value.length <= maxChars) return value;
244
+ return `${value.slice(0, Math.max(0, maxChars - 1))}…`;
132
245
  }
133
246
 
134
247
  export function normalizeTaskName(value: unknown): string | undefined {
135
- if (typeof value !== "string") return undefined;
136
- const normalized = compactWhitespace(stripMatchingQuotes(value));
137
- if (!normalized) return undefined;
138
- return truncateChars(normalized, 80);
248
+ if (typeof value !== 'string') return undefined;
249
+ const normalized = compactWhitespace(stripMatchingQuotes(value));
250
+ if (!normalized) return undefined;
251
+ return truncateChars(normalized, 80);
139
252
  }
140
253
 
141
254
  export function deriveTaskNameFromCommand(command: string): string {
142
- const normalized = compactWhitespace(stripMatchingQuotes(command));
143
- if (!normalized) return "Background task";
144
-
145
- const packageScript = normalized.match(/^(npm|pnpm|yarn|bun)\s+(?:(run)\s+)?([^\s;&|]+)/);
146
- if (packageScript) {
147
- const runner = packageScript[1];
148
- const run = packageScript[2] ? " run" : "";
149
- const script = packageScript[3];
150
- return truncateChars(`${runner}${run} ${script}`, 48);
151
- }
152
-
153
- const words = normalized.split(/\s+/).slice(0, 5).join(" ");
154
- return truncateChars(words || normalized, 48);
255
+ const normalized = compactWhitespace(stripMatchingQuotes(command));
256
+ if (!normalized) return 'Background task';
257
+
258
+ const packageScript = /^(npm|pnpm|yarn|bun)\s+(?:(run)\s+)?([^\s;&|]+)/.exec(normalized);
259
+ if (packageScript) {
260
+ const runner = packageScript[1] ?? 'npm';
261
+ const run = packageScript[2] !== undefined ? ' run' : '';
262
+ const script = packageScript[3] ?? '';
263
+ return truncateChars(`${runner}${run} ${script}`, 48);
264
+ }
265
+
266
+ const words = normalized.split(/\s+/).slice(0, 5).join(' ');
267
+ return truncateChars(words.length > 0 ? words : normalized, 48);
155
268
  }
156
269
 
157
- export function taskDisplayName(task: { name?: string | undefined; description?: string | undefined; command?: string | undefined; id?: string | undefined }): string {
158
- return normalizeTaskName(task.name)
159
- ?? normalizeTaskName(task.description)
160
- ?? (task.command ? deriveTaskNameFromCommand(task.command) : undefined)
161
- ?? task.id
162
- ?? "Background task";
270
+ export function taskDisplayName(task: {
271
+ name?: string | undefined;
272
+ description?: string | undefined;
273
+ command?: string | undefined;
274
+ id?: string | undefined;
275
+ }): string {
276
+ const commandName =
277
+ task.command && task.command.length > 0 ? deriveTaskNameFromCommand(task.command) : undefined;
278
+ return (
279
+ normalizeTaskName(task.name) ??
280
+ normalizeTaskName(task.description) ??
281
+ commandName ??
282
+ task.id ??
283
+ 'Background task'
284
+ );
163
285
  }
164
286
 
165
287
  function parseNameValueAndRest(valueAndRest: string): { value: string; rest: string } | undefined {
166
- const input = valueAndRest.trimStart();
167
- if (!input) return undefined;
168
- const quote = input[0];
169
- if (quote === '"' || quote === "'") {
170
- let escaped = false;
171
- let value = "";
172
- for (let i = 1; i < input.length; i++) {
173
- const char = input.charAt(i);
174
- if (escaped) {
175
- value += char;
176
- escaped = false;
177
- continue;
178
- }
179
- if (char === "\\") {
180
- escaped = true;
181
- continue;
182
- }
183
- if (char === quote) {
184
- return { value, rest: input.slice(i + 1).trimStart() };
185
- }
186
- value += char;
187
- }
188
- return undefined;
189
- }
190
- const match = input.match(/^(\S+)(?:\s+([\s\S]*))?$/);
191
- if (!match) return undefined;
192
- const parsedValue = match[1];
193
- if (parsedValue === undefined) return undefined;
194
- return { value: parsedValue, rest: match[2]?.trimStart() ?? "" };
195
- }
196
-
197
- export function parseBgCommandArgs(args: string): { name?: string; command: string; isAgent: boolean } {
198
- let input = args.trim();
199
- let name: string | undefined;
200
- let isAgent = false;
201
-
202
- while (input) {
203
- let consumed = false;
204
- for (const prefix of ["--name=", "-n="]) {
205
- if (input.startsWith(prefix)) {
206
- const parsed = parseNameValueAndRest(input.slice(prefix.length));
207
- if (!parsed) throw new Error(`${prefix.slice(0, -1)} requires a task name`);
208
- name = normalizeTaskName(parsed.value);
209
- input = parsed.rest;
210
- consumed = true;
211
- break;
212
- }
213
- }
214
- if (consumed) continue;
215
-
216
- for (const prefix of ["--name", "-n"]) {
217
- if (input === prefix || input.startsWith(`${prefix} `) || input.startsWith(`${prefix}\t`)) {
218
- const parsed = parseNameValueAndRest(input.slice(prefix.length));
219
- if (!parsed) throw new Error(`${prefix} requires a task name`);
220
- name = normalizeTaskName(parsed.value);
221
- input = parsed.rest;
222
- consumed = true;
223
- break;
224
- }
225
- }
226
- if (consumed) continue;
227
-
228
- for (const flag of ["--agent", "--llm-agent"]) {
229
- if (input === flag || input.startsWith(`${flag} `) || input.startsWith(`${flag}\t`)) {
230
- isAgent = true;
231
- input = input.slice(flag.length).trimStart();
232
- consumed = true;
233
- break;
234
- }
235
- }
236
- if (consumed) continue;
237
-
238
- for (const flag of ["--script", "--no-agent"]) {
239
- if (input === flag || input.startsWith(`${flag} `) || input.startsWith(`${flag}\t`)) {
240
- isAgent = false;
241
- input = input.slice(flag.length).trimStart();
242
- consumed = true;
243
- break;
244
- }
245
- }
246
- if (consumed) continue;
247
-
248
- if (input === "--") {
249
- input = "";
250
- break;
251
- }
252
- if (input.startsWith("-- ")) {
253
- input = input.slice(3).trimStart();
254
- break;
255
- }
256
- break;
257
- }
258
-
259
- return name ? { name, command: input, isAgent } : { command: input, isAgent };
288
+ const input = valueAndRest.trimStart();
289
+ if (!input) return undefined;
290
+ const quote = input[0];
291
+ if (quote === '"' || quote === "'") {
292
+ let escaped = false;
293
+ let value = '';
294
+ for (let i = 1; i < input.length; i++) {
295
+ const char = input.charAt(i);
296
+ if (escaped) {
297
+ value += char;
298
+ escaped = false;
299
+ continue;
300
+ }
301
+ if (char === '\\') {
302
+ escaped = true;
303
+ continue;
304
+ }
305
+ if (char === quote) {
306
+ return { value, rest: input.slice(i + 1).trimStart() };
307
+ }
308
+ value += char;
309
+ }
310
+ return undefined;
311
+ }
312
+ const match = /^(\S+)(?:\s+([\s\S]*))?$/.exec(input);
313
+ if (!match) return undefined;
314
+ const parsedValue = match[1];
315
+ if (parsedValue === undefined) return undefined;
316
+ return { value: parsedValue, rest: match[2]?.trimStart() ?? '' };
317
+ }
318
+
319
+ export function parseBgCommandArgs(args: string): {
320
+ name?: string;
321
+ command: string;
322
+ isAgent: boolean;
323
+ } {
324
+ let input = args.trim();
325
+ let name: string | undefined;
326
+ let isAgent = false;
327
+
328
+ while (input) {
329
+ let consumed = false;
330
+ for (const prefix of ['--name=', '-n=']) {
331
+ if (input.startsWith(prefix)) {
332
+ const parsed = parseNameValueAndRest(input.slice(prefix.length));
333
+ if (!parsed) throw new Error(`${prefix.slice(0, -1)} requires a task name`);
334
+ name = normalizeTaskName(parsed.value);
335
+ input = parsed.rest;
336
+ consumed = true;
337
+ break;
338
+ }
339
+ }
340
+ if (consumed) continue;
341
+
342
+ for (const prefix of ['--name', '-n']) {
343
+ if (input === prefix || input.startsWith(`${prefix} `) || input.startsWith(`${prefix}\t`)) {
344
+ const parsed = parseNameValueAndRest(input.slice(prefix.length));
345
+ if (!parsed) throw new Error(`${prefix} requires a task name`);
346
+ name = normalizeTaskName(parsed.value);
347
+ input = parsed.rest;
348
+ consumed = true;
349
+ break;
350
+ }
351
+ }
352
+ if (consumed) continue;
353
+
354
+ for (const flag of ['--agent', '--llm-agent']) {
355
+ if (input === flag || input.startsWith(`${flag} `) || input.startsWith(`${flag}\t`)) {
356
+ isAgent = true;
357
+ input = input.slice(flag.length).trimStart();
358
+ consumed = true;
359
+ break;
360
+ }
361
+ }
362
+ if (consumed) continue;
363
+
364
+ for (const flag of ['--script', '--no-agent']) {
365
+ if (input === flag || input.startsWith(`${flag} `) || input.startsWith(`${flag}\t`)) {
366
+ isAgent = false;
367
+ input = input.slice(flag.length).trimStart();
368
+ consumed = true;
369
+ break;
370
+ }
371
+ }
372
+ if (consumed) continue;
373
+
374
+ if (input === '--') {
375
+ input = '';
376
+ break;
377
+ }
378
+ if (input.startsWith('-- ')) {
379
+ input = input.slice(3).trimStart();
380
+ break;
381
+ }
382
+ break;
383
+ }
384
+
385
+ return name ? { name, command: input, isAgent } : { command: input, isAgent };
260
386
  }
261
387
 
262
388
  export function formatDuration(ms: number): string {
263
- if (ms < 1000) return `${ms}ms`;
264
- const seconds = Math.floor(ms / 1000);
265
- if (seconds < 60) return `${seconds}s`;
266
- const minutes = Math.floor(seconds / 60);
267
- const remSeconds = seconds % 60;
268
- if (minutes < 60) return `${minutes}m${remSeconds ? `${remSeconds}s` : ""}`;
269
- const hours = Math.floor(minutes / 60);
270
- const remMinutes = minutes % 60;
271
- return `${hours}h${remMinutes ? `${remMinutes}m` : ""}`;
389
+ if (ms < 1000) return `${String(ms)}ms`;
390
+ const seconds = Math.floor(ms / 1000);
391
+ if (seconds < 60) return `${String(seconds)}s`;
392
+ const minutes = Math.floor(seconds / 60);
393
+ const remSeconds = seconds % 60;
394
+ if (minutes < 60) return `${String(minutes)}m${remSeconds > 0 ? `${String(remSeconds)}s` : ''}`;
395
+ const hours = Math.floor(minutes / 60);
396
+ const remMinutes = minutes % 60;
397
+ return `${String(hours)}h${remMinutes > 0 ? `${String(remMinutes)}m` : ''}`;
272
398
  }
273
399
 
274
400
  export function formatCompactNumber(count: number): string {
275
- const normalized = Math.max(0, Math.floor(count));
276
- if (normalized < 1000) return normalized.toString();
277
- if (normalized < 10000) return `${(normalized / 1000).toFixed(1)}k`;
278
- if (normalized < 1000000) return `${Math.round(normalized / 1000)}k`;
279
- if (normalized < 10000000) return `${(normalized / 1000000).toFixed(1)}M`;
280
- return `${Math.round(normalized / 1000000)}M`;
401
+ const normalized = Math.max(0, Math.floor(count));
402
+ if (normalized < 1000) return normalized.toString();
403
+ if (normalized < 10000) return `${(normalized / 1000).toFixed(1)}k`;
404
+ if (normalized < 1000000) return `${String(Math.round(normalized / 1000))}k`;
405
+ if (normalized < 10000000) return `${(normalized / 1000000).toFixed(1)}M`;
406
+ return `${String(Math.round(normalized / 1000000))}M`;
281
407
  }
282
408
 
283
409
  export function formatContextUsageSummary(usage?: TaskContextUsage): string | undefined {
284
- if (!usage || !usage.contextWindow) return undefined;
285
- const window = formatCompactNumber(usage.contextWindow);
286
- if (usage.percent === null || usage.tokens === null) return `ctx=?/${window}`;
287
- return `ctx=${usage.percent.toFixed(1)}%/${window}`;
410
+ if (usage?.contextWindow === undefined || usage.contextWindow <= 0) return undefined;
411
+ const window = formatCompactNumber(usage.contextWindow);
412
+ if (usage.percent === null || usage.tokens === null) return `ctx=?/${window}`;
413
+ return `ctx=${usage.percent.toFixed(1)}%/${window}`;
288
414
  }
289
415
 
290
416
  export function formatTokenUsageSummary(usage?: TaskTokenUsage): string | undefined {
291
- if (!usage || usage.totalTokens <= 0) return undefined;
292
- return `tokens=${formatCompactNumber(usage.totalTokens)}`;
417
+ if (!usage || usage.totalTokens <= 0) return undefined;
418
+ return `tokens=${formatCompactNumber(usage.totalTokens)}`;
293
419
  }
294
420
 
295
421
  export function formatToolUsageSummary(usage?: TaskToolUsage): string | undefined {
296
- if (!usage || (usage.total <= 0 && usage.failed <= 0)) return undefined;
297
- const failed = usage.failed > 0 ? ` failed=${usage.failed}` : "";
298
- return `tools=${usage.total}${failed}`;
422
+ if (!usage || (usage.total <= 0 && usage.failed <= 0)) return undefined;
423
+ const failed = usage.failed > 0 ? ` failed=${String(usage.failed)}` : '';
424
+ return `tools=${String(usage.total)}${failed}`;
299
425
  }
300
426
 
301
427
  export function formatModelSummary(model?: string): string | undefined {
302
- if (!model) return undefined;
303
- return `model=${model}`;
428
+ if (!model) return undefined;
429
+ return `model=${model}`;
304
430
  }
305
431
 
306
432
  /**
@@ -312,45 +438,58 @@ export function formatModelSummary(model?: string): string | undefined {
312
438
  * file instead of leaking raw telemetry JSON. Both the parser and the formatter
313
439
  * are pure so the visible transcript is fully unit-testable.
314
440
  */
315
- export const AGENT_ACTIVITY_TYPE = "background-task-activity";
441
+ export const AGENT_ACTIVITY_TYPE = 'background-task-activity';
316
442
  const AGENT_ACTIVITY_DETAIL_MAX = 80;
317
443
 
318
444
  export type AgentActivity =
319
- | { kind: "assistant_text"; text: string }
320
- | { kind: "reasoning"; text: string }
321
- | { kind: "tool_start"; tool: string; argsSummary: string }
322
- | { kind: "tool_end"; tool: string; isError: boolean; error?: string };
445
+ | { kind: 'assistant_text'; text: string }
446
+ | { kind: 'reasoning'; text: string }
447
+ | { kind: 'tool_start'; tool: string; argsSummary: string }
448
+ | { kind: 'tool_end'; tool: string; isError: boolean; error?: string };
449
+
450
+ interface AgentActivityPayload extends JsonObject {
451
+ readonly type?: unknown;
452
+ readonly kind?: unknown;
453
+ readonly text?: unknown;
454
+ readonly tool?: unknown;
455
+ readonly argsSummary?: unknown;
456
+ readonly isError?: unknown;
457
+ readonly error?: unknown;
458
+ }
323
459
 
324
- function readActivityString(record: Record<string, unknown>, key: string): string | undefined {
325
- const value = record[key];
326
- return typeof value === "string" ? value : undefined;
460
+ function readActivityString(
461
+ record: AgentActivityPayload,
462
+ key: 'text' | 'tool' | 'argsSummary' | 'error',
463
+ ): string | undefined {
464
+ const value = record[key];
465
+ return typeof value === 'string' ? value : undefined;
327
466
  }
328
467
 
329
468
  /** Narrow a parsed `background-task-activity` control payload into a typed {@link AgentActivity}. */
330
469
  export function parseAgentActivity(payload: unknown): AgentActivity | undefined {
331
- if (typeof payload !== "object" || payload === null) return undefined;
332
- const record = payload as Record<string, unknown>;
333
- if (record["type"] !== AGENT_ACTIVITY_TYPE) return undefined;
334
- const kind = record["kind"];
335
- if (kind === "assistant_text" || kind === "reasoning") {
336
- const text = readActivityString(record, "text");
337
- if (text === undefined) return undefined;
338
- return { kind, text };
339
- }
340
- if (kind === "tool_start") {
341
- const tool = readActivityString(record, "tool");
342
- if (!tool) return undefined;
343
- return { kind, tool, argsSummary: readActivityString(record, "argsSummary") ?? "" };
344
- }
345
- if (kind === "tool_end") {
346
- const tool = readActivityString(record, "tool");
347
- if (!tool) return undefined;
348
- const activity: AgentActivity = { kind, tool, isError: record["isError"] === true };
349
- const error = readActivityString(record, "error");
350
- if (error !== undefined && error.trim().length > 0) activity.error = error;
351
- return activity;
352
- }
353
- return undefined;
470
+ if (!isJsonObject(payload)) return undefined;
471
+ const record: AgentActivityPayload = payload;
472
+ if (record.type !== AGENT_ACTIVITY_TYPE) return undefined;
473
+ const kind = record.kind;
474
+ if (kind === 'assistant_text' || kind === 'reasoning') {
475
+ const text = readActivityString(record, 'text');
476
+ if (text === undefined) return undefined;
477
+ return { kind, text };
478
+ }
479
+ if (kind === 'tool_start') {
480
+ const tool = readActivityString(record, 'tool');
481
+ if (!tool) return undefined;
482
+ return { kind, tool, argsSummary: readActivityString(record, 'argsSummary') ?? '' };
483
+ }
484
+ if (kind === 'tool_end') {
485
+ const tool = readActivityString(record, 'tool');
486
+ if (!tool) return undefined;
487
+ const activity: AgentActivity = { kind, tool, isError: record.isError === true };
488
+ const error = readActivityString(record, 'error');
489
+ if (error !== undefined && error.trim().length > 0) activity.error = error;
490
+ return activity;
491
+ }
492
+ return undefined;
354
493
  }
355
494
 
356
495
  /**
@@ -360,189 +499,217 @@ export function parseAgentActivity(payload: unknown): AgentActivity | undefined
360
499
  * line already announced the call, and the next line implies completion.
361
500
  */
362
501
  export function formatAgentActivityLine(activity: AgentActivity): string | undefined {
363
- if (activity.kind === "assistant_text") {
364
- const text = activity.text.replace(/\s+$/u, "");
365
- return text.trim().length > 0 ? text : undefined;
366
- }
367
- if (activity.kind === "reasoning") {
368
- const text = activity.text.replace(/\s+$/u, "");
369
- return text.trim().length > 0 ? `\u2026 ${text}` : undefined;
370
- }
371
- if (activity.kind === "tool_start") {
372
- const summary = compactWhitespace(activity.argsSummary);
373
- const suffix = summary.length > 0 ? ` ${truncateChars(summary, AGENT_ACTIVITY_DETAIL_MAX)}` : "";
374
- return `\u2192 ${activity.tool}${suffix}`;
375
- }
376
- if (!activity.isError) return undefined;
377
- const detail = activity.error ? `: ${truncateChars(compactWhitespace(activity.error), AGENT_ACTIVITY_DETAIL_MAX)}` : "";
378
- return `\u2717 ${activity.tool} failed${detail}`;
502
+ if (activity.kind === 'assistant_text') {
503
+ const text = activity.text.replace(/\s+$/u, '');
504
+ return text.trim().length > 0 ? text : undefined;
505
+ }
506
+ if (activity.kind === 'reasoning') {
507
+ const text = activity.text.replace(/\s+$/u, '');
508
+ return text.trim().length > 0 ? `\u2026 ${text}` : undefined;
509
+ }
510
+ if (activity.kind === 'tool_start') {
511
+ const summary = compactWhitespace(activity.argsSummary);
512
+ const suffix =
513
+ summary.length > 0 ? ` ${truncateChars(summary, AGENT_ACTIVITY_DETAIL_MAX)}` : '';
514
+ return `\u2192 ${activity.tool}${suffix}`;
515
+ }
516
+ if (!activity.isError) return undefined;
517
+ const detail = activity.error
518
+ ? `: ${truncateChars(compactWhitespace(activity.error), AGENT_ACTIVITY_DETAIL_MAX)}`
519
+ : '';
520
+ return `\u2717 ${activity.tool} failed${detail}`;
379
521
  }
380
522
 
381
523
  export function shellQuote(value: string): string {
382
- return `'${value.replace(/'/g, `'"'"'`)}'`;
524
+ return `'${value.replace(/'/g, `'"'"'`)}'`;
383
525
  }
384
526
 
385
527
  export function shellInvocation(
386
- command: string,
387
- platform: NodeJS.Platform = process.platform,
388
- env: NodeJS.ProcessEnv = process.env,
528
+ command: string,
529
+ platform: NodeJS.Platform = process.platform,
530
+ env: NodeJS.ProcessEnv = process.env,
389
531
  ): { shell: string; args: string[] } {
390
- if (platform === "win32") {
391
- return { shell: env["ComSpec"] || "cmd.exe", args: ["/d", "/s", "/c", command] };
392
- }
393
- return { shell: env["SHELL"] || "/bin/sh", args: ["-c", command] };
532
+ if (platform === 'win32') {
533
+ const comSpec = env['ComSpec'];
534
+ return {
535
+ shell: comSpec && comSpec.length > 0 ? comSpec : 'cmd.exe',
536
+ args: ['/d', '/s', '/c', command],
537
+ };
538
+ }
539
+ const shell = env['SHELL'];
540
+ return { shell: shell && shell.length > 0 ? shell : '/bin/sh', args: ['-c', command] };
394
541
  }
395
542
 
396
543
  export function normalizeMaxBytes(value: unknown, fallback = DEFAULT_LOG_BYTES): number {
397
- const raw = typeof value === "number" && Number.isFinite(value) ? Math.floor(value) : fallback;
398
- return Math.max(1, Math.min(MAX_LOG_BYTES, raw));
544
+ const raw = typeof value === 'number' && Number.isFinite(value) ? Math.floor(value) : fallback;
545
+ return Math.max(1, Math.min(MAX_LOG_BYTES, raw));
399
546
  }
400
547
 
401
548
  export function snapshot(task: BgTask): BgTaskSnapshot {
402
- return {
403
- id: task.id,
404
- name: taskDisplayName(task),
405
- command: task.command,
406
- description: task.description,
407
- status: task.status,
408
- outputPath: task.outputPath,
409
- cwd: task.cwd,
410
- startTime: task.startTime,
411
- endTime: task.endTime,
412
- exitCode: task.exitCode,
413
- signal: task.signal,
414
- pid: task.pid,
415
- bytesWritten: task.bytesWritten,
416
- isAgent: task.isAgent,
417
- error: task.error,
418
- notified: task.notified,
419
- notifyOnCompletion: task.notifyOnCompletion,
420
- triggerOnCompletion: task.triggerOnCompletion,
421
- timeoutSeconds: task.timeoutSeconds,
422
- contextUsage: task.contextUsage,
423
- tokenUsage: task.tokenUsage,
424
- toolUsage: task.toolUsage,
425
- model: task.model,
426
- };
549
+ return {
550
+ id: task.id,
551
+ name: taskDisplayName(task),
552
+ command: task.command,
553
+ description: task.description,
554
+ status: task.status,
555
+ outputPath: task.outputPath,
556
+ cwd: task.cwd,
557
+ startTime: task.startTime,
558
+ endTime: task.endTime,
559
+ exitCode: task.exitCode,
560
+ signal: task.signal,
561
+ pid: task.pid,
562
+ bytesWritten: task.bytesWritten,
563
+ isAgent: task.isAgent,
564
+ error: task.error,
565
+ notified: task.notified,
566
+ notifyOnCompletion: task.notifyOnCompletion,
567
+ triggerOnCompletion: task.triggerOnCompletion,
568
+ timeoutSeconds: task.timeoutSeconds,
569
+ contextUsage: task.contextUsage,
570
+ tokenUsage: task.tokenUsage,
571
+ toolUsage: task.toolUsage,
572
+ model: task.model,
573
+ attestationPath: task.attestationPath,
574
+ };
427
575
  }
428
576
 
429
577
  export function formatSnapshotList(tasks: BgTaskSnapshot[], now = Date.now()): string {
430
- if (tasks.length === 0) return "No background tasks in this Pi extension runtime.";
431
- return tasks.map((task) => {
432
- const statusIcon = task.status === "running" ? "▶" : task.status === "completed" ? "✓" : task.status === "killed" ? "■" : "✗";
433
- const age = formatDuration((task.endTime ?? now) - task.startTime);
434
- const code = task.exitCode !== undefined ? ` exit=${task.exitCode}` : "";
435
- const pid = task.pid ? ` pid=${task.pid}` : "";
436
- const error = task.error ? ` error=${truncateChars(task.error, 80)}` : "";
437
- const telemetry = [
438
- formatContextUsageSummary(task.contextUsage),
439
- formatModelSummary(task.model),
440
- formatTokenUsageSummary(task.tokenUsage),
441
- formatToolUsageSummary(task.toolUsage),
442
- ].filter(Boolean).join(" ");
443
- const telemetryText = telemetry ? ` ${telemetry}` : "";
444
- return `${statusIcon} ${task.id} ${task.status} ${age}${code}${pid}${telemetryText} ${truncateChars(taskDisplayName(task), COMMAND_PREVIEW_CHARS)}${error}\n output: ${task.outputPath}`;
445
- }).join("\n");
578
+ if (tasks.length === 0) return 'No background tasks in this Pi extension runtime.';
579
+ return tasks
580
+ .map((task) => {
581
+ const statusIcon =
582
+ task.status === 'running'
583
+ ? '▶'
584
+ : task.status === 'completed'
585
+ ? '✓'
586
+ : task.status === 'killed'
587
+ ? '■'
588
+ : '✗';
589
+ const age = formatDuration((task.endTime ?? now) - task.startTime);
590
+ const code = task.exitCode !== undefined ? ` exit=${String(task.exitCode)}` : '';
591
+ const pid = task.pid !== undefined ? ` pid=${String(task.pid)}` : '';
592
+ const error = task.error ? ` error=${truncateChars(task.error, 80)}` : '';
593
+ const telemetry = [
594
+ formatContextUsageSummary(task.contextUsage),
595
+ formatModelSummary(task.model),
596
+ formatTokenUsageSummary(task.tokenUsage),
597
+ formatToolUsageSummary(task.toolUsage),
598
+ ]
599
+ .filter(Boolean)
600
+ .join(' ');
601
+ const telemetryText = telemetry ? ` ${telemetry}` : '';
602
+ return `${statusIcon} ${task.id} ${task.status} ${age}${code}${pid}${telemetryText} — ${truncateChars(taskDisplayName(task), COMMAND_PREVIEW_CHARS)}${error}\n output: ${task.outputPath}`;
603
+ })
604
+ .join('\n');
446
605
  }
447
606
 
448
607
  export async function boundedRead(
449
- filePath: string,
450
- maxBytes: number,
451
- tail: boolean,
608
+ filePath: string,
609
+ maxBytes: number,
610
+ tail: boolean,
452
611
  ): Promise<{ content: string; truncated: boolean; bytesRead: number; totalBytes: number }> {
453
- const stats = statSync(filePath);
454
- const totalBytes = stats.size;
455
- const bytesToRead = Math.min(totalBytes, maxBytes);
456
- if (bytesToRead === 0) return { content: "", truncated: false, bytesRead: 0, totalBytes };
457
-
458
- const file = await open(filePath, "r");
459
- try {
460
- const buffer = Buffer.alloc(bytesToRead);
461
- const position = tail ? Math.max(0, totalBytes - bytesToRead) : 0;
462
- const { bytesRead } = await file.read(buffer, 0, bytesToRead, position);
463
- return {
464
- content: buffer.subarray(0, bytesRead).toString("utf8"),
465
- truncated: totalBytes > bytesRead,
466
- bytesRead,
467
- totalBytes,
468
- };
469
- } finally {
470
- await file.close();
471
- }
612
+ const stats = statSync(filePath);
613
+ const totalBytes = stats.size;
614
+ const bytesToRead = Math.min(totalBytes, maxBytes);
615
+ if (bytesToRead === 0) return { content: '', truncated: false, bytesRead: 0, totalBytes };
616
+
617
+ const file = await open(filePath, 'r');
618
+ try {
619
+ const buffer = Buffer.alloc(bytesToRead);
620
+ const position = tail ? Math.max(0, totalBytes - bytesToRead) : 0;
621
+ const { bytesRead } = await file.read(buffer, 0, bytesToRead, position);
622
+ return {
623
+ content: buffer.subarray(0, bytesRead).toString('utf8'),
624
+ truncated: totalBytes > bytesRead,
625
+ bytesRead,
626
+ totalBytes,
627
+ };
628
+ } finally {
629
+ await file.close();
630
+ }
472
631
  }
473
632
 
474
633
  export function escapeXml(value: string): string {
475
- return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
634
+ return value.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
476
635
  }
477
636
 
478
- export const UPDATE_COMMAND = "/bg-update";
637
+ export const UPDATE_COMMAND = '/bg-update';
479
638
 
480
- type ParsedSemver = {
481
- major: number;
482
- minor: number;
483
- patch: number;
484
- prerelease: string[];
485
- };
639
+ interface ParsedSemver {
640
+ major: number;
641
+ minor: number;
642
+ patch: number;
643
+ prerelease: string[];
644
+ }
486
645
 
487
646
  const SEMVER_PATTERN = /^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/;
488
647
 
489
648
  export function parseSemver(value: string): ParsedSemver | undefined {
490
- if (typeof value !== "string") return undefined;
491
- const match = value.trim().match(SEMVER_PATTERN);
492
- if (!match) return undefined;
493
- const major = Number(match[1]);
494
- const minor = Number(match[2]);
495
- const patch = Number(match[3]);
496
- if (!Number.isInteger(major) || !Number.isInteger(minor) || !Number.isInteger(patch)) return undefined;
497
- const prerelease = match[4] ? match[4].split(".") : [];
498
- return { major, minor, patch, prerelease };
649
+ if (typeof value !== 'string') return undefined;
650
+ const match = SEMVER_PATTERN.exec(value.trim());
651
+ if (!match) return undefined;
652
+ const majorRaw = match[1];
653
+ const minorRaw = match[2];
654
+ const patchRaw = match[3];
655
+ if (majorRaw === undefined || minorRaw === undefined || patchRaw === undefined) return undefined;
656
+ const major = Number(majorRaw);
657
+ const minor = Number(minorRaw);
658
+ const patch = Number(patchRaw);
659
+ if (!Number.isInteger(major) || !Number.isInteger(minor) || !Number.isInteger(patch))
660
+ return undefined;
661
+ const prerelease = match[4] !== undefined ? match[4].split('.') : [];
662
+ return { major, minor, patch, prerelease };
499
663
  }
500
664
 
501
665
  function comparePrerelease(a: string[], b: string[]): number {
502
- if (a.length === 0 && b.length === 0) return 0;
503
- // A version without prerelease identifiers outranks the same core with prerelease identifiers.
504
- if (a.length === 0) return 1;
505
- if (b.length === 0) return -1;
506
- const shared = Math.min(a.length, b.length);
507
- for (let i = 0; i < shared; i++) {
508
- const idA = a[i];
509
- const idB = b[i];
510
- if (idA === undefined || idB === undefined) break;
511
- if (idA === idB) continue;
512
- const numericA = /^\d+$/.test(idA);
513
- const numericB = /^\d+$/.test(idB);
514
- if (numericA && numericB) {
515
- const diff = Number(idA) - Number(idB);
516
- if (diff !== 0) return diff < 0 ? -1 : 1;
517
- continue;
518
- }
519
- // Numeric identifiers always have lower precedence than non-numeric identifiers.
520
- if (numericA) return -1;
521
- if (numericB) return 1;
522
- return idA < idB ? -1 : 1;
523
- }
524
- if (a.length === b.length) return 0;
525
- return a.length < b.length ? -1 : 1;
666
+ if (a.length === 0 && b.length === 0) return 0;
667
+ // A version without prerelease identifiers outranks the same core with prerelease identifiers.
668
+ if (a.length === 0) return 1;
669
+ if (b.length === 0) return -1;
670
+ const shared = Math.min(a.length, b.length);
671
+ for (let i = 0; i < shared; i++) {
672
+ const idA = a[i];
673
+ const idB = b[i];
674
+ if (idA === undefined || idB === undefined) break;
675
+ if (idA === idB) continue;
676
+ const numericA = /^\d+$/.test(idA);
677
+ const numericB = /^\d+$/.test(idB);
678
+ if (numericA && numericB) {
679
+ const diff = Number(idA) - Number(idB);
680
+ if (diff !== 0) return diff < 0 ? -1 : 1;
681
+ continue;
682
+ }
683
+ // Numeric identifiers always have lower precedence than non-numeric identifiers.
684
+ if (numericA) return -1;
685
+ if (numericB) return 1;
686
+ return idA < idB ? -1 : 1;
687
+ }
688
+ if (a.length === b.length) return 0;
689
+ return a.length < b.length ? -1 : 1;
526
690
  }
527
691
 
528
692
  /** Compare two semver strings. Returns -1/0/1, or undefined when either side is not valid semver. */
529
693
  export function compareSemver(a: string, b: string): number | undefined {
530
- const left = parseSemver(a);
531
- const right = parseSemver(b);
532
- if (!left || !right) return undefined;
533
- if (left.major !== right.major) return left.major < right.major ? -1 : 1;
534
- if (left.minor !== right.minor) return left.minor < right.minor ? -1 : 1;
535
- if (left.patch !== right.patch) return left.patch < right.patch ? -1 : 1;
536
- return comparePrerelease(left.prerelease, right.prerelease);
694
+ const left = parseSemver(a);
695
+ const right = parseSemver(b);
696
+ if (!left || !right) return undefined;
697
+ if (left.major !== right.major) return left.major < right.major ? -1 : 1;
698
+ if (left.minor !== right.minor) return left.minor < right.minor ? -1 : 1;
699
+ if (left.patch !== right.patch) return left.patch < right.patch ? -1 : 1;
700
+ return comparePrerelease(left.prerelease, right.prerelease);
537
701
  }
538
702
 
539
703
  export function isNewerVersion(latest: string, current: string): boolean {
540
- return compareSemver(latest, current) === 1;
704
+ return compareSemver(latest, current) === 1;
541
705
  }
542
706
 
543
707
  /** Footer segment shown only when a newer published version exists; undefined otherwise. */
544
- export function formatUpdateSegment(latest: string | undefined, current: string): string | undefined {
545
- if (!latest) return undefined;
546
- if (!isNewerVersion(latest, current)) return undefined;
547
- return `\u2b06 v${latest} ${UPDATE_COMMAND}`;
708
+ export function formatUpdateSegment(
709
+ latest: string | undefined,
710
+ current: string,
711
+ ): string | undefined {
712
+ if (!latest) return undefined;
713
+ if (!isNewerVersion(latest, current)) return undefined;
714
+ return `\u2b06 v${latest} ${UPDATE_COMMAND}`;
548
715
  }