@pixelbyte-software/pixcode 1.40.9 → 1.41.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.
@@ -3,13 +3,12 @@ import webPush from 'web-push';
3
3
  import { notificationPreferencesDb, pushSubscriptionsDb, sessionNamesDb } from '../database/db.js';
4
4
 
5
5
  import { notifyUser as notifyTelegramUser } from './telegram/bot.js';
6
-
7
- const KIND_TO_PREF_KEY = {
8
- action_required: 'actionRequired',
9
- stop: 'stop',
10
- error: 'error',
11
- update: 'updates'
12
- };
6
+ import {
7
+ NOTIFICATION_EVENT_TYPES,
8
+ createNotificationEventContract,
9
+ getNotificationPreferenceKey,
10
+ normalizeNotificationEvent
11
+ } from './notification-taxonomy.js';
13
12
 
14
13
  const PROVIDER_LABELS = {
15
14
  claude: 'Claude',
@@ -40,7 +39,7 @@ const cleanupOldEventKeys = () => {
40
39
 
41
40
  function shouldSendPush(preferences, event) {
42
41
  const webPushEnabled = Boolean(preferences?.channels?.webPush);
43
- const prefEventKey = KIND_TO_PREF_KEY[event.kind];
42
+ const prefEventKey = getNotificationPreferenceKey(event);
44
43
  const eventEnabled = prefEventKey ? Boolean(preferences?.events?.[prefEventKey]) : true;
45
44
 
46
45
  return webPushEnabled && eventEnabled;
@@ -48,7 +47,7 @@ function shouldSendPush(preferences, event) {
48
47
 
49
48
  function shouldSendInApp(preferences, event) {
50
49
  const inAppEnabled = preferences?.channels?.inApp !== false;
51
- const prefEventKey = KIND_TO_PREF_KEY[event.kind];
50
+ const prefEventKey = getNotificationPreferenceKey(event);
52
51
  const eventEnabled = prefEventKey ? preferences?.events?.[prefEventKey] !== false : true;
53
52
 
54
53
  return inAppEnabled && eventEnabled;
@@ -56,7 +55,7 @@ function shouldSendInApp(preferences, event) {
56
55
 
57
56
  function shouldSendTelegram(preferences, event) {
58
57
  const telegramEnabled = preferences?.channels?.telegram !== false;
59
- const prefEventKey = KIND_TO_PREF_KEY[event.kind];
58
+ const prefEventKey = getNotificationPreferenceKey(event);
60
59
  const eventEnabled = prefEventKey ? preferences?.events?.[prefEventKey] !== false : true;
61
60
 
62
61
  return telegramEnabled && eventEnabled;
@@ -75,6 +74,7 @@ function isDuplicate(event) {
75
74
  function createNotificationEvent({
76
75
  provider,
77
76
  sessionId = null,
77
+ eventType = null,
78
78
  kind = 'info',
79
79
  code = 'generic.info',
80
80
  meta = {},
@@ -82,9 +82,10 @@ function createNotificationEvent({
82
82
  dedupeKey = null,
83
83
  requiresUserAction = false
84
84
  }) {
85
- return {
85
+ return normalizeNotificationEvent({
86
86
  provider,
87
87
  sessionId,
88
+ eventType,
88
89
  kind,
89
90
  code,
90
91
  meta,
@@ -92,7 +93,7 @@ function createNotificationEvent({
92
93
  requiresUserAction,
93
94
  dedupeKey,
94
95
  createdAt: new Date().toISOString()
95
- };
96
+ });
96
97
  }
97
98
 
98
99
  function normalizeErrorMessage(error) {
@@ -138,6 +139,16 @@ function resolveSessionName(event) {
138
139
  }
139
140
 
140
141
  function buildPushBody(event) {
142
+ const EVENT_TYPE_MAP = {
143
+ [NOTIFICATION_EVENT_TYPES.CHAT_DONE]: event.meta?.stopReason || 'Chat run completed',
144
+ [NOTIFICATION_EVENT_TYPES.ORCHESTRATION_DONE]: event.meta?.stopReason || 'Orchestration completed',
145
+ [NOTIFICATION_EVENT_TYPES.APPROVAL_NEEDED]: event.meta?.toolName
146
+ ? `Action Required: Tool "${event.meta.toolName}" needs approval`
147
+ : 'Action Required: A run needs your approval',
148
+ [NOTIFICATION_EVENT_TYPES.ERROR]: event.meta?.error ? `Error: ${event.meta.error}` : 'Pixcode encountered an error',
149
+ [NOTIFICATION_EVENT_TYPES.TEST_FAILED]: event.meta?.error ? `Test Failed: ${event.meta.error}` : 'A verification command failed',
150
+ [NOTIFICATION_EVENT_TYPES.LIVE_VIEW_FAILED]: event.meta?.error ? `Live View Failed: ${event.meta.error}` : 'Live View failed'
151
+ };
141
152
  const CODE_MAP = {
142
153
  'permission.required': event.meta?.toolName
143
154
  ? `Action Required: Tool "${event.meta.toolName}" needs approval`
@@ -155,16 +166,19 @@ function buildPushBody(event) {
155
166
  };
156
167
  const providerLabel = PROVIDER_LABELS[event.provider] || 'Assistant';
157
168
  const sessionName = resolveSessionName(event);
158
- const message = CODE_MAP[event.code] || 'You have a new notification';
169
+ const message = EVENT_TYPE_MAP[event.eventType] || CODE_MAP[event.code] || 'You have a new notification';
159
170
 
160
171
  return {
161
172
  title: sessionName || 'Pixcode',
162
173
  body: `${providerLabel}: ${message}`,
163
174
  data: {
164
175
  sessionId: event.sessionId || null,
176
+ eventType: event.eventType || null,
165
177
  code: event.code,
166
178
  provider: event.provider || null,
167
179
  sessionName,
180
+ category: event.category || null,
181
+ preferenceKey: event.preferenceKey || getNotificationPreferenceKey(event),
168
182
  tag: `${event.provider || 'assistant'}:${event.sessionId || 'none'}:${event.code}`
169
183
  }
170
184
  };
@@ -172,11 +186,15 @@ function buildPushBody(event) {
172
186
 
173
187
  function buildNotificationPayload(event) {
174
188
  const pushBody = buildPushBody(event);
189
+ const contract = createNotificationEventContract(event);
175
190
  const baseId = event.dedupeKey || `${event.provider || 'system'}:${event.kind || 'info'}:${event.code || 'generic'}:${event.sessionId || 'none'}`;
176
191
  return {
177
192
  id: `${baseId}:${event.createdAt}`,
178
193
  title: pushBody.title,
179
194
  body: pushBody.body,
195
+ eventType: contract.eventType,
196
+ category: contract.category,
197
+ preferenceKey: contract.preferenceKey,
180
198
  kind: event.kind || 'info',
181
199
  code: event.code || 'generic.info',
182
200
  severity: event.severity || 'info',
@@ -188,6 +206,28 @@ function buildNotificationPayload(event) {
188
206
  };
189
207
  }
190
208
 
209
+ function inferRunStoppedEventType({ provider, stopReason }) {
210
+ const reason = typeof stopReason === 'string' ? stopReason.toLowerCase() : '';
211
+ if (provider === 'system' && reason.includes('orchestration')) {
212
+ return NOTIFICATION_EVENT_TYPES.ORCHESTRATION_DONE;
213
+ }
214
+
215
+ return NOTIFICATION_EVENT_TYPES.CHAT_DONE;
216
+ }
217
+
218
+ function inferRunFailedEventType({ provider, error }) {
219
+ const message = normalizeErrorMessage(error).toLowerCase();
220
+ if (message.includes('live view')) {
221
+ return NOTIFICATION_EVENT_TYPES.LIVE_VIEW_FAILED;
222
+ }
223
+
224
+ if (message.includes('test') || message.includes('typecheck') || message.includes('lint') || message.includes('build')) {
225
+ return NOTIFICATION_EVENT_TYPES.TEST_FAILED;
226
+ }
227
+
228
+ return provider === 'system' ? NOTIFICATION_EVENT_TYPES.ERROR : NOTIFICATION_EVENT_TYPES.RUN_FAILED;
229
+ }
230
+
191
231
  function broadcastInAppNotification(userId, event) {
192
232
  if (!notificationWebSocketServer || !userId || !event) {
193
233
  return;
@@ -280,12 +320,13 @@ function notifyUserIfEnabled({ userId, event }) {
280
320
  }
281
321
  }
282
322
 
283
- function notifyRunStopped({ userId, provider, sessionId = null, stopReason = 'completed', sessionName = null }) {
323
+ function notifyRunStopped({ userId, provider, sessionId = null, stopReason = 'completed', sessionName = null, eventType = null }) {
284
324
  notifyUserIfEnabled({
285
325
  userId,
286
326
  event: createNotificationEvent({
287
327
  provider,
288
328
  sessionId,
329
+ eventType: eventType || inferRunStoppedEventType({ provider, stopReason }),
289
330
  kind: 'stop',
290
331
  code: 'run.stopped',
291
332
  meta: { stopReason, sessionName },
@@ -295,7 +336,7 @@ function notifyRunStopped({ userId, provider, sessionId = null, stopReason = 'co
295
336
  });
296
337
  }
297
338
 
298
- function notifyRunFailed({ userId, provider, sessionId = null, error, sessionName = null }) {
339
+ function notifyRunFailed({ userId, provider, sessionId = null, error, sessionName = null, eventType = null }) {
299
340
  const errorMessage = normalizeErrorMessage(error);
300
341
 
301
342
  notifyUserIfEnabled({
@@ -303,6 +344,7 @@ function notifyRunFailed({ userId, provider, sessionId = null, error, sessionNam
303
344
  event: createNotificationEvent({
304
345
  provider,
305
346
  sessionId,
347
+ eventType: eventType || inferRunFailedEventType({ provider, error }),
306
348
  kind: 'error',
307
349
  code: 'run.failed',
308
350
  meta: { error: errorMessage, sessionName },
@@ -314,6 +356,7 @@ function notifyRunFailed({ userId, provider, sessionId = null, error, sessionNam
314
356
 
315
357
  export {
316
358
  createNotificationEvent,
359
+ createNotificationEventContract,
317
360
  setNotificationWebSocketServer,
318
361
  broadcastInAppNotification,
319
362
  notifyUserIfEnabled,
@@ -0,0 +1,204 @@
1
+ const NOTIFICATION_PREFERENCE_KEYS = Object.freeze({
2
+ ACTION_REQUIRED: 'actionRequired',
3
+ STOP: 'stop',
4
+ ERROR: 'error',
5
+ UPDATES: 'updates',
6
+ });
7
+
8
+ const NOTIFICATION_EVENT_TYPES = Object.freeze({
9
+ CHAT_DONE: 'chat.done',
10
+ ORCHESTRATION_DONE: 'orchestration.done',
11
+ APPROVAL_NEEDED: 'approval.needed',
12
+ ERROR: 'error',
13
+ TEST_FAILED: 'test.failed',
14
+ LIVE_VIEW_FAILED: 'live_view.failed',
15
+ RUN_STOPPED: 'run.stopped',
16
+ RUN_FAILED: 'run.failed',
17
+ AGENT_NOTIFICATION: 'agent.notification',
18
+ PUSH_ENABLED: 'push.enabled',
19
+ APP_UPDATE_AVAILABLE: 'app.update.available',
20
+ CLI_UPDATE_AVAILABLE: 'cli.update.available',
21
+ });
22
+
23
+ const NOTIFICATION_TAXONOMY = Object.freeze({
24
+ [NOTIFICATION_EVENT_TYPES.CHAT_DONE]: {
25
+ category: 'chat',
26
+ kind: 'stop',
27
+ severity: 'info',
28
+ preferenceKey: NOTIFICATION_PREFERENCE_KEYS.STOP,
29
+ description: 'A chat/provider run completed.',
30
+ },
31
+ [NOTIFICATION_EVENT_TYPES.ORCHESTRATION_DONE]: {
32
+ category: 'orchestration',
33
+ kind: 'stop',
34
+ severity: 'info',
35
+ preferenceKey: NOTIFICATION_PREFERENCE_KEYS.STOP,
36
+ description: 'An orchestration workflow completed.',
37
+ },
38
+ [NOTIFICATION_EVENT_TYPES.APPROVAL_NEEDED]: {
39
+ category: 'approval',
40
+ kind: 'action_required',
41
+ severity: 'warning',
42
+ preferenceKey: NOTIFICATION_PREFERENCE_KEYS.ACTION_REQUIRED,
43
+ requiresUserAction: true,
44
+ description: 'A run is waiting for human approval.',
45
+ },
46
+ [NOTIFICATION_EVENT_TYPES.ERROR]: {
47
+ category: 'system',
48
+ kind: 'error',
49
+ severity: 'error',
50
+ preferenceKey: NOTIFICATION_PREFERENCE_KEYS.ERROR,
51
+ description: 'A generic Pixcode error occurred.',
52
+ },
53
+ [NOTIFICATION_EVENT_TYPES.TEST_FAILED]: {
54
+ category: 'verification',
55
+ kind: 'error',
56
+ severity: 'error',
57
+ preferenceKey: NOTIFICATION_PREFERENCE_KEYS.ERROR,
58
+ description: 'A build, lint, typecheck, or test command failed.',
59
+ },
60
+ [NOTIFICATION_EVENT_TYPES.LIVE_VIEW_FAILED]: {
61
+ category: 'live_view',
62
+ kind: 'error',
63
+ severity: 'error',
64
+ preferenceKey: NOTIFICATION_PREFERENCE_KEYS.ERROR,
65
+ description: 'A Live View preview failed to start or stay healthy.',
66
+ },
67
+ [NOTIFICATION_EVENT_TYPES.RUN_STOPPED]: {
68
+ category: 'run',
69
+ kind: 'stop',
70
+ severity: 'info',
71
+ preferenceKey: NOTIFICATION_PREFERENCE_KEYS.STOP,
72
+ description: 'A run stopped without reporting a failure.',
73
+ },
74
+ [NOTIFICATION_EVENT_TYPES.RUN_FAILED]: {
75
+ category: 'run',
76
+ kind: 'error',
77
+ severity: 'error',
78
+ preferenceKey: NOTIFICATION_PREFERENCE_KEYS.ERROR,
79
+ description: 'A run failed.',
80
+ },
81
+ [NOTIFICATION_EVENT_TYPES.AGENT_NOTIFICATION]: {
82
+ category: 'agent',
83
+ kind: 'action_required',
84
+ severity: 'warning',
85
+ preferenceKey: NOTIFICATION_PREFERENCE_KEYS.ACTION_REQUIRED,
86
+ requiresUserAction: true,
87
+ description: 'An agent emitted a notification for the user.',
88
+ },
89
+ [NOTIFICATION_EVENT_TYPES.PUSH_ENABLED]: {
90
+ category: 'settings',
91
+ kind: 'info',
92
+ severity: 'info',
93
+ preferenceKey: NOTIFICATION_PREFERENCE_KEYS.UPDATES,
94
+ description: 'Push notifications were enabled.',
95
+ },
96
+ [NOTIFICATION_EVENT_TYPES.APP_UPDATE_AVAILABLE]: {
97
+ category: 'update',
98
+ kind: 'update',
99
+ severity: 'info',
100
+ preferenceKey: NOTIFICATION_PREFERENCE_KEYS.UPDATES,
101
+ description: 'A Pixcode app update is available.',
102
+ },
103
+ [NOTIFICATION_EVENT_TYPES.CLI_UPDATE_AVAILABLE]: {
104
+ category: 'update',
105
+ kind: 'update',
106
+ severity: 'info',
107
+ preferenceKey: NOTIFICATION_PREFERENCE_KEYS.UPDATES,
108
+ description: 'A CLI update is available.',
109
+ },
110
+ });
111
+
112
+ const LEGACY_CODE_TO_EVENT_TYPE = Object.freeze({
113
+ 'permission.required': NOTIFICATION_EVENT_TYPES.APPROVAL_NEEDED,
114
+ 'run.stopped': NOTIFICATION_EVENT_TYPES.RUN_STOPPED,
115
+ 'run.failed': NOTIFICATION_EVENT_TYPES.RUN_FAILED,
116
+ 'agent.notification': NOTIFICATION_EVENT_TYPES.AGENT_NOTIFICATION,
117
+ 'push.enabled': NOTIFICATION_EVENT_TYPES.PUSH_ENABLED,
118
+ 'app.update.available': NOTIFICATION_EVENT_TYPES.APP_UPDATE_AVAILABLE,
119
+ 'cli.update.available': NOTIFICATION_EVENT_TYPES.CLI_UPDATE_AVAILABLE,
120
+ 'chat.done': NOTIFICATION_EVENT_TYPES.CHAT_DONE,
121
+ 'orchestration.done': NOTIFICATION_EVENT_TYPES.ORCHESTRATION_DONE,
122
+ 'test.failed': NOTIFICATION_EVENT_TYPES.TEST_FAILED,
123
+ 'live_view.failed': NOTIFICATION_EVENT_TYPES.LIVE_VIEW_FAILED,
124
+ });
125
+
126
+ const KIND_TO_EVENT_TYPE = Object.freeze({
127
+ action_required: NOTIFICATION_EVENT_TYPES.APPROVAL_NEEDED,
128
+ stop: NOTIFICATION_EVENT_TYPES.RUN_STOPPED,
129
+ error: NOTIFICATION_EVENT_TYPES.ERROR,
130
+ update: NOTIFICATION_EVENT_TYPES.APP_UPDATE_AVAILABLE,
131
+ info: NOTIFICATION_EVENT_TYPES.AGENT_NOTIFICATION,
132
+ });
133
+
134
+ function resolveNotificationEventType(event = {}) {
135
+ if (typeof event.eventType === 'string' && NOTIFICATION_TAXONOMY[event.eventType]) {
136
+ return event.eventType;
137
+ }
138
+
139
+ if (typeof event.code === 'string' && LEGACY_CODE_TO_EVENT_TYPE[event.code]) {
140
+ return LEGACY_CODE_TO_EVENT_TYPE[event.code];
141
+ }
142
+
143
+ if (typeof event.kind === 'string' && KIND_TO_EVENT_TYPE[event.kind]) {
144
+ return KIND_TO_EVENT_TYPE[event.kind];
145
+ }
146
+
147
+ return NOTIFICATION_EVENT_TYPES.AGENT_NOTIFICATION;
148
+ }
149
+
150
+ function getNotificationTaxonomy(event = {}) {
151
+ return NOTIFICATION_TAXONOMY[resolveNotificationEventType(event)];
152
+ }
153
+
154
+ function getNotificationPreferenceKey(event = {}) {
155
+ return getNotificationTaxonomy(event)?.preferenceKey || NOTIFICATION_PREFERENCE_KEYS.UPDATES;
156
+ }
157
+
158
+ function normalizeNotificationEvent(event = {}) {
159
+ const eventType = resolveNotificationEventType(event);
160
+ const taxonomy = NOTIFICATION_TAXONOMY[eventType];
161
+
162
+ return {
163
+ ...event,
164
+ eventType,
165
+ category: taxonomy.category,
166
+ kind: event.kind || taxonomy.kind,
167
+ severity: event.severity || taxonomy.severity,
168
+ preferenceKey: taxonomy.preferenceKey,
169
+ requiresUserAction: Boolean(event.requiresUserAction ?? taxonomy.requiresUserAction),
170
+ };
171
+ }
172
+
173
+ function createNotificationEventContract(event = {}) {
174
+ const normalized = normalizeNotificationEvent(event);
175
+ const taxonomy = NOTIFICATION_TAXONOMY[normalized.eventType];
176
+
177
+ return {
178
+ eventType: normalized.eventType,
179
+ category: normalized.category,
180
+ kind: normalized.kind,
181
+ severity: normalized.severity,
182
+ preferenceKey: normalized.preferenceKey,
183
+ requiresUserAction: normalized.requiresUserAction,
184
+ description: taxonomy.description,
185
+ };
186
+ }
187
+
188
+ function listNotificationTaxonomy() {
189
+ return Object.keys(NOTIFICATION_TAXONOMY).map((eventType) =>
190
+ createNotificationEventContract({ eventType })
191
+ );
192
+ }
193
+
194
+ export {
195
+ NOTIFICATION_EVENT_TYPES,
196
+ NOTIFICATION_PREFERENCE_KEYS,
197
+ NOTIFICATION_TAXONOMY,
198
+ createNotificationEventContract,
199
+ getNotificationPreferenceKey,
200
+ getNotificationTaxonomy,
201
+ listNotificationTaxonomy,
202
+ normalizeNotificationEvent,
203
+ resolveNotificationEventType,
204
+ };