@usejarvis/brain 0.1.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 (266) hide show
  1. package/LICENSE +153 -0
  2. package/README.md +278 -0
  3. package/bin/jarvis.ts +413 -0
  4. package/package.json +74 -0
  5. package/scripts/ensure-bun.cjs +8 -0
  6. package/src/actions/README.md +421 -0
  7. package/src/actions/app-control/desktop-controller.test.ts +26 -0
  8. package/src/actions/app-control/desktop-controller.ts +438 -0
  9. package/src/actions/app-control/interface.ts +64 -0
  10. package/src/actions/app-control/linux.ts +273 -0
  11. package/src/actions/app-control/macos.ts +54 -0
  12. package/src/actions/app-control/sidecar-launcher.test.ts +23 -0
  13. package/src/actions/app-control/sidecar-launcher.ts +286 -0
  14. package/src/actions/app-control/windows.ts +44 -0
  15. package/src/actions/browser/cdp.ts +138 -0
  16. package/src/actions/browser/chrome-launcher.ts +252 -0
  17. package/src/actions/browser/session.ts +437 -0
  18. package/src/actions/browser/stealth.ts +49 -0
  19. package/src/actions/index.ts +20 -0
  20. package/src/actions/terminal/executor.ts +157 -0
  21. package/src/actions/terminal/wsl-bridge.ts +126 -0
  22. package/src/actions/test.ts +93 -0
  23. package/src/actions/tools/agents.ts +321 -0
  24. package/src/actions/tools/builtin.ts +846 -0
  25. package/src/actions/tools/commitments.ts +192 -0
  26. package/src/actions/tools/content.ts +217 -0
  27. package/src/actions/tools/delegate.ts +147 -0
  28. package/src/actions/tools/desktop.test.ts +55 -0
  29. package/src/actions/tools/desktop.ts +305 -0
  30. package/src/actions/tools/goals.ts +376 -0
  31. package/src/actions/tools/local-tools-guard.ts +20 -0
  32. package/src/actions/tools/registry.ts +171 -0
  33. package/src/actions/tools/research.ts +111 -0
  34. package/src/actions/tools/sidecar-list.ts +57 -0
  35. package/src/actions/tools/sidecar-route.ts +105 -0
  36. package/src/actions/tools/workflows.ts +216 -0
  37. package/src/agents/agent.ts +132 -0
  38. package/src/agents/delegation.ts +107 -0
  39. package/src/agents/hierarchy.ts +113 -0
  40. package/src/agents/index.ts +19 -0
  41. package/src/agents/messaging.ts +125 -0
  42. package/src/agents/orchestrator.ts +576 -0
  43. package/src/agents/role-discovery.ts +61 -0
  44. package/src/agents/sub-agent-runner.ts +307 -0
  45. package/src/agents/task-manager.ts +151 -0
  46. package/src/authority/approval-delivery.ts +59 -0
  47. package/src/authority/approval.ts +196 -0
  48. package/src/authority/audit.ts +158 -0
  49. package/src/authority/authority.test.ts +519 -0
  50. package/src/authority/deferred-executor.ts +103 -0
  51. package/src/authority/emergency.ts +66 -0
  52. package/src/authority/engine.ts +297 -0
  53. package/src/authority/index.ts +12 -0
  54. package/src/authority/learning.ts +111 -0
  55. package/src/authority/tool-action-map.ts +74 -0
  56. package/src/awareness/analytics.ts +466 -0
  57. package/src/awareness/awareness.test.ts +332 -0
  58. package/src/awareness/capture-engine.ts +305 -0
  59. package/src/awareness/context-graph.ts +130 -0
  60. package/src/awareness/context-tracker.ts +349 -0
  61. package/src/awareness/index.ts +25 -0
  62. package/src/awareness/intelligence.ts +321 -0
  63. package/src/awareness/ocr-engine.ts +88 -0
  64. package/src/awareness/service.ts +528 -0
  65. package/src/awareness/struggle-detector.ts +342 -0
  66. package/src/awareness/suggestion-engine.ts +476 -0
  67. package/src/awareness/types.ts +201 -0
  68. package/src/cli/autostart.ts +241 -0
  69. package/src/cli/deps.ts +449 -0
  70. package/src/cli/doctor.ts +230 -0
  71. package/src/cli/helpers.ts +401 -0
  72. package/src/cli/onboard.ts +580 -0
  73. package/src/comms/README.md +329 -0
  74. package/src/comms/auth-error.html +48 -0
  75. package/src/comms/channels/discord.ts +228 -0
  76. package/src/comms/channels/signal.ts +56 -0
  77. package/src/comms/channels/telegram.ts +316 -0
  78. package/src/comms/channels/whatsapp.ts +60 -0
  79. package/src/comms/channels.test.ts +173 -0
  80. package/src/comms/desktop-notify.ts +114 -0
  81. package/src/comms/example.ts +129 -0
  82. package/src/comms/index.ts +129 -0
  83. package/src/comms/streaming.ts +142 -0
  84. package/src/comms/voice.test.ts +152 -0
  85. package/src/comms/voice.ts +291 -0
  86. package/src/comms/websocket.test.ts +409 -0
  87. package/src/comms/websocket.ts +473 -0
  88. package/src/config/README.md +387 -0
  89. package/src/config/index.ts +6 -0
  90. package/src/config/loader.test.ts +137 -0
  91. package/src/config/loader.ts +142 -0
  92. package/src/config/types.ts +260 -0
  93. package/src/daemon/README.md +232 -0
  94. package/src/daemon/agent-service-interface.ts +9 -0
  95. package/src/daemon/agent-service.ts +600 -0
  96. package/src/daemon/api-routes.ts +2119 -0
  97. package/src/daemon/background-agent-service.ts +396 -0
  98. package/src/daemon/background-agent.test.ts +78 -0
  99. package/src/daemon/channel-service.ts +201 -0
  100. package/src/daemon/commitment-executor.ts +297 -0
  101. package/src/daemon/event-classifier.ts +239 -0
  102. package/src/daemon/event-coalescer.ts +123 -0
  103. package/src/daemon/event-reactor.ts +214 -0
  104. package/src/daemon/health.ts +220 -0
  105. package/src/daemon/index.ts +1004 -0
  106. package/src/daemon/llm-settings.ts +316 -0
  107. package/src/daemon/observer-service.ts +150 -0
  108. package/src/daemon/pid.ts +98 -0
  109. package/src/daemon/research-queue.ts +155 -0
  110. package/src/daemon/services.ts +175 -0
  111. package/src/daemon/ws-service.ts +788 -0
  112. package/src/goals/accountability.ts +240 -0
  113. package/src/goals/awareness-bridge.ts +185 -0
  114. package/src/goals/estimator.ts +185 -0
  115. package/src/goals/events.ts +28 -0
  116. package/src/goals/goals.test.ts +400 -0
  117. package/src/goals/integration.test.ts +329 -0
  118. package/src/goals/nl-builder.test.ts +220 -0
  119. package/src/goals/nl-builder.ts +256 -0
  120. package/src/goals/rhythm.test.ts +177 -0
  121. package/src/goals/rhythm.ts +275 -0
  122. package/src/goals/service.test.ts +135 -0
  123. package/src/goals/service.ts +348 -0
  124. package/src/goals/types.ts +106 -0
  125. package/src/goals/workflow-bridge.ts +96 -0
  126. package/src/integrations/google-api.ts +134 -0
  127. package/src/integrations/google-auth.ts +175 -0
  128. package/src/llm/README.md +291 -0
  129. package/src/llm/anthropic.ts +386 -0
  130. package/src/llm/gemini.ts +371 -0
  131. package/src/llm/index.ts +19 -0
  132. package/src/llm/manager.ts +153 -0
  133. package/src/llm/ollama.ts +307 -0
  134. package/src/llm/openai.ts +350 -0
  135. package/src/llm/provider.test.ts +231 -0
  136. package/src/llm/provider.ts +60 -0
  137. package/src/llm/test.ts +87 -0
  138. package/src/observers/README.md +278 -0
  139. package/src/observers/calendar.ts +113 -0
  140. package/src/observers/clipboard.ts +136 -0
  141. package/src/observers/email.ts +109 -0
  142. package/src/observers/example.ts +58 -0
  143. package/src/observers/file-watcher.ts +124 -0
  144. package/src/observers/index.ts +159 -0
  145. package/src/observers/notifications.ts +197 -0
  146. package/src/observers/observers.test.ts +203 -0
  147. package/src/observers/processes.ts +225 -0
  148. package/src/personality/README.md +61 -0
  149. package/src/personality/adapter.ts +196 -0
  150. package/src/personality/index.ts +20 -0
  151. package/src/personality/learner.ts +209 -0
  152. package/src/personality/model.ts +132 -0
  153. package/src/personality/personality.test.ts +236 -0
  154. package/src/roles/README.md +252 -0
  155. package/src/roles/authority.ts +119 -0
  156. package/src/roles/example-usage.ts +198 -0
  157. package/src/roles/index.ts +42 -0
  158. package/src/roles/loader.ts +143 -0
  159. package/src/roles/prompt-builder.ts +194 -0
  160. package/src/roles/test-multi.ts +102 -0
  161. package/src/roles/test-role.yaml +77 -0
  162. package/src/roles/test-utils.ts +93 -0
  163. package/src/roles/test.ts +106 -0
  164. package/src/roles/tool-guide.ts +190 -0
  165. package/src/roles/types.ts +36 -0
  166. package/src/roles/utils.ts +200 -0
  167. package/src/scripts/google-setup.ts +168 -0
  168. package/src/sidecar/connection.ts +179 -0
  169. package/src/sidecar/index.ts +6 -0
  170. package/src/sidecar/manager.ts +542 -0
  171. package/src/sidecar/protocol.ts +85 -0
  172. package/src/sidecar/rpc.ts +161 -0
  173. package/src/sidecar/scheduler.ts +136 -0
  174. package/src/sidecar/types.ts +112 -0
  175. package/src/sidecar/validator.ts +144 -0
  176. package/src/vault/README.md +110 -0
  177. package/src/vault/awareness.ts +341 -0
  178. package/src/vault/commitments.ts +299 -0
  179. package/src/vault/content-pipeline.ts +260 -0
  180. package/src/vault/conversations.ts +173 -0
  181. package/src/vault/entities.ts +180 -0
  182. package/src/vault/extractor.test.ts +356 -0
  183. package/src/vault/extractor.ts +345 -0
  184. package/src/vault/facts.ts +190 -0
  185. package/src/vault/goals.ts +477 -0
  186. package/src/vault/index.ts +87 -0
  187. package/src/vault/keychain.ts +99 -0
  188. package/src/vault/observations.ts +115 -0
  189. package/src/vault/relationships.ts +178 -0
  190. package/src/vault/retrieval.test.ts +126 -0
  191. package/src/vault/retrieval.ts +227 -0
  192. package/src/vault/schema.ts +658 -0
  193. package/src/vault/settings.ts +38 -0
  194. package/src/vault/vectors.ts +92 -0
  195. package/src/vault/workflows.ts +403 -0
  196. package/src/workflows/auto-suggest.ts +290 -0
  197. package/src/workflows/engine.ts +366 -0
  198. package/src/workflows/events.ts +24 -0
  199. package/src/workflows/executor.ts +207 -0
  200. package/src/workflows/nl-builder.ts +198 -0
  201. package/src/workflows/nodes/actions/agent-task.ts +73 -0
  202. package/src/workflows/nodes/actions/calendar-action.ts +85 -0
  203. package/src/workflows/nodes/actions/code-execution.ts +73 -0
  204. package/src/workflows/nodes/actions/discord.ts +77 -0
  205. package/src/workflows/nodes/actions/file-write.ts +73 -0
  206. package/src/workflows/nodes/actions/gmail.ts +69 -0
  207. package/src/workflows/nodes/actions/http-request.ts +117 -0
  208. package/src/workflows/nodes/actions/notification.ts +85 -0
  209. package/src/workflows/nodes/actions/run-tool.ts +55 -0
  210. package/src/workflows/nodes/actions/send-message.ts +82 -0
  211. package/src/workflows/nodes/actions/shell-command.ts +76 -0
  212. package/src/workflows/nodes/actions/telegram.ts +60 -0
  213. package/src/workflows/nodes/builtin.ts +119 -0
  214. package/src/workflows/nodes/error/error-handler.ts +37 -0
  215. package/src/workflows/nodes/error/fallback.ts +47 -0
  216. package/src/workflows/nodes/error/retry.ts +82 -0
  217. package/src/workflows/nodes/logic/delay.ts +42 -0
  218. package/src/workflows/nodes/logic/if-else.ts +41 -0
  219. package/src/workflows/nodes/logic/loop.ts +90 -0
  220. package/src/workflows/nodes/logic/merge.ts +38 -0
  221. package/src/workflows/nodes/logic/race.ts +40 -0
  222. package/src/workflows/nodes/logic/switch.ts +59 -0
  223. package/src/workflows/nodes/logic/template-render.ts +53 -0
  224. package/src/workflows/nodes/logic/variable-get.ts +37 -0
  225. package/src/workflows/nodes/logic/variable-set.ts +59 -0
  226. package/src/workflows/nodes/registry.ts +99 -0
  227. package/src/workflows/nodes/transform/aggregate.ts +99 -0
  228. package/src/workflows/nodes/transform/csv-parse.ts +70 -0
  229. package/src/workflows/nodes/transform/json-parse.ts +63 -0
  230. package/src/workflows/nodes/transform/map-filter.ts +84 -0
  231. package/src/workflows/nodes/transform/regex-match.ts +89 -0
  232. package/src/workflows/nodes/triggers/calendar.ts +33 -0
  233. package/src/workflows/nodes/triggers/clipboard.ts +32 -0
  234. package/src/workflows/nodes/triggers/cron.ts +40 -0
  235. package/src/workflows/nodes/triggers/email.ts +40 -0
  236. package/src/workflows/nodes/triggers/file-change.ts +45 -0
  237. package/src/workflows/nodes/triggers/git.ts +46 -0
  238. package/src/workflows/nodes/triggers/manual.ts +23 -0
  239. package/src/workflows/nodes/triggers/poll.ts +81 -0
  240. package/src/workflows/nodes/triggers/process.ts +44 -0
  241. package/src/workflows/nodes/triggers/screen-event.ts +37 -0
  242. package/src/workflows/nodes/triggers/webhook.ts +39 -0
  243. package/src/workflows/safe-eval.ts +139 -0
  244. package/src/workflows/template.ts +118 -0
  245. package/src/workflows/triggers/cron.ts +311 -0
  246. package/src/workflows/triggers/manager.ts +285 -0
  247. package/src/workflows/triggers/observer-bridge.ts +172 -0
  248. package/src/workflows/triggers/poller.ts +201 -0
  249. package/src/workflows/triggers/screen-condition.ts +218 -0
  250. package/src/workflows/triggers/triggers.test.ts +740 -0
  251. package/src/workflows/triggers/webhook.ts +191 -0
  252. package/src/workflows/types.ts +133 -0
  253. package/src/workflows/variables.ts +72 -0
  254. package/src/workflows/workflows.test.ts +383 -0
  255. package/src/workflows/yaml.ts +104 -0
  256. package/ui/dist/index-j75njzc1.css +1199 -0
  257. package/ui/dist/index-p2zh407q.js +80603 -0
  258. package/ui/dist/index.html +13 -0
  259. package/ui/public/openwakeword/models/embedding_model.onnx +0 -0
  260. package/ui/public/openwakeword/models/hey_jarvis_v0.1.onnx +0 -0
  261. package/ui/public/openwakeword/models/melspectrogram.onnx +0 -0
  262. package/ui/public/openwakeword/models/silero_vad.onnx +0 -0
  263. package/ui/public/ort/ort-wasm-simd-threaded.jsep.mjs +106 -0
  264. package/ui/public/ort/ort-wasm-simd-threaded.jsep.wasm +0 -0
  265. package/ui/public/ort/ort-wasm-simd-threaded.mjs +59 -0
  266. package/ui/public/ort/ort-wasm-simd-threaded.wasm +0 -0
