pi-better-background-tasks 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/tools.ts ADDED
@@ -0,0 +1,476 @@
1
+ import { Type } from "typebox";
2
+ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
3
+ import { readLog } from "./logs.js";
4
+ import { refreshBackgroundTasksNavigator } from "./navigator-provider.js";
5
+ import { listMetas, readMeta, writeMeta } from "./registry.js";
6
+ import { resumeRunningTask, spawnTask, startWatchTask, stopTask } from "./runtime.js";
7
+ import type { BackgroundTaskCallbackOrigin, BackgroundTaskMeta } from "./types.js";
8
+ import { isTerminalStatus } from "./types.js";
9
+
10
+ const ConditionSchema = Type.Union([
11
+ Type.Object({ type: Type.Literal("exit_code"), equals: Type.Number() }),
12
+ Type.Object({ type: Type.Literal("stdout_contains"), value: Type.String() }),
13
+ Type.Object({ type: Type.Literal("stderr_contains"), value: Type.String() }),
14
+ Type.Object({ type: Type.Literal("json_path_equals"), path: Type.String(), value: Type.Any() }),
15
+ Type.Object({ type: Type.Literal("json_path_exists"), path: Type.String() }),
16
+ ]);
17
+
18
+ const CommandFields = {
19
+ name: Type.Optional(Type.String({ description: "Human-readable task label." })),
20
+ command: Type.Optional(Type.String({ description: "Shell command to run. Required unless shell:false with argv is used." })),
21
+ argv: Type.Optional(Type.Array(Type.String(), { description: "Argument vector. Use with shell:false to avoid shell parsing." })),
22
+ shell: Type.Optional(Type.Boolean({ description: "Run command through the package's bash-compatible shell. Default true." })),
23
+ cwd: Type.Optional(Type.String({ description: "Working directory. Defaults to the current pi cwd." })),
24
+ env: Type.Optional(Type.Record(Type.String(), Type.String(), { description: "Extra environment variables." })),
25
+ callback: Type.Optional(Type.Boolean({ description: "Queue a follow-up when the task reaches a terminal state. Default true." })),
26
+ timeout_seconds: Type.Optional(Type.Number({ description: "Optional timeout in seconds. Command watchers default to 900 seconds when omitted; pass 0 to disable. Spawned processes have no default timeout." })),
27
+ };
28
+
29
+ const SpawnParams = Type.Object(CommandFields);
30
+
31
+ const WatchParams = Type.Object({
32
+ ...CommandFields,
33
+ interval_seconds: Type.Optional(Type.Number({ description: "Polling interval in seconds. Default 30." })),
34
+ success_when: ConditionSchema,
35
+ failure_when: Type.Optional(ConditionSchema),
36
+ });
37
+
38
+ const IdParams = Type.Object({
39
+ id: Type.String({ description: "Background task id." }),
40
+ verbose: Type.Optional(Type.Boolean({ description: "Return full raw metadata JSON. Default false returns the compact model-facing summary. Use true only for debugging or explicit recovery." })),
41
+ });
42
+ const ListParams = Type.Object({
43
+ status: Type.Optional(Type.Array(Type.String({ description: "Statuses to include." }))),
44
+ limit: Type.Optional(Type.Number({ description: "Maximum tasks to show. Default 20." })),
45
+ });
46
+ const LogParams = Type.Object({
47
+ id: Type.String({ description: "Background task id." }),
48
+ tail_lines: Type.Optional(Type.Number({ description: "Number of trailing lines. Default 20 for compact model ingestion. Set <=0 only when the full log is explicitly required." })),
49
+ });
50
+
51
+ const ActionParams = Type.Object({
52
+ action: Type.Union([
53
+ Type.Literal("spawn"),
54
+ Type.Literal("watch"),
55
+ Type.Literal("list"),
56
+ Type.Literal("status"),
57
+ Type.Literal("log"),
58
+ Type.Literal("stop"),
59
+ Type.Literal("clear"),
60
+ ]),
61
+ id: Type.Optional(Type.String()),
62
+ status: Type.Optional(Type.Array(Type.String())),
63
+ limit: Type.Optional(Type.Number()),
64
+ tail_lines: Type.Optional(Type.Number()),
65
+ verbose: Type.Optional(Type.Boolean()),
66
+ ...CommandFields,
67
+ interval_seconds: Type.Optional(Type.Number()),
68
+ success_when: Type.Optional(ConditionSchema),
69
+ failure_when: Type.Optional(ConditionSchema),
70
+ });
71
+
72
+ const StatusActionParams = Type.Object({
73
+ action: Type.Union([Type.Literal("list"), Type.Literal("status"), Type.Literal("log"), Type.Literal("stop"), Type.Literal("clear")]),
74
+ id: Type.Optional(Type.String()),
75
+ status: Type.Optional(Type.Array(Type.String())),
76
+ limit: Type.Optional(Type.Number()),
77
+ tail_lines: Type.Optional(Type.Number()),
78
+ verbose: Type.Optional(Type.Boolean()),
79
+ });
80
+
81
+ export function registerTools(pi: ExtensionAPI): void {
82
+ let activeSession: BackgroundTaskCallbackOrigin | undefined;
83
+ const getActiveSession = () => activeSession;
84
+
85
+ pi.on("session_start", async (_event, ctx) => {
86
+ activeSession = getCallbackOrigin(ctx);
87
+ for (const meta of listMetas()) resumeRunningTask(pi, meta, getActiveSession);
88
+ });
89
+ pi.on("session_before_switch", () => {
90
+ activeSession = undefined;
91
+ });
92
+ pi.on("session_shutdown", () => {
93
+ activeSession = undefined;
94
+ });
95
+
96
+ pi.registerTool({
97
+ name: "bg_task_spawn",
98
+ label: "BG Spawn",
99
+ description: "Start a long-running background process and return immediately with its task id. Never wait or poll in the foreground.",
100
+ parameters: SpawnParams,
101
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
102
+ activeSession = getCallbackOrigin(ctx);
103
+ const meta = spawnTask(pi, params, ctx.cwd, activeSession, getActiveSession);
104
+ refreshBackgroundTasksNavigator(ctx);
105
+ return text(formatLaunch(meta));
106
+ },
107
+ });
108
+
109
+ pi.registerTool({
110
+ name: "bg_task_watch",
111
+ label: "BG Watch",
112
+ description: "Poll a command in the background until success_when, failure_when, or timeout matches. Returns immediately with its task id. Default timeout 900 seconds; pass timeout_seconds:0 to disable.",
113
+ parameters: WatchParams,
114
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
115
+ activeSession = getCallbackOrigin(ctx);
116
+ const meta = startWatchTask(pi, params, ctx.cwd, activeSession, getActiveSession);
117
+ refreshBackgroundTasksNavigator(ctx);
118
+ return text(formatLaunch(meta));
119
+ },
120
+ });
121
+
122
+ pi.registerTool({
123
+ name: "bg_task_list",
124
+ label: "BG List",
125
+ description: "List durable background tasks. Nonblocking.",
126
+ parameters: ListParams,
127
+ async execute(_toolCallId, params) {
128
+ return text(formatList(resolveList(params.status, params.limit)));
129
+ },
130
+ });
131
+
132
+ pi.registerTool({
133
+ name: "bg_task_status",
134
+ label: "BG Status",
135
+ description: "Inspect one background task. Default output is a compact model-facing summary; pass verbose:true only when full raw metadata is explicitly needed. After a terminal callback, call this first and call bg_task_log only if the summary is insufficient.",
136
+ parameters: IdParams,
137
+ async execute(_toolCallId, params) {
138
+ const meta = readMeta(params.id);
139
+ return text(formatStatus(meta, params.id, { verbose: params.verbose === true }));
140
+ },
141
+ });
142
+
143
+ pi.registerTool({
144
+ name: "bg_task_log",
145
+ label: "BG Log",
146
+ description: "Read a background task log. Default output is a compact 20-line tail for model ingestion. Pass tail_lines for a bounded tail; pass tail_lines:0 only when the full log is explicitly required. Nonblocking.",
147
+ parameters: LogParams,
148
+ renderResult(result: unknown, options: unknown, theme: unknown) {
149
+ return renderBackgroundTaskLogDisplay(result, options, theme);
150
+ },
151
+ async execute(_toolCallId, params) {
152
+ return logText(params.id, params.tail_lines);
153
+ },
154
+ });
155
+
156
+ pi.registerTool({
157
+ name: "bg_task_stop",
158
+ label: "BG Stop",
159
+ description: "Cancel a watcher or terminate a background process group. Nonblocking.",
160
+ parameters: IdParams,
161
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
162
+ activeSession = getCallbackOrigin(ctx);
163
+ const result = formatStop(pi, params.id, ctx, getActiveSession);
164
+ refreshBackgroundTasksNavigator(ctx);
165
+ return text(result);
166
+ },
167
+ });
168
+
169
+ pi.registerTool({
170
+ name: "bg_task",
171
+ label: "BG Task",
172
+ description: "Action wrapper for background tasks: spawn, watch, list, status, log, stop, or clear. Spawn/watch return immediately; do not poll in foreground. For action:status, default compact output and use verbose:true only for full metadata. For action:log, default compact tail and use tail_lines:0 only for explicit full logs.",
173
+ parameters: ActionParams,
174
+ renderResult(result: unknown, options: unknown, theme: unknown) {
175
+ return renderBackgroundTaskLogDisplay(result, options, theme);
176
+ },
177
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
178
+ activeSession = getCallbackOrigin(ctx);
179
+ return actionText(pi, params, ctx, activeSession, getActiveSession);
180
+ },
181
+ });
182
+
183
+ pi.registerTool({
184
+ name: "bg_status",
185
+ label: "BG Status",
186
+ description: "Action wrapper for inspecting background tasks: list, status, log, stop, or clear. Nonblocking. Status is compact by default; log returns a compact tail by default. Use verbose:true or tail_lines:0 only for explicit full-data recovery.",
187
+ parameters: StatusActionParams,
188
+ renderResult(result: unknown, options: unknown, theme: unknown) {
189
+ return renderBackgroundTaskLogDisplay(result, options, theme);
190
+ },
191
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
192
+ activeSession = getCallbackOrigin(ctx);
193
+ return actionText(pi, params, ctx, activeSession, getActiveSession);
194
+ },
195
+ });
196
+ }
197
+
198
+ export function text(textValue: string, details?: unknown) {
199
+ return { content: [{ type: "text" as const, text: textValue }], details };
200
+ }
201
+
202
+ function actionText(
203
+ pi: ExtensionAPI,
204
+ params: Record<string, unknown>,
205
+ ctx: ExtensionContext,
206
+ callbackOrigin: BackgroundTaskCallbackOrigin,
207
+ getActiveSession: () => BackgroundTaskCallbackOrigin | undefined,
208
+ ) {
209
+ if (params.action === "log" && params.id) {
210
+ return logText(String(params.id), params.tail_lines as number | undefined);
211
+ }
212
+ return text(runAction(pi, params, ctx, callbackOrigin, getActiveSession));
213
+ }
214
+
215
+ function runAction(
216
+ pi: ExtensionAPI,
217
+ params: Record<string, unknown>,
218
+ ctx: ExtensionContext,
219
+ callbackOrigin: BackgroundTaskCallbackOrigin,
220
+ getActiveSession: () => BackgroundTaskCallbackOrigin | undefined,
221
+ ): string {
222
+ switch (params.action) {
223
+ case "spawn":
224
+ return withNavigatorRefresh(ctx, formatLaunch(spawnTask(pi, params, ctx.cwd, callbackOrigin, getActiveSession)));
225
+ case "watch":
226
+ if (!params.success_when) return "Invalid parameters: watch requires success_when.";
227
+ return withNavigatorRefresh(ctx, formatLaunch(startWatchTask(pi, params as never, ctx.cwd, callbackOrigin, getActiveSession)));
228
+ case "list":
229
+ return formatList(resolveList(params.status as string[] | undefined, params.limit as number | undefined));
230
+ case "status":
231
+ if (!params.id) return "Invalid parameters: status requires id.";
232
+ return formatStatus(readMeta(String(params.id)) ?? undefined, String(params.id), { verbose: params.verbose === true });
233
+ case "log":
234
+ if (!params.id) return "Invalid parameters: log requires id.";
235
+ return formatLog(String(params.id), params.tail_lines as number | undefined);
236
+ case "stop":
237
+ if (!params.id) return "Invalid parameters: stop requires id.";
238
+ return withNavigatorRefresh(ctx, formatStop(pi, String(params.id), ctx, getActiveSession));
239
+ case "clear":
240
+ return withNavigatorRefresh(ctx, formatClear(params.status as string[] | undefined, callbackOrigin));
241
+ default:
242
+ return `Unknown action: ${String(params.action)}`;
243
+ }
244
+ }
245
+
246
+ function withNavigatorRefresh(ctx: ExtensionContext, result: string): string {
247
+ refreshBackgroundTasksNavigator(ctx);
248
+ return result;
249
+ }
250
+
251
+ function getCallbackOrigin(ctx: ExtensionContext): BackgroundTaskCallbackOrigin {
252
+ let sessionId: string | undefined;
253
+ try {
254
+ sessionId = ctx.sessionManager?.getSessionId();
255
+ } catch {
256
+ sessionId = undefined;
257
+ }
258
+ return { cwd: ctx.cwd, sessionId };
259
+ }
260
+
261
+ function resolveList(statuses?: string[], limit?: number): BackgroundTaskMeta[] {
262
+ let metas = listMetas();
263
+ if (statuses && statuses.length > 0) {
264
+ const wanted = new Set(statuses);
265
+ metas = metas.filter((meta) => wanted.has(meta.status));
266
+ }
267
+ return metas.slice(0, Math.max(1, Math.min(limit ?? 20, 100)));
268
+ }
269
+
270
+ function formatLaunch(meta: BackgroundTaskMeta): string {
271
+ const label = meta.name ? `${meta.name} (${meta.id})` : meta.id;
272
+ return `Started background ${meta.kind} ${label}. Status: ${meta.status}. Log: ${meta.logPath}`;
273
+ }
274
+
275
+ function formatList(metas: BackgroundTaskMeta[]): string {
276
+ if (metas.length === 0) return "No background tasks found.";
277
+ return metas.map((meta) => {
278
+ const age = formatDuration((meta.endedAt ?? Date.now()) - meta.startedAt);
279
+ const label = meta.name ? `${meta.name} ` : "";
280
+ return `${meta.id} ${label}${meta.kind} ${meta.status} ${age}`;
281
+ }).join("\n");
282
+ }
283
+
284
+ function formatStatus(meta: BackgroundTaskMeta | undefined, id?: string, options: { verbose?: boolean } = {}): string {
285
+ if (!meta) return `No background task found${id ? ` for id ${id}` : ""}.`;
286
+ if (!options.verbose) return formatCompactStatus(meta);
287
+ return JSON.stringify(meta, null, 2);
288
+ }
289
+
290
+ function formatCompactStatus(meta: BackgroundTaskMeta): string {
291
+ const lines = [
292
+ `Background task ${meta.id}${meta.name ? ` (${meta.name})` : ""} is ${meta.status}.`,
293
+ `kind: ${meta.kind}`,
294
+ `elapsed: ${formatDuration((meta.endedAt ?? Date.now()) - meta.startedAt)}`,
295
+ ];
296
+ if (meta.deadlineAt && meta.status === "running") lines.push(`deadline: ${formatDuration(meta.deadlineAt - Date.now())} left`);
297
+ if (meta.lastExitCode !== undefined || meta.lastSignal !== undefined) lines.push(`last exit: ${meta.lastExitCode ?? "null"}${meta.lastSignal ? ` signal=${meta.lastSignal}` : ""}`);
298
+ const reason = resultReason(meta.result);
299
+ if (reason) lines.push(`result: ${reason}`);
300
+ if (meta.error) lines.push(`error: ${oneLine(meta.error, 500)}`);
301
+ if (meta.lastState !== undefined) lines.push(`last state: ${oneLine(meta.lastState, 800)}`);
302
+ lines.push(`log: ${meta.logPath}`);
303
+ lines.push(`For full metadata use bg_task_status id=${meta.id} verbose=true. For logs use bg_task_log id=${meta.id} tail_lines=20, or tail_lines=0 for the full log.`);
304
+ return lines.join("\n");
305
+ }
306
+
307
+ function resultReason(result: unknown): string | undefined {
308
+ if (!result) return undefined;
309
+ if (typeof result === "object" && result !== null && "reason" in result) {
310
+ const reason = (result as { reason?: unknown }).reason;
311
+ return reason === undefined ? undefined : oneLine(reason, 500);
312
+ }
313
+ return oneLine(result, 500);
314
+ }
315
+
316
+ function oneLine(value: unknown, maxLength: number): string {
317
+ const raw = typeof value === "string" ? value : JSON.stringify(value);
318
+ const single = String(raw ?? "").replace(/\s+/g, " ").trim();
319
+ return single.length <= maxLength ? single : `${single.slice(0, Math.max(0, maxLength - 1))}…`;
320
+ }
321
+
322
+ function formatLog(id: string, tailLines?: number): string {
323
+ const meta = readMeta(id);
324
+ if (!meta) return `No background task found for id ${id}.`;
325
+ const log = readLog(meta.logPath, tailLines ?? 20);
326
+ const prefix = log.truncated ? `[showing tail of ${meta.logPath}]\n` : `[${meta.logPath}]\n`;
327
+ return prefix + (log.text || "(log is empty)");
328
+ }
329
+
330
+ function logText(id: string, tailLines?: number) {
331
+ const body = formatLog(id, tailLines);
332
+ if (!readMeta(id)) return text(body);
333
+ return text(body, buildBackgroundTaskLogDisplayDetails(body));
334
+ }
335
+
336
+ export function buildBackgroundTaskLogDisplayDetails(body: string) {
337
+ const fullLines = String(body ?? "").split(/\r?\n/);
338
+ const head = fullLines[0] || "bg_task_log";
339
+ const rest = fullLines.slice(1);
340
+ const compactLines = nonEmptyPreviewLines(rest);
341
+ return {
342
+ kind: "background-task-log-display",
343
+ head,
344
+ fullLineCount: fullLines.length,
345
+ compactLines,
346
+ foldedLineCount: Math.max(0, rest.length - compactLines.length),
347
+ };
348
+ }
349
+
350
+ export function renderBackgroundTaskLogDisplay(result: unknown, options: unknown = {}, theme: unknown = {}) {
351
+ const fullText = resultTextContent(result);
352
+ const details = ((result as { details?: unknown })?.details as ReturnType<typeof buildBackgroundTaskLogDisplayDetails> | undefined);
353
+ if (!details || details.kind !== "background-task-log-display") return renderLines(fullText.split(/\r?\n/));
354
+ const expanded = (options as { expanded?: boolean })?.expanded === true;
355
+ const meta = `${details.fullLineCount} lines`;
356
+
357
+ if (expanded) {
358
+ return renderLines([
359
+ `${themed(theme, "accent", "bg_task_log")} ${themed(theme, "dim", `· ${meta}`)}`,
360
+ themed(theme, "dim", "Full displayed log. Click or collapse to fold."),
361
+ "",
362
+ ...fullText.split(/\r?\n/),
363
+ ], "wrap");
364
+ }
365
+
366
+ const folded = details.foldedLineCount > 0
367
+ ? themed(theme, "dim", `Folded ${details.foldedLineCount} display lines. Click or expand for the requested log payload.`)
368
+ : themed(theme, "dim", "Compact log. Expand for full display if needed.");
369
+ return renderLines([
370
+ `${themed(theme, "accent", "bg_task_log")} ${themed(theme, "dim", `· ${meta}`)}`,
371
+ details.head,
372
+ "",
373
+ themed(theme, "dim", "preview"),
374
+ ...details.compactLines,
375
+ folded,
376
+ ]);
377
+ }
378
+
379
+ function resultTextContent(result: unknown): string {
380
+ const content = (result as { content?: Array<{ text?: string }> })?.content;
381
+ if (!Array.isArray(content)) return String(result ?? "");
382
+ return content.map((part) => part.text ?? "").join("\n");
383
+ }
384
+
385
+ function nonEmptyPreviewLines(lines: string[]): string[] {
386
+ const nonEmpty = lines.filter((line) => line.trim().length > 0).slice(0, 8);
387
+ return nonEmpty.length ? nonEmpty : lines.slice(0, 3);
388
+ }
389
+
390
+ function themed(theme: unknown, color: string, value: string): string {
391
+ const fg = (theme as { fg?: (color: string, text: string) => string })?.fg;
392
+ return typeof fg === "function" ? fg(color, value) : value;
393
+ }
394
+
395
+ function renderLines(lines: string[], mode: "truncate" | "wrap" = "truncate") {
396
+ return {
397
+ render(width: number = 80) {
398
+ return mode === "wrap"
399
+ ? lines.flatMap((line) => wrapLineToVisibleWidth(line, width))
400
+ : lines.map((line) => truncateToVisibleWidth(line, width));
401
+ },
402
+ invalidate() { /* stateless */ },
403
+ };
404
+ }
405
+
406
+ function wrapLineToVisibleWidth(line: string, width: number): string[] {
407
+ const str = String(line ?? "");
408
+ const max = Math.max(1, Number(width) || 80);
409
+ if (truncateToVisibleWidth(str, max) === str) return [str];
410
+ const out: string[] = [];
411
+ let current = "";
412
+ let visible = 0;
413
+ for (const char of str) {
414
+ if (visible >= max) {
415
+ out.push(current);
416
+ current = "";
417
+ visible = 0;
418
+ }
419
+ current += char;
420
+ visible += 1;
421
+ }
422
+ out.push(current);
423
+ return out;
424
+ }
425
+
426
+ function truncateToVisibleWidth(value: string, width: number): string {
427
+ const max = Math.max(0, Math.floor(width || 0));
428
+ return String(value ?? "").slice(0, max);
429
+ }
430
+
431
+ function formatStop(
432
+ pi: ExtensionAPI,
433
+ id: string,
434
+ _ctx: ExtensionContext,
435
+ getActiveSession?: () => BackgroundTaskCallbackOrigin | undefined,
436
+ ): string {
437
+ const meta = stopTask(pi, id, getActiveSession);
438
+ if (!meta) return `No background task found for id ${id}.`;
439
+ return `Background task ${id} is ${meta.status}.`;
440
+ }
441
+
442
+ function formatClear(statuses: string[] | undefined, active: BackgroundTaskCallbackOrigin): string {
443
+ const wanted = statuses && statuses.length > 0 ? new Set(statuses) : undefined;
444
+ const now = Date.now();
445
+ let cleared = 0;
446
+ for (const meta of listMetas()) {
447
+ if (meta.dismissedAt !== undefined) continue;
448
+ if (!isTerminalStatus(meta.status)) continue;
449
+ if (wanted && !wanted.has(meta.status)) continue;
450
+ if (!belongsToActiveToolSession(meta, active)) continue;
451
+ meta.dismissedAt = now;
452
+ writeMeta(meta);
453
+ cleared += 1;
454
+ }
455
+ const statusLabel = wanted ? ` matching ${Array.from(wanted).join(",")}` : "";
456
+ return `Dismissed ${cleared} terminal background task${cleared === 1 ? "" : "s"}${statusLabel}.`;
457
+ }
458
+
459
+ function belongsToActiveToolSession(meta: BackgroundTaskMeta, active: BackgroundTaskCallbackOrigin): boolean {
460
+ const origin = meta.callbackOrigin;
461
+ if (origin) {
462
+ if (origin.cwd !== active.cwd) return false;
463
+ if (origin.sessionId || active.sessionId) return origin.sessionId === active.sessionId;
464
+ return true;
465
+ }
466
+ if (active.sessionId) return false;
467
+ return meta.cwd === active.cwd;
468
+ }
469
+
470
+ function formatDuration(ms: number): string {
471
+ const seconds = Math.max(0, Math.round(ms / 1000));
472
+ if (seconds < 60) return `${seconds}s`;
473
+ const minutes = Math.floor(seconds / 60);
474
+ const rest = seconds % 60;
475
+ return `${minutes}m${rest.toString().padStart(2, "0")}s`;
476
+ }
package/src/types.ts ADDED
@@ -0,0 +1,84 @@
1
+ export type BackgroundTaskStatus =
2
+ | "running"
3
+ | "succeeded"
4
+ | "failed"
5
+ | "cancelled"
6
+ | "timed_out";
7
+
8
+ export type BackgroundTaskKind = "process" | "command_watch";
9
+
10
+ export type Condition =
11
+ | { type: "exit_code"; equals: number }
12
+ | { type: "stdout_contains"; value: string }
13
+ | { type: "stderr_contains"; value: string }
14
+ | { type: "json_path_equals"; path: string; value: unknown }
15
+ | { type: "json_path_exists"; path: string };
16
+
17
+ export interface CommandSpec {
18
+ command?: string;
19
+ argv?: string[];
20
+ shell?: boolean;
21
+ cwd?: string;
22
+ env?: Record<string, string>;
23
+ }
24
+
25
+ export interface CommandResult {
26
+ exitCode: number | null;
27
+ signal: NodeJS.Signals | null;
28
+ stdout: string;
29
+ stderr: string;
30
+ startedAt: number;
31
+ endedAt: number;
32
+ }
33
+
34
+ export interface BackgroundTaskCallbackOrigin {
35
+ cwd: string;
36
+ sessionId?: string;
37
+ }
38
+
39
+ export interface BackgroundTaskMeta {
40
+ id: string;
41
+ name?: string;
42
+ kind: BackgroundTaskKind;
43
+ status: BackgroundTaskStatus;
44
+ startedAt: number;
45
+ endedAt?: number;
46
+ deadlineAt?: number;
47
+ intervalMs?: number;
48
+ lastCheckedAt?: number;
49
+ lastExitCode?: number | null;
50
+ lastSignal?: NodeJS.Signals | null;
51
+ lastState?: unknown;
52
+ result?: unknown;
53
+ logPath: string;
54
+ callback?: boolean;
55
+ callbackSentAt?: number;
56
+ callbackOrigin?: BackgroundTaskCallbackOrigin;
57
+ callbackSuppressedAt?: number;
58
+ callbackSuppressedReason?: string;
59
+ dismissedAt?: number;
60
+ command?: string;
61
+ argv?: string[];
62
+ shell?: boolean;
63
+ cwd: string;
64
+ env?: Record<string, string>;
65
+ pid?: number;
66
+ pgid?: number;
67
+ spawnPid: number;
68
+ successWhen?: Condition;
69
+ failureWhen?: Condition;
70
+ notifyOn?: "terminal";
71
+ stopRequestedAt?: number;
72
+ error?: string;
73
+ }
74
+
75
+ export interface TerminalResult {
76
+ status: Exclude<BackgroundTaskStatus, "running">;
77
+ reason: string;
78
+ matchedCondition?: Condition;
79
+ commandResult?: CommandResult;
80
+ }
81
+
82
+ export function isTerminalStatus(status: BackgroundTaskStatus): boolean {
83
+ return status !== "running";
84
+ }