@mjakl/pi-processes 0.8.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 +137 -0
- package/package.json +68 -0
- package/src/commands/index.ts +10 -0
- package/src/commands/processes/command.ts +31 -0
- package/src/commands/processes/index.ts +1 -0
- package/src/components/log-file-viewer.ts +317 -0
- package/src/components/processes-component.ts +458 -0
- package/src/config.ts +125 -0
- package/src/constants/index.ts +11 -0
- package/src/constants/types.ts +65 -0
- package/src/hooks/background-blocker.ts +295 -0
- package/src/hooks/cleanup.ts +8 -0
- package/src/hooks/index.ts +24 -0
- package/src/hooks/message-renderer.ts +83 -0
- package/src/hooks/process-end.ts +86 -0
- package/src/hooks/status-widget.ts +65 -0
- package/src/index.ts +25 -0
- package/src/manager.ts +513 -0
- package/src/tools/actions/clear.ts +20 -0
- package/src/tools/actions/index.ts +48 -0
- package/src/tools/actions/kill.ts +107 -0
- package/src/tools/actions/list.ts +43 -0
- package/src/tools/actions/logs.ts +85 -0
- package/src/tools/actions/output.ts +186 -0
- package/src/tools/actions/start.ts +70 -0
- package/src/tools/index.ts +347 -0
- package/src/tools/tool-rendering.ts +164 -0
- package/src/utils/ansi.ts +36 -0
- package/src/utils/command-executor.ts +56 -0
- package/src/utils/format.ts +42 -0
- package/src/utils/index.ts +3 -0
- package/src/utils/process-group.ts +22 -0
- package/src/utils/shell-utils.ts +133 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { type ExecuteResult, LIVE_STATUSES } 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 hasLiveProcess = processes.some((process) =>
|
|
28
|
+
LIVE_STATUSES.has(process.status),
|
|
29
|
+
);
|
|
30
|
+
const waitNotice = hasLiveProcess
|
|
31
|
+
? "\n\nActive processes notify automatically on exit. Do not call process list/output/logs repeatedly just to wait."
|
|
32
|
+
: "";
|
|
33
|
+
const message = `${processes.length} process(es):\n${summary}${waitNotice}`;
|
|
34
|
+
return {
|
|
35
|
+
content: [{ type: "text", text: message }],
|
|
36
|
+
details: {
|
|
37
|
+
action: "list",
|
|
38
|
+
success: true,
|
|
39
|
+
message,
|
|
40
|
+
processes,
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
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 resolved = manager.resolve(params.id);
|
|
24
|
+
if (!resolved.ok) {
|
|
25
|
+
if (resolved.reason === "ambiguous") {
|
|
26
|
+
const choices = (resolved.matches ?? [])
|
|
27
|
+
.map((match) => `${match.id} ("${match.name}")`)
|
|
28
|
+
.join(", ");
|
|
29
|
+
const message =
|
|
30
|
+
`Process name is ambiguous: ${params.id}. ` +
|
|
31
|
+
`Use an exact process ID instead. Matches: ${choices}`;
|
|
32
|
+
return {
|
|
33
|
+
content: [{ type: "text", text: message }],
|
|
34
|
+
details: {
|
|
35
|
+
action: "logs",
|
|
36
|
+
success: false,
|
|
37
|
+
message,
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const message = `Process not found: ${params.id}`;
|
|
43
|
+
return {
|
|
44
|
+
content: [{ type: "text", text: message }],
|
|
45
|
+
details: {
|
|
46
|
+
action: "logs",
|
|
47
|
+
success: false,
|
|
48
|
+
message,
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const proc = resolved.info;
|
|
54
|
+
const logFiles = manager.getLogFiles(proc.id);
|
|
55
|
+
if (!logFiles) {
|
|
56
|
+
const message = `Could not get log files for: ${proc.id}`;
|
|
57
|
+
return {
|
|
58
|
+
content: [{ type: "text", text: message }],
|
|
59
|
+
details: {
|
|
60
|
+
action: "logs",
|
|
61
|
+
success: false,
|
|
62
|
+
message,
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const message = [
|
|
68
|
+
`Log files for "${proc.name}" (${proc.id}):`,
|
|
69
|
+
` stdout: ${logFiles.stdoutFile}`,
|
|
70
|
+
` stderr: ${logFiles.stderrFile}`,
|
|
71
|
+
` combined: ${logFiles.combinedFile}`,
|
|
72
|
+
"",
|
|
73
|
+
"Use the read tool to inspect these files.",
|
|
74
|
+
].join("\n");
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
content: [{ type: "text", text: message }],
|
|
78
|
+
details: {
|
|
79
|
+
action: "logs",
|
|
80
|
+
success: true,
|
|
81
|
+
message,
|
|
82
|
+
logFiles,
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { configLoader } from "../../config";
|
|
2
|
+
import {
|
|
3
|
+
type ExecuteResult,
|
|
4
|
+
LIVE_STATUSES,
|
|
5
|
+
type ResolveProcessResult,
|
|
6
|
+
} from "../../constants";
|
|
7
|
+
import type { ProcessManager } from "../../manager";
|
|
8
|
+
import { formatStatus, stripAnsi } from "../../utils";
|
|
9
|
+
|
|
10
|
+
const MAX_BYTES = 50 * 1024; // 50KB
|
|
11
|
+
|
|
12
|
+
interface OutputParams {
|
|
13
|
+
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} ("${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
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function executeOutput(
|
|
52
|
+
params: OutputParams,
|
|
53
|
+
manager: ProcessManager,
|
|
54
|
+
): ExecuteResult {
|
|
55
|
+
if (!params.id) {
|
|
56
|
+
return {
|
|
57
|
+
content: [{ type: "text", text: "Missing required parameter: id" }],
|
|
58
|
+
details: {
|
|
59
|
+
action: "output",
|
|
60
|
+
success: false,
|
|
61
|
+
message: "Missing required parameter: id",
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const resolved = manager.resolve(params.id);
|
|
67
|
+
if (!resolved.ok) {
|
|
68
|
+
return resolveProcessResult(resolved, "output", params.id) as ExecuteResult;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const proc = resolved.info;
|
|
72
|
+
const { defaultTailLines } = configLoader.getConfig().output;
|
|
73
|
+
const output = manager.getOutput(proc.id, defaultTailLines);
|
|
74
|
+
if (!output) {
|
|
75
|
+
const message = `Could not read output for: ${proc.id}`;
|
|
76
|
+
return {
|
|
77
|
+
content: [{ type: "text", text: message }],
|
|
78
|
+
details: {
|
|
79
|
+
action: "output",
|
|
80
|
+
success: false,
|
|
81
|
+
message,
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const logFiles = manager.getLogFiles(proc.id);
|
|
87
|
+
const stdoutLines = output.stdout.length;
|
|
88
|
+
const stderrLines = output.stderr.length;
|
|
89
|
+
const message = `"${proc.name}" (${proc.id}) [${formatStatus(proc)}]: ${stdoutLines} stdout lines, ${stderrLines} stderr lines`;
|
|
90
|
+
|
|
91
|
+
// Build the full text content (ANSI-stripped), then truncate from the tail
|
|
92
|
+
// like bash does, so the agent sees the most recent output.
|
|
93
|
+
const outputParts: string[] = [message];
|
|
94
|
+
if (output.stdout.length > 0) {
|
|
95
|
+
outputParts.push("\nstdout:");
|
|
96
|
+
outputParts.push(...output.stdout.map(stripAnsi));
|
|
97
|
+
}
|
|
98
|
+
if (output.stderr.length > 0) {
|
|
99
|
+
outputParts.push("\nstderr:");
|
|
100
|
+
outputParts.push(...output.stderr.map(stripAnsi));
|
|
101
|
+
}
|
|
102
|
+
if (LIVE_STATUSES.has(proc.status)) {
|
|
103
|
+
outputParts.push(
|
|
104
|
+
"",
|
|
105
|
+
"[Process is still active. This was a one-off snapshot; wait for the automatic process-end notification instead of calling process output/list/logs repeatedly.]",
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const fullText = outputParts.join("\n");
|
|
110
|
+
const { maxOutputLines } = configLoader.getConfig().output;
|
|
111
|
+
const contentText = truncateTail(fullText, logFiles, maxOutputLines);
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
content: [{ type: "text", text: contentText }],
|
|
115
|
+
details: {
|
|
116
|
+
action: "output",
|
|
117
|
+
success: true,
|
|
118
|
+
message,
|
|
119
|
+
output,
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Truncate text from the tail (keep last N lines / MAX_BYTES), matching
|
|
126
|
+
* the behaviour of pi's built-in bash tool. When truncated, appends a
|
|
127
|
+
* notice pointing the agent to the full log files.
|
|
128
|
+
*/
|
|
129
|
+
function truncateTail(
|
|
130
|
+
text: string,
|
|
131
|
+
logFiles: {
|
|
132
|
+
stdoutFile: string;
|
|
133
|
+
stderrFile: string;
|
|
134
|
+
combinedFile: string;
|
|
135
|
+
} | null,
|
|
136
|
+
maxLines: number,
|
|
137
|
+
): string {
|
|
138
|
+
const totalBytes = Buffer.byteLength(text, "utf-8");
|
|
139
|
+
const lines = text.split("\n");
|
|
140
|
+
const totalLines = lines.length;
|
|
141
|
+
|
|
142
|
+
if (totalLines <= maxLines && totalBytes <= MAX_BYTES) {
|
|
143
|
+
return text;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Work backwards, collecting lines that fit
|
|
147
|
+
const kept: string[] = [];
|
|
148
|
+
let keptBytes = 0;
|
|
149
|
+
let hitBytes = false;
|
|
150
|
+
|
|
151
|
+
for (let i = lines.length - 1; i >= 0 && kept.length < maxLines; i--) {
|
|
152
|
+
const line = lines[i] ?? "";
|
|
153
|
+
const lineBytes =
|
|
154
|
+
Buffer.byteLength(line, "utf-8") + (kept.length > 0 ? 1 : 0);
|
|
155
|
+
|
|
156
|
+
if (keptBytes + lineBytes > MAX_BYTES) {
|
|
157
|
+
hitBytes = true;
|
|
158
|
+
break;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
kept.unshift(line);
|
|
162
|
+
keptBytes += lineBytes;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
let result = kept.join("\n");
|
|
166
|
+
|
|
167
|
+
// Append a notice so the agent knows output was truncated
|
|
168
|
+
const shownLines = kept.length;
|
|
169
|
+
const startLine = totalLines - shownLines + 1;
|
|
170
|
+
const sizeNote = hitBytes ? ` (${formatSize(MAX_BYTES)} limit)` : "";
|
|
171
|
+
result += `\n\n[Showing lines ${startLine}-${totalLines} of ${totalLines}${sizeNote}.`;
|
|
172
|
+
|
|
173
|
+
if (logFiles) {
|
|
174
|
+
result += ` Full logs: ${logFiles.stdoutFile} , ${logFiles.stderrFile}`;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
result += "]";
|
|
178
|
+
|
|
179
|
+
return result;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function formatSize(bytes: number): string {
|
|
183
|
+
if (bytes < 1024) return `${bytes}B`;
|
|
184
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
|
|
185
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
|
|
186
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { ExtensionContext } from "@earendil-works/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
|
+
continueAfterStart?: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function executeStart(
|
|
12
|
+
params: StartParams,
|
|
13
|
+
manager: ProcessManager,
|
|
14
|
+
ctx: ExtensionContext,
|
|
15
|
+
): ExecuteResult {
|
|
16
|
+
if (!params.name) {
|
|
17
|
+
return {
|
|
18
|
+
content: [{ type: "text", text: "Missing required parameter: name" }],
|
|
19
|
+
details: {
|
|
20
|
+
action: "start",
|
|
21
|
+
success: false,
|
|
22
|
+
message: "Missing required parameter: name",
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
if (!params.command) {
|
|
27
|
+
return {
|
|
28
|
+
content: [{ type: "text", text: "Missing required parameter: command" }],
|
|
29
|
+
details: {
|
|
30
|
+
action: "start",
|
|
31
|
+
success: false,
|
|
32
|
+
message: "Missing required parameter: command",
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
const proc = manager.start(params.name, params.command, ctx.cwd);
|
|
39
|
+
const shouldContinue = params.continueAfterStart === true;
|
|
40
|
+
const nextStep = shouldContinue
|
|
41
|
+
? "Continue with specific non-polling work now. Do not call process list/output/logs just to wait; the extension will notify you when this process ends."
|
|
42
|
+
: "This turn will stop after start so you can wait for the automatic process-end notification. Do not call process list/output/logs just to check whether it is still running.";
|
|
43
|
+
|
|
44
|
+
const message = `Started "${proc.name}" (${proc.id}, PID: ${proc.pid})\nLogs: ${proc.stdoutFile}\n${nextStep}`;
|
|
45
|
+
return {
|
|
46
|
+
content: [{ type: "text", text: message }],
|
|
47
|
+
details: {
|
|
48
|
+
action: "start",
|
|
49
|
+
success: true,
|
|
50
|
+
message,
|
|
51
|
+
process: proc,
|
|
52
|
+
},
|
|
53
|
+
terminate: !shouldContinue,
|
|
54
|
+
};
|
|
55
|
+
} catch (error) {
|
|
56
|
+
const message =
|
|
57
|
+
error instanceof Error
|
|
58
|
+
? `Failed to start process: ${error.message}`
|
|
59
|
+
: "Failed to start process";
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
content: [{ type: "text", text: message }],
|
|
63
|
+
details: {
|
|
64
|
+
action: "start",
|
|
65
|
+
success: false,
|
|
66
|
+
message,
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
}
|