@auxiora/ambient 1.0.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 (48) hide show
  1. package/LICENSE +191 -0
  2. package/dist/anticipation.d.ts +27 -0
  3. package/dist/anticipation.d.ts.map +1 -0
  4. package/dist/anticipation.js +128 -0
  5. package/dist/anticipation.js.map +1 -0
  6. package/dist/briefing.d.ts +43 -0
  7. package/dist/briefing.d.ts.map +1 -0
  8. package/dist/briefing.js +109 -0
  9. package/dist/briefing.js.map +1 -0
  10. package/dist/index.d.ts +9 -0
  11. package/dist/index.d.ts.map +1 -0
  12. package/dist/index.js +8 -0
  13. package/dist/index.js.map +1 -0
  14. package/dist/notification.d.ts +29 -0
  15. package/dist/notification.d.ts.map +1 -0
  16. package/dist/notification.js +87 -0
  17. package/dist/notification.js.map +1 -0
  18. package/dist/orchestrator.d.ts +40 -0
  19. package/dist/orchestrator.d.ts.map +1 -0
  20. package/dist/orchestrator.js +130 -0
  21. package/dist/orchestrator.js.map +1 -0
  22. package/dist/pattern-engine.d.ts +27 -0
  23. package/dist/pattern-engine.d.ts.map +1 -0
  24. package/dist/pattern-engine.js +187 -0
  25. package/dist/pattern-engine.js.map +1 -0
  26. package/dist/scheduler.d.ts +88 -0
  27. package/dist/scheduler.d.ts.map +1 -0
  28. package/dist/scheduler.js +172 -0
  29. package/dist/scheduler.js.map +1 -0
  30. package/dist/types.d.ts +73 -0
  31. package/dist/types.d.ts.map +1 -0
  32. package/dist/types.js +8 -0
  33. package/dist/types.js.map +1 -0
  34. package/package.json +29 -0
  35. package/src/anticipation.ts +141 -0
  36. package/src/briefing.ts +152 -0
  37. package/src/index.ts +26 -0
  38. package/src/notification.ts +101 -0
  39. package/src/orchestrator.ts +188 -0
  40. package/src/pattern-engine.ts +212 -0
  41. package/src/scheduler.ts +238 -0
  42. package/src/types.ts +85 -0
  43. package/tests/ambient.test.ts +363 -0
  44. package/tests/orchestrator.test.ts +343 -0
  45. package/tests/scheduler.test.ts +310 -0
  46. package/tests/wiring.test.ts +12 -0
  47. package/tsconfig.json +15 -0
  48. package/tsconfig.tsbuildinfo +1 -0
