@aliou/pi-processes 0.9.4 → 0.10.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/CONTRIBUTING.md +40 -42
- package/README.md +54 -84
- package/extensions/process-tabs.ts +26 -0
- package/extensions/processes/client.ts +194 -0
- package/extensions/processes/commands/clear.ts +29 -0
- package/extensions/processes/commands/kill.ts +152 -0
- package/extensions/processes/commands/overview.ts +102 -0
- package/extensions/processes/components/overview-component.ts +729 -0
- package/extensions/processes/components/overview-panel.ts +177 -0
- package/extensions/processes/config/defaults.ts +28 -0
- package/extensions/processes/config/index.ts +13 -0
- package/extensions/processes/config/loader.ts +62 -0
- package/extensions/processes/config/migrations/000-import-legacy-process-config.ts +113 -0
- package/extensions/processes/config/migrations/001-v0-9-4-to-v0-10-0-config.ts +184 -0
- package/extensions/processes/config/migrations/002-stamp-v0-10-0-config-version.ts +21 -0
- package/extensions/processes/config/migrations/index.ts +25 -0
- package/extensions/processes/config/schema.ts +9 -0
- package/extensions/processes/config/types.ts +72 -0
- package/extensions/processes/constants.ts +1 -0
- package/extensions/processes/handlers/commands.ts +113 -0
- package/extensions/processes/handlers/kill-process.ts +31 -0
- package/extensions/processes/handlers/notifications.ts +147 -0
- package/extensions/processes/handlers/requests.ts +114 -0
- package/extensions/processes/handlers/subscriptions.ts +144 -0
- package/extensions/processes/hooks/background-blocker.ts +201 -0
- package/extensions/processes/hooks/cleanup.ts +43 -0
- package/extensions/processes/hooks/event-bridge.ts +32 -0
- package/extensions/processes/i18n/index.ts +3 -0
- package/extensions/processes/i18n/messages.ts +27 -0
- package/extensions/processes/i18n/translator.ts +49 -0
- package/extensions/processes/index.ts +109 -0
- package/extensions/processes/message-renderer.ts +117 -0
- package/extensions/processes/notification-sender.ts +43 -0
- package/extensions/processes/notifications/classify.ts +27 -0
- package/extensions/processes/notifications/log-matchers.ts +146 -0
- package/extensions/processes/notifications/registry.ts +225 -0
- package/extensions/processes/notifications/render-content.ts +122 -0
- package/extensions/processes/notifications/service.ts +336 -0
- package/extensions/processes/notifications/types.ts +11 -0
- package/extensions/processes/settings/apply-setting-change.ts +30 -0
- package/extensions/processes/settings/build-sections.ts +420 -0
- package/extensions/processes/settings/index.ts +42 -0
- package/extensions/processes/tools/clear/index.ts +21 -0
- package/extensions/processes/tools/clear/render.ts +40 -0
- package/extensions/processes/tools/components/index.ts +4 -0
- package/extensions/processes/tools/components/process-action-header.ts +31 -0
- package/extensions/processes/tools/components/process-action-title.ts +17 -0
- package/extensions/processes/tools/components/tool-layout.ts +70 -0
- package/extensions/processes/tools/components/watch.ts +53 -0
- package/extensions/processes/tools/index.ts +265 -0
- package/extensions/processes/tools/list/index.ts +157 -0
- package/extensions/processes/tools/list/render.ts +211 -0
- package/extensions/processes/tools/notify.ts +191 -0
- package/extensions/processes/tools/output/index.ts +307 -0
- package/extensions/processes/tools/output/render.ts +229 -0
- package/extensions/processes/tools/schema.ts +263 -0
- package/extensions/processes/tools/start/index.ts +64 -0
- package/extensions/processes/tools/start/render.ts +67 -0
- package/extensions/processes/tools/stop/index.ts +35 -0
- package/extensions/processes/tools/stop/render.ts +43 -0
- package/extensions/processes/tools/update/index.ts +320 -0
- package/extensions/processes/tools/update/render.ts +251 -0
- package/extensions/processes/tools/utils.ts +112 -0
- package/extensions/processes/tools/watch-format.ts +14 -0
- package/extensions/processes/tools/write/index.ts +75 -0
- package/extensions/processes/tools/write/render.ts +68 -0
- package/extensions/processes/utils/truncate.ts +285 -0
- package/extensions/processes-dock/client.ts +60 -0
- package/extensions/processes-dock/commands/dock.ts +70 -0
- package/extensions/processes-dock/commands/pin.ts +166 -0
- package/extensions/processes-dock/components/log-dock-component.ts +375 -0
- package/extensions/processes-dock/dock-state.ts +33 -0
- package/extensions/processes-dock/index.ts +28 -0
- package/extensions/processes-dock/logs-client.ts +5 -0
- package/extensions/processes-dock/widget/setup.ts +520 -0
- package/extensions/processes-dock/widget/status.ts +149 -0
- package/extensions/processes-dock/widget/types.ts +16 -0
- package/extensions/processes-logs/client.ts +51 -0
- package/extensions/processes-logs/commands/logs.ts +108 -0
- package/extensions/processes-logs/completions.ts +31 -0
- package/extensions/processes-logs/components/log-file-viewer.ts +274 -0
- package/extensions/processes-logs/components/log-overlay-component.ts +685 -0
- package/extensions/processes-logs/index.ts +27 -0
- package/extensions/processes-logs/logs-client.ts +101 -0
- package/extensions/shared/line-buffer.ts +35 -0
- package/extensions/shared/output-payload.ts +28 -0
- package/extensions/shared/ui.ts +146 -0
- package/package.json +47 -20
- package/schema.json +134 -0
- package/skills/pi-processes/SKILL.md +496 -25
- package/src/get-manager.ts +15 -0
- package/src/manager/index.ts +182 -0
- package/src/manager/internal-types.ts +130 -0
- package/src/manager/limits.ts +15 -0
- package/src/manager/process-log-store.ts +422 -0
- package/src/manager/process-output.ts +345 -0
- package/src/manager/process-registry.ts +78 -0
- package/src/manager/process-runtime-controller.ts +499 -0
- package/src/protocol/broadcasts.ts +14 -0
- package/src/protocol/channels.ts +30 -0
- package/src/protocol/commands.ts +24 -0
- package/src/protocol/index.ts +34 -0
- package/src/protocol/logs.ts +23 -0
- package/src/protocol/notifications.ts +48 -0
- package/src/protocol/requests.ts +76 -0
- package/src/types.ts +66 -0
- package/src/utils/ansi.ts +2 -4
- package/src/utils/format.ts +24 -32
- package/src/utils/index.ts +5 -1
- package/src/utils/is-record.ts +3 -0
- package/src/utils/match-line.ts +32 -0
- package/src/utils/platform.ts +13 -0
- package/src/utils/shorten-path.ts +11 -0
- package/src/utils/signals.ts +22 -0
- package/src/commands/clear/command.ts +0 -14
- package/src/commands/clear/index.ts +0 -1
- package/src/commands/completions.ts +0 -38
- package/src/commands/dock/command.ts +0 -29
- package/src/commands/dock/index.ts +0 -1
- package/src/commands/index.ts +0 -33
- package/src/commands/kill/command.ts +0 -69
- package/src/commands/kill/index.ts +0 -1
- package/src/commands/logs/command.ts +0 -47
- package/src/commands/logs/index.ts +0 -1
- package/src/commands/pick-process.ts +0 -34
- package/src/commands/pin/command.ts +0 -34
- package/src/commands/pin/index.ts +0 -1
- package/src/commands/processes/command.ts +0 -39
- package/src/commands/processes/index.ts +0 -1
- package/src/commands/settings/apply-setting-change.ts +0 -72
- package/src/commands/settings/build-sections.ts +0 -160
- package/src/commands/settings/command.ts +0 -20
- package/src/commands/settings/index.ts +0 -1
- package/src/components/log-dock-component.test.ts +0 -29
- package/src/components/log-dock-component.ts +0 -236
- package/src/components/log-file-viewer.ts +0 -317
- package/src/components/log-overlay-component.ts +0 -546
- package/src/components/panel-helpers.ts +0 -37
- package/src/components/process-picker-component.ts +0 -166
- package/src/components/processes-component.ts +0 -485
- package/src/components/status-format.ts +0 -38
- package/src/config.ts +0 -107
- package/src/constants/index.ts +0 -16
- package/src/constants/types.ts +0 -106
- package/src/hooks/background-blocker.ts +0 -66
- package/src/hooks/cleanup.ts +0 -10
- package/src/hooks/index.ts +0 -32
- package/src/hooks/message-renderer.ts +0 -115
- package/src/hooks/process-end.ts +0 -69
- package/src/hooks/process-watch.ts +0 -85
- package/src/hooks/utils.ts +0 -23
- package/src/hooks/widget/index.ts +0 -2
- package/src/hooks/widget/setup.ts +0 -167
- package/src/hooks/widget/status-widget.ts +0 -105
- package/src/hooks/widget/types.ts +0 -20
- package/src/index.ts +0 -35
- package/src/manager.test.ts +0 -331
- package/src/manager.ts +0 -758
- package/src/tools/actions/clear.ts +0 -20
- package/src/tools/actions/debug.ts +0 -148
- package/src/tools/actions/index.ts +0 -171
- package/src/tools/actions/kill.ts +0 -89
- package/src/tools/actions/list.ts +0 -188
- package/src/tools/actions/logs.ts +0 -117
- package/src/tools/actions/output.ts +0 -269
- package/src/tools/actions/start.ts +0 -245
- package/src/tools/actions/write.ts +0 -113
- package/src/tools/index.ts +0 -207
- package/src/utils/ansi.test.ts +0 -22
- package/src/utils/command-executor.test.ts +0 -48
- package/src/utils/keybindings.ts +0 -71
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { isRecord } from "../../../src/utils/is-record";
|
|
2
|
+
import {
|
|
3
|
+
MAX_LOG_MATCH_PATTERN_LENGTH,
|
|
4
|
+
MAX_LOG_MATCHERS_PER_PROCESS,
|
|
5
|
+
} from "../notifications/log-matchers";
|
|
6
|
+
import type { LogMatcherConfig, NotifyConfig } from "../notifications/registry";
|
|
7
|
+
import type { Attention } from "../notifications/types";
|
|
8
|
+
|
|
9
|
+
const ATTENTIONS = ["turn", "context", "ignore"] as const;
|
|
10
|
+
const LOG_MATCH_MODES = ["literal", "regex"] as const;
|
|
11
|
+
const LOG_MATCH_STREAMS = ["stdout", "stderr", "both"] as const;
|
|
12
|
+
|
|
13
|
+
export const MAX_NOTIFY_LOG_MATCHERS = MAX_LOG_MATCHERS_PER_PROCESS;
|
|
14
|
+
export const MAX_NOTIFY_PATTERN_LENGTH = MAX_LOG_MATCH_PATTERN_LENGTH;
|
|
15
|
+
|
|
16
|
+
const DEFAULT_NOTIFY_CONFIG = {
|
|
17
|
+
onSuccess: "context",
|
|
18
|
+
onFailure: "turn",
|
|
19
|
+
// External kills (outside this manager) surface as context by default so
|
|
20
|
+
// the agent and user learn that a managed process disappeared. Intentional
|
|
21
|
+
// stops via the tool/command path are classified separately and never reach
|
|
22
|
+
// this branch.
|
|
23
|
+
onKilled: "context",
|
|
24
|
+
} as const satisfies Pick<NotifyConfig, "onSuccess" | "onFailure" | "onKilled">;
|
|
25
|
+
|
|
26
|
+
export function normalizeNotifyConfig(input: unknown): NotifyConfig {
|
|
27
|
+
if (input === undefined || input === null) {
|
|
28
|
+
return { ...DEFAULT_NOTIFY_CONFIG, logMatches: [] };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (!isRecord(input)) {
|
|
32
|
+
throw new Error("process start notify must be an object");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const logMatches = input.logMatches;
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
onSuccess:
|
|
39
|
+
normalizeAttention(input.onSuccess, "notify.onSuccess") ?? "context",
|
|
40
|
+
onFailure:
|
|
41
|
+
normalizeAttention(input.onFailure, "notify.onFailure") ?? "turn",
|
|
42
|
+
onKilled:
|
|
43
|
+
normalizeAttention(input.onKilled, "notify.onKilled") ?? "context",
|
|
44
|
+
logMatches: normalizeLogMatches(logMatches),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function normalizeLogMatches(input: unknown): LogMatcherConfig[] {
|
|
49
|
+
if (input === undefined || input === null) return [];
|
|
50
|
+
|
|
51
|
+
if (!Array.isArray(input)) {
|
|
52
|
+
throw new Error("process start notify.logMatches must be an array");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (input.length > MAX_NOTIFY_LOG_MATCHERS) {
|
|
56
|
+
throw new Error(
|
|
57
|
+
`process start notify.logMatches supports at most ${MAX_NOTIFY_LOG_MATCHERS} matchers`,
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return input.map((entry, index) => normalizeLogMatch(entry, index));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function normalizeLogMatchItems(
|
|
65
|
+
input: unknown,
|
|
66
|
+
options: {
|
|
67
|
+
actionLabel: string;
|
|
68
|
+
pathPrefix: string;
|
|
69
|
+
maxItems?: number;
|
|
70
|
+
},
|
|
71
|
+
): LogMatcherConfig[] {
|
|
72
|
+
const { actionLabel, pathPrefix } = options;
|
|
73
|
+
const maxItems = options.maxItems ?? MAX_NOTIFY_LOG_MATCHERS;
|
|
74
|
+
|
|
75
|
+
if (input === undefined || input === null) {
|
|
76
|
+
throw new Error(`${actionLabel} ${pathPrefix} is required`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (!Array.isArray(input)) {
|
|
80
|
+
throw new Error(`${actionLabel} ${pathPrefix} must be an array`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (input.length === 0) {
|
|
84
|
+
throw new Error(`${actionLabel} ${pathPrefix} must not be empty`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (input.length > maxItems) {
|
|
88
|
+
throw new Error(
|
|
89
|
+
`${actionLabel} ${pathPrefix} supports at most ${maxItems} items`,
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return input.map((entry, index) =>
|
|
94
|
+
normalizeLogMatch(entry, index, { actionLabel, pathPrefix }),
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function normalizeLogMatch(
|
|
99
|
+
input: unknown,
|
|
100
|
+
index: number,
|
|
101
|
+
options?: { actionLabel?: string; pathPrefix?: string },
|
|
102
|
+
): LogMatcherConfig {
|
|
103
|
+
const actionLabel = options?.actionLabel ?? "process start";
|
|
104
|
+
const path = `${options?.pathPrefix ?? "notify.logMatches"}[${index}]`;
|
|
105
|
+
|
|
106
|
+
if (!isRecord(input)) {
|
|
107
|
+
throw new Error(`${actionLabel} ${path} must be an object`);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (typeof input.pattern !== "string") {
|
|
111
|
+
throw new Error(`${actionLabel} ${path}.pattern must be a string`);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// An empty or whitespace-only literal pattern matches every line
|
|
115
|
+
// (String#includes("")), and an empty regex matches every line too. Reject
|
|
116
|
+
// early so a stray "" from the model does not fire a notification per line.
|
|
117
|
+
if (input.pattern.trim().length === 0) {
|
|
118
|
+
throw new Error(
|
|
119
|
+
`${actionLabel} ${path}.pattern must not be empty or whitespace-only`,
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (input.pattern.length > MAX_NOTIFY_PATTERN_LENGTH) {
|
|
124
|
+
throw new Error(
|
|
125
|
+
`${actionLabel} ${path}.pattern must be at most ${MAX_NOTIFY_PATTERN_LENGTH} characters`,
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const mode =
|
|
130
|
+
normalizeStringEnum(input.mode, LOG_MATCH_MODES, `${path}.mode`) ??
|
|
131
|
+
"literal";
|
|
132
|
+
const stream =
|
|
133
|
+
normalizeStringEnum(input.stream, LOG_MATCH_STREAMS, `${path}.stream`) ??
|
|
134
|
+
"both";
|
|
135
|
+
const repeat = normalizeBoolean(input.repeat, `${path}.repeat`) ?? false;
|
|
136
|
+
const on = normalizeAttention(input.on, `${path}.on`) ?? "turn";
|
|
137
|
+
|
|
138
|
+
if (mode === "regex") {
|
|
139
|
+
validateRegex(input.pattern, path);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
pattern: input.pattern,
|
|
144
|
+
mode,
|
|
145
|
+
stream,
|
|
146
|
+
repeat,
|
|
147
|
+
on,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function normalizeAttention(
|
|
152
|
+
input: unknown,
|
|
153
|
+
path: string,
|
|
154
|
+
): Attention | undefined {
|
|
155
|
+
return normalizeStringEnum(input, ATTENTIONS, path);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function normalizeStringEnum<const T extends readonly string[]>(
|
|
159
|
+
input: unknown,
|
|
160
|
+
allowed: T,
|
|
161
|
+
path: string,
|
|
162
|
+
): T[number] | undefined {
|
|
163
|
+
if (input === undefined || input === null) return undefined;
|
|
164
|
+
|
|
165
|
+
if (typeof input !== "string" || !allowed.includes(input)) {
|
|
166
|
+
throw new Error(`${path} must be one of: ${allowed.join(", ")}`);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return input;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function normalizeBoolean(input: unknown, path: string): boolean | undefined {
|
|
173
|
+
if (input === undefined || input === null) return undefined;
|
|
174
|
+
|
|
175
|
+
if (typeof input !== "boolean") {
|
|
176
|
+
throw new Error(`${path} must be a boolean`);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return input;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function validateRegex(pattern: string, path: string): void {
|
|
183
|
+
try {
|
|
184
|
+
new RegExp(pattern);
|
|
185
|
+
} catch (error) {
|
|
186
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
187
|
+
throw new Error(
|
|
188
|
+
`${path}.pattern is not a valid regular expression: ${message}`,
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
import {
|
|
2
|
+
formatSize,
|
|
3
|
+
type TruncationResult,
|
|
4
|
+
truncateTail,
|
|
5
|
+
} from "@earendil-works/pi-coding-agent";
|
|
6
|
+
|
|
7
|
+
import type { ProcessManager } from "../../../../src/manager";
|
|
8
|
+
import { stripAnsi } from "../../../../src/utils";
|
|
9
|
+
import type { LineMatchMode } from "../../../../src/utils/match-line";
|
|
10
|
+
import { compileLineMatcher } from "../../../../src/utils/match-line";
|
|
11
|
+
import type { ProcessesParamsType } from "../schema";
|
|
12
|
+
import {
|
|
13
|
+
DEFAULT_OUTPUT_TAIL_LINES,
|
|
14
|
+
MAX_OUTPUT_BYTES,
|
|
15
|
+
MAX_OUTPUT_SCAN_LINES,
|
|
16
|
+
MAX_OUTPUT_TAIL_LINES,
|
|
17
|
+
type ProcessOutputMatchMode,
|
|
18
|
+
type ProcessOutputStream,
|
|
19
|
+
} from "../schema";
|
|
20
|
+
|
|
21
|
+
const MAX_OUTPUT_CONTENT_JSON_BYTES = 96 * 1024;
|
|
22
|
+
const MAX_OUTPUT_PROCESS_NAME_BYTES = 256;
|
|
23
|
+
|
|
24
|
+
export interface OutputDetails {
|
|
25
|
+
action: "output";
|
|
26
|
+
id: string;
|
|
27
|
+
processName: string;
|
|
28
|
+
processStatus: string;
|
|
29
|
+
stream: ProcessOutputStream;
|
|
30
|
+
tailLines: number;
|
|
31
|
+
pattern: string | null;
|
|
32
|
+
mode: ProcessOutputMatchMode;
|
|
33
|
+
stdoutFile: string;
|
|
34
|
+
stderrFile: string;
|
|
35
|
+
truncation?: OutputTruncationDetails;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export type OutputTruncationDetails = Omit<TruncationResult, "content">;
|
|
39
|
+
|
|
40
|
+
interface OutputSelection {
|
|
41
|
+
stdout: string[];
|
|
42
|
+
stderr: string[];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface OutputExecutionResult {
|
|
46
|
+
content: string;
|
|
47
|
+
details: OutputDetails;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function executeOutput(
|
|
51
|
+
params: ProcessesParamsType,
|
|
52
|
+
manager: ProcessManager,
|
|
53
|
+
): OutputExecutionResult {
|
|
54
|
+
if (!params.id) {
|
|
55
|
+
throw new Error("process output requires id");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const process = manager.get(params.id);
|
|
59
|
+
if (!process) {
|
|
60
|
+
throw new Error(`process not found: ${params.id}`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const stream = params.stream ?? "both";
|
|
64
|
+
const tailLines = clampTailLines(params.tailLines);
|
|
65
|
+
const mode = (params.mode ?? "literal") as LineMatchMode;
|
|
66
|
+
const pattern = params.pattern ?? null;
|
|
67
|
+
|
|
68
|
+
if (pattern && mode === "regex") {
|
|
69
|
+
validateRegex(pattern);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Determine scan window: if filtering by pattern, read a larger bounded
|
|
73
|
+
// window so we can find matches beyond the narrow tail.
|
|
74
|
+
const scanLines = pattern ? MAX_OUTPUT_SCAN_LINES : tailLines;
|
|
75
|
+
|
|
76
|
+
const output = manager.getOutput(process.id, scanLines);
|
|
77
|
+
if (!output) {
|
|
78
|
+
throw new Error(
|
|
79
|
+
`could not read output for "${process.name}" (${process.id})`,
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Apply stream filter
|
|
84
|
+
let stdout: string[] = stream === "stderr" ? [] : output.stdout;
|
|
85
|
+
let stderr: string[] = stream === "stdout" ? [] : output.stderr;
|
|
86
|
+
|
|
87
|
+
// Apply pattern filter (filter first, then tail)
|
|
88
|
+
if (pattern) {
|
|
89
|
+
const lineMatcher = compileLineMatcher(pattern, mode);
|
|
90
|
+
stdout = stdout.filter(lineMatcher);
|
|
91
|
+
stderr = stderr.filter(lineMatcher);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Tail to requested line count
|
|
95
|
+
stdout = stdout.slice(-tailLines);
|
|
96
|
+
stderr = stderr.slice(-tailLines);
|
|
97
|
+
|
|
98
|
+
const selection: OutputSelection = { stdout, stderr };
|
|
99
|
+
const processName = formatProcessName(process.name);
|
|
100
|
+
|
|
101
|
+
const contentParts = formatOutputContent({
|
|
102
|
+
processName,
|
|
103
|
+
id: process.id,
|
|
104
|
+
processStatus: output.status,
|
|
105
|
+
stream,
|
|
106
|
+
tailLines,
|
|
107
|
+
pattern,
|
|
108
|
+
mode,
|
|
109
|
+
...selection,
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
const { content, truncation } = buildBoundedOutput(contentParts, {
|
|
113
|
+
stdoutFile: process.stdoutFile,
|
|
114
|
+
stderrFile: process.stderrFile,
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
const details: OutputDetails = {
|
|
118
|
+
action: "output",
|
|
119
|
+
id: process.id,
|
|
120
|
+
processName,
|
|
121
|
+
processStatus: output.status,
|
|
122
|
+
stream,
|
|
123
|
+
tailLines,
|
|
124
|
+
pattern,
|
|
125
|
+
mode,
|
|
126
|
+
stdoutFile: process.stdoutFile,
|
|
127
|
+
stderrFile: process.stderrFile,
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
if (truncation.truncated) {
|
|
131
|
+
const { content: _content, ...truncationDetails } = truncation;
|
|
132
|
+
details.truncation = truncationDetails;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return { content, details };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
interface OutputContentInput extends OutputSelection {
|
|
139
|
+
processName: string;
|
|
140
|
+
id: string;
|
|
141
|
+
processStatus: string;
|
|
142
|
+
stream: ProcessOutputStream;
|
|
143
|
+
tailLines: number;
|
|
144
|
+
pattern: string | null;
|
|
145
|
+
mode: ProcessOutputMatchMode;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
interface OutputContentParts {
|
|
149
|
+
header: string;
|
|
150
|
+
body: string;
|
|
151
|
+
guidance: string | null;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function formatOutputContent(input: OutputContentInput): OutputContentParts {
|
|
155
|
+
const header: string[] = [];
|
|
156
|
+
header.push(`"${input.processName}" (${input.id}) [${input.processStatus}]`);
|
|
157
|
+
|
|
158
|
+
if (input.pattern) {
|
|
159
|
+
const modeTag = input.mode === "regex" ? " (regex)" : "";
|
|
160
|
+
header.push(`filter: ${input.pattern}${modeTag}`);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const body: string[] = [];
|
|
164
|
+
const { stdout, stderr } = input;
|
|
165
|
+
|
|
166
|
+
if (stdout.length > 0) {
|
|
167
|
+
body.push("stdout:");
|
|
168
|
+
body.push(...stdout.map(stripAnsi));
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (stderr.length > 0) {
|
|
172
|
+
if (body.length > 0) body.push("");
|
|
173
|
+
body.push("stderr:");
|
|
174
|
+
body.push(...stderr.map(stripAnsi));
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (stdout.length === 0 && stderr.length === 0) {
|
|
178
|
+
body.push(input.pattern ? "No matching lines found." : "No output yet.");
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return {
|
|
182
|
+
header: header.join("\n"),
|
|
183
|
+
body: body.join("\n"),
|
|
184
|
+
guidance:
|
|
185
|
+
input.processStatus === "running"
|
|
186
|
+
? "Process is still running. Use watches instead of polling."
|
|
187
|
+
: null,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function buildBoundedOutput(
|
|
192
|
+
parts: OutputContentParts,
|
|
193
|
+
logFiles: { stdoutFile: string; stderrFile: string },
|
|
194
|
+
): { content: string; truncation: TruncationResult } {
|
|
195
|
+
let maxBodyBytes = MAX_OUTPUT_BYTES;
|
|
196
|
+
let maxBodyLines = MAX_OUTPUT_TAIL_LINES;
|
|
197
|
+
let truncation = truncateTail(parts.body, {
|
|
198
|
+
maxBytes: maxBodyBytes,
|
|
199
|
+
maxLines: maxBodyLines,
|
|
200
|
+
});
|
|
201
|
+
let content = composeOutputContent(parts, truncation, logFiles);
|
|
202
|
+
|
|
203
|
+
// The header, guidance, truncation notice, and log paths are part of the
|
|
204
|
+
// persisted content limit. JSON escaping can expand control characters, so
|
|
205
|
+
// keep the serialized content bounded too. Reduce the body budget until the
|
|
206
|
+
// complete result fits while keeping the newest output.
|
|
207
|
+
for (let attempt = 0; attempt < 10; attempt++) {
|
|
208
|
+
const excessBytes = Buffer.byteLength(content, "utf8") - MAX_OUTPUT_BYTES;
|
|
209
|
+
const excessLines = countLines(content) - MAX_OUTPUT_TAIL_LINES;
|
|
210
|
+
const excessJsonBytes =
|
|
211
|
+
Buffer.byteLength(JSON.stringify(content), "utf8") -
|
|
212
|
+
MAX_OUTPUT_CONTENT_JSON_BYTES;
|
|
213
|
+
if (excessBytes <= 0 && excessLines <= 0 && excessJsonBytes <= 0) {
|
|
214
|
+
return { content, truncation };
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
maxBodyBytes = Math.max(0, maxBodyBytes - Math.max(0, excessBytes));
|
|
218
|
+
if (excessJsonBytes > 0) {
|
|
219
|
+
maxBodyBytes = Math.floor(maxBodyBytes / 2);
|
|
220
|
+
}
|
|
221
|
+
maxBodyLines = Math.max(1, maxBodyLines - Math.max(0, excessLines));
|
|
222
|
+
truncation = truncateTail(parts.body, {
|
|
223
|
+
maxBytes: maxBodyBytes,
|
|
224
|
+
maxLines: maxBodyLines,
|
|
225
|
+
});
|
|
226
|
+
content = composeOutputContent(parts, truncation, logFiles);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Pathological metadata can consume the whole budget. Keep the final tail,
|
|
230
|
+
// which contains the truncation notice and complete-log paths, bounded.
|
|
231
|
+
const finalTruncation = truncateTail(content, {
|
|
232
|
+
maxBytes: MAX_OUTPUT_BYTES,
|
|
233
|
+
maxLines: MAX_OUTPUT_TAIL_LINES,
|
|
234
|
+
});
|
|
235
|
+
return { content: finalTruncation.content, truncation };
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function composeOutputContent(
|
|
239
|
+
parts: OutputContentParts,
|
|
240
|
+
truncation: TruncationResult,
|
|
241
|
+
logFiles: { stdoutFile: string; stderrFile: string },
|
|
242
|
+
): string {
|
|
243
|
+
const sections = [parts.header, truncation.content];
|
|
244
|
+
|
|
245
|
+
if (parts.guidance) {
|
|
246
|
+
sections.push(parts.guidance);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (truncation.truncated) {
|
|
250
|
+
sections.push(formatTruncationNotice(truncation));
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
sections.push(
|
|
254
|
+
`[Complete currently-retained logs:\nstdout=${logFiles.stdoutFile}\nstderr=${logFiles.stderrFile}]`,
|
|
255
|
+
);
|
|
256
|
+
return sections.filter((section) => section.length > 0).join("\n\n");
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function formatTruncationNotice(truncation: TruncationResult): string {
|
|
260
|
+
const limit =
|
|
261
|
+
truncation.truncatedBy === "bytes"
|
|
262
|
+
? `${formatSize(truncation.maxBytes)} byte limit`
|
|
263
|
+
: `${truncation.maxLines} line limit`;
|
|
264
|
+
const partialNote = truncation.lastLinePartial
|
|
265
|
+
? " (final line is a partial suffix)"
|
|
266
|
+
: "";
|
|
267
|
+
return `[Preview truncated by ${limit}${partialNote}; showing ${truncation.outputLines} lines / ${formatSize(truncation.outputBytes)} of ${truncation.totalLines} lines / ${formatSize(truncation.totalBytes)}.]`;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function countLines(text: string): number {
|
|
271
|
+
if (text.length === 0) return 0;
|
|
272
|
+
return text.split("\n").length;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function formatProcessName(name: string): string {
|
|
276
|
+
const clean = stripAnsi(name).replace(/\s+/gu, " ").trim() || "(unnamed)";
|
|
277
|
+
if (Buffer.byteLength(clean, "utf8") <= MAX_OUTPUT_PROCESS_NAME_BYTES) {
|
|
278
|
+
return clean;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const suffix = "…";
|
|
282
|
+
const buffer = Buffer.from(clean, "utf8");
|
|
283
|
+
let end = MAX_OUTPUT_PROCESS_NAME_BYTES - Buffer.byteLength(suffix, "utf8");
|
|
284
|
+
while (end > 0 && (buffer[end] ?? 0) >> 6 === 0b10) {
|
|
285
|
+
end--;
|
|
286
|
+
}
|
|
287
|
+
return `${buffer.subarray(0, end).toString("utf8")}${suffix}`;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function clampTailLines(value: number | undefined): number {
|
|
291
|
+
if (value === undefined || value === null) return DEFAULT_OUTPUT_TAIL_LINES;
|
|
292
|
+
const n = Math.floor(value);
|
|
293
|
+
if (n < 1) return DEFAULT_OUTPUT_TAIL_LINES;
|
|
294
|
+
if (n > MAX_OUTPUT_TAIL_LINES) return MAX_OUTPUT_TAIL_LINES;
|
|
295
|
+
return n;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function validateRegex(pattern: string): void {
|
|
299
|
+
try {
|
|
300
|
+
new RegExp(pattern);
|
|
301
|
+
} catch (error) {
|
|
302
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
303
|
+
throw new Error(
|
|
304
|
+
`process output pattern is not a valid regular expression: ${message}`,
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
}
|