@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,231 @@
1
+ import { test, expect, describe } from 'bun:test';
2
+ import { AnthropicProvider } from './anthropic.ts';
3
+ import { OpenAIProvider } from './openai.ts';
4
+ import { OllamaProvider } from './ollama.ts';
5
+ import { LLMManager } from './manager.ts';
6
+ import { guardImageSize, type LLMMessage, type ContentBlock } from './provider.ts';
7
+ import { isToolResult, type ToolResult } from '../actions/tools/registry.ts';
8
+
9
+ describe('LLM Provider Types', () => {
10
+ test('AnthropicProvider can be instantiated', () => {
11
+ const provider = new AnthropicProvider('test-key', 'test-model');
12
+ expect(provider.name).toBe('anthropic');
13
+ });
14
+
15
+ test('OpenAIProvider can be instantiated', () => {
16
+ const provider = new OpenAIProvider('test-key', 'test-model');
17
+ expect(provider.name).toBe('openai');
18
+ });
19
+
20
+ test('OllamaProvider can be instantiated', () => {
21
+ const provider = new OllamaProvider('http://localhost:11434', 'llama3');
22
+ expect(provider.name).toBe('ollama');
23
+ });
24
+ });
25
+
26
+ describe('LLMManager', () => {
27
+ test('can register providers', () => {
28
+ const manager = new LLMManager();
29
+ const anthropic = new AnthropicProvider('test-key');
30
+
31
+ manager.registerProvider(anthropic);
32
+ expect(manager.getProvider('anthropic')).toBe(anthropic);
33
+ });
34
+
35
+ test('sets first registered provider as primary', () => {
36
+ const manager = new LLMManager();
37
+ const anthropic = new AnthropicProvider('test-key');
38
+
39
+ manager.registerProvider(anthropic);
40
+ // Primary is set automatically
41
+ expect(manager.getProvider('anthropic')).toBeDefined();
42
+ });
43
+
44
+ test('can change primary provider', () => {
45
+ const manager = new LLMManager();
46
+ const anthropic = new AnthropicProvider('test-key-1');
47
+ const openai = new OpenAIProvider('test-key-2');
48
+
49
+ manager.registerProvider(anthropic);
50
+ manager.registerProvider(openai);
51
+ manager.setPrimary('openai');
52
+
53
+ // Should not throw
54
+ expect(manager.getProvider('openai')).toBeDefined();
55
+ });
56
+
57
+ test('throws when setting non-existent provider as primary', () => {
58
+ const manager = new LLMManager();
59
+ expect(() => manager.setPrimary('nonexistent')).toThrow();
60
+ });
61
+
62
+ test('can set fallback chain', () => {
63
+ const manager = new LLMManager();
64
+ const anthropic = new AnthropicProvider('test-key-1');
65
+ const openai = new OpenAIProvider('test-key-2');
66
+
67
+ manager.registerProvider(anthropic);
68
+ manager.registerProvider(openai);
69
+ manager.setPrimary('anthropic');
70
+ manager.setFallbackChain(['openai']);
71
+
72
+ // Should not throw
73
+ expect(manager.getProvider('anthropic')).toBeDefined();
74
+ expect(manager.getProvider('openai')).toBeDefined();
75
+ });
76
+
77
+ test('throws when setting non-existent fallback provider', () => {
78
+ const manager = new LLMManager();
79
+ const anthropic = new AnthropicProvider('test-key');
80
+
81
+ manager.registerProvider(anthropic);
82
+ expect(() => manager.setFallbackChain(['nonexistent'])).toThrow();
83
+ });
84
+ });
85
+
86
+ describe('Message Types', () => {
87
+ test('LLMMessage has correct structure', () => {
88
+ const message: LLMMessage = {
89
+ role: 'user',
90
+ content: 'Hello',
91
+ };
92
+
93
+ expect(message.role).toBe('user');
94
+ expect(message.content).toBe('Hello');
95
+ });
96
+
97
+ test('supports all message roles', () => {
98
+ const messages: LLMMessage[] = [
99
+ { role: 'system', content: 'You are helpful' },
100
+ { role: 'user', content: 'Hello' },
101
+ { role: 'assistant', content: 'Hi there' },
102
+ ];
103
+
104
+ expect(messages).toHaveLength(3);
105
+ expect(messages[0]!.role).toBe('system');
106
+ expect(messages[1]!.role).toBe('user');
107
+ expect(messages[2]!.role).toBe('assistant');
108
+ });
109
+ });
110
+
111
+ describe('Provider URLs', () => {
112
+ test('AnthropicProvider uses correct API URL', () => {
113
+ const provider = new AnthropicProvider('test-key') as any;
114
+ expect(provider.apiUrl).toBe('https://api.anthropic.com/v1/messages');
115
+ });
116
+
117
+ test('OpenAIProvider uses correct API URL', () => {
118
+ const provider = new OpenAIProvider('test-key') as any;
119
+ expect(provider.apiUrl).toBe('https://api.openai.com/v1/chat/completions');
120
+ });
121
+
122
+ test('OllamaProvider uses correct base URL', () => {
123
+ const provider = new OllamaProvider() as any;
124
+ expect(provider.baseUrl).toBe('http://localhost:11434');
125
+ });
126
+
127
+ test('OllamaProvider removes trailing slash from base URL', () => {
128
+ const provider = new OllamaProvider('http://localhost:11434/') as any;
129
+ expect(provider.baseUrl).toBe('http://localhost:11434');
130
+ });
131
+ });
132
+
133
+ describe('Default Models', () => {
134
+ test('AnthropicProvider has correct default model', () => {
135
+ const provider = new AnthropicProvider('test-key') as any;
136
+ expect(provider.defaultModel).toBe('claude-sonnet-4-5-20250929');
137
+ });
138
+
139
+ test('OpenAIProvider has correct default model', () => {
140
+ const provider = new OpenAIProvider('test-key') as any;
141
+ expect(provider.defaultModel).toBe('gpt-4o');
142
+ });
143
+
144
+ test('OllamaProvider has correct default model', () => {
145
+ const provider = new OllamaProvider() as any;
146
+ expect(provider.defaultModel).toBe('llama3');
147
+ });
148
+
149
+ test('can override default models', () => {
150
+ const anthropic = new AnthropicProvider('key', 'custom-model') as any;
151
+ const openai = new OpenAIProvider('key', 'custom-model') as any;
152
+ const ollama = new OllamaProvider('http://localhost:11434', 'custom-model') as any;
153
+
154
+ expect(anthropic.defaultModel).toBe('custom-model');
155
+ expect(openai.defaultModel).toBe('custom-model');
156
+ expect(ollama.defaultModel).toBe('custom-model');
157
+ });
158
+ });
159
+
160
+ describe('Vision Support', () => {
161
+ describe('guardImageSize', () => {
162
+ test('passes text blocks through unchanged', () => {
163
+ const block: ContentBlock = { type: 'text', text: 'hello' };
164
+ expect(guardImageSize(block)).toBe(block);
165
+ });
166
+
167
+ test('passes small images through unchanged', () => {
168
+ const block: ContentBlock = {
169
+ type: 'image',
170
+ source: { type: 'base64', media_type: 'image/png', data: 'abc123' },
171
+ };
172
+ expect(guardImageSize(block)).toBe(block);
173
+ });
174
+
175
+ test('replaces oversized images with text warning', () => {
176
+ const bigData = 'x'.repeat(6 * 1024 * 1024); // 6 MB
177
+ const block: ContentBlock = {
178
+ type: 'image',
179
+ source: { type: 'base64', media_type: 'image/png', data: bigData },
180
+ };
181
+ const result = guardImageSize(block);
182
+ expect(result.type).toBe('text');
183
+ expect((result as { type: 'text'; text: string }).text).toContain('too large');
184
+ });
185
+ });
186
+
187
+ describe('isToolResult', () => {
188
+ test('returns true for valid ToolResult', () => {
189
+ const tr: ToolResult = {
190
+ content: [{ type: 'text', text: 'hello' }],
191
+ };
192
+ expect(isToolResult(tr)).toBe(true);
193
+ });
194
+
195
+ test('returns false for plain string', () => {
196
+ expect(isToolResult('hello')).toBe(false);
197
+ });
198
+
199
+ test('returns false for null', () => {
200
+ expect(isToolResult(null)).toBe(false);
201
+ });
202
+
203
+ test('returns false for object without content array', () => {
204
+ expect(isToolResult({ content: 'not an array' })).toBe(false);
205
+ });
206
+
207
+ test('returns false for object with no content field', () => {
208
+ expect(isToolResult({ data: 'something' })).toBe(false);
209
+ });
210
+ });
211
+
212
+ describe('ContentBlock in LLMMessage', () => {
213
+ test('LLMMessage accepts string content', () => {
214
+ const msg: LLMMessage = { role: 'user', content: 'Hello' };
215
+ expect(typeof msg.content).toBe('string');
216
+ });
217
+
218
+ test('LLMMessage accepts ContentBlock[] content', () => {
219
+ const msg: LLMMessage = {
220
+ role: 'tool',
221
+ content: [
222
+ { type: 'text', text: 'Screenshot captured' },
223
+ { type: 'image', source: { type: 'base64', media_type: 'image/png', data: 'abc' } },
224
+ ],
225
+ tool_call_id: 'test-id',
226
+ };
227
+ expect(Array.isArray(msg.content)).toBe(true);
228
+ expect((msg.content as ContentBlock[]).length).toBe(2);
229
+ });
230
+ });
231
+ });
@@ -0,0 +1,60 @@
1
+ export type ContentBlock =
2
+ | { type: 'text'; text: string }
3
+ | { type: 'image'; source: { type: 'base64'; media_type: string; data: string } };
4
+
5
+ export type LLMMessage = {
6
+ role: 'system' | 'user' | 'assistant' | 'tool';
7
+ content: string | ContentBlock[];
8
+ tool_calls?: LLMToolCall[]; // present on assistant messages with tool use
9
+ tool_call_id?: string; // present on tool result messages
10
+ };
11
+
12
+ export type LLMTool = {
13
+ name: string;
14
+ description: string;
15
+ parameters: Record<string, unknown>; // JSON Schema
16
+ };
17
+
18
+ export type LLMToolCall = {
19
+ id: string;
20
+ name: string;
21
+ arguments: Record<string, unknown>;
22
+ };
23
+
24
+ export type LLMResponse = {
25
+ content: string;
26
+ tool_calls: LLMToolCall[];
27
+ usage: { input_tokens: number; output_tokens: number };
28
+ model: string;
29
+ finish_reason: 'stop' | 'tool_use' | 'length' | 'error';
30
+ };
31
+
32
+ export type LLMStreamEvent =
33
+ | { type: 'text'; text: string }
34
+ | { type: 'tool_call'; tool_call: LLMToolCall }
35
+ | { type: 'done'; response: LLMResponse }
36
+ | { type: 'error'; error: string };
37
+
38
+ export type LLMOptions = {
39
+ model?: string;
40
+ temperature?: number;
41
+ max_tokens?: number;
42
+ tools?: LLMTool[];
43
+ stream?: boolean;
44
+ };
45
+
46
+ export interface LLMProvider {
47
+ name: string;
48
+ chat(messages: LLMMessage[], options?: LLMOptions): Promise<LLMResponse>;
49
+ stream(messages: LLMMessage[], options?: LLMOptions): AsyncIterable<LLMStreamEvent>;
50
+ listModels(): Promise<string[]>;
51
+ }
52
+
53
+ const MAX_IMAGE_BYTES = 5 * 1024 * 1024; // 5 MB base64 limit
54
+
55
+ export function guardImageSize(block: ContentBlock): ContentBlock {
56
+ if (block.type === 'image' && block.source.data.length > MAX_IMAGE_BYTES) {
57
+ return { type: 'text', text: '[Image too large to send — saved to disk instead]' };
58
+ }
59
+ return block;
60
+ }
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Manual test file for LLM providers
3
+ *
4
+ * Run with: bun run src/llm/test.ts
5
+ */
6
+
7
+ import { LLMManager, AnthropicProvider, OpenAIProvider, OllamaProvider } from './index.ts';
8
+ import { loadConfig } from '../config/index.ts';
9
+
10
+ async function testProviders() {
11
+ console.log('Loading config...');
12
+ const config = await loadConfig();
13
+
14
+ const manager = new LLMManager();
15
+
16
+ // Register providers based on config
17
+ if (config.llm.anthropic?.api_key) {
18
+ const anthropic = new AnthropicProvider(
19
+ config.llm.anthropic.api_key,
20
+ config.llm.anthropic.model
21
+ );
22
+ manager.registerProvider(anthropic);
23
+ console.log('Registered Anthropic provider');
24
+ }
25
+
26
+ if (config.llm.openai?.api_key) {
27
+ const openai = new OpenAIProvider(
28
+ config.llm.openai.api_key,
29
+ config.llm.openai.model
30
+ );
31
+ manager.registerProvider(openai);
32
+ console.log('Registered OpenAI provider');
33
+ }
34
+
35
+ if (config.llm.ollama) {
36
+ const ollama = new OllamaProvider(
37
+ config.llm.ollama.base_url,
38
+ config.llm.ollama.model
39
+ );
40
+ manager.registerProvider(ollama);
41
+ console.log('Registered Ollama provider');
42
+ }
43
+
44
+ // Set primary and fallbacks
45
+ manager.setPrimary(config.llm.primary);
46
+ manager.setFallbackChain(config.llm.fallback);
47
+
48
+ console.log(`\nPrimary provider: ${config.llm.primary}`);
49
+ console.log(`Fallback chain: ${config.llm.fallback.join(', ')}\n`);
50
+
51
+ // Test basic chat
52
+ console.log('Testing chat...');
53
+ const messages = [
54
+ { role: 'system' as const, content: 'You are a helpful assistant.' },
55
+ { role: 'user' as const, content: 'Say hello in exactly 5 words.' },
56
+ ];
57
+
58
+ try {
59
+ const response = await manager.chat(messages);
60
+ console.log('Response:', response.content);
61
+ console.log('Model:', response.model);
62
+ console.log('Usage:', response.usage);
63
+ console.log('Finish reason:', response.finish_reason);
64
+ } catch (err) {
65
+ console.error('Chat failed:', err);
66
+ }
67
+
68
+ // Test streaming
69
+ console.log('\n\nTesting streaming...');
70
+ try {
71
+ for await (const event of manager.stream(messages)) {
72
+ if (event.type === 'text') {
73
+ process.stdout.write(event.text);
74
+ } else if (event.type === 'done') {
75
+ console.log('\n\nStream completed!');
76
+ console.log('Model:', event.response.model);
77
+ console.log('Usage:', event.response.usage);
78
+ } else if (event.type === 'error') {
79
+ console.error('Stream error:', event.error);
80
+ }
81
+ }
82
+ } catch (err) {
83
+ console.error('Stream failed:', err);
84
+ }
85
+ }
86
+
87
+ testProviders().catch(console.error);
@@ -0,0 +1,278 @@
1
+ # Observer Layer
2
+
3
+ The Observer Layer monitors system events and emits standardized observations to the Vault. All observers implement a common interface and can be managed centrally through the `ObserverManager`.
4
+
5
+ ## Architecture
6
+
7
+ ### Common Interface
8
+
9
+ All observers implement the `Observer` interface:
10
+
11
+ ```typescript
12
+ interface Observer {
13
+ name: string;
14
+ start(): Promise<void>;
15
+ stop(): Promise<void>;
16
+ isRunning(): boolean;
17
+ onEvent(handler: ObserverEventHandler): void;
18
+ }
19
+ ```
20
+
21
+ Events are emitted in a standardized format:
22
+
23
+ ```typescript
24
+ type ObserverEvent = {
25
+ type: string;
26
+ data: Record<string, unknown>;
27
+ timestamp: number;
28
+ };
29
+ ```
30
+
31
+ ## Observers
32
+
33
+ ### FileWatcher (Fully Implemented)
34
+
35
+ Monitors file system changes in specified directories.
36
+
37
+ ```typescript
38
+ import { FileWatcher } from './file-watcher';
39
+
40
+ const watcher = new FileWatcher([
41
+ '/home/user/projects',
42
+ '/home/user/documents'
43
+ ]);
44
+
45
+ watcher.onEvent((event) => {
46
+ // event.type: 'file_change'
47
+ // event.data: { path, eventType: 'rename'|'change', filename, basePath }
48
+ });
49
+
50
+ await watcher.start();
51
+ ```
52
+
53
+ Features:
54
+ - Recursive directory watching using `node:fs` watch API
55
+ - Debouncing (100ms) to avoid duplicate rapid-fire events
56
+ - Automatic cleanup of old debounce entries to prevent memory leaks
57
+
58
+ ### ClipboardMonitor (Fully Implemented)
59
+
60
+ Polls the system clipboard and detects content changes.
61
+
62
+ ```typescript
63
+ import { ClipboardMonitor } from './clipboard';
64
+
65
+ const clipboard = new ClipboardMonitor(1000); // Poll every 1 second
66
+
67
+ clipboard.onEvent((event) => {
68
+ // event.type: 'clipboard'
69
+ // event.data: { content, length }
70
+ });
71
+
72
+ await clipboard.start();
73
+ ```
74
+
75
+ Features:
76
+ - Cross-platform clipboard reading (Linux/macOS/Windows/WSL)
77
+ - Automatic platform detection and command selection
78
+ - Silent failure on read errors to avoid spam
79
+
80
+ Platform commands used:
81
+ - Linux: `xclip` or `xsel`
82
+ - macOS: `pbpaste`
83
+ - Windows/WSL: `powershell.exe Get-Clipboard`
84
+
85
+ ### ProcessMonitor (Fully Implemented)
86
+
87
+ Monitors running processes and detects lifecycle changes.
88
+
89
+ ```typescript
90
+ import { ProcessMonitor } from './processes';
91
+
92
+ const processes = new ProcessMonitor(5000); // Poll every 5 seconds
93
+
94
+ processes.onEvent((event) => {
95
+ // event.type: 'process_started' or 'process_stopped'
96
+ // event.data: { pid, name, cpu?, memory? }
97
+ });
98
+
99
+ await processes.start();
100
+ ```
101
+
102
+ Features:
103
+ - Cross-platform process listing (Linux/macOS/Windows)
104
+ - Detects new and terminated processes
105
+ - Provides CPU and memory usage data
106
+ - Automatic cleanup of terminated process records
107
+
108
+ ### NotificationListener (Stub)
109
+
110
+ Placeholder for system notification monitoring.
111
+
112
+ **TODO:**
113
+ - Linux: Monitor D-Bus `org.freedesktop.Notifications`
114
+ - Windows: Use PowerShell to read notification center
115
+ - macOS: Use notification center API
116
+
117
+ ### CalendarSync (Stub)
118
+
119
+ Placeholder for calendar integration.
120
+
121
+ **TODO:**
122
+ - Google Calendar API integration (OAuth2)
123
+ - Microsoft Graph API (Outlook/Office 365)
124
+ - CalDAV support for generic providers
125
+
126
+ ### EmailSync (Stub)
127
+
128
+ Placeholder for email integration.
129
+
130
+ **TODO:**
131
+ - Gmail API integration (OAuth2)
132
+ - Microsoft Graph API (Outlook/Office 365)
133
+ - IMAP support for generic providers
134
+
135
+ ## ObserverManager
136
+
137
+ The `ObserverManager` provides centralized control over all observers.
138
+
139
+ ```typescript
140
+ import { ObserverManager, FileWatcher, ClipboardMonitor } from './observers';
141
+
142
+ const manager = new ObserverManager();
143
+
144
+ // Register observers
145
+ manager.register(new FileWatcher(['/path/to/watch']));
146
+ manager.register(new ClipboardMonitor(1000));
147
+
148
+ // Set event handler (typically the Vault's ingestion function)
149
+ manager.setEventHandler((event) => {
150
+ console.log(`[${event.type}]`, event.data);
151
+ // Forward to Vault for storage/analysis
152
+ });
153
+
154
+ // Start all observers
155
+ await manager.startAll();
156
+
157
+ // Get status
158
+ console.log(manager.getStatus());
159
+ // => { 'file-watcher': true, 'clipboard': true }
160
+
161
+ // Stop specific observer
162
+ await manager.stopObserver('clipboard');
163
+
164
+ // Stop all observers
165
+ await manager.stopAll();
166
+ ```
167
+
168
+ ### Manager API
169
+
170
+ - `register(observer: Observer)` - Register a new observer
171
+ - `setEventHandler(handler: ObserverEventHandler)` - Set global event handler
172
+ - `startAll()` - Start all registered observers
173
+ - `stopAll()` - Stop all registered observers
174
+ - `startObserver(name: string)` - Start specific observer
175
+ - `stopObserver(name: string)` - Stop specific observer
176
+ - `getStatus()` - Get running status of all observers
177
+ - `listObservers()` - Get list of registered observer names
178
+
179
+ ## Usage Example
180
+
181
+ See `example.ts` for a complete working example:
182
+
183
+ ```bash
184
+ bun run src/observers/example.ts
185
+ ```
186
+
187
+ This will run all observers for 30 seconds and log events to the console.
188
+
189
+ ## Integration with Vault
190
+
191
+ The Observer Layer is designed to integrate with the Vault for persistent storage:
192
+
193
+ ```typescript
194
+ import { ObserverManager } from './observers';
195
+ import { Vault } from '../vault';
196
+
197
+ const vault = new Vault();
198
+ const manager = new ObserverManager();
199
+
200
+ // Register all observers...
201
+ manager.register(new FileWatcher(['/home/user/projects']));
202
+ // ... etc
203
+
204
+ // Connect to Vault
205
+ manager.setEventHandler((event) => {
206
+ vault.storeObservation(event);
207
+ });
208
+
209
+ await manager.startAll();
210
+ ```
211
+
212
+ ## Event Types
213
+
214
+ Current event types emitted by observers:
215
+
216
+ - `file_change` - File system change detected
217
+ - `clipboard` - Clipboard content changed
218
+ - `process_started` - New process started
219
+ - `process_stopped` - Process terminated
220
+
221
+ Future event types (when stubs are implemented):
222
+
223
+ - `notification` - System notification received
224
+ - `calendar_event` - Calendar event upcoming/created/updated
225
+ - `new_email` - New email received
226
+ - `email_read` - Email marked as read
227
+ - `email_sent` - Email sent
228
+
229
+ ## Performance Considerations
230
+
231
+ - **FileWatcher**: Uses native `node:fs` watch API with minimal overhead
232
+ - **ClipboardMonitor**: Polling-based, configurable interval (default 1s)
233
+ - **ProcessMonitor**: Polling-based, configurable interval (default 5s)
234
+ - **Debouncing**: FileWatcher implements 100ms debouncing for rapid changes
235
+ - **Memory**: Old debounce entries are automatically cleaned up
236
+
237
+ ## Error Handling
238
+
239
+ All observers implement graceful error handling:
240
+
241
+ - Silent failures for transient errors (e.g., clipboard read failures)
242
+ - Console logging for persistent errors
243
+ - Proper cleanup in `stop()` methods
244
+ - Error isolation (one observer's failure doesn't affect others)
245
+
246
+ ## Platform Support
247
+
248
+ - **Linux**: Full support for all implemented observers
249
+ - **macOS**: Full support for all implemented observers
250
+ - **Windows**: Full support for all implemented observers
251
+ - **WSL**: Full support (uses Windows clipboard via PowerShell)
252
+
253
+ ## Testing
254
+
255
+ Run the example to test all observers:
256
+
257
+ ```bash
258
+ bun run src/observers/example.ts
259
+ ```
260
+
261
+ Or create custom tests:
262
+
263
+ ```typescript
264
+ import { test, expect } from 'bun:test';
265
+ import { FileWatcher } from './file-watcher';
266
+
267
+ test('FileWatcher starts and stops', async () => {
268
+ const watcher = new FileWatcher(['/tmp']);
269
+
270
+ expect(watcher.isRunning()).toBe(false);
271
+
272
+ await watcher.start();
273
+ expect(watcher.isRunning()).toBe(true);
274
+
275
+ await watcher.stop();
276
+ expect(watcher.isRunning()).toBe(false);
277
+ });
278
+ ```