@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.
Files changed (171) 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 +47 -20
  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 +499 -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/shorten-path.ts +11 -0
  114. package/src/utils/signals.ts +22 -0
  115. package/src/commands/clear/command.ts +0 -14
  116. package/src/commands/clear/index.ts +0 -1
  117. package/src/commands/completions.ts +0 -38
  118. package/src/commands/dock/command.ts +0 -29
  119. package/src/commands/dock/index.ts +0 -1
  120. package/src/commands/index.ts +0 -33
  121. package/src/commands/kill/command.ts +0 -69
  122. package/src/commands/kill/index.ts +0 -1
  123. package/src/commands/logs/command.ts +0 -47
  124. package/src/commands/logs/index.ts +0 -1
  125. package/src/commands/pick-process.ts +0 -34
  126. package/src/commands/pin/command.ts +0 -34
  127. package/src/commands/pin/index.ts +0 -1
  128. package/src/commands/processes/command.ts +0 -39
  129. package/src/commands/processes/index.ts +0 -1
  130. package/src/commands/settings/apply-setting-change.ts +0 -72
  131. package/src/commands/settings/build-sections.ts +0 -160
  132. package/src/commands/settings/command.ts +0 -20
  133. package/src/commands/settings/index.ts +0 -1
  134. package/src/components/log-dock-component.test.ts +0 -29
  135. package/src/components/log-dock-component.ts +0 -236
  136. package/src/components/log-file-viewer.ts +0 -317
  137. package/src/components/log-overlay-component.ts +0 -546
  138. package/src/components/panel-helpers.ts +0 -37
  139. package/src/components/process-picker-component.ts +0 -166
  140. package/src/components/processes-component.ts +0 -485
  141. package/src/components/status-format.ts +0 -38
  142. package/src/config.ts +0 -107
  143. package/src/constants/index.ts +0 -16
  144. package/src/constants/types.ts +0 -106
  145. package/src/hooks/background-blocker.ts +0 -66
  146. package/src/hooks/cleanup.ts +0 -10
  147. package/src/hooks/index.ts +0 -32
  148. package/src/hooks/message-renderer.ts +0 -115
  149. package/src/hooks/process-end.ts +0 -69
  150. package/src/hooks/process-watch.ts +0 -85
  151. package/src/hooks/utils.ts +0 -23
  152. package/src/hooks/widget/index.ts +0 -2
  153. package/src/hooks/widget/setup.ts +0 -167
  154. package/src/hooks/widget/status-widget.ts +0 -105
  155. package/src/hooks/widget/types.ts +0 -20
  156. package/src/index.ts +0 -35
  157. package/src/manager.test.ts +0 -331
  158. package/src/manager.ts +0 -758
  159. package/src/tools/actions/clear.ts +0 -20
  160. package/src/tools/actions/debug.ts +0 -148
  161. package/src/tools/actions/index.ts +0 -171
  162. package/src/tools/actions/kill.ts +0 -89
  163. package/src/tools/actions/list.ts +0 -188
  164. package/src/tools/actions/logs.ts +0 -117
  165. package/src/tools/actions/output.ts +0 -269
  166. package/src/tools/actions/start.ts +0 -245
  167. package/src/tools/actions/write.ts +0 -113
  168. package/src/tools/index.ts +0 -207
  169. package/src/utils/ansi.test.ts +0 -22
  170. package/src/utils/command-executor.test.ts +0 -48
  171. 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
+ }