@haemmid/pi-processes 0.9.0 → 0.9.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/CHANGELOG.md CHANGED
@@ -4,6 +4,23 @@ All notable changes to this project will be documented in this file.
4
4
 
5
5
  This project follows semantic versioning for public releases.
6
6
 
7
+ ## [0.9.1] - 2026-07-02
8
+
9
+ ### Added
10
+
11
+ - `output`, `logs`, and `kill` accept either `id` or `name` as process selector.
12
+ - `start` and `restart` return suggested next commands with `name="..."`.
13
+ - Shared `resolveSelector` utility for consistent validation across actions.
14
+
15
+ ### Changed
16
+
17
+ - Ambiguous name resolution now uses simpler error messages.
18
+ - Tool description updated to mention id/name duality.
19
+
20
+ ### Fixed
21
+
22
+ - `kill` test: message assertion updated for new ambiguous error format.
23
+
7
24
  ## [0.9.0] - 2026-07-01
8
25
 
9
26
  Initial public release of `@haemmid/pi-processes`.
package/README.md CHANGED
@@ -173,22 +173,25 @@ process start "pnpm dev" name="backend-dev" cwd="/path/to/project"
173
173
  process restart "pnpm dev" name="backend-dev"
174
174
  process restart "pnpm dev" name="backend-dev" force=true
175
175
  process list
176
- process output id="backend-dev"
176
+ process output id="proc_1"
177
+ process output name="backend-dev"
177
178
  process logs id="proc_1"
179
+ process logs name="backend-dev"
178
180
  process kill id="backend-dev"
179
- process kill id="proc_1" force=true
181
+ process kill name="backend-dev" force=true
180
182
  process clear
