@affectively/aeon-pages 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.
Files changed (124) hide show
  1. package/CHANGELOG.md +112 -0
  2. package/README.md +625 -0
  3. package/examples/basic/aeon.config.ts +39 -0
  4. package/examples/basic/components/Cursor.tsx +86 -0
  5. package/examples/basic/components/OfflineIndicator.tsx +103 -0
  6. package/examples/basic/components/PresenceBar.tsx +77 -0
  7. package/examples/basic/package.json +20 -0
  8. package/examples/basic/pages/index.tsx +80 -0
  9. package/package.json +101 -0
  10. package/packages/analytics/README.md +309 -0
  11. package/packages/analytics/build.ts +35 -0
  12. package/packages/analytics/package.json +50 -0
  13. package/packages/analytics/src/click-tracker.ts +368 -0
  14. package/packages/analytics/src/context-bridge.ts +319 -0
  15. package/packages/analytics/src/data-layer.ts +302 -0
  16. package/packages/analytics/src/gtm-loader.ts +239 -0
  17. package/packages/analytics/src/index.ts +230 -0
  18. package/packages/analytics/src/merkle-tree.ts +489 -0
  19. package/packages/analytics/src/provider.tsx +300 -0
  20. package/packages/analytics/src/types.ts +320 -0
  21. package/packages/analytics/src/use-analytics.ts +296 -0
  22. package/packages/analytics/tsconfig.json +19 -0
  23. package/packages/benchmarks/src/benchmark.test.ts +691 -0
  24. package/packages/cli/dist/index.js +61899 -0
  25. package/packages/cli/package.json +43 -0
  26. package/packages/cli/src/commands/build.test.ts +682 -0
  27. package/packages/cli/src/commands/build.ts +890 -0
  28. package/packages/cli/src/commands/dev.ts +473 -0
  29. package/packages/cli/src/commands/init.ts +409 -0
  30. package/packages/cli/src/commands/start.ts +297 -0
  31. package/packages/cli/src/index.ts +105 -0
  32. package/packages/directives/src/use-aeon.ts +272 -0
  33. package/packages/mcp-server/package.json +51 -0
  34. package/packages/mcp-server/src/index.ts +178 -0
  35. package/packages/mcp-server/src/resources.ts +346 -0
  36. package/packages/mcp-server/src/tools/index.ts +36 -0
  37. package/packages/mcp-server/src/tools/navigation.ts +545 -0
  38. package/packages/mcp-server/tsconfig.json +21 -0
  39. package/packages/react/package.json +40 -0
  40. package/packages/react/src/Link.tsx +388 -0
  41. package/packages/react/src/components/InstallPrompt.tsx +286 -0
  42. package/packages/react/src/components/OfflineDiagnostics.tsx +677 -0
  43. package/packages/react/src/components/PushNotifications.tsx +453 -0
  44. package/packages/react/src/hooks/useAeonNavigation.ts +219 -0
  45. package/packages/react/src/hooks/useConflicts.ts +277 -0
  46. package/packages/react/src/hooks/useNetworkState.ts +209 -0
  47. package/packages/react/src/hooks/usePilotNavigation.ts +254 -0
  48. package/packages/react/src/hooks/useServiceWorker.ts +278 -0
  49. package/packages/react/src/hooks.ts +195 -0
  50. package/packages/react/src/index.ts +151 -0
  51. package/packages/react/src/provider.tsx +467 -0
  52. package/packages/react/tsconfig.json +19 -0
  53. package/packages/runtime/README.md +399 -0
  54. package/packages/runtime/build.ts +48 -0
  55. package/packages/runtime/package.json +71 -0
  56. package/packages/runtime/schema.sql +40 -0
  57. package/packages/runtime/src/api-routes.ts +465 -0
  58. package/packages/runtime/src/benchmark.ts +171 -0
  59. package/packages/runtime/src/cache.ts +479 -0
  60. package/packages/runtime/src/durable-object.ts +1341 -0
  61. package/packages/runtime/src/index.ts +360 -0
  62. package/packages/runtime/src/navigation.test.ts +421 -0
  63. package/packages/runtime/src/navigation.ts +422 -0
  64. package/packages/runtime/src/nextjs-adapter.ts +272 -0
  65. package/packages/runtime/src/offline/encrypted-queue.test.ts +607 -0
  66. package/packages/runtime/src/offline/encrypted-queue.ts +478 -0
  67. package/packages/runtime/src/offline/encryption.test.ts +412 -0
  68. package/packages/runtime/src/offline/encryption.ts +397 -0
  69. package/packages/runtime/src/offline/types.ts +465 -0
  70. package/packages/runtime/src/predictor.ts +371 -0
  71. package/packages/runtime/src/registry.ts +351 -0
  72. package/packages/runtime/src/router/context-extractor.ts +661 -0
  73. package/packages/runtime/src/router/esi-control-react.tsx +2053 -0
  74. package/packages/runtime/src/router/esi-control.ts +541 -0
  75. package/packages/runtime/src/router/esi-cyrano.ts +779 -0
  76. package/packages/runtime/src/router/esi-format-react.tsx +1744 -0
  77. package/packages/runtime/src/router/esi-react.tsx +1065 -0
  78. package/packages/runtime/src/router/esi-translate-observer.ts +476 -0
  79. package/packages/runtime/src/router/esi-translate-react.tsx +556 -0
  80. package/packages/runtime/src/router/esi-translate.ts +503 -0
  81. package/packages/runtime/src/router/esi.ts +666 -0
  82. package/packages/runtime/src/router/heuristic-adapter.test.ts +295 -0
  83. package/packages/runtime/src/router/heuristic-adapter.ts +557 -0
  84. package/packages/runtime/src/router/index.ts +298 -0
  85. package/packages/runtime/src/router/merkle-capability.ts +473 -0
  86. package/packages/runtime/src/router/speculation.ts +451 -0
  87. package/packages/runtime/src/router/types.ts +630 -0
  88. package/packages/runtime/src/router.test.ts +470 -0
  89. package/packages/runtime/src/router.ts +302 -0
  90. package/packages/runtime/src/server.ts +481 -0
  91. package/packages/runtime/src/service-worker-push.ts +319 -0
  92. package/packages/runtime/src/service-worker.ts +553 -0
  93. package/packages/runtime/src/skeleton-hydrate.ts +237 -0
  94. package/packages/runtime/src/speculation.test.ts +389 -0
  95. package/packages/runtime/src/speculation.ts +486 -0
  96. package/packages/runtime/src/storage.test.ts +1297 -0
  97. package/packages/runtime/src/storage.ts +1048 -0
  98. package/packages/runtime/src/sync/conflict-resolver.test.ts +528 -0
  99. package/packages/runtime/src/sync/conflict-resolver.ts +565 -0
  100. package/packages/runtime/src/sync/coordinator.test.ts +608 -0
  101. package/packages/runtime/src/sync/coordinator.ts +596 -0
  102. package/packages/runtime/src/tree-compiler.ts +295 -0
  103. package/packages/runtime/src/types.ts +728 -0
  104. package/packages/runtime/src/worker.ts +327 -0
  105. package/packages/runtime/tsconfig.json +20 -0
  106. package/packages/runtime/wasm/aeon_pages_runtime.d.ts +504 -0
  107. package/packages/runtime/wasm/aeon_pages_runtime.js +1657 -0
  108. package/packages/runtime/wasm/aeon_pages_runtime_bg.wasm +0 -0
  109. package/packages/runtime/wasm/aeon_pages_runtime_bg.wasm.d.ts +196 -0
  110. package/packages/runtime/wasm/package.json +21 -0
  111. package/packages/runtime/wrangler.toml +41 -0
  112. package/packages/runtime-wasm/Cargo.lock +436 -0
  113. package/packages/runtime-wasm/Cargo.toml +29 -0
  114. package/packages/runtime-wasm/pkg/aeon_pages_runtime.d.ts +480 -0
  115. package/packages/runtime-wasm/pkg/aeon_pages_runtime.js +1568 -0
  116. package/packages/runtime-wasm/pkg/aeon_pages_runtime_bg.wasm +0 -0
  117. package/packages/runtime-wasm/pkg/aeon_pages_runtime_bg.wasm.d.ts +192 -0
  118. package/packages/runtime-wasm/pkg/package.json +21 -0
  119. package/packages/runtime-wasm/src/hydrate.rs +352 -0
  120. package/packages/runtime-wasm/src/lib.rs +191 -0
  121. package/packages/runtime-wasm/src/render.rs +629 -0
  122. package/packages/runtime-wasm/src/router.rs +298 -0
  123. package/packages/runtime-wasm/src/skeleton.rs +430 -0
  124. package/rfcs/RFC-001-ZERO-DEPENDENCY-RENDERING.md +1446 -0
