@aliou/pi-processes 0.1.0 → 0.2.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.
@@ -0,0 +1,76 @@
1
+ import type { ExecuteResult } from "../../constants";
2
+ import type { ProcessManager } from "../../manager";
3
+
4
+ interface KillParams {
5
+ id?: string;
6
+ }
7
+
8
+ export async function executeKill(
9
+ params: KillParams,
10
+ manager: ProcessManager,
11
+ ): Promise<ExecuteResult> {
12
+ if (!params.id) {
13
+ return {
14
+ content: [{ type: "text", text: "Missing required parameter: id" }],
15
+ details: {
16
+ action: "kill",
17
+ success: false,
18
+ message: "Missing required parameter: id",
19
+ },
20
+ };
21
+ }
22
+
23
+ const proc = manager.find(params.id);
24
+ if (!proc) {
25
+ const message = `Process not found: ${params.id}`;
26
+ return {
27
+ content: [{ type: "text", text: message }],
28
+ details: {
29
+ action: "kill",
30
+ success: false,
31
+ message,
32
+ },
33
+ };
34
+ }
35
+
36
+ const result = await manager.kill(proc.id, {
37
+ signal: "SIGTERM",
38
+ timeoutMs: 3000,
39
+ });
40
+
41
+ if (result.ok) {
42
+ const message = `Terminated "${proc.name}" (${proc.id})`;
43
+ return {
44
+ content: [{ type: "text", text: message }],
45
+ details: {
46
+ action: "kill",
47
+ success: true,
48
+ message,
49
+ },
50
+ };
51
+ }
52
+
53
+ if (result.reason === "timeout") {
54
+ const message =
55
+ `SIGTERM timed out for "${proc.name}" (${proc.id}). ` +
56
+ "Run /processes and press x on terminate_timeout to force kill (SIGKILL).";
57
+ return {
58
+ content: [{ type: "text", text: message }],
59
+ details: {
60
+ action: "kill",
61
+ success: false,
62
+ message,
63
+ },
64
+ };
65
+ }
66
+
67
+ const message = `Failed to terminate "${proc.name}" (${proc.id})`;
68
+ return {
69
+ content: [{ type: "text", text: message }],
70
+ details: {
71
+ action: "kill",
72
+ success: false,
73
+ message,
74
+ },
75
+ };
76
+ }
@@ -0,0 +1,37 @@
1
+ import type { ExecuteResult } from "../../constants";
2
+ import type { ProcessManager } from "../../manager";
3
+ import { formatRuntime, formatStatus, truncateCmd } from "../../utils";
4
+
5
+ export function executeList(manager: ProcessManager): ExecuteResult {
6
+ const processes = manager.list();
7
+
8
+ if (processes.length === 0) {
9
+ return {
10
+ content: [{ type: "text", text: "No background processes running" }],
11
+ details: {
12
+ action: "list",
13
+ success: true,
14
+ message: "No background processes running",
15
+ processes: [],
16
+ },
17
+ };
18
+ }
19
+
20
+ const summary = processes
21
+ .map(
22
+ (p) =>
23
+ `${p.id} "${p.name}": ${truncateCmd(p.command)} [${formatStatus(p)}] ${formatRuntime(p.startTime, p.endTime)}`,
24
+ )
25
+ .join("\n");
26
+
27
+ const message = `${processes.length} process(es):\n${summary}`;
28
+ return {
29
+ content: [{ type: "text", text: message }],
30
+ details: {
31
+ action: "list",
32
+ success: true,
33
+ message,
34
+ processes,
35
+ },
36
+ };
37
+ }
@@ -0,0 +1,59 @@
1
+ import type { ExecuteResult } from "../../constants";
2
+ import type { ProcessManager } from "../../manager";
3
+
4
+ interface LogsParams {
5
+ id?: string;
6
+ }
7
+
8
+ export function executeLogs(
9
+ params: LogsParams,
10
+ manager: ProcessManager,
11
+ ): ExecuteResult {
12
+ if (!params.id) {
13
+ return {
14
+ content: [{ type: "text", text: "Missing required parameter: id" }],
15
+ details: {
16
+ action: "logs",
17
+ success: false,
18
+ message: "Missing required parameter: id",
19
+ },
20
+ };
21
+ }
22
+
23
+ const proc = manager.find(params.id);
24
+ if (!proc) {
25
+ const message = `Process not found: ${params.id}`;
26
+ return {
27
+ content: [{ type: "text", text: message }],
28
+ details: {
29
+ action: "logs",
30
+ success: false,
31
+ message,
32
+ },
33
+ };
34
+ }
35
+
36
+ const logFiles = manager.getLogFiles(proc.id);
37
+ if (!logFiles) {
38
+ const message = `Could not get log files for: ${proc.id}`;
39
+ return {
40
+ content: [{ type: "text", text: message }],
41
+ details: {
42
+ action: "logs",
43
+ success: false,
44
+ message,
45
+ },
46
+ };
47
+ }
48
+
49
+ const message = `Log files for "${proc.name}" (${proc.id}):\n stdout: ${logFiles.stdoutFile}\n stderr: ${logFiles.stderrFile}\n\nUse the read tool to inspect these files.`;
50
+ return {
51
+ content: [{ type: "text", text: message }],
52
+ details: {
53
+ action: "logs",
54
+ success: true,
55
+ message,
56
+ logFiles,
57
+ },
58
+ };
59
+ }
@@ -0,0 +1,73 @@
1
+ import type { ExecuteResult } from "../../constants";
2
+ import type { ProcessManager } from "../../manager";
3
+ import { formatStatus } from "../../utils";
4
+
5
+ interface OutputParams {
6
+ id?: string;
7
+ }
8
+
9
+ export function executeOutput(
10
+ params: OutputParams,
11
+ manager: ProcessManager,
12
+ ): ExecuteResult {
13
+ if (!params.id) {
14
+ return {
15
+ content: [{ type: "text", text: "Missing required parameter: id" }],
16
+ details: {
17
+ action: "output",
18
+ success: false,
19
+ message: "Missing required parameter: id",
20
+ },
21
+ };
22
+ }
23
+
24
+ const proc = manager.find(params.id);
25
+ if (!proc) {
26
+ const message = `Process not found: ${params.id}`;
27
+ return {
28
+ content: [{ type: "text", text: message }],
29
+ details: {
30
+ action: "output",
31
+ success: false,
32
+ message,
33
+ },
34
+ };
35
+ }
36
+
37
+ const output = manager.getOutput(proc.id);
38
+ if (!output) {
39
+ const message = `Could not read output for: ${proc.id}`;
40
+ return {
41
+ content: [{ type: "text", text: message }],
42
+ details: {
43
+ action: "output",
44
+ success: false,
45
+ message,
46
+ },
47
+ };
48
+ }
49
+
50
+ const stdoutLines = output.stdout.length;
51
+ const stderrLines = output.stderr.length;
52
+ const message = `"${proc.name}" (${proc.id}) [${formatStatus(proc)}]: ${stdoutLines} stdout lines, ${stderrLines} stderr lines`;
53
+
54
+ const outputParts: string[] = [message];
55
+ if (output.stdout.length > 0) {
56
+ outputParts.push("\n--- stdout (last 100 lines) ---");
57
+ outputParts.push(...output.stdout.slice(-100));
58
+ }
59
+ if (output.stderr.length > 0) {
60
+ outputParts.push("\n--- stderr (last 100 lines) ---");
61
+ outputParts.push(...output.stderr.slice(-100));
62
+ }
63
+
64
+ return {
65
+ content: [{ type: "text", text: outputParts.join("\n") }],
66
+ details: {
67
+ action: "output",
68
+ success: true,
69
+ message,
70
+ output,
71
+ },
72
+ };
73
+ }
@@ -0,0 +1,55 @@
1
+ import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
2
+ import type { ExecuteResult } from "../../constants";
3
+ import type { ProcessManager } from "../../manager";
4
+
5
+ interface StartParams {
6
+ name?: string;
7
+ command?: string;
8
+ notifyOnSuccess?: boolean;
9
+ notifyOnFailure?: boolean;
10
+ notifyOnKill?: boolean;
11
+ }
12
+
13
+ export function executeStart(
14
+ params: StartParams,
15
+ manager: ProcessManager,
16
+ ctx: ExtensionContext,
17
+ ): ExecuteResult {
18
+ if (!params.name) {
19
+ return {
20
+ content: [{ type: "text", text: "Missing required parameter: name" }],
21
+ details: {
22
+ action: "start",
23
+ success: false,
24
+ message: "Missing required parameter: name",
25
+ },
26
+ };
27
+ }
28
+ if (!params.command) {
29
+ return {
30
+ content: [{ type: "text", text: "Missing required parameter: command" }],
31
+ details: {
32
+ action: "start",
33
+ success: false,
34
+ message: "Missing required parameter: command",
35
+ },
36
+ };
37
+ }
38
+
39
+ const proc = manager.start(params.name, params.command, ctx.cwd, {
40
+ notifyOnSuccess: params.notifyOnSuccess,
41
+ notifyOnFailure: params.notifyOnFailure,
42
+ notifyOnKill: params.notifyOnKill,
43
+ });
44
+
45
+ const message = `Started "${proc.name}" (${proc.id}, PID: ${proc.pid})\nLogs: ${proc.stdoutFile}`;
46
+ return {
47
+ content: [{ type: "text", text: message }],
48
+ details: {
49
+ action: "start",
50
+ success: true,
51
+ message,
52
+ process: proc,
53
+ },
54
+ };
55
+ }
package/tools/index.ts CHANGED
@@ -2,13 +2,15 @@ import { StringEnum } from "@mariozechner/pi-ai";
2
2
  import type {
3
3
  AgentToolResult,
4
4
  ExtensionAPI,
5
- ExtensionContext,
6
5
  Theme,
7
6
  ToolRenderResultOptions,
8
7
  } from "@mariozechner/pi-coding-agent";