@@ -0,0 +1,101 @@
1
+ import * as crypto from 'node:crypto';
2
+ import type { NotificationPriority, QuietNotification } from './types.js';
3
+
4
+ /** Priority ordering for queue sorting. */
5
+ const PRIORITY_ORDER: Record<NotificationPriority, number> = {
6
+ alert: 3,
7
+ nudge: 2,
8
+ whisper: 1,
9
+ };
10
+
11
+ /**
12
+ * Priority-based queue for quiet notifications.
13
+ */
14
+ export class QuietNotificationManager {
15
+ private notifications: Map<string, QuietNotification> = new Map();
16
+
17
+ /** Create and queue a notification. */
18
+ notify(
19
+ priority: NotificationPriority,
20
+ message: string,
21
+ options?: { detail?: string; source?: string }
22
+ ): QuietNotification {
23
+ const notification: QuietNotification = {
24
+ id: crypto.randomUUID(),
25
+ priority,
26
+ message,
27
+ detail: options?.detail,
28
+ createdAt: Date.now(),
29
+ dismissed: false,
30
+ source: options?.source ?? 'ambient',
31
+ };
32
+
33
+ this.notifications.set(notification.id, notification);
34
+ return notification;
35
+ }
36
+
37
+ /** Get all pending notifications, sorted by priority (highest first). */
38
+ getQueue(): QuietNotification[] {
39
+ return Array.from(this.notifications.values())
40
+ .filter(n => !n.dismissed)
41
+ .sort((a, b) => PRIORITY_ORDER[b.priority] - PRIORITY_ORDER[a.priority]);
42
+ }
43
+
44
+ /** Get notifications filtered by priority. */
45
+ getByPriority(priority: NotificationPriority): QuietNotification[] {
46
+ return Array.from(this.notifications.values())
47
+ .filter(n => !n.dismissed && n.priority === priority);
48
+ }
49
+
50
+ /** Dismiss a notification. */
51
+ dismiss(id: string): boolean {
52
+ const n = this.notifications.get(id);
53
+ if (!n) return false;
54
+ n.dismissed = true;
55
+ return true;
56
+ }
57
+
58
+ /** Dismiss all notifications. */
59
+ dismissAll(): number {
60
+ let count = 0;
61
+ for (const n of this.notifications.values()) {
62
+ if (!n.dismissed) {
63
+ n.dismissed = true;
64
+ count++;
65
+ }
66
+ }
67
+ return count;
68
+ }
69
+
70
+ /** Get a notification by ID. */
71
+ get(id: string): QuietNotification | undefined {
72
+ return this.notifications.get(id);
73
+ }
74
+
75
+ /** Get count of pending notifications. */
76
+ getPendingCount(): number {
77
+ let count = 0;
78
+ for (const n of this.notifications.values()) {
79
+ if (!n.dismissed) count++;
80
+ }
81
+ return count;
82
+ }
83
+
84
+ /** Remove old dismissed notifications. */
85
+ prune(maxAge = 24 * 60 * 60 * 1000): number {
86
+ const cutoff = Date.now() - maxAge;
87
+ let pruned = 0;
88
+ for (const [id, n] of this.notifications) {
89
+ if (n.dismissed && n.createdAt < cutoff) {
90
+ this.notifications.delete(id);
91
+ pruned++;
92
+ }
93
+ }
94
+ return pruned;
95
+ }
96
+
97
+ /** Clear all notifications. */
98
+ clear(): void {
99
+ this.notifications.clear();
100
+ }
101
+ }
@@ -0,0 +1,188 @@
1
+ import type { TriggerEvent } from '@auxiora/connectors';
2
+ import type { NotificationHub, DoNotDisturbManager, NotificationPriority } from '@auxiora/notification-hub';
3
+
4
+ /** Configuration for the NotificationOrchestrator. */
5
+ export interface OrchestratorConfig {
6
+ /** Calendar alert window in ms (default 15 minutes). */
7
+ calendarAlertWindowMs?: number;
8
+ }
9
+
10
+ /** A pending orchestrator notification. */
11
+ export interface OrchestratorNotification {
12
+ id: string;
13
+ source: string;
14
+ priority: NotificationPriority;
15
+ message: string;
16
+ createdAt: number;
17
+ delivered: boolean;
18
+ }
19
+
20
+ /** Function that delivers a notification to the user. */
21
+ export type DeliveryChannelFn = (notification: OrchestratorNotification) => void;
22
+
23
+ const URGENCY_KEYWORDS = ['urgent', 'asap', 'important', 'action required', 'deadline'];
24
+
25
+ const DEFAULT_CALENDAR_ALERT_WINDOW_MS = 15 * 60 * 1000; // 15 minutes
26
+
27
+ export class NotificationOrchestrator {
28
+ private hub: NotificationHub;
29
+ private dnd: DoNotDisturbManager;
30
+ private deliveryChannel: DeliveryChannelFn;
31
+ private pending: OrchestratorNotification[] = [];
32
+ private calendarAlertWindowMs: number;
33
+
34
+ constructor(
35
+ hub: NotificationHub,
36
+ dnd: DoNotDisturbManager,
37
+ deliveryChannel: DeliveryChannelFn,
38
+ config?: OrchestratorConfig,
39
+ ) {
40
+ this.hub = hub;
41
+ this.dnd = dnd;
42
+ this.deliveryChannel = deliveryChannel;
43
+ this.calendarAlertWindowMs = config?.calendarAlertWindowMs ?? DEFAULT_CALENDAR_ALERT_WINDOW_MS;
44
+ }
45
+
46
+ /** Map trigger events to notifications and deliver or queue them. */
47
+ processTriggerEvents(events: TriggerEvent[]): OrchestratorNotification[] {
48
+ const results: OrchestratorNotification[] = [];
49
+
50
+ for (const event of events) {
51
+ const { priority, message, source } = this.mapTriggerEvent(event);
52
+
53
+ const notification: OrchestratorNotification = {
54
+ id: crypto.randomUUID(),
55
+ source,
56
+ priority,
57
+ message,
58
+ createdAt: Date.now(),
59
+ delivered: false,
60
+ };
61
+
62
+ this.hub.send({
63
+ source: source as 'email' | 'calendar' | 'system',
64
+ priority,
65
+ title: event.triggerId,
66
+ body: message,
67
+ });
68
+
69
+ this.routeNotification(notification);
70
+ results.push(notification);
71
+ }
72
+
73
+ return results;
74
+ }
75
+
76
+ /** Check calendar events and create notifications for those starting soon. */
77
+ processCalendarCheck(
78
+ events: Array<{ title: string; startTime: number }>,
79
+ now?: number,
80
+ ): OrchestratorNotification[] {
81
+ const currentTime = now ?? Date.now();
82
+ const results: OrchestratorNotification[] = [];
83
+
84
+ for (const event of events) {
85
+ const timeUntilStart = event.startTime - currentTime;
86
+ if (timeUntilStart > 0 && timeUntilStart <= this.calendarAlertWindowMs) {
87
+ const minutesUntil = Math.round(timeUntilStart / 60_000);
88
+ const message = `"${event.title}" starts in ${minutesUntil} minute${minutesUntil === 1 ? '' : 's'}`;
89
+
90
+ const notification: OrchestratorNotification = {
91
+ id: crypto.randomUUID(),
92
+ source: 'calendar',
93
+ priority: 'important',
94
+ message,
95
+ createdAt: currentTime,
96
+ delivered: false,
97
+ };
98
+
99
+ this.hub.send({
100
+ source: 'calendar',
101
+ priority: 'important',
102
+ title: event.title,
103
+ body: message,
104
+ });
105
+
106
+ this.routeNotification(notification);
107
+ results.push(notification);
108
+ }
109
+ }
110
+
111
+ return results;
112
+ }
113
+
114
+ /** Get all pending (undelivered or queued) notifications. */
115
+ getPending(): OrchestratorNotification[] {
116
+ return this.pending.filter((n) => !n.delivered);
117
+ }
118
+
119
+ /** Dismiss a pending notification by ID. Returns true if found and removed. */
120
+ dismiss(id: string): boolean {
121
+ const index = this.pending.findIndex((n) => n.id === id);
122
+ if (index === -1) return false;
123
+ this.pending.splice(index, 1);
124
+ return true;
125
+ }
126
+
127
+ private mapTriggerEvent(event: TriggerEvent): {
128
+ priority: NotificationPriority;
129
+ message: string;
130
+ source: string;
131
+ } {
132
+ switch (event.triggerId) {
133
+ case 'new-email': {
134
+ const subject = String(event.data['subject'] ?? '');
135
+ const from = String(event.data['from'] ?? 'unknown sender');
136
+ const isUrgent = URGENCY_KEYWORDS.some((kw) =>
137
+ subject.toLowerCase().includes(kw),
138
+ );
139
+ return {
140
+ priority: isUrgent ? 'urgent' : 'important',
141
+ message: `New email from ${from}: ${subject}`,
142
+ source: 'email',
143
+ };
144
+ }
145
+
146
+ case 'event-starting-soon': {
147
+ const title = String(event.data['title'] ?? 'Untitled event');
148
+ return {
149
+ priority: 'important',
150
+ message: `Event starting soon: ${title}`,
151
+ source: 'calendar',
152
+ };
153
+ }
154
+
155
+ case 'file-shared': {
156
+ const fileName = String(event.data['fileName'] ?? 'a file');
157
+ const sharedBy = String(event.data['sharedBy'] ?? 'someone');
158
+ return {
159
+ priority: 'low',
160
+ message: `${sharedBy} shared ${fileName} with you`,
161
+ source: 'system',
162
+ };
163
+ }
164
+
165
+ default:
166
+ return {
167
+ priority: 'low',
168
+ message: `Notification from ${event.connectorId}: ${event.triggerId}`,
169
+ source: 'system',
170
+ };
171
+ }
172
+ }
173
+
174
+ private routeNotification(notification: OrchestratorNotification): void {
175
+ const dndActive = this.dnd.isActive();
176
+
177
+ if (dndActive && notification.priority !== 'urgent') {
178
+ // Queue silently — do not deliver
179
+ notification.delivered = false;
180
+ this.pending.push(notification);
181
+ return;
182
+ }
183
+
184
+ // Deliver immediately
185
+ notification.delivered = true;
186
+ this.deliveryChannel(notification);
187
+ }
188
+ }
@@ -0,0 +1,212 @@
1
+ import * as crypto from 'node:crypto';
2
+ import type { AmbientPattern, AmbientPatternType, ObservedEvent } from './types.js';
3
+
4
+ /** Sliding window size for frequency analysis. */
5
+ const DEFAULT_WINDOW_SIZE = 7 * 24 * 60 * 60 * 1000; // 7 days
6
+ /** Minimum occurrences to consider something a pattern. */
7
+ const MIN_OCCURRENCES = 3;
8
+ /** Minimum confidence to emit a pattern. */
9
+ const MIN_CONFIDENCE = 0.3;
10
+
11
+ /**
12
+ * Ambient pattern engine — observes events and detects behavioral patterns
13
+ * using sliding window frequency analysis.
14
+ */
15
+ export class AmbientPatternEngine {
16
+ private events: ObservedEvent[] = [];
17
+ private patterns: Map<string, AmbientPattern> = new Map();
18
+ private windowSize: number;
19
+
20
+ constructor(windowSize?: number) {
21
+ this.windowSize = windowSize ?? DEFAULT_WINDOW_SIZE;
22
+ }
23
+
24
+ /** Observe a new event. */
25
+ observe(event: ObservedEvent): void {
26
+ this.events.push(event);
27
+ // Prune events outside the window
28
+ const cutoff = Date.now() - this.windowSize;
29
+ this.events = this.events.filter(e => e.timestamp >= cutoff);
30
+ }
31
+
32
+ /** Run pattern detection on observed events. */
33
+ detectPatterns(): AmbientPattern[] {
34
+ const detected: AmbientPattern[] = [];
35
+
36
+ // Group events by type
37
+ const byType = new Map<string, ObservedEvent[]>();
38
+ for (const event of this.events) {
39
+ const existing = byType.get(event.type) ?? [];
40
+ existing.push(event);
41
+ byType.set(event.type, existing);
42
+ }
43
+
44
+ // Detect schedule patterns (recurring events at similar times)
45
+ for (const [type, events] of byType) {
46
+ if (events.length >= MIN_OCCURRENCES) {
47
+ const schedulePattern = this.detectSchedulePattern(type, events);
48
+ if (schedulePattern) detected.push(schedulePattern);
49
+
50
+ const frequencyPattern = this.detectFrequencyPattern(type, events);
51
+ if (frequencyPattern) detected.push(frequencyPattern);
52
+ }
53
+ }
54
+
55
+ // Detect correlations between event types
56
+ const correlations = this.detectCorrelations(byType);
57
+ detected.push(...correlations);
58
+
59
+ // Update stored patterns
60
+ for (const pattern of detected) {
61
+ const existing = this.patterns.get(pattern.id);
62
+ if (existing) {
63
+ existing.lastConfirmedAt = Date.now();
64
+ existing.occurrences++;
65
+ existing.confidence = Math.min(1, existing.confidence + 0.05);
66
+ } else {
67
+ this.patterns.set(pattern.id, pattern);
68
+ }
69
+ }
70
+
71
+ return detected;
72
+ }
73
+
74
+ /** Get all detected patterns above minimum confidence. */
75
+ getPatterns(): AmbientPattern[] {
76
+ return Array.from(this.patterns.values())
77
+ .filter(p => p.confidence >= MIN_CONFIDENCE)
78
+ .sort((a, b) => b.confidence - a.confidence);
79
+ }
80
+
81
+ /** Get a pattern by ID. */
82
+ getPattern(id: string): AmbientPattern | undefined {
83
+ return this.patterns.get(id);
84
+ }
85
+
86
+ /** Get the number of observed events in the window. */
87
+ getEventCount(): number {
88
+ return this.events.length;
89
+ }
90
+
91
+ /** Clear all events and patterns. */
92
+ reset(): void {
93
+ this.events = [];
94
+ this.patterns.clear();
95
+ }
96
+
97
+ private detectSchedulePattern(type: string, events: ObservedEvent[]): AmbientPattern | null {
98
+ // Check if events happen at similar hours of the day
99
+ const hours = events.map(e => new Date(e.timestamp).getHours());
100
+ const hourCounts = new Map<number, number>();
101
+ for (const h of hours) {
102
+ hourCounts.set(h, (hourCounts.get(h) ?? 0) + 1);
103
+ }
104
+
105
+ // Find the most common hour
106
+ let maxHour = 0;
107
+ let maxCount = 0;
108
+ for (const [hour, count] of hourCounts) {
109
+ if (count > maxCount) {
110
+ maxHour = hour;
111
+ maxCount = count;
112
+ }
113
+ }
114
+
115
+ const ratio = maxCount / events.length;
116
+ if (ratio < 0.5 || maxCount < MIN_OCCURRENCES) return null;
117
+
118
+ const id = crypto.randomUUID();
119
+ return {
120
+ id,
121
+ type: 'schedule',
122
+ description: `"${type}" events frequently occur around ${maxHour}:00`,
123
+ confidence: Math.min(1, ratio * 0.8 + (maxCount / 10) * 0.2),
124
+ evidence: events.slice(-3).map(e => `${type} at ${new Date(e.timestamp).toISOString()}`),
125
+ detectedAt: Date.now(),
126
+ lastConfirmedAt: Date.now(),
127
+ occurrences: maxCount,
128
+ };
129
+ }
130
+
131
+ private detectFrequencyPattern(type: string, events: ObservedEvent[]): AmbientPattern | null {
132
+ if (events.length < MIN_OCCURRENCES) return null;
133
+
134
+ // Calculate average interval between events
135
+ const sorted = [...events].sort((a, b) => a.timestamp - b.timestamp);
136
+ const intervals: number[] = [];
137
+ for (let i = 1; i < sorted.length; i++) {
138
+ intervals.push(sorted[i].timestamp - sorted[i - 1].timestamp);
139
+ }
140
+
141
+ if (intervals.length === 0) return null;
142
+
143
+ const avgInterval = intervals.reduce((a, b) => a + b, 0) / intervals.length;
144
+ const stdDev = Math.sqrt(
145
+ intervals.reduce((sum, iv) => sum + (iv - avgInterval) ** 2, 0) / intervals.length
146
+ );
147
+
148
+ // Low variance means regular frequency
149
+ const cv = avgInterval > 0 ? stdDev / avgInterval : Infinity;
150
+ if (cv > 0.5) return null; // Too irregular
151
+
152
+ const confidence = Math.min(1, 1 - cv);
153
+ if (confidence < MIN_CONFIDENCE) return null;
154
+
155
+ const hours = Math.round(avgInterval / (60 * 60 * 1000) * 10) / 10;
156
+ const id = crypto.randomUUID();
157
+ return {
158
+ id,
159
+ type: 'preference',
160
+ description: `"${type}" occurs roughly every ${hours} hours`,
161
+ confidence,
162
+ evidence: [`${events.length} occurrences over window`, `Average interval: ${hours}h`],
163
+ detectedAt: Date.now(),
164
+ lastConfirmedAt: Date.now(),
165
+ occurrences: events.length,
166
+ };
167
+ }
168
+
169
+ private detectCorrelations(byType: Map<string, ObservedEvent[]>): AmbientPattern[] {
170
+ const patterns: AmbientPattern[] = [];
171
+ const types = Array.from(byType.keys());
172
+
173
+ // Check pairs of event types for temporal correlation
174
+ for (let i = 0; i < types.length; i++) {
175
+ for (let j = i + 1; j < types.length; j++) {
176
+ const eventsA = byType.get(types[i])!;
177
+ const eventsB = byType.get(types[j])!;
178
+
179
+ if (eventsA.length < 2 || eventsB.length < 2) continue;
180
+
181
+ // Count how often B follows A within 5 minutes
182
+ const followWindow = 5 * 60 * 1000;
183
+ let follows = 0;
184
+ for (const a of eventsA) {
185
+ for (const b of eventsB) {
186
+ if (b.timestamp > a.timestamp && b.timestamp - a.timestamp <= followWindow) {
187
+ follows++;
188
+ break;
189
+ }
190
+ }
191
+ }
192
+
193
+ const followRatio = follows / eventsA.length;
194
+ if (followRatio >= 0.5 && follows >= 2) {
195
+ const id = crypto.randomUUID();
196
+ patterns.push({
197
+ id,
198
+ type: 'correlation',
199
+ description: `"${types[j]}" often follows "${types[i]}" within 5 minutes`,
200
+ confidence: Math.min(1, followRatio * 0.9),
201
+ evidence: [`${follows} of ${eventsA.length} "${types[i]}" events followed by "${types[j]}"`],
202
+ detectedAt: Date.now(),
203
+ lastConfirmedAt: Date.now(),
204
+ occurrences: follows,
205
+ });
206
+ }
207
+ }
208
+ }
209
+
210
+ return patterns;
211
+ }
212
+ }