@aliou/pi-processes 0.6.4 → 0.7.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/README.md +49 -0
- package/package.json +2 -1
- package/src/commands/kill/command.ts +6 -6
- package/src/commands/logs/command.ts +1 -1
- package/src/commands/pin/command.ts +2 -1
- package/src/components/log-dock-component.ts +0 -11
- package/src/components/log-overlay-component.ts +0 -9
- package/src/components/processes-component.ts +32 -16
- package/src/constants/index.ts +4 -0
- package/src/constants/types.ts +36 -1
- package/src/hooks/index.ts +2 -0
- package/src/hooks/message-renderer.ts +35 -3
- package/src/hooks/process-end.ts +2 -0
- package/src/hooks/process-watch.ts +83 -0
- package/src/manager.test.ts +331 -0
- package/src/manager.ts +214 -30
- package/src/tools/actions/debug.ts +148 -0
- package/src/tools/actions/index.ts +105 -10
- package/src/tools/actions/kill.ts +14 -1
- package/src/tools/actions/list.ts +153 -2
- package/src/tools/actions/logs.ts +61 -3
- package/src/tools/actions/output.ts +129 -4
- package/src/tools/actions/start.ts +197 -8
- package/src/tools/actions/write.ts +28 -2
- package/src/tools/index.ts +93 -246
- package/src/utils/command-executor.ts +3 -0
- package/src/utils/format.ts +32 -0
- package/src/utils/index.ts +7 -1
package/src/tools/index.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ToolBody
|
|
1
|
+
import { ToolBody } from "@aliou/pi-utils-ui";
|
|
2
2
|
import { StringEnum } from "@mariozechner/pi-ai";
|
|
3
3
|
import type {
|
|
4
4
|
AgentToolResult,
|
|
@@ -10,17 +10,27 @@ import { Text } from "@mariozechner/pi-tui";
|
|
|
10
10
|
import { type Static, Type } from "@sinclair/typebox";
|
|
11
11
|
import type { ProcessesDetails } from "../constants";
|
|
12
12
|
import type { ProcessManager } from "../manager";
|
|
13
|
-
import {
|
|
14
|
-
|
|
13
|
+
import { executeAction, renderActionCall, renderActionResult } from "./actions";
|
|
14
|
+
|
|
15
|
+
const DEBUG_PREVIEW_ENABLED = process.env.PI_PROCESSES_DEBUG_PREVIEW === "1";
|
|
16
|
+
|
|
17
|
+
const PROCESS_ACTIONS = [
|
|
18
|
+
"start",
|
|
19
|
+
"list",
|
|
20
|
+
"output",
|
|
21
|
+
"logs",
|
|
22
|
+
"kill",
|
|
23
|
+
"clear",
|
|
24
|
+
"write",
|
|
25
|
+
...(DEBUG_PREVIEW_ENABLED ? (["debug_preview"] as const) : []),
|
|
26
|
+
] as const;
|
|
15
27
|
|
|
16
28
|
const ProcessesParams = Type.Object({
|
|
17
|
-
action: StringEnum(
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
},
|
|
23
|
-
),
|
|
29
|
+
action: StringEnum(PROCESS_ACTIONS, {
|
|
30
|
+
description: DEBUG_PREVIEW_ENABLED
|
|
31
|
+
? "Action: start (run command), list (show all), output (get recent output), logs (get log file paths), kill (terminate), clear (remove finished), write (write to stdin), debug_preview (temporary UI preview, no side effects)"
|
|
32
|
+
: "Action: start (run command), list (show all), output (get recent output), logs (get log file paths), kill (terminate), clear (remove finished), write (write to stdin)",
|
|
33
|
+
}),
|
|
24
34
|
command: Type.Optional(
|
|
25
35
|
Type.String({ description: "Command to run (required for start)" }),
|
|
26
36
|
),
|
|
@@ -33,7 +43,7 @@ const ProcessesParams = Type.Object({
|
|
|
33
43
|
id: Type.Optional(
|
|
34
44
|
Type.String({
|
|
35
45
|
description:
|
|
36
|
-
"Process ID
|
|
46
|
+
"Process ID, returned by start and list actions (required for output/kill/logs/write)",
|
|
37
47
|
}),
|
|
38
48
|
),
|
|
39
49
|
input: Type.Optional(
|
|
@@ -65,6 +75,41 @@ const ProcessesParams = Type.Object({
|
|
|
65
75
|
"Get a turn to react when process is killed by external signal (default: false). Note: killing via tool never triggers a turn.",
|
|
66
76
|
}),
|
|
67
77
|
),
|
|
78
|
+
...(DEBUG_PREVIEW_ENABLED
|
|
79
|
+
? {
|
|
80
|
+
preview: Type.Optional(
|
|
81
|
+
StringEnum(["start", "list", "output", "logs", "error"] as const, {
|
|
82
|
+
description:
|
|
83
|
+
"For action=debug_preview only: which rendered result variant to preview (default: start)",
|
|
84
|
+
}),
|
|
85
|
+
),
|
|
86
|
+
}
|
|
87
|
+
: {}),
|
|
88
|
+
logWatches: Type.Optional(
|
|
89
|
+
Type.Array(
|
|
90
|
+
Type.Object(
|
|
91
|
+
{
|
|
92
|
+
pattern: Type.String({
|
|
93
|
+
description:
|
|
94
|
+
"Regular expression pattern to match against process output lines",
|
|
95
|
+
}),
|
|
96
|
+
stream: Type.Optional(
|
|
97
|
+
StringEnum(["stdout", "stderr", "both"] as const, {
|
|
98
|
+
description:
|
|
99
|
+
"Which stream to watch (default: both). Use stdout/stderr to reduce noise.",
|
|
100
|
+
}),
|
|
101
|
+
),
|
|
102
|
+
repeat: Type.Optional(
|
|
103
|
+
Type.Boolean({
|
|
104
|
+
description:
|
|
105
|
+
"Trigger every time this pattern matches (default: false, one-time)",
|
|
106
|
+
}),
|
|
107
|
+
),
|
|
108
|
+
},
|
|
109
|
+
{ additionalProperties: false },
|
|
110
|
+
),
|
|
111
|
+
),
|
|
112
|
+
),
|
|
68
113
|
});
|
|
69
114
|
|
|
70
115
|
type ProcessesParamsType = Static<typeof ProcessesParams>;
|
|
@@ -78,13 +123,21 @@ export function setupProcessesTools(pi: ExtensionAPI, manager: ProcessManager) {
|
|
|
78
123
|
- alertOnSuccess (default: false): Get a turn to react when process completes successfully
|
|
79
124
|
- alertOnFailure (default: true): Get a turn to react when process crashes/fails
|
|
80
125
|
- alertOnKill (default: false): Get a turn to react if killed by external signal (killing via tool never triggers a turn)
|
|
126
|
+
- logWatches (optional): Runtime output watches that trigger immediate alerts while running
|
|
127
|
+
- pattern: regex string to match per output line
|
|
128
|
+
- stream: stdout | stderr | both (default both)
|
|
129
|
+
- repeat: false by default (single-fire). Set true for repeat alerts
|
|
81
130
|
- list: Show all managed processes with their IDs and names
|
|
82
|
-
- output: Get recent stdout/stderr (requires 'id'
|
|
131
|
+
- output: Get recent stdout/stderr (requires 'id')
|
|
83
132
|
- logs: Get log file paths to inspect with read tool (requires 'id')
|
|
84
|
-
- kill: Terminate a process (requires 'id'
|
|
133
|
+
- kill: Terminate a process (requires 'id')
|
|
85
134
|
- clear: Remove all finished processes from the list
|
|
86
135
|
- write: Write to process stdin (requires 'id' and 'input', optional 'end' to close stdin)
|
|
87
|
-
|
|
136
|
+
${
|
|
137
|
+
DEBUG_PREVIEW_ENABLED
|
|
138
|
+
? "- debug_preview: Temporary renderer preview for process tool UIs (no process side effects)\n - preview: start | list | output | logs | error (default: start)\n"
|
|
139
|
+
: ""
|
|
140
|
+
}
|
|
88
141
|
Important: You DON'T need to poll or wait for processes. Notifications arrive automatically based on your preferences. Start processes and continue with other work - you'll be informed if something requires attention.
|
|
89
142
|
|
|
90
143
|
Note: User always sees process updates in the UI. The notify flags control whether YOU (the agent) get a turn to react (e.g. check results, fix code, restart).`,
|
|
@@ -104,53 +157,7 @@ Note: User always sees process updates in the UI. The notify flags control wheth
|
|
|
104
157
|
},
|
|
105
158
|
|
|
106
159
|
renderCall(args: ProcessesParamsType, theme: Theme) {
|
|
107
|
-
|
|
108
|
-
const optionArgs: Array<{ label: string; value: string }> = [];
|
|
109
|
-
let mainArg: string | undefined;
|
|
110
|
-
|
|
111
|
-
if (args.action === "start") {
|
|
112
|
-
if (args.name) {
|
|
113
|
-
mainArg = `"${args.name}"`;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
if (args.command) {
|
|
117
|
-
if (!mainArg && args.command.length <= 60) {
|
|
118
|
-
mainArg = args.command;
|
|
119
|
-
} else if (args.command.length <= 60) {
|
|
120
|
-
optionArgs.push({ label: "command", value: args.command });
|
|
121
|
-
} else {
|
|
122
|
-
longArgs.push({ label: "command", value: args.command });
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
if (
|
|
128
|
-
(args.action === "output" ||
|
|
129
|
-
args.action === "kill" ||
|
|
130
|
-
args.action === "logs" ||
|
|
131
|
-
args.action === "write") &&
|
|
132
|
-
args.id
|
|
133
|
-
) {
|
|
134
|
-
mainArg = args.id;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
if (args.action === "write" && args.input) {
|
|
138
|
-
optionArgs.push({ label: "input", value: args.input });
|
|
139
|
-
if (args.end) {
|
|
140
|
-
optionArgs.push({ label: "end", value: "true" });
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
return new ToolCallHeader(
|
|
145
|
-
{
|
|
146
|
-
toolName: "Process",
|
|
147
|
-
action: args.action,
|
|
148
|
-
mainArg,
|
|
149
|
-
optionArgs,
|
|
150
|
-
longArgs,
|
|
151
|
-
},
|
|
152
|
-
theme,
|
|
153
|
-
);
|
|
160
|
+
return renderActionCall(args, theme);
|
|
154
161
|
},
|
|
155
162
|
|
|
156
163
|
renderResult(
|
|
@@ -158,199 +165,39 @@ Note: User always sees process updates in the UI. The notify flags control wheth
|
|
|
158
165
|
options: ToolRenderResultOptions,
|
|
159
166
|
theme: Theme,
|
|
160
167
|
) {
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
if (!details) {
|
|
164
|
-
const text = result.content[0];
|
|
165
|
-
return new Text(
|
|
166
|
-
text?.type === "text" && text.text ? text.text : "No result",
|
|
167
|
-
0,
|
|
168
|
-
0,
|
|
169
|
-
);
|
|
168
|
+
if (options.isPartial) {
|
|
169
|
+
return new Text(theme.fg("muted", "Process: running..."), 0, 0);
|
|
170
170
|
}
|
|
171
171
|
|
|
172
|
-
const
|
|
173
|
-
{ label: string; value: string; showCollapsed?: boolean } | Text
|
|
174
|
-
> = [];
|
|
175
|
-
|
|
176
|
-
if (!details.success) {
|
|
177
|
-
fields.push({
|
|
178
|
-
label: "Error",
|
|
179
|
-
value: theme.fg("error", details.message),
|
|
180
|
-
showCollapsed: true,
|
|
181
|
-
});
|
|
182
|
-
} else if (details.action === "start" && details.process) {
|
|
183
|
-
const process = details.process;
|
|
184
|
-
fields.push({
|
|
185
|
-
label: "Status",
|
|
186
|
-
value:
|
|
187
|
-
theme.fg("success", "Started") +
|
|
188
|
-
` ${theme.fg("accent", `"${process.name}"`)} (${process.id}, PID: ${process.pid})`,
|
|
189
|
-
showCollapsed: true,
|
|
190
|
-
});
|
|
191
|
-
} else if (details.action === "output" && details.output) {
|
|
192
|
-
const lines: string[] = [theme.fg("muted", details.message)];
|
|
193
|
-
let hadAnsi = false;
|
|
194
|
-
|
|
195
|
-
if (details.output.stdout.length > 0) {
|
|
196
|
-
lines.push("", theme.fg("accent", "stdout:"));
|
|
197
|
-
for (const line of details.output.stdout.slice(-20)) {
|
|
198
|
-
if (!hadAnsi && hasAnsi(line)) hadAnsi = true;
|
|
199
|
-
lines.push(stripAnsi(line));
|
|
200
|
-
}
|
|
201
|
-
if (details.output.stdout.length > 20) {
|
|
202
|
-
lines.push(
|
|
203
|
-
theme.fg(
|
|
204
|
-
"muted",
|
|
205
|
-
`... (${details.output.stdout.length - 20} more lines)`,
|
|
206
|
-
),
|
|
207
|
-
);
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
if (details.output.stderr.length > 0) {
|
|
212
|
-
lines.push("", theme.fg("warning", "stderr:"));
|
|
213
|
-
for (const line of details.output.stderr.slice(-10)) {
|
|
214
|
-
if (!hadAnsi && hasAnsi(line)) hadAnsi = true;
|
|
215
|
-
lines.push(theme.fg("warning", stripAnsi(line)));
|
|
216
|
-
}
|
|
217
|
-
if (details.output.stderr.length > 10) {
|
|
218
|
-
lines.push(
|
|
219
|
-
theme.fg(
|
|
220
|
-
"muted",
|
|
221
|
-
`... (${details.output.stderr.length - 10} more lines)`,
|
|
222
|
-
),
|
|
223
|
-
);
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
if (hadAnsi) {
|
|
228
|
-
lines.push(
|
|
229
|
-
"",
|
|
230
|
-
theme.fg("muted", "ANSI escape codes were stripped from output"),
|
|
231
|
-
);
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
fields.push(new Text(lines.join("\n"), 0, 0));
|
|
235
|
-
|
|
236
|
-
// Collapsed summary
|
|
237
|
-
const previewSource =
|
|
238
|
-
details.output.stdout.length > 0
|
|
239
|
-
? details.output.stdout
|
|
240
|
-
: details.output.stderr;
|
|
241
|
-
const preview = previewSource
|
|
242
|
-
.slice(-2)
|
|
243
|
-
.map((l) => stripAnsi(l))
|
|
244
|
-
.join("\n");
|
|
245
|
-
fields.push({
|
|
246
|
-
label: "Output",
|
|
247
|
-
value: preview
|
|
248
|
-
? `${theme.fg("muted", preview)}`
|
|
249
|
-
: theme.fg("muted", "(empty)"),
|
|
250
|
-
showCollapsed: true,
|
|
251
|
-
});
|
|
252
|
-
} else if (
|
|
253
|
-
details.action === "list" &&
|
|
254
|
-
details.processes &&
|
|
255
|
-
details.processes.length > 0
|
|
256
|
-
) {
|
|
257
|
-
const lines: string[] = [
|
|
258
|
-
theme.fg("success", `${details.processes.length} process(es):`),
|
|
259
|
-
];
|
|
260
|
-
|
|
261
|
-
for (const process of details.processes) {
|
|
262
|
-
let status: string;
|
|
263
|
-
switch (process.status) {
|
|
264
|
-
case "running":
|
|
265
|
-
status = theme.fg("accent", "running");
|
|
266
|
-
break;
|
|
267
|
-
case "terminating":
|
|
268
|
-
status = theme.fg("warning", "terminating");
|
|
269
|
-
break;
|
|
270
|
-
case "terminate_timeout":
|
|
271
|
-
status = theme.fg("error", "terminate_timeout");
|
|
272
|
-
break;
|
|
273
|
-
case "killed":
|
|
274
|
-
status = theme.fg("warning", "killed");
|
|
275
|
-
break;
|
|
276
|
-
case "exited":
|
|
277
|
-
status = process.success
|
|
278
|
-
? theme.fg("success", "exit(0)")
|
|
279
|
-
: theme.fg("error", `exit(${process.exitCode ?? "?"})`);
|
|
280
|
-
break;
|
|
281
|
-
default:
|
|
282
|
-
status = theme.fg("muted", process.status);
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
lines.push(
|
|
286
|
-
` ${process.id} ${theme.fg("accent", `"${process.name}"`)}: ${truncateCmd(process.command)} [${status}] ${formatRuntime(process.startTime, process.endTime)}`,
|
|
287
|
-
);
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
fields.push(new Text(lines.join("\n"), 0, 0));
|
|
172
|
+
const { details } = result;
|
|
291
173
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
? theme.fg("success", "exit(0)")
|
|
301
|
-
: p.status === "exited"
|
|
302
|
-
? theme.fg("error", `exit(${p.exitCode ?? "?"})`)
|
|
303
|
-
: theme.fg("muted", p.status);
|
|
304
|
-
return `${theme.fg("accent", `"${p.name}"`)} [${s}]`;
|
|
305
|
-
})
|
|
306
|
-
.join(", ");
|
|
307
|
-
const more =
|
|
308
|
-
details.processes.length > 3
|
|
309
|
-
? theme.fg("muted", ` +${details.processes.length - 3} more`)
|
|
310
|
-
: "";
|
|
311
|
-
fields.push({
|
|
312
|
-
label: "Processes",
|
|
313
|
-
value: summary + more,
|
|
314
|
-
showCollapsed: true,
|
|
315
|
-
});
|
|
316
|
-
} else if (details.action === "logs" && details.logFiles) {
|
|
317
|
-
fields.push(
|
|
318
|
-
new Text(
|
|
319
|
-
[
|
|
320
|
-
theme.fg("success", "Log files:"),
|
|
321
|
-
` stdout: ${theme.fg("accent", details.logFiles.stdoutFile)}`,
|
|
322
|
-
` stderr: ${theme.fg("accent", details.logFiles.stderrFile)}`,
|
|
323
|
-
].join("\n"),
|
|
324
|
-
0,
|
|
325
|
-
0,
|
|
326
|
-
),
|
|
327
|
-
);
|
|
328
|
-
} else {
|
|
329
|
-
fields.push({
|
|
330
|
-
label: "Result",
|
|
331
|
-
value: details.message,
|
|
332
|
-
showCollapsed: true,
|
|
333
|
-
});
|
|
174
|
+
// Framework sets details to {} when tool throws.
|
|
175
|
+
// Detect by checking for missing expected fields.
|
|
176
|
+
if (!details?.action) {
|
|
177
|
+
const textBlock = result.content.find((c) => c.type === "text");
|
|
178
|
+
const errorMsg =
|
|
179
|
+
(textBlock?.type === "text" && textBlock.text) ||
|
|
180
|
+
"Tool execution failed";
|
|
181
|
+
return new Text(theme.fg("error", errorMsg), 0, 0);
|
|
334
182
|
}
|
|
335
183
|
|
|
336
|
-
const footerItems: Array<{
|
|
337
|
-
label: string;
|
|
338
|
-
value: string;
|
|
339
|
-
tone: "accent" | "success" | "error" | "warning" | "muted";
|
|
340
|
-
}> = [];
|
|
341
184
|
if (!details.success) {
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
185
|
+
return new ToolBody(
|
|
186
|
+
{
|
|
187
|
+
fields: [
|
|
188
|
+
{
|
|
189
|
+
label: "Error",
|
|
190
|
+
value: theme.fg("error", details.message),
|
|
191
|
+
showCollapsed: true,
|
|
192
|
+
},
|
|
193
|
+
],
|
|
194
|
+
},
|
|
195
|
+
options,
|
|
196
|
+
theme,
|
|
197
|
+
);
|
|
347
198
|
}
|
|
348
|
-
const footer =
|
|
349
|
-
footerItems.length > 0
|
|
350
|
-
? new ToolFooter(theme, { items: footerItems })
|
|
351
|
-
: undefined;
|
|
352
199
|
|
|
353
|
-
return
|
|
200
|
+
return renderActionResult(result, options, theme);
|
|
354
201
|
},
|
|
355
202
|
});
|
|
356
203
|
}
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
// Uses node:child_process directly instead of pi.exec() because process
|
|
2
|
+
// management requires long-lived streaming processes with stdin/stdout piping
|
|
3
|
+
// and detached process groups, which pi.exec() does not support.
|
|
1
4
|
import { type ChildProcess, spawn } from "node:child_process";
|
|
2
5
|
import { existsSync } from "node:fs";
|
|
3
6
|
import { isAbsolute } from "node:path";
|
package/src/utils/format.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { Theme } from "@mariozechner/pi-coding-agent";
|
|
1
2
|
import type { ProcessInfo } from "../constants";
|
|
2
3
|
|
|
3
4
|
export function formatRuntime(
|
|
@@ -40,3 +41,34 @@ export function truncateCmd(cmd: string, max = 40): string {
|
|
|
40
41
|
if (cmd.length <= max) return cmd;
|
|
41
42
|
return `${cmd.slice(0, max - 3)}...`;
|
|
42
43
|
}
|
|
44
|
+
|
|
45
|
+
export function formatTimestamp(ts: number | null): string {
|
|
46
|
+
if (!ts) return "-";
|
|
47
|
+
return new Date(ts).toISOString().replace("T", " ").slice(0, 19);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function formatStatusTag(
|
|
51
|
+
process: {
|
|
52
|
+
status: string;
|
|
53
|
+
success: boolean | null;
|
|
54
|
+
exitCode: number | null;
|
|
55
|
+
},
|
|
56
|
+
theme: Theme,
|
|
57
|
+
): string {
|
|
58
|
+
switch (process.status) {
|
|
59
|
+
case "running":
|
|
60
|
+
return theme.fg("accent", "running");
|
|
61
|
+
case "terminating":
|
|
62
|
+
return theme.fg("warning", "terminating");
|
|
63
|
+
case "terminate_timeout":
|
|
64
|
+
return theme.fg("error", "terminate_timeout");
|
|
65
|
+
case "killed":
|
|
66
|
+
return theme.fg("warning", "killed");
|
|
67
|
+
case "exited":
|
|
68
|
+
return process.success
|
|
69
|
+
? theme.fg("success", "exit(0)")
|
|
70
|
+
: theme.fg("error", `exit(${process.exitCode ?? "?"})`);
|
|
71
|
+
default:
|
|
72
|
+
return theme.fg("muted", process.status);
|
|
73
|
+
}
|
|
74
|
+
}
|
package/src/utils/index.ts
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
1
|
export { hasAnsi, stripAnsi } from "./ansi";
|
|
2
|
-
export {
|
|
2
|
+
export {
|
|
3
|
+
formatRuntime,
|
|
4
|
+
formatStatus,
|
|
5
|
+
formatStatusTag,
|
|
6
|
+
formatTimestamp,
|
|
7
|
+
truncateCmd,
|
|
8
|
+
} from "./format";
|
|
3
9
|
export { isProcessGroupAlive, killProcessGroup } from "./process-group";
|