@shykaruu/jarvis-brain 0.4.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 (330) hide show
  1. package/LICENSE +153 -0
  2. package/README.md +428 -0
  3. package/bin/jarvis.ts +449 -0
  4. package/package.json +79 -0
  5. package/roles/activity-observer.yaml +60 -0
  6. package/roles/ceo-founder.yaml +144 -0
  7. package/roles/chief-of-staff.yaml +158 -0
  8. package/roles/dev-lead.yaml +182 -0
  9. package/roles/executive-assistant.yaml +77 -0
  10. package/roles/marketing-director.yaml +168 -0
  11. package/roles/personal-assistant.yaml +266 -0
  12. package/roles/research-specialist.yaml +60 -0
  13. package/roles/specialists/content-writer.yaml +53 -0
  14. package/roles/specialists/customer-support.yaml +57 -0
  15. package/roles/specialists/data-analyst.yaml +57 -0
  16. package/roles/specialists/financial-analyst.yaml +56 -0
  17. package/roles/specialists/hr-specialist.yaml +55 -0
  18. package/roles/specialists/legal-advisor.yaml +58 -0
  19. package/roles/specialists/marketing-strategist.yaml +56 -0
  20. package/roles/specialists/project-coordinator.yaml +55 -0
  21. package/roles/specialists/research-analyst.yaml +58 -0
  22. package/roles/specialists/software-engineer.yaml +57 -0
  23. package/roles/specialists/system-administrator.yaml +57 -0
  24. package/roles/system-admin.yaml +76 -0
  25. package/scripts/ensure-bun.cjs +16 -0
  26. package/src/actions/README.md +421 -0
  27. package/src/actions/app-control/desktop-controller.test.ts +26 -0
  28. package/src/actions/app-control/desktop-controller.ts +438 -0
  29. package/src/actions/app-control/interface.ts +64 -0
  30. package/src/actions/app-control/linux.ts +273 -0
  31. package/src/actions/app-control/macos.ts +54 -0
  32. package/src/actions/app-control/sidecar-launcher.test.ts +23 -0
  33. package/src/actions/app-control/sidecar-launcher.ts +286 -0
  34. package/src/actions/app-control/windows.ts +44 -0
  35. package/src/actions/browser/cdp.ts +138 -0
  36. package/src/actions/browser/chrome-launcher.ts +261 -0
  37. package/src/actions/browser/session.ts +506 -0
  38. package/src/actions/browser/stealth.ts +49 -0
  39. package/src/actions/index.ts +20 -0
  40. package/src/actions/terminal/executor.ts +157 -0
  41. package/src/actions/terminal/wsl-bridge.ts +126 -0
  42. package/src/actions/test.ts +93 -0
  43. package/src/actions/tools/agents.ts +363 -0
  44. package/src/actions/tools/builtin.ts +950 -0
  45. package/src/actions/tools/commitments.ts +192 -0
  46. package/src/actions/tools/content.ts +217 -0
  47. package/src/actions/tools/delegate.ts +147 -0
  48. package/src/actions/tools/desktop.test.ts +55 -0
  49. package/src/actions/tools/desktop.ts +305 -0
  50. package/src/actions/tools/documents.ts +169 -0
  51. package/src/actions/tools/goals.ts +376 -0
  52. package/src/actions/tools/local-tools-guard.ts +31 -0
  53. package/src/actions/tools/registry.ts +173 -0
  54. package/src/actions/tools/research.ts +111 -0
  55. package/src/actions/tools/sidecar-list.ts +57 -0
  56. package/src/actions/tools/sidecar-route.ts +105 -0
  57. package/src/actions/tools/workflows.ts +216 -0
  58. package/src/agents/agent.ts +132 -0
  59. package/src/agents/delegation.ts +107 -0
  60. package/src/agents/hierarchy.ts +113 -0
  61. package/src/agents/index.ts +19 -0
  62. package/src/agents/messaging.ts +125 -0
  63. package/src/agents/orchestrator.ts +592 -0
  64. package/src/agents/role-discovery.ts +61 -0
  65. package/src/agents/sub-agent-runner.ts +309 -0
  66. package/src/agents/task-manager.ts +151 -0
  67. package/src/authority/approval-delivery.ts +59 -0
  68. package/src/authority/approval.ts +196 -0
  69. package/src/authority/audit.ts +158 -0
  70. package/src/authority/authority.test.ts +519 -0
  71. package/src/authority/deferred-executor.ts +103 -0
  72. package/src/authority/emergency.ts +66 -0
  73. package/src/authority/engine.ts +301 -0
  74. package/src/authority/index.ts +12 -0
  75. package/src/authority/learning.ts +111 -0
  76. package/src/authority/tool-action-map.ts +74 -0
  77. package/src/awareness/analytics.ts +466 -0
  78. package/src/awareness/awareness.test.ts +332 -0
  79. package/src/awareness/capture-engine.ts +305 -0
  80. package/src/awareness/context-graph.ts +130 -0
  81. package/src/awareness/context-tracker.ts +349 -0
  82. package/src/awareness/index.ts +25 -0
  83. package/src/awareness/intelligence.ts +321 -0
  84. package/src/awareness/ocr-engine.ts +88 -0
  85. package/src/awareness/service.ts +528 -0
  86. package/src/awareness/struggle-detector.ts +342 -0
  87. package/src/awareness/suggestion-engine.ts +476 -0
  88. package/src/awareness/types.ts +201 -0
  89. package/src/cli/autostart.ts +417 -0
  90. package/src/cli/deps.ts +449 -0
  91. package/src/cli/doctor.ts +238 -0
  92. package/src/cli/helpers.ts +401 -0
  93. package/src/cli/onboard.ts +827 -0
  94. package/src/cli/uninstall.test.ts +37 -0
  95. package/src/cli/uninstall.ts +202 -0
  96. package/src/comms/README.md +329 -0
  97. package/src/comms/auth-error.html +48 -0
  98. package/src/comms/channels/discord.ts +228 -0
  99. package/src/comms/channels/signal.ts +56 -0
  100. package/src/comms/channels/telegram.ts +316 -0
  101. package/src/comms/channels/whatsapp.ts +60 -0
  102. package/src/comms/channels.test.ts +173 -0
  103. package/src/comms/dashboard-auth.ts +75 -0
  104. package/src/comms/desktop-notify.ts +114 -0
  105. package/src/comms/example.ts +129 -0
  106. package/src/comms/index.ts +129 -0
  107. package/src/comms/streaming.ts +149 -0
  108. package/src/comms/voice.test.ts +504 -0
  109. package/src/comms/voice.ts +341 -0
  110. package/src/comms/websocket.test.ts +409 -0
  111. package/src/comms/websocket.ts +669 -0
  112. package/src/config/README.md +389 -0
  113. package/src/config/index.ts +6 -0
  114. package/src/config/loader.test.ts +183 -0
  115. package/src/config/loader.ts +148 -0
  116. package/src/config/types.ts +293 -0
  117. package/src/daemon/README.md +232 -0
  118. package/src/daemon/agent-service-interface.ts +9 -0
  119. package/src/daemon/agent-service.ts +667 -0
  120. package/src/daemon/api-routes.ts +3067 -0
  121. package/src/daemon/background-agent-service.ts +396 -0
  122. package/src/daemon/background-agent.test.ts +78 -0
  123. package/src/daemon/channel-service.ts +201 -0
  124. package/src/daemon/commitment-executor.ts +297 -0
  125. package/src/daemon/dashboard-auth.test.ts +170 -0
  126. package/src/daemon/event-classifier.ts +239 -0
  127. package/src/daemon/event-coalescer.ts +123 -0
  128. package/src/daemon/event-reactor.ts +214 -0
  129. package/src/daemon/flock.c +7 -0
  130. package/src/daemon/health.ts +220 -0
  131. package/src/daemon/index.ts +1070 -0
  132. package/src/daemon/llm-settings.test.ts +78 -0
  133. package/src/daemon/llm-settings.ts +450 -0
  134. package/src/daemon/observer-service.ts +150 -0
  135. package/src/daemon/pid.test.ts +283 -0
  136. package/src/daemon/pid.ts +224 -0
  137. package/src/daemon/research-queue.ts +155 -0
  138. package/src/daemon/services.ts +175 -0
  139. package/src/daemon/ws-service.ts +926 -0
  140. package/src/global.d.ts +4 -0
  141. package/src/goals/accountability.ts +240 -0
  142. package/src/goals/awareness-bridge.ts +185 -0
  143. package/src/goals/estimator.ts +185 -0
  144. package/src/goals/events.ts +28 -0
  145. package/src/goals/goals.test.ts +400 -0
  146. package/src/goals/integration.test.ts +329 -0
  147. package/src/goals/nl-builder.test.ts +220 -0
  148. package/src/goals/nl-builder.ts +256 -0
  149. package/src/goals/rhythm.test.ts +177 -0
  150. package/src/goals/rhythm.ts +275 -0
  151. package/src/goals/service.test.ts +135 -0
  152. package/src/goals/service.ts +407 -0
  153. package/src/goals/types.ts +106 -0
  154. package/src/goals/workflow-bridge.ts +96 -0
  155. package/src/integrations/google-api.ts +134 -0
  156. package/src/integrations/google-auth.ts +175 -0
  157. package/src/llm/README.md +291 -0
  158. package/src/llm/anthropic.ts +400 -0
  159. package/src/llm/gemini.ts +380 -0
  160. package/src/llm/groq.ts +406 -0
  161. package/src/llm/history.ts +147 -0
  162. package/src/llm/index.ts +21 -0
  163. package/src/llm/manager.ts +226 -0
  164. package/src/llm/ollama.ts +316 -0
  165. package/src/llm/openai.ts +411 -0
  166. package/src/llm/openrouter.ts +390 -0
  167. package/src/llm/provider.test.ts +487 -0
  168. package/src/llm/provider.ts +61 -0
  169. package/src/llm/test.ts +88 -0
  170. package/src/observers/README.md +278 -0
  171. package/src/observers/calendar.ts +113 -0
  172. package/src/observers/clipboard.ts +136 -0
  173. package/src/observers/email.ts +109 -0
  174. package/src/observers/example.ts +58 -0
  175. package/src/observers/file-watcher.ts +124 -0
  176. package/src/observers/index.ts +159 -0
  177. package/src/observers/notifications.ts +197 -0
  178. package/src/observers/observers.test.ts +203 -0
  179. package/src/observers/processes.ts +225 -0
  180. package/src/personality/README.md +61 -0
  181. package/src/personality/adapter.ts +196 -0
  182. package/src/personality/index.ts +20 -0
  183. package/src/personality/learner.ts +209 -0
  184. package/src/personality/model.ts +132 -0
  185. package/src/personality/personality.test.ts +236 -0
  186. package/src/roles/README.md +252 -0
  187. package/src/roles/authority.ts +120 -0
  188. package/src/roles/example-usage.ts +198 -0
  189. package/src/roles/index.ts +42 -0
  190. package/src/roles/loader.ts +143 -0
  191. package/src/roles/prompt-builder.ts +218 -0
  192. package/src/roles/test-multi.ts +102 -0
  193. package/src/roles/test-role.yaml +77 -0
  194. package/src/roles/test-utils.ts +93 -0
  195. package/src/roles/test.ts +106 -0
  196. package/src/roles/tool-guide.ts +195 -0
  197. package/src/roles/types.ts +36 -0
  198. package/src/roles/utils.ts +200 -0
  199. package/src/scripts/google-setup.ts +168 -0
  200. package/src/sidecar/connection.ts +179 -0
  201. package/src/sidecar/index.ts +6 -0
  202. package/src/sidecar/manager.ts +542 -0
  203. package/src/sidecar/protocol.ts +85 -0
  204. package/src/sidecar/rpc.ts +161 -0
  205. package/src/sidecar/scheduler.ts +136 -0
  206. package/src/sidecar/types.ts +112 -0
  207. package/src/sidecar/validator.ts +144 -0
  208. package/src/sites/builder-tools.ts +215 -0
  209. package/src/sites/dev-server-manager.ts +286 -0
  210. package/src/sites/fixtures/security-test-site/.jarvis-project.json +6 -0
  211. package/src/sites/fixtures/security-test-site/Makefile +15 -0
  212. package/src/sites/fixtures/security-test-site/README.md +18 -0
  213. package/src/sites/fixtures/security-test-site/index.html +12 -0
  214. package/src/sites/fixtures/security-test-site/index.ts +16 -0
  215. package/src/sites/fixtures/security-test-site/package.json +13 -0
  216. package/src/sites/fixtures/security-test-site/src/app.tsx +780 -0
  217. package/src/sites/fixtures/security-test-site/tsconfig.json +10 -0
  218. package/src/sites/git-manager.ts +240 -0
  219. package/src/sites/github-manager.ts +355 -0
  220. package/src/sites/index.ts +25 -0
  221. package/src/sites/project-manager.ts +389 -0
  222. package/src/sites/proxy.ts +133 -0
  223. package/src/sites/service.ts +136 -0
  224. package/src/sites/templates.ts +169 -0
  225. package/src/sites/types.ts +89 -0
  226. package/src/user/profile-followup.test.ts +84 -0
  227. package/src/user/profile-followup.ts +185 -0
  228. package/src/user/profile.ts +224 -0
  229. package/src/vault/README.md +110 -0
  230. package/src/vault/awareness.ts +341 -0
  231. package/src/vault/commitments.ts +299 -0
  232. package/src/vault/content-pipeline.ts +270 -0
  233. package/src/vault/conversations.ts +173 -0
  234. package/src/vault/dashboard-sessions.ts +44 -0
  235. package/src/vault/documents.ts +130 -0
  236. package/src/vault/entities.ts +185 -0
  237. package/src/vault/extractor.test.ts +356 -0
  238. package/src/vault/extractor.ts +345 -0
  239. package/src/vault/facts.ts +190 -0
  240. package/src/vault/goals.ts +477 -0
  241. package/src/vault/index.ts +87 -0
  242. package/src/vault/keychain.ts +99 -0
  243. package/src/vault/observations.ts +115 -0
  244. package/src/vault/relationships.ts +178 -0
  245. package/src/vault/retrieval.test.ts +139 -0
  246. package/src/vault/retrieval.ts +258 -0
  247. package/src/vault/schema.ts +709 -0
  248. package/src/vault/settings.ts +38 -0
  249. package/src/vault/user-profile.test.ts +113 -0
  250. package/src/vault/user-profile.ts +176 -0
  251. package/src/vault/vectors.ts +92 -0
  252. package/src/vault/webapp-template-seeds.ts +116 -0
  253. package/src/vault/webapp-templates.ts +244 -0
  254. package/src/vault/workflows.ts +403 -0
  255. package/src/workflows/auto-suggest.ts +290 -0
  256. package/src/workflows/engine.ts +366 -0
  257. package/src/workflows/events.ts +24 -0
  258. package/src/workflows/executor.ts +207 -0
  259. package/src/workflows/nl-builder.ts +198 -0
  260. package/src/workflows/nodes/actions/agent-task.ts +73 -0
  261. package/src/workflows/nodes/actions/calendar-action.ts +85 -0
  262. package/src/workflows/nodes/actions/code-execution.ts +73 -0
  263. package/src/workflows/nodes/actions/discord.ts +77 -0
  264. package/src/workflows/nodes/actions/file-write.ts +73 -0
  265. package/src/workflows/nodes/actions/gmail.ts +69 -0
  266. package/src/workflows/nodes/actions/http-request.ts +117 -0
  267. package/src/workflows/nodes/actions/notification.ts +85 -0
  268. package/src/workflows/nodes/actions/run-tool.ts +55 -0
  269. package/src/workflows/nodes/actions/send-message.ts +82 -0
  270. package/src/workflows/nodes/actions/shell-command.ts +76 -0
  271. package/src/workflows/nodes/actions/telegram.ts +60 -0
  272. package/src/workflows/nodes/builtin.ts +119 -0
  273. package/src/workflows/nodes/error/error-handler.ts +37 -0
  274. package/src/workflows/nodes/error/fallback.ts +47 -0
  275. package/src/workflows/nodes/error/retry.ts +82 -0
  276. package/src/workflows/nodes/logic/delay.ts +42 -0
  277. package/src/workflows/nodes/logic/if-else.ts +41 -0
  278. package/src/workflows/nodes/logic/loop.ts +90 -0
  279. package/src/workflows/nodes/logic/merge.ts +38 -0
  280. package/src/workflows/nodes/logic/race.ts +40 -0
  281. package/src/workflows/nodes/logic/switch.ts +59 -0
  282. package/src/workflows/nodes/logic/template-render.ts +53 -0
  283. package/src/workflows/nodes/logic/variable-get.ts +37 -0
  284. package/src/workflows/nodes/logic/variable-set.ts +59 -0
  285. package/src/workflows/nodes/registry.ts +99 -0
  286. package/src/workflows/nodes/transform/aggregate.ts +99 -0
  287. package/src/workflows/nodes/transform/csv-parse.ts +70 -0
  288. package/src/workflows/nodes/transform/json-parse.ts +63 -0
  289. package/src/workflows/nodes/transform/map-filter.ts +84 -0
  290. package/src/workflows/nodes/transform/regex-match.ts +89 -0
  291. package/src/workflows/nodes/triggers/calendar.ts +33 -0
  292. package/src/workflows/nodes/triggers/clipboard.ts +32 -0
  293. package/src/workflows/nodes/triggers/cron.ts +40 -0
  294. package/src/workflows/nodes/triggers/email.ts +40 -0
  295. package/src/workflows/nodes/triggers/file-change.ts +45 -0
  296. package/src/workflows/nodes/triggers/git.ts +46 -0
  297. package/src/workflows/nodes/triggers/manual.ts +23 -0
  298. package/src/workflows/nodes/triggers/poll.ts +81 -0
  299. package/src/workflows/nodes/triggers/process.ts +44 -0
  300. package/src/workflows/nodes/triggers/screen-event.ts +37 -0
  301. package/src/workflows/nodes/triggers/webhook.ts +39 -0
  302. package/src/workflows/safe-eval.ts +139 -0
  303. package/src/workflows/template.ts +118 -0
  304. package/src/workflows/triggers/cron.ts +311 -0
  305. package/src/workflows/triggers/manager.ts +285 -0
  306. package/src/workflows/triggers/observer-bridge.ts +172 -0
  307. package/src/workflows/triggers/poller.ts +201 -0
  308. package/src/workflows/triggers/screen-condition.ts +218 -0
  309. package/src/workflows/triggers/triggers.test.ts +740 -0
  310. package/src/workflows/triggers/webhook.ts +191 -0
  311. package/src/workflows/types.ts +133 -0
  312. package/src/workflows/variables.ts +72 -0
  313. package/src/workflows/workflows.test.ts +383 -0
  314. package/src/workflows/yaml.ts +104 -0
  315. package/ui/dist/index-3gr23jt9.js +112614 -0
  316. package/ui/dist/index-9vmj8127.css +14239 -0
  317. package/ui/dist/index-hy9pc1gm.js +112873 -0
  318. package/ui/dist/index-j2ep5d1w.js +112374 -0
  319. package/ui/dist/index-jt00vjqs.js +112858 -0
  320. package/ui/dist/index-k9ymx5qb.js +112374 -0
  321. package/ui/dist/index.html +16 -0
  322. package/ui/public/audio/pcm-capture-processor.js +11 -0
  323. package/ui/public/openwakeword/models/embedding_model.onnx +0 -0
  324. package/ui/public/openwakeword/models/hey_jarvis_v0.1.onnx +0 -0
  325. package/ui/public/openwakeword/models/melspectrogram.onnx +0 -0
  326. package/ui/public/openwakeword/models/silero_vad.onnx +0 -0
  327. package/ui/public/ort/ort-wasm-simd-threaded.jsep.mjs +106 -0
  328. package/ui/public/ort/ort-wasm-simd-threaded.jsep.wasm +0 -0
  329. package/ui/public/ort/ort-wasm-simd-threaded.mjs +59 -0
  330. package/ui/public/ort/ort-wasm-simd-threaded.wasm +0 -0
