@aliou/pi-processes 0.9.5 → 0.10.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.
Files changed (174) hide show
  1. package/CONTRIBUTING.md +40 -42
  2. package/README.md +54 -84
  3. package/extensions/process-tabs.ts +26 -0
  4. package/extensions/processes/client.ts +194 -0
  5. package/extensions/processes/commands/clear.ts +29 -0
  6. package/extensions/processes/commands/kill.ts +152 -0
  7. package/extensions/processes/commands/overview.ts +102 -0
  8. package/extensions/processes/components/overview-component.ts +729 -0
  9. package/extensions/processes/components/overview-panel.ts +177 -0
  10. package/extensions/processes/config/defaults.ts +28 -0
  11. package/extensions/processes/config/index.ts +13 -0
  12. package/extensions/processes/config/loader.ts +62 -0
  13. package/extensions/processes/config/migrations/000-import-legacy-process-config.ts +113 -0
  14. package/extensions/processes/config/migrations/001-v0-9-4-to-v0-10-0-config.ts +184 -0
  15. package/extensions/processes/config/migrations/002-stamp-v0-10-0-config-version.ts +21 -0
  16. package/extensions/processes/config/migrations/index.ts +25 -0
  17. package/extensions/processes/config/schema.ts +9 -0
  18. package/extensions/processes/config/types.ts +72 -0
  19. package/extensions/processes/constants.ts +1 -0
  20. package/extensions/processes/handlers/commands.ts +113 -0
  21. package/extensions/processes/handlers/kill-process.ts +31 -0
  22. package/extensions/processes/handlers/notifications.ts +147 -0
  23. package/extensions/processes/handlers/requests.ts +114 -0
  24. package/extensions/processes/handlers/subscriptions.ts +144 -0
  25. package/extensions/processes/hooks/background-blocker.ts +201 -0
  26. package/extensions/processes/hooks/cleanup.ts +43 -0
  27. package/extensions/processes/hooks/event-bridge.ts +32 -0
  28. package/extensions/processes/i18n/index.ts +3 -0
  29. package/extensions/processes/i18n/messages.ts +27 -0
  30. package/extensions/processes/i18n/translator.ts +49 -0
  31. package/extensions/processes/index.ts +109 -0
  32. package/extensions/processes/message-renderer.ts +117 -0
  33. package/extensions/processes/notification-sender.ts +43 -0
  34. package/extensions/processes/notifications/classify.ts +27 -0
  35. package/extensions/processes/notifications/log-matchers.ts +146 -0
  36. package/extensions/processes/notifications/registry.ts +225 -0
  37. package/extensions/processes/notifications/render-content.ts +122 -0
  38. package/extensions/processes/notifications/service.ts +336 -0
  39. package/extensions/processes/notifications/types.ts +11 -0
  40. package/extensions/processes/settings/apply-setting-change.ts +30 -0
  41. package/extensions/processes/settings/build-sections.ts +420 -0
  42. package/extensions/processes/settings/index.ts +42 -0
  43. package/extensions/processes/tools/clear/index.ts +21 -0
  44. package/extensions/processes/tools/clear/render.ts +40 -0
  45. package/extensions/processes/tools/components/index.ts +4 -0
  46. package/extensions/processes/tools/components/process-action-header.ts +31 -0
  47. package/extensions/processes/tools/components/process-action-title.ts +17 -0
  48. package/extensions/processes/tools/components/tool-layout.ts +70 -0
  49. package/extensions/processes/tools/components/watch.ts +53 -0
  50. package/extensions/processes/tools/index.ts +265 -0
  51. package/extensions/processes/tools/list/index.ts +157 -0
  52. package/extensions/processes/tools/list/render.ts +211 -0
  53. package/extensions/processes/tools/notify.ts +191 -0
  54. package/extensions/processes/tools/output/index.ts +307 -0
  55. package/extensions/processes/tools/output/render.ts +229 -0
  56. package/extensions/processes/tools/schema.ts +263 -0
  57. package/extensions/processes/tools/start/index.ts +64 -0
  58. package/extensions/processes/tools/start/render.ts +67 -0
  59. package/extensions/processes/tools/stop/index.ts +35 -0
  60. package/extensions/processes/tools/stop/render.ts +43 -0
  61. package/extensions/processes/tools/update/index.ts +320 -0
  62. package/extensions/processes/tools/update/render.ts +251 -0
  63. package/extensions/processes/tools/utils.ts +112 -0
  64. package/extensions/processes/tools/watch-format.ts +14 -0
  65. package/extensions/processes/tools/write/index.ts +75 -0
  66. package/extensions/processes/tools/write/render.ts +68 -0
  67. package/extensions/processes/utils/truncate.ts +285 -0
  68. package/extensions/processes-dock/client.ts +60 -0
  69. package/extensions/processes-dock/commands/dock.ts +70 -0
  70. package/extensions/processes-dock/commands/pin.ts +166 -0
  71. package/extensions/processes-dock/components/log-dock-component.ts +375 -0
  72. package/extensions/processes-dock/dock-state.ts +33 -0
  73. package/extensions/processes-dock/index.ts +28 -0
  74. package/extensions/processes-dock/logs-client.ts +5 -0
  75. package/extensions/processes-dock/widget/setup.ts +520 -0
  76. package/extensions/processes-dock/widget/status.ts +149 -0
  77. package/extensions/processes-dock/widget/types.ts +16 -0
  78. package/extensions/processes-logs/client.ts +51 -0
  79. package/extensions/processes-logs/commands/logs.ts +108 -0
  80. package/extensions/processes-logs/completions.ts +31 -0
  81. package/extensions/processes-logs/components/log-file-viewer.ts +274 -0
  82. package/extensions/processes-logs/components/log-overlay-component.ts +685 -0
  83. package/extensions/processes-logs/index.ts +27 -0
  84. package/extensions/processes-logs/logs-client.ts +101 -0
  85. package/extensions/shared/line-buffer.ts +35 -0
  86. package/extensions/shared/output-payload.ts +28 -0
  87. package/extensions/shared/ui.ts +146 -0
  88. package/package.json +29 -10
  89. package/schema.json +134 -0
  90. package/skills/pi-processes/SKILL.md +496 -25
  91. package/src/get-manager.ts +15 -0
  92. package/src/manager/index.ts +182 -0
  93. package/src/manager/internal-types.ts +130 -0
  94. package/src/manager/limits.ts +15 -0
  95. package/src/manager/process-log-store.ts +422 -0
  96. package/src/manager/process-output.ts +345 -0
  97. package/src/manager/process-registry.ts +78 -0
  98. package/src/manager/process-runtime-controller.ts +519 -0
  99. package/src/protocol/broadcasts.ts +14 -0
  100. package/src/protocol/channels.ts +30 -0
  101. package/src/protocol/commands.ts +24 -0
  102. package/src/protocol/index.ts +34 -0
  103. package/src/protocol/logs.ts +23 -0
  104. package/src/protocol/notifications.ts +48 -0
  105. package/src/protocol/requests.ts +76 -0
  106. package/src/types.ts +66 -0
  107. package/src/utils/ansi.ts +2 -4
  108. package/src/utils/format.ts +24 -32
  109. package/src/utils/index.ts +5 -1
  110. package/src/utils/is-record.ts +3 -0
  111. package/src/utils/match-line.ts +32 -0
  112. package/src/utils/platform.ts +13 -0
  113. package/src/utils/process-group.ts +4 -0
  114. package/src/utils/shorten-path.ts +11 -0
  115. package/src/utils/signals.ts +22 -0
  116. package/src/commands/clear/command.ts +0 -14
  117. package/src/commands/clear/index.ts +0 -1
  118. package/src/commands/completions.ts +0 -38
  119. package/src/commands/dock/command.ts +0 -29
  120. package/src/commands/dock/index.ts +0 -1
  121. package/src/commands/index.ts +0 -33
  122. package/src/commands/kill/command.ts +0 -69
  123. package/src/commands/kill/index.ts +0 -1
  124. package/src/commands/logs/command.ts +0 -47
  125. package/src/commands/logs/index.ts +0 -1
  126. package/src/commands/pick-process.ts +0 -34
  127. package/src/commands/pin/command.ts +0 -34
  128. package/src/commands/pin/index.ts +0 -1
  129. package/src/commands/processes/command.ts +0 -39
  130. package/src/commands/processes/index.ts +0 -1
  131. package/src/commands/settings/apply-setting-change.ts +0 -72
  132. package/src/commands/settings/build-sections.ts +0 -160
  133. package/src/commands/settings/command.ts +0 -20
  134. package/src/commands/settings/index.ts +0 -1
  135. package/src/components/log-dock-component.test.ts +0 -29
  136. package/src/components/log-dock-component.ts +0 -236
  137. package/src/components/log-file-viewer.ts +0 -317
  138. package/src/components/log-overlay-component.ts +0 -546
  139. package/src/components/panel-helpers.ts +0 -37
  140. package/src/components/process-picker-component.ts +0 -166
  141. package/src/components/processes-component.ts +0 -485
  142. package/src/components/status-format.ts +0 -38
  143. package/src/config.ts +0 -107
  144. package/src/constants/index.ts +0 -16
  145. package/src/constants/types.ts +0 -119
  146. package/src/hooks/background-blocker.ts +0 -66
  147. package/src/hooks/cleanup.ts +0 -10
  148. package/src/hooks/index.ts +0 -32
  149. package/src/hooks/message-renderer.ts +0 -115
  150. package/src/hooks/process-end.ts +0 -69
  151. package/src/hooks/process-watch.ts +0 -85
  152. package/src/hooks/utils.ts +0 -23
  153. package/src/hooks/widget/index.ts +0 -2
  154. package/src/hooks/widget/setup.ts +0 -167
  155. package/src/hooks/widget/status-widget.ts +0 -105
  156. package/src/hooks/widget/types.ts +0 -20
  157. package/src/index.ts +0 -35
  158. package/src/manager.test.ts +0 -331
  159. package/src/manager.ts +0 -758
  160. package/src/tools/actions/clear.ts +0 -20
  161. package/src/tools/actions/debug.ts +0 -155
  162. package/src/tools/actions/index.ts +0 -171
  163. package/src/tools/actions/kill.ts +0 -89
  164. package/src/tools/actions/list.ts +0 -188
  165. package/src/tools/actions/logs.ts +0 -117
  166. package/src/tools/actions/output-truncate.ts +0 -187
  167. package/src/tools/actions/output.test.ts +0 -392
  168. package/src/tools/actions/output.ts +0 -447
  169. package/src/tools/actions/start.ts +0 -245
  170. package/src/tools/actions/write.ts +0 -113
  171. package/src/tools/index.ts +0 -207
  172. package/src/utils/ansi.test.ts +0 -22
  173. package/src/utils/command-executor.test.ts +0 -48
  174. package/src/utils/keybindings.ts +0 -71