@@ -0,0 +1,113 @@
1
+ /**
2
+ * CalendarSync — Google Calendar Observer
3
+ *
4
+ * Polls Calendar API every 2 minutes with a 30-minute look-ahead.
5
+ * Tracks announced event IDs to avoid duplicate alerts.
6
+ * Graceful: if no Google tokens, logs warning and stays no-op.
7
+ */
8
+
9
+ import type { Observer, ObserverEventHandler } from './index';
10
+ import type { GoogleAuth } from '../integrations/google-auth.ts';
11
+ import { listUpcomingEvents } from '../integrations/google-api.ts';
12
+
13
+ const POLL_INTERVAL_MS = 2 * 60_000; // 2 minutes
14
+ const LOOK_AHEAD_MS = 30 * 60_000; // 30 minutes
15
+
16
+ export class CalendarSync implements Observer {
17
+ name = 'calendar';
18
+ private running = false;
19
+ private handler: ObserverEventHandler | null = null;
20
+ private pollTimer: Timer | null = null;
21
+ private googleAuth: GoogleAuth | null;
22
+ private announcedEventIds: Set<string> = new Set();
23
+ private calendarId: string;
24
+
25
+ constructor(googleAuth?: GoogleAuth, calendarId?: string) {
26
+ this.googleAuth = googleAuth ?? null;
27
+ this.calendarId = calendarId ?? 'primary';
28
+ }
29
+
30
+ async start(): Promise<void> {
31
+ this.running = true;
32
+
33
+ if (!this.googleAuth || !this.googleAuth.isAuthenticated()) {
34
+ console.log('[calendar] No Google auth configured — calendar monitoring disabled');
35
+ console.log('[calendar] Run: bun run src/scripts/google-setup.ts to set up Calendar');
36
+ return;
37
+ }
38
+
39
+ console.log('[calendar] Observer started — polling Calendar every 2min (30min look-ahead)');
40
+
41
+ // Initial poll
42
+ this.poll();
43
+
44
+ // Set up recurring poll
45
+ this.pollTimer = setInterval(() => this.poll(), POLL_INTERVAL_MS);
46
+ }
47
+
48
+ async stop(): Promise<void> {
49
+ this.running = false;
50
+
51
+ if (this.pollTimer) {
52
+ clearInterval(this.pollTimer);
53
+ this.pollTimer = null;
54
+ }
55
+
56
+ console.log('[calendar] Observer stopped');
57
+ }
58
+
59
+ isRunning(): boolean {
60
+ return this.running;
61
+ }
62
+
63
+ onEvent(handler: ObserverEventHandler): void {
64
+ this.handler = handler;
65
+ }
66
+
67
+ private async poll(): Promise<void> {
68
+ if (!this.googleAuth || !this.handler) return;
69
+
70
+ try {
71
+ const accessToken = await this.googleAuth.getAccessToken();
72
+ const now = new Date();
73
+ const later = new Date(now.getTime() + LOOK_AHEAD_MS);
74
+
75
+ const events = await listUpcomingEvents(
76
+ accessToken,
77
+ this.calendarId,
78
+ now.toISOString(),
79
+ later.toISOString(),
80
+ 10
81
+ );
82
+
83
+ for (const event of events) {
84
+ // Skip already-announced events
85
+ if (this.announcedEventIds.has(event.id)) continue;
86
+ this.announcedEventIds.add(event.id);
87
+
88
+ this.handler({
89
+ type: 'calendar',
90
+ data: {
91
+ id: event.id,
92
+ summary: event.summary,
93
+ description: event.description,
94
+ start: event.start,
95
+ end: event.end,
96
+ location: event.location,
97
+ attendees: event.attendees,
98
+ htmlLink: event.htmlLink,
99
+ },
100
+ timestamp: Date.now(),
101
+ });
102
+ }
103
+
104
+ // Cap announcedEventIds to prevent unbounded growth
105
+ if (this.announcedEventIds.size > 500) {
106
+ const arr = Array.from(this.announcedEventIds);
107
+ this.announcedEventIds = new Set(arr.slice(arr.length - 250));
108
+ }
109
+ } catch (err) {
110
+ console.error('[calendar] Poll error:', err);
111
+ }
112
+ }
113
+ }
@@ -0,0 +1,136 @@
1
+ /**
2
+ * ClipboardMonitor - Monitors clipboard changes
3
+ *
4
+ * Polls the system clipboard at regular intervals and emits events when content changes.
5
+ * Uses platform-specific commands to read clipboard content.
6
+ */
7
+
8
+ import type { Observer, ObserverEvent, ObserverEventHandler } from './index';
9
+
10
+ export class ClipboardMonitor implements Observer {
11
+ name = 'clipboard';
12
+ private interval: Timer | null = null;
13
+ private lastContent: string = '';
14
+ private handler: ObserverEventHandler | null = null;
15
+ private running = false;
16
+ private pollMs: number;
17
+
18
+ constructor(pollMs: number = 1000) {
19
+ this.pollMs = pollMs;
20
+ }
21
+
22
+ async start(): Promise<void> {
23
+ if (this.running) {
24
+ console.log('[clipboard] Already running');
25
+ return;
26
+ }
27
+
28
+ console.log(`[clipboard] Starting clipboard monitoring (polling every ${this.pollMs}ms)...`);
29
+
30
+ // Initialize with current clipboard content
31
+ try {
32
+ this.lastContent = await this.readClipboard();
33
+ } catch (error) {
34
+ console.error('[clipboard] Failed to read initial clipboard:', error);
35
+ this.lastContent = '';
36
+ }
37
+
38
+ // Start polling
39
+ this.interval = setInterval(async () => {
40
+ try {
41
+ const content = await this.readClipboard();
42
+
43
+ if (content !== this.lastContent) {
44
+ this.lastContent = content;
45
+
46
+ if (this.handler) {
47
+ const event: ObserverEvent = {
48
+ type: 'clipboard',
49
+ data: {
50
+ content,
51
+ length: content.length,
52
+ },
53
+ timestamp: Date.now(),
54
+ };
55
+
56
+ this.handler(event);
57
+ }
58
+ }
59
+ } catch (error) {
60
+ // Silent fail on read errors to avoid spam
61
+ // console.error('[clipboard] Failed to read clipboard:', error);
62
+ }
63
+ }, this.pollMs);
64
+
65
+ this.running = true;
66
+ }
67
+
68
+ async stop(): Promise<void> {
69
+ if (!this.running) {
70
+ return;
71
+ }
72
+
73
+ console.log('[clipboard] Stopping clipboard monitoring...');
74
+
75
+ if (this.interval) {
76
+ clearInterval(this.interval);
77
+ this.interval = null;
78
+ }
79
+
80
+ this.running = false;
81
+ }
82
+
83
+ isRunning(): boolean {
84
+ return this.running;
85
+ }
86
+
87
+ onEvent(handler: ObserverEventHandler): void {
88
+ this.handler = handler;
89
+ }
90
+
91
+ /**
92
+ * Read clipboard content using platform-specific commands
93
+ */
94
+ private async readClipboard(): Promise<string> {
95
+ const platform = process.platform;
96
+
97
+ try {
98
+ let result: { stdout: Buffer; stderr: Buffer };
99
+
100
+ if (platform === 'linux') {
101
+ // Try xclip first
102
+ try {
103
+ result = await Bun.$`xclip -selection clipboard -o`.quiet();
104
+ return result.stdout.toString().trim();
105
+ } catch {
106
+ // Fall back to xsel
107
+ try {
108
+ result = await Bun.$`xsel --clipboard --output`.quiet();
109
+ return result.stdout.toString().trim();
110
+ } catch {
111
+ // Check if we're in WSL and can use PowerShell
112
+ try {
113
+ result = await Bun.$`powershell.exe Get-Clipboard`.quiet();
114
+ return result.stdout.toString().trim();
115
+ } catch {
116
+ throw new Error('No clipboard tool available (tried xclip, xsel, powershell.exe)');
117
+ }
118
+ }
119
+ }
120
+ } else if (platform === 'darwin') {
121
+ // macOS
122
+ result = await Bun.$`pbpaste`.quiet();
123
+ return result.stdout.toString().trim();
124
+ } else if (platform === 'win32') {
125
+ // Windows
126
+ result = await Bun.$`powershell.exe Get-Clipboard`.quiet();
127
+ return result.stdout.toString().trim();
128
+ } else {
129
+ throw new Error(`Unsupported platform: ${platform}`);
130
+ }
131
+ } catch (error) {
132
+ // Return empty string on error
133
+ return '';
134
+ }
135
+ }
136
+ }
@@ -0,0 +1,109 @@
1
+ /**
2
+ * EmailSync — Gmail Observer
3
+ *
4
+ * Polls Gmail API every 60s for unread messages.
5
+ * Tracks seen message IDs to avoid re-emitting.
6
+ * Fetches detail (subject, from, snippet) for new messages.
7
+ * Graceful: if no Google tokens, logs warning and stays no-op.
8
+ */
9
+
10
+ import type { Observer, ObserverEventHandler } from './index';
11
+ import type { GoogleAuth } from '../integrations/google-auth.ts';
12
+ import { listUnreadEmails, getEmailDetail } from '../integrations/google-api.ts';
13
+
14
+ const POLL_INTERVAL_MS = 60_000; // 60 seconds
15
+
16
+ export class EmailSync implements Observer {
17
+ name = 'email';
18
+ private running = false;
19
+ private handler: ObserverEventHandler | null = null;
20
+ private pollTimer: Timer | null = null;
21
+ private googleAuth: GoogleAuth | null;
22
+ private seenMessageIds: Set<string> = new Set();
23
+
24
+ constructor(googleAuth?: GoogleAuth) {
25
+ this.googleAuth = googleAuth ?? null;
26
+ }
27
+
28
+ async start(): Promise<void> {
29
+ this.running = true;
30
+
31
+ if (!this.googleAuth || !this.googleAuth.isAuthenticated()) {
32
+ console.log('[email] No Google auth configured — email monitoring disabled');
33
+ console.log('[email] Run: bun run src/scripts/google-setup.ts to set up Gmail');
34
+ return;
35
+ }
36
+
37
+ console.log('[email] Observer started — polling Gmail every 60s');
38
+
39
+ // Initial poll
40
+ this.poll();
41
+
42
+ // Set up recurring poll
43
+ this.pollTimer = setInterval(() => this.poll(), POLL_INTERVAL_MS);
44
+ }
45
+
46
+ async stop(): Promise<void> {
47
+ this.running = false;
48
+
49
+ if (this.pollTimer) {
50
+ clearInterval(this.pollTimer);
51
+ this.pollTimer = null;
52
+ }
53
+
54
+ console.log('[email] Observer stopped');
55
+ }
56
+
57
+ isRunning(): boolean {
58
+ return this.running;
59
+ }
60
+
61
+ onEvent(handler: ObserverEventHandler): void {
62
+ this.handler = handler;
63
+ }
64
+
65
+ private async poll(): Promise<void> {
66
+ if (!this.googleAuth || !this.handler) return;
67
+
68
+ try {
69
+ const accessToken = await this.googleAuth.getAccessToken();
70
+ const messages = await listUnreadEmails(accessToken, 10);
71
+
72
+ for (const msg of messages) {
73
+ // Skip already-seen messages
74
+ if (this.seenMessageIds.has(msg.id)) continue;
75
+ this.seenMessageIds.add(msg.id);
76
+
77
+ // Fetch detail
78
+ try {
79
+ const detail = await getEmailDetail(accessToken, msg.id);
80
+
81
+ this.handler({
82
+ type: 'email',
83
+ data: {
84
+ id: detail.id,
85
+ threadId: detail.threadId,
86
+ subject: detail.subject,
87
+ from: detail.from,
88
+ to: detail.to,
89
+ date: detail.date,
90
+ snippet: detail.snippet,
91
+ labels: detail.labels,
92
+ },
93
+ timestamp: Date.now(),
94
+ });
95
+ } catch (err) {
96
+ console.error(`[email] Failed to get detail for ${msg.id}:`, err);
97
+ }
98
+ }
99
+
100
+ // Cap seenMessageIds to prevent unbounded growth
101
+ if (this.seenMessageIds.size > 1000) {
102
+ const arr = Array.from(this.seenMessageIds);
103
+ this.seenMessageIds = new Set(arr.slice(arr.length - 500));
104
+ }
105
+ } catch (err) {
106
+ console.error('[email] Poll error:', err);
107
+ }
108
+ }
109
+ }
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Example usage of the Observer Layer
3
+ *
4
+ * This demonstrates how to set up and use the ObserverManager with various observers.
5
+ */
6
+
7
+ import {
8
+ ObserverManager,
9
+ FileWatcher,
10
+ ClipboardMonitor,
11
+ ProcessMonitor,
12
+ NotificationListener,
13
+ CalendarSync,
14
+ EmailSync,
15
+ type ObserverEvent,
16
+ } from './index';
17
+
18
+ async function main() {
19
+ // Create the manager
20
+ const manager = new ObserverManager();
21
+
22
+ // Register observers
23
+ manager.register(new FileWatcher([import.meta.dir + '/../'])); // Watch src directory
24
+ manager.register(new ClipboardMonitor(2000)); // Poll clipboard every 2 seconds
25
+ manager.register(new ProcessMonitor(10000)); // Poll processes every 10 seconds
26
+ manager.register(new NotificationListener()); // Stub
27
+ manager.register(new CalendarSync()); // Stub
28
+ manager.register(new EmailSync()); // Stub
29
+
30
+ // Set up event handler (this would typically be the Vault's ingestion function)
31
+ manager.setEventHandler((event: ObserverEvent) => {
32
+ console.log(`[EVENT] ${event.type} at ${new Date(event.timestamp).toISOString()}`);
33
+ console.log(' Data:', JSON.stringify(event.data, null, 2));
34
+ });
35
+
36
+ // Start all observers
37
+ await manager.startAll();
38
+
39
+ // Get status
40
+ console.log('\n[STATUS] Observer running status:', manager.getStatus());
41
+
42
+ // Run for 30 seconds, then stop
43
+ console.log('\n[INFO] Running observers for 30 seconds...\n');
44
+
45
+ await new Promise(resolve => setTimeout(resolve, 30000));
46
+
47
+ // Stop all observers
48
+ await manager.stopAll();
49
+
50
+ console.log('\n[INFO] Example complete');
51
+ }
52
+
53
+ // Run example if executed directly
54
+ if (import.meta.main) {
55
+ main().catch(console.error);
56
+ }
57
+
58
+ export { main };
@@ -0,0 +1,124 @@
1
+ /**
2
+ * FileWatcher - Monitors file system changes
3
+ *
4
+ * Watches specified directories recursively and emits events when files change.
5
+ * Includes debouncing to avoid duplicate rapid-fire events.
6
+ */
7
+
8
+ import { watch, type FSWatcher } from 'node:fs';
9
+ import type { Observer, ObserverEvent, ObserverEventHandler } from './index';
10
+
11
+ type DebounceEntry = {
12
+ path: string;
13
+ timestamp: number;
14
+ };
15
+
16
+ export class FileWatcher implements Observer {
17
+ name = 'file-watcher';
18
+ private watchers: FSWatcher[] = [];
19
+ private paths: string[];
20
+ private handler: ObserverEventHandler | null = null;
21
+ private running = false;
22
+ private recentChanges: Map<string, number> = new Map();
23
+ private debounceMs = 100; // Ignore duplicate events within 100ms
24
+
25
+ constructor(paths: string[]) {
26
+ this.paths = paths;
27
+ }
28
+
29
+ async start(): Promise<void> {
30
+ if (this.running) {
31
+ console.log('[file-watcher] Already running');
32
+ return;
33
+ }
34
+
35
+ console.log('[file-watcher] Starting file system monitoring...');
36
+
37
+ for (const path of this.paths) {
38
+ try {
39
+ const watcher = watch(path, { recursive: true }, (eventType, filename) => {
40
+ this.handleFileEvent(path, eventType, filename);
41
+ });
42
+
43
+ watcher.on('error', (error) => {
44
+ console.error(`[file-watcher] Error watching ${path}:`, error);
45
+ });
46
+
47
+ this.watchers.push(watcher);
48
+ console.log(`[file-watcher] Watching: ${path}`);
49
+ } catch (error) {
50
+ console.error(`[file-watcher] Failed to watch ${path}:`, error);
51
+ }
52
+ }
53
+
54
+ this.running = true;
55
+ }
56
+
57
+ async stop(): Promise<void> {
58
+ if (!this.running) {
59
+ return;
60
+ }
61
+
62
+ console.log('[file-watcher] Stopping file system monitoring...');
63
+
64
+ for (const watcher of this.watchers) {
65
+ watcher.close();
66
+ }
67
+
68
+ this.watchers = [];
69
+ this.recentChanges.clear();
70
+ this.running = false;
71
+ }
72
+
73
+ isRunning(): boolean {
74
+ return this.running;
75
+ }
76
+
77
+ onEvent(handler: ObserverEventHandler): void {
78
+ this.handler = handler;
79
+ }
80
+
81
+ private handleFileEvent(
82
+ basePath: string,
83
+ eventType: 'rename' | 'change',
84
+ filename: string | null
85
+ ): void {
86
+ if (!filename || !this.handler) {
87
+ return;
88
+ }
89
+
90
+ const fullPath = `${basePath}/${filename}`;
91
+ const now = Date.now();
92
+
93
+ // Debounce: skip if same file changed within debounceMs
94
+ const lastChange = this.recentChanges.get(fullPath);
95
+ if (lastChange && now - lastChange < this.debounceMs) {
96
+ return;
97
+ }
98
+
99
+ this.recentChanges.set(fullPath, now);
100
+
101
+ // Clean up old entries to prevent memory leak
102
+ if (this.recentChanges.size > 1000) {
103
+ const cutoff = now - this.debounceMs * 2;
104
+ for (const [path, timestamp] of this.recentChanges.entries()) {
105
+ if (timestamp < cutoff) {
106
+ this.recentChanges.delete(path);
107
+ }
108
+ }
109
+ }
110
+
111
+ const event: ObserverEvent = {
112
+ type: 'file_change',
113
+ data: {
114
+ path: fullPath,
115
+ eventType,
116
+ filename,
117
+ basePath,
118
+ },
119
+ timestamp: now,
120
+ };
121
+
122
+ this.handler(event);
123
+ }
124
+ }
@@ -0,0 +1,159 @@
1
+ /**
2
+ * Observer Layer - Monitors system events and emits observations to the Vault
3
+ *
4
+ * All observers implement a common interface and emit standardized events.
5
+ */
6
+
7
+ // Common Observer Interface
8
+ export type ObserverEvent = {
9
+ type: string;
10
+ data: Record<string, unknown>;
11
+ timestamp: number;
12
+ };
13
+
14
+ export type ObserverEventHandler = (event: ObserverEvent) => void;
15
+
16
+ export interface Observer {
17
+ name: string;
18
+ start(): Promise<void>;
19
+ stop(): Promise<void>;
20
+ isRunning(): boolean;
21
+ onEvent(handler: ObserverEventHandler): void;
22
+ }
23
+
24
+ // Export all observers
25
+ export { FileWatcher } from './file-watcher';
26
+ export { ClipboardMonitor } from './clipboard';
27
+ export { NotificationListener } from './notifications';
28
+ export { ProcessMonitor } from './processes';
29
+ export { CalendarSync } from './calendar';
30
+ export { EmailSync } from './email';
31
+
32
+ /**
33
+ * ObserverManager - Centralized coordinator for all observers
34
+ */
35
+ export class ObserverManager {
36
+ private observers: Map<string, Observer> = new Map();
37
+ private eventHandler: ObserverEventHandler | null = null;
38
+
39
+ /**
40
+ * Register a new observer
41
+ */
42
+ register(observer: Observer): void {
43
+ this.observers.set(observer.name, observer);
44
+
45
+ // If we already have a handler, apply it to the new observer
46
+ if (this.eventHandler) {
47
+ observer.onEvent(this.eventHandler);
48
+ }
49
+
50
+ console.log(`[ObserverManager] Registered observer: ${observer.name}`);
51
+ }
52
+
53
+ /**
54
+ * Set the global event handler for all observers
55
+ * This is typically the Vault's observation ingestion function
56
+ */
57
+ setEventHandler(handler: ObserverEventHandler): void {
58
+ this.eventHandler = handler;
59
+
60
+ // Apply handler to all registered observers
61
+ for (const observer of this.observers.values()) {
62
+ observer.onEvent(handler);
63
+ }
64
+
65
+ console.log(`[ObserverManager] Event handler configured for ${this.observers.size} observers`);
66
+ }
67
+
68
+ /**
69
+ * Start all registered observers
70
+ */
71
+ async startAll(): Promise<void> {
72
+ console.log('[ObserverManager] Starting all observers...');
73
+
74
+ const promises = Array.from(this.observers.values()).map(async (observer) => {
75
+ try {
76
+ await observer.start();
77
+ console.log(`[ObserverManager] ✓ Started ${observer.name}`);
78
+ } catch (error) {
79
+ console.error(`[ObserverManager] ✗ Failed to start ${observer.name}:`, error);
80
+ }
81
+ });
82
+
83
+ await Promise.all(promises);
84
+ console.log('[ObserverManager] All observers started');
85
+ }
86
+
87
+ /**
88
+ * Stop all registered observers
89
+ */
90
+ async stopAll(): Promise<void> {
91
+ console.log('[ObserverManager] Stopping all observers...');
92
+
93
+ const promises = Array.from(this.observers.values()).map(async (observer) => {
94
+ try {
95
+ await observer.stop();
96
+ console.log(`[ObserverManager] ✓ Stopped ${observer.name}`);
97
+ } catch (error) {
98
+ console.error(`[ObserverManager] ✗ Failed to stop ${observer.name}:`, error);
99
+ }
100
+ });
101
+
102
+ await Promise.all(promises);
103
+ console.log('[ObserverManager] All observers stopped');
104
+ }
105
+
106
+ /**
107
+ * Start a specific observer by name
108
+ */
109
+ async startObserver(name: string): Promise<void> {
110
+ const observer = this.observers.get(name);
111
+ if (!observer) {
112
+ throw new Error(`Observer not found: ${name}`);
113
+ }
114
+
115
+ if (observer.isRunning()) {
116
+ console.log(`[ObserverManager] Observer ${name} is already running`);
117
+ return;
118
+ }
119
+
120
+ await observer.start();
121
+ console.log(`[ObserverManager] Started ${name}`);
122
+ }
123
+
124
+ /**
125
+ * Stop a specific observer by name
126
+ */
127
+ async stopObserver(name: string): Promise<void> {
128
+ const observer = this.observers.get(name);
129
+ if (!observer) {
130
+ throw new Error(`Observer not found: ${name}`);
131
+ }
132
+
133
+ if (!observer.isRunning()) {
134
+ console.log(`[ObserverManager] Observer ${name} is not running`);
135
+ return;
136
+ }
137
+
138
+ await observer.stop();
139
+ console.log(`[ObserverManager] Stopped ${name}`);
140
+ }
141
+
142
+ /**
143
+ * Get running status of all observers
144
+ */
145
+ getStatus(): Record<string, boolean> {
146
+ const status: Record<string, boolean> = {};
147
+ for (const [name, observer] of this.observers) {
148
+ status[name] = observer.isRunning();
149
+ }
150
+ return status;
151
+ }
152
+
153
+ /**
154
+ * List all registered observer names
155
+ */
156
+ listObservers(): string[] {
157
+ return Array.from(this.observers.keys());
158
+ }
159
+ }