@@ -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
+ }
@@ -0,0 +1,197 @@
1
+ /**
2
+ * NotificationListener — D-Bus Notification Monitor (Linux/WSL2)
3
+ *
4
+ * Monitors system notifications by watching D-Bus for
5
+ * org.freedesktop.Notifications.Notify method calls.
6
+ * Parses notification fields: app_name, summary, body, urgency.
7
+ *
8
+ * Graceful: if dbus-monitor is not found, logs warning and stays no-op.
9
+ */
10
+
11
+ import type { Observer, ObserverEventHandler } from './index';
12
+ import type { Subprocess } from 'bun';
13
+
14
+ type NotificationData = {
15
+ app: string;
16
+ title: string;
17
+ body: string;
18
+ urgency: string;
19
+ };
20
+
21
+ export class NotificationListener implements Observer {
22
+ name = 'notifications';
23
+ private running = false;
24
+ private handler: ObserverEventHandler | null = null;
25
+ private process: Subprocess | null = null;
26
+ private available = false;
27
+
28
+ async start(): Promise<void> {
29
+ this.running = true;
30
+
31
+ // Check if dbus-monitor exists
32
+ try {
33
+ const check = Bun.spawnSync(['which', 'dbus-monitor']);
34
+ if (check.exitCode !== 0) {
35
+ console.log('[notifications] dbus-monitor not found — notification monitoring disabled');
36
+ return;
37
+ }
38
+ this.available = true;
39
+ } catch {
40
+ console.log('[notifications] Cannot check for dbus-monitor — notification monitoring disabled');
41
+ return;
42
+ }
43
+
44
+ // Spawn dbus-monitor watching for Notify signals
45
+ try {
46
+ this.process = Bun.spawn(
47
+ [
48
+ 'dbus-monitor',
49
+ '--session',
50
+ "interface='org.freedesktop.Notifications',member='Notify'",
51
+ ],
52
+ {
53
+ stdout: 'pipe',
54
+ stderr: 'ignore',
55
+ }
56
+ );
57
+
58
+ console.log('[notifications] Observer started — monitoring D-Bus notifications');
59
+
60
+ // Parse stdout in background
61
+ this.readOutput();
62
+ } catch (err) {
63
+ console.error('[notifications] Failed to start dbus-monitor:', err);
64
+ this.available = false;
65
+ }
66
+ }
67
+
68
+ async stop(): Promise<void> {
69
+ this.running = false;
70
+
71
+ if (this.process) {
72
+ try {
73
+ this.process.kill();
74
+ } catch {
75
+ // Ignore
76
+ }
77
+ this.process = null;
78
+ }
79
+
80
+ console.log('[notifications] Observer stopped');
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 dbus-monitor output and parse notification blocks.
93
+ *
94
+ * D-Bus notification format (method_call):
95
+ * method call ... dest=org.freedesktop.Notifications ... member=Notify
96
+ * string "app_name"
97
+ * uint32 ...
98
+ * string "icon"
99
+ * string "summary"
100
+ * string "body"
101
+ * array [ ... ]
102
+ * ...
103
+ */
104
+ private async readOutput(): Promise<void> {
105
+ if (!this.process?.stdout) return;
106
+
107
+ const reader = (this.process.stdout as ReadableStream<Uint8Array>).getReader();
108
+ const decoder = new TextDecoder();
109
+ let buffer = '';
110
+
111
+ // State machine for parsing method_call blocks
112
+ let inMethodCall = false;
113
+ let stringIndex = 0;
114
+ let currentNotification: Partial<NotificationData> = {};
115
+
116
+ try {
117
+ while (this.running) {
118
+ const { done, value } = await reader.read();
119
+ if (done) break;
120
+
121
+ buffer += decoder.decode(value, { stream: true });
122
+ const lines = buffer.split('\n');
123
+ buffer = lines.pop() ?? '';
124
+
125
+ for (const line of lines) {
126
+ const trimmed = line.trim();
127
+
128
+ // Detect start of a new method call
129
+ if (trimmed.startsWith('method call') && trimmed.includes('member=Notify')) {
130
+ inMethodCall = true;
131
+ stringIndex = 0;
132
+ currentNotification = {};
133
+ continue;
134
+ }
135
+
136
+ // Detect start of next method/signal (end of current block)
137
+ if (inMethodCall && (trimmed.startsWith('method call') || trimmed.startsWith('signal') || trimmed.startsWith('method return'))) {
138
+ // Emit the notification if we got enough data
139
+ this.emitNotification(currentNotification);
140
+ inMethodCall = trimmed.startsWith('method call') && trimmed.includes('member=Notify');
141
+ stringIndex = 0;
142
+ currentNotification = {};
143
+ continue;
144
+ }
145
+
146
+ if (!inMethodCall) continue;
147
+
148
+ // Parse string values — the Notify method has these string args in order:
149
+ // 0: app_name, 1: (replaces_id is uint32, skip), 2: icon, 3: summary, 4: body
150
+ const stringMatch = trimmed.match(/^string\s+"(.*)"/);
151
+ if (stringMatch) {
152
+ const val = stringMatch[1];
153
+ switch (stringIndex) {
154
+ case 0: currentNotification.app = val; break;
155
+ // index 1 is icon (we skip)
156
+ case 1: break; // icon
157
+ case 2: currentNotification.title = val; break;
158
+ case 3: currentNotification.body = val; break;
159
+ }
160
+ stringIndex++;
161
+ continue;
162
+ }
163
+
164
+ // Parse urgency from hints dict (byte value)
165
+ const urgencyMatch = trimmed.match(/byte\s+(\d+)/);
166
+ if (urgencyMatch) {
167
+ const urgencyLevel = parseInt(urgencyMatch[1]!);
168
+ currentNotification.urgency =
169
+ urgencyLevel === 2 ? 'critical' :
170
+ urgencyLevel === 1 ? 'normal' :
171
+ 'low';
172
+ }
173
+ }
174
+ }
175
+ } catch (err) {
176
+ if (this.running) {
177
+ console.error('[notifications] Read error:', err);
178
+ }
179
+ }
180
+ }
181
+
182
+ private emitNotification(data: Partial<NotificationData>): void {
183
+ if (!data.title && !data.body) return;
184
+ if (!this.handler) return;
185
+
186
+ this.handler({
187
+ type: 'notification',
188
+ data: {
189
+ app: data.app ?? 'unknown',
190
+ title: data.title ?? '',
191
+ body: data.body ?? '',
192
+ urgency: data.urgency ?? 'normal',
193
+ },
194
+ timestamp: Date.now(),
195
+ });
196
+ }
197
+ }
@@ -0,0 +1,203 @@
1
+ /**
2
+ * Tests for Observer Layer
3
+ */
4
+
5
+ import { test, expect, describe, beforeAll, afterAll } from 'bun:test';
6
+ import { mkdtempSync, rmSync } from 'node:fs';
7
+ import { join } from 'node:path';
8
+ import { tmpdir } from 'node:os';
9
+ import {
10
+ ObserverManager,
11
+ FileWatcher,
12
+ ClipboardMonitor,
13
+ ProcessMonitor,
14
+ NotificationListener,
15
+ CalendarSync,
16
+ EmailSync,
17
+ type ObserverEvent,
18
+ } from './index';
19
+
20
+ // Use an isolated temp dir instead of /tmp to avoid slow recursive watches on CI
21
+ let testDir: string;
22
+ beforeAll(() => {
23
+ testDir = mkdtempSync(join(tmpdir(), 'jarvis-test-'));
24
+ });
25
+ afterAll(() => {
26
+ try { rmSync(testDir, { recursive: true }); } catch {}
27
+ });
28
+
29
+ describe('ObserverManager', () => {
30
+ test('registers observers', () => {
31
+ const manager = new ObserverManager();
32
+ const watcher = new FileWatcher([testDir]);
33
+
34
+ manager.register(watcher);
35
+
36
+ expect(manager.listObservers()).toEqual(['file-watcher']);
37
+ });
38
+
39
+ test('propagates event handler to observers', () => {
40
+ const manager = new ObserverManager();
41
+ const watcher = new FileWatcher([testDir]);
42
+
43
+ manager.register(watcher);
44
+
45
+ let handlerCalled = false;
46
+ manager.setEventHandler(() => {
47
+ handlerCalled = true;
48
+ });
49
+
50
+ // Handler should be set on the observer
51
+ expect(handlerCalled).toBe(false); // Not called yet, just registered
52
+ });
53
+
54
+ test('starts and stops all observers', async () => {
55
+ const manager = new ObserverManager();
56
+ const watcher = new FileWatcher([testDir]);
57
+ const clipboard = new ClipboardMonitor(5000);
58
+
59
+ manager.register(watcher);
60
+ manager.register(clipboard);
61
+
62
+ await manager.startAll();
63
+
64
+ const status = manager.getStatus();
65
+ expect(status['file-watcher']).toBe(true);
66
+ expect(status['clipboard']).toBe(true);
67
+
68
+ await manager.stopAll();
69
+
70
+ const statusAfter = manager.getStatus();
71
+ expect(statusAfter['file-watcher']).toBe(false);
72
+ expect(statusAfter['clipboard']).toBe(false);
73
+ });
74
+
75
+ test('starts and stops individual observers', async () => {
76
+ const manager = new ObserverManager();
77
+ const watcher = new FileWatcher([testDir]);
78
+
79
+ manager.register(watcher);
80
+
81
+ await manager.startObserver('file-watcher');
82
+ expect(manager.getStatus()['file-watcher']).toBe(true);
83
+
84
+ await manager.stopObserver('file-watcher');
85
+ expect(manager.getStatus()['file-watcher']).toBe(false);
86
+ });
87
+ });
88
+
89
+ describe('FileWatcher', () => {
90
+ test('starts and stops', async () => {
91
+ const watcher = new FileWatcher([testDir]);
92
+
93
+ expect(watcher.isRunning()).toBe(false);
94
+
95
+ await watcher.start();
96
+ expect(watcher.isRunning()).toBe(true);
97
+
98
+ await watcher.stop();
99
+ expect(watcher.isRunning()).toBe(false);
100
+ });
101
+
102
+ test('prevents double start', async () => {
103
+ const watcher = new FileWatcher([testDir]);
104
+
105
+ await watcher.start();
106
+ await watcher.start(); // Should not throw
107
+
108
+ expect(watcher.isRunning()).toBe(true);
109
+
110
+ await watcher.stop();
111
+ });
112
+ });
113
+
114
+ describe('ClipboardMonitor', () => {
115
+ test('starts and stops', async () => {
116
+ const clipboard = new ClipboardMonitor(5000);
117
+
118
+ expect(clipboard.isRunning()).toBe(false);
119
+
120
+ await clipboard.start();
121
+ expect(clipboard.isRunning()).toBe(true);
122
+
123
+ await clipboard.stop();
124
+ expect(clipboard.isRunning()).toBe(false);
125
+ });
126
+
127
+ test('uses custom poll interval', async () => {
128
+ const clipboard = new ClipboardMonitor(10000);
129
+
130
+ await clipboard.start();
131
+ expect(clipboard.isRunning()).toBe(true);
132
+
133
+ await clipboard.stop();
134
+ });
135
+ });
136
+
137
+ describe('ProcessMonitor', () => {
138
+ test('starts and stops', async () => {
139
+ const monitor = new ProcessMonitor(10000);
140
+
141
+ expect(monitor.isRunning()).toBe(false);
142
+
143
+ await monitor.start();
144
+ expect(monitor.isRunning()).toBe(true);
145
+
146
+ await monitor.stop();
147
+ expect(monitor.isRunning()).toBe(false);
148
+ });
149
+
150
+ test('gets process list', async () => {
151
+ const monitor = new ProcessMonitor(10000);
152
+
153
+ const processes = await monitor.getProcessList();
154
+
155
+ expect(Array.isArray(processes)).toBe(true);
156
+ expect(processes.length).toBeGreaterThan(0);
157
+
158
+ // Check structure of first process
159
+ const proc = processes[0];
160
+ expect(proc).toHaveProperty('pid');
161
+ expect(proc).toHaveProperty('name');
162
+ expect(proc).toHaveProperty('cpu');
163
+ expect(proc).toHaveProperty('memory');
164
+ });
165
+ });
166
+
167
+ describe('Stub Observers', () => {
168
+ test('NotificationListener starts and stops', async () => {
169
+ const listener = new NotificationListener();
170
+
171
+ expect(listener.isRunning()).toBe(false);
172
+
173
+ await listener.start();
174
+ expect(listener.isRunning()).toBe(true);
175
+
176
+ await listener.stop();
177
+ expect(listener.isRunning()).toBe(false);
178
+ });
179
+
180
+ test('CalendarSync starts and stops', async () => {
181
+ const sync = new CalendarSync();
182
+
183
+ expect(sync.isRunning()).toBe(false);
184
+
185
+ await sync.start();
186
+ expect(sync.isRunning()).toBe(true);
187
+
188
+ await sync.stop();
189
+ expect(sync.isRunning()).toBe(false);
190
+ });
191
+
192
+ test('EmailSync starts and stops', async () => {
193
+ const sync = new EmailSync();
194
+
195
+ expect(sync.isRunning()).toBe(false);
196
+
197
+ await sync.start();
198
+ expect(sync.isRunning()).toBe(true);
199
+
200
+ await sync.stop();
201
+ expect(sync.isRunning()).toBe(false);
202
+ });
203
+ });