@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.
- package/LICENSE +153 -0
- package/README.md +428 -0
- package/bin/jarvis.ts +449 -0
- package/package.json +79 -0
- package/roles/activity-observer.yaml +60 -0
- package/roles/ceo-founder.yaml +144 -0
- package/roles/chief-of-staff.yaml +158 -0
- package/roles/dev-lead.yaml +182 -0
- package/roles/executive-assistant.yaml +77 -0
- package/roles/marketing-director.yaml +168 -0
- package/roles/personal-assistant.yaml +266 -0
- package/roles/research-specialist.yaml +60 -0
- package/roles/specialists/content-writer.yaml +53 -0
- package/roles/specialists/customer-support.yaml +57 -0
- package/roles/specialists/data-analyst.yaml +57 -0
- package/roles/specialists/financial-analyst.yaml +56 -0
- package/roles/specialists/hr-specialist.yaml +55 -0
- package/roles/specialists/legal-advisor.yaml +58 -0
- package/roles/specialists/marketing-strategist.yaml +56 -0
- package/roles/specialists/project-coordinator.yaml +55 -0
- package/roles/specialists/research-analyst.yaml +58 -0
- package/roles/specialists/software-engineer.yaml +57 -0
- package/roles/specialists/system-administrator.yaml +57 -0
- package/roles/system-admin.yaml +76 -0
- package/scripts/ensure-bun.cjs +16 -0
- package/src/actions/README.md +421 -0
- package/src/actions/app-control/desktop-controller.test.ts +26 -0
- package/src/actions/app-control/desktop-controller.ts +438 -0
- package/src/actions/app-control/interface.ts +64 -0
- package/src/actions/app-control/linux.ts +273 -0
- package/src/actions/app-control/macos.ts +54 -0
- package/src/actions/app-control/sidecar-launcher.test.ts +23 -0
- package/src/actions/app-control/sidecar-launcher.ts +286 -0
- package/src/actions/app-control/windows.ts +44 -0
- package/src/actions/browser/cdp.ts +138 -0
- package/src/actions/browser/chrome-launcher.ts +261 -0
- package/src/actions/browser/session.ts +506 -0
- package/src/actions/browser/stealth.ts +49 -0
- package/src/actions/index.ts +20 -0
- package/src/actions/terminal/executor.ts +157 -0
- package/src/actions/terminal/wsl-bridge.ts +126 -0
- package/src/actions/test.ts +93 -0
- package/src/actions/tools/agents.ts +363 -0
- package/src/actions/tools/builtin.ts +950 -0
- package/src/actions/tools/commitments.ts +192 -0
- package/src/actions/tools/content.ts +217 -0
- package/src/actions/tools/delegate.ts +147 -0
- package/src/actions/tools/desktop.test.ts +55 -0
- package/src/actions/tools/desktop.ts +305 -0
- package/src/actions/tools/documents.ts +169 -0
- package/src/actions/tools/goals.ts +376 -0
- package/src/actions/tools/local-tools-guard.ts +31 -0
- package/src/actions/tools/registry.ts +173 -0
- package/src/actions/tools/research.ts +111 -0
- package/src/actions/tools/sidecar-list.ts +57 -0
- package/src/actions/tools/sidecar-route.ts +105 -0
- package/src/actions/tools/workflows.ts +216 -0
- package/src/agents/agent.ts +132 -0
- package/src/agents/delegation.ts +107 -0
- package/src/agents/hierarchy.ts +113 -0
- package/src/agents/index.ts +19 -0
- package/src/agents/messaging.ts +125 -0
- package/src/agents/orchestrator.ts +592 -0
- package/src/agents/role-discovery.ts +61 -0
- package/src/agents/sub-agent-runner.ts +309 -0
- package/src/agents/task-manager.ts +151 -0
- package/src/authority/approval-delivery.ts +59 -0
- package/src/authority/approval.ts +196 -0
- package/src/authority/audit.ts +158 -0
- package/src/authority/authority.test.ts +519 -0
- package/src/authority/deferred-executor.ts +103 -0
- package/src/authority/emergency.ts +66 -0
- package/src/authority/engine.ts +301 -0
- package/src/authority/index.ts +12 -0
- package/src/authority/learning.ts +111 -0
- package/src/authority/tool-action-map.ts +74 -0
- package/src/awareness/analytics.ts +466 -0
- package/src/awareness/awareness.test.ts +332 -0
- package/src/awareness/capture-engine.ts +305 -0
- package/src/awareness/context-graph.ts +130 -0
- package/src/awareness/context-tracker.ts +349 -0
- package/src/awareness/index.ts +25 -0
- package/src/awareness/intelligence.ts +321 -0
- package/src/awareness/ocr-engine.ts +88 -0
- package/src/awareness/service.ts +528 -0
- package/src/awareness/struggle-detector.ts +342 -0
- package/src/awareness/suggestion-engine.ts +476 -0
- package/src/awareness/types.ts +201 -0
- package/src/cli/autostart.ts +417 -0
- package/src/cli/deps.ts +449 -0
- package/src/cli/doctor.ts +238 -0
- package/src/cli/helpers.ts +401 -0
- package/src/cli/onboard.ts +827 -0
- package/src/cli/uninstall.test.ts +37 -0
- package/src/cli/uninstall.ts +202 -0
- package/src/comms/README.md +329 -0
- package/src/comms/auth-error.html +48 -0
- package/src/comms/channels/discord.ts +228 -0
- package/src/comms/channels/signal.ts +56 -0
- package/src/comms/channels/telegram.ts +316 -0
- package/src/comms/channels/whatsapp.ts +60 -0
- package/src/comms/channels.test.ts +173 -0
- package/src/comms/dashboard-auth.ts +75 -0
- package/src/comms/desktop-notify.ts +114 -0
- package/src/comms/example.ts +129 -0
- package/src/comms/index.ts +129 -0
- package/src/comms/streaming.ts +149 -0
- package/src/comms/voice.test.ts +504 -0
- package/src/comms/voice.ts +341 -0
- package/src/comms/websocket.test.ts +409 -0
- package/src/comms/websocket.ts +669 -0
- package/src/config/README.md +389 -0
- package/src/config/index.ts +6 -0
- package/src/config/loader.test.ts +183 -0
- package/src/config/loader.ts +148 -0
- package/src/config/types.ts +293 -0
- package/src/daemon/README.md +232 -0
- package/src/daemon/agent-service-interface.ts +9 -0
- package/src/daemon/agent-service.ts +667 -0
- package/src/daemon/api-routes.ts +3067 -0
- package/src/daemon/background-agent-service.ts +396 -0
- package/src/daemon/background-agent.test.ts +78 -0
- package/src/daemon/channel-service.ts +201 -0
- package/src/daemon/commitment-executor.ts +297 -0
- package/src/daemon/dashboard-auth.test.ts +170 -0
- package/src/daemon/event-classifier.ts +239 -0
- package/src/daemon/event-coalescer.ts +123 -0
- package/src/daemon/event-reactor.ts +214 -0
- package/src/daemon/flock.c +7 -0
- package/src/daemon/health.ts +220 -0
- package/src/daemon/index.ts +1070 -0
- package/src/daemon/llm-settings.test.ts +78 -0
- package/src/daemon/llm-settings.ts +450 -0
- package/src/daemon/observer-service.ts +150 -0
- package/src/daemon/pid.test.ts +283 -0
- package/src/daemon/pid.ts +224 -0
- package/src/daemon/research-queue.ts +155 -0
- package/src/daemon/services.ts +175 -0
- package/src/daemon/ws-service.ts +926 -0
- package/src/global.d.ts +4 -0
- package/src/goals/accountability.ts +240 -0
- package/src/goals/awareness-bridge.ts +185 -0
- package/src/goals/estimator.ts +185 -0
- package/src/goals/events.ts +28 -0
- package/src/goals/goals.test.ts +400 -0
- package/src/goals/integration.test.ts +329 -0
- package/src/goals/nl-builder.test.ts +220 -0
- package/src/goals/nl-builder.ts +256 -0
- package/src/goals/rhythm.test.ts +177 -0
- package/src/goals/rhythm.ts +275 -0
- package/src/goals/service.test.ts +135 -0
- package/src/goals/service.ts +407 -0
- package/src/goals/types.ts +106 -0
- package/src/goals/workflow-bridge.ts +96 -0
- package/src/integrations/google-api.ts +134 -0
- package/src/integrations/google-auth.ts +175 -0
- package/src/llm/README.md +291 -0
- package/src/llm/anthropic.ts +400 -0
- package/src/llm/gemini.ts +380 -0
- package/src/llm/groq.ts +406 -0
- package/src/llm/history.ts +147 -0
- package/src/llm/index.ts +21 -0
- package/src/llm/manager.ts +226 -0
- package/src/llm/ollama.ts +316 -0
- package/src/llm/openai.ts +411 -0
- package/src/llm/openrouter.ts +390 -0
- package/src/llm/provider.test.ts +487 -0
- package/src/llm/provider.ts +61 -0
- package/src/llm/test.ts +88 -0
- package/src/observers/README.md +278 -0
- package/src/observers/calendar.ts +113 -0
- package/src/observers/clipboard.ts +136 -0
- package/src/observers/email.ts +109 -0
- package/src/observers/example.ts +58 -0
- package/src/observers/file-watcher.ts +124 -0
- package/src/observers/index.ts +159 -0
- package/src/observers/notifications.ts +197 -0
- package/src/observers/observers.test.ts +203 -0
- package/src/observers/processes.ts +225 -0
- package/src/personality/README.md +61 -0
- package/src/personality/adapter.ts +196 -0
- package/src/personality/index.ts +20 -0
- package/src/personality/learner.ts +209 -0
- package/src/personality/model.ts +132 -0
- package/src/personality/personality.test.ts +236 -0
- package/src/roles/README.md +252 -0
- package/src/roles/authority.ts +120 -0
- package/src/roles/example-usage.ts +198 -0
- package/src/roles/index.ts +42 -0
- package/src/roles/loader.ts +143 -0
- package/src/roles/prompt-builder.ts +218 -0
- package/src/roles/test-multi.ts +102 -0
- package/src/roles/test-role.yaml +77 -0
- package/src/roles/test-utils.ts +93 -0
- package/src/roles/test.ts +106 -0
- package/src/roles/tool-guide.ts +195 -0
- package/src/roles/types.ts +36 -0
- package/src/roles/utils.ts +200 -0
- package/src/scripts/google-setup.ts +168 -0
- package/src/sidecar/connection.ts +179 -0
- package/src/sidecar/index.ts +6 -0
- package/src/sidecar/manager.ts +542 -0
- package/src/sidecar/protocol.ts +85 -0
- package/src/sidecar/rpc.ts +161 -0
- package/src/sidecar/scheduler.ts +136 -0
- package/src/sidecar/types.ts +112 -0
- package/src/sidecar/validator.ts +144 -0
- package/src/sites/builder-tools.ts +215 -0
- package/src/sites/dev-server-manager.ts +286 -0
- package/src/sites/fixtures/security-test-site/.jarvis-project.json +6 -0
- package/src/sites/fixtures/security-test-site/Makefile +15 -0
- package/src/sites/fixtures/security-test-site/README.md +18 -0
- package/src/sites/fixtures/security-test-site/index.html +12 -0
- package/src/sites/fixtures/security-test-site/index.ts +16 -0
- package/src/sites/fixtures/security-test-site/package.json +13 -0
- package/src/sites/fixtures/security-test-site/src/app.tsx +780 -0
- package/src/sites/fixtures/security-test-site/tsconfig.json +10 -0
- package/src/sites/git-manager.ts +240 -0
- package/src/sites/github-manager.ts +355 -0
- package/src/sites/index.ts +25 -0
- package/src/sites/project-manager.ts +389 -0
- package/src/sites/proxy.ts +133 -0
- package/src/sites/service.ts +136 -0
- package/src/sites/templates.ts +169 -0
- package/src/sites/types.ts +89 -0
- package/src/user/profile-followup.test.ts +84 -0
- package/src/user/profile-followup.ts +185 -0
- package/src/user/profile.ts +224 -0
- package/src/vault/README.md +110 -0
- package/src/vault/awareness.ts +341 -0
- package/src/vault/commitments.ts +299 -0
- package/src/vault/content-pipeline.ts +270 -0
- package/src/vault/conversations.ts +173 -0
- package/src/vault/dashboard-sessions.ts +44 -0
- package/src/vault/documents.ts +130 -0
- package/src/vault/entities.ts +185 -0
- package/src/vault/extractor.test.ts +356 -0
- package/src/vault/extractor.ts +345 -0
- package/src/vault/facts.ts +190 -0
- package/src/vault/goals.ts +477 -0
- package/src/vault/index.ts +87 -0
- package/src/vault/keychain.ts +99 -0
- package/src/vault/observations.ts +115 -0
- package/src/vault/relationships.ts +178 -0
- package/src/vault/retrieval.test.ts +139 -0
- package/src/vault/retrieval.ts +258 -0
- package/src/vault/schema.ts +709 -0
- package/src/vault/settings.ts +38 -0
- package/src/vault/user-profile.test.ts +113 -0
- package/src/vault/user-profile.ts +176 -0
- package/src/vault/vectors.ts +92 -0
- package/src/vault/webapp-template-seeds.ts +116 -0
- package/src/vault/webapp-templates.ts +244 -0
- package/src/vault/workflows.ts +403 -0
- package/src/workflows/auto-suggest.ts +290 -0
- package/src/workflows/engine.ts +366 -0
- package/src/workflows/events.ts +24 -0
- package/src/workflows/executor.ts +207 -0
- package/src/workflows/nl-builder.ts +198 -0
- package/src/workflows/nodes/actions/agent-task.ts +73 -0
- package/src/workflows/nodes/actions/calendar-action.ts +85 -0
- package/src/workflows/nodes/actions/code-execution.ts +73 -0
- package/src/workflows/nodes/actions/discord.ts +77 -0
- package/src/workflows/nodes/actions/file-write.ts +73 -0
- package/src/workflows/nodes/actions/gmail.ts +69 -0
- package/src/workflows/nodes/actions/http-request.ts +117 -0
- package/src/workflows/nodes/actions/notification.ts +85 -0
- package/src/workflows/nodes/actions/run-tool.ts +55 -0
- package/src/workflows/nodes/actions/send-message.ts +82 -0
- package/src/workflows/nodes/actions/shell-command.ts +76 -0
- package/src/workflows/nodes/actions/telegram.ts +60 -0
- package/src/workflows/nodes/builtin.ts +119 -0
- package/src/workflows/nodes/error/error-handler.ts +37 -0
- package/src/workflows/nodes/error/fallback.ts +47 -0
- package/src/workflows/nodes/error/retry.ts +82 -0
- package/src/workflows/nodes/logic/delay.ts +42 -0
- package/src/workflows/nodes/logic/if-else.ts +41 -0
- package/src/workflows/nodes/logic/loop.ts +90 -0
- package/src/workflows/nodes/logic/merge.ts +38 -0
- package/src/workflows/nodes/logic/race.ts +40 -0
- package/src/workflows/nodes/logic/switch.ts +59 -0
- package/src/workflows/nodes/logic/template-render.ts +53 -0
- package/src/workflows/nodes/logic/variable-get.ts +37 -0
- package/src/workflows/nodes/logic/variable-set.ts +59 -0
- package/src/workflows/nodes/registry.ts +99 -0
- package/src/workflows/nodes/transform/aggregate.ts +99 -0
- package/src/workflows/nodes/transform/csv-parse.ts +70 -0
- package/src/workflows/nodes/transform/json-parse.ts +63 -0
- package/src/workflows/nodes/transform/map-filter.ts +84 -0
- package/src/workflows/nodes/transform/regex-match.ts +89 -0
- package/src/workflows/nodes/triggers/calendar.ts +33 -0
- package/src/workflows/nodes/triggers/clipboard.ts +32 -0
- package/src/workflows/nodes/triggers/cron.ts +40 -0
- package/src/workflows/nodes/triggers/email.ts +40 -0
- package/src/workflows/nodes/triggers/file-change.ts +45 -0
- package/src/workflows/nodes/triggers/git.ts +46 -0
- package/src/workflows/nodes/triggers/manual.ts +23 -0
- package/src/workflows/nodes/triggers/poll.ts +81 -0
- package/src/workflows/nodes/triggers/process.ts +44 -0
- package/src/workflows/nodes/triggers/screen-event.ts +37 -0
- package/src/workflows/nodes/triggers/webhook.ts +39 -0
- package/src/workflows/safe-eval.ts +139 -0
- package/src/workflows/template.ts +118 -0
- package/src/workflows/triggers/cron.ts +311 -0
- package/src/workflows/triggers/manager.ts +285 -0
- package/src/workflows/triggers/observer-bridge.ts +172 -0
- package/src/workflows/triggers/poller.ts +201 -0
- package/src/workflows/triggers/screen-condition.ts +218 -0
- package/src/workflows/triggers/triggers.test.ts +740 -0
- package/src/workflows/triggers/webhook.ts +191 -0
- package/src/workflows/types.ts +133 -0
- package/src/workflows/variables.ts +72 -0
- package/src/workflows/workflows.test.ts +383 -0
- package/src/workflows/yaml.ts +104 -0
- package/ui/dist/index-3gr23jt9.js +112614 -0
- package/ui/dist/index-9vmj8127.css +14239 -0
- package/ui/dist/index-hy9pc1gm.js +112873 -0
- package/ui/dist/index-j2ep5d1w.js +112374 -0
- package/ui/dist/index-jt00vjqs.js +112858 -0
- package/ui/dist/index-k9ymx5qb.js +112374 -0
- package/ui/dist/index.html +16 -0
- package/ui/public/audio/pcm-capture-processor.js +11 -0
- package/ui/public/openwakeword/models/embedding_model.onnx +0 -0
- package/ui/public/openwakeword/models/hey_jarvis_v0.1.onnx +0 -0
- package/ui/public/openwakeword/models/melspectrogram.onnx +0 -0
- package/ui/public/openwakeword/models/silero_vad.onnx +0 -0
- package/ui/public/ort/ort-wasm-simd-threaded.jsep.mjs +106 -0
- package/ui/public/ort/ort-wasm-simd-threaded.jsep.wasm +0 -0
- package/ui/public/ort/ort-wasm-simd-threaded.mjs +59 -0
- package/ui/public/ort/ort-wasm-simd-threaded.wasm +0 -0
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, mock, test } from 'bun:test';
|
|
2
|
+
import { testLLMProvider } from './llm-settings.ts';
|
|
3
|
+
import { DEFAULT_CONFIG, type JarvisConfig } from '../config/types.ts';
|
|
4
|
+
|
|
5
|
+
function makeConfig(): JarvisConfig {
|
|
6
|
+
return structuredClone(DEFAULT_CONFIG);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
describe('testLLMProvider', () => {
|
|
10
|
+
const originalFetch = globalThis.fetch;
|
|
11
|
+
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
globalThis.fetch = mock(async (_url: string) => {
|
|
14
|
+
return new Response(JSON.stringify({
|
|
15
|
+
id: 'cmpl_test',
|
|
16
|
+
object: 'chat.completion',
|
|
17
|
+
created: Date.now(),
|
|
18
|
+
model: 'gpt-test',
|
|
19
|
+
choices: [
|
|
20
|
+
{
|
|
21
|
+
index: 0,
|
|
22
|
+
message: { role: 'assistant', content: 'OK' },
|
|
23
|
+
finish_reason: 'stop',
|
|
24
|
+
},
|
|
25
|
+
],
|
|
26
|
+
usage: {
|
|
27
|
+
prompt_tokens: 1,
|
|
28
|
+
completion_tokens: 1,
|
|
29
|
+
total_tokens: 2,
|
|
30
|
+
},
|
|
31
|
+
}), {
|
|
32
|
+
status: 200,
|
|
33
|
+
headers: { 'Content-Type': 'application/json' },
|
|
34
|
+
});
|
|
35
|
+
}) as unknown as typeof fetch;
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
afterEach(() => {
|
|
39
|
+
globalThis.fetch = originalFetch;
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test('uses the official OpenAI endpoint when no base URL is configured', async () => {
|
|
43
|
+
const config = makeConfig();
|
|
44
|
+
const result = await testLLMProvider(
|
|
45
|
+
{
|
|
46
|
+
provider: 'openai',
|
|
47
|
+
api_key: 'sk-test-key',
|
|
48
|
+
model: 'gpt-5.4',
|
|
49
|
+
},
|
|
50
|
+
config,
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
expect(result.ok).toBe(true);
|
|
54
|
+
|
|
55
|
+
const fetchMock = globalThis.fetch as unknown as ReturnType<typeof mock>;
|
|
56
|
+
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
57
|
+
expect(fetchMock.mock.calls[0]?.[0]).toBe('https://api.openai.com/v1/chat/completions');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test('uses custom compatible base URLs without assuming an OpenAI key prefix', async () => {
|
|
61
|
+
const config = makeConfig();
|
|
62
|
+
const result = await testLLMProvider(
|
|
63
|
+
{
|
|
64
|
+
provider: 'openai',
|
|
65
|
+
api_key: 'nvapi-custom-key',
|
|
66
|
+
model: 'gpt-5.4',
|
|
67
|
+
base_url: 'https://gateway.example.com/openai',
|
|
68
|
+
},
|
|
69
|
+
config,
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
expect(result.ok).toBe(true);
|
|
73
|
+
|
|
74
|
+
const fetchMock = globalThis.fetch as unknown as ReturnType<typeof mock>;
|
|
75
|
+
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
76
|
+
expect(fetchMock.mock.calls[0]?.[0]).toBe('https://gateway.example.com/openai/chat/completions');
|
|
77
|
+
});
|
|
78
|
+
});
|
|
@@ -0,0 +1,450 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LLM Settings — Bridge between DB settings, encrypted keychain, and in-memory config.
|
|
3
|
+
*
|
|
4
|
+
* Non-secret settings (provider, model, fallback) are stored in the SQLite `settings` table.
|
|
5
|
+
* API keys are stored in the encrypted secrets file via the keychain module.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { getSetting, setSetting, getSettingsByPrefix } from '../vault/settings.ts';
|
|
9
|
+
import { getSecret, setSecret, deleteSecret, hasSecret } from '../vault/keychain.ts';
|
|
10
|
+
import type { JarvisConfig } from '../config/types.ts';
|
|
11
|
+
import { AnthropicProvider } from '../llm/anthropic.ts';
|
|
12
|
+
import { OpenAIProvider } from '../llm/openai.ts';
|
|
13
|
+
import { GroqProvider } from '../llm/groq.ts';
|
|
14
|
+
import { GeminiProvider } from '../llm/gemini.ts';
|
|
15
|
+
import { OllamaProvider } from '../llm/ollama.ts';
|
|
16
|
+
import { OpenRouterProvider } from '../llm/openrouter.ts';
|
|
17
|
+
import type { LLMProvider } from '../llm/provider.ts';
|
|
18
|
+
import type { LLMManager } from '../llm/manager.ts';
|
|
19
|
+
|
|
20
|
+
// Keychain key names
|
|
21
|
+
const KEY_ANTHROPIC = 'llm.anthropic.api_key';
|
|
22
|
+
const KEY_OPENAI = 'llm.openai.api_key';
|
|
23
|
+
const KEY_GROQ = 'llm.groq.api_key';
|
|
24
|
+
const KEY_GEMINI = 'llm.gemini.api_key';
|
|
25
|
+
const KEY_OPENROUTER = 'llm.openrouter.api_key';
|
|
26
|
+
|
|
27
|
+
// DB setting keys
|
|
28
|
+
const SETTING_PRIMARY = 'llm.primary';
|
|
29
|
+
const SETTING_FALLBACK = 'llm.fallback';
|
|
30
|
+
const SETTING_ANTHROPIC_MODEL = 'llm.anthropic.model';
|
|
31
|
+
const SETTING_OPENAI_MODEL = 'llm.openai.model';
|
|
32
|
+
const SETTING_OPENAI_BASE_URL = 'llm.openai.base_url';
|
|
33
|
+
const SETTING_GROQ_MODEL = 'llm.groq.model';
|
|
34
|
+
const SETTING_GEMINI_MODEL = 'llm.gemini.model';
|
|
35
|
+
const SETTING_OLLAMA_MODEL = 'llm.ollama.model';
|
|
36
|
+
const SETTING_OLLAMA_BASE_URL = 'llm.ollama.base_url';
|
|
37
|
+
const SETTING_OPENROUTER_MODEL = 'llm.openrouter.model';
|
|
38
|
+
|
|
39
|
+
export type LLMSettingsResponse = {
|
|
40
|
+
primary: string;
|
|
41
|
+
fallback: string[];
|
|
42
|
+
anthropic: { model: string; has_api_key: boolean } | null;
|
|
43
|
+
openai: { model: string; has_api_key: boolean; base_url?: string } | null;
|
|
44
|
+
groq: { model: string; has_api_key: boolean } | null;
|
|
45
|
+
gemini: { model: string; has_api_key: boolean } | null;
|
|
46
|
+
ollama: { base_url: string; model: string } | null;
|
|
47
|
+
openrouter: { model: string; has_api_key: boolean } | null;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Read LLM settings from DB + keychain and return a dashboard-safe response.
|
|
52
|
+
* Falls back to in-memory config values for anything not yet saved to DB.
|
|
53
|
+
*/
|
|
54
|
+
export function getLLMSettings(config: JarvisConfig): LLMSettingsResponse {
|
|
55
|
+
const primary = getSetting(SETTING_PRIMARY) ?? config.llm.primary;
|
|
56
|
+
const fallbackRaw = getSetting(SETTING_FALLBACK);
|
|
57
|
+
const fallback = fallbackRaw ? JSON.parse(fallbackRaw) : config.llm.fallback;
|
|
58
|
+
|
|
59
|
+
const anthropicModel = getSetting(SETTING_ANTHROPIC_MODEL) ?? config.llm.anthropic?.model ?? 'claude-sonnet-4-6';
|
|
60
|
+
const openaiModel = getSetting(SETTING_OPENAI_MODEL) ?? config.llm.openai?.model ?? 'gpt-5.4';
|
|
61
|
+
const openaiBaseUrl = getSetting(SETTING_OPENAI_BASE_URL) ?? config.llm.openai?.base_url;
|
|
62
|
+
const groqModel = getSetting(SETTING_GROQ_MODEL) ?? config.llm.groq?.model ?? 'llama-3.3-70b-versatile';
|
|
63
|
+
const geminiModel = getSetting(SETTING_GEMINI_MODEL) ?? config.llm.gemini?.model ?? 'gemini-3-flash-preview';
|
|
64
|
+
const ollamaModel = getSetting(SETTING_OLLAMA_MODEL) ?? config.llm.ollama?.model ?? 'llama3';
|
|
65
|
+
const ollamaBaseUrl = getSetting(SETTING_OLLAMA_BASE_URL) ?? config.llm.ollama?.base_url ?? 'http://localhost:11434';
|
|
66
|
+
|
|
67
|
+
const openrouterModel = getSetting(SETTING_OPENROUTER_MODEL) ?? config.llm.openrouter?.model ?? 'anthropic/claude-sonnet-4';
|
|
68
|
+
|
|
69
|
+
const hasAnthropicKey = hasSecret(KEY_ANTHROPIC) || !!config.llm.anthropic?.api_key;
|
|
70
|
+
const hasOpenaiKey = hasSecret(KEY_OPENAI) || !!config.llm.openai?.api_key;
|
|
71
|
+
const hasGroqKey = hasSecret(KEY_GROQ) || !!config.llm.groq?.api_key;
|
|
72
|
+
const hasGeminiKey = hasSecret(KEY_GEMINI) || !!config.llm.gemini?.api_key;
|
|
73
|
+
const hasOpenrouterKey = hasSecret(KEY_OPENROUTER) || !!config.llm.openrouter?.api_key;
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
primary,
|
|
77
|
+
fallback,
|
|
78
|
+
anthropic: { model: anthropicModel, has_api_key: hasAnthropicKey },
|
|
79
|
+
openai: { model: openaiModel, has_api_key: hasOpenaiKey, ...(openaiBaseUrl ? { base_url: openaiBaseUrl } : {}) },
|
|
80
|
+
groq: { model: groqModel, has_api_key: hasGroqKey },
|
|
81
|
+
gemini: { model: geminiModel, has_api_key: hasGeminiKey },
|
|
82
|
+
ollama: { base_url: ollamaBaseUrl, model: ollamaModel },
|
|
83
|
+
openrouter: { model: openrouterModel, has_api_key: hasOpenrouterKey },
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Save LLM settings to DB + keychain and update the in-memory config.
|
|
89
|
+
*/
|
|
90
|
+
export function saveLLMSettings(
|
|
91
|
+
config: JarvisConfig,
|
|
92
|
+
body: {
|
|
93
|
+
primary?: string;
|
|
94
|
+
fallback?: string[];
|
|
95
|
+
anthropic?: { api_key?: string; model?: string };
|
|
96
|
+
openai?: { api_key?: string; model?: string; base_url?: string };
|
|
97
|
+
groq?: { api_key?: string; model?: string };
|
|
98
|
+
gemini?: { api_key?: string; model?: string };
|
|
99
|
+
ollama?: { base_url?: string; model?: string };
|
|
100
|
+
openrouter?: { api_key?: string; model?: string };
|
|
101
|
+
},
|
|
102
|
+
): void {
|
|
103
|
+
// Save non-secret settings to DB
|
|
104
|
+
if (body.primary) {
|
|
105
|
+
setSetting(SETTING_PRIMARY, body.primary);
|
|
106
|
+
config.llm.primary = body.primary;
|
|
107
|
+
}
|
|
108
|
+
if (body.fallback) {
|
|
109
|
+
setSetting(SETTING_FALLBACK, JSON.stringify(body.fallback));
|
|
110
|
+
config.llm.fallback = body.fallback;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Anthropic
|
|
114
|
+
if (body.anthropic) {
|
|
115
|
+
if (body.anthropic.model) {
|
|
116
|
+
setSetting(SETTING_ANTHROPIC_MODEL, body.anthropic.model);
|
|
117
|
+
}
|
|
118
|
+
if (body.anthropic.api_key) {
|
|
119
|
+
setSecret(KEY_ANTHROPIC, body.anthropic.api_key);
|
|
120
|
+
}
|
|
121
|
+
config.llm.anthropic = {
|
|
122
|
+
...config.llm.anthropic,
|
|
123
|
+
model: body.anthropic.model ?? config.llm.anthropic?.model,
|
|
124
|
+
api_key: body.anthropic.api_key ?? getAnthropicApiKey(config) ?? '',
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// OpenAI
|
|
129
|
+
if (body.openai) {
|
|
130
|
+
if (body.openai.base_url !== undefined) {
|
|
131
|
+
const trimmedBaseUrl = body.openai.base_url.trim();
|
|
132
|
+
if (trimmedBaseUrl) {
|
|
133
|
+
try {
|
|
134
|
+
new URL(trimmedBaseUrl);
|
|
135
|
+
} catch {
|
|
136
|
+
throw new Error(`Invalid OpenAI base URL: ${trimmedBaseUrl}`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
if (body.openai.model) {
|
|
141
|
+
setSetting(SETTING_OPENAI_MODEL, body.openai.model);
|
|
142
|
+
}
|
|
143
|
+
if (body.openai.base_url !== undefined) {
|
|
144
|
+
const trimmedBaseUrl = body.openai.base_url.trim();
|
|
145
|
+
if (trimmedBaseUrl) {
|
|
146
|
+
setSetting(SETTING_OPENAI_BASE_URL, trimmedBaseUrl);
|
|
147
|
+
} else {
|
|
148
|
+
setSetting(SETTING_OPENAI_BASE_URL, '');
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
if (body.openai.api_key) {
|
|
152
|
+
setSecret(KEY_OPENAI, body.openai.api_key);
|
|
153
|
+
}
|
|
154
|
+
config.llm.openai = {
|
|
155
|
+
...config.llm.openai,
|
|
156
|
+
model: body.openai.model ?? config.llm.openai?.model,
|
|
157
|
+
base_url: body.openai.base_url !== undefined
|
|
158
|
+
? (body.openai.base_url.trim() || undefined)
|
|
159
|
+
: config.llm.openai?.base_url,
|
|
160
|
+
api_key: body.openai.api_key ?? getOpenAIApiKey(config) ?? '',
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Groq
|
|
165
|
+
if (body.groq) {
|
|
166
|
+
if (body.groq.model) {
|
|
167
|
+
setSetting(SETTING_GROQ_MODEL, body.groq.model);
|
|
168
|
+
}
|
|
169
|
+
if (body.groq.api_key) {
|
|
170
|
+
setSecret(KEY_GROQ, body.groq.api_key);
|
|
171
|
+
}
|
|
172
|
+
config.llm.groq = {
|
|
173
|
+
...config.llm.groq,
|
|
174
|
+
model: body.groq.model ?? config.llm.groq?.model,
|
|
175
|
+
api_key: body.groq.api_key ?? getGroqApiKey(config) ?? '',
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Gemini
|
|
180
|
+
if (body.gemini) {
|
|
181
|
+
if (body.gemini.model) {
|
|
182
|
+
setSetting(SETTING_GEMINI_MODEL, body.gemini.model);
|
|
183
|
+
}
|
|
184
|
+
if (body.gemini.api_key) {
|
|
185
|
+
setSecret(KEY_GEMINI, body.gemini.api_key);
|
|
186
|
+
}
|
|
187
|
+
config.llm.gemini = {
|
|
188
|
+
...config.llm.gemini,
|
|
189
|
+
model: body.gemini.model ?? config.llm.gemini?.model,
|
|
190
|
+
api_key: body.gemini.api_key ?? getGeminiApiKey(config) ?? '',
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Ollama
|
|
195
|
+
if (body.ollama) {
|
|
196
|
+
if (body.ollama.model) {
|
|
197
|
+
setSetting(SETTING_OLLAMA_MODEL, body.ollama.model);
|
|
198
|
+
}
|
|
199
|
+
if (body.ollama.base_url) {
|
|
200
|
+
setSetting(SETTING_OLLAMA_BASE_URL, body.ollama.base_url);
|
|
201
|
+
}
|
|
202
|
+
config.llm.ollama = {
|
|
203
|
+
...config.llm.ollama,
|
|
204
|
+
model: body.ollama.model ?? config.llm.ollama?.model,
|
|
205
|
+
base_url: body.ollama.base_url ?? config.llm.ollama?.base_url,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// OpenRouter
|
|
210
|
+
if (body.openrouter) {
|
|
211
|
+
if (body.openrouter.model) {
|
|
212
|
+
setSetting(SETTING_OPENROUTER_MODEL, body.openrouter.model);
|
|
213
|
+
}
|
|
214
|
+
if (body.openrouter.api_key) {
|
|
215
|
+
setSecret(KEY_OPENROUTER, body.openrouter.api_key);
|
|
216
|
+
}
|
|
217
|
+
config.llm.openrouter = {
|
|
218
|
+
...config.llm.openrouter,
|
|
219
|
+
model: body.openrouter.model ?? config.llm.openrouter?.model,
|
|
220
|
+
api_key: body.openrouter.api_key ?? getOpenRouterApiKey(config) ?? '',
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Resolve the Anthropic API key: keychain > config.yaml > env var.
|
|
227
|
+
*/
|
|
228
|
+
function getAnthropicApiKey(config: JarvisConfig): string | null {
|
|
229
|
+
return getSecret(KEY_ANTHROPIC) ?? config.llm.anthropic?.api_key ?? null;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Resolve the OpenAI API key: keychain > config.yaml > env var.
|
|
234
|
+
*/
|
|
235
|
+
function getOpenAIApiKey(config: JarvisConfig): string | null {
|
|
236
|
+
return getSecret(KEY_OPENAI) ?? config.llm.openai?.api_key ?? null;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Resolve the Groq API key: keychain > config.yaml > env var.
|
|
241
|
+
*/
|
|
242
|
+
function getGroqApiKey(config: JarvisConfig): string | null {
|
|
243
|
+
return getSecret(KEY_GROQ) ?? config.llm.groq?.api_key ?? null;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Resolve the Gemini API key: keychain > config.yaml > env var.
|
|
248
|
+
*/
|
|
249
|
+
function getGeminiApiKey(config: JarvisConfig): string | null {
|
|
250
|
+
return getSecret(KEY_GEMINI) ?? config.llm.gemini?.api_key ?? null;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Resolve the OpenRouter API key: keychain > config.yaml > env var.
|
|
255
|
+
*/
|
|
256
|
+
function getOpenRouterApiKey(config: JarvisConfig): string | null {
|
|
257
|
+
return getSecret(KEY_OPENROUTER) ?? config.llm.openrouter?.api_key ?? null;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Merge DB/keychain LLM settings into config at startup.
|
|
262
|
+
* Env vars (already applied by loadConfig) take priority over DB values.
|
|
263
|
+
*/
|
|
264
|
+
export function mergeLLMSettingsIntoConfig(config: JarvisConfig): void {
|
|
265
|
+
// Only override from DB if env vars are NOT set
|
|
266
|
+
const dbPrimary = getSetting(SETTING_PRIMARY);
|
|
267
|
+
if (dbPrimary) config.llm.primary = dbPrimary;
|
|
268
|
+
|
|
269
|
+
const dbFallback = getSetting(SETTING_FALLBACK);
|
|
270
|
+
if (dbFallback) config.llm.fallback = JSON.parse(dbFallback);
|
|
271
|
+
|
|
272
|
+
// Anthropic
|
|
273
|
+
const dbAnthropicModel = getSetting(SETTING_ANTHROPIC_MODEL);
|
|
274
|
+
const keychainAnthropicKey = getSecret(KEY_ANTHROPIC);
|
|
275
|
+
if (dbAnthropicModel || keychainAnthropicKey) {
|
|
276
|
+
config.llm.anthropic = {
|
|
277
|
+
...config.llm.anthropic,
|
|
278
|
+
api_key: (!process.env.JARVIS_API_KEY && keychainAnthropicKey)
|
|
279
|
+
? keychainAnthropicKey
|
|
280
|
+
: (config.llm.anthropic?.api_key ?? ''),
|
|
281
|
+
model: dbAnthropicModel ?? config.llm.anthropic?.model,
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// OpenAI
|
|
286
|
+
const dbOpenaiModel = getSetting(SETTING_OPENAI_MODEL);
|
|
287
|
+
const dbOpenaiBaseUrl = getSetting(SETTING_OPENAI_BASE_URL);
|
|
288
|
+
const keychainOpenaiKey = getSecret(KEY_OPENAI);
|
|
289
|
+
if (dbOpenaiModel || dbOpenaiBaseUrl !== null || keychainOpenaiKey) {
|
|
290
|
+
config.llm.openai = {
|
|
291
|
+
...config.llm.openai,
|
|
292
|
+
api_key: (!process.env.JARVIS_OPENAI_KEY && keychainOpenaiKey)
|
|
293
|
+
? keychainOpenaiKey
|
|
294
|
+
: (config.llm.openai?.api_key ?? ''),
|
|
295
|
+
model: dbOpenaiModel ?? config.llm.openai?.model,
|
|
296
|
+
base_url: dbOpenaiBaseUrl !== null
|
|
297
|
+
? (dbOpenaiBaseUrl || undefined)
|
|
298
|
+
: config.llm.openai?.base_url,
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Groq
|
|
303
|
+
const dbGroqModel = getSetting(SETTING_GROQ_MODEL);
|
|
304
|
+
const keychainGroqKey = getSecret(KEY_GROQ);
|
|
305
|
+
if (dbGroqModel || keychainGroqKey) {
|
|
306
|
+
config.llm.groq = {
|
|
307
|
+
...config.llm.groq,
|
|
308
|
+
api_key: (!process.env.JARVIS_GROQ_KEY && keychainGroqKey)
|
|
309
|
+
? keychainGroqKey
|
|
310
|
+
: (config.llm.groq?.api_key ?? ''),
|
|
311
|
+
model: dbGroqModel ?? config.llm.groq?.model,
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Gemini
|
|
316
|
+
const dbGeminiModel = getSetting(SETTING_GEMINI_MODEL);
|
|
317
|
+
const keychainGeminiKey = getSecret(KEY_GEMINI);
|
|
318
|
+
if (dbGeminiModel || keychainGeminiKey) {
|
|
319
|
+
config.llm.gemini = {
|
|
320
|
+
...config.llm.gemini,
|
|
321
|
+
api_key: (!process.env.JARVIS_GEMINI_KEY && keychainGeminiKey)
|
|
322
|
+
? keychainGeminiKey
|
|
323
|
+
: (config.llm.gemini?.api_key ?? ''),
|
|
324
|
+
model: dbGeminiModel ?? config.llm.gemini?.model,
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// Ollama
|
|
329
|
+
const dbOllamaModel = getSetting(SETTING_OLLAMA_MODEL);
|
|
330
|
+
const dbOllamaUrl = getSetting(SETTING_OLLAMA_BASE_URL);
|
|
331
|
+
if (dbOllamaModel || dbOllamaUrl) {
|
|
332
|
+
config.llm.ollama = {
|
|
333
|
+
...config.llm.ollama,
|
|
334
|
+
model: dbOllamaModel ?? config.llm.ollama?.model,
|
|
335
|
+
base_url: (!process.env.JARVIS_OLLAMA_URL && dbOllamaUrl)
|
|
336
|
+
? dbOllamaUrl
|
|
337
|
+
: (config.llm.ollama?.base_url ?? 'http://localhost:11434'),
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// OpenRouter
|
|
342
|
+
const dbOpenrouterModel = getSetting(SETTING_OPENROUTER_MODEL);
|
|
343
|
+
const keychainOpenrouterKey = getSecret(KEY_OPENROUTER);
|
|
344
|
+
if (dbOpenrouterModel || keychainOpenrouterKey) {
|
|
345
|
+
config.llm.openrouter = {
|
|
346
|
+
...config.llm.openrouter,
|
|
347
|
+
api_key: (!process.env.JARVIS_OPENROUTER_KEY && keychainOpenrouterKey)
|
|
348
|
+
? keychainOpenrouterKey
|
|
349
|
+
: (config.llm.openrouter?.api_key ?? ''),
|
|
350
|
+
model: dbOpenrouterModel ?? config.llm.openrouter?.model,
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Build fresh LLM provider instances from the current config and hot-reload them
|
|
357
|
+
* into the shared LLMManager (atomic swap, safe for in-flight requests).
|
|
358
|
+
*/
|
|
359
|
+
export function hotReloadLLMProviders(config: JarvisConfig, llmManager: LLMManager): void {
|
|
360
|
+
const { llm } = config;
|
|
361
|
+
const providers: LLMProvider[] = [];
|
|
362
|
+
|
|
363
|
+
if (llm.anthropic?.api_key) {
|
|
364
|
+
providers.push(new AnthropicProvider(llm.anthropic.api_key, llm.anthropic.model));
|
|
365
|
+
console.log('[LLM] Hot-reloaded Anthropic provider');
|
|
366
|
+
}
|
|
367
|
+
if (llm.openai?.api_key) {
|
|
368
|
+
providers.push(new OpenAIProvider(llm.openai.api_key, llm.openai.model, llm.openai.base_url));
|
|
369
|
+
console.log('[LLM] Hot-reloaded OpenAI provider');
|
|
370
|
+
}
|
|
371
|
+
if (llm.groq?.api_key) {
|
|
372
|
+
providers.push(new GroqProvider(llm.groq.api_key, llm.groq.model));
|
|
373
|
+
console.log('[LLM] Hot-reloaded Groq provider');
|
|
374
|
+
}
|
|
375
|
+
if (llm.gemini?.api_key) {
|
|
376
|
+
providers.push(new GeminiProvider(llm.gemini.api_key, llm.gemini.model));
|
|
377
|
+
console.log('[LLM] Hot-reloaded Gemini provider');
|
|
378
|
+
}
|
|
379
|
+
if (llm.openrouter?.api_key) {
|
|
380
|
+
providers.push(new OpenRouterProvider(llm.openrouter.api_key, llm.openrouter.model));
|
|
381
|
+
console.log('[LLM] Hot-reloaded OpenRouter provider');
|
|
382
|
+
}
|
|
383
|
+
if (llm.ollama) {
|
|
384
|
+
providers.push(new OllamaProvider(llm.ollama.base_url, llm.ollama.model));
|
|
385
|
+
console.log('[LLM] Hot-reloaded Ollama provider');
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
const fallback = llm.fallback.filter(n => providers.some(p => p.name === n));
|
|
389
|
+
llmManager.replaceProviders(providers, llm.primary, fallback);
|
|
390
|
+
console.log(`[LLM] Providers active: ${providers.map(p => p.name).join(', ') || 'none'} (primary: ${llm.primary})`);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Test an LLM provider connection. Uses provided credentials if given,
|
|
395
|
+
* otherwise falls back to stored keys (keychain > config).
|
|
396
|
+
*/
|
|
397
|
+
export async function testLLMProvider(
|
|
398
|
+
opts: {
|
|
399
|
+
provider: string;
|
|
400
|
+
api_key?: string;
|
|
401
|
+
model?: string;
|
|
402
|
+
base_url?: string;
|
|
403
|
+
},
|
|
404
|
+
config: JarvisConfig,
|
|
405
|
+
): Promise<{ ok: boolean; model?: string; error?: string }> {
|
|
406
|
+
try {
|
|
407
|
+
let instance: LLMProvider;
|
|
408
|
+
|
|
409
|
+
if (opts.provider === 'anthropic') {
|
|
410
|
+
const key = opts.api_key || getSecret(KEY_ANTHROPIC) || config.llm.anthropic?.api_key;
|
|
411
|
+
if (!key) return { ok: false, error: 'API key required' };
|
|
412
|
+
instance = new AnthropicProvider(key, opts.model ?? config.llm.anthropic?.model);
|
|
413
|
+
} else if (opts.provider === 'openai') {
|
|
414
|
+
const key = opts.api_key || getSecret(KEY_OPENAI) || config.llm.openai?.api_key;
|
|
415
|
+
if (!key) return { ok: false, error: 'API key required' };
|
|
416
|
+
instance = new OpenAIProvider(
|
|
417
|
+
key,
|
|
418
|
+
opts.model ?? config.llm.openai?.model,
|
|
419
|
+
opts.base_url ?? config.llm.openai?.base_url,
|
|
420
|
+
);
|
|
421
|
+
} else if (opts.provider === 'groq') {
|
|
422
|
+
const key = opts.api_key || getSecret(KEY_GROQ) || config.llm.groq?.api_key;
|
|
423
|
+
if (!key) return { ok: false, error: 'API key required' };
|
|
424
|
+
instance = new GroqProvider(key, opts.model ?? config.llm.groq?.model);
|
|
425
|
+
} else if (opts.provider === 'gemini') {
|
|
426
|
+
const key = opts.api_key || config.llm.gemini?.api_key;
|
|
427
|
+
if (!key) return { ok: false, error: 'API key required' };
|
|
428
|
+
instance = new GeminiProvider(key, opts.model ?? config.llm.gemini?.model);
|
|
429
|
+
} else if (opts.provider === 'openrouter') {
|
|
430
|
+
const key = opts.api_key || getSecret(KEY_OPENROUTER) || config.llm.openrouter?.api_key;
|
|
431
|
+
if (!key) return { ok: false, error: 'API key required' };
|
|
432
|
+
instance = new OpenRouterProvider(key, opts.model ?? config.llm.openrouter?.model);
|
|
433
|
+
} else if (opts.provider === 'ollama') {
|
|
434
|
+
instance = new OllamaProvider(
|
|
435
|
+
opts.base_url ?? config.llm.ollama?.base_url,
|
|
436
|
+
opts.model ?? config.llm.ollama?.model,
|
|
437
|
+
);
|
|
438
|
+
} else {
|
|
439
|
+
return { ok: false, error: `Unknown provider: ${opts.provider}` };
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
const resp = await instance.chat(
|
|
443
|
+
[{ role: 'user', content: 'Say OK' }],
|
|
444
|
+
{ max_tokens: 5 },
|
|
445
|
+
);
|
|
446
|
+
return { ok: true, model: resp.model };
|
|
447
|
+
} catch (err) {
|
|
448
|
+
return { ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
449
|
+
}
|
|
450
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Observer Service — The Eyes
|
|
3
|
+
*
|
|
4
|
+
* Wraps ObserverManager. Registers system observers (file watcher,
|
|
5
|
+
* clipboard monitor, process monitor, email, calendar, notifications)
|
|
6
|
+
* and routes events to the vault.
|
|
7
|
+
* Also classifies events and routes them to the EventReactor (immediate)
|
|
8
|
+
* or EventCoalescer (batched for heartbeat).
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { Service, ServiceStatus } from './services.ts';
|
|
12
|
+
import type { ObserverEvent } from '../observers/index.ts';
|
|
13
|
+
import type { ObservationType } from '../vault/observations.ts';
|
|
14
|
+
import type { EventReactor } from './event-reactor.ts';
|
|
15
|
+
import type { EventCoalescer } from './event-coalescer.ts';
|
|
16
|
+
import type { GoogleAuth } from '../integrations/google-auth.ts';
|
|
17
|
+
|
|
18
|
+
import { homedir } from 'node:os';
|
|
19
|
+
import {
|
|
20
|
+
ObserverManager,
|
|
21
|
+
FileWatcher,
|
|
22
|
+
ClipboardMonitor,
|
|
23
|
+
ProcessMonitor,
|
|
24
|
+
} from '../observers/index.ts';
|
|
25
|
+
import { EmailSync } from '../observers/email.ts';
|
|
26
|
+
import { CalendarSync } from '../observers/calendar.ts';
|
|
27
|
+
import { NotificationListener } from '../observers/notifications.ts';
|
|
28
|
+
import { createObservation } from '../vault/observations.ts';
|
|
29
|
+
import { classifyEvent } from './event-classifier.ts';
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Map observer event types to vault observation types.
|
|
33
|
+
*/
|
|
34
|
+
function mapEventType(eventType: string): ObservationType {
|
|
35
|
+
switch (eventType) {
|
|
36
|
+
case 'file_change':
|
|
37
|
+
return 'file_change';
|
|
38
|
+
case 'clipboard':
|
|
39
|
+
return 'clipboard';
|
|
40
|
+
case 'process_started':
|
|
41
|
+
case 'process_stopped':
|
|
42
|
+
return 'process';
|
|
43
|
+
case 'notification':
|
|
44
|
+
return 'notification';
|
|
45
|
+
case 'calendar':
|
|
46
|
+
return 'calendar';
|
|
47
|
+
case 'email':
|
|
48
|
+
return 'email';
|
|
49
|
+
case 'browser':
|
|
50
|
+
return 'browser';
|
|
51
|
+
case 'screen_capture':
|
|
52
|
+
case 'context_changed':
|
|
53
|
+
case 'error_detected':
|
|
54
|
+
case 'stuck_detected':
|
|
55
|
+
case 'session_started':
|
|
56
|
+
case 'session_ended':
|
|
57
|
+
case 'suggestion_ready':
|
|
58
|
+
return 'screen_capture';
|
|
59
|
+
default:
|
|
60
|
+
return 'app_activity';
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export class ObserverService implements Service {
|
|
65
|
+
name = 'observers';
|
|
66
|
+
private _status: ServiceStatus = 'stopped';
|
|
67
|
+
private manager: ObserverManager;
|
|
68
|
+
private reactor: EventReactor | null;
|
|
69
|
+
private coalescer: EventCoalescer | null;
|
|
70
|
+
private googleAuth: GoogleAuth | null;
|
|
71
|
+
|
|
72
|
+
constructor(reactor?: EventReactor, coalescer?: EventCoalescer, googleAuth?: GoogleAuth) {
|
|
73
|
+
this.manager = new ObserverManager();
|
|
74
|
+
this.reactor = reactor ?? null;
|
|
75
|
+
this.coalescer = coalescer ?? null;
|
|
76
|
+
this.googleAuth = googleAuth ?? null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async start(): Promise<void> {
|
|
80
|
+
this._status = 'starting';
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
// Register core observers
|
|
84
|
+
this.manager.register(new FileWatcher([homedir()]));
|
|
85
|
+
this.manager.register(new ClipboardMonitor());
|
|
86
|
+
this.manager.register(new ProcessMonitor());
|
|
87
|
+
|
|
88
|
+
// Register D-Bus notification observer (Linux/WSL2)
|
|
89
|
+
this.manager.register(new NotificationListener());
|
|
90
|
+
|
|
91
|
+
// Register Gmail observer (if Google auth available)
|
|
92
|
+
this.manager.register(new EmailSync(this.googleAuth ?? undefined));
|
|
93
|
+
|
|
94
|
+
// Register Calendar observer (if Google auth available)
|
|
95
|
+
this.manager.register(new CalendarSync(this.googleAuth ?? undefined));
|
|
96
|
+
|
|
97
|
+
// Set event handler: store in vault + classify + route
|
|
98
|
+
this.manager.setEventHandler((event: ObserverEvent) => {
|
|
99
|
+
// 1. Always store in vault
|
|
100
|
+
try {
|
|
101
|
+
const obsType = mapEventType(event.type);
|
|
102
|
+
createObservation(obsType, event.data);
|
|
103
|
+
} catch (err) {
|
|
104
|
+
console.error('[ObserverService] Error storing observation:', err);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// 2. Classify and route
|
|
108
|
+
try {
|
|
109
|
+
const classified = classifyEvent(event);
|
|
110
|
+
|
|
111
|
+
if (classified.priority === 'critical' || classified.priority === 'high') {
|
|
112
|
+
// Route to reactor for immediate handling
|
|
113
|
+
if (this.reactor) {
|
|
114
|
+
this.reactor.react(classified).catch(err =>
|
|
115
|
+
console.error('[ObserverService] Reactor error:', err)
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
} else {
|
|
119
|
+
// Route to coalescer for batched delivery at heartbeat
|
|
120
|
+
if (this.coalescer) {
|
|
121
|
+
this.coalescer.addEvent(classified);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
} catch (err) {
|
|
125
|
+
console.error('[ObserverService] Error classifying event:', err);
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
// Start all observers (individual failures don't crash the service)
|
|
130
|
+
await this.manager.startAll();
|
|
131
|
+
|
|
132
|
+
this._status = 'running';
|
|
133
|
+
console.log('[ObserverService] Started');
|
|
134
|
+
} catch (error) {
|
|
135
|
+
this._status = 'error';
|
|
136
|
+
throw error;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async stop(): Promise<void> {
|
|
141
|
+
this._status = 'stopping';
|
|
142
|
+
await this.manager.stopAll();
|
|
143
|
+
this._status = 'stopped';
|
|
144
|
+
console.log('[ObserverService] Stopped');
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
status(): ServiceStatus {
|
|
148
|
+
return this._status;
|
|
149
|
+
}
|
|
150
|
+
}
|