@fermindi/pwn-cli 0.1.0 → 0.2.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 (46) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +265 -251
  3. package/cli/batch.js +333 -333
  4. package/cli/codespaces.js +303 -303
  5. package/cli/index.js +98 -91
  6. package/cli/inject.js +78 -53
  7. package/cli/knowledge.js +531 -531
  8. package/cli/migrate.js +466 -0
  9. package/cli/notify.js +135 -135
  10. package/cli/patterns.js +665 -665
  11. package/cli/status.js +91 -91
  12. package/cli/validate.js +61 -61
  13. package/package.json +70 -70
  14. package/src/core/inject.js +208 -128
  15. package/src/core/state.js +91 -91
  16. package/src/core/validate.js +202 -202
  17. package/src/core/workspace.js +176 -176
  18. package/src/index.js +20 -20
  19. package/src/knowledge/gc.js +308 -308
  20. package/src/knowledge/lifecycle.js +401 -401
  21. package/src/knowledge/promote.js +364 -364
  22. package/src/knowledge/references.js +342 -342
  23. package/src/patterns/matcher.js +218 -218
  24. package/src/patterns/registry.js +375 -375
  25. package/src/patterns/triggers.js +423 -423
  26. package/src/services/batch-service.js +849 -849
  27. package/src/services/notification-service.js +342 -342
  28. package/templates/codespaces/devcontainer.json +52 -52
  29. package/templates/codespaces/setup.sh +70 -70
  30. package/templates/workspace/.ai/README.md +164 -164
  31. package/templates/workspace/.ai/agents/README.md +204 -204
  32. package/templates/workspace/.ai/agents/claude.md +625 -625
  33. package/templates/workspace/.ai/config/README.md +79 -79
  34. package/templates/workspace/.ai/config/notifications.template.json +20 -20
  35. package/templates/workspace/.ai/memory/deadends.md +79 -79
  36. package/templates/workspace/.ai/memory/decisions.md +58 -58
  37. package/templates/workspace/.ai/memory/patterns.md +65 -65
  38. package/templates/workspace/.ai/patterns/backend/README.md +126 -126
  39. package/templates/workspace/.ai/patterns/frontend/README.md +103 -103
  40. package/templates/workspace/.ai/patterns/index.md +256 -256
  41. package/templates/workspace/.ai/patterns/triggers.json +1087 -1087
  42. package/templates/workspace/.ai/patterns/universal/README.md +141 -141
  43. package/templates/workspace/.ai/state.template.json +8 -8
  44. package/templates/workspace/.ai/tasks/active.md +77 -77
  45. package/templates/workspace/.ai/tasks/backlog.md +95 -95
  46. package/templates/workspace/.ai/workflows/batch-task.md +356 -356