@@ -0,0 +1,779 @@
1
+ /**
2
+ * ESI Cyrano Whisper Channel
3
+ *
4
+ * Bidirectional ESI directives for ambient Cyrano intelligence:
5
+ * - esi:context - Site drops context INTO the stream (page → Cyrano)
6
+ * - esi:cyrano - Cyrano whispers back (Cyrano → page)
7
+ * - esi:halo - Halo meta-insight (Halo → page adaptation)
8
+ *
9
+ * These directives create "chat exhaust" - every interaction becomes
10
+ * part of the ongoing conversation between user and Cyrano.
11
+ */
12
+
13
+ import type {
14
+ ESIDirective,
15
+ ESIResult,
16
+ UserContext,
17
+ UserTier,
18
+ ESIParams,
19
+ } from './types';
20
+
21
+ // ============================================================================
22
+ // Whisper Channel Types
23
+ // ============================================================================
24
+
25
+ /**
26
+ * Emotional state for context dropping
27
+ */
28
+ export interface EmotionContext {
29
+ /** Primary detected emotion */
30
+ primary?: string;
31
+ /** Valence: negative (-1) to positive (1) */
32
+ valence?: number;
33
+ /** Arousal: calm (0) to excited (1) */
34
+ arousal?: number;
35
+ /** Dominance: submissive (0) to dominant (1) */
36
+ dominance?: number;
37
+ /** Detection source (facial, vocal, behavioral, combined) */
38
+ source?: 'facial' | 'vocal' | 'behavioral' | 'combined';
39
+ /** Confidence in detection (0-1) */
40
+ confidence?: number;
41
+ }
42
+
43
+ /**
44
+ * Behavioral signals for context dropping
45
+ */
46
+ export interface BehaviorContext {
47
+ /** Recent pages visited */
48
+ recentPages?: string[];
49
+ /** Scroll depth on current page (0-1) */
50
+ scrollDepth?: number;
51
+ /** Dwell time on current page (ms) */
52
+ dwellTime?: number;
53
+ /** Whether aimless clicking detected */
54
+ isAimlessClicking?: boolean;
55
+ /** Whether hesitation detected */
56
+ hesitationDetected?: boolean;
57
+ /** Inferred search intent */
58
+ searchingFor?: string;
59
+ /** Interaction velocity (clicks per minute) */
60
+ interactionVelocity?: number;
61
+ }
62
+
63
+ /**
64
+ * Environmental context
65
+ */
66
+ export interface EnvironmentContext {
67
+ /** Weather conditions */
68
+ weather?: {
69
+ temp?: number;
70
+ condition?: string;
71
+ humidity?: number;
72
+ };
73
+ /** UV index */
74
+ uv?: number;
75
+ /** Pollen count */
76
+ pollen?: number;
77
+ /** Air quality index */
78
+ aqi?: number;
79
+ /** Location (city/region) */
80
+ location?: string;
81
+ /** Local hour (0-23) */
82
+ localHour?: number;
83
+ /** Is daylight? */
84
+ isDaylight?: boolean;
85
+ }
86
+
87
+ /**
88
+ * Biometric signals
89
+ */
90
+ export interface BiometricContext {
91
+ /** Heart rate (BPM) */
92
+ heartRate?: number;
93
+ /** Heart rate variability (ms) */
94
+ hrv?: number;
95
+ /** Stress score (0-100) */
96
+ stressScore?: number;
97
+ /** Energy level (0-100) */
98
+ energyLevel?: number;
99
+ /** Sleep score (0-100) */
100
+ sleepScore?: number;
101
+ /** Readiness score (0-100) */
102
+ readinessScore?: number;
103
+ }
104
+
105
+ /**
106
+ * Full session context dropped via esi:context
107
+ */
108
+ export interface SessionContext {
109
+ /** User identifier */
110
+ userId?: string;
111
+ /** User tier */
112
+ tier?: UserTier;
113
+ /** Emotional state (multi-source) */
114
+ emotion?: EmotionContext;
115
+ /** Behavioral signals */
116
+ behavior?: BehaviorContext;
117
+ /** Environmental context */
118
+ environment?: EnvironmentContext;
119
+ /** Biometric signals */
120
+ biometric?: BiometricContext;
121
+ /** Current route */
122
+ currentRoute?: string;
123
+ /** Session start timestamp */
124
+ sessionStartedAt?: number;
125
+ /** Custom metadata */
126
+ metadata?: Record<string, unknown>;
127
+ }
128
+
129
+ /**
130
+ * Cyrano response intent types
131
+ */
132
+ export type CyranoIntent =
133
+ | 'greeting' // Initial greeting
134
+ | 'proactive-check-in' // Unprompted check-in
135
+ | 'supportive-presence' // Gentle acknowledgment
136
+ | 'gentle-nudge' // Soft suggestion
137
+ | 'tool-suggestion' // Recommend a tool
138
+ | 'navigation-hint' // Suggest a route
139
+ | 'intervention' // Protective intervention
140
+ | 'celebration' // Celebrate progress
141
+ | 'reflection' // Prompt reflection
142
+ | 'guidance' // Offer guidance
143
+ | 'farewell' // Session ending
144
+ | 'custom'; // Custom intent
145
+
146
+ /**
147
+ * Cyrano response tone
148
+ */
149
+ export type CyranoTone =
150
+ | 'warm'
151
+ | 'calm'
152
+ | 'encouraging'
153
+ | 'playful'
154
+ | 'professional'
155
+ | 'empathetic'
156
+ | 'neutral';
157
+
158
+ /**
159
+ * Trigger conditions for Cyrano response
160
+ */
161
+ export type CyranoTrigger =
162
+ | `dwell:>${number}s` // Dwell time exceeded
163
+ | `scroll:>${number}` // Scroll depth exceeded
164
+ | `emotion:${string}` // Emotion detected
165
+ | `behavior:aimless` // Aimless clicking
166
+ | `behavior:hesitation` // Hesitation detected
167
+ | `hrv:<${number}` // HRV below threshold
168
+ | `stress:>${number}` // Stress above threshold
169
+ | `session:start` // Session started
170
+ | `session:idle:${number}m` // Idle for N minutes
171
+ | `navigation:to:${string}` // Navigated to route
172
+ | `tool:completed:${string}` // Tool completed
173
+ | `time:${string}` // Time-based trigger
174
+ | 'always' // Always trigger
175
+ | 'never'; // Never trigger (manual only)
176
+
177
+ /**
178
+ * Cyrano whisper configuration
179
+ */
180
+ export interface CyranoWhisperConfig {
181
+ /** Response intent */
182
+ intent: CyranoIntent;
183
+ /** Response tone */
184
+ tone?: CyranoTone;
185
+ /** Trigger condition */
186
+ trigger?: CyranoTrigger;
187
+ /** Fallback text if inference fails */
188
+ fallback?: string;
189
+ /** Suggested tool to surface */
190
+ suggestTool?: string;
191
+ /** Suggested route to navigate */
192
+ suggestRoute?: string;
193
+ /** Auto-accept navigation (for MCP) */
194
+ autoAcceptNavigation?: boolean;
195
+ /** Priority (higher = more important) */
196
+ priority?: number;
197
+ /** Maximum times to trigger per session */
198
+ maxTriggersPerSession?: number;
199
+ /** Cooldown between triggers (seconds) */
200
+ cooldownSeconds?: number;
201
+ /** Whether to speak via TTS */
202
+ speak?: boolean;
203
+ /** Whether to show as caption */
204
+ showCaption?: boolean;
205
+ /** Required user tier */
206
+ requiredTier?: UserTier;
207
+ }
208
+
209
+ /**
210
+ * Halo observation pattern types
211
+ */
212
+ export type HaloObservation =
213
+ | 'anxiety-pattern'
214
+ | 'stress-accumulation'
215
+ | 'emotional-shift'
216
+ | 'behavioral-loop'
217
+ | 'decision-paralysis'
218
+ | 'growth-opportunity'
219
+ | 'values-misalignment'
220
+ | 'blind-spot'
221
+ | 'crisis-indicators'
222
+ | 'positive-momentum'
223
+ | 'synchronicity'
224
+ | 'temporal-echo'
225
+ | 'custom';
226
+
227
+ /**
228
+ * Halo action types
229
+ */
230
+ export type HaloAction =
231
+ | 'suggest-breathing'
232
+ | 'suggest-grounding'
233
+ | 'suggest-journaling'
234
+ | 'suggest-break'
235
+ | 'offer-tool'
236
+ | 'adjust-pace'
237
+ | 'reduce-complexity'
238
+ | 'increase-support'
239
+ | 'celebrate-progress'
240
+ | 'shield-intervention'
241
+ | 'crisis-protocol'
242
+ | 'whisper-to-cyrano'
243
+ | 'adapt-content'
244
+ | 'none';
245
+
246
+ /**
247
+ * Halo meta-insight configuration
248
+ */
249
+ export interface HaloInsightConfig {
250
+ /** Pattern to observe */
251
+ observe: HaloObservation;
252
+ /** Observation window (e.g., '3-pages', '5-minutes', 'session') */
253
+ window?: string;
254
+ /** Action to take when pattern detected */
255
+ action?: HaloAction;
256
+ /** Sensitivity threshold (0-1) */
257
+ sensitivity?: number;
258
+ /** Whether this is a crisis-level observation */
259
+ crisisLevel?: boolean;
260
+ /** Custom insight parameters */
261
+ parameters?: Record<string, unknown>;
262
+ }
263
+
264
+ // ============================================================================
265
+ // Chat Exhaust Types
266
+ // ============================================================================
267
+
268
+ /**
269
+ * Chat exhaust types - everything becomes conversation
270
+ */
271
+ export type ChatExhaustType =
272
+ | 'system' // Session start, env data
273
+ | 'esi:context' // Page drops context
274
+ | 'halo→cyrano' // Halo whispers to Cyrano
275
+ | 'cyrano→page' // Cyrano whispers to page
276
+ | 'behavior' // User action
277
+ | 'emotion' // Emotion shift detected
278
+ | 'user' // Explicit user message
279
+ | 'cyrano' // Cyrano response
280
+ | 'tool:invoke' // Tool invocation
281
+ | 'tool:complete' // Tool completion
282
+ | 'navigation'; // Route change
283
+
284
+ /**
285
+ * Individual chat exhaust entry
286
+ */
287
+ export interface ChatExhaustEntry {
288
+ /** Entry type */
289
+ type: ChatExhaustType;
290
+ /** Timestamp */
291
+ timestamp: number;
292
+ /** Entry content (varies by type) */
293
+ content: unknown;
294
+ /** Whether this was visible to user */
295
+ visible?: boolean;
296
+ /** Source of the entry */
297
+ source?: string;
298
+ }
299
+
300
+ /**
301
+ * ESI result extended with whisper channel data
302
+ */
303
+ export interface ESIWhisperResult extends ESIResult {
304
+ /** Chat exhaust generated by this directive */
305
+ exhaust?: ChatExhaustEntry[];
306
+ /** Suggested tool from Cyrano */
307
+ suggestedTool?: string;
308
+ /** Suggested route from Cyrano */
309
+ suggestedRoute?: string;
310
+ /** Whether to auto-accept navigation */
311
+ autoAcceptNavigation?: boolean;
312
+ /** Halo insights triggered */
313
+ haloInsights?: HaloInsightConfig[];
314
+ }
315
+
316
+ // ============================================================================
317
+ // ESI Context Directive (Page → Cyrano)
318
+ // ============================================================================
319
+
320
+ /**
321
+ * Create an ESI directive to drop session context into the whisper stream.
322
+ * This is how pages communicate their state to Cyrano.
323
+ *
324
+ * @example
325
+ * ```tsx
326
+ * <esiContext
327
+ * emotion={{ primary: 'anxious', valence: -0.3, arousal: 0.7 }}
328
+ * behavior={{ scrollDepth: 0.8, dwellTime: 45000 }}
329
+ * environment={{ weather: { temp: 72, condition: 'sunny' }, uv: 6 }}
330
+ * />
331
+ * ```
332
+ */
333
+ export function esiContext(
334
+ context: SessionContext,
335
+ options: {
336
+ /** Whether to emit as chat exhaust */
337
+ emitExhaust?: boolean;
338
+ /** Custom directive ID */
339
+ id?: string;
340
+ } = {},
341
+ ): ESIDirective {
342
+ const { emitExhaust = true, id } = options;
343
+
344
+ return {
345
+ id:
346
+ id ||
347
+ `esi-context-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`,
348
+ params: {
349
+ model: 'custom',
350
+ custom: {
351
+ type: 'context-drop',
352
+ emitExhaust,
353
+ },
354
+ },
355
+ content: {
356
+ type: 'json',
357
+ value: JSON.stringify(context),
358
+ },
359
+ contextAware: true,
360
+ signals: ['emotion', 'preferences', 'history', 'time', 'device'],
361
+ };
362
+ }
363
+
364
+ // ============================================================================
365
+ // ESI Cyrano Directive (Cyrano → Page)
366
+ // ============================================================================
367
+
368
+ /**
369
+ * Create an ESI directive for Cyrano to whisper to the page.
370
+ * Cyrano responds based on intent, tone, and trigger conditions.
371
+ *
372
+ * @example
373
+ * ```tsx
374
+ * <esiCyrano
375
+ * intent="proactive-check-in"
376
+ * trigger="dwell:>60s"
377
+ * tone="warm"
378
+ * fallback="I'm here if you need me"
379
+ * />
380
+ * ```
381
+ */
382
+ export function esiCyrano(
383
+ config: CyranoWhisperConfig,
384
+ options: Partial<ESIParams> = {},
385
+ ): ESIDirective {
386
+ const {
387
+ intent,
388
+ tone = 'warm',
389
+ trigger = 'always',
390
+ fallback,
391
+ suggestTool,
392
+ suggestRoute,
393
+ autoAcceptNavigation = false,
394
+ priority = 1,
395
+ maxTriggersPerSession,
396
+ cooldownSeconds,
397
+ speak = false,
398
+ showCaption = true,
399
+ requiredTier,
400
+ } = config;
401
+
402
+ // Build system prompt based on intent and tone
403
+ const systemPrompt = buildCyranoSystemPrompt(intent, tone);
404
+
405
+ return {
406
+ id: `esi-cyrano-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`,
407
+ params: {
408
+ model: 'llm',
409
+ system: systemPrompt,
410
+ temperature: 0.7,
411
+ maxTokens: 150,
412
+ fallback,
413
+ custom: {
414
+ type: 'cyrano-whisper',
415
+ intent,
416
+ tone,
417
+ trigger,
418
+ suggestTool,
419
+ suggestRoute,
420
+ autoAcceptNavigation,
421
+ priority,
422
+ maxTriggersPerSession,
423
+ cooldownSeconds,
424
+ speak,
425
+ showCaption,
426
+ },
427
+ ...options,
428
+ },
429
+ content: {
430
+ type: 'template',
431
+ value: buildCyranoPrompt(intent, trigger),
432
+ variables: {
433
+ intent,
434
+ tone,
435
+ trigger,
436
+ },
437
+ },
438
+ contextAware: true,
439
+ signals: ['emotion', 'preferences', 'history', 'time'],
440
+ requiredTier,
441
+ };
442
+ }
443
+
444
+ /**
445
+ * Build system prompt for Cyrano based on intent and tone
446
+ */
447
+ function buildCyranoSystemPrompt(
448
+ intent: CyranoIntent,
449
+ tone: CyranoTone,
450
+ ): string {
451
+ const toneGuide: Record<CyranoTone, string> = {
452
+ warm: 'Be warm, caring, and approachable. Use gentle language.',
453
+ calm: 'Be calm, measured, and reassuring. Use a steady pace.',
454
+ encouraging: 'Be supportive and uplifting. Celebrate small wins.',
455
+ playful: 'Be light-hearted and fun. Use appropriate humor.',
456
+ professional: 'Be clear and direct. Maintain professionalism.',
457
+ empathetic: 'Show deep understanding. Validate feelings.',
458
+ neutral: 'Be balanced and objective. Provide information.',
459
+ };
460
+
461
+ const intentGuide: Record<CyranoIntent, string> = {
462
+ greeting: 'Welcome the user. Make them feel at home.',
463
+ 'proactive-check-in': 'Check in gently. Ask how they are doing.',
464
+ 'supportive-presence': 'Simply acknowledge. Let them know you are here.',
465
+ 'gentle-nudge': 'Suggest an action softly. No pressure.',
466
+ 'tool-suggestion': 'Recommend a tool that might help.',
467
+ 'navigation-hint': 'Suggest exploring another area.',
468
+ intervention: 'Step in supportively. Offer help.',
469
+ celebration: 'Celebrate their progress. Be genuinely happy for them.',
470
+ reflection: 'Invite them to reflect. Ask thoughtful questions.',
471
+ guidance: 'Offer helpful guidance. Be a trusted advisor.',
472
+ farewell: 'Wish them well. Leave the door open.',
473
+ custom: 'Respond appropriately to the context.',
474
+ };
475
+
476
+ return `You are Cyrano, an ambient AI companion. ${toneGuide[tone]} ${intentGuide[intent]}
477
+
478
+ Keep responses brief (1-2 sentences). Be natural and conversational.
479
+ Never start with "I" - use "You" or the situation as the subject.
480
+ Never say "As an AI" or similar phrases.
481
+ Respond to the emotional context provided.`;
482
+ }
483
+
484
+ /**
485
+ * Build prompt for Cyrano based on intent and trigger
486
+ */
487
+ function buildCyranoPrompt(
488
+ intent: CyranoIntent,
489
+ trigger: CyranoTrigger,
490
+ ): string {
491
+ const prompts: Record<CyranoIntent, string> = {
492
+ greeting:
493
+ 'Generate a warm greeting based on the time of day and user context.',
494
+ 'proactive-check-in':
495
+ 'Check in with the user based on their emotional state and behavior.',
496
+ 'supportive-presence':
497
+ "Acknowledge the user's presence and current activity.",
498
+ 'gentle-nudge':
499
+ 'Gently suggest the user might benefit from a particular action.',
500
+ 'tool-suggestion':
501
+ "Suggest a specific tool that could help with the user's current state.",
502
+ 'navigation-hint':
503
+ 'Suggest the user might want to explore a different area.',
504
+ intervention:
505
+ 'Offer supportive intervention based on detected stress or difficulty.',
506
+ celebration: "Celebrate the user's progress or achievement.",
507
+ reflection: 'Invite the user to reflect on their current experience.',
508
+ guidance: "Offer helpful guidance for the user's current situation.",
509
+ farewell: 'Say goodbye warmly, acknowledging the session.',
510
+ custom: 'Respond appropriately to the context provided.',
511
+ };
512
+
513
+ let prompt = prompts[intent] || prompts.custom;
514
+
515
+ // Add trigger context
516
+ if (trigger !== 'always' && trigger !== 'never') {
517
+ prompt += ` The trigger condition is: ${trigger}.`;
518
+ }
519
+
520
+ return prompt;
521
+ }
522
+
523
+ // ============================================================================
524
+ // ESI Halo Directive (Halo Meta-Insight)
525
+ // ============================================================================
526
+
527
+ /**
528
+ * Create an ESI directive for Halo meta-insight observation.
529
+ * Halo observes patterns across pages and whispers to Cyrano.
530
+ *
531
+ * @example
532
+ * ```tsx
533
+ * <esiHalo
534
+ * observe="anxiety-pattern"
535
+ * window="3-pages"
536
+ * action="suggest-breathing"
537
+ * />
538
+ * ```
539
+ */
540
+ export function esiHalo(
541
+ config: HaloInsightConfig,
542
+ options: Partial<ESIParams> = {},
543
+ ): ESIDirective {
544
+ const {
545
+ observe,
546
+ window = 'session',
547
+ action = 'whisper-to-cyrano',
548
+ sensitivity = 0.5,
549
+ crisisLevel = false,
550
+ parameters = {},
551
+ } = config;
552
+
553
+ return {
554
+ id: `esi-halo-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`,
555
+ params: {
556
+ model: 'custom',
557
+ custom: {
558
+ type: 'halo-insight',
559
+ observe,
560
+ window,
561
+ action,
562
+ sensitivity,
563
+ crisisLevel,
564
+ parameters,
565
+ },
566
+ ...options,
567
+ },
568
+ content: {
569
+ type: 'json',
570
+ value: JSON.stringify({
571
+ observation: observe,
572
+ window,
573
+ action,
574
+ sensitivity,
575
+ crisisLevel,
576
+ }),
577
+ },
578
+ contextAware: true,
579
+ signals: ['emotion', 'history', 'time'],
580
+ };
581
+ }
582
+
583
+ // ============================================================================
584
+ // Whisper Channel Processing
585
+ // ============================================================================
586
+
587
+ /**
588
+ * Check if a Cyrano trigger condition is met
589
+ */
590
+ export function evaluateTrigger(
591
+ trigger: CyranoTrigger,
592
+ context: UserContext,
593
+ sessionContext?: SessionContext,
594
+ ): boolean {
595
+ if (trigger === 'always') return true;
596
+ if (trigger === 'never') return false;
597
+
598
+ // Parse trigger string
599
+ const [type, condition] = trigger.split(':');
600
+
601
+ switch (type) {
602
+ case 'dwell': {
603
+ // dwell:>60s
604
+ const match = condition?.match(/>(\d+)s/);
605
+ if (!match) return false;
606
+ const threshold = parseInt(match[1], 10) * 1000;
607
+ const dwellTime = sessionContext?.behavior?.dwellTime || 0;
608
+ return dwellTime > threshold;
609
+ }
610
+
611
+ case 'scroll': {
612
+ // scroll:>0.8
613
+ const match = condition?.match(/>(\d+\.?\d*)/);
614
+ if (!match) return false;
615
+ const threshold = parseFloat(match[1]);
616
+ const scrollDepth = sessionContext?.behavior?.scrollDepth || 0;
617
+ return scrollDepth > threshold;
618
+ }
619
+
620
+ case 'emotion': {
621
+ // emotion:anxious
622
+ const targetEmotion = condition;
623
+ return (
624
+ sessionContext?.emotion?.primary === targetEmotion ||
625
+ context.emotionState?.primary === targetEmotion
626
+ );
627
+ }
628
+
629
+ case 'behavior': {
630
+ // behavior:aimless, behavior:hesitation
631
+ if (condition === 'aimless') {
632
+ return sessionContext?.behavior?.isAimlessClicking === true;
633
+ }
634
+ if (condition === 'hesitation') {
635
+ return sessionContext?.behavior?.hesitationDetected === true;
636
+ }
637
+ return false;
638
+ }
639
+
640
+ case 'hrv': {
641
+ // hrv:<40
642
+ const match = condition?.match(/<(\d+)/);
643
+ if (!match) return false;
644
+ const threshold = parseInt(match[1], 10);
645
+ const hrv = sessionContext?.biometric?.hrv || 100;
646
+ return hrv < threshold;
647
+ }
648
+
649
+ case 'stress': {
650
+ // stress:>70
651
+ const match = condition?.match(/>(\d+)/);
652
+ if (!match) return false;
653
+ const threshold = parseInt(match[1], 10);
654
+ const stress = sessionContext?.biometric?.stressScore || 0;
655
+ return stress > threshold;
656
+ }
657
+
658
+ case 'session': {
659
+ // session:start, session:idle:5m
660
+ if (condition === 'start') {
661
+ return context.isNewSession;
662
+ }
663
+ const idleMatch = condition?.match(/idle:(\d+)m/);
664
+ if (idleMatch) {
665
+ // Would need last activity timestamp to evaluate
666
+ return false;
667
+ }
668
+ return false;
669
+ }
670
+
671
+ case 'navigation': {
672
+ // navigation:to:/breathing
673
+ const targetRoute = condition?.replace('to:', '');
674
+ return sessionContext?.currentRoute === targetRoute;
675
+ }
676
+
677
+ case 'time': {
678
+ // time:morning (6-11), time:evening (18-21)
679
+ const hour = context.localHour;
680
+ if (condition === 'morning') return hour >= 6 && hour < 12;
681
+ if (condition === 'afternoon') return hour >= 12 && hour < 18;
682
+ if (condition === 'evening') return hour >= 18 && hour < 22;
683
+ if (condition === 'night') return hour >= 22 || hour < 6;
684
+ return false;
685
+ }
686
+
687
+ default:
688
+ return false;
689
+ }
690
+ }
691
+
692
+ /**
693
+ * Generate chat exhaust entry from directive result
694
+ */
695
+ export function createExhaustEntry(
696
+ directive: ESIDirective,
697
+ result: ESIResult,
698
+ type: ChatExhaustType,
699
+ ): ChatExhaustEntry {
700
+ return {
701
+ type,
702
+ timestamp: Date.now(),
703
+ content: {
704
+ directiveId: directive.id,
705
+ output: result.output,
706
+ model: result.model,
707
+ success: result.success,
708
+ latencyMs: result.latencyMs,
709
+ },
710
+ visible: type === 'cyrano' || type === 'user',
711
+ source: directive.params.model,
712
+ };
713
+ }
714
+
715
+ // ============================================================================
716
+ // Tool Suggestion Helpers
717
+ // ============================================================================
718
+
719
+ /**
720
+ * Common tool suggestions based on context
721
+ */
722
+ export const CYRANO_TOOL_SUGGESTIONS: Record<
723
+ string,
724
+ {
725
+ triggers: CyranoTrigger[];
726
+ tool: string;
727
+ reason: string;
728
+ }
729
+ > = {
730
+ breathing: {
731
+ triggers: ['stress:>70', 'hrv:<40', 'emotion:anxious'],
732
+ tool: 'breathing/4-7-8',
733
+ reason: 'You seem stressed - a breathing exercise might help',
734
+ },
735
+ grounding: {
736
+ triggers: ['emotion:overwhelmed', 'behavior:aimless'],
737
+ tool: 'grounding/5-4-3-2-1',
738
+ reason: 'A grounding exercise can help center you',
739
+ },
740
+ journaling: {
741
+ triggers: ['dwell:>120s', 'emotion:reflective'],
742
+ tool: 'journaling/freeform',
743
+ reason: "Would you like to write about what's on your mind?",
744
+ },
745
+ insights: {
746
+ triggers: ['navigation:to:/insights', 'dwell:>60s'],
747
+ tool: 'insights/dashboard',
748
+ reason: 'Your recent patterns are ready to explore',
749
+ },
750
+ };
751
+
752
+ /**
753
+ * Get tool suggestions based on session context
754
+ */
755
+ export function getToolSuggestions(
756
+ context: UserContext,
757
+ sessionContext?: SessionContext,
758
+ ): Array<{ tool: string; reason: string; priority: number }> {
759
+ const suggestions: Array<{ tool: string; reason: string; priority: number }> =
760
+ [];
761
+
762
+ for (const [, config] of Object.entries(CYRANO_TOOL_SUGGESTIONS)) {
763
+ for (const trigger of config.triggers) {
764
+ if (evaluateTrigger(trigger, context, sessionContext)) {
765
+ suggestions.push({
766
+ tool: config.tool,
767
+ reason: config.reason,
768
+ priority:
769
+ trigger.startsWith('stress') || trigger.startsWith('hrv') ? 2 : 1,
770
+ });
771
+ break; // One suggestion per tool
772
+ }
773
+ }
774
+ }
775
+
776
+ return suggestions.sort((a, b) => b.priority - a.priority);
777
+ }
778
+
779
+ // All functions are exported at their definition site