181
183
  ```
182
184
 
183
185
  ### Field rules
184
186
 
185
187
  - `start`/`restart` require `command` and `name`.
186
- - `output`, `logs`, and `kill` require `id`.
188
+ - `output`, `logs`, and `kill` accept either `id` (exact process ID) or `name` (process name).
187
189
  - `kill` accepts `force=true` to send `SIGKILL` instead of `SIGTERM`.
188
190
  - `start` refuses if a process with the same name is already running.
189
191
  - `restart` safely kills the existing process (awaited) before starting a new one.
190
192
  - `restart` accepts `force=true` to send `SIGKILL` instead of `SIGTERM`.
191
193
  - `start`/`restart` accept `cwd` to override the working directory (defaults to session cwd).
194
+ - `output`, `logs`, and `kill` return an error if both `id` and `name` are provided.
192
195
 
193
196
  ## Killing processes
194
197
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@haemmid/pi-processes",
3
- "version": "0.9.0",
3
+ "version": "0.9.1",
4
4
  "description": "Agent-facing background process manager for Pi and pi-web automation workflows.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -1,66 +1,36 @@
1
1
  import type { ExecuteResult } from "../../constants";
2
2
  import type { ProcessManager } from "../../manager";
3
3
  import { sanitizeLine } from "../../utils";
4
+ import { resolveSelector } from "./utils";
4
5
 
5
6
  interface KillParams {
6
7
  id?: string;
8
+ name?: string;
7
9
  force?: boolean;
8
10
  }
9
11
 
10
- function notFoundResult(id: string): ExecuteResult {
11
- const message = `Process not found: ${id}`;
12
- return {
13
- content: [{ type: "text", text: message }],
14
- details: {
15
- action: "kill",
16
- success: false,
17
- message,
18
- },
19
- };
20
- }
21
-
22
- function ambiguousResult(
23
- id: string,
24
- matches: Array<{ id: string; name: string }>,
25
- ): ExecuteResult {
26
- const choices = matches
27
- .map((match) => `${match.id} ("${sanitizeLine(match.name)}")`)
28
- .join(", ");
29
- const message =
30
- `Process name is ambiguous: ${id}. ` +
31
- `Use an exact process ID instead. Matches: ${choices}`;
32
- return {
33
- content: [{ type: "text", text: message }],
34
- details: {
35
- action: "kill",
36
- success: false,
37
- message,
38
- },
39
- };
40
- }
41
-
42
12
  export async function executeKill(
43
13
  params: KillParams,
44
14
  manager: ProcessManager,
45
15
  ): Promise<ExecuteResult> {
46
- if (!params.id) {
16
+ const error = resolveSelector(params, manager);
17
+ if (error) {
18
+ return { ...error, details: { ...error.details, action: "kill" } };
19
+ }
20
+
21
+ const query = params.id || params.name || "";
22
+ const resolved = manager.resolve(query);
23
+ if (!resolved.ok) {
47
24
  return {
48
- content: [{ type: "text", text: "Missing required parameter: id" }],
25
+ content: [{ type: "text", text: `Process not found: "${query}"` }],
49
26
  details: {
50
27
  action: "kill",
51
28
  success: false,
52
- message: "Missing required parameter: id",
29
+ message: `Process not found: "${query}"`,
53
30
  },
54
31
  };
55
32
  }
56
33
 
57
- const resolved = manager.resolve(params.id);
58
- if (!resolved.ok) {
59
- return resolved.reason === "ambiguous"
60
- ? ambiguousResult(params.id, resolved.matches ?? [])
61
- : notFoundResult(params.id);
62
- }
63
-
64
34
  const proc = resolved.info;
65
35
  const force = params.force ?? false;
66
36
  const signal = force ? "SIGKILL" : "SIGTERM";
@@ -1,52 +1,31 @@
1
1
  import type { ExecuteResult } from "../../constants";
2
2
  import type { ProcessManager } from "../../manager";
3
3
  import { sanitizeLine } from "../../utils";
4
+ import { resolveSelector } from "./utils";
4
5
 
5
6
  interface LogsParams {
6
7
  id?: string;
8
+ name?: string;
7
9
  }
8
10
 
9
11
  export function executeLogs(
10
12
  params: LogsParams,
11
13
  manager: ProcessManager,
12
14
  ): ExecuteResult {
13
- if (!params.id) {
14
- return {
15
- content: [{ type: "text", text: "Missing required parameter: id" }],
16
- details: {
17
- action: "logs",
18
- success: false,
19
- message: "Missing required parameter: id",
20
- },
21
- };
15
+ const error = resolveSelector(params, manager);
16
+ if (error) {
17
+ return { ...error, details: { ...error.details, action: "logs" } };
22
18
  }
23
19
 
24
- const resolved = manager.resolve(params.id);
20
+ const query = params.id || params.name || "";
21
+ const resolved = manager.resolve(query);
25
22
  if (!resolved.ok) {
26
- if (resolved.reason === "ambiguous") {
27
- const choices = (resolved.matches ?? [])
28
- .map((match) => `${match.id} ("${sanitizeLine(match.name)}")`)
29
- .join(", ");
30
- const message =
31
- `Process name is ambiguous: ${params.id}. ` +
32
- `Use an exact process ID instead. Matches: ${choices}`;
33
- return {
34
- content: [{ type: "text", text: message }],
35
- details: {
36
- action: "logs",
37
- success: false,
38
- message,
39
- },
40
- };
41
- }
42
-
43
- const message = `Process not found: ${params.id}`;
44
23
  return {
45
- content: [{ type: "text", text: message }],
24
+ content: [{ type: "text", text: `Process not found: "${query}"` }],
46
25
  details: {
47
26
  action: "logs",
48
27
  success: false,
49
- message,
28
+ message: `Process not found: "${query}"`,
50
29
  },
51
30
  };
52
31
  }
@@ -1,73 +1,39 @@
1
1
  import { configLoader } from "../../config";
2
- import {
3
- type ExecuteResult,
4
- LIVE_STATUSES,
5
- type ResolveProcessResult,
6
- } from "../../constants";
2
+ import { type ExecuteResult, LIVE_STATUSES } from "../../constants";
7
3
  import type { ProcessManager } from "../../manager";
8
4
  import { formatStatus, sanitizeLine } from "../../utils";
5
+ import { resolveSelector } from "./utils";
9
6
 
10
7
  const MAX_BYTES = 50 * 1024; // 50KB
11
8
 
12
9
  interface OutputParams {
13
10
  id?: string;
14
- }
15
-
16
- function resolveProcessResult(
17
- result: ResolveProcessResult,
18
- action: "output" | "logs",
19
- id: string,
20
- ): ExecuteResult | null {
21
- if (result.ok) return null;
22
-
23
- if (result.reason === "ambiguous") {
24
- const choices = (result.matches ?? [])
25
- .map((match) => `${match.id} ("${sanitizeLine(match.name)}")`)
26
- .join(", ");
27
- const message =
28
- `Process name is ambiguous: ${id}. ` +
29
- `Use an exact process ID instead. Matches: ${choices}`;
30
- return {
31
- content: [{ type: "text", text: message }],
32
- details: {
33
- action,
34
- success: false,
35
- message,
36
- },
37
- };
38
- }
39
-
40
- const message = `Process not found: ${id}`;
41
- return {
42
- content: [{ type: "text", text: message }],
43
- details: {
44
- action,
45
- success: false,
46
- message,
47
- },
48
- };
11
+ name?: string;
49
12
  }
50
13
 
51
14
  export function executeOutput(
52
15
  params: OutputParams,
53
16
  manager: ProcessManager,
54
17
  ): ExecuteResult {
55
- if (!params.id) {
18
+ const error = resolveSelector(params, manager);
19
+ if (error) {
20
+ return { ...error, details: { ...error.details, action: "output" } };
21
+ }
22
+
23
+ const query = params.id || params.name || "";
24
+ const resolved = manager.resolve(query);
25
+ if (!resolved.ok) {
26
+ // Should not reach here, but guard anyway
56
27
  return {
57
- content: [{ type: "text", text: "Missing required parameter: id" }],
28
+ content: [{ type: "text", text: `Process not found: "${query}"` }],
58
29
  details: {
59
30
  action: "output",
60
31
  success: false,
61
- message: "Missing required parameter: id",
32
+ message: `Process not found: "${query}"`,
62
33
  },
63
34
  };
64
35
  }
65
36
 
66
- const resolved = manager.resolve(params.id);
67
- if (!resolved.ok) {
68
- return resolveProcessResult(resolved, "output", params.id) as ExecuteResult;
69
- }
70
-
71
37
  const proc = resolved.info;
72
38
  const { defaultTailLines } = configLoader.getConfig().output;
73
39
  const output = manager.getOutput(proc.id, defaultTailLines);
@@ -3,6 +3,7 @@ import type { ExecuteResult } from "../../constants";
3
3
  import type { ProcessManager } from "../../manager";
4
4
  import { formatTimestamp, sanitizeLine } from "../../utils";
5
5
  import { executeKill } from "./kill";
6
+ import { buildNextCommands } from "./utils";
6
7
 
7
8
  interface RestartParams {
8
9
  name?: string;
@@ -100,7 +101,12 @@ export async function executeRestart(
100
101
  }
101
102
 
102
103
  const startedAt = formatTimestamp(proc.startTime);
103
- const message = `Restarted "${sanitizeLine(proc.name)}" (${proc.id}, PID: ${proc.pid})\nStarted at: ${startedAt}\nLogs: ${proc.stdoutFile}`;
104
+ const message = [
105
+ `Restarted "${sanitizeLine(proc.name)}" (${proc.id}, PID: ${proc.pid})`,
106
+ `Started at: ${startedAt}`,
107
+ `Logs: ${proc.stdoutFile}`,
108
+ buildNextCommands(proc.name),
109
+ ].join("\n");
104
110
  return {
105
111
  content: [{ type: "text", text: message }],
106
112
  details: {
@@ -2,6 +2,7 @@ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
2
  import type { ExecuteResult } from "../../constants";
3
3
  import type { ProcessManager } from "../../manager";
4
4
  import { formatTimestamp, sanitizeLine } from "../../utils";
5
+ import { buildNextCommands } from "./utils";
5
6
 
6
7
  interface StartParams {
7
8
  name?: string;
@@ -59,7 +60,12 @@ export function executeStart(
59
60
  }
60
61
 
61
62
  const startedAt = formatTimestamp(proc.startTime);
62
- const message = `Started "${sanitizeLine(proc.name)}" (${proc.id}, PID: ${proc.pid})\nStarted at: ${startedAt}\nLogs: ${proc.stdoutFile}`;
63
+ const message = [
64
+ `Started "${sanitizeLine(proc.name)}" (${proc.id}, PID: ${proc.pid})`,
65
+ `Started at: ${startedAt}`,
66
+ `Logs: ${proc.stdoutFile}`,
67
+ buildNextCommands(proc.name),
68
+ ].join("\n");
63
69
  return {
64
70
  content: [{ type: "text", text: message }],
65
71
  details: {
@@ -0,0 +1,104 @@
1
+ import type { ExecuteResult } from "../../constants";
2
+ import type { ProcessManager } from "../../manager";
3
+ import { sanitizeLine } from "../../utils";
4
+
5
+ interface SelectorParams {
6
+ id?: string;
7
+ name?: string;
8
+ }
9
+
10
+ export interface ResolvedProcess {
11
+ id: string;
12
+ name: string;
13
+ }
14
+
15
+ export function resolveSelector(
16
+ params: SelectorParams,
17
+ manager: ProcessManager,
18
+ ): ExecuteResult | null {
19
+ // Missing selector
20
+ if (!params.id && !params.name) {
21
+ return {
22
+ content: [
23
+ {
24
+ type: "text",
25
+ text: 'Missing process selector. Use either id="proc_1" or name="backend-dev".',
26
+ },
27
+ ],
28
+ details: {
29
+ action: "output",
30
+ success: false,
31
+ message: "Missing process selector. Use either id or name.",
32
+ },
33
+ };
34
+ }
35
+
36
+ // Both specified
37
+ if (params.id && params.name) {
38
+ return {
39
+ content: [
40
+ {
41
+ type: "text",
42
+ text: "Use either id or name, not both.",
43
+ },
44
+ ],
45
+ details: {
46
+ action: "output",
47
+ success: false,
48
+ message: "Use either id or name, not both.",
49
+ },
50
+ };
51
+ }
52
+
53
+ // Resolve by id or name
54
+ const query = params.id || params.name || "";
55
+ const resolved = manager.resolve(query);
56
+
57
+ if (resolved.ok) {
58
+ return null; // resolved successfully
59
+ }
60
+
61
+ if (resolved.reason === "ambiguous") {
62
+ const choices = (resolved.matches ?? [])
63
+ .map((match) => `${match.id} ("${sanitizeLine(match.name)}")`)
64
+ .join(", ");
65
+ return {
66
+ content: [
67
+ {
68
+ type: "text",
69
+ text: `Process name is ambiguous: "${query}". Use an exact process ID instead. Matches: ${choices}`,
70
+ },
71
+ ],
72
+ details: {
73
+ action: "output",
74
+ success: false,
75
+ message: `Process name is ambiguous: "${query}". Matches: ${choices}`,
76
+ },
77
+ };
78
+ }
79
+
80
+ return {
81
+ content: [
82
+ {
83
+ type: "text",
84
+ text: `Process not found: "${query}"`,
85
+ },
86
+ ],
87
+ details: {
88
+ action: "output",
89
+ success: false,
90
+ message: `Process not found: "${query}"`,
91
+ },
92
+ };
93
+ }
94
+
95
+ export function buildNextCommands(name: string): string {
96
+ return [
97
+ "",
98
+ "Next commands:",
99
+ ` process output name="${name}"`,
100
+ ` process logs name="${name}"`,
101
+ ` process restart "<command>" name="${name}"`,
102
+ ` process kill name="${name}"`,
103
+ ].join("\n");
104
+ }
@@ -26,7 +26,7 @@ const ProcessesParams = Type.Object({
26
26
  name: Type.Optional(
27
27
  Type.String({
28
28
  description:
29
- "Friendly name for the process (required for start/restart, e.g. 'backend-dev', 'test-runner')",
29
+ "Friendly name for the process (required for start/restart; accepted for output/logs/kill as alternative to id)",
30
30
  }),
31
31
  ),
32
32
  cwd: Type.Optional(
@@ -38,7 +38,7 @@ const ProcessesParams = Type.Object({
38
38
  id: Type.Optional(
39
39
  Type.String({
40
40
  description:
41
- "Exact process ID or exact friendly name to match (required for output/kill/logs).",
41
+ "Exact process ID (e.g. 'proc_1') or process name (for output/logs/kill as alternative to name)",
42
42
  }),
43
43
  ),
44
44
  force: Type.Optional(
@@ -57,10 +57,13 @@ export function setupProcessesTools(pi: ExtensionAPI, manager: ProcessManager) {
57
57
 
58
58
  Actions: start, list, output, logs, kill, clear, restart.
59
59
  - start/restart require 'name' and 'command' — restart kills existing process first
60
- - output/logs/kill require 'id' (exact process ID or exact friendly name)
60
+ - output/logs/kill accept either 'id' (exact process ID) or 'name' (process name)
61
61
  - kill supports optional 'force=true' for SIGKILL
62
62
  - restart is preferred over start+kill: it safely awaits kill before starting new process
63
63
 
64
+ Processes have both an exact process id (e.g. proc_1) and a stable name (e.g. astro-dev).
65
+ For output/logs/kill, prefer name="..." when referring to a named dev server.
66
+
64
67
  Processes continue in the background. Use process output or process logs for a one-off snapshot when you need to inspect status. Do not poll repeatedly just to wait.
65
68
  Tool-triggered kills never notify.`,
