@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.
- package/LICENSE +153 -0
- package/README.md +278 -0
- package/bin/jarvis.ts +413 -0
- package/package.json +74 -0
- package/scripts/ensure-bun.cjs +8 -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 +252 -0
- package/src/actions/browser/session.ts +437 -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 +321 -0
- package/src/actions/tools/builtin.ts +846 -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/goals.ts +376 -0
- package/src/actions/tools/local-tools-guard.ts +20 -0
- package/src/actions/tools/registry.ts +171 -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 +576 -0
- package/src/agents/role-discovery.ts +61 -0
- package/src/agents/sub-agent-runner.ts +307 -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 +297 -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 +241 -0
- package/src/cli/deps.ts +449 -0
- package/src/cli/doctor.ts +230 -0
- package/src/cli/helpers.ts +401 -0
- package/src/cli/onboard.ts +580 -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/desktop-notify.ts +114 -0
- package/src/comms/example.ts +129 -0
- package/src/comms/index.ts +129 -0
- package/src/comms/streaming.ts +142 -0
- package/src/comms/voice.test.ts +152 -0
- package/src/comms/voice.ts +291 -0
- package/src/comms/websocket.test.ts +409 -0
- package/src/comms/websocket.ts +473 -0
- package/src/config/README.md +387 -0
- package/src/config/index.ts +6 -0
- package/src/config/loader.test.ts +137 -0
- package/src/config/loader.ts +142 -0
- package/src/config/types.ts +260 -0
- package/src/daemon/README.md +232 -0
- package/src/daemon/agent-service-interface.ts +9 -0
- package/src/daemon/agent-service.ts +600 -0
- package/src/daemon/api-routes.ts +2119 -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/event-classifier.ts +239 -0
- package/src/daemon/event-coalescer.ts +123 -0
- package/src/daemon/event-reactor.ts +214 -0
- package/src/daemon/health.ts +220 -0
- package/src/daemon/index.ts +1004 -0
- package/src/daemon/llm-settings.ts +316 -0
- package/src/daemon/observer-service.ts +150 -0
- package/src/daemon/pid.ts +98 -0
- package/src/daemon/research-queue.ts +155 -0
- package/src/daemon/services.ts +175 -0
- package/src/daemon/ws-service.ts +788 -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 +348 -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 +386 -0
- package/src/llm/gemini.ts +371 -0
- package/src/llm/index.ts +19 -0
- package/src/llm/manager.ts +153 -0
- package/src/llm/ollama.ts +307 -0
- package/src/llm/openai.ts +350 -0
- package/src/llm/provider.test.ts +231 -0
- package/src/llm/provider.ts +60 -0
- package/src/llm/test.ts +87 -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 +119 -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 +194 -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 +190 -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/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 +260 -0
- package/src/vault/conversations.ts +173 -0
- package/src/vault/entities.ts +180 -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 +126 -0
- package/src/vault/retrieval.ts +227 -0
- package/src/vault/schema.ts +658 -0
- package/src/vault/settings.ts +38 -0
- package/src/vault/vectors.ts +92 -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-j75njzc1.css +1199 -0
- package/ui/dist/index-p2zh407q.js +80603 -0
- package/ui/dist/index.html +13 -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,316 @@
|
|
|
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 { GeminiProvider } from '../llm/gemini.ts';
|
|
14
|
+
import { OllamaProvider } from '../llm/ollama.ts';
|
|
15
|
+
import type { LLMProvider } from '../llm/provider.ts';
|
|
16
|
+
import type { LLMManager } from '../llm/manager.ts';
|
|
17
|
+
|
|
18
|
+
// Keychain key names
|
|
19
|
+
const KEY_ANTHROPIC = 'llm.anthropic.api_key';
|
|
20
|
+
const KEY_OPENAI = 'llm.openai.api_key';
|
|
21
|
+
const KEY_GEMINI = 'llm.gemini.api_key';
|
|
22
|
+
|
|
23
|
+
// DB setting keys
|
|
24
|
+
const SETTING_PRIMARY = 'llm.primary';
|
|
25
|
+
const SETTING_FALLBACK = 'llm.fallback';
|
|
26
|
+
const SETTING_ANTHROPIC_MODEL = 'llm.anthropic.model';
|
|
27
|
+
const SETTING_OPENAI_MODEL = 'llm.openai.model';
|
|
28
|
+
const SETTING_GEMINI_MODEL = 'llm.gemini.model';
|
|
29
|
+
const SETTING_OLLAMA_MODEL = 'llm.ollama.model';
|
|
30
|
+
const SETTING_OLLAMA_BASE_URL = 'llm.ollama.base_url';
|
|
31
|
+
|
|
32
|
+
export type LLMSettingsResponse = {
|
|
33
|
+
primary: string;
|
|
34
|
+
fallback: string[];
|
|
35
|
+
anthropic: { model: string; has_api_key: boolean } | null;
|
|
36
|
+
openai: { model: string; has_api_key: boolean } | null;
|
|
37
|
+
gemini: { model: string; has_api_key: boolean } | null;
|
|
38
|
+
ollama: { base_url: string; model: string } | null;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Read LLM settings from DB + keychain and return a dashboard-safe response.
|
|
43
|
+
* Falls back to in-memory config values for anything not yet saved to DB.
|
|
44
|
+
*/
|
|
45
|
+
export function getLLMSettings(config: JarvisConfig): LLMSettingsResponse {
|
|
46
|
+
const primary = getSetting(SETTING_PRIMARY) ?? config.llm.primary;
|
|
47
|
+
const fallbackRaw = getSetting(SETTING_FALLBACK);
|
|
48
|
+
const fallback = fallbackRaw ? JSON.parse(fallbackRaw) : config.llm.fallback;
|
|
49
|
+
|
|
50
|
+
const anthropicModel = getSetting(SETTING_ANTHROPIC_MODEL) ?? config.llm.anthropic?.model ?? 'claude-sonnet-4-6';
|
|
51
|
+
const openaiModel = getSetting(SETTING_OPENAI_MODEL) ?? config.llm.openai?.model ?? 'gpt-5.4';
|
|
52
|
+
const geminiModel = getSetting(SETTING_GEMINI_MODEL) ?? config.llm.gemini?.model ?? 'gemini-3-flash-preview';
|
|
53
|
+
const ollamaModel = getSetting(SETTING_OLLAMA_MODEL) ?? config.llm.ollama?.model ?? 'llama3';
|
|
54
|
+
const ollamaBaseUrl = getSetting(SETTING_OLLAMA_BASE_URL) ?? config.llm.ollama?.base_url ?? 'http://localhost:11434';
|
|
55
|
+
|
|
56
|
+
const hasAnthropicKey = hasSecret(KEY_ANTHROPIC) || !!config.llm.anthropic?.api_key;
|
|
57
|
+
const hasOpenaiKey = hasSecret(KEY_OPENAI) || !!config.llm.openai?.api_key;
|
|
58
|
+
const hasGeminiKey = hasSecret(KEY_GEMINI) || !!config.llm.gemini?.api_key;
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
primary,
|
|
62
|
+
fallback,
|
|
63
|
+
anthropic: { model: anthropicModel, has_api_key: hasAnthropicKey },
|
|
64
|
+
openai: { model: openaiModel, has_api_key: hasOpenaiKey },
|
|
65
|
+
gemini: { model: geminiModel, has_api_key: hasGeminiKey },
|
|
66
|
+
ollama: { base_url: ollamaBaseUrl, model: ollamaModel },
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Save LLM settings to DB + keychain and update the in-memory config.
|
|
72
|
+
*/
|
|
73
|
+
export function saveLLMSettings(
|
|
74
|
+
config: JarvisConfig,
|
|
75
|
+
body: {
|
|
76
|
+
primary?: string;
|
|
77
|
+
fallback?: string[];
|
|
78
|
+
anthropic?: { api_key?: string; model?: string };
|
|
79
|
+
openai?: { api_key?: string; model?: string };
|
|
80
|
+
gemini?: { api_key?: string; model?: string };
|
|
81
|
+
ollama?: { base_url?: string; model?: string };
|
|
82
|
+
},
|
|
83
|
+
): void {
|
|
84
|
+
// Save non-secret settings to DB
|
|
85
|
+
if (body.primary) {
|
|
86
|
+
setSetting(SETTING_PRIMARY, body.primary);
|
|
87
|
+
config.llm.primary = body.primary;
|
|
88
|
+
}
|
|
89
|
+
if (body.fallback) {
|
|
90
|
+
setSetting(SETTING_FALLBACK, JSON.stringify(body.fallback));
|
|
91
|
+
config.llm.fallback = body.fallback;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Anthropic
|
|
95
|
+
if (body.anthropic) {
|
|
96
|
+
if (body.anthropic.model) {
|
|
97
|
+
setSetting(SETTING_ANTHROPIC_MODEL, body.anthropic.model);
|
|
98
|
+
}
|
|
99
|
+
if (body.anthropic.api_key) {
|
|
100
|
+
setSecret(KEY_ANTHROPIC, body.anthropic.api_key);
|
|
101
|
+
}
|
|
102
|
+
config.llm.anthropic = {
|
|
103
|
+
...config.llm.anthropic,
|
|
104
|
+
model: body.anthropic.model ?? config.llm.anthropic?.model,
|
|
105
|
+
api_key: body.anthropic.api_key ?? getAnthropicApiKey(config) ?? '',
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// OpenAI
|
|
110
|
+
if (body.openai) {
|
|
111
|
+
if (body.openai.model) {
|
|
112
|
+
setSetting(SETTING_OPENAI_MODEL, body.openai.model);
|
|
113
|
+
}
|
|
114
|
+
if (body.openai.api_key) {
|
|
115
|
+
setSecret(KEY_OPENAI, body.openai.api_key);
|
|
116
|
+
}
|
|
117
|
+
config.llm.openai = {
|
|
118
|
+
...config.llm.openai,
|
|
119
|
+
model: body.openai.model ?? config.llm.openai?.model,
|
|
120
|
+
api_key: body.openai.api_key ?? getOpenAIApiKey(config) ?? '',
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Gemini
|
|
125
|
+
if (body.gemini) {
|
|
126
|
+
if (body.gemini.model) {
|
|
127
|
+
setSetting(SETTING_GEMINI_MODEL, body.gemini.model);
|
|
128
|
+
}
|
|
129
|
+
if (body.gemini.api_key) {
|
|
130
|
+
setSecret(KEY_GEMINI, body.gemini.api_key);
|
|
131
|
+
}
|
|
132
|
+
config.llm.gemini = {
|
|
133
|
+
...config.llm.gemini,
|
|
134
|
+
model: body.gemini.model ?? config.llm.gemini?.model,
|
|
135
|
+
api_key: body.gemini.api_key ?? getGeminiApiKey(config) ?? '',
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Ollama
|
|
140
|
+
if (body.ollama) {
|
|
141
|
+
if (body.ollama.model) {
|
|
142
|
+
setSetting(SETTING_OLLAMA_MODEL, body.ollama.model);
|
|
143
|
+
}
|
|
144
|
+
if (body.ollama.base_url) {
|
|
145
|
+
setSetting(SETTING_OLLAMA_BASE_URL, body.ollama.base_url);
|
|
146
|
+
}
|
|
147
|
+
config.llm.ollama = {
|
|
148
|
+
...config.llm.ollama,
|
|
149
|
+
model: body.ollama.model ?? config.llm.ollama?.model,
|
|
150
|
+
base_url: body.ollama.base_url ?? config.llm.ollama?.base_url,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Resolve the Anthropic API key: keychain > config.yaml > env var.
|
|
157
|
+
*/
|
|
158
|
+
function getAnthropicApiKey(config: JarvisConfig): string | null {
|
|
159
|
+
return getSecret(KEY_ANTHROPIC) ?? config.llm.anthropic?.api_key ?? null;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Resolve the OpenAI API key: keychain > config.yaml > env var.
|
|
164
|
+
*/
|
|
165
|
+
function getOpenAIApiKey(config: JarvisConfig): string | null {
|
|
166
|
+
return getSecret(KEY_OPENAI) ?? config.llm.openai?.api_key ?? null;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Resolve the Gemini API key: keychain > config.yaml > env var.
|
|
171
|
+
*/
|
|
172
|
+
function getGeminiApiKey(config: JarvisConfig): string | null {
|
|
173
|
+
return getSecret(KEY_GEMINI) ?? config.llm.gemini?.api_key ?? null;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Merge DB/keychain LLM settings into config at startup.
|
|
178
|
+
* Env vars (already applied by loadConfig) take priority over DB values.
|
|
179
|
+
*/
|
|
180
|
+
export function mergeLLMSettingsIntoConfig(config: JarvisConfig): void {
|
|
181
|
+
// Only override from DB if env vars are NOT set
|
|
182
|
+
const dbPrimary = getSetting(SETTING_PRIMARY);
|
|
183
|
+
if (dbPrimary) config.llm.primary = dbPrimary;
|
|
184
|
+
|
|
185
|
+
const dbFallback = getSetting(SETTING_FALLBACK);
|
|
186
|
+
if (dbFallback) config.llm.fallback = JSON.parse(dbFallback);
|
|
187
|
+
|
|
188
|
+
// Anthropic
|
|
189
|
+
const dbAnthropicModel = getSetting(SETTING_ANTHROPIC_MODEL);
|
|
190
|
+
const keychainAnthropicKey = getSecret(KEY_ANTHROPIC);
|
|
191
|
+
if (dbAnthropicModel || keychainAnthropicKey) {
|
|
192
|
+
config.llm.anthropic = {
|
|
193
|
+
...config.llm.anthropic,
|
|
194
|
+
api_key: (!process.env.JARVIS_API_KEY && keychainAnthropicKey)
|
|
195
|
+
? keychainAnthropicKey
|
|
196
|
+
: (config.llm.anthropic?.api_key ?? ''),
|
|
197
|
+
model: dbAnthropicModel ?? config.llm.anthropic?.model,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// OpenAI
|
|
202
|
+
const dbOpenaiModel = getSetting(SETTING_OPENAI_MODEL);
|
|
203
|
+
const keychainOpenaiKey = getSecret(KEY_OPENAI);
|
|
204
|
+
if (dbOpenaiModel || keychainOpenaiKey) {
|
|
205
|
+
config.llm.openai = {
|
|
206
|
+
...config.llm.openai,
|
|
207
|
+
api_key: (!process.env.JARVIS_OPENAI_KEY && keychainOpenaiKey)
|
|
208
|
+
? keychainOpenaiKey
|
|
209
|
+
: (config.llm.openai?.api_key ?? ''),
|
|
210
|
+
model: dbOpenaiModel ?? config.llm.openai?.model,
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Gemini
|
|
215
|
+
const dbGeminiModel = getSetting(SETTING_GEMINI_MODEL);
|
|
216
|
+
const keychainGeminiKey = getSecret(KEY_GEMINI);
|
|
217
|
+
if (dbGeminiModel || keychainGeminiKey) {
|
|
218
|
+
config.llm.gemini = {
|
|
219
|
+
...config.llm.gemini,
|
|
220
|
+
api_key: (!process.env.JARVIS_GEMINI_KEY && keychainGeminiKey)
|
|
221
|
+
? keychainGeminiKey
|
|
222
|
+
: (config.llm.gemini?.api_key ?? ''),
|
|
223
|
+
model: dbGeminiModel ?? config.llm.gemini?.model,
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Ollama
|
|
228
|
+
const dbOllamaModel = getSetting(SETTING_OLLAMA_MODEL);
|
|
229
|
+
const dbOllamaUrl = getSetting(SETTING_OLLAMA_BASE_URL);
|
|
230
|
+
if (dbOllamaModel || dbOllamaUrl) {
|
|
231
|
+
config.llm.ollama = {
|
|
232
|
+
...config.llm.ollama,
|
|
233
|
+
model: dbOllamaModel ?? config.llm.ollama?.model,
|
|
234
|
+
base_url: (!process.env.JARVIS_OLLAMA_URL && dbOllamaUrl)
|
|
235
|
+
? dbOllamaUrl
|
|
236
|
+
: (config.llm.ollama?.base_url ?? 'http://localhost:11434'),
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Build fresh LLM provider instances from the current config and hot-reload them
|
|
243
|
+
* into the shared LLMManager (atomic swap, safe for in-flight requests).
|
|
244
|
+
*/
|
|
245
|
+
export function hotReloadLLMProviders(config: JarvisConfig, llmManager: LLMManager): void {
|
|
246
|
+
const { llm } = config;
|
|
247
|
+
const providers: LLMProvider[] = [];
|
|
248
|
+
|
|
249
|
+
if (llm.anthropic?.api_key) {
|
|
250
|
+
providers.push(new AnthropicProvider(llm.anthropic.api_key, llm.anthropic.model));
|
|
251
|
+
console.log('[LLM] Hot-reloaded Anthropic provider');
|
|
252
|
+
}
|
|
253
|
+
if (llm.openai?.api_key) {
|
|
254
|
+
providers.push(new OpenAIProvider(llm.openai.api_key, llm.openai.model));
|
|
255
|
+
console.log('[LLM] Hot-reloaded OpenAI provider');
|
|
256
|
+
}
|
|
257
|
+
if (llm.gemini?.api_key) {
|
|
258
|
+
providers.push(new GeminiProvider(llm.gemini.api_key, llm.gemini.model));
|
|
259
|
+
console.log('[LLM] Hot-reloaded Gemini provider');
|
|
260
|
+
}
|
|
261
|
+
if (llm.ollama) {
|
|
262
|
+
providers.push(new OllamaProvider(llm.ollama.base_url, llm.ollama.model));
|
|
263
|
+
console.log('[LLM] Hot-reloaded Ollama provider');
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const fallback = llm.fallback.filter(n => providers.some(p => p.name === n));
|
|
267
|
+
llmManager.replaceProviders(providers, llm.primary, fallback);
|
|
268
|
+
console.log(`[LLM] Providers active: ${providers.map(p => p.name).join(', ') || 'none'} (primary: ${llm.primary})`);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Test an LLM provider connection. Uses provided credentials if given,
|
|
273
|
+
* otherwise falls back to stored keys (keychain > config).
|
|
274
|
+
*/
|
|
275
|
+
export async function testLLMProvider(
|
|
276
|
+
opts: {
|
|
277
|
+
provider: string;
|
|
278
|
+
api_key?: string;
|
|
279
|
+
model?: string;
|
|
280
|
+
base_url?: string;
|
|
281
|
+
},
|
|
282
|
+
config: JarvisConfig,
|
|
283
|
+
): Promise<{ ok: boolean; model?: string; error?: string }> {
|
|
284
|
+
try {
|
|
285
|
+
let instance: LLMProvider;
|
|
286
|
+
|
|
287
|
+
if (opts.provider === 'anthropic') {
|
|
288
|
+
const key = opts.api_key || getSecret(KEY_ANTHROPIC) || config.llm.anthropic?.api_key;
|
|
289
|
+
if (!key) return { ok: false, error: 'API key required' };
|
|
290
|
+
instance = new AnthropicProvider(key, opts.model ?? config.llm.anthropic?.model);
|
|
291
|
+
} else if (opts.provider === 'openai') {
|
|
292
|
+
const key = opts.api_key || getSecret(KEY_OPENAI) || config.llm.openai?.api_key;
|
|
293
|
+
if (!key) return { ok: false, error: 'API key required' };
|
|
294
|
+
instance = new OpenAIProvider(key, opts.model ?? config.llm.openai?.model);
|
|
295
|
+
} else if (opts.provider === 'gemini') {
|
|
296
|
+
const key = opts.api_key || config.llm.gemini?.api_key;
|
|
297
|
+
if (!key) return { ok: false, error: 'API key required' };
|
|
298
|
+
instance = new GeminiProvider(key, opts.model ?? config.llm.gemini?.model);
|
|
299
|
+
} else if (opts.provider === 'ollama') {
|
|
300
|
+
instance = new OllamaProvider(
|
|
301
|
+
opts.base_url ?? config.llm.ollama?.base_url,
|
|
302
|
+
opts.model ?? config.llm.ollama?.model,
|
|
303
|
+
);
|
|
304
|
+
} else {
|
|
305
|
+
return { ok: false, error: `Unknown provider: ${opts.provider}` };
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const resp = await instance.chat(
|
|
309
|
+
[{ role: 'user', content: 'Say OK' }],
|
|
310
|
+
{ max_tokens: 5 },
|
|
311
|
+
);
|
|
312
|
+
return { ok: true, model: resp.model };
|
|
313
|
+
} catch (err) {
|
|
314
|
+
return { ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
315
|
+
}
|
|
316
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PID File Manager for J.A.R.V.I.S. Daemon
|
|
3
|
+
*
|
|
4
|
+
* Manages the daemon PID file at ~/.jarvis/jarvis.pid
|
|
5
|
+
* for start/stop/status lifecycle commands.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { join } from 'node:path';
|
|
9
|
+
import { homedir } from 'node:os';
|
|
10
|
+
import { existsSync, readFileSync, writeFileSync, unlinkSync, mkdirSync } from 'node:fs';
|
|
11
|
+
|
|
12
|
+
const JARVIS_DIR = join(homedir(), '.jarvis');
|
|
13
|
+
const LOG_DIR = join(JARVIS_DIR, 'logs');
|
|
14
|
+
const PID_PATH = join(JARVIS_DIR, 'jarvis.pid');
|
|
15
|
+
const LOG_PATH = join(LOG_DIR, 'jarvis.log');
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Write the current daemon PID to the PID file.
|
|
19
|
+
*/
|
|
20
|
+
export function writePid(pid: number): void {
|
|
21
|
+
try {
|
|
22
|
+
mkdirSync(JARVIS_DIR, { recursive: true }); // idempotent, no race
|
|
23
|
+
writeFileSync(PID_PATH, String(pid), 'utf-8');
|
|
24
|
+
} catch (err) {
|
|
25
|
+
console.error(`[PID] Failed to write PID file: ${err}`);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Read the PID from the PID file. Returns null if no PID file exists.
|
|
31
|
+
*/
|
|
32
|
+
export function readPid(): number | null {
|
|
33
|
+
if (!existsSync(PID_PATH)) return null;
|
|
34
|
+
try {
|
|
35
|
+
const content = readFileSync(PID_PATH, 'utf-8').trim();
|
|
36
|
+
const pid = parseInt(content, 10);
|
|
37
|
+
if (isNaN(pid) || pid <= 0) return null;
|
|
38
|
+
return pid;
|
|
39
|
+
} catch {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Clear (delete) the PID file.
|
|
46
|
+
*/
|
|
47
|
+
export function clearPid(): void {
|
|
48
|
+
try {
|
|
49
|
+
if (existsSync(PID_PATH)) {
|
|
50
|
+
unlinkSync(PID_PATH);
|
|
51
|
+
}
|
|
52
|
+
} catch (err) {
|
|
53
|
+
// Log but don't crash — file may already be gone or permissions issue
|
|
54
|
+
console.warn(`[PID] Could not remove PID file: ${err}`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Check if a daemon process is currently running.
|
|
60
|
+
* Returns the PID if running, null otherwise.
|
|
61
|
+
* Also cleans up stale PID files.
|
|
62
|
+
*/
|
|
63
|
+
export function isRunning(): number | null {
|
|
64
|
+
const pid = readPid();
|
|
65
|
+
if (pid === null) return null;
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
// signal 0 doesn't kill the process — just checks if it exists
|
|
69
|
+
process.kill(pid, 0);
|
|
70
|
+
return pid;
|
|
71
|
+
} catch {
|
|
72
|
+
// Process doesn't exist — stale PID file
|
|
73
|
+
clearPid();
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Get the PID file path (for display purposes).
|
|
80
|
+
*/
|
|
81
|
+
export function getPidPath(): string {
|
|
82
|
+
return PID_PATH;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Get the log file path. Creates the log directory if needed.
|
|
87
|
+
*/
|
|
88
|
+
export function getLogPath(): string {
|
|
89
|
+
mkdirSync(LOG_DIR, { recursive: true }); // idempotent
|
|
90
|
+
return LOG_PATH;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Get the log directory path.
|
|
95
|
+
*/
|
|
96
|
+
export function getLogDir(): string {
|
|
97
|
+
return LOG_DIR;
|
|
98
|
+
}
|