@aliou/pi-processes 0.6.3 → 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/src/manager.ts CHANGED
@@ -14,6 +14,8 @@ import type { Writable } from "node:stream";
14
14
  import {
15
15
  type KillResult,
16
16
  LIVE_STATUSES,
17
+ type LogWatch,
18
+ type LogWatchStream,
17
19
  type ManagerEvent,
18
20
  type ProcessInfo,
19
21
  type ProcessStatus,
@@ -23,12 +25,24 @@ import {
23
25
  import { isProcessGroupAlive, killProcessGroup } from "./utils";
24
26
  import { spawnCommand } from "./utils/command-executor";
25
27
 
28
+ interface ResolvedWatch {
29
+ index: number;
30
+ pattern: string;
31
+ regex: RegExp;
32
+ stream: LogWatchStream;
33
+ repeat: boolean;
34
+ fired: boolean;
35
+ }
36
+
26
37
  interface ManagedProcess extends ProcessInfo {
27
38
  process: ChildProcess;
28
39
  stdin: Writable | null;
29
40
  stdinClosed: boolean;
30
41
  lastSignalSent: NodeJS.Signals | null;
31
42
  combinedFile: string;
43
+ stdoutPendingLine: string;
44
+ stderrPendingLine: string;
45
+ watches: ResolvedWatch[];
32
46
  }
33
47
 
34
48
  interface ProcessManagerOptions {
@@ -43,6 +57,9 @@ export class ProcessManager {
43
57
  private watcher: ReturnType<typeof setInterval> | null = null;
44
58
  private getConfiguredShellPath: () => string | undefined;
45
59
 
60
+ private lastOutputEmitAt: Map<string, number> = new Map();
61
+ private pendingOutputEmit: Map<string, NodeJS.Timeout> = new Map();
62
+
46
63
  constructor(options?: ProcessManagerOptions) {
47
64
  this.logDir = join(tmpdir(), `pi-processes-${Date.now()}`);
48
65
  mkdirSync(this.logDir, { recursive: true });
@@ -59,6 +76,48 @@ export class ProcessManager {
59
76
  this.events.emit("event", event);
60
77
  }
61
78
 
79
+ private notifyOutputChanged(id: string): void {
80
+ const now = Date.now();
81
+ const lastEmit = this.lastOutputEmitAt.get(id) ?? 0;
82
+ const elapsed = now - lastEmit;
83
+
84
+ if (elapsed >= 100) {
85
+ this.lastOutputEmitAt.set(id, now);
86
+ this.emit({ type: "process_output_changed", id });
87
+ return;
88
+ }
89
+
90
+ if (!this.pendingOutputEmit.has(id)) {
91
+ const delay = 100 - elapsed;
92
+ const timeout = setTimeout(() => {
93
+ this.pendingOutputEmit.delete(id);
94
+ // Invariant: every path that removes a process from `this.processes`
95
+ // must call `clearOutputChangedState(id)` first, which clears this
96
+ // timeout. This guard is a safety net, not a primary mechanism.
97
+ if (!this.processes.has(id)) return;
98
+ this.lastOutputEmitAt.set(id, Date.now());
99
+ this.emit({ type: "process_output_changed", id });
100
+ }, delay);
101
+ this.pendingOutputEmit.set(id, timeout);
102
+ }
103
+ }
104
+
105
+ private flushPendingOutputChanged(id: string): void {
106
+ const timeout = this.pendingOutputEmit.get(id);
107
+ if (!timeout) return;
108
+ clearTimeout(timeout);
109
+ this.pendingOutputEmit.delete(id);
110
+ this.lastOutputEmitAt.set(id, Date.now());
111
+ this.emit({ type: "process_output_changed", id });
112
+ }
113
+
114
+ private clearOutputChangedState(id: string): void {
115
+ const timeout = this.pendingOutputEmit.get(id);
116
+ if (timeout) clearTimeout(timeout);
117
+ this.pendingOutputEmit.delete(id);
118
+ this.lastOutputEmitAt.delete(id);
119
+ }
120
+
62
121
  private transition(managed: ManagedProcess, next: ProcessStatus): void {
63
122
  if (managed.status === next) return;
64
123
  managed.status = next;
@@ -107,6 +166,9 @@ export class ProcessManager {
107
166
  managed.endTime = Date.now();
108
167
  }
109
168
 
169
+ this.flushPendingOutputChanged(managed.id);
170
+ this.flushPendingLines(managed);
171
+
110
172
  if (managed.lastSignalSent) {
111
173
  managed.success = false;
112
174
  managed.exitCode = null;
@@ -125,6 +187,7 @@ export class ProcessManager {
125
187
  cwd: string,
126
188
  options?: StartOptions,
127
189
  ): ProcessInfo {
190
+ const resolvedWatches = this.resolveLogWatches(options?.logWatches);
128
191
  const id = `proc_${++this.counter}`;
129
192
  const stdoutFile = join(this.logDir, `${id}-stdout.log`);
130
193
  const stderrFile = join(this.logDir, `${id}-stderr.log`);
@@ -159,6 +222,9 @@ export class ProcessManager {
159
222
  stdin: child.stdin,
160
223
  stdinClosed: false,
161
224
  lastSignalSent: null,
225
+ stdoutPendingLine: "",
226
+ stderrPendingLine: "",
227
+ watches: resolvedWatches,
162
228
  };
163
229
 
164
230
  this.processes.set(id, managed);
@@ -179,15 +245,11 @@ export class ProcessManager {
179
245
  child.stdout?.on("data", (data: Buffer) => {
180
246
  try {
181
247
  appendFileSync(stdoutFile, data);
182
- const lines = data.toString().split("\n");
183
- // The last element after split is either empty (if data ended with \n)
184
- // or a partial line. We write all parts with the prefix and newline.
185
- const tagged = lines
186
- .map((line, i) =>
187
- i < lines.length - 1 ? `1:${line}\n` : line ? `1:${line}\n` : "",
188
- )
189
- .join("");
248
+ const lines = this.extractCompleteLines(managed, "stdout", data);
249
+ const tagged = lines.map((line) => `1:${line}\n`).join("");
190
250
  if (tagged) appendFileSync(combinedFile, tagged);
251
+ this.matchWatches(managed, "stdout", lines);
252
+ this.notifyOutputChanged(id);
191
253
  } catch {
192
254
  // Ignore
193
255
  }
@@ -196,13 +258,11 @@ export class ProcessManager {
196
258
  child.stderr?.on("data", (data: Buffer) => {
197
259
  try {
198
260
  appendFileSync(stderrFile, data);
199
- const lines = data.toString().split("\n");
200
- const tagged = lines
201
- .map((line, i) =>
202
- i < lines.length - 1 ? `2:${line}\n` : line ? `2:${line}\n` : "",
203
- )
204
- .join("");
261
+ const lines = this.extractCompleteLines(managed, "stderr", data);
262
+ const tagged = lines.map((line) => `2:${line}\n`).join("");
205
263
  if (tagged) appendFileSync(combinedFile, tagged);
264
+ this.matchWatches(managed, "stderr", lines);
265
+ this.notifyOutputChanged(id);
206
266
  } catch {
207
267
  // Ignore
208
268
  }
@@ -215,6 +275,9 @@ export class ProcessManager {
215
275
  managed.endTime = Date.now();
216
276
  managed.success = code === 0;
217
277
 
278
+ this.flushPendingOutputChanged(id);
279
+ this.flushPendingLines(managed);
280
+
218
281
  if (signal) {
219
282
  this.transition(managed, "killed");
220
283
  } else {
@@ -233,6 +296,8 @@ export class ProcessManager {
233
296
  managed.exitCode = -1;
234
297
  managed.success = false;
235
298
  managed.endTime = Date.now();
299
+ this.flushPendingOutputChanged(id);
300
+ this.flushPendingLines(managed);
236
301
  this.transition(managed, "exited");
237
302
  }
238
303
  });
@@ -254,22 +319,6 @@ export class ProcessManager {
254
319
  return managed ? this.toProcessInfo(managed) : null;
255
320
  }
256
321
 
257
- find(query: string): ProcessInfo | null {
258
- const byId = this.processes.get(query);
259
- if (byId) return this.toProcessInfo(byId);
260
-
261
- const queryLower = query.toLowerCase();
262
- for (const managed of this.processes.values()) {
263
- if (managed.name.toLowerCase().includes(queryLower)) {
264
- return this.toProcessInfo(managed);
265
- }
266
- if (managed.command.toLowerCase().includes(queryLower)) {
267
- return this.toProcessInfo(managed);
268
- }
269
- }
270
- return null;
271
- }
272
-
273
322
  getOutput(
274
323
  id: string,
275
324
  tailLines = 100,
@@ -405,6 +454,8 @@ export class ProcessManager {
405
454
  managed.success = false;
406
455
  }
407
456
 
457
+ this.flushPendingOutputChanged(id);
458
+ this.flushPendingLines(managed);
408
459
  this.transition(managed, "killed");
409
460
  return { ok: true, info: this.toProcessInfo(managed) };
410
461
  }
@@ -468,6 +519,7 @@ export class ProcessManager {
468
519
  // Ignore
469
520
  }
470
521
 
522
+ this.clearOutputChangedState(id);
471
523
  this.processes.delete(id);
472
524
  cleared++;
473
525
  }
@@ -501,6 +553,12 @@ export class ProcessManager {
501
553
  cleanup(): void {
502
554
  this.stopWatcher();
503
555
 
556
+ for (const timeout of this.pendingOutputEmit.values()) {
557
+ clearTimeout(timeout);
558
+ }
559
+ this.pendingOutputEmit.clear();
560
+ this.lastOutputEmitAt.clear();
561
+
504
562
  for (const p of this.processes.values()) {
505
563
  if (!LIVE_STATUSES.has(p.status)) continue;
506
564
  try {
@@ -531,6 +589,132 @@ export class ProcessManager {
531
589
  }
532
590
  }
533
591
 
592
+ private resolveLogWatches(input?: LogWatch[]): ResolvedWatch[] {
593
+ if (!input || input.length === 0) return [];
594
+
595
+ return input.map((watch, index) => {
596
+ const pattern = watch.pattern?.trim();
597
+ if (!pattern) {
598
+ throw new Error(`logWatches[${index}].pattern is required`);
599
+ }
600
+
601
+ let regex: RegExp;
602
+ try {
603
+ regex = new RegExp(pattern);
604
+ } catch (error) {
605
+ const message =
606
+ error instanceof Error ? error.message : "invalid regular expression";
607
+ throw new Error(
608
+ `Invalid log watch pattern at logWatches[${index}]: ${message}`,
609
+ );
610
+ }
611
+
612
+ const stream = watch.stream ?? "both";
613
+ if (stream !== "stdout" && stream !== "stderr" && stream !== "both") {
614
+ throw new Error(
615
+ `Invalid logWatches[${index}].stream: ${stream}. Expected stdout, stderr, or both`,
616
+ );
617
+ }
618
+
619
+ return {
620
+ index,
621
+ pattern,
622
+ regex,
623
+ stream,
624
+ repeat: watch.repeat ?? false,
625
+ fired: false,
626
+ };
627
+ });
628
+ }
629
+
630
+ private extractCompleteLines(
631
+ managed: ManagedProcess,
632
+ source: "stdout" | "stderr",
633
+ data: Buffer,
634
+ ): string[] {
635
+ const chunk = data.toString();
636
+ const pending =
637
+ source === "stdout"
638
+ ? managed.stdoutPendingLine
639
+ : managed.stderrPendingLine;
640
+ const merged = pending + chunk;
641
+ const parts = merged.split(/\r?\n/);
642
+ const completeLines = parts.slice(0, -1);
643
+ const nextPending = parts[parts.length - 1] ?? "";
644
+
645
+ if (source === "stdout") {
646
+ managed.stdoutPendingLine = nextPending;
647
+ } else {
648
+ managed.stderrPendingLine = nextPending;
649
+ }
650
+
651
+ return completeLines;
652
+ }
653
+
654
+ private flushPendingLines(managed: ManagedProcess): void {
655
+ if (managed.stdoutPendingLine) {
656
+ try {
657
+ appendFileSync(
658
+ managed.combinedFile,
659
+ `1:${managed.stdoutPendingLine}\n`,
660
+ );
661
+ } catch {
662
+ // Ignore
663
+ }
664
+ this.matchWatches(managed, "stdout", [managed.stdoutPendingLine]);
665
+ managed.stdoutPendingLine = "";
666
+ }
667
+
668
+ if (managed.stderrPendingLine) {
669
+ try {
670
+ appendFileSync(
671
+ managed.combinedFile,
672
+ `2:${managed.stderrPendingLine}\n`,
673
+ );
674
+ } catch {
675
+ // Ignore
676
+ }
677
+ this.matchWatches(managed, "stderr", [managed.stderrPendingLine]);
678
+ managed.stderrPendingLine = "";
679
+ }
680
+ }
681
+
682
+ private matchWatches(
683
+ managed: ManagedProcess,
684
+ source: "stdout" | "stderr",
685
+ lines: string[],
686
+ ): void {
687
+ if (managed.watches.length === 0 || lines.length === 0) return;
688
+
689
+ for (const line of lines) {
690
+ for (const watch of managed.watches) {
691
+ if (!watch.repeat && watch.fired) continue;
692
+ if (watch.stream !== "both" && watch.stream !== source) continue;
693
+
694
+ if (!watch.regex.test(line)) continue;
695
+
696
+ watch.fired = true;
697
+
698
+ this.emit({
699
+ type: "process_watch_matched",
700
+ match: {
701
+ processId: managed.id,
702
+ processName: managed.name,
703
+ processCommand: managed.command,
704
+ source,
705
+ line,
706
+ watch: {
707
+ index: watch.index,
708
+ pattern: watch.pattern,
709
+ stream: watch.stream,
710
+ repeat: watch.repeat,
711
+ },
712
+ },
713
+ });
714
+ }
715
+ }
716
+ }
717
+
534
718
  private readTailLines(filePath: string, lines: number): string[] {
535
719
  try {
536
720
  const content = readFileSync(filePath, "utf-8");
@@ -0,0 +1,148 @@
1
+ import { ToolCallHeader } from "@aliou/pi-utils-ui";
2
+ import type { Theme } from "@mariozechner/pi-coding-agent";
3
+ import type { ExecuteResult, ProcessInfo } from "../../constants";
4
+
5
+ interface DebugParams {
6
+ preview?: "start" | "list" | "output" | "logs" | "error";
7
+ }
8
+
9
+ export function renderDebugCall(
10
+ args: DebugParams,
11
+ theme: Theme,
12
+ ): ToolCallHeader {
13
+ return new ToolCallHeader(
14
+ {
15
+ toolName: "Process",
16
+ action: "debug_preview",
17
+ mainArg: args.preview,
18
+ },
19
+ theme,
20
+ );
21
+ }
22
+
23
+ function mockProcess(overrides?: Partial<ProcessInfo>): ProcessInfo {
24
+ const now = Date.now();
25
+ return {
26
+ id: "proc_42",
27
+ name: "demo-server",
28
+ pid: 4242,
29
+ command: "pnpm dev --port 3000",
30
+ cwd: "/tmp/demo",
31
+ startTime: now - 12_000,
32
+ endTime: null,
33
+ status: "running",
34
+ exitCode: null,
35
+ success: null,
36
+ stdoutFile: "/tmp/pi-processes-demo/proc_42-stdout.log",
37
+ stderrFile: "/tmp/pi-processes-demo/proc_42-stderr.log",
38
+ alertOnSuccess: false,
39
+ alertOnFailure: true,
40
+ alertOnKill: false,
41
+ ...overrides,
42
+ };
43
+ }
44
+
45
+ /**
46
+ * Temporary no-side-effect previews for process tool renderers.
47
+ * Remove before release.
48
+ */
49
+ export function executeDebugPreview(params: DebugParams): ExecuteResult {
50
+ const preview = params.preview ?? "start";
51
+
52
+ if (preview === "start") {
53
+ const process = mockProcess();
54
+ const message = [
55
+ `Started "${process.name}" (${process.id}, PID: ${process.pid})`,
56
+ "Log files:",
57
+ ` stdout: ${process.stdoutFile}`,
58
+ ` stderr: ${process.stderrFile}`,
59
+ ].join("\n");
60
+ return {
61
+ content: [{ type: "text", text: message }],
62
+ details: {
63
+ action: "start",
64
+ success: true,
65
+ message,
66
+ process,
67
+ },
68
+ };
69
+ }
70
+
71
+ if (preview === "list") {
72
+ const processes = [
73
+ mockProcess(),
74
+ mockProcess({
75
+ id: "proc_11",
76
+ name: "builder",
77
+ pid: 1011,
78
+ command: "pnpm build --watch",
79
+ }),
80
+ mockProcess({
81
+ id: "proc_10",
82
+ name: "tests",
83
+ pid: 1010,
84
+ command: "pnpm test",
85
+ status: "exited",
86
+ success: false,
87
+ endTime: Date.now() - 3_000,
88
+ exitCode: 1,
89
+ }),
90
+ ];
91
+
92
+ return {
93
+ content: [{ type: "text", text: "Debug preview: list" }],
94
+ details: {
95
+ action: "list",
96
+ success: true,
97
+ message: "Debug preview: list",
98
+ processes,
99
+ },
100
+ };
101
+ }
102
+
103
+ if (preview === "output") {
104
+ return {
105
+ content: [{ type: "text", text: "Debug preview: output" }],
106
+ details: {
107
+ action: "output",
108
+ success: true,
109
+ message:
110
+ '"demo-server" (proc_42) [running]: 4 stdout lines, 2 stderr lines',
111
+ output: {
112
+ status: "running",
113
+ stdout: [
114
+ "starting...",
115
+ "loading config",
116
+ "ready on http://localhost:3000",
117
+ "watching for changes",
118
+ ],
119
+ stderr: [
120
+ "warn: deprecated option in config",
121
+ "error: simulated stack trace line",
122
+ ],
123
+ },
124
+ logFiles: {
125
+ stdoutFile: "/tmp/pi-processes-demo/proc_42-stdout.log",
126
+ stderrFile: "/tmp/pi-processes-demo/proc_42-stderr.log",
127
+ },
128
+ },
129
+ };
130
+ }
131
+
132
+ if (preview === "logs") {
133
+ return {
134
+ content: [{ type: "text", text: "Debug preview: logs" }],
135
+ details: {
136
+ action: "logs",
137
+ success: true,
138
+ message: "Debug preview: logs",
139
+ logFiles: {
140
+ stdoutFile: "/tmp/pi-processes-demo/proc_42-stdout.log",
141
+ stderrFile: "/tmp/pi-processes-demo/proc_42-stderr.log",
142
+ },
143
+ },
144
+ };
145
+ }
146
+
147
+ throw new Error("Invalid logWatches[0].pattern: Unterminated group");
148
+ }
@@ -1,16 +1,30 @@
1
- import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
2
- import type { ExecuteResult } from "../../constants";
1
+ import { ToolBody, ToolCallHeader } from "@aliou/pi-utils-ui";
2
+ import type {
3
+ AgentToolResult,
4
+ ExtensionContext,
5
+ Theme,
6
+ ToolRenderResultOptions,
7
+ } from "@mariozechner/pi-coding-agent";
8
+ import type { Component } from "@mariozechner/pi-tui";
9
+ import type {
10
+ ExecuteResult,
11
+ ProcessAction,
12
+ ProcessesDetails,
13
+ } from "../../constants";
3
14
  import type { ProcessManager } from "../../manager";
4
15
  import { executeClear } from "./clear";
5
- import { executeKill } from "./kill";
6
- import { executeList } from "./list";
7
- import { executeLogs } from "./logs";
8
- import { executeOutput } from "./output";
9
- import { executeStart } from "./start";
10
- import { executeWrite } from "./write";
16
+ import { executeDebugPreview, renderDebugCall } from "./debug";
17
+ import { executeKill, renderKillCall } from "./kill";
18
+ import { executeList, renderListResult } from "./list";
19
+ import { executeLogs, renderLogsCall, renderLogsResult } from "./logs";
20
+ import { executeOutput, renderOutputCall, renderOutputResult } from "./output";
21
+ import { executeStart, renderStartCall, renderStartResult } from "./start";
22
+ import { executeWrite, renderWriteCall } from "./write";
23
+
24
+ const DEBUG_PREVIEW_ENABLED = process.env.PI_PROCESSES_DEBUG_PREVIEW === "1";
11
25
 
12
26
  interface ActionParams {
13
- action: string;
27
+ action: ProcessAction | string;
14
28
  command?: string;
15
29
  name?: string;
16
30
  id?: string;
@@ -19,6 +33,12 @@ interface ActionParams {
19
33
  alertOnSuccess?: boolean;
20
34
  alertOnFailure?: boolean;
21
35
  alertOnKill?: boolean;
36
+ logWatches?: Array<{
37
+ pattern: string;
38
+ stream?: "stdout" | "stderr" | "both";
39
+ repeat?: boolean;
40
+ }>;
41
+ preview?: "start" | "list" | "output" | "logs" | "error";
22
42
  }
23
43
 
24
44
  export async function executeAction(
@@ -41,14 +61,89 @@ export async function executeAction(
41
61
  return executeClear(manager);
42
62
  case "write":
43
63
  return executeWrite(params, manager);
64
+ case "debug_preview":
65
+ if (!DEBUG_PREVIEW_ENABLED) {
66
+ throw new Error(
67
+ "Action 'debug_preview' is disabled. Set PI_PROCESSES_DEBUG_PREVIEW=1 to enable.",
68
+ );
69
+ }
70
+ return executeDebugPreview(params);
44
71
  default:
45
72
  return {
46
73
  content: [{ type: "text", text: `Unknown action: ${params.action}` }],
47
74
  details: {
48
- action: params.action,
75
+ action: params.action as ProcessAction,
49
76
  success: false,
50
77
  message: `Unknown action: ${params.action}`,
51
78
  },
52
79
  };
53
80
  }
54
81
  }
82
+
83
+ export function renderActionCall(
84
+ args: ActionParams,
85
+ theme: Theme,
86
+ ): Component | undefined {
87
+ switch (args.action) {
88
+ case "start":
89
+ return renderStartCall(args, theme);
90
+ case "output":
91
+ return renderOutputCall(args, theme);
92
+ case "logs":
93
+ return renderLogsCall(args, theme);
94
+ case "kill":
95
+ return renderKillCall(args, theme);
96
+ case "write":
97
+ return renderWriteCall(args, theme);
98
+ case "debug_preview":
99
+ return renderDebugCall(args, theme);
100
+ default:
101
+ return new ToolCallHeader(
102
+ { toolName: "Process", action: args.action },
103
+ theme,
104
+ );
105
+ }
106
+ }
107
+
108
+ export function renderActionResult(
109
+ result: AgentToolResult<ProcessesDetails>,
110
+ options: ToolRenderResultOptions,
111
+ theme: Theme,
112
+ ): Component | undefined {
113
+ const { details } = result;
114
+
115
+ if (!details) {
116
+ return undefined;
117
+ }
118
+
119
+ switch (details.action) {
120
+ case "start":
121
+ return renderStartResult(result, options, theme);
122
+ case "list":
123
+ return renderListResult(result, options, theme);
124
+ case "output":
125
+ return renderOutputResult(result, options, theme);
126
+ case "logs":
127
+ return renderLogsResult(result, options, theme);
128
+ case "kill":
129
+ case "write":
130
+ case "clear":
131
+ case "debug_preview":
132
+ // Default rendering for these actions
133
+ return new ToolBody(
134
+ {
135
+ fields: [
136
+ {
137
+ label: "Result",
138
+ value: details.message,
139
+ showCollapsed: true,
140
+ },
141
+ ],
142
+ },
143
+ options,
144
+ theme,
145
+ );
146
+ default:
147
+ return undefined;
148
+ }
149
+ }
@@ -1,3 +1,5 @@
1
+ import { ToolCallHeader } from "@aliou/pi-utils-ui";
2
+ import type { Theme } from "@mariozechner/pi-coding-agent";
1
3
  import type { ExecuteResult } from "../../constants";
2
4
  import type { ProcessManager } from "../../manager";
3
5
 
@@ -5,6 +7,17 @@ interface KillParams {
5
7
  id?: string;
6
8
  }
7
9
 
10
+ export function renderKillCall(args: KillParams, theme: Theme): ToolCallHeader {
11
+ return new ToolCallHeader(
12
+ {
13
+ toolName: "Process",
14
+ action: "kill",
15
+ mainArg: args.id,
16
+ },
17
+ theme,
18
+ );
19
+ }
20
+
8
21
  export async function executeKill(
9
22
  params: KillParams,
10
23
  manager: ProcessManager,
@@ -20,7 +33,7 @@ export async function executeKill(
20
33
  };
21
34
  }
22
35
 
23
- const proc = manager.find(params.id);
36
+ const proc = manager.get(params.id);
24
37
  if (!proc) {
25
38
  const message = `Process not found: ${params.id}`;
26
39
  return {
@@ -53,7 +66,7 @@ export async function executeKill(
53
66
  if (result.reason === "timeout") {
54
67
  const message =
55
68
  `SIGTERM timed out for "${proc.name}" (${proc.id}). ` +
56
- "Run /process:list and press x on terminate_timeout to force kill (SIGKILL).";
69
+ "Run /ps and press x on terminate_timeout to force kill (SIGKILL).";
57
70
  return {
58
71
  content: [{ type: "text", text: message }],
59
72
  details: {