@@ -1,342 +1,342 @@
1
- /**
2
- * PWN Notification Service
3
- *
4
- * Multi-channel notification system for task completion alerts.
5
- * Supports: ntfy.sh, desktop notifications, and extensible channels.
6
- */
7
-
8
- import { existsSync, readFileSync } from 'fs';
9
- import { join } from 'path';
10
-
11
- /**
12
- * @typedef {Object} NotificationConfig
13
- * @property {boolean} enabled - Global enable/disable
14
- * @property {string} defaultChannel - Default channel to use
15
- * @property {Object} channels - Channel-specific configurations
16
- */
17
-
18
- /**
19
- * @typedef {Object} NotificationPayload
20
- * @property {string} title - Notification title
21
- * @property {string} message - Notification body
22
- * @property {string} [priority] - Priority level (low, default, high, urgent)
23
- * @property {string[]} [tags] - Tags/emojis for the notification
24
- * @property {string} [url] - Click action URL
25
- */
26
-
27
- /**
28
- * Default configuration
29
- */
30
- const DEFAULT_CONFIG = {
31
- enabled: true,
32
- defaultChannel: 'desktop',
33
- channels: {
34
- ntfy: {
35
- enabled: false,
36
- server: 'https://ntfy.sh',
37
- topic: '',
38
- priority: 'default'
39
- },
40
- desktop: {
41
- enabled: true
42
- },
43
- pushover: {
44
- enabled: false,
45
- userKey: '',
46
- apiToken: ''
47
- }
48
- }
49
- };
50
-
51
- /**
52
- * Load notification configuration from workspace
53
- * @param {string} cwd - Working directory
54
- * @returns {NotificationConfig}
55
- */
56
- export function loadConfig(cwd = process.cwd()) {
57
- const configPath = join(cwd, '.ai', 'config', 'notifications.json');
58
-
59
- if (!existsSync(configPath)) {
60
- return DEFAULT_CONFIG;
61
- }
62
-
63
- try {
64
- const content = readFileSync(configPath, 'utf8');
65
- const userConfig = JSON.parse(content);
66
-
67
- // Merge with defaults
68
- return {
69
- ...DEFAULT_CONFIG,
70
- ...userConfig,
71
- channels: {
72
- ...DEFAULT_CONFIG.channels,
73
- ...userConfig.channels
74
- }
75
- };
76
- } catch {
77
- return DEFAULT_CONFIG;
78
- }
79
- }
80
-
81
- /**
82
- * Send notification through configured channels
83
- * @param {NotificationPayload} payload - Notification data
84
- * @param {Object} options - Send options
85
- * @param {string} [options.channel] - Specific channel to use
86
- * @param {string} [options.cwd] - Working directory
87
- * @returns {Promise<{success: boolean, channel: string, error?: string}>}
88
- */
89
- export async function send(payload, options = {}) {
90
- const { channel: specificChannel, cwd = process.cwd() } = options;
91
- const config = loadConfig(cwd);
92
-
93
- if (!config.enabled) {
94
- return { success: false, channel: 'none', error: 'Notifications disabled' };
95
- }
96
-
97
- const channelName = specificChannel || config.defaultChannel;
98
- const channelConfig = config.channels[channelName];
99
-
100
- if (!channelConfig) {
101
- return { success: false, channel: channelName, error: `Unknown channel: ${channelName}` };
102
- }
103
-
104
- if (!channelConfig.enabled) {
105
- return { success: false, channel: channelName, error: `Channel ${channelName} is disabled` };
106
- }
107
-
108
- try {
109
- switch (channelName) {
110
- case 'ntfy':
111
- return await sendNtfy(payload, channelConfig);
112
- case 'desktop':
113
- return await sendDesktop(payload, channelConfig);
114
- case 'pushover':
115
- return await sendPushover(payload, channelConfig);
116
- default:
117
- return { success: false, channel: channelName, error: `No handler for channel: ${channelName}` };
118
- }
119
- } catch (error) {
120
- return { success: false, channel: channelName, error: error.message };
121
- }
122
- }
123
-
124
- /**
125
- * Send notification via ntfy.sh
126
- * @param {NotificationPayload} payload
127
- * @param {Object} config
128
- */
129
- async function sendNtfy(payload, config) {
130
- const { server = 'https://ntfy.sh', topic, priority = 'default' } = config;
131
-
132
- if (!topic) {
133
- return { success: false, channel: 'ntfy', error: 'ntfy topic not configured' };
134
- }
135
-
136
- const url = `${server}/${topic}`;
137
-
138
- const headers = {
139
- 'Title': payload.title,
140
- 'Priority': payload.priority || priority,
141
- };
142
-
143
- if (payload.tags && payload.tags.length > 0) {
144
- headers['Tags'] = payload.tags.join(',');
145
- }
146
-
147
- if (payload.url) {
148
- headers['Click'] = payload.url;
149
- }
150
-
151
- const response = await fetch(url, {
152
- method: 'POST',
153
- headers,
154
- body: payload.message
155
- });
156
-
157
- if (!response.ok) {
158
- return { success: false, channel: 'ntfy', error: `HTTP ${response.status}` };
159
- }
160
-
161
- return { success: true, channel: 'ntfy' };
162
- }
163
-
164
- /**
165
- * Send desktop notification
166
- * Uses native OS notifications via child process
167
- * @param {NotificationPayload} payload
168
- * @param {Object} config
169
- */
170
- async function sendDesktop(payload, config) {
171
- const { title, message } = payload;
172
-
173
- // Use PowerShell on Windows for native toast notifications
174
- if (process.platform === 'win32') {
175
- const { exec } = await import('child_process');
176
- const { promisify } = await import('util');
177
- const execAsync = promisify(exec);
178
-
179
- // Escape quotes in message
180
- const escapedTitle = title.replace(/"/g, '`"');
181
- const escapedMessage = message.replace(/"/g, '`"');
182
-
183
- const script = `
184
- [Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] | Out-Null
185
- [Windows.Data.Xml.Dom.XmlDocument, Windows.Data.Xml.Dom.XmlDocument, ContentType = WindowsRuntime] | Out-Null
186
- $template = @"
187
- <toast>
188
- <visual>
189
- <binding template="ToastText02">
190
- <text id="1">${escapedTitle}</text>
191
- <text id="2">${escapedMessage}</text>
192
- </binding>
193
- </visual>
194
- </toast>
195
- "@
196
- $xml = New-Object Windows.Data.Xml.Dom.XmlDocument
197
- $xml.LoadXml($template)
198
- $toast = [Windows.UI.Notifications.ToastNotification]::new($xml)
199
- [Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier("PWN").Show($toast)
200
- `;
201
-
202
- try {
203
- await execAsync(`powershell -Command "${script.replace(/\n/g, ' ')}"`, {
204
- windowsHide: true
205
- });
206
- return { success: true, channel: 'desktop' };
207
- } catch (error) {
208
- // Fallback to simple msg command
209
- try {
210
- await execAsync(`msg %username% "${escapedTitle}: ${escapedMessage}"`, {
211
- windowsHide: true
212
- });
213
- return { success: true, channel: 'desktop' };
214
- } catch {
215
- return { success: false, channel: 'desktop', error: 'Desktop notifications not available' };
216
- }
217
- }
218
- }
219
-
220
- // macOS
221
- if (process.platform === 'darwin') {
222
- const { exec } = await import('child_process');
223
- const { promisify } = await import('util');
224
- const execAsync = promisify(exec);
225
-
226
- const escapedTitle = title.replace(/"/g, '\\"');
227
- const escapedMessage = message.replace(/"/g, '\\"');
228
-
229
- try {
230
- await execAsync(`osascript -e 'display notification "${escapedMessage}" with title "${escapedTitle}"'`);
231
- return { success: true, channel: 'desktop' };
232
- } catch (error) {
233
- return { success: false, channel: 'desktop', error: error.message };
234
- }
235
- }
236
-
237
- // Linux (notify-send)
238
- if (process.platform === 'linux') {
239
- const { exec } = await import('child_process');
240
- const { promisify } = await import('util');
241
- const execAsync = promisify(exec);
242
-
243
- try {
244
- await execAsync(`notify-send "${title}" "${message}"`);
245
- return { success: true, channel: 'desktop' };
246
- } catch (error) {
247
- return { success: false, channel: 'desktop', error: 'notify-send not available' };
248
- }
249
- }
250
-
251
- return { success: false, channel: 'desktop', error: `Unsupported platform: ${process.platform}` };
252
- }
253
-
254
- /**
255
- * Send notification via Pushover
256
- * @param {NotificationPayload} payload
257
- * @param {Object} config
258
- */
259
- async function sendPushover(payload, config) {
260
- const { userKey, apiToken } = config;
261
-
262
- if (!userKey || !apiToken) {
263
- return { success: false, channel: 'pushover', error: 'Pushover credentials not configured' };
264
- }
265
-
266
- const priorityMap = {
267
- 'low': -1,
268
- 'default': 0,
269
- 'high': 1,
270
- 'urgent': 2
271
- };
272
-
273
- const body = new URLSearchParams({
274
- token: apiToken,
275
- user: userKey,
276
- title: payload.title,
277
- message: payload.message,
278
- priority: priorityMap[payload.priority || 'default'] || 0
279
- });
280
-
281
- if (payload.url) {
282
- body.append('url', payload.url);
283
- }
284
-
285
- const response = await fetch('https://api.pushover.net/1/messages.json', {
286
- method: 'POST',
287
- body
288
- });
289
-
290
- if (!response.ok) {
291
- const data = await response.json().catch(() => ({}));
292
- return { success: false, channel: 'pushover', error: data.errors?.join(', ') || `HTTP ${response.status}` };
293
- }
294
-
295
- return { success: true, channel: 'pushover' };
296
- }
297
-
298
- /**
299
- * Test notification configuration
300
- * @param {string} channel - Channel to test
301
- * @param {string} cwd - Working directory
302
- * @returns {Promise<{success: boolean, channel: string, error?: string}>}
303
- */
304
- export async function test(channel, cwd = process.cwd()) {
305
- return send({
306
- title: 'PWN Test Notification',
307
- message: 'If you see this, notifications are working!',
308
- tags: ['white_check_mark', 'robot']
309
- }, { channel, cwd });
310
- }
311
-
312
- /**
313
- * Send task completion notification
314
- * @param {string} taskId - Task identifier
315
- * @param {string} taskName - Task description
316
- * @param {Object} options - Additional options
317
- */
318
- export async function notifyTaskComplete(taskId, taskName, options = {}) {
319
- return send({
320
- title: 'Task Completed',
321
- message: `${taskId}: ${taskName}`,
322
- tags: ['white_check_mark'],
323
- priority: 'default',
324
- ...options
325
- }, options);
326
- }
327
-
328
- /**
329
- * Send error notification
330
- * @param {string} title - Error title
331
- * @param {string} message - Error details
332
- * @param {Object} options - Additional options
333
- */
334
- export async function notifyError(title, message, options = {}) {
335
- return send({
336
- title,
337
- message,
338
- tags: ['x', 'warning'],
339
- priority: 'high',
340
- ...options
341
- }, options);
342
- }
1
+ /**
2
+ * PWN Notification Service
3
+ *
4
+ * Multi-channel notification system for task completion alerts.
5
+ * Supports: ntfy.sh, desktop notifications, and extensible channels.
6
+ */
7
+
8
+ import { existsSync, readFileSync } from 'fs';
9
+ import { join } from 'path';
10
+
11
+ /**
12
+ * @typedef {Object} NotificationConfig
13
+ * @property {boolean} enabled - Global enable/disable
14
+ * @property {string} defaultChannel - Default channel to use
15
+ * @property {Object} channels - Channel-specific configurations
16
+ */
17
+
18
+ /**
19
+ * @typedef {Object} NotificationPayload
20
+ * @property {string} title - Notification title
21
+ * @property {string} message - Notification body
22
+ * @property {string} [priority] - Priority level (low, default, high, urgent)
23
+ * @property {string[]} [tags] - Tags/emojis for the notification
24
+ * @property {string} [url] - Click action URL
25
+ */
26
+
27
+ /**
28
+ * Default configuration
29
+ */
30
+ const DEFAULT_CONFIG = {
31
+ enabled: true,
32
+ defaultChannel: 'desktop',
33
+ channels: {
34
+ ntfy: {
35
+ enabled: false,
36
+ server: 'https://ntfy.sh',
37
+ topic: '',
38
+ priority: 'default'
39
+ },
40
+ desktop: {
41
+ enabled: true
42
+ },
43
+ pushover: {
44
+ enabled: false,
45
+ userKey: '',
46
+ apiToken: ''
47
+ }
48
+ }
49
+ };
50
+
51
+ /**
52
+ * Load notification configuration from workspace
53
+ * @param {string} cwd - Working directory
54
+ * @returns {NotificationConfig}
55
+ */
56
+ export function loadConfig(cwd = process.cwd()) {
57
+ const configPath = join(cwd, '.ai', 'config', 'notifications.json');
58
+
59
+ if (!existsSync(configPath)) {
60
+ return DEFAULT_CONFIG;
61
+ }
62
+
63
+ try {
64
+ const content = readFileSync(configPath, 'utf8');
65
+ const userConfig = JSON.parse(content);
66
+
67
+ // Merge with defaults
68
+ return {
69
+ ...DEFAULT_CONFIG,
70
+ ...userConfig,
71
+ channels: {
72
+ ...DEFAULT_CONFIG.channels,
73
+ ...userConfig.channels
74
+ }
75
+ };
76
+ } catch {
77
+ return DEFAULT_CONFIG;
78
+ }
79
+ }
80
+
81
+ /**
82
+ * Send notification through configured channels
83
+ * @param {NotificationPayload} payload - Notification data
84
+ * @param {Object} options - Send options
85
+ * @param {string} [options.channel] - Specific channel to use
86
+ * @param {string} [options.cwd] - Working directory
87
+ * @returns {Promise<{success: boolean, channel: string, error?: string}>}
88
+ */
89
+ export async function send(payload, options = {}) {
90
+ const { channel: specificChannel, cwd = process.cwd() } = options;
91
+ const config = loadConfig(cwd);
92
+
93
+ if (!config.enabled) {
94
+ return { success: false, channel: 'none', error: 'Notifications disabled' };
95
+ }
96
+
97
+ const channelName = specificChannel || config.defaultChannel;
98
+ const channelConfig = config.channels[channelName];
99
+
100
+ if (!channelConfig) {
101
+ return { success: false, channel: channelName, error: `Unknown channel: ${channelName}` };
102
+ }
103
+
104
+ if (!channelConfig.enabled) {
105
+ return { success: false, channel: channelName, error: `Channel ${channelName} is disabled` };
106
+ }
107
+
108
+ try {
109
+ switch (channelName) {
110
+ case 'ntfy':
111
+ return await sendNtfy(payload, channelConfig);
112
+ case 'desktop':
113
+ return await sendDesktop(payload, channelConfig);
114
+ case 'pushover':
115
+ return await sendPushover(payload, channelConfig);
116
+ default:
117
+ return { success: false, channel: channelName, error: `No handler for channel: ${channelName}` };
118
+ }
119
+ } catch (error) {
120
+ return { success: false, channel: channelName, error: error.message };
121
+ }
122
+ }
123
+
124
+ /**
125
+ * Send notification via ntfy.sh
126
+ * @param {NotificationPayload} payload
127
+ * @param {Object} config
128
+ */
129
+ async function sendNtfy(payload, config) {
130
+ const { server = 'https://ntfy.sh', topic, priority = 'default' } = config;
131
+
132
+ if (!topic) {
133
+ return { success: false, channel: 'ntfy', error: 'ntfy topic not configured' };
134
+ }
135
+
136
+ const url = `${server}/${topic}`;
137
+
138
+ const headers = {
139
+ 'Title': payload.title,
140
+ 'Priority': payload.priority || priority,
141
+ };
142
+
143
+ if (payload.tags && payload.tags.length > 0) {
144
+ headers['Tags'] = payload.tags.join(',');
145
+ }
146
+
147
+ if (payload.url) {
148
+ headers['Click'] = payload.url;
149
+ }
150
+
151
+ const response = await fetch(url, {
152
+ method: 'POST',
153
+ headers,
154
+ body: payload.message
155
+ });
156
+
157
+ if (!response.ok) {
158
+ return { success: false, channel: 'ntfy', error: `HTTP ${response.status}` };
159
+ }
160
+
161
+ return { success: true, channel: 'ntfy' };
162
+ }
163
+
164
+ /**
165
+ * Send desktop notification
166
+ * Uses native OS notifications via child process
167
+ * @param {NotificationPayload} payload
168
+ * @param {Object} config
169
+ */
170
+ async function sendDesktop(payload, config) {
171
+ const { title, message } = payload;
172
+
173
+ // Use PowerShell on Windows for native toast notifications
174
+ if (process.platform === 'win32') {
175
+ const { exec } = await import('child_process');
176
+ const { promisify } = await import('util');
177
+ const execAsync = promisify(exec);
178
+
179
+ // Escape quotes in message
180
+ const escapedTitle = title.replace(/"/g, '`"');
181
+ const escapedMessage = message.replace(/"/g, '`"');
182
+
183
+ const script = `
184
+ [Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] | Out-Null
185
+ [Windows.Data.Xml.Dom.XmlDocument, Windows.Data.Xml.Dom.XmlDocument, ContentType = WindowsRuntime] | Out-Null
186
+ $template = @"
187
+ <toast>
188
+ <visual>
189
+ <binding template="ToastText02">
190
+ <text id="1">${escapedTitle}</text>
191
+ <text id="2">${escapedMessage}</text>
192
+ </binding>
193
+ </visual>
194
+ </toast>
195
+ "@
196
+ $xml = New-Object Windows.Data.Xml.Dom.XmlDocument
197
+ $xml.LoadXml($template)
198
+ $toast = [Windows.UI.Notifications.ToastNotification]::new($xml)
199
+ [Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier("PWN").Show($toast)
200
+ `;
201
+
202
+ try {
203
+ await execAsync(`powershell -Command "${script.replace(/\n/g, ' ')}"`, {
204
+ windowsHide: true
205
+ });
206
+ return { success: true, channel: 'desktop' };
207
+ } catch (error) {
208
+ // Fallback to simple msg command
209
+ try {
210
+ await execAsync(`msg %username% "${escapedTitle}: ${escapedMessage}"`, {
211
+ windowsHide: true
212
+ });
213
+ return { success: true, channel: 'desktop' };
214
+ } catch {
215
+ return { success: false, channel: 'desktop', error: 'Desktop notifications not available' };
216
+ }
217
+ }
218
+ }
219
+
220
+ // macOS
221
+ if (process.platform === 'darwin') {
222
+ const { exec } = await import('child_process');
223
+ const { promisify } = await import('util');
224
+ const execAsync = promisify(exec);
225
+
226
+ const escapedTitle = title.replace(/"/g, '\\"');
227
+ const escapedMessage = message.replace(/"/g, '\\"');
228
+
229
+ try {
230
+ await execAsync(`osascript -e 'display notification "${escapedMessage}" with title "${escapedTitle}"'`);
231
+ return { success: true, channel: 'desktop' };
232
+ } catch (error) {
233
+ return { success: false, channel: 'desktop', error: error.message };
234
+ }
235
+ }
236
+
237
+ // Linux (notify-send)
238
+ if (process.platform === 'linux') {
239
+ const { exec } = await import('child_process');
240
+ const { promisify } = await import('util');
241
+ const execAsync = promisify(exec);
242
+
243
+ try {
244
+ await execAsync(`notify-send "${title}" "${message}"`);
245
+ return { success: true, channel: 'desktop' };
246
+ } catch (error) {
247
+ return { success: false, channel: 'desktop', error: 'notify-send not available' };
248
+ }
249
+ }
250
+
251
+ return { success: false, channel: 'desktop', error: `Unsupported platform: ${process.platform}` };
252
+ }
253
+
254
+ /**
255
+ * Send notification via Pushover
256
+ * @param {NotificationPayload} payload
257
+ * @param {Object} config
258
+ */
259
+ async function sendPushover(payload, config) {
260
+ const { userKey, apiToken } = config;
261
+
262
+ if (!userKey || !apiToken) {
263
+ return { success: false, channel: 'pushover', error: 'Pushover credentials not configured' };
264
+ }
265
+
266
+ const priorityMap = {
267
+ 'low': -1,
268
+ 'default': 0,
269
+ 'high': 1,
270
+ 'urgent': 2
271
+ };
272
+
273
+ const body = new URLSearchParams({
274
+ token: apiToken,
275
+ user: userKey,
276
+ title: payload.title,
277
+ message: payload.message,
278
+ priority: priorityMap[payload.priority || 'default'] || 0
279
+ });
280
+
281
+ if (payload.url) {
282
+ body.append('url', payload.url);
283
+ }
284
+
285
+ const response = await fetch('https://api.pushover.net/1/messages.json', {
286
+ method: 'POST',
287
+ body
288
+ });
289
+
290
+ if (!response.ok) {
291
+ const data = await response.json().catch(() => ({}));
292
+ return { success: false, channel: 'pushover', error: data.errors?.join(', ') || `HTTP ${response.status}` };
293
+ }
294
+
295
+ return { success: true, channel: 'pushover' };
296
+ }
297
+
298
+ /**
299
+ * Test notification configuration
300
+ * @param {string} channel - Channel to test
301
+ * @param {string} cwd - Working directory
302
+ * @returns {Promise<{success: boolean, channel: string, error?: string}>}
303
+ */
304
+ export async function test(channel, cwd = process.cwd()) {
305
+ return send({
306
+ title: 'PWN Test Notification',
307
+ message: 'If you see this, notifications are working!',
308
+ tags: ['white_check_mark', 'robot']
309
+ }, { channel, cwd });
310
+ }
311
+
312
+ /**
313
+ * Send task completion notification
314
+ * @param {string} taskId - Task identifier
315
+ * @param {string} taskName - Task description
316
+ * @param {Object} options - Additional options
317
+ */
318
+ export async function notifyTaskComplete(taskId, taskName, options = {}) {
319
+ return send({
320
+ title: 'Task Completed',
321
+ message: `${taskId}: ${taskName}`,
322
+ tags: ['white_check_mark'],
323
+ priority: 'default',
324
+ ...options
325
+ }, options);
326
+ }
327
+
328
+ /**
329
+ * Send error notification
330
+ * @param {string} title - Error title
331
+ * @param {string} message - Error details
332
+ * @param {Object} options - Additional options
333
+ */
334
+ export async function notifyError(title, message, options = {}) {
335
+ return send({
336
+ title,
337
+ message,
338
+ tags: ['x', 'warning'],
339
+ priority: 'high',
340
+ ...options
341
+ }, options);
342
+ }