pi-better-background-tasks 0.1.18 → 0.2.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.
- package/README.md +25 -0
- package/package.json +2 -1
- package/src/logs.ts +6 -0
- package/src/navigator-provider.ts +10 -3
- package/src/process.ts +25 -3
- package/src/registry.ts +5 -1
- package/src/remote-task-preset.ts +631 -0
- package/src/runtime.ts +483 -55
- package/src/tools.ts +53 -18
- package/src/types.ts +43 -0
package/src/tools.ts
CHANGED
|
@@ -16,9 +16,28 @@ const ConditionSchema = Type.Union([
|
|
|
16
16
|
Type.Object({ type: Type.Literal("json_path_exists"), path: Type.String() }),
|
|
17
17
|
]);
|
|
18
18
|
|
|
19
|
+
const SshSchema = Type.Object({
|
|
20
|
+
host: Type.String({ description: "SSH host. Required when ssh is set." }),
|
|
21
|
+
user: Type.Optional(Type.String({ description: "SSH user." })),
|
|
22
|
+
port: Type.Optional(Type.Integer({ minimum: 1, maximum: 65_535, description: "SSH port." })),
|
|
23
|
+
identity_file: Type.Optional(Type.String({ description: "SSH identity file path." })),
|
|
24
|
+
jump: Type.Optional(Type.String({ description: "SSH jump host passed with -J." })),
|
|
25
|
+
options: Type.Optional(Type.Record(Type.String(), Type.String(), { description: "Additional SSH -o key/value options. Agent-safe defaults remain enforced." })),
|
|
26
|
+
}, {
|
|
27
|
+
description: "Structured SSH connection for remote background tasks. Set ssh instead of wrapping command in a hand-written ssh command; command is the remote command, and Pi keeps durable local logs, status, callbacks, and remote stop semantics.",
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const RemoteSchema = Type.Object({
|
|
31
|
+
session: Type.Optional(Type.Union([Type.Literal("tmux"), Type.Literal("direct")], { description: "Remote session mode. SSH spawn defaults to durable tmux. Watch always uses direct one-shot polls. Explicit direct spawn has weaker stop semantics." })),
|
|
32
|
+
install_tmux: Type.Optional(Type.Boolean({ description: "Allow SSH spawn to install tmux non-interactively when missing. Defaults true in tmux mode and is ignored for watch and direct spawn." })),
|
|
33
|
+
workdir: Type.Optional(Type.String({ description: "Remote working directory for the spawned command." })),
|
|
34
|
+
}, {
|
|
35
|
+
description: "Remote execution controls used with ssh. Omit session for SSH spawn to get the durable tmux default; SSH watch runs direct one-shot polls regardless of session and does not install tmux.",
|
|
36
|
+
});
|
|
37
|
+
|
|
19
38
|
const CommandFields = {
|
|
20
39
|
name: Type.Optional(Type.String({ description: "Human-readable task label." })),
|
|
21
|
-
command: Type.Optional(Type.String({ description: "Shell command to run. Required unless shell:false with argv is used." })),
|
|
40
|
+
command: Type.Optional(Type.String({ description: "Shell command to run, or the remote command when ssh is set. Required unless shell:false with argv is used." })),
|
|
22
41
|
argv: Type.Optional(Type.Array(Type.String(), { description: "Argument vector. Use with shell:false to avoid shell parsing." })),
|
|
23
42
|
shell: Type.Optional(Type.Boolean({ description: "Run command through the package's bash-compatible shell. Default true." })),
|
|
24
43
|
cwd: Type.Optional(Type.String({ description: "Working directory. Defaults to the current pi cwd." })),
|
|
@@ -26,6 +45,8 @@ const CommandFields = {
|
|
|
26
45
|
max_log_bytes: Type.Optional(Type.Number({ description: "Maximum retained raw-log bytes. Default 4194304 (4 MiB). Older output is compacted while the task runs." })),
|
|
27
46
|
callback: Type.Optional(Type.Boolean({ description: "Queue a follow-up when the task reaches a terminal state. Default true." })),
|
|
28
47
|
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." })),
|
|
48
|
+
ssh: Type.Optional(SshSchema),
|
|
49
|
+
remote: Type.Optional(RemoteSchema),
|
|
29
50
|
};
|
|
30
51
|
|
|
31
52
|
const SpawnParams = Type.Object(CommandFields);
|
|
@@ -100,7 +121,7 @@ export function registerTools(pi: ExtensionAPI): void {
|
|
|
100
121
|
pi.registerTool({
|
|
101
122
|
name: "bg_task_spawn",
|
|
102
123
|
label: "BG Spawn",
|
|
103
|
-
description: "Start a long-running background process and return immediately with its task id. Never wait or poll in the foreground.",
|
|
124
|
+
description: "Start a long-running background process and return immediately with its task id. For remote work, prefer structured ssh: pass ssh:{host,user} and put the remote command in command; spawn defaults to a remote tmux session with durable local logs and real remote stop. If tmux is missing, the preset attempts to install tmux non-interactively and fails closed with operator guidance when setup cannot proceed. Explicit remote.session=direct skips tmux, but direct mode has weaker stop semantics and may leave the remote process running. Never wait or poll in the foreground.",
|
|
104
125
|
parameters: SpawnParams,
|
|
105
126
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
106
127
|
activeSession = getCallbackOrigin(ctx);
|
|
@@ -113,7 +134,7 @@ export function registerTools(pi: ExtensionAPI): void {
|
|
|
113
134
|
pi.registerTool({
|
|
114
135
|
name: "bg_task_watch",
|
|
115
136
|
label: "BG Watch",
|
|
116
|
-
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.",
|
|
137
|
+
description: "Poll a command in the background until success_when, failure_when, or timeout matches. For remote work, prefer structured ssh: pass ssh:{host,user} and provide the remote command in command; each interval opens a direct one-shot SSH poll without tmux installation. Returns immediately with its task id. Default timeout 900 seconds; pass timeout_seconds:0 to disable.",
|
|
117
138
|
parameters: WatchParams,
|
|
118
139
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
119
140
|
activeSession = getCallbackOrigin(ctx);
|
|
@@ -160,11 +181,11 @@ export function registerTools(pi: ExtensionAPI): void {
|
|
|
160
181
|
pi.registerTool({
|
|
161
182
|
name: "bg_task_stop",
|
|
162
183
|
label: "BG Stop",
|
|
163
|
-
description: "Cancel a watcher or terminate a background
|
|
184
|
+
description: "Cancel a watcher or terminate a background task. For a tmux-backed SSH task, stop kills its remote tmux session before marking it cancelled. Direct SSH stop only tears down the local client and may leave the remote process running.",
|
|
164
185
|
parameters: IdParams,
|
|
165
186
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
166
187
|
activeSession = getCallbackOrigin(ctx);
|
|
167
|
-
const result = formatStop(pi, params.id, ctx, getActiveSession);
|
|
188
|
+
const result = await formatStop(pi, params.id, ctx, getActiveSession);
|
|
168
189
|
refreshBackgroundTasksNavigator(ctx);
|
|
169
190
|
return text(result);
|
|
170
191
|
},
|
|
@@ -173,7 +194,7 @@ export function registerTools(pi: ExtensionAPI): void {
|
|
|
173
194
|
pi.registerTool({
|
|
174
195
|
name: "bg_task",
|
|
175
196
|
label: "BG Task",
|
|
176
|
-
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.",
|
|
197
|
+
description: "Action wrapper for background tasks: spawn, watch, list, status, log, stop, or clear. For remote work, prefer structured ssh: pass ssh:{host,user} and provide the remote command in command. SSH spawn defaults to durable tmux; SSH watches use direct one-shot polls without tmux installation; remote.session=direct is a weaker-stop spawn escape hatch. 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.",
|
|
177
198
|
parameters: ActionParams,
|
|
178
199
|
renderResult(result: unknown, options: unknown, theme: unknown) {
|
|
179
200
|
return renderBackgroundTaskLogDisplay(result, options, theme);
|
|
@@ -203,7 +224,7 @@ export function text(textValue: string, details?: unknown) {
|
|
|
203
224
|
return { content: [{ type: "text" as const, text: textValue }], details };
|
|
204
225
|
}
|
|
205
226
|
|
|
206
|
-
function actionText(
|
|
227
|
+
async function actionText(
|
|
207
228
|
pi: ExtensionAPI,
|
|
208
229
|
params: Record<string, unknown>,
|
|
209
230
|
ctx: ExtensionContext,
|
|
@@ -213,16 +234,16 @@ function actionText(
|
|
|
213
234
|
if (params.action === "log" && params.id) {
|
|
214
235
|
return logText(String(params.id), params.tail_lines as number | undefined);
|
|
215
236
|
}
|
|
216
|
-
return text(runAction(pi, params, ctx, callbackOrigin, getActiveSession));
|
|
237
|
+
return text(await runAction(pi, params, ctx, callbackOrigin, getActiveSession));
|
|
217
238
|
}
|
|
218
239
|
|
|
219
|
-
function runAction(
|
|
240
|
+
async function runAction(
|
|
220
241
|
pi: ExtensionAPI,
|
|
221
242
|
params: Record<string, unknown>,
|
|
222
243
|
ctx: ExtensionContext,
|
|
223
244
|
callbackOrigin: BackgroundTaskCallbackOrigin,
|
|
224
245
|
getActiveSession: () => BackgroundTaskCallbackOrigin | undefined,
|
|
225
|
-
): string {
|
|
246
|
+
): Promise<string> {
|
|
226
247
|
switch (params.action) {
|
|
227
248
|
case "spawn":
|
|
228
249
|
return withNavigatorRefresh(ctx, formatLaunch(spawnTask(pi, params, ctx.cwd, callbackOrigin, getActiveSession)));
|
|
@@ -239,7 +260,7 @@ function runAction(
|
|
|
239
260
|
return formatLog(String(params.id), params.tail_lines as number | undefined);
|
|
240
261
|
case "stop":
|
|
241
262
|
if (!params.id) return "Invalid parameters: stop requires id.";
|
|
242
|
-
return withNavigatorRefresh(ctx, formatStop(pi, String(params.id), ctx, getActiveSession));
|
|
263
|
+
return withNavigatorRefresh(ctx, await formatStop(pi, String(params.id), ctx, getActiveSession));
|
|
243
264
|
case "clear":
|
|
244
265
|
return withNavigatorRefresh(ctx, formatClear(params.status as string[] | undefined, callbackOrigin));
|
|
245
266
|
default:
|
|
@@ -271,9 +292,14 @@ function resolveList(statuses?: string[], limit?: number): BackgroundTaskMeta[]
|
|
|
271
292
|
return metas.slice(0, Math.max(1, Math.min(limit ?? 20, 100)));
|
|
272
293
|
}
|
|
273
294
|
|
|
274
|
-
function formatLaunch(meta: BackgroundTaskMeta): string {
|
|
295
|
+
export function formatLaunch(meta: BackgroundTaskMeta): string {
|
|
275
296
|
const label = meta.name ? `${meta.name} (${meta.id})` : meta.id;
|
|
276
|
-
|
|
297
|
+
const remote = meta.ssh
|
|
298
|
+
? ` Remote: ${meta.ssh.target}${meta.remote?.session ? ` mode=${meta.remote.session}` : ""}${meta.remote?.sessionName ? ` session=${meta.remote.sessionName}` : ""}.`
|
|
299
|
+
: "";
|
|
300
|
+
const setup = meta.remote?.bootstrapMessage ? ` Remote setup: ${meta.remote.bootstrapMessage}` : "";
|
|
301
|
+
const warning = meta.remote?.warning ? ` Warning: ${meta.remote.warning}` : "";
|
|
302
|
+
return `Started background ${meta.kind} ${label}. Status: ${meta.status}.${remote}${setup}${warning} Log: ${meta.logPath}`;
|
|
277
303
|
}
|
|
278
304
|
|
|
279
305
|
function formatList(metas: BackgroundTaskMeta[]): string {
|
|
@@ -281,7 +307,8 @@ function formatList(metas: BackgroundTaskMeta[]): string {
|
|
|
281
307
|
return metas.map((meta) => {
|
|
282
308
|
const age = formatDuration((meta.endedAt ?? Date.now()) - meta.startedAt);
|
|
283
309
|
const label = meta.name ? `${meta.name} ` : "";
|
|
284
|
-
|
|
310
|
+
const remote = meta.ssh ? ` ${meta.ssh.target}${meta.remote?.session ? ` ${meta.remote.session}` : ""}` : "";
|
|
311
|
+
return `${meta.id} ${label}${meta.kind} ${meta.status} ${age}${remote}`;
|
|
285
312
|
}).join("\n");
|
|
286
313
|
}
|
|
287
314
|
|
|
@@ -297,6 +324,12 @@ function formatCompactStatus(meta: BackgroundTaskMeta): string {
|
|
|
297
324
|
`kind: ${meta.kind}`,
|
|
298
325
|
`elapsed: ${formatDuration((meta.endedAt ?? Date.now()) - meta.startedAt)}`,
|
|
299
326
|
];
|
|
327
|
+
if (meta.ssh) lines.push(`remote: ${meta.ssh.target}`);
|
|
328
|
+
if (meta.remote?.session) lines.push(`remote mode: ${meta.remote.session}`);
|
|
329
|
+
if (meta.remote?.sessionName) lines.push(`remote session: ${meta.remote.sessionName}`);
|
|
330
|
+
if (meta.remote?.bootstrapMessage) lines.push(`remote setup: ${oneLine(meta.remote.bootstrapMessage, 500)}`);
|
|
331
|
+
if (meta.remote?.warning) lines.push(`warning: ${oneLine(meta.remote.warning, 500)}`);
|
|
332
|
+
if (meta.remote?.stopMessage) lines.push(`remote stop: ${oneLine(meta.remote.stopMessage, 500)}`);
|
|
300
333
|
if (meta.deadlineAt && meta.status === "running") lines.push(`deadline: ${formatDuration(meta.deadlineAt - Date.now())} left`);
|
|
301
334
|
if (meta.lastExitCode !== undefined || meta.lastSignal !== undefined) lines.push(`last exit: ${meta.lastExitCode ?? "null"}${meta.lastSignal ? ` signal=${meta.lastSignal}` : ""}`);
|
|
302
335
|
const reason = resultReason(meta.result);
|
|
@@ -433,15 +466,17 @@ function truncateToVisibleWidth(value: string, width: number): string {
|
|
|
433
466
|
return String(value ?? "").slice(0, max);
|
|
434
467
|
}
|
|
435
468
|
|
|
436
|
-
function formatStop(
|
|
469
|
+
async function formatStop(
|
|
437
470
|
pi: ExtensionAPI,
|
|
438
471
|
id: string,
|
|
439
472
|
_ctx: ExtensionContext,
|
|
440
473
|
getActiveSession?: () => BackgroundTaskCallbackOrigin | undefined,
|
|
441
|
-
): string {
|
|
442
|
-
const meta = stopTask(pi, id, getActiveSession);
|
|
474
|
+
): Promise<string> {
|
|
475
|
+
const meta = await stopTask(pi, id, getActiveSession);
|
|
443
476
|
if (!meta) return `No background task found for id ${id}.`;
|
|
444
|
-
|
|
477
|
+
const remoteStop = meta.remote?.stopMessage ? ` ${meta.remote.stopMessage}` : "";
|
|
478
|
+
const weakStop = meta.remote?.session === "direct" ? ` Warning: ${meta.remote.warning}` : "";
|
|
479
|
+
return `Background task ${id} is ${meta.status}.${remoteStop}${weakStop}`;
|
|
445
480
|
}
|
|
446
481
|
|
|
447
482
|
function formatClear(statuses: string[] | undefined, active: BackgroundTaskCallbackOrigin): string {
|
package/src/types.ts
CHANGED
|
@@ -29,6 +29,47 @@ export interface CommandResult {
|
|
|
29
29
|
stderr: string;
|
|
30
30
|
startedAt: number;
|
|
31
31
|
endedAt: number;
|
|
32
|
+
timedOut?: boolean;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface SshConnectionParams {
|
|
36
|
+
host: string;
|
|
37
|
+
user?: string;
|
|
38
|
+
port?: number;
|
|
39
|
+
identity_file?: string;
|
|
40
|
+
jump?: string;
|
|
41
|
+
options?: Record<string, string>;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface RemoteTaskParams {
|
|
45
|
+
session?: "tmux" | "direct";
|
|
46
|
+
install_tmux?: boolean;
|
|
47
|
+
workdir?: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface ResolvedSshIdentity {
|
|
51
|
+
host: string;
|
|
52
|
+
user?: string;
|
|
53
|
+
port?: number;
|
|
54
|
+
identityFile?: string;
|
|
55
|
+
jump?: string;
|
|
56
|
+
options?: Record<string, string>;
|
|
57
|
+
target: string;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface ResolvedRemoteTaskMetadata {
|
|
61
|
+
command: string;
|
|
62
|
+
session?: "tmux" | "direct";
|
|
63
|
+
installTmux?: boolean;
|
|
64
|
+
workdir?: string;
|
|
65
|
+
sessionName?: string;
|
|
66
|
+
bootstrapStatus?: "pending" | "present" | "installed" | "needs_user" | "unknown_package_manager" | "install_failed" | "timed_out";
|
|
67
|
+
bootstrapMessage?: string;
|
|
68
|
+
tmuxInstalled?: boolean;
|
|
69
|
+
sessionStarted?: boolean;
|
|
70
|
+
logOffset?: number;
|
|
71
|
+
warning?: string;
|
|
72
|
+
stopMessage?: string;
|
|
32
73
|
}
|
|
33
74
|
|
|
34
75
|
export interface BackgroundTaskCallbackOrigin {
|
|
@@ -75,6 +116,8 @@ export interface BackgroundTaskMeta {
|
|
|
75
116
|
notifyOn?: "terminal";
|
|
76
117
|
stopRequestedAt?: number;
|
|
77
118
|
error?: string;
|
|
119
|
+
ssh?: ResolvedSshIdentity;
|
|
120
|
+
remote?: ResolvedRemoteTaskMetadata;
|
|
78
121
|
}
|
|
79
122
|
|
|
80
123
|
export interface TerminalResult {
|