@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,487 @@
1
+ import { test, expect, describe, beforeEach, afterEach, mock } from 'bun:test';
2
+ import { AnthropicProvider } from './anthropic.ts';
3
+ import { OpenAIProvider, isOfficialOpenAI, resolveOpenAIBaseUrl } from './openai.ts';
4
+ import { GroqProvider } from './groq.ts';
5
+ import { OllamaProvider } from './ollama.ts';
6
+ import { OpenRouterProvider } from './openrouter.ts';
7
+ import { LLMManager } from './manager.ts';
8
+ import { guardImageSize, type LLMMessage, type ContentBlock } from './provider.ts';
9
+ import { isToolResult, type ToolResult } from '../actions/tools/registry.ts';
10
+
11
+ describe('LLM Provider Types', () => {
12
+ test('AnthropicProvider can be instantiated', () => {
13
+ const provider = new AnthropicProvider('test-key', 'test-model');
14
+ expect(provider.name).toBe('anthropic');
15
+ });
16
+
17
+ test('OpenAIProvider can be instantiated', () => {
18
+ const provider = new OpenAIProvider('test-key', 'test-model');
19
+ expect(provider.name).toBe('openai');
20
+ });
21
+
22
+ test('GroqProvider can be instantiated', () => {
23
+ const provider = new GroqProvider('test-key', 'test-model');
24
+ expect(provider.name).toBe('groq');
25
+ });
26
+
27
+ test('OllamaProvider can be instantiated', () => {
28
+ const provider = new OllamaProvider('http://localhost:11434', 'llama3');
29
+ expect(provider.name).toBe('ollama');
30
+ });
31
+
32
+ test('OpenRouterProvider can be instantiated', () => {
33
+ const provider = new OpenRouterProvider('test-key', 'anthropic/claude-sonnet-4');
34
+ expect(provider.name).toBe('openrouter');
35
+ });
36
+ });
37
+
38
+ describe('LLMManager', () => {
39
+ test('can register providers', () => {
40
+ const manager = new LLMManager();
41
+ const anthropic = new AnthropicProvider('test-key');
42
+
43
+ manager.registerProvider(anthropic);
44
+ expect(manager.getProvider('anthropic')).toBe(anthropic);
45
+ });
46
+
47
+ test('sets first registered provider as primary', () => {
48
+ const manager = new LLMManager();
49
+ const anthropic = new AnthropicProvider('test-key');
50
+
51
+ manager.registerProvider(anthropic);
52
+ // Primary is set automatically
53
+ expect(manager.getProvider('anthropic')).toBeDefined();
54
+ });
55
+
56
+ test('can change primary provider', () => {
57
+ const manager = new LLMManager();
58
+ const anthropic = new AnthropicProvider('test-key-1');
59
+ const openai = new OpenAIProvider('test-key-2');
60
+
61
+ manager.registerProvider(anthropic);
62
+ manager.registerProvider(openai);
63
+ manager.setPrimary('openai');
64
+
65
+ // Should not throw
66
+ expect(manager.getProvider('openai')).toBeDefined();
67
+ });
68
+
69
+ test('throws when setting non-existent provider as primary', () => {
70
+ const manager = new LLMManager();
71
+ expect(() => manager.setPrimary('nonexistent')).toThrow();
72
+ });
73
+
74
+ test('can set fallback chain', () => {
75
+ const manager = new LLMManager();
76
+ const anthropic = new AnthropicProvider('test-key-1');
77
+ const openai = new OpenAIProvider('test-key-2');
78
+
79
+ manager.registerProvider(anthropic);
80
+ manager.registerProvider(openai);
81
+ manager.setPrimary('anthropic');
82
+ manager.setFallbackChain(['openai']);
83
+
84
+ // Should not throw
85
+ expect(manager.getProvider('anthropic')).toBeDefined();
86
+ expect(manager.getProvider('openai')).toBeDefined();
87
+ });
88
+
89
+ test('throws when setting non-existent fallback provider', () => {
90
+ const manager = new LLMManager();
91
+ const anthropic = new AnthropicProvider('test-key');
92
+
93
+ manager.registerProvider(anthropic);
94
+ expect(() => manager.setFallbackChain(['nonexistent'])).toThrow();
95
+ });
96
+ });
97
+
98
+ describe('Message Types', () => {
99
+ test('LLMMessage has correct structure', () => {
100
+ const message: LLMMessage = {
101
+ role: 'user',
102
+ content: 'Hello',
103
+ };
104
+
105
+ expect(message.role).toBe('user');
106
+ expect(message.content).toBe('Hello');
107
+ });
108
+
109
+ test('supports all message roles', () => {
110
+ const messages: LLMMessage[] = [
111
+ { role: 'system', content: 'You are helpful' },
112
+ { role: 'user', content: 'Hello' },
113
+ { role: 'assistant', content: 'Hi there' },
114
+ ];
115
+
116
+ expect(messages).toHaveLength(3);
117
+ expect(messages[0]!.role).toBe('system');
118
+ expect(messages[1]!.role).toBe('user');
119
+ expect(messages[2]!.role).toBe('assistant');
120
+ });
121
+ });
122
+
123
+ describe('Provider URLs', () => {
124
+ test('AnthropicProvider uses correct API URL', () => {
125
+ const provider = new AnthropicProvider('test-key') as any;
126
+ expect(provider.apiUrl).toBe('https://api.anthropic.com/v1/messages');
127
+ });
128
+
129
+ test('OpenAIProvider uses correct API URL', () => {
130
+ const provider = new OpenAIProvider('test-key') as any;
131
+ expect(provider.apiUrl).toBe('https://api.openai.com/v1/chat/completions');
132
+ });
133
+
134
+ test('OpenAIProvider uses custom compatible base URL as the request base', () => {
135
+ const provider = new OpenAIProvider('test-key', 'test-model', 'https://gateway.example.com/v1/') as any;
136
+ expect(provider.baseUrl).toBe('https://gateway.example.com/v1/');
137
+ expect(provider.apiUrl).toBe('https://gateway.example.com/v1/chat/completions');
138
+ });
139
+
140
+ test('OpenAIProvider preserves custom path prefixes in compatible base URLs', () => {
141
+ const provider = new OpenAIProvider('test-key', 'test-model', 'https://gateway.example.com/openai/') as any;
142
+ expect(provider.baseUrl).toBe('https://gateway.example.com/openai/');
143
+ expect(provider.apiUrl).toBe('https://gateway.example.com/openai/chat/completions');
144
+ });
145
+
146
+ test('OpenRouterProvider uses correct API URL', () => {
147
+ const provider = new OpenRouterProvider('test-key') as any;
148
+ expect(provider.apiUrl).toBe('https://openrouter.ai/api/v1/chat/completions');
149
+ });
150
+
151
+ test('GroqProvider uses correct API URL', () => {
152
+ const provider = new GroqProvider('test-key') as any;
153
+ expect(provider.apiUrl).toBe('https://api.groq.com/openai/v1/chat/completions');
154
+ });
155
+
156
+ test('OllamaProvider uses correct base URL', () => {
157
+ const provider = new OllamaProvider() as any;
158
+ expect(provider.baseUrl).toBe('http://localhost:11434');
159
+ });
160
+
161
+ test('OllamaProvider removes trailing slash from base URL', () => {
162
+ const provider = new OllamaProvider('http://localhost:11434/') as any;
163
+ expect(provider.baseUrl).toBe('http://localhost:11434');
164
+ });
165
+ });
166
+
167
+ describe('OpenAI base URL resolution', () => {
168
+ test('uses official OpenAI API when omitted', () => {
169
+ expect(resolveOpenAIBaseUrl().toString()).toBe('https://api.openai.com/v1/');
170
+ });
171
+
172
+ test('adds a trailing slash so relative endpoints resolve consistently', () => {
173
+ expect(resolveOpenAIBaseUrl('https://gateway.example.com/v1').toString()).toBe('https://gateway.example.com/v1/');
174
+ });
175
+
176
+ test('preserves the provided base path', () => {
177
+ expect(resolveOpenAIBaseUrl('https://gateway.example.com/openai').toString()).toBe('https://gateway.example.com/openai/');
178
+ });
179
+
180
+ test('rejects invalid URLs', () => {
181
+ expect(() => resolveOpenAIBaseUrl('not-a-url')).toThrow();
182
+ });
183
+
184
+ test('treats the default endpoint as official OpenAI', () => {
185
+ expect(isOfficialOpenAI()).toBe(true);
186
+ expect(isOfficialOpenAI('https://api.openai.com/v1')).toBe(true);
187
+ });
188
+
189
+ test('treats custom compatible gateways as non-official OpenAI', () => {
190
+ expect(isOfficialOpenAI('https://gateway.example.com/openai')).toBe(false);
191
+ });
192
+ });
193
+
194
+ describe('Default Models', () => {
195
+ test('AnthropicProvider has correct default model', () => {
196
+ const provider = new AnthropicProvider('test-key') as any;
197
+ expect(provider.defaultModel).toBe('claude-sonnet-4-5-20250929');
198
+ });
199
+
200
+ test('OpenAIProvider has correct default model', () => {
201
+ const provider = new OpenAIProvider('test-key') as any;
202
+ expect(provider.defaultModel).toBe('gpt-4o');
203
+ });
204
+
205
+ test('OllamaProvider has correct default model', () => {
206
+ const provider = new OllamaProvider() as any;
207
+ expect(provider.defaultModel).toBe('llama3');
208
+ });
209
+
210
+ test('GroqProvider has correct default model', () => {
211
+ const provider = new GroqProvider('test-key') as any;
212
+ expect(provider.defaultModel).toBe('llama-3.3-70b-versatile');
213
+ });
214
+
215
+ test('OpenRouterProvider has correct default model', () => {
216
+ const provider = new OpenRouterProvider('test-key') as any;
217
+ expect(provider.defaultModel).toBe('anthropic/claude-sonnet-4');
218
+ });
219
+
220
+ test('can override default models', () => {
221
+ const anthropic = new AnthropicProvider('key', 'custom-model') as any;
222
+ const openai = new OpenAIProvider('key', 'custom-model') as any;
223
+ const groq = new GroqProvider('key', 'custom-model') as any;
224
+ const ollama = new OllamaProvider('http://localhost:11434', 'custom-model') as any;
225
+ const openrouter = new OpenRouterProvider('key', 'custom-model') as any;
226
+
227
+ expect(anthropic.defaultModel).toBe('custom-model');
228
+ expect(openai.defaultModel).toBe('custom-model');
229
+ expect(groq.defaultModel).toBe('custom-model');
230
+ expect(ollama.defaultModel).toBe('custom-model');
231
+ expect(openrouter.defaultModel).toBe('custom-model');
232
+ });
233
+ });
234
+
235
+ describe('Vision Support', () => {
236
+ describe('guardImageSize', () => {
237
+ test('passes text blocks through unchanged', () => {
238
+ const block: ContentBlock = { type: 'text', text: 'hello' };
239
+ expect(guardImageSize(block)).toBe(block);
240
+ });
241
+
242
+ test('passes small images through unchanged', () => {
243
+ const block: ContentBlock = {
244
+ type: 'image',
245
+ source: { type: 'base64', media_type: 'image/png', data: 'abc123' },
246
+ };
247
+ expect(guardImageSize(block)).toBe(block);
248
+ });
249
+
250
+ test('replaces oversized images with text warning', () => {
251
+ const bigData = 'x'.repeat(6 * 1024 * 1024); // 6 MB
252
+ const block: ContentBlock = {
253
+ type: 'image',
254
+ source: { type: 'base64', media_type: 'image/png', data: bigData },
255
+ };
256
+ const result = guardImageSize(block);
257
+ expect(result.type).toBe('text');
258
+ expect((result as { type: 'text'; text: string }).text).toContain('too large');
259
+ });
260
+ });
261
+
262
+ describe('isToolResult', () => {
263
+ test('returns true for valid ToolResult', () => {
264
+ const tr: ToolResult = {
265
+ content: [{ type: 'text', text: 'hello' }],
266
+ };
267
+ expect(isToolResult(tr)).toBe(true);
268
+ });
269
+
270
+ test('returns false for plain string', () => {
271
+ expect(isToolResult('hello')).toBe(false);
272
+ });
273
+
274
+ test('returns false for null', () => {
275
+ expect(isToolResult(null)).toBe(false);
276
+ });
277
+
278
+ test('returns false for object without content array', () => {
279
+ expect(isToolResult({ content: 'not an array' })).toBe(false);
280
+ });
281
+
282
+ test('returns false for object with no content field', () => {
283
+ expect(isToolResult({ data: 'something' })).toBe(false);
284
+ });
285
+ });
286
+
287
+ describe('ContentBlock in LLMMessage', () => {
288
+ test('LLMMessage accepts string content', () => {
289
+ const msg: LLMMessage = { role: 'user', content: 'Hello' };
290
+ expect(typeof msg.content).toBe('string');
291
+ });
292
+
293
+ test('LLMMessage accepts ContentBlock[] content', () => {
294
+ const msg: LLMMessage = {
295
+ role: 'tool',
296
+ content: [
297
+ { type: 'text', text: 'Screenshot captured' },
298
+ { type: 'image', source: { type: 'base64', media_type: 'image/png', data: 'abc' } },
299
+ ],
300
+ tool_call_id: 'test-id',
301
+ };
302
+ expect(Array.isArray(msg.content)).toBe(true);
303
+ expect((msg.content as ContentBlock[]).length).toBe(2);
304
+ });
305
+ });
306
+ });
307
+
308
+ describe('Tool Call Conversion', () => {
309
+ const toolUseConversation: LLMMessage[] = [
310
+ { role: 'system', content: 'You are helpful' },
311
+ { role: 'user', content: 'What time is it?' },
312
+ {
313
+ role: 'assistant',
314
+ content: '',
315
+ tool_calls: [
316
+ { id: 'call_1', name: 'get_time', arguments: { timezone: 'UTC' } },
317
+ ],
318
+ },
319
+ {
320
+ role: 'tool',
321
+ content: '2026-03-30T12:00:00Z',
322
+ tool_call_id: 'call_1',
323
+ },
324
+ ];
325
+
326
+ test('OpenAIProvider preserves tool_calls on assistant messages', () => {
327
+ const provider = new OpenAIProvider('test-key') as any;
328
+ const converted = provider.convertMessages(toolUseConversation);
329
+
330
+ const assistant = converted[2];
331
+ expect(assistant.role).toBe('assistant');
332
+ expect(assistant.tool_calls).toBeDefined();
333
+ expect(assistant.tool_calls).toHaveLength(1);
334
+ expect(assistant.tool_calls[0].id).toBe('call_1');
335
+ expect(assistant.tool_calls[0].type).toBe('function');
336
+ expect(assistant.tool_calls[0].function.name).toBe('get_time');
337
+ expect(assistant.tool_calls[0].function.arguments).toBe('{"timezone":"UTC"}');
338
+ });
339
+
340
+ test('OpenAIProvider preserves tool_call_id on tool messages', () => {
341
+ const provider = new OpenAIProvider('test-key') as any;
342
+ const converted = provider.convertMessages(toolUseConversation);
343
+
344
+ const tool = converted[3];
345
+ expect(tool.role).toBe('tool');
346
+ expect(tool.tool_call_id).toBe('call_1');
347
+ expect(tool.content).toBe('2026-03-30T12:00:00Z');
348
+ });
349
+
350
+ test('GroqProvider preserves tool_calls on assistant messages', () => {
351
+ const provider = new GroqProvider('test-key') as any;
352
+ const converted = provider.convertMessages(toolUseConversation);
353
+
354
+ const assistant = converted[2];
355
+ expect(assistant.role).toBe('assistant');
356
+ expect(assistant.tool_calls).toBeDefined();
357
+ expect(assistant.tool_calls).toHaveLength(1);
358
+ expect(assistant.tool_calls[0].id).toBe('call_1');
359
+ expect(assistant.tool_calls[0].type).toBe('function');
360
+ expect(assistant.tool_calls[0].function.name).toBe('get_time');
361
+ expect(assistant.tool_calls[0].function.arguments).toBe('{"timezone":"UTC"}');
362
+ expect(assistant.content).toBeNull();
363
+ });
364
+
365
+ test('GroqProvider preserves tool_call_id on tool messages', () => {
366
+ const provider = new GroqProvider('test-key') as any;
367
+ const converted = provider.convertMessages(toolUseConversation);
368
+
369
+ const tool = converted[3];
370
+ expect(tool.role).toBe('tool');
371
+ expect(tool.tool_call_id).toBe('call_1');
372
+ expect(tool.content).toBe('2026-03-30T12:00:00Z');
373
+ });
374
+
375
+ test('Messages without tool_calls omit the field', () => {
376
+ const provider = new OpenAIProvider('test-key') as any;
377
+ const converted = provider.convertMessages([
378
+ { role: 'user', content: 'Hello' },
379
+ { role: 'assistant', content: 'Hi there' },
380
+ ]);
381
+ expect(converted[0].tool_calls).toBeUndefined();
382
+ expect(converted[1].tool_calls).toBeUndefined();
383
+ expect(converted[0].tool_call_id).toBeUndefined();
384
+ });
385
+ });
386
+
387
+ describe('Groq request shaping', () => {
388
+ const originalFetch = globalThis.fetch;
389
+
390
+ beforeEach(() => {
391
+ globalThis.fetch = mock(async (_url: string, init?: RequestInit) => {
392
+ return new Response(JSON.stringify({
393
+ id: 'cmpl_test',
394
+ object: 'chat.completion',
395
+ created: Date.now(),
396
+ model: 'llama-test',
397
+ choices: [
398
+ {
399
+ index: 0,
400
+ message: { role: 'assistant', content: 'ok' },
401
+ finish_reason: 'stop',
402
+ },
403
+ ],
404
+ usage: {
405
+ prompt_tokens: 10,
406
+ completion_tokens: 5,
407
+ total_tokens: 15,
408
+ },
409
+ }), {
410
+ status: 200,
411
+ headers: {
412
+ 'Content-Type': 'application/json',
413
+ 'x-test-body': typeof init?.body === 'string' ? init.body : '',
414
+ },
415
+ });
416
+ }) as unknown as typeof fetch;
417
+ });
418
+
419
+ afterEach(() => {
420
+ globalThis.fetch = originalFetch;
421
+ });
422
+
423
+ test('GroqProvider uses Groq-compatible tool fields', async () => {
424
+ const provider = new GroqProvider('test-key') as any;
425
+ const messages: LLMMessage[] = [
426
+ { role: 'system', content: 'You are helpful.' },
427
+ { role: 'user', content: 'Use the weather tool.' },
428
+ ];
429
+
430
+ await provider.chat(messages, {
431
+ max_tokens: 321,
432
+ tools: [
433
+ {
434
+ name: 'weather_lookup',
435
+ description: 'Look up weather',
436
+ parameters: {
437
+ type: 'object',
438
+ properties: {
439
+ city: { type: 'string' },
440
+ },
441
+ required: ['city'],
442
+ },
443
+ },
444
+ ],
445
+ });
446
+
447
+ const fetchMock = globalThis.fetch as unknown as ReturnType<typeof mock>;
448
+ expect(fetchMock).toHaveBeenCalledTimes(1);
449
+ const init = fetchMock.mock.calls[0]?.[1] as RequestInit;
450
+ const body = JSON.parse(String(init.body));
451
+
452
+ expect(body.max_completion_tokens).toBe(321);
453
+ expect(body.max_tokens).toBeUndefined();
454
+ expect(body.tool_choice).toBe('auto');
455
+ expect(body.parallel_tool_calls).toBe(true);
456
+ expect(body.tools).toHaveLength(1);
457
+ });
458
+
459
+ test('GroqProvider trims oversized history but keeps system and latest turn', async () => {
460
+ const provider = new GroqProvider('test-key') as any;
461
+ const long = 'x'.repeat(12_000);
462
+ const messages: LLMMessage[] = [
463
+ { role: 'system', content: 'System prompt' },
464
+ { role: 'user', content: long },
465
+ { role: 'assistant', content: long },
466
+ { role: 'user', content: 'latest question' },
467
+ ];
468
+
469
+ await provider.chat(messages, {
470
+ tools: [
471
+ {
472
+ name: 'delegate_task',
473
+ description: 'Delegate focused work',
474
+ parameters: { type: 'object', properties: {}, required: [] },
475
+ },
476
+ ],
477
+ });
478
+
479
+ const fetchMock = globalThis.fetch as unknown as ReturnType<typeof mock>;
480
+ const init = fetchMock.mock.calls[0]?.[1] as RequestInit;
481
+ const body = JSON.parse(String(init.body));
482
+
483
+ expect(body.messages[0].role).toBe('system');
484
+ expect(body.messages.at(-1).content).toBe('latest question');
485
+ expect(body.messages.length).toBeLessThan(messages.length);
486
+ });
487
+ });
@@ -0,0 +1,61 @@
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
+ tool_choice?: 'auto' | 'none' | 'required'; // 'auto' enables tool calling when available
45
+ };
46
+
47
+ export interface LLMProvider {
48
+ name: string;
49
+ chat(messages: LLMMessage[], options?: LLMOptions): Promise<LLMResponse>;
50
+ stream(messages: LLMMessage[], options?: LLMOptions): AsyncIterable<LLMStreamEvent>;
51
+ listModels(): Promise<string[]>;
52
+ }
53
+
54
+ const MAX_IMAGE_BYTES = 5 * 1024 * 1024; // 5 MB base64 limit
55
+
56
+ export function guardImageSize(block: ContentBlock): ContentBlock {
57
+ if (block.type === 'image' && block.source.data.length > MAX_IMAGE_BYTES) {
58
+ return { type: 'text', text: '[Image too large to send — saved to disk instead]' };
59
+ }
60
+ return block;
61
+ }
@@ -0,0 +1,88 @@
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
+ config.llm.openai.base_url,
31
+ );
32
+ manager.registerProvider(openai);
33
+ console.log('Registered OpenAI provider');
34
+ }
35
+
36
+ if (config.llm.ollama) {
37
+ const ollama = new OllamaProvider(
38
+ config.llm.ollama.base_url,
39
+ config.llm.ollama.model
40
+ );
41
+ manager.registerProvider(ollama);
42
+ console.log('Registered Ollama provider');
43
+ }
44
+
45
+ // Set primary and fallbacks
46
+ manager.setPrimary(config.llm.primary);
47
+ manager.setFallbackChain(config.llm.fallback);
48
+
49
+ console.log(`\nPrimary provider: ${config.llm.primary}`);
50
+ console.log(`Fallback chain: ${config.llm.fallback.join(', ')}\n`);
51
+
52
+ // Test basic chat
53
+ console.log('Testing chat...');
54
+ const messages = [
55
+ { role: 'system' as const, content: 'You are a helpful assistant.' },
56
+ { role: 'user' as const, content: 'Say hello in exactly 5 words.' },
57
+ ];
58
+
59
+ try {
60
+ const response = await manager.chat(messages);
61
+ console.log('Response:', response.content);
62
+ console.log('Model:', response.model);
63
+ console.log('Usage:', response.usage);
64
+ console.log('Finish reason:', response.finish_reason);
65
+ } catch (err) {
66
+ console.error('Chat failed:', err);
67
+ }
68
+
69
+ // Test streaming
70
+ console.log('\n\nTesting streaming...');
71
+ try {
72
+ for await (const event of manager.stream(messages)) {
73
+ if (event.type === 'text') {
74
+ process.stdout.write(event.text);
75
+ } else if (event.type === 'done') {
76
+ console.log('\n\nStream completed!');
77
+ console.log('Model:', event.response.model);
78
+ console.log('Usage:', event.response.usage);
79
+ } else if (event.type === 'error') {
80
+ console.error('Stream error:', event.error);
81
+ }
82
+ }
83
+ } catch (err) {
84
+ console.error('Stream failed:', err);
85
+ }
86
+ }
87
+
88
+ testProviders().catch(console.error);