@@ -0,0 +1,146 @@
1
+ import { compileLineMatcher } from "../../../src/utils/match-line";
2
+ import type { NotifyConfig } from "./registry";
3
+ import type { Attention } from "./types";
4
+
5
+ export interface CompiledLogMatcher {
6
+ pattern: string;
7
+ mode: "literal" | "regex";
8
+ stream: "stdout" | "stderr" | "both";
9
+ repeat: boolean;
10
+ on: Attention;
11
+ lineMatcher: ((line: string) => boolean) | null;
12
+ matcherIndex: number;
13
+ fired: boolean;
14
+ lastMatchTime: number;
15
+ }
16
+
17
+ export const MAX_LOG_MATCHERS_PER_PROCESS = 20;
18
+ export const MAX_LOG_MATCH_PATTERN_LENGTH = 500;
19
+ const MAX_LINE_LENGTH = 10_000;
20
+ const LOG_MATCH_COOLDOWN_MS = 15_000;
21
+
22
+ export function compileLogMatchers(config: NotifyConfig): CompiledLogMatcher[] {
23
+ const raw = config.logMatches;
24
+ if (!raw || raw.length === 0) return [];
25
+
26
+ const matchers: CompiledLogMatcher[] = [];
27
+
28
+ for (let i = 0; i < Math.min(raw.length, MAX_LOG_MATCHERS_PER_PROCESS); i++) {
29
+ const entry = raw[i];
30
+
31
+ if (entry.pattern.length > MAX_LOG_MATCH_PATTERN_LENGTH) {
32
+ continue;
33
+ }
34
+
35
+ const mode = entry.mode ?? "literal";
36
+ const stream = entry.stream ?? "both";
37
+ const repeat = entry.repeat ?? false;
38
+ const on = entry.on ?? "turn";
39
+
40
+ let lineMatcher: ((line: string) => boolean) | null = null;
41
+
42
+ if (mode === "regex") {
43
+ try {
44
+ lineMatcher = compileLineMatcher(entry.pattern, mode);
45
+ } catch (_error) {
46
+ void _error; // Invalid regex: skip this matcher
47
+ continue;
48
+ }
49
+ } else {
50
+ lineMatcher = compileLineMatcher(entry.pattern, mode);
51
+ }
52
+
53
+ matchers.push({
54
+ pattern: entry.pattern,
55
+ mode,
56
+ stream,
57
+ repeat,
58
+ on,
59
+ lineMatcher,
60
+ matcherIndex: i,
61
+ fired: false,
62
+ lastMatchTime: 0,
63
+ });
64
+ }
65
+
66
+ return matchers;
67
+ }
68
+
69
+ export interface LogMatchResult {
70
+ matcherIndex: number;
71
+ pattern: string;
72
+ mode: "literal" | "regex";
73
+ stream: "stdout" | "stderr";
74
+ line: string;
75
+ on: Attention;
76
+ }
77
+
78
+ export function evaluateLogMatchers(
79
+ matchers: CompiledLogMatcher[],
80
+ appendedText: Array<{ type: "stdout" | "stderr"; text: string }>,
81
+ now: number,
82
+ ): LogMatchResult[] {
83
+ const results: LogMatchResult[] = [];
84
+
85
+ const lines = splitAppendedIntoLines(appendedText);
86
+
87
+ for (const matcher of matchers) {
88
+ if (!matcher.repeat && matcher.fired) continue;
89
+
90
+ if (
91
+ matcher.repeat &&
92
+ matcher.fired &&
93
+ now - matcher.lastMatchTime < LOG_MATCH_COOLDOWN_MS
94
+ ) {
95
+ continue;
96
+ }
97
+
98
+ for (const line of lines) {
99
+ if (!streamMatches(matcher.stream, line.type)) continue;
100
+ if (line.text.length > MAX_LINE_LENGTH) continue;
101
+
102
+ const matched = matcher.lineMatcher?.(line.text);
103
+
104
+ if (matched) {
105
+ matcher.fired = true;
106
+ matcher.lastMatchTime = now;
107
+
108
+ results.push({
109
+ matcherIndex: matcher.matcherIndex,
110
+ pattern: matcher.pattern,
111
+ mode: matcher.mode,
112
+ stream: line.type,
113
+ line: line.text,
114
+ on: matcher.on,
115
+ });
116
+
117
+ break;
118
+ }
119
+ }
120
+ }
121
+
122
+ return results;
123
+ }
124
+
125
+ function splitAppendedIntoLines(
126
+ appended: Array<{ type: "stdout" | "stderr"; text: string }>,
127
+ ): Array<{ type: "stdout" | "stderr"; text: string }> {
128
+ const lines: Array<{ type: "stdout" | "stderr"; text: string }> = [];
129
+
130
+ for (const entry of appended) {
131
+ const split = entry.text.split("\n");
132
+ for (const line of split) {
133
+ if (line.length === 0) continue;
134
+ lines.push({ type: entry.type, text: line });
135
+ }
136
+ }
137
+
138
+ return lines;
139
+ }
140
+
141
+ function streamMatches(
142
+ matcherStream: "stdout" | "stderr" | "both",
143
+ lineStream: "stdout" | "stderr",
144
+ ): boolean {
145
+ return matcherStream === "both" || matcherStream === lineStream;
146
+ }
@@ -0,0 +1,225 @@
1
+ import { MAX_LOG_MATCHERS_PER_PROCESS } from "./log-matchers";
2
+ import type { Attention } from "./types";
3
+
4
+ export interface LogMatcherConfig {
5
+ pattern: string;
6
+ mode?: "literal" | "regex";
7
+ stream?: "stdout" | "stderr" | "both";
8
+ repeat?: boolean;
9
+ on?: Attention;
10
+ }
11
+
12
+ export interface NotifyConfig {
13
+ onSuccess?: Attention;
14
+ onFailure?: Attention;
15
+ onKilled?: Attention;
16
+ logMatches?: LogMatcherConfig[];
17
+ }
18
+
19
+ export interface WatchMeta {
20
+ revision: number;
21
+ generation: number;
22
+ }
23
+
24
+ export interface WatchState {
25
+ logMatches: LogMatcherConfig[];
26
+ revision: number;
27
+ generation: number;
28
+ }
29
+
30
+ export interface WatchRemoveSpec {
31
+ index?: number;
32
+ pattern?: string;
33
+ mode?: "literal" | "regex";
34
+ stream?: "stdout" | "stderr" | "both";
35
+ repeat?: boolean;
36
+ on?: Attention;
37
+ }
38
+
39
+ export interface WatchUpdateResult {
40
+ logMatches: LogMatcherConfig[];
41
+ revision: number;
42
+ generation: number;
43
+ }
44
+
45
+ export interface NotificationRegistry {
46
+ register(processId: string, config: NotifyConfig): void;
47
+ unregister(processId: string): void;
48
+ get(processId: string): NotifyConfig | null;
49
+ markIntentionalStop(processId: string): void;
50
+ consumeIntentionalStop(processId: string): boolean;
51
+ clear(): void;
52
+ appendWatches(
53
+ processId: string,
54
+ items: LogMatcherConfig[],
55
+ ): WatchUpdateResult | null;
56
+ replaceWatches(
57
+ processId: string,
58
+ items: LogMatcherConfig[],
59
+ ): WatchUpdateResult | null;
60
+ removeWatches(
61
+ processId: string,
62
+ specs: WatchRemoveSpec[],
63
+ ): WatchUpdateResult | null;
64
+ clearWatches(processId: string): WatchUpdateResult | null;
65
+ getWatchState(processId: string): WatchState | null;
66
+ }
67
+
68
+ export function createNotificationRegistry(): NotificationRegistry {
69
+ const configs = new Map<string, NotifyConfig>();
70
+ const intentionalStops = new Set<string>();
71
+ const watchMeta = new Map<string, WatchMeta>();
72
+
73
+ function currentMatches(processId: string): LogMatcherConfig[] {
74
+ const config = configs.get(processId);
75
+ return config?.logMatches?.map((m) => ({ ...m })) ?? [];
76
+ }
77
+
78
+ function setMatches(processId: string, matches: LogMatcherConfig[]): void {
79
+ const config = configs.get(processId);
80
+ if (config) {
81
+ config.logMatches = matches;
82
+ }
83
+ }
84
+
85
+ function bumpRevision(processId: string, bumpGeneration: boolean): WatchMeta {
86
+ const meta = watchMeta.get(processId) ?? { revision: 0, generation: 0 };
87
+ meta.revision++;
88
+ if (bumpGeneration) {
89
+ meta.generation++;
90
+ }
91
+ watchMeta.set(processId, meta);
92
+ return meta;
93
+ }
94
+
95
+ return {
96
+ register(processId: string, config: NotifyConfig): void {
97
+ configs.set(processId, config);
98
+ watchMeta.set(processId, { revision: 0, generation: 0 });
99
+ },
100
+
101
+ unregister(processId: string): void {
102
+ configs.delete(processId);
103
+ intentionalStops.delete(processId);
104
+ watchMeta.delete(processId);
105
+ },
106
+
107
+ get(processId: string): NotifyConfig | null {
108
+ return configs.get(processId) ?? null;
109
+ },
110
+
111
+ markIntentionalStop(processId: string): void {
112
+ intentionalStops.add(processId);
113
+ },
114
+
115
+ consumeIntentionalStop(processId: string): boolean {
116
+ return intentionalStops.delete(processId);
117
+ },
118
+
119
+ clear(): void {
120
+ configs.clear();
121
+ intentionalStops.clear();
122
+ watchMeta.clear();
123
+ },
124
+
125
+ appendWatches(
126
+ processId: string,
127
+ items: LogMatcherConfig[],
128
+ ): WatchUpdateResult | null {
129
+ if (!configs.has(processId)) return null;
130
+ const existing = currentMatches(processId);
131
+ const merged = [...existing, ...items];
132
+ if (merged.length > MAX_LOG_MATCHERS_PER_PROCESS) {
133
+ throw new Error(
134
+ `process update watches would exceed maximum of ${MAX_LOG_MATCHERS_PER_PROCESS} matchers`,
135
+ );
136
+ }
137
+ setMatches(processId, merged);
138
+ const meta = bumpRevision(processId, false);
139
+ return {
140
+ logMatches: merged,
141
+ revision: meta.revision,
142
+ generation: meta.generation,
143
+ };
144
+ },
145
+
146
+ replaceWatches(
147
+ processId: string,
148
+ items: LogMatcherConfig[],
149
+ ): WatchUpdateResult | null {
150
+ if (!configs.has(processId)) return null;
151
+ if (items.length > MAX_LOG_MATCHERS_PER_PROCESS) {
152
+ throw new Error(
153
+ `process update watches would exceed maximum of ${MAX_LOG_MATCHERS_PER_PROCESS} matchers`,
154
+ );
155
+ }
156
+ setMatches(processId, [...items]);
157
+ const meta = bumpRevision(processId, true);
158
+ return {
159
+ logMatches: [...items],
160
+ revision: meta.revision,
161
+ generation: meta.generation,
162
+ };
163
+ },
164
+
165
+ removeWatches(
166
+ processId: string,
167
+ specs: WatchRemoveSpec[],
168
+ ): WatchUpdateResult | null {
169
+ if (!configs.has(processId)) return null;
170
+ const existing = currentMatches(processId);
171
+ const filtered = existing.filter(
172
+ (matcher, index) => !matchesRemoveSpec(matcher, index, specs),
173
+ );
174
+ setMatches(processId, filtered);
175
+ const meta = bumpRevision(processId, false);
176
+ return {
177
+ logMatches: filtered,
178
+ revision: meta.revision,
179
+ generation: meta.generation,
180
+ };
181
+ },
182
+
183
+ clearWatches(processId: string): WatchUpdateResult | null {
184
+ if (!configs.has(processId)) return null;
185
+ setMatches(processId, []);
186
+ const meta = bumpRevision(processId, true);
187
+ return {
188
+ logMatches: [],
189
+ revision: meta.revision,
190
+ generation: meta.generation,
191
+ };
192
+ },
193
+
194
+ getWatchState(processId: string): WatchState | null {
195
+ if (!configs.has(processId)) return null;
196
+ const meta = watchMeta.get(processId) ?? { revision: 0, generation: 0 };
197
+ return {
198
+ logMatches: currentMatches(processId),
199
+ revision: meta.revision,
200
+ generation: meta.generation,
201
+ };
202
+ },
203
+ };
204
+ }
205
+
206
+ function matchesRemoveSpec(
207
+ matcher: LogMatcherConfig,
208
+ index: number,
209
+ specs: WatchRemoveSpec[],
210
+ ): boolean {
211
+ return specs.some((spec) => {
212
+ if (spec.index !== undefined) return spec.index === index;
213
+ if (!spec.pattern) return false;
214
+ if (matcher.pattern !== spec.pattern) return false;
215
+ if (spec.mode !== undefined && (matcher.mode ?? "literal") !== spec.mode)
216
+ return false;
217
+ if (spec.stream !== undefined && (matcher.stream ?? "both") !== spec.stream)
218
+ return false;
219
+ if (spec.repeat !== undefined && (matcher.repeat ?? false) !== spec.repeat)
220
+ return false;
221
+ if (spec.on !== undefined && (matcher.on ?? "turn") !== spec.on)
222
+ return false;
223
+ return true;
224
+ });
225
+ }
@@ -0,0 +1,122 @@
1
+ import type { ProcessNotificationDetails } from "./types";
2
+
3
+ export function buildProcessNotificationContent(
4
+ details: ProcessNotificationDetails,
5
+ ): string {
6
+ if (details.kind === "log_match" && details.logMatch) {
7
+ return buildLogMatchContent(details);
8
+ }
9
+ if (details.kind === "log_match_suppressed") {
10
+ return buildSuppressedContent(details);
11
+ }
12
+
13
+ return buildLifecycleContent(details);
14
+ }
15
+
16
+ function buildSuppressedContent(details: ProcessNotificationDetails): string {
17
+ return [
18
+ '<process_event type="notification_summary" kind="log_match_suppressed">',
19
+ element("summary", details.summary, 1),
20
+ "</process_event>",
21
+ ].join("\n");
22
+ }
23
+
24
+ function buildLifecycleContent(details: ProcessNotificationDetails): string {
25
+ const attrs = [
26
+ attr("type", "lifecycle"),
27
+ attr("kind", details.kind),
28
+ attr("process_id", details.processId),
29
+ attr("process_name", details.processName),
30
+ details.status ? attr("status", details.status) : null,
31
+ ]
32
+ .filter(Boolean)
33
+ .join(" ");
34
+
35
+ const lines = [
36
+ `<process_event ${attrs}>`,
37
+ element("summary", details.summary, 1),
38
+ element("command", details.command, 1),
39
+ ];
40
+
41
+ if (details.exitCode !== undefined && details.exitCode !== null) {
42
+ lines.push(element("exit_code", String(details.exitCode), 1));
43
+ }
44
+
45
+ if (details.endReason) {
46
+ lines.push(element("end_reason", details.endReason, 1));
47
+ }
48
+
49
+ if (details.signal) {
50
+ const signalAttrs = [
51
+ attr("name", details.signal.name),
52
+ details.signal.number === null
53
+ ? null
54
+ : attr("number", String(details.signal.number)),
55
+ ]
56
+ .filter(Boolean)
57
+ .join(" ");
58
+ lines.push(element("signal", details.signal.description, 1, signalAttrs));
59
+ }
60
+
61
+ lines.push(
62
+ element(
63
+ "logs_hint",
64
+ `Use process output/logs for process ${details.processId} to inspect recent output.`,
65
+ 1,
66
+ ),
67
+ );
68
+ lines.push("</process_event>");
69
+
70
+ return lines.join("\n");
71
+ }
72
+
73
+ function buildLogMatchContent(details: ProcessNotificationDetails): string {
74
+ const logMatch = details.logMatch;
75
+ if (!logMatch) {
76
+ return buildLifecycleContent(details);
77
+ }
78
+
79
+ const attrs = [
80
+ attr("type", "log_match"),
81
+ attr("kind", details.kind),
82
+ attr("process_id", details.processId),
83
+ attr("process_name", details.processName),
84
+ ].join(" ");
85
+
86
+ return [
87
+ `<process_event ${attrs}>`,
88
+ element("summary", details.summary, 1),
89
+ element("pattern", logMatch.pattern, 1, attr("mode", logMatch.mode)),
90
+ element("stream", logMatch.stream, 1),
91
+ element("matched_line", logMatch.line, 1),
92
+ "</process_event>",
93
+ ].join("\n");
94
+ }
95
+
96
+ function element(
97
+ name: string,
98
+ value: string,
99
+ indent: number,
100
+ attrs?: string,
101
+ ): string {
102
+ const padding = " ".repeat(indent);
103
+ const attrText = attrs ? ` ${attrs}` : "";
104
+ return `${padding}<${name}${attrText}>${escapeXmlText(value)}</${name}>`;
105
+ }
106
+
107
+ function attr(name: string, value: string): string {
108
+ return `${name}="${escapeXmlAttribute(value)}"`;
109
+ }
110
+
111
+ export function escapeXmlText(value: string): string {
112
+ return value
113
+ .replaceAll("&", "&amp;")
114
+ .replaceAll("<", "&lt;")
115
+ .replaceAll(">", "&gt;");
116
+ }
117
+
118
+ export function escapeXmlAttribute(value: string): string {
119
+ return escapeXmlText(value)
120
+ .replaceAll('"', "&quot;")
121
+ .replaceAll("'", "&apos;");
122
+ }