pi-usereq 0.5.0 → 0.7.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/CHANGELOG.md +48 -0
- package/README.md +1 -1
- package/package.json +1 -1
- package/pi-usereq/docs/REFERENCES.md +24470 -4590
- package/pi-usereq/docs/REQUIREMENTS.md +89 -68
- package/pi-usereq/docs/WORKFLOW.md +133 -106
- package/src/core/agent-tool-json.ts +42 -193
- package/src/core/compress-payload.ts +7 -15
- package/src/core/config.ts +84 -29
- package/src/core/extension-status.ts +49 -177
- package/src/core/find-payload.ts +21 -44
- package/src/core/pi-notify.ts +473 -313
- package/src/core/reference-payload.ts +6 -14
- package/src/core/settings-menu.ts +21 -1
- package/src/core/token-counter.ts +58 -124
- package/src/index.ts +798 -537
- package/src/resources/images/favicon.svg +21 -0
- package/src/resources/images/pi.dev.png +0 -0
- package/tests/debug-extension-harness.test.ts +17 -23
- package/tests/extension-registration.test.ts +632 -215
package/src/core/pi-notify.ts
CHANGED
|
@@ -1,70 +1,93 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @file
|
|
3
|
-
* @brief Implements pi-usereq
|
|
4
|
-
* @details Centralizes configuration defaults, status serialization,
|
|
3
|
+
* @brief Implements pi-usereq command-notify, sound, and Pushover prompt-end helpers.
|
|
4
|
+
* @details Centralizes configuration defaults, status serialization, prompt-end outcome classification, placeholder substitution, detached shell-command execution, and native Pushover delivery. Runtime is O(m + c + b) in `agent_end` message count plus command length and Pushover payload size. Side effects include detached child-process spawning and outbound HTTPS requests.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import
|
|
7
|
+
import os from "node:os";
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
import { spawn } from "node:child_process";
|
|
8
10
|
import * as https from "node:https";
|
|
9
11
|
import type { AgentEndEvent } from "@mariozechner/pi-coding-agent";
|
|
10
|
-
import { getInstallationPath } from "./path-context.js";
|
|
12
|
+
import { getInstallationPath, normalizePathSlashes } from "./path-context.js";
|
|
11
13
|
import type { UseReqConfig } from "./config.js";
|
|
12
14
|
|
|
13
15
|
/**
|
|
14
|
-
* @brief Enumerates supported
|
|
15
|
-
* @details Defines the canonical persisted order used by config normalization, shortcut cycling, status rendering, and
|
|
16
|
+
* @brief Enumerates supported sound levels.
|
|
17
|
+
* @details Defines the canonical persisted order used by config normalization, shortcut cycling, status rendering, and sound-command selection. Access complexity is O(1).
|
|
16
18
|
*/
|
|
17
19
|
export const PI_NOTIFY_SOUND_LEVELS = ["none", "low", "mid", "high"] as const;
|
|
18
20
|
|
|
19
21
|
/**
|
|
20
|
-
* @brief Represents one supported
|
|
21
|
-
* @details Narrows configuration parsing and runtime command dispatch to the canonical four-state
|
|
22
|
+
* @brief Represents one supported sound level.
|
|
23
|
+
* @details Narrows configuration parsing and runtime sound-command dispatch to the canonical four-state domain. Compile-time only and introduces no runtime cost.
|
|
22
24
|
*/
|
|
23
25
|
export type PiNotifySoundLevel = (typeof PI_NOTIFY_SOUND_LEVELS)[number];
|
|
24
26
|
|
|
25
27
|
/**
|
|
26
|
-
* @brief Enumerates supported prompt-end
|
|
27
|
-
* @details Distinguishes
|
|
28
|
+
* @brief Enumerates supported prompt-end outcomes.
|
|
29
|
+
* @details Distinguishes completed, interrupted, and failed prompt termination states for cross-channel notification routing. Access complexity is O(1).
|
|
28
30
|
*/
|
|
29
|
-
export const PI_NOTIFY_OUTCOMES = ["
|
|
31
|
+
export const PI_NOTIFY_OUTCOMES = ["completed", "interrupted", "failed"] as const;
|
|
30
32
|
|
|
31
33
|
/**
|
|
32
|
-
* @brief Represents one supported prompt-end
|
|
33
|
-
* @details Narrows prompt-end event classification and
|
|
34
|
+
* @brief Represents one supported prompt-end outcome.
|
|
35
|
+
* @details Narrows prompt-end event classification and per-feature toggle routing to the canonical completed/interrupted/failed domain. Compile-time only and introduces no runtime cost.
|
|
34
36
|
*/
|
|
35
37
|
export type PiNotifyOutcome = (typeof PI_NOTIFY_OUTCOMES)[number];
|
|
36
38
|
|
|
37
39
|
/**
|
|
38
|
-
* @brief Defines the default
|
|
40
|
+
* @brief Defines the default sound toggle shortcut.
|
|
39
41
|
* @details Seeds persisted configuration when no project-specific shortcut exists. Access complexity is O(1).
|
|
40
42
|
* @satisfies REQ-134
|
|
41
43
|
*/
|
|
42
44
|
export const DEFAULT_PI_NOTIFY_SOUND_TOGGLE_SHORTCUT = "alt+s";
|
|
43
45
|
|
|
44
46
|
/**
|
|
45
|
-
* @brief Defines the default
|
|
46
|
-
* @details Uses the bundled
|
|
47
|
-
* @satisfies REQ-
|
|
47
|
+
* @brief Defines the default command-notify shell command.
|
|
48
|
+
* @details Uses `notify-send`, the bundled icon, the fixed `PI-useReq` application name, and runtime placeholder substitution for prompt name, base path, elapsed time, and terminal outcome text. Access complexity is O(1).
|
|
49
|
+
* @satisfies REQ-175
|
|
50
|
+
*/
|
|
51
|
+
export const DEFAULT_PI_NOTIFY_CMD = "notify-send -i %%INSTALLATION_PATH%%/resources/images/pi.dev.png -a \"PI-useReq\" \"%%PROMT%% @ %%BASE%% [%%TIME%%]\" \"%%RESULT%%\"";
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* @brief Defines the default low-volume sound command.
|
|
55
|
+
* @details Uses the bundled notification asset with a low playback volume and defers `%%INSTALLATION_PATH%%` substitution until runtime execution. Access complexity is O(1).
|
|
56
|
+
* @satisfies REQ-133
|
|
48
57
|
*/
|
|
49
58
|
export const DEFAULT_PI_NOTIFY_SOUND_LOW_CMD = "paplay --volume=21845 %%INSTALLATION_PATH%%/resources/sounds/Soft-high-tech-notification-sound-effect.mp3";
|
|
50
59
|
|
|
51
60
|
/**
|
|
52
|
-
* @brief Defines the default mid-
|
|
53
|
-
* @details Uses the bundled
|
|
54
|
-
* @satisfies REQ-133
|
|
61
|
+
* @brief Defines the default mid-volume sound command.
|
|
62
|
+
* @details Uses the bundled notification asset with a mid playback volume and defers `%%INSTALLATION_PATH%%` substitution until runtime execution. Access complexity is O(1).
|
|
63
|
+
* @satisfies REQ-133
|
|
55
64
|
*/
|
|
56
65
|
export const DEFAULT_PI_NOTIFY_SOUND_MID_CMD = "paplay --volume=43690 %%INSTALLATION_PATH%%/resources/sounds/Soft-high-tech-notification-sound-effect.mp3";
|
|
57
66
|
|
|
58
67
|
/**
|
|
59
|
-
* @brief Defines the default high-
|
|
60
|
-
* @details Uses the bundled
|
|
61
|
-
* @satisfies REQ-133
|
|
68
|
+
* @brief Defines the default high-volume sound command.
|
|
69
|
+
* @details Uses the bundled notification asset with a high playback volume and defers `%%INSTALLATION_PATH%%` substitution until runtime execution. Access complexity is O(1).
|
|
70
|
+
* @satisfies REQ-133
|
|
62
71
|
*/
|
|
63
72
|
export const DEFAULT_PI_NOTIFY_SOUND_HIGH_CMD = "paplay --volume=65535 %%INSTALLATION_PATH%%/resources/sounds/Soft-high-tech-notification-sound-effect.mp3";
|
|
64
73
|
|
|
74
|
+
/**
|
|
75
|
+
* @brief Defines the default Pushover title template.
|
|
76
|
+
* @details Reuses the same runtime placeholder contract required by `PI_NOTIFY_CMD` except for `%%INSTALLATION_PATH%%`. Access complexity is O(1).
|
|
77
|
+
* @satisfies REQ-185
|
|
78
|
+
*/
|
|
79
|
+
export const DEFAULT_PI_NOTIFY_PUSHOVER_TITLE = "%%PROMT%% @ %%BASE%% [%%TIME%%]";
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* @brief Defines the default Pushover text template.
|
|
83
|
+
* @details Prefixes outbound Pushover messages with the terminal outcome token and preserves the raw prompt arguments on the following line so notifications remain traceable to the invoked prompt command. Access complexity is O(1).
|
|
84
|
+
* @satisfies REQ-185
|
|
85
|
+
*/
|
|
86
|
+
export const DEFAULT_PI_NOTIFY_PUSHOVER_TEXT = "%%RESULT%%\n%%ARGS%%";
|
|
87
|
+
|
|
65
88
|
/**
|
|
66
89
|
* @brief Enumerates supported Pushover priorities.
|
|
67
|
-
* @details Restricts persisted Pushover delivery to the canonical normal and high-priority values accepted by the
|
|
90
|
+
* @details Restricts persisted Pushover delivery to the canonical normal and high-priority values accepted by the configuration menu. Access complexity is O(1).
|
|
68
91
|
*/
|
|
69
92
|
export const PI_NOTIFY_PUSHOVER_PRIORITIES = [0, 1] as const;
|
|
70
93
|
|
|
@@ -75,10 +98,10 @@ export const PI_NOTIFY_PUSHOVER_PRIORITIES = [0, 1] as const;
|
|
|
75
98
|
export type PiNotifyPushoverPriority = (typeof PI_NOTIFY_PUSHOVER_PRIORITIES)[number];
|
|
76
99
|
|
|
77
100
|
/**
|
|
78
|
-
* @brief Describes one
|
|
79
|
-
* @details Stores the prompt command name,
|
|
101
|
+
* @brief Describes one prompt-end request used for command-notify and Pushover substitution.
|
|
102
|
+
* @details Stores the prompt command name, raw prompt arguments, runtime base path, and final prompt duration required to resolve runtime placeholders. The interface is compile-time only and introduces no runtime cost.
|
|
80
103
|
*/
|
|
81
|
-
export interface
|
|
104
|
+
export interface PiNotifyEventRequest {
|
|
82
105
|
promptName: string;
|
|
83
106
|
promptArgs: string;
|
|
84
107
|
basePath: string;
|
|
@@ -87,31 +110,52 @@ export interface PiNotifyPushoverRequest {
|
|
|
87
110
|
|
|
88
111
|
/**
|
|
89
112
|
* @brief Describes the configuration fields consumed by pi-notify helpers.
|
|
90
|
-
* @details Narrows the full project config to the persisted
|
|
113
|
+
* @details Narrows the full project config to the persisted notify, sound, and Pushover fields used by status rendering, prompt-end routing, and shortcut toggles. Compile-time only and introduces no runtime cost.
|
|
91
114
|
*/
|
|
92
115
|
export type PiNotifyConfigFields = Pick<
|
|
93
116
|
UseReqConfig,
|
|
94
|
-
| "notify-
|
|
95
|
-
| "notify-
|
|
96
|
-
| "notify-
|
|
117
|
+
| "notify-enabled"
|
|
118
|
+
| "notify-on-completed"
|
|
119
|
+
| "notify-on-interrupted"
|
|
120
|
+
| "notify-on-failed"
|
|
97
121
|
| "notify-sound"
|
|
122
|
+
| "notify-sound-on-completed"
|
|
123
|
+
| "notify-sound-on-interrupted"
|
|
124
|
+
| "notify-sound-on-failed"
|
|
98
125
|
| "notify-sound-toggle-shortcut"
|
|
99
|
-
| "notify-pushover-
|
|
100
|
-
| "notify-pushover-on-
|
|
126
|
+
| "notify-pushover-enabled"
|
|
127
|
+
| "notify-pushover-on-completed"
|
|
128
|
+
| "notify-pushover-on-interrupted"
|
|
129
|
+
| "notify-pushover-on-failed"
|
|
101
130
|
| "notify-pushover-user-key"
|
|
102
131
|
| "notify-pushover-api-token"
|
|
103
132
|
| "notify-pushover-priority"
|
|
133
|
+
| "notify-pushover-title"
|
|
134
|
+
| "notify-pushover-text"
|
|
135
|
+
| "PI_NOTIFY_CMD"
|
|
104
136
|
| "PI_NOTIFY_SOUND_LOW_CMD"
|
|
105
137
|
| "PI_NOTIFY_SOUND_MID_CMD"
|
|
106
138
|
| "PI_NOTIFY_SOUND_HIGH_CMD"
|
|
107
139
|
>;
|
|
108
140
|
|
|
141
|
+
/**
|
|
142
|
+
* @brief Describes the shell-spawn callback used by prompt-end command dispatch.
|
|
143
|
+
* @details Narrows the injected spawn surface so deterministic tests can capture detached shell invocations without patching global module state externally. Compile-time only and introduces no runtime cost.
|
|
144
|
+
*/
|
|
145
|
+
type PiNotifySpawn = typeof spawn;
|
|
146
|
+
|
|
109
147
|
/**
|
|
110
148
|
* @brief Stores the currently configured native HTTPS request function used for Pushover delivery.
|
|
111
149
|
* @details Defaults to `node:https.request` and can be replaced by deterministic tests so Pushover dispatch remains observable without real network I/O. Access complexity is O(1). Side effect: mutated only through the dedicated test hook.
|
|
112
150
|
*/
|
|
113
151
|
let piNotifyHttpsRequest: typeof https.request = https.request;
|
|
114
152
|
|
|
153
|
+
/**
|
|
154
|
+
* @brief Stores the currently configured shell-spawn function used for notify and sound commands.
|
|
155
|
+
* @details Defaults to `node:child_process.spawn` and can be replaced by deterministic tests so detached shell-command execution remains observable without launching real child processes. Access complexity is O(1). Side effect: mutated only through the dedicated test hook.
|
|
156
|
+
*/
|
|
157
|
+
let piNotifySpawn: PiNotifySpawn = spawn;
|
|
158
|
+
|
|
115
159
|
/**
|
|
116
160
|
* @brief Normalizes one persisted sound level.
|
|
117
161
|
* @details Accepts only canonical `none|low|mid|high` values and falls back to `none` for missing or invalid payloads. Runtime is O(1). No external state is mutated.
|
|
@@ -139,17 +183,29 @@ export function normalizePiNotifyShortcut(value: unknown): string {
|
|
|
139
183
|
}
|
|
140
184
|
|
|
141
185
|
/**
|
|
142
|
-
* @brief Normalizes one persisted
|
|
143
|
-
* @details Accepts any non-empty string so project config can override bundled
|
|
186
|
+
* @brief Normalizes one persisted shell-command string.
|
|
187
|
+
* @details Accepts any non-empty string so project config can override bundled command templates verbatim and falls back to the supplied default when the payload is empty or invalid. Runtime is O(n) in command length. No external state is mutated.
|
|
144
188
|
* @param[in] value {unknown} Raw persisted command payload.
|
|
145
189
|
* @param[in] fallback {string} Canonical fallback command.
|
|
146
190
|
* @return {string} Canonical non-empty command string.
|
|
147
|
-
* @satisfies REQ-133
|
|
191
|
+
* @satisfies REQ-133, REQ-175
|
|
148
192
|
*/
|
|
149
193
|
export function normalizePiNotifyCommand(value: unknown, fallback: string): string {
|
|
150
194
|
return typeof value === "string" && value.trim() ? value.trim() : fallback;
|
|
151
195
|
}
|
|
152
196
|
|
|
197
|
+
/**
|
|
198
|
+
* @brief Normalizes one persisted template string.
|
|
199
|
+
* @details Accepts any non-empty string so Pushover title and text templates can be user-configured verbatim and falls back to the supplied default when the payload is empty or invalid. Runtime is O(n) in template length. No external state is mutated.
|
|
200
|
+
* @param[in] value {unknown} Raw persisted template payload.
|
|
201
|
+
* @param[in] fallback {string} Canonical fallback template.
|
|
202
|
+
* @return {string} Canonical non-empty template string.
|
|
203
|
+
* @satisfies REQ-185
|
|
204
|
+
*/
|
|
205
|
+
export function normalizePiNotifyTemplateValue(value: unknown, fallback: string): string {
|
|
206
|
+
return typeof value === "string" && value.trim() ? value.trim() : fallback;
|
|
207
|
+
}
|
|
208
|
+
|
|
153
209
|
/**
|
|
154
210
|
* @brief Normalizes one persisted Pushover credential string.
|
|
155
211
|
* @details Accepts any trimmed string so the project config can store raw Pushover user and token values verbatim and falls back to the empty string for missing or invalid payloads. Runtime is O(n) in credential length. No external state is mutated.
|
|
@@ -166,42 +222,32 @@ export function normalizePiNotifyPushoverCredential(value: unknown): string {
|
|
|
166
222
|
* @details Accepts only canonical `0` and `1` values, treating numeric-string `"1"` as high priority and every other payload as normal priority. Runtime is O(1). No external state is mutated.
|
|
167
223
|
* @param[in] value {unknown} Raw persisted priority payload.
|
|
168
224
|
* @return {PiNotifyPushoverPriority} Canonical priority value.
|
|
169
|
-
* @satisfies REQ-
|
|
225
|
+
* @satisfies REQ-172
|
|
170
226
|
*/
|
|
171
227
|
export function normalizePiNotifyPushoverPriority(value: unknown): PiNotifyPushoverPriority {
|
|
172
228
|
return value === 1 || value === "1" ? 1 : 0;
|
|
173
229
|
}
|
|
174
230
|
|
|
175
231
|
/**
|
|
176
|
-
* @brief Formats
|
|
177
|
-
* @details
|
|
178
|
-
* @param[in] config {
|
|
179
|
-
* @return {string}
|
|
180
|
-
* @satisfies REQ-
|
|
232
|
+
* @brief Formats the global command-notify flag for UI value rendering.
|
|
233
|
+
* @details Serializes only the persisted global command-notify enable state so configuration menus and summaries can report `on|off` independently from per-event toggles. Runtime is O(1). No external state is mutated.
|
|
234
|
+
* @param[in] config {Pick<UseReqConfig, "notify-enabled">} Effective command-notify configuration subset.
|
|
235
|
+
* @return {string} `on` when command-notify is globally enabled; otherwise `off`.
|
|
236
|
+
* @satisfies REQ-196
|
|
181
237
|
*/
|
|
182
|
-
export function
|
|
183
|
-
|
|
184
|
-
if (config["notify-beep-on-end"]) {
|
|
185
|
-
enabledOutcomes.push("end");
|
|
186
|
-
}
|
|
187
|
-
if (config["notify-beep-on-esc"]) {
|
|
188
|
-
enabledOutcomes.push("esc");
|
|
189
|
-
}
|
|
190
|
-
if (config["notify-beep-on-error"]) {
|
|
191
|
-
enabledOutcomes.push("err");
|
|
192
|
-
}
|
|
193
|
-
return enabledOutcomes.length > 0 ? enabledOutcomes.join(",") : "none";
|
|
238
|
+
export function formatPiNotifyStatus(config: Pick<UseReqConfig, "notify-enabled">): string {
|
|
239
|
+
return config["notify-enabled"] ? "on" : "off";
|
|
194
240
|
}
|
|
195
241
|
|
|
196
242
|
/**
|
|
197
|
-
* @brief Formats the Pushover
|
|
198
|
-
* @details Serializes only the persisted
|
|
199
|
-
* @param[in] config {Pick<UseReqConfig, "notify-pushover-
|
|
200
|
-
* @return {string} `on` when
|
|
201
|
-
* @satisfies REQ-
|
|
243
|
+
* @brief Formats the global Pushover flag for UI value rendering.
|
|
244
|
+
* @details Serializes only the persisted global Pushover enable state so configuration menus and summaries can report `on|off` independently from per-event toggles. Runtime is O(1). No external state is mutated.
|
|
245
|
+
* @param[in] config {Pick<UseReqConfig, "notify-pushover-enabled">} Effective Pushover configuration subset.
|
|
246
|
+
* @return {string} `on` when Pushover is globally enabled; otherwise `off`.
|
|
247
|
+
* @satisfies REQ-163
|
|
202
248
|
*/
|
|
203
|
-
export function formatPiNotifyPushoverStatus(config: Pick<UseReqConfig, "notify-pushover-
|
|
204
|
-
return config["notify-pushover-
|
|
249
|
+
export function formatPiNotifyPushoverStatus(config: Pick<UseReqConfig, "notify-pushover-enabled">): string {
|
|
250
|
+
return config["notify-pushover-enabled"] ? "on" : "off";
|
|
205
251
|
}
|
|
206
252
|
|
|
207
253
|
/**
|
|
@@ -220,114 +266,28 @@ export function cyclePiNotifySoundLevel(currentLevel: PiNotifySoundLevel): PiNot
|
|
|
220
266
|
}
|
|
221
267
|
|
|
222
268
|
/**
|
|
223
|
-
* @brief
|
|
224
|
-
* @details
|
|
225
|
-
* @param[in]
|
|
226
|
-
* @
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
/**
|
|
233
|
-
* @brief Builds the PowerShell script used for Windows toast notifications.
|
|
234
|
-
* @details Reuses the pi notify example contract, escapes title and body literals, and emits a one-shot script that shows a toast notification through the Windows Runtime API. Runtime is O(n) in payload length. No external state is mutated.
|
|
235
|
-
* @param[in] title {string} Notification title.
|
|
236
|
-
* @param[in] body {string} Notification body.
|
|
237
|
-
* @return {string} PowerShell script passed to `powershell.exe`.
|
|
269
|
+
* @brief Tests whether one outcome-specific toggle is enabled.
|
|
270
|
+
* @details Reuses the canonical completed/interrupted/failed routing order shared by notify, sound, and Pushover event toggles. Runtime is O(1). No external state is mutated.
|
|
271
|
+
* @param[in] outcome {PiNotifyOutcome} Classified prompt-end outcome.
|
|
272
|
+
* @param[in] completedEnabled {boolean} Enabled state for completed prompt termination.
|
|
273
|
+
* @param[in] interruptedEnabled {boolean} Enabled state for interrupted prompt termination.
|
|
274
|
+
* @param[in] failedEnabled {boolean} Enabled state for failed prompt termination.
|
|
275
|
+
* @return {boolean} `true` when the selected outcome flag is enabled.
|
|
238
276
|
*/
|
|
239
|
-
function
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
`[${type}.ToastNotificationManager]::CreateToastNotifier('pi-usereq').Show($toast)`,
|
|
253
|
-
].join("; ");
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
/**
|
|
257
|
-
* @brief Emits one OSC 777 terminal notification.
|
|
258
|
-
* @details Writes the Ghostty/iTerm2/WezTerm-compatible OSC 777 escape sequence directly to stdout using the supplied title and body payloads. Runtime is O(n) in payload length. Side effect: writes to stdout.
|
|
259
|
-
* @param[in] title {string} Notification title.
|
|
260
|
-
* @param[in] body {string} Notification body.
|
|
261
|
-
* @return {void} No return value.
|
|
262
|
-
* @satisfies REQ-130
|
|
263
|
-
*/
|
|
264
|
-
export function notifyOSC777(title: string, body: string): void {
|
|
265
|
-
process.stdout.write(`\u001b]777;notify;${title};${body}\u0007`);
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
/**
|
|
269
|
-
* @brief Emits one Kitty OSC 99 terminal notification.
|
|
270
|
-
* @details Writes the two-part Kitty OSC 99 sequence that carries the title and body payloads under one notification identifier. Runtime is O(n) in payload length. Side effect: writes to stdout.
|
|
271
|
-
* @param[in] title {string} Notification title.
|
|
272
|
-
* @param[in] body {string} Notification body.
|
|
273
|
-
* @return {void} No return value.
|
|
274
|
-
* @satisfies REQ-130
|
|
275
|
-
*/
|
|
276
|
-
export function notifyOSC99(title: string, body: string): void {
|
|
277
|
-
process.stdout.write(`\u001b]99;i=1:d=0;${title}\u001b\\`);
|
|
278
|
-
process.stdout.write(`\u001b]99;i=1:p=body;${body}\u001b\\`);
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
/**
|
|
282
|
-
* @brief Emits one OSC 9 terminal notification.
|
|
283
|
-
* @details Writes the single-part OSC 9 sequence commonly used by terminals that accept message-only desktop notifications. Runtime is O(n) in payload length. Side effect: writes to stdout.
|
|
284
|
-
* @param[in] title {string} Notification title.
|
|
285
|
-
* @param[in] body {string} Notification body.
|
|
286
|
-
* @return {void} No return value.
|
|
287
|
-
* @satisfies REQ-130
|
|
288
|
-
*/
|
|
289
|
-
export function notifyOSC9(title: string, body: string): void {
|
|
290
|
-
process.stdout.write(`\u001b]9;${title}: ${body}\u0007`);
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
/**
|
|
294
|
-
* @brief Emits one Windows toast notification.
|
|
295
|
-
* @details Spawns `powershell.exe` without waiting, delegates payload rendering to `windowsToastScript(...)`, and ignores transport failures so prompt-end handling remains non-blocking. Runtime is dominated by process spawn. Side effects include child-process execution.
|
|
296
|
-
* @param[in] title {string} Notification title.
|
|
297
|
-
* @param[in] body {string} Notification body.
|
|
298
|
-
* @return {void} No return value.
|
|
299
|
-
* @satisfies REQ-130
|
|
300
|
-
*/
|
|
301
|
-
export function notifyWindows(title: string, body: string): void {
|
|
302
|
-
void execFile(
|
|
303
|
-
"powershell.exe",
|
|
304
|
-
["-NoProfile", "-Command", windowsToastScript(title, body)],
|
|
305
|
-
() => undefined,
|
|
306
|
-
);
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
/**
|
|
310
|
-
* @brief Routes one prompt-end terminal notification through the detected terminal protocol.
|
|
311
|
-
* @details Prefers Windows toast delivery inside Windows Terminal, Kitty OSC 99 inside Kitty, OSC 9 inside iTerm-like terminals, and OSC 777 as the fallback path. Runtime is O(n) in payload length plus optional process spawn cost. Side effects include stdout writes or child-process execution.
|
|
312
|
-
* @param[in] title {string} Notification title.
|
|
313
|
-
* @param[in] body {string} Notification body.
|
|
314
|
-
* @return {void} No return value.
|
|
315
|
-
* @satisfies REQ-130
|
|
316
|
-
*/
|
|
317
|
-
export function notifyPiTerminal(title: string, body: string): void {
|
|
318
|
-
if (process.env.WT_SESSION) {
|
|
319
|
-
notifyWindows(title, body);
|
|
320
|
-
return;
|
|
321
|
-
}
|
|
322
|
-
if (process.env.KITTY_WINDOW_ID) {
|
|
323
|
-
notifyOSC99(title, body);
|
|
324
|
-
return;
|
|
325
|
-
}
|
|
326
|
-
if (process.env.TERM_PROGRAM === "iTerm.app" || process.env.TERM_PROGRAM === "Apple_Terminal") {
|
|
327
|
-
notifyOSC9(title, body);
|
|
328
|
-
return;
|
|
277
|
+
function isPiNotifyOutcomeEnabled(
|
|
278
|
+
outcome: PiNotifyOutcome,
|
|
279
|
+
completedEnabled: boolean,
|
|
280
|
+
interruptedEnabled: boolean,
|
|
281
|
+
failedEnabled: boolean,
|
|
282
|
+
): boolean {
|
|
283
|
+
switch (outcome) {
|
|
284
|
+
case "interrupted":
|
|
285
|
+
return interruptedEnabled;
|
|
286
|
+
case "failed":
|
|
287
|
+
return failedEnabled;
|
|
288
|
+
default:
|
|
289
|
+
return completedEnabled;
|
|
329
290
|
}
|
|
330
|
-
notifyOSC777(title, body);
|
|
331
291
|
}
|
|
332
292
|
|
|
333
293
|
/**
|
|
@@ -351,120 +311,388 @@ function hasAgentEndStopReason(
|
|
|
351
311
|
|
|
352
312
|
/**
|
|
353
313
|
* @brief Classifies one agent-end payload into the canonical pi-notify outcome.
|
|
354
|
-
* @details Treats assistant `stopReason=error` as `
|
|
314
|
+
* @details Treats assistant `stopReason=error` as `failed`, `stopReason=aborted` as `interrupted`, and every remaining terminal state as `completed`. Runtime is O(m) in message count. No external state is mutated.
|
|
355
315
|
* @param[in] event {Pick<AgentEndEvent, "messages">} Agent-end payload subset.
|
|
356
316
|
* @return {PiNotifyOutcome} Canonical prompt-end outcome.
|
|
357
|
-
* @satisfies REQ-129
|
|
358
317
|
*/
|
|
359
318
|
export function classifyPiNotifyOutcome(event: Pick<AgentEndEvent, "messages">): PiNotifyOutcome {
|
|
360
319
|
if (hasAgentEndStopReason(event.messages, "error")) {
|
|
361
|
-
return "
|
|
320
|
+
return "failed";
|
|
362
321
|
}
|
|
363
322
|
if (hasAgentEndStopReason(event.messages, "aborted")) {
|
|
364
|
-
return "
|
|
323
|
+
return "interrupted";
|
|
365
324
|
}
|
|
366
|
-
return "
|
|
325
|
+
return "completed";
|
|
367
326
|
}
|
|
368
327
|
|
|
369
328
|
/**
|
|
370
|
-
* @brief
|
|
371
|
-
* @details
|
|
372
|
-
* @
|
|
329
|
+
* @brief Formats one prompt-end duration for runtime placeholders.
|
|
330
|
+
* @details Floors the supplied duration to whole seconds, keeps minutes unbounded above 59, and zero-pads seconds to two digits so `%%TIME%%` aligns with status-bar elapsed formatting. Runtime is O(1). No external state is mutated.
|
|
331
|
+
* @param[in] durationMs {number} Prompt-end duration in milliseconds.
|
|
332
|
+
* @return {string} Duration rendered as `M:SS`.
|
|
333
|
+
* @satisfies REQ-187
|
|
373
334
|
*/
|
|
374
|
-
function
|
|
375
|
-
|
|
335
|
+
function formatPiNotifyDuration(durationMs: number): string {
|
|
336
|
+
const totalSeconds = Math.max(0, Math.floor(durationMs / 1000));
|
|
337
|
+
const minutes = Math.floor(totalSeconds / 60);
|
|
338
|
+
const seconds = totalSeconds % 60;
|
|
339
|
+
return `${minutes}:${String(seconds).padStart(2, "0")}`;
|
|
376
340
|
}
|
|
377
341
|
|
|
378
342
|
/**
|
|
379
|
-
* @brief
|
|
380
|
-
* @details
|
|
381
|
-
* @param[in]
|
|
382
|
-
* @return {string}
|
|
343
|
+
* @brief Formats one runtime base path for placeholder substitution.
|
|
344
|
+
* @details Emits `~` or `~/...` when the supplied path equals or descends from the current user home directory; otherwise emits the slash-normalized absolute path. Runtime is O(p) in path length. No external state is mutated.
|
|
345
|
+
* @param[in] basePath {string} Runtime base path.
|
|
346
|
+
* @return {string} Placeholder-ready base path string.
|
|
347
|
+
* @satisfies REQ-187
|
|
383
348
|
*/
|
|
384
|
-
function
|
|
349
|
+
function formatPiNotifyBasePath(basePath: string): string {
|
|
350
|
+
const normalizedBasePath = path.resolve(basePath);
|
|
351
|
+
const homePath = path.resolve(os.homedir());
|
|
352
|
+
if (normalizedBasePath === homePath) {
|
|
353
|
+
return "~";
|
|
354
|
+
}
|
|
355
|
+
const relativePath = path.relative(homePath, normalizedBasePath);
|
|
356
|
+
if (relativePath !== "" && !relativePath.startsWith("..") && !path.isAbsolute(relativePath)) {
|
|
357
|
+
return `~/${normalizePathSlashes(relativePath)}`;
|
|
358
|
+
}
|
|
359
|
+
return normalizePathSlashes(normalizedBasePath);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* @brief Formats one prompt-end outcome for `%%RESULT%%` substitution.
|
|
364
|
+
* @details Maps canonical prompt-end outcomes to the persisted human-readable result tokens reused by default notify-command and Pushover templates. Runtime is O(1). No external state is mutated.
|
|
365
|
+
* @param[in] outcome {PiNotifyOutcome} Classified prompt-end outcome.
|
|
366
|
+
* @return {string} `successed`, `aborted`, or `failed`.
|
|
367
|
+
* @satisfies REQ-169, REQ-186, REQ-199
|
|
368
|
+
*/
|
|
369
|
+
function formatPiNotifyResult(outcome: PiNotifyOutcome): string {
|
|
385
370
|
switch (outcome) {
|
|
386
|
-
case "
|
|
387
|
-
return "
|
|
388
|
-
case "
|
|
389
|
-
return "
|
|
371
|
+
case "interrupted":
|
|
372
|
+
return "aborted";
|
|
373
|
+
case "failed":
|
|
374
|
+
return "failed";
|
|
390
375
|
default:
|
|
391
|
-
return "
|
|
376
|
+
return "successed";
|
|
392
377
|
}
|
|
393
378
|
}
|
|
394
379
|
|
|
395
380
|
/**
|
|
396
|
-
* @brief
|
|
397
|
-
* @details
|
|
398
|
-
* @param[in]
|
|
399
|
-
* @
|
|
400
|
-
* @
|
|
381
|
+
* @brief Builds the raw runtime placeholder map for one prompt-end request.
|
|
382
|
+
* @details Resolves every placeholder value exactly once so notify-command and Pushover template substitution reuse the same prompt name, base path, elapsed time, argument string, and terminal outcome token. Runtime is O(p) in path length. No external state is mutated.
|
|
383
|
+
* @param[in] request {PiNotifyEventRequest} Prompt-end request metadata.
|
|
384
|
+
* @param[in] outcome {PiNotifyOutcome} Classified prompt-end outcome.
|
|
385
|
+
* @return {{ "%%PROMT%%": string; "%%BASE%%": string; "%%TIME%%": string; "%%ARGS%%": string; "%%RESULT%%": string }} Raw placeholder-value map.
|
|
401
386
|
*/
|
|
402
|
-
function
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
return
|
|
387
|
+
function buildPiNotifyRuntimeTemplateValues(
|
|
388
|
+
request: PiNotifyEventRequest,
|
|
389
|
+
outcome: PiNotifyOutcome,
|
|
390
|
+
): Record<string, string> {
|
|
391
|
+
return {
|
|
392
|
+
"%%PROMT%%": request.promptName,
|
|
393
|
+
"%%BASE%%": formatPiNotifyBasePath(request.basePath),
|
|
394
|
+
"%%TIME%%": formatPiNotifyDuration(request.completionTimeMs),
|
|
395
|
+
"%%ARGS%%": request.promptArgs,
|
|
396
|
+
"%%RESULT%%": formatPiNotifyResult(outcome),
|
|
397
|
+
};
|
|
407
398
|
}
|
|
408
399
|
|
|
409
400
|
/**
|
|
410
|
-
* @brief
|
|
411
|
-
* @details
|
|
412
|
-
* @param[in]
|
|
413
|
-
* @return {string}
|
|
414
|
-
* @satisfies REQ-169
|
|
401
|
+
* @brief Quotes one installation path for shell substitution.
|
|
402
|
+
* @details Emits POSIX single-quoted literals for `sh -lc` execution and CMD double-quoted literals for `cmd.exe /c` execution so `%%INSTALLATION_PATH%%` substitutions preserve whitespace safely. Runtime is O(n) in path length. No external state is mutated.
|
|
403
|
+
* @param[in] installationPath {string} Absolute extension installation path.
|
|
404
|
+
* @return {string} Shell-quoted installation path fragment.
|
|
415
405
|
*/
|
|
416
|
-
function
|
|
417
|
-
|
|
406
|
+
function quotePiNotifyInstallPath(installationPath: string): string {
|
|
407
|
+
if (process.platform === "win32") {
|
|
408
|
+
return `"${installationPath.replace(/"/g, '""')}"`;
|
|
409
|
+
}
|
|
410
|
+
return `'${installationPath.replace(/'/g, `'\\''`)}'`;
|
|
418
411
|
}
|
|
419
412
|
|
|
420
413
|
/**
|
|
421
|
-
* @brief
|
|
422
|
-
* @details
|
|
423
|
-
* @param[in]
|
|
424
|
-
* @return {string}
|
|
414
|
+
* @brief Escapes one placeholder value for double-quoted shell insertion.
|
|
415
|
+
* @details Escapes the characters interpreted specially by POSIX or CMD double-quoted strings so default notify-command templates remain safe when placeholders are embedded inside double quotes. Runtime is O(n) in value length. No external state is mutated.
|
|
416
|
+
* @param[in] value {string} Raw placeholder value.
|
|
417
|
+
* @return {string} Shell-escaped placeholder fragment without surrounding quotes.
|
|
418
|
+
*/
|
|
419
|
+
function escapePiNotifyShellTemplateValue(value: string): string {
|
|
420
|
+
if (process.platform === "win32") {
|
|
421
|
+
return value.replace(/%/g, "%%").replace(/"/g, '""');
|
|
422
|
+
}
|
|
423
|
+
return value
|
|
424
|
+
.replace(/\\/g, "\\\\")
|
|
425
|
+
.replace(/\$/g, "\\$")
|
|
426
|
+
.replace(/`/g, "\\`")
|
|
427
|
+
.replace(/"/g, '\\"');
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* @brief Substitutes `%%INSTALLATION_PATH%%` inside one shell command.
|
|
432
|
+
* @details Replaces every `%%INSTALLATION_PATH%%` token with a shell-quoted runtime installation path so bundled assets can be addressed safely from external commands. Runtime is O(n) in command length. No external state is mutated.
|
|
433
|
+
* @param[in] command {string} Raw configured shell command.
|
|
434
|
+
* @param[in] installationPath {string} Absolute extension installation path.
|
|
435
|
+
* @return {string} Runtime-ready command string.
|
|
425
436
|
* @satisfies REQ-169
|
|
426
437
|
*/
|
|
427
|
-
function
|
|
428
|
-
return
|
|
438
|
+
export function substitutePiNotifyInstallPath(command: string, installationPath: string): string {
|
|
439
|
+
return command.replaceAll(
|
|
440
|
+
"%%INSTALLATION_PATH%%",
|
|
441
|
+
quotePiNotifyInstallPath(installationPath),
|
|
442
|
+
);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* @brief Substitutes runtime placeholders inside one shell-command template.
|
|
447
|
+
* @details Applies shell-quoted installation-path substitution plus shell-escaped prompt, base-path, elapsed-time, raw-argument, and outcome-result substitution expected by `PI_NOTIFY_CMD`. Runtime is O(n) in template length. No external state is mutated.
|
|
448
|
+
* @param[in] template {string} Raw shell-command template.
|
|
449
|
+
* @param[in] request {PiNotifyEventRequest} Prompt-end request metadata.
|
|
450
|
+
* @param[in] outcome {PiNotifyOutcome} Classified prompt-end outcome.
|
|
451
|
+
* @param[in] installationPath {string} Absolute extension installation path.
|
|
452
|
+
* @return {string} Runtime-ready shell command.
|
|
453
|
+
* @satisfies REQ-169, REQ-199
|
|
454
|
+
*/
|
|
455
|
+
function substitutePiNotifyShellTemplate(
|
|
456
|
+
template: string,
|
|
457
|
+
request: PiNotifyEventRequest,
|
|
458
|
+
outcome: PiNotifyOutcome,
|
|
459
|
+
installationPath: string,
|
|
460
|
+
): string {
|
|
461
|
+
const runtimeValues = buildPiNotifyRuntimeTemplateValues(request, outcome);
|
|
462
|
+
let result = substitutePiNotifyInstallPath(template, installationPath);
|
|
463
|
+
for (const [token, value] of Object.entries(runtimeValues)) {
|
|
464
|
+
result = result.replaceAll(token, escapePiNotifyShellTemplateValue(value));
|
|
465
|
+
}
|
|
466
|
+
return result;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/**
|
|
470
|
+
* @brief Substitutes runtime placeholders inside one text template.
|
|
471
|
+
* @details Applies raw prompt name, home-relative base path, elapsed time, raw prompt arguments, and terminal outcome text without shell escaping so Pushover payloads preserve literal text. Runtime is O(n) in template length. No external state is mutated.
|
|
472
|
+
* @param[in] template {string} Raw text template.
|
|
473
|
+
* @param[in] request {PiNotifyEventRequest} Prompt-end request metadata.
|
|
474
|
+
* @param[in] outcome {PiNotifyOutcome} Classified prompt-end outcome.
|
|
475
|
+
* @return {string} Placeholder-resolved text string.
|
|
476
|
+
* @satisfies REQ-186, REQ-187, REQ-199
|
|
477
|
+
*/
|
|
478
|
+
function substitutePiNotifyTextTemplate(
|
|
479
|
+
template: string,
|
|
480
|
+
request: PiNotifyEventRequest,
|
|
481
|
+
outcome: PiNotifyOutcome,
|
|
482
|
+
): string {
|
|
483
|
+
let result = template;
|
|
484
|
+
for (const [token, value] of Object.entries(buildPiNotifyRuntimeTemplateValues(request, outcome))) {
|
|
485
|
+
result = result.replaceAll(token, value);
|
|
486
|
+
}
|
|
487
|
+
return result;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* @brief Executes one detached shell command without waiting for completion.
|
|
492
|
+
* @details Uses the platform-default shell contract already employed by sound-command execution and ignores transport failures so prompt-end handling remains non-blocking. Runtime is dominated by process spawn. Side effects include detached child-process execution.
|
|
493
|
+
* @param[in] command {string} Runtime-ready shell command.
|
|
494
|
+
* @return {void} No return value.
|
|
495
|
+
*/
|
|
496
|
+
function runPiNotifyShellCommand(command: string): void {
|
|
497
|
+
const shell = process.platform === "win32" ? "cmd.exe" : (process.env.SHELL ?? "sh");
|
|
498
|
+
const shellArgs = process.platform === "win32"
|
|
499
|
+
? ["/d", "/s", "/c", command]
|
|
500
|
+
: ["-lc", command];
|
|
501
|
+
const child = piNotifySpawn(shell, shellArgs, {
|
|
502
|
+
detached: true,
|
|
503
|
+
stdio: "ignore",
|
|
504
|
+
});
|
|
505
|
+
child.once("error", () => undefined);
|
|
506
|
+
child.unref();
|
|
429
507
|
}
|
|
430
508
|
|
|
431
509
|
/**
|
|
432
|
-
* @brief Determines whether one
|
|
433
|
-
* @details Requires a
|
|
510
|
+
* @brief Determines whether one prompt-end outcome should trigger command-notify.
|
|
511
|
+
* @details Requires a prompt-end request, the global command-notify enable flag, and the corresponding per-event notify toggle. Runtime is O(1). No external state is mutated.
|
|
434
512
|
* @param[in] config {PiNotifyConfigFields} Effective notification configuration.
|
|
435
|
-
* @param[in]
|
|
513
|
+
* @param[in] outcome {PiNotifyOutcome} Classified prompt-end outcome.
|
|
514
|
+
* @param[in] request {PiNotifyEventRequest | undefined} Prompt-end request metadata.
|
|
515
|
+
* @return {boolean} `true` when command-notify prerequisites are satisfied.
|
|
516
|
+
* @satisfies REQ-174, REQ-176
|
|
517
|
+
*/
|
|
518
|
+
function shouldRunPiNotifyCommand(
|
|
519
|
+
config: PiNotifyConfigFields,
|
|
520
|
+
outcome: PiNotifyOutcome,
|
|
521
|
+
request: PiNotifyEventRequest | undefined,
|
|
522
|
+
): boolean {
|
|
523
|
+
return request !== undefined
|
|
524
|
+
&& config["notify-enabled"]
|
|
525
|
+
&& isPiNotifyOutcomeEnabled(
|
|
526
|
+
outcome,
|
|
527
|
+
config["notify-on-completed"],
|
|
528
|
+
config["notify-on-interrupted"],
|
|
529
|
+
config["notify-on-failed"],
|
|
530
|
+
);
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
/**
|
|
534
|
+
* @brief Executes the configured command-notify shell command.
|
|
535
|
+
* @details Resolves the runtime installation path, substitutes runtime placeholders into `PI_NOTIFY_CMD`, and spawns the resulting command without waiting for completion. Runtime is dominated by process spawn. Side effects include detached child-process execution.
|
|
536
|
+
* @param[in] config {PiNotifyConfigFields} Effective notification configuration.
|
|
537
|
+
* @param[in] outcome {PiNotifyOutcome} Classified prompt-end outcome.
|
|
538
|
+
* @param[in] request {PiNotifyEventRequest} Prompt-end request metadata.
|
|
539
|
+
* @return {void} No return value.
|
|
540
|
+
* @satisfies REQ-169, REQ-175, REQ-176, REQ-199
|
|
541
|
+
*/
|
|
542
|
+
function runPiNotifyCommand(
|
|
543
|
+
config: PiNotifyConfigFields,
|
|
544
|
+
outcome: PiNotifyOutcome,
|
|
545
|
+
request: PiNotifyEventRequest,
|
|
546
|
+
): void {
|
|
547
|
+
const installationPath = getInstallationPath();
|
|
548
|
+
const command = substitutePiNotifyShellTemplate(
|
|
549
|
+
config.PI_NOTIFY_CMD,
|
|
550
|
+
request,
|
|
551
|
+
outcome,
|
|
552
|
+
installationPath,
|
|
553
|
+
);
|
|
554
|
+
runPiNotifyShellCommand(command);
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
/**
|
|
558
|
+
* @brief Resolves the configured command for one non-`none` sound level.
|
|
559
|
+
* @details Selects the matching persisted command string from config without performing runtime substitution or shell execution. Runtime is O(1). No external state is mutated.
|
|
560
|
+
* @param[in] config {PiNotifyConfigFields} Effective notification configuration.
|
|
561
|
+
* @param[in] soundLevel {Exclude<PiNotifySoundLevel, "none">} Non-disabled sound level.
|
|
562
|
+
* @return {string} Configured command string for the requested level.
|
|
563
|
+
*/
|
|
564
|
+
function resolvePiNotifySoundCommand(
|
|
565
|
+
config: PiNotifyConfigFields,
|
|
566
|
+
soundLevel: Exclude<PiNotifySoundLevel, "none">,
|
|
567
|
+
): string {
|
|
568
|
+
switch (soundLevel) {
|
|
569
|
+
case "low":
|
|
570
|
+
return config.PI_NOTIFY_SOUND_LOW_CMD;
|
|
571
|
+
case "mid":
|
|
572
|
+
return config.PI_NOTIFY_SOUND_MID_CMD;
|
|
573
|
+
case "high":
|
|
574
|
+
return config.PI_NOTIFY_SOUND_HIGH_CMD;
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
/**
|
|
579
|
+
* @brief Determines whether one prompt-end outcome should trigger sound-command execution.
|
|
580
|
+
* @details Requires a non-`none` sound level and the corresponding per-event sound toggle. Runtime is O(1). No external state is mutated.
|
|
581
|
+
* @param[in] config {PiNotifyConfigFields} Effective notification configuration.
|
|
582
|
+
* @param[in] outcome {PiNotifyOutcome} Classified prompt-end outcome.
|
|
583
|
+
* @return {boolean} `true` when sound-command prerequisites are satisfied.
|
|
584
|
+
* @satisfies REQ-178
|
|
585
|
+
*/
|
|
586
|
+
function shouldRunPiNotifySound(
|
|
587
|
+
config: PiNotifyConfigFields,
|
|
588
|
+
outcome: PiNotifyOutcome,
|
|
589
|
+
): boolean {
|
|
590
|
+
return config["notify-sound"] !== "none"
|
|
591
|
+
&& isPiNotifyOutcomeEnabled(
|
|
592
|
+
outcome,
|
|
593
|
+
config["notify-sound-on-completed"],
|
|
594
|
+
config["notify-sound-on-interrupted"],
|
|
595
|
+
config["notify-sound-on-failed"],
|
|
596
|
+
);
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
/**
|
|
600
|
+
* @brief Executes the configured sound command on an external shell.
|
|
601
|
+
* @details Resolves the runtime installation path, substitutes `%%INSTALLATION_PATH%%`, and spawns the configured command without waiting for completion. Runtime is dominated by process spawn. Side effects include detached child-process execution.
|
|
602
|
+
* @param[in] config {PiNotifyConfigFields} Effective notification configuration.
|
|
603
|
+
* @param[in] soundLevel {Exclude<PiNotifySoundLevel, "none">} Requested non-disabled sound level.
|
|
604
|
+
* @return {void} No return value.
|
|
605
|
+
* @satisfies REQ-132, REQ-133
|
|
606
|
+
*/
|
|
607
|
+
export function runPiNotifySoundCommand(
|
|
608
|
+
config: PiNotifyConfigFields,
|
|
609
|
+
soundLevel: Exclude<PiNotifySoundLevel, "none">,
|
|
610
|
+
): void {
|
|
611
|
+
const rawCommand = resolvePiNotifySoundCommand(config, soundLevel);
|
|
612
|
+
const command = substitutePiNotifyInstallPath(rawCommand, getInstallationPath());
|
|
613
|
+
runPiNotifyShellCommand(command);
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
/**
|
|
617
|
+
* @brief Determines whether one prompt-end outcome should trigger Pushover delivery.
|
|
618
|
+
* @details Requires a prompt-end request, the global Pushover enable flag, the corresponding per-event Pushover toggle, and non-empty user plus token credentials. Runtime is O(1). No external state is mutated.
|
|
619
|
+
* @param[in] config {PiNotifyConfigFields} Effective notification configuration.
|
|
620
|
+
* @param[in] outcome {PiNotifyOutcome} Classified prompt-end outcome.
|
|
621
|
+
* @param[in] request {PiNotifyEventRequest | undefined} Prompt-end request metadata.
|
|
436
622
|
* @return {boolean} `true` when Pushover delivery prerequisites are satisfied.
|
|
437
|
-
* @satisfies REQ-166, REQ-168, REQ-
|
|
623
|
+
* @satisfies REQ-166, REQ-168, REQ-184
|
|
438
624
|
*/
|
|
439
625
|
function shouldRunPiNotifyPushover(
|
|
440
626
|
config: PiNotifyConfigFields,
|
|
441
|
-
|
|
627
|
+
outcome: PiNotifyOutcome,
|
|
628
|
+
request: PiNotifyEventRequest | undefined,
|
|
442
629
|
): boolean {
|
|
443
630
|
return request !== undefined
|
|
444
|
-
&& config["notify-pushover-
|
|
445
|
-
&&
|
|
631
|
+
&& config["notify-pushover-enabled"]
|
|
632
|
+
&& isPiNotifyOutcomeEnabled(
|
|
633
|
+
outcome,
|
|
634
|
+
config["notify-pushover-on-completed"],
|
|
635
|
+
config["notify-pushover-on-interrupted"],
|
|
636
|
+
config["notify-pushover-on-failed"],
|
|
637
|
+
)
|
|
446
638
|
&& config["notify-pushover-user-key"] !== ""
|
|
447
639
|
&& config["notify-pushover-api-token"] !== "";
|
|
448
640
|
}
|
|
449
641
|
|
|
450
642
|
/**
|
|
451
|
-
* @brief Builds the Pushover
|
|
452
|
-
* @details
|
|
643
|
+
* @brief Builds the Pushover notification title for one prompt-end request.
|
|
644
|
+
* @details Resolves the configured `notify-pushover-title` template with raw runtime placeholder substitution so the pushed title remains configurable and deterministic. Runtime is O(n) in template length. No external state is mutated.
|
|
645
|
+
* @param[in] config {PiNotifyConfigFields} Effective notification configuration.
|
|
646
|
+
* @param[in] outcome {PiNotifyOutcome} Classified prompt-end outcome.
|
|
647
|
+
* @param[in] request {PiNotifyEventRequest} Prompt-end request metadata.
|
|
648
|
+
* @return {string} Pushover title string.
|
|
649
|
+
* @satisfies REQ-185, REQ-186, REQ-187, REQ-199
|
|
650
|
+
*/
|
|
651
|
+
function buildPiNotifyPushoverTitle(
|
|
652
|
+
config: PiNotifyConfigFields,
|
|
653
|
+
outcome: PiNotifyOutcome,
|
|
654
|
+
request: PiNotifyEventRequest,
|
|
655
|
+
): string {
|
|
656
|
+
return substitutePiNotifyTextTemplate(config["notify-pushover-title"], request, outcome);
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
/**
|
|
660
|
+
* @brief Builds the Pushover message body for one prompt-end request.
|
|
661
|
+
* @details Resolves the configured `notify-pushover-text` template with raw runtime placeholder substitution so the pushed text remains configurable and deterministic. Runtime is O(n) in template length. No external state is mutated.
|
|
662
|
+
* @param[in] config {PiNotifyConfigFields} Effective notification configuration.
|
|
663
|
+
* @param[in] outcome {PiNotifyOutcome} Classified prompt-end outcome.
|
|
664
|
+
* @param[in] request {PiNotifyEventRequest} Prompt-end request metadata.
|
|
665
|
+
* @return {string} Pushover message body.
|
|
666
|
+
* @satisfies REQ-185, REQ-186, REQ-187, REQ-199
|
|
667
|
+
*/
|
|
668
|
+
function buildPiNotifyPushoverBody(
|
|
669
|
+
config: PiNotifyConfigFields,
|
|
670
|
+
outcome: PiNotifyOutcome,
|
|
671
|
+
request: PiNotifyEventRequest,
|
|
672
|
+
): string {
|
|
673
|
+
return substitutePiNotifyTextTemplate(config["notify-pushover-text"], request, outcome);
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
/**
|
|
677
|
+
* @brief Builds the Pushover API payload for one prompt-end request.
|
|
678
|
+
* @details Encodes the configured token, user key, substituted title, priority, and substituted text as `application/x-www-form-urlencoded` fields accepted by the Pushover Message API. Runtime is O(n) in payload size. No external state is mutated.
|
|
453
679
|
* @param[in] config {PiNotifyConfigFields} Effective notification configuration.
|
|
454
|
-
* @param[in]
|
|
680
|
+
* @param[in] outcome {PiNotifyOutcome} Classified prompt-end outcome.
|
|
681
|
+
* @param[in] request {PiNotifyEventRequest} Prompt-end request metadata.
|
|
455
682
|
* @return {URLSearchParams} Encoded Pushover request payload.
|
|
456
|
-
* @satisfies REQ-167, REQ-
|
|
683
|
+
* @satisfies REQ-167, REQ-172, REQ-185, REQ-186, REQ-199
|
|
457
684
|
*/
|
|
458
685
|
function buildPiNotifyPushoverPayload(
|
|
459
686
|
config: PiNotifyConfigFields,
|
|
460
|
-
|
|
687
|
+
outcome: PiNotifyOutcome,
|
|
688
|
+
request: PiNotifyEventRequest,
|
|
461
689
|
): URLSearchParams {
|
|
462
690
|
return new URLSearchParams({
|
|
463
691
|
token: config["notify-pushover-api-token"],
|
|
464
692
|
user: config["notify-pushover-user-key"],
|
|
465
|
-
title: buildPiNotifyPushoverTitle(request),
|
|
693
|
+
title: buildPiNotifyPushoverTitle(config, outcome, request),
|
|
466
694
|
priority: String(config["notify-pushover-priority"]),
|
|
467
|
-
message: buildPiNotifyPushoverBody(request),
|
|
695
|
+
message: buildPiNotifyPushoverBody(config, outcome, request),
|
|
468
696
|
});
|
|
469
697
|
}
|
|
470
698
|
|
|
@@ -472,16 +700,18 @@ function buildPiNotifyPushoverPayload(
|
|
|
472
700
|
* @brief Dispatches one native HTTPS request to the Pushover Message API.
|
|
473
701
|
* @details Serializes the request body as URL-encoded form data, posts it to `https://api.pushover.net/1/messages.json`, drains the response, and ignores transport failures so prompt-end handling remains non-blocking. Runtime is dominated by outbound I/O. Side effects include one HTTPS request.
|
|
474
702
|
* @param[in] config {PiNotifyConfigFields} Effective notification configuration.
|
|
475
|
-
* @param[in]
|
|
703
|
+
* @param[in] outcome {PiNotifyOutcome} Classified prompt-end outcome.
|
|
704
|
+
* @param[in] request {PiNotifyEventRequest} Prompt-end request metadata.
|
|
476
705
|
* @return {void} No return value.
|
|
477
|
-
* @satisfies REQ-167, REQ-
|
|
706
|
+
* @satisfies REQ-167, REQ-199
|
|
478
707
|
*/
|
|
479
708
|
function runPiNotifyPushoverRequest(
|
|
480
709
|
config: PiNotifyConfigFields,
|
|
481
|
-
|
|
710
|
+
outcome: PiNotifyOutcome,
|
|
711
|
+
request: PiNotifyEventRequest,
|
|
482
712
|
): void {
|
|
483
713
|
const url = new URL("https://api.pushover.net/1/messages.json");
|
|
484
|
-
const body = buildPiNotifyPushoverPayload(config, request).toString();
|
|
714
|
+
const body = buildPiNotifyPushoverPayload(config, outcome, request).toString();
|
|
485
715
|
const httpRequest = piNotifyHttpsRequest(url, {
|
|
486
716
|
method: "POST",
|
|
487
717
|
headers: {
|
|
@@ -507,111 +737,41 @@ export function setPiNotifyHttpsRequestForTests(requestImpl: typeof https.reques
|
|
|
507
737
|
}
|
|
508
738
|
|
|
509
739
|
/**
|
|
510
|
-
* @brief
|
|
511
|
-
* @details
|
|
512
|
-
* @param[in]
|
|
513
|
-
* @return {string} Shell-quoted installation path fragment.
|
|
514
|
-
*/
|
|
515
|
-
function quotePiNotifyInstallPath(installationPath: string): string {
|
|
516
|
-
if (process.platform === "win32") {
|
|
517
|
-
return `"${installationPath.replace(/"/g, '""')}"`;
|
|
518
|
-
}
|
|
519
|
-
return `'${installationPath.replace(/'/g, `'\\''`)}'`;
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
/**
|
|
523
|
-
* @brief Substitutes `%%INSTALLATION_PATH%%` inside one sound command.
|
|
524
|
-
* @details Replaces every `%%INSTALLATION_PATH%%` token with a shell-quoted runtime installation path so bundled sound assets can be addressed safely from external commands. Runtime is O(n) in command length. No external state is mutated.
|
|
525
|
-
* @param[in] command {string} Raw configured sound command.
|
|
526
|
-
* @param[in] installationPath {string} Absolute extension installation path.
|
|
527
|
-
* @return {string} Runtime-ready command string.
|
|
528
|
-
* @satisfies REQ-133
|
|
529
|
-
*/
|
|
530
|
-
export function substitutePiNotifyInstallPath(command: string, installationPath: string): string {
|
|
531
|
-
return command.replaceAll("%%INSTALLATION_PATH%%", quotePiNotifyInstallPath(installationPath));
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
/**
|
|
535
|
-
* @brief Resolves the configured command for one non-`none` sound level.
|
|
536
|
-
* @details Selects the matching persisted command string from config without performing runtime substitution or shell execution. Runtime is O(1). No external state is mutated.
|
|
537
|
-
* @param[in] config {PiNotifyConfigFields} Effective notification configuration.
|
|
538
|
-
* @param[in] soundLevel {Exclude<PiNotifySoundLevel, "none">} Non-disabled sound level.
|
|
539
|
-
* @return {string} Configured command string for the requested level.
|
|
540
|
-
*/
|
|
541
|
-
function resolvePiNotifySoundCommand(
|
|
542
|
-
config: PiNotifyConfigFields,
|
|
543
|
-
soundLevel: Exclude<PiNotifySoundLevel, "none">,
|
|
544
|
-
): string {
|
|
545
|
-
switch (soundLevel) {
|
|
546
|
-
case "low":
|
|
547
|
-
return config.PI_NOTIFY_SOUND_LOW_CMD;
|
|
548
|
-
case "mid":
|
|
549
|
-
return config.PI_NOTIFY_SOUND_MID_CMD;
|
|
550
|
-
case "high":
|
|
551
|
-
return config.PI_NOTIFY_SOUND_HIGH_CMD;
|
|
552
|
-
}
|
|
553
|
-
}
|
|
554
|
-
|
|
555
|
-
/**
|
|
556
|
-
* @brief Executes the configured successful-run sound command on an external shell.
|
|
557
|
-
* @details Resolves the runtime installation path, substitutes `%%INSTALLATION_PATH%%`, spawns the configured shell command without waiting, and ignores transport failures so prompt-end handling remains non-blocking. Runtime is dominated by process spawn. Side effects include detached child-process execution.
|
|
558
|
-
* @param[in] config {PiNotifyConfigFields} Effective notification configuration.
|
|
559
|
-
* @param[in] soundLevel {Exclude<PiNotifySoundLevel, "none">} Requested non-disabled sound level.
|
|
740
|
+
* @brief Replaces the shell-spawn function used for notify and sound commands in deterministic tests.
|
|
741
|
+
* @details Accepts a drop-in `node:child_process.spawn` replacement and restores the native implementation when `undefined` is supplied. Runtime is O(1). Side effect: mutates the module-local shell transport hook.
|
|
742
|
+
* @param[in] spawnImpl {PiNotifySpawn | undefined} Replacement shell-spawn function.
|
|
560
743
|
* @return {void} No return value.
|
|
561
|
-
* @satisfies REQ-132, REQ-133
|
|
562
744
|
*/
|
|
563
|
-
export function
|
|
564
|
-
|
|
565
|
-
soundLevel: Exclude<PiNotifySoundLevel, "none">,
|
|
566
|
-
): void {
|
|
567
|
-
const rawCommand = resolvePiNotifySoundCommand(config, soundLevel);
|
|
568
|
-
const command = substitutePiNotifyInstallPath(rawCommand, getInstallationPath());
|
|
569
|
-
const shell = process.platform === "win32" ? "cmd.exe" : (process.env.SHELL ?? "sh");
|
|
570
|
-
const shellArgs = process.platform === "win32"
|
|
571
|
-
? ["/d", "/s", "/c", command]
|
|
572
|
-
: ["-lc", command];
|
|
573
|
-
const child = spawn(shell, shellArgs, {
|
|
574
|
-
detached: true,
|
|
575
|
-
stdio: "ignore",
|
|
576
|
-
});
|
|
577
|
-
child.once("error", () => undefined);
|
|
578
|
-
child.unref();
|
|
745
|
+
export function setPiNotifySpawnForTests(spawnImpl: PiNotifySpawn | undefined): void {
|
|
746
|
+
piNotifySpawn = spawnImpl ?? spawn;
|
|
579
747
|
}
|
|
580
748
|
|
|
581
749
|
/**
|
|
582
|
-
* @brief Dispatches prompt-end
|
|
583
|
-
* @details Classifies the terminal outcome,
|
|
750
|
+
* @brief Dispatches prompt-end notify, sound, and Pushover effects for one agent-end payload.
|
|
751
|
+
* @details Classifies the terminal outcome, executes `PI_NOTIFY_CMD` when command-notify prerequisites are satisfied, executes the configured sound command when sound prerequisites are satisfied, and dispatches the native Pushover request when Pushover prerequisites are satisfied. Runtime is O(m + c + b) in message count, command length, and Pushover payload size. Side effects include child-process spawning and outbound HTTPS requests.
|
|
584
752
|
* @param[in] config {PiNotifyConfigFields} Effective notification configuration.
|
|
585
753
|
* @param[in] event {Pick<AgentEndEvent, "messages">} Agent-end payload subset.
|
|
586
|
-
* @param[in]
|
|
754
|
+
* @param[in] request {PiNotifyEventRequest | undefined} Optional prompt-end request metadata used for command-notify and Pushover substitution.
|
|
587
755
|
* @return {void} No return value.
|
|
588
|
-
* @satisfies REQ-
|
|
756
|
+
* @satisfies REQ-131, REQ-132, REQ-133, REQ-166, REQ-167, REQ-168, REQ-169, REQ-172, REQ-176, REQ-178, REQ-184, REQ-185, REQ-186, REQ-187, REQ-199
|
|
589
757
|
*/
|
|
590
758
|
export function runPiNotifyEffects(
|
|
591
759
|
config: PiNotifyConfigFields,
|
|
592
760
|
event: Pick<AgentEndEvent, "messages">,
|
|
593
|
-
|
|
761
|
+
request?: PiNotifyEventRequest,
|
|
594
762
|
): void {
|
|
595
763
|
const outcome = classifyPiNotifyOutcome(event);
|
|
596
|
-
|
|
597
|
-
(outcome
|
|
598
|
-
|| (outcome === "esc" && config["notify-beep-on-esc"])
|
|
599
|
-
|| (outcome === "err" && config["notify-beep-on-error"])
|
|
600
|
-
);
|
|
601
|
-
if (shouldNotify) {
|
|
602
|
-
notifyPiTerminal(buildPiNotifyTitle(), buildPiNotifyBody(outcome));
|
|
603
|
-
}
|
|
604
|
-
if (outcome !== "end") {
|
|
605
|
-
return;
|
|
764
|
+
if (shouldRunPiNotifyCommand(config, outcome, request)) {
|
|
765
|
+
runPiNotifyCommand(config, outcome, request as PiNotifyEventRequest);
|
|
606
766
|
}
|
|
607
|
-
if (config
|
|
767
|
+
if (shouldRunPiNotifySound(config, outcome)) {
|
|
608
768
|
runPiNotifySoundCommand(
|
|
609
769
|
config,
|
|
610
770
|
config["notify-sound"] as Exclude<PiNotifySoundLevel, "none">,
|
|
611
771
|
);
|
|
612
772
|
}
|
|
613
|
-
if (!shouldRunPiNotifyPushover(config,
|
|
773
|
+
if (!shouldRunPiNotifyPushover(config, outcome, request)) {
|
|
614
774
|
return;
|
|
615
775
|
}
|
|
616
|
-
runPiNotifyPushoverRequest(config,
|
|
776
|
+
runPiNotifyPushoverRequest(config, outcome, request as PiNotifyEventRequest);
|
|
617
777
|
}
|