66
69
  promptSnippet:
@@ -71,6 +74,7 @@ Tool-triggered kills never notify.`,
71
74
  "Use process output or process logs only for a one-off inspection, explicit user request, or debugging.",
72
75
  "Use process restart to replace an existing process — it safely awaits kill before starting the new one.",
73
76
  "Do not poll process output/list repeatedly just to wait for a process to finish.",
77
+ 'For output/logs/kill, prefer name="..." when referring to a named dev server.',
74
78
  ],
75
79
 
76
80
  parameters: ProcessesParams,
package/src/utils/ansi.ts CHANGED
@@ -41,7 +41,7 @@ export function stripAnsi(str: string): string {
41
41
  }
42
42
 
43
43
  // Strip terminal control chars like carriage return/backspace that can
44
- // corrupt TUI layout when rendered back into pi.
44
+ // corrupt output formatting.
45
45
  return Array.from(clean)
46
46
  .filter((char) => {
47
47
  const code = char.codePointAt(0) ?? 0;
@@ -55,12 +55,11 @@ export function stripAnsi(str: string): string {
55
55
  }
56
56
 
57
57
  /**
58
- * Sanitize process output for single-line TUI rendering.
58
+ * Sanitize process output for single-line display.
59
59
  *
60
- * This is stricter than stripAnsi(): after terminal escapes are removed, any
61
- * remaining control bytes (carriage returns, backspaces, BEL, embedded
62
- * newlines, etc.) are dropped so process output cannot move the cursor or
63
- * alter Pi's surrounding TUI.
60
+ * After terminal escapes are removed, any remaining control bytes
61
+ * (carriage returns, backspaces, BEL, embedded newlines, etc.) are dropped
62
+ * so process output is safe for agent display.
64
63
  */
65
64
  export function sanitizeLine(str: string): string {
66
65
  return stripAnsi(str).replace(/\t/g, " ");