9
8
  import { Text } from "@mariozechner/pi-tui";
10
9
  import { type Static, Type } from "@sinclair/typebox";
11
- import type { ProcessInfo, ProcessManager } from "../manager";
10
+ import type { ProcessesDetails } from "../constants";
11
+ import type { ProcessManager } from "../manager";
12
+ import { formatRuntime, truncateCmd } from "../utils";
13
+ import { executeAction } from "./actions";
12
14
 
13
15
  const ProcessesParams = Type.Object({
14
16
  action: StringEnum(
@@ -24,7 +26,7 @@ const ProcessesParams = Type.Object({
24
26
  name: Type.Optional(
25
27
  Type.String({
26
28
  description:
27
- "Friendly name for the process (optional for start, e.g. 'backend-dev', 'test-runner')",
29
+ "Friendly name for the process (required for start, e.g. 'backend-dev', 'test-runner')",
28
30
  }),
29
31
  ),
30
32
  id: Type.Optional(
@@ -55,56 +57,12 @@ const ProcessesParams = Type.Object({
55
57
 
56
58
  type ProcessesParamsType = Static<typeof ProcessesParams>;
57
59
 
58
- interface ProcessesDetails {
59
- action: string;
60
- success: boolean;
61
- message: string;
62
- process?: ProcessInfo;
63
- processes?: ProcessInfo[];
64
- output?: { stdout: string[]; stderr: string[]; status: string };
65
- logFiles?: { stdoutFile: string; stderrFile: string };
66
- cleared?: number;
67
- }
68
-
69
- interface ExecuteResult {
70
- content: Array<{ type: "text"; text: string }>;
71
- details: ProcessesDetails;
72
- }
73
-
74
- function formatRuntime(startTime: number, endTime: number | null): string {
75
- const end = endTime ?? Date.now();
76
- const ms = end - startTime;
77
- const seconds = Math.floor(ms / 1000);
78
- const minutes = Math.floor(seconds / 60);
79
- const hours = Math.floor(minutes / 60);
80
-
81
- if (hours > 0) {
82
- return `${hours}h ${minutes % 60}m`;
83
- }
84
- if (minutes > 0) {
85
- return `${minutes}m ${seconds % 60}s`;
86
- }
87
- return `${seconds}s`;
88
- }
89
-
90
- function formatStatus(proc: ProcessInfo): string {
91
- if (proc.status === "running") return "running";
92
- if (proc.status === "killed") return "killed";
93
- if (proc.success) return "exited(0)";
94
- return `exited(${proc.exitCode ?? "?"})`;
95
- }
96
-
97
- function truncateCmd(cmd: string, max = 40): string {
98
- if (cmd.length <= max) return cmd;
99
- return `${cmd.slice(0, max - 3)}...`;
100
- }
101
-
102
60
  export function setupProcessesTools(pi: ExtensionAPI, manager: ProcessManager) {
103
61
  pi.registerTool<typeof ProcessesParams, ProcessesDetails>({
104
62
  name: "processes",
105
63
  label: "Processes",
106
64
  description: `Manage background processes. Actions:
107
- - start: Run command in background (requires 'command', optional 'name' for friendly display name)
65
+ - start: Run command in background (requires 'name' and 'command')
108
66
  - notifyOnSuccess (default: false): Get notified when process completes successfully
109
67
  - notifyOnFailure (default: true): Get notified when process crashes/fails
110
68
  - notifyOnKill (default: false): Get notified if killed by external signal (killing via tool never notifies)
@@ -120,265 +78,8 @@ Note: User always sees notifications in UI. Notification preferences only contro
120
78
 
121
79
  parameters: ProcessesParams,
122
80
 
123
- async execute(
124
- _toolCallId: string,
125
- params: ProcessesParamsType,
126
- _onUpdate: unknown,
127
- ctx: ExtensionContext,
128
- _signal?: AbortSignal,
129
- ): Promise<ExecuteResult> {
130
- switch (params.action) {
131
- case "start": {
132
- if (!params.command) {
133
- return {
134
- content: [
135
- { type: "text", text: "Missing required parameter: command" },
136
- ],
137
- details: {
138
- action: "start",
139
- success: false,
140
- message: "Missing required parameter: command",
141
- },
142
- };
143
- }
144
- const proc = manager.start(params.command, ctx.cwd, params.name, {
145
- notifyOnSuccess: params.notifyOnSuccess,
146
- notifyOnFailure: params.notifyOnFailure,
147
- notifyOnKill: params.notifyOnKill,
148
- });
149
- const message = `Started "${proc.name}" (${proc.id}, PID: ${proc.pid})\nLogs: ${proc.stdoutFile}`;
150
- return {
151
- content: [{ type: "text", text: message }],
152
- details: {
153
- action: "start",
154
- success: true,
155
- message,
156
- process: proc,
157
- },
158
- };
159
- }
160
-
161
- case "list": {
162
- const processes = manager.list();
163
- if (processes.length === 0) {
164
- return {
165
- content: [
166
- { type: "text", text: "No background processes running" },
167
- ],
168
- details: {
169
- action: "list",
170
- success: true,
171
- message: "No background processes running",
172
- processes: [],
173
- },
174
- };
175
- }
176
- const summary = processes
177
- .map(
178
- (p) =>
179
- `${p.id} "${p.name}": ${truncateCmd(p.command)} [${formatStatus(p)}] ${formatRuntime(p.startTime, p.endTime)}`,
180
- )
181
- .join("\n");
182
- const message = `${processes.length} process(es):\n${summary}`;
183
- return {
184
- content: [{ type: "text", text: message }],
185
- details: {
186
- action: "list",
187
- success: true,
188
- message,
189
- processes,
190
- },
191
- };
192
- }
193
-
194
- case "output": {
195
- if (!params.id) {
196
- return {
197
- content: [
198
- { type: "text", text: "Missing required parameter: id" },
199
- ],
200
- details: {
201
- action: "output",
202
- success: false,
203
- message: "Missing required parameter: id",
204
- },
205
- };
206
- }
207
- const proc = manager.find(params.id);
208
- if (!proc) {
209
- const message = `Process not found: ${params.id}`;
210
- return {
211
- content: [{ type: "text", text: message }],
212
- details: {
213
- action: "output",
214
- success: false,
215
- message,
216
- },
217
- };
218
- }
219
- const output = manager.getOutput(proc.id);
220
- if (!output) {
221
- const message = `Could not read output for: ${proc.id}`;
222
- return {
223
- content: [{ type: "text", text: message }],
224
- details: {
225
- action: "output",
226
- success: false,
227
- message,
228
- },
229
- };
230
- }
231
- const stdoutLines = output.stdout.length;
232
- const stderrLines = output.stderr.length;
233
- const message = `"${proc.name}" (${proc.id}) [${formatStatus(proc)}]: ${stdoutLines} stdout lines, ${stderrLines} stderr lines`;
234
-
235
- const outputParts: string[] = [message];
236
- if (output.stdout.length > 0) {
237
- outputParts.push("\n--- stdout (last 100 lines) ---");
238
- outputParts.push(...output.stdout.slice(-100));
239
- }
240
- if (output.stderr.length > 0) {
241
- outputParts.push("\n--- stderr (last 100 lines) ---");
242
- outputParts.push(...output.stderr.slice(-100));
243
- }
244
-
245
- return {
246
- content: [{ type: "text", text: outputParts.join("\n") }],
247
- details: {
248
- action: "output",
249
- success: true,
250
- message,
251
- output,
252
- },
253
- };
254
- }
255
-
256
- case "logs": {
257
- if (!params.id) {
258
- return {
259
- content: [
260
- { type: "text", text: "Missing required parameter: id" },
261
- ],
262
- details: {
263
- action: "logs",
264
- success: false,
265
- message: "Missing required parameter: id",
266
- },
267
- };
268
- }
269
- const proc = manager.find(params.id);
270
- if (!proc) {
271
- const message = `Process not found: ${params.id}`;
272
- return {
273
- content: [{ type: "text", text: message }],
274
- details: {
275
- action: "logs",
276
- success: false,
277
- message,
278
- },
279
- };
280
- }
281
- const logFiles = manager.getLogFiles(proc.id);
282
- if (!logFiles) {
283
- const message = `Could not get log files for: ${proc.id}`;
284
- return {
285
- content: [{ type: "text", text: message }],
286
- details: {
287
- action: "logs",
288
- success: false,
289
- message,
290
- },
291
- };
292
- }
293
- const message = `Log files for "${proc.name}" (${proc.id}):\n stdout: ${logFiles.stdoutFile}\n stderr: ${logFiles.stderrFile}\n\nUse the read tool to inspect these files.`;
294
- return {
295
- content: [{ type: "text", text: message }],
296
- details: {
297
- action: "logs",
298
- success: true,
299
- message,
300
- logFiles,
301
- },
302
- };
303
- }
304
-
305
- case "kill": {
306
- if (!params.id) {
307
- return {
308
- content: [
309
- { type: "text", text: "Missing required parameter: id" },
310
- ],
311
- details: {
312
- action: "kill",
313
- success: false,
314
- message: "Missing required parameter: id",
315
- },
316
- };
317
- }
318
- const proc = manager.find(params.id);
319
- if (!proc) {
320
- const message = `Process not found: ${params.id}`;
321
- return {
322
- content: [{ type: "text", text: message }],
323
- details: {
324
- action: "kill",
325
- success: false,
326
- message,
327
- },
328
- };
329
- }
330
- const killed = manager.kill(proc.id);
331
- if (killed) {
332
- const message = `Killed "${proc.name}" (${proc.id})`;
333
- return {
334
- content: [{ type: "text", text: message }],
335
- details: {
336
- action: "kill",
337
- success: true,
338
- message,
339
- },
340
- };
341
- }
342
- const message = `Failed to kill "${proc.name}" (${proc.id})`;
343
- return {
344
- content: [{ type: "text", text: message }],
345
- details: {
346
- action: "kill",
347
- success: false,
348
- message,
349
- },
350
- };
351
- }
352
-
353
- case "clear": {
354
- const cleared = manager.clearFinished();
355
- const message =
356
- cleared > 0
357
- ? `Cleared ${cleared} finished process(es)`
358
- : "No finished processes to clear";
359
- return {
360
- content: [{ type: "text", text: message }],
361
- details: {
362
- action: "clear",
363
- success: true,
364
- message,
365
- cleared,
366
- },
367
- };
368
- }
369
-
370
- default:
371
- return {
372
- content: [
373
- { type: "text", text: `Unknown action: ${params.action}` },
374
- ],
375
- details: {
376
- action: params.action,
377
- success: false,
378
- message: `Unknown action: ${params.action}`,
379
- },
380
- };
381
- }
81
+ async execute(_toolCallId, params, _onUpdate, ctx) {
82
+ return executeAction(params, manager, ctx);
382
83
  },
383
84
 
384
85
  renderCall(args: ProcessesParamsType, theme: Theme): Text {
@@ -491,12 +192,28 @@ Note: User always sees notifications in UI. Notification preferences only contro
491
192
  theme.fg("success", `${details.processes.length} process(es):`),
492
193
  );
493
194
  for (const p of details.processes) {
494
- const status =
495
- p.status === "running"
496
- ? theme.fg("accent", "running")
497
- : p.success
195
+ let status: string;
196
+ switch (p.status) {
197
+ case "running":
198
+ status = theme.fg("accent", "running");
199
+ break;
200
+ case "terminating":
201
+ status = theme.fg("warning", "terminating");
202
+ break;
203
+ case "terminate_timeout":
204
+ status = theme.fg("error", "terminate_timeout");
205
+ break;
206
+ case "killed":
207
+ status = theme.fg("warning", "killed");
208
+ break;
209
+ case "exited":
210
+ status = p.success
498
211
  ? theme.fg("success", "exit(0)")
499
- : theme.fg("error", `exit(${p.exitCode})`);
212
+ : theme.fg("error", `exit(${p.exitCode ?? "?"})`);
213
+ break;
214
+ default:
215
+ status = theme.fg("muted", p.status);
216
+ }
500
217
  lines.push(
501
218
  ` ${p.id} ${theme.fg("accent", `"${p.name}"`)}: ${truncateCmd(p.command)} [${status}] ${formatRuntime(p.startTime, p.endTime)}`,
502
219
  );
package/constants.ts DELETED
@@ -1,2 +0,0 @@
1
- // Custom message type for process update notifications
2
- export const MESSAGE_TYPE_PROCESS_UPDATE = "ad-process:update";