@auxiora/ambient 1.0.0 → 1.3.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/package.json +12 -6
- package/src/anticipation.ts +0 -141
- package/src/briefing.ts +0 -152
- package/src/index.ts +0 -26
- package/src/notification.ts +0 -101
- package/src/orchestrator.ts +0 -188
- package/src/pattern-engine.ts +0 -212
- package/src/scheduler.ts +0 -238
- package/src/types.ts +0 -85
- package/tests/ambient.test.ts +0 -363
- package/tests/orchestrator.test.ts +0 -343
- package/tests/scheduler.test.ts +0 -310
- package/tests/wiring.test.ts +0 -12
- package/tsconfig.json +0 -15
- package/tsconfig.tsbuildinfo +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@auxiora/ambient",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "Ambient pattern detection, anticipation, briefings, and quiet notifications",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -12,15 +12,21 @@
|
|
|
12
12
|
}
|
|
13
13
|
},
|
|
14
14
|
"dependencies": {
|
|
15
|
-
"@auxiora/
|
|
16
|
-
"@auxiora/
|
|
17
|
-
"@auxiora/personality": "1.
|
|
18
|
-
"@auxiora/notification-hub": "1.
|
|
19
|
-
"@auxiora/connectors": "1.
|
|
15
|
+
"@auxiora/behaviors": "1.3.0",
|
|
16
|
+
"@auxiora/memory": "1.3.0",
|
|
17
|
+
"@auxiora/personality": "1.3.0",
|
|
18
|
+
"@auxiora/notification-hub": "1.3.0",
|
|
19
|
+
"@auxiora/connectors": "1.3.0"
|
|
20
20
|
},
|
|
21
21
|
"engines": {
|
|
22
22
|
"node": ">=22.0.0"
|
|
23
23
|
},
|
|
24
|
+
"publishConfig": {
|
|
25
|
+
"access": "public"
|
|
26
|
+
},
|
|
27
|
+
"files": [
|
|
28
|
+
"dist/"
|
|
29
|
+
],
|
|
24
30
|
"scripts": {
|
|
25
31
|
"build": "tsc",
|
|
26
32
|
"clean": "rm -rf dist",
|
package/src/anticipation.ts
DELETED
|
@@ -1,141 +0,0 @@
|
|
|
1
|
-
import * as crypto from 'node:crypto';
|
|
2
|
-
import type { AmbientPattern, Anticipation } from './types.js';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Anticipation engine — predicts user needs based on detected patterns.
|
|
6
|
-
*/
|
|
7
|
-
export class AnticipationEngine {
|
|
8
|
-
private anticipations: Map<string, Anticipation> = new Map();
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Generate anticipations from detected patterns and current context.
|
|
12
|
-
* @param patterns - Currently detected patterns.
|
|
13
|
-
* @param context - Current context (time, active tasks, etc.).
|
|
14
|
-
*/
|
|
15
|
-
generateAnticipations(
|
|
16
|
-
patterns: AmbientPattern[],
|
|
17
|
-
context?: { currentTime?: number; activeTaskCount?: number }
|
|
18
|
-
): Anticipation[] {
|
|
19
|
-
const now = context?.currentTime ?? Date.now();
|
|
20
|
-
const generated: Anticipation[] = [];
|
|
21
|
-
|
|
22
|
-
for (const pattern of patterns) {
|
|
23
|
-
if (pattern.confidence < 0.4) continue;
|
|
24
|
-
|
|
25
|
-
switch (pattern.type) {
|
|
26
|
-
case 'schedule': {
|
|
27
|
-
const anticipation = this.anticipateSchedule(pattern, now);
|
|
28
|
-
if (anticipation) generated.push(anticipation);
|
|
29
|
-
break;
|
|
30
|
-
}
|
|
31
|
-
case 'preference': {
|
|
32
|
-
const anticipation = this.anticipatePreference(pattern, now);
|
|
33
|
-
if (anticipation) generated.push(anticipation);
|
|
34
|
-
break;
|
|
35
|
-
}
|
|
36
|
-
case 'correlation': {
|
|
37
|
-
const anticipation = this.anticipateCorrelation(pattern, now);
|
|
38
|
-
if (anticipation) generated.push(anticipation);
|
|
39
|
-
break;
|
|
40
|
-
}
|
|
41
|
-
case 'trigger': {
|
|
42
|
-
const anticipation = this.anticipateTrigger(pattern, now);
|
|
43
|
-
if (anticipation) generated.push(anticipation);
|
|
44
|
-
break;
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
// Store generated anticipations
|
|
50
|
-
for (const a of generated) {
|
|
51
|
-
this.anticipations.set(a.id, a);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
return generated;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/** Get all current anticipations sorted by expected time. */
|
|
58
|
-
getAnticipations(): Anticipation[] {
|
|
59
|
-
return Array.from(this.anticipations.values())
|
|
60
|
-
.filter(a => a.expectedAt > Date.now())
|
|
61
|
-
.sort((a, b) => a.expectedAt - b.expectedAt);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/** Clear expired anticipations. */
|
|
65
|
-
prune(): number {
|
|
66
|
-
const now = Date.now();
|
|
67
|
-
let pruned = 0;
|
|
68
|
-
for (const [id, a] of this.anticipations) {
|
|
69
|
-
if (a.expectedAt < now) {
|
|
70
|
-
this.anticipations.delete(id);
|
|
71
|
-
pruned++;
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
return pruned;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/** Reset all anticipations. */
|
|
78
|
-
reset(): void {
|
|
79
|
-
this.anticipations.clear();
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
private anticipateSchedule(pattern: AmbientPattern, now: number): Anticipation | null {
|
|
83
|
-
// Extract hour from description like '... around 14:00'
|
|
84
|
-
const match = pattern.description.match(/around (\d+):00/);
|
|
85
|
-
if (!match) return null;
|
|
86
|
-
|
|
87
|
-
const hour = parseInt(match[1], 10);
|
|
88
|
-
const nextOccurrence = new Date(now);
|
|
89
|
-
nextOccurrence.setHours(hour, 0, 0, 0);
|
|
90
|
-
if (nextOccurrence.getTime() <= now) {
|
|
91
|
-
nextOccurrence.setDate(nextOccurrence.getDate() + 1);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
return {
|
|
95
|
-
id: crypto.randomUUID(),
|
|
96
|
-
description: `Based on your pattern: ${pattern.description}`,
|
|
97
|
-
expectedAt: nextOccurrence.getTime(),
|
|
98
|
-
confidence: pattern.confidence * 0.8,
|
|
99
|
-
sourcePatterns: [pattern.id],
|
|
100
|
-
suggestedAction: `Prepare for upcoming "${pattern.description.split('"')[1]}" activity`,
|
|
101
|
-
};
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
private anticipatePreference(pattern: AmbientPattern, now: number): Anticipation | null {
|
|
105
|
-
// Extract interval from description like 'every 2.5 hours'
|
|
106
|
-
const match = pattern.description.match(/every ([\d.]+) hours/);
|
|
107
|
-
if (!match) return null;
|
|
108
|
-
|
|
109
|
-
const intervalHours = parseFloat(match[1]);
|
|
110
|
-
const expectedAt = now + intervalHours * 60 * 60 * 1000;
|
|
111
|
-
|
|
112
|
-
return {
|
|
113
|
-
id: crypto.randomUUID(),
|
|
114
|
-
description: `You typically do this every ${intervalHours} hours`,
|
|
115
|
-
expectedAt,
|
|
116
|
-
confidence: pattern.confidence * 0.7,
|
|
117
|
-
sourcePatterns: [pattern.id],
|
|
118
|
-
};
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
private anticipateCorrelation(pattern: AmbientPattern, now: number): Anticipation | null {
|
|
122
|
-
return {
|
|
123
|
-
id: crypto.randomUUID(),
|
|
124
|
-
description: `When this happens: ${pattern.description}`,
|
|
125
|
-
expectedAt: now + 5 * 60 * 1000, // 5 minutes from now
|
|
126
|
-
confidence: pattern.confidence * 0.6,
|
|
127
|
-
sourcePatterns: [pattern.id],
|
|
128
|
-
suggestedAction: `Proactively prepare the follow-up action`,
|
|
129
|
-
};
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
private anticipateTrigger(pattern: AmbientPattern, now: number): Anticipation | null {
|
|
133
|
-
return {
|
|
134
|
-
id: crypto.randomUUID(),
|
|
135
|
-
description: `Trigger detected: ${pattern.description}`,
|
|
136
|
-
expectedAt: now + 60 * 1000, // 1 minute
|
|
137
|
-
confidence: pattern.confidence * 0.9,
|
|
138
|
-
sourcePatterns: [pattern.id],
|
|
139
|
-
};
|
|
140
|
-
}
|
|
141
|
-
}
|
package/src/briefing.ts
DELETED
|
@@ -1,152 +0,0 @@
|
|
|
1
|
-
import type { AmbientPattern, Anticipation, BriefingConfig, QuietNotification } from './types.js';
|
|
2
|
-
import { DEFAULT_BRIEFING_CONFIG } from './types.js';
|
|
3
|
-
|
|
4
|
-
/** A compiled briefing for the user. */
|
|
5
|
-
export interface Briefing {
|
|
6
|
-
userId: string;
|
|
7
|
-
generatedAt: number;
|
|
8
|
-
timeOfDay: 'morning' | 'evening' | 'custom';
|
|
9
|
-
sections: BriefingSection[];
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
/** A section of a briefing. */
|
|
13
|
-
export interface BriefingSection {
|
|
14
|
-
title: string;
|
|
15
|
-
items: string[];
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/** Data sources for briefing generation. */
|
|
19
|
-
export interface BriefingDataSources {
|
|
20
|
-
patterns?: AmbientPattern[];
|
|
21
|
-
anticipations?: Anticipation[];
|
|
22
|
-
notifications?: QuietNotification[];
|
|
23
|
-
calendarEvents?: Array<{ title: string; time: string }>;
|
|
24
|
-
tasks?: Array<{ title: string; status: string }>;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Generates personalized briefings/digests for users.
|
|
29
|
-
*/
|
|
30
|
-
export class BriefingGenerator {
|
|
31
|
-
private config: BriefingConfig;
|
|
32
|
-
|
|
33
|
-
constructor(config?: Partial<BriefingConfig>) {
|
|
34
|
-
this.config = { ...DEFAULT_BRIEFING_CONFIG, ...config };
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/** Generate a briefing for the given user at the given time. */
|
|
38
|
-
generateBriefing(
|
|
39
|
-
userId: string,
|
|
40
|
-
time: 'morning' | 'evening' | 'custom',
|
|
41
|
-
sources: BriefingDataSources
|
|
42
|
-
): Briefing {
|
|
43
|
-
const sections: BriefingSection[] = [];
|
|
44
|
-
|
|
45
|
-
// Notifications section
|
|
46
|
-
if (sources.notifications && sources.notifications.length > 0) {
|
|
47
|
-
const pending = sources.notifications.filter(n => !n.dismissed);
|
|
48
|
-
if (pending.length > 0) {
|
|
49
|
-
sections.push({
|
|
50
|
-
title: 'Notifications',
|
|
51
|
-
items: pending.slice(0, this.config.maxItemsPerSection).map(
|
|
52
|
-
n => `[${n.priority}] ${n.message}`
|
|
53
|
-
),
|
|
54
|
-
});
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// Calendar section
|
|
59
|
-
if (this.config.categories.includes('calendar') && sources.calendarEvents) {
|
|
60
|
-
const events = sources.calendarEvents.slice(0, this.config.maxItemsPerSection);
|
|
61
|
-
if (events.length > 0) {
|
|
62
|
-
sections.push({
|
|
63
|
-
title: time === 'morning' ? 'Today\'s Schedule' : 'Tomorrow\'s Schedule',
|
|
64
|
-
items: events.map(e => `${e.time} - ${e.title}`),
|
|
65
|
-
});
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
// Tasks section
|
|
70
|
-
if (this.config.categories.includes('tasks') && sources.tasks) {
|
|
71
|
-
const activeTasks = sources.tasks
|
|
72
|
-
.filter(t => t.status !== 'completed')
|
|
73
|
-
.slice(0, this.config.maxItemsPerSection);
|
|
74
|
-
if (activeTasks.length > 0) {
|
|
75
|
-
sections.push({
|
|
76
|
-
title: 'Active Tasks',
|
|
77
|
-
items: activeTasks.map(t => `${t.title} (${t.status})`),
|
|
78
|
-
});
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
// Patterns section
|
|
83
|
-
if (this.config.categories.includes('patterns') && sources.patterns) {
|
|
84
|
-
const topPatterns = sources.patterns
|
|
85
|
-
.filter(p => p.confidence >= 0.5)
|
|
86
|
-
.slice(0, this.config.maxItemsPerSection);
|
|
87
|
-
if (topPatterns.length > 0) {
|
|
88
|
-
sections.push({
|
|
89
|
-
title: 'Observed Patterns',
|
|
90
|
-
items: topPatterns.map(p => `${p.description} (${Math.round(p.confidence * 100)}% confidence)`),
|
|
91
|
-
});
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// Anticipations section
|
|
96
|
-
if (sources.anticipations && sources.anticipations.length > 0) {
|
|
97
|
-
const upcoming = sources.anticipations
|
|
98
|
-
.filter(a => a.expectedAt > Date.now())
|
|
99
|
-
.slice(0, this.config.maxItemsPerSection);
|
|
100
|
-
if (upcoming.length > 0) {
|
|
101
|
-
sections.push({
|
|
102
|
-
title: 'Upcoming',
|
|
103
|
-
items: upcoming.map(a => {
|
|
104
|
-
const timeStr = new Date(a.expectedAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
|
105
|
-
return `~${timeStr}: ${a.description}`;
|
|
106
|
-
}),
|
|
107
|
-
});
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
return {
|
|
112
|
-
userId,
|
|
113
|
-
generatedAt: Date.now(),
|
|
114
|
-
timeOfDay: time,
|
|
115
|
-
sections,
|
|
116
|
-
};
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
/** Check if it's time for a briefing. */
|
|
120
|
-
isBriefingTime(time: 'morning' | 'evening'): boolean {
|
|
121
|
-
if (!this.config.enabled) return false;
|
|
122
|
-
const now = new Date();
|
|
123
|
-
const currentTime = `${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}`;
|
|
124
|
-
const target = time === 'morning' ? this.config.morningTime : this.config.eveningTime;
|
|
125
|
-
return currentTime === target;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
/** Get config. */
|
|
129
|
-
getConfig(): BriefingConfig {
|
|
130
|
-
return { ...this.config };
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
/** Format a briefing as human-readable text. */
|
|
135
|
-
export function formatBriefingAsText(briefing: Briefing): string {
|
|
136
|
-
const greeting = briefing.timeOfDay === 'morning'
|
|
137
|
-
? 'Good morning! Here\'s your day:'
|
|
138
|
-
: briefing.timeOfDay === 'evening'
|
|
139
|
-
? 'Here\'s your evening summary:'
|
|
140
|
-
: 'Here\'s your briefing:';
|
|
141
|
-
|
|
142
|
-
if (briefing.sections.length === 0) {
|
|
143
|
-
return `${greeting}\n\nNo updates right now.`;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
const sections = briefing.sections.map(s => {
|
|
147
|
-
const items = s.items.map(item => ` ${item}`).join('\n');
|
|
148
|
-
return `${s.title}\n${items}`;
|
|
149
|
-
}).join('\n\n');
|
|
150
|
-
|
|
151
|
-
return `${greeting}\n\n${sections}`;
|
|
152
|
-
}
|
package/src/index.ts
DELETED
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
export type {
|
|
2
|
-
AmbientPattern,
|
|
3
|
-
AmbientPatternType,
|
|
4
|
-
Anticipation,
|
|
5
|
-
QuietNotification,
|
|
6
|
-
NotificationPriority,
|
|
7
|
-
BriefingConfig,
|
|
8
|
-
ObservedEvent,
|
|
9
|
-
} from './types.js';
|
|
10
|
-
export { DEFAULT_BRIEFING_CONFIG } from './types.js';
|
|
11
|
-
export { AmbientPatternEngine } from './pattern-engine.js';
|
|
12
|
-
export { AnticipationEngine } from './anticipation.js';
|
|
13
|
-
export { BriefingGenerator, formatBriefingAsText, type Briefing, type BriefingSection, type BriefingDataSources } from './briefing.js';
|
|
14
|
-
export { QuietNotificationManager } from './notification.js';
|
|
15
|
-
export {
|
|
16
|
-
AmbientScheduler,
|
|
17
|
-
DEFAULT_AMBIENT_SCHEDULER_CONFIG,
|
|
18
|
-
type AmbientSchedulerConfig,
|
|
19
|
-
type AmbientSchedulerDeps,
|
|
20
|
-
} from './scheduler.js';
|
|
21
|
-
export {
|
|
22
|
-
NotificationOrchestrator,
|
|
23
|
-
type OrchestratorConfig,
|
|
24
|
-
type OrchestratorNotification,
|
|
25
|
-
type DeliveryChannelFn,
|
|
26
|
-
} from './orchestrator.js';
|
package/src/notification.ts
DELETED
|
@@ -1,101 +0,0 @@
|
|
|
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
|
-
}
|
package/src/orchestrator.ts
DELETED
|
@@ -1,188 +0,0 @@
|
|
|
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
|
-
}
|