@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,827 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* J.A.R.V.I.S. Interactive Onboard Wizard
|
|
3
|
+
*
|
|
4
|
+
* Full first-time setup: user info, LLM provider, API keys, TTS, STT,
|
|
5
|
+
* channels, personality, authority, autostart.
|
|
6
|
+
* All steps are skippable except LLM configuration.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { isAbsolute, join } from 'node:path';
|
|
10
|
+
import { homedir } from 'node:os';
|
|
11
|
+
import { existsSync, mkdirSync } from 'node:fs';
|
|
12
|
+
import {
|
|
13
|
+
c, printBanner, printStep, printOk, printWarn, printErr, printInfo,
|
|
14
|
+
ask, askSecret, askYesNo, askChoice, startSpinner, closeRL, detectPlatform,
|
|
15
|
+
} from './helpers.ts';
|
|
16
|
+
import { DEFAULT_CONFIG, type JarvisConfig } from '../config/types.ts';
|
|
17
|
+
import { loadConfig, saveConfig } from '../config/loader.ts';
|
|
18
|
+
import { isOfficialOpenAI } from '../llm/openai.ts';
|
|
19
|
+
import { installAutostart, startAutostartService, getAutostartName, isAutostartSupported } from './autostart.ts';
|
|
20
|
+
import { runDependencyCheck } from './deps.ts';
|
|
21
|
+
import { initDatabase, closeDb } from '../vault/schema.ts';
|
|
22
|
+
import { saveUserProfile } from '../vault/user-profile.ts';
|
|
23
|
+
import { USER_PROFILE_QUESTIONS, normalizeUserProfileAnswers } from '../user/profile.ts';
|
|
24
|
+
|
|
25
|
+
const JARVIS_DIR = join(homedir(), '.jarvis');
|
|
26
|
+
const CONFIG_PATH = join(JARVIS_DIR, 'config.yaml');
|
|
27
|
+
const TOTAL_STEPS = 11;
|
|
28
|
+
|
|
29
|
+
export async function runOnboard(): Promise<void> {
|
|
30
|
+
printBanner();
|
|
31
|
+
console.log(c.bold('Welcome to the J.A.R.V.I.S. setup wizard!\n'));
|
|
32
|
+
console.log('This wizard will configure your personal AI assistant.');
|
|
33
|
+
console.log(c.dim('Most steps can be skipped and configured later.\n'));
|
|
34
|
+
|
|
35
|
+
// Load existing config or start with defaults
|
|
36
|
+
let config: JarvisConfig;
|
|
37
|
+
if (existsSync(CONFIG_PATH)) {
|
|
38
|
+
console.log(c.dim(`Found existing config at ${CONFIG_PATH}`));
|
|
39
|
+
const useExisting = await askYesNo('Use existing config as base?', true);
|
|
40
|
+
config = useExisting ? await loadConfig() : structuredClone(DEFAULT_CONFIG);
|
|
41
|
+
} else {
|
|
42
|
+
config = structuredClone(DEFAULT_CONFIG);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Ensure data directory
|
|
46
|
+
if (!existsSync(JARVIS_DIR)) {
|
|
47
|
+
mkdirSync(JARVIS_DIR, { recursive: true });
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// ── Step 1: About You ──────────────────────────────────────────────
|
|
51
|
+
|
|
52
|
+
printStep(1, TOTAL_STEPS, 'About You');
|
|
53
|
+
console.log(' Let\'s get to know each other.\n');
|
|
54
|
+
|
|
55
|
+
const userName = await ask('What\'s your name?', config.user?.name || '');
|
|
56
|
+
config.user = { name: userName };
|
|
57
|
+
|
|
58
|
+
const assistantName = await ask(
|
|
59
|
+
'What would you like to call your assistant?',
|
|
60
|
+
config.personality.assistant_name ?? 'Jarvis',
|
|
61
|
+
);
|
|
62
|
+
config.personality.assistant_name = assistantName;
|
|
63
|
+
|
|
64
|
+
if (userName) {
|
|
65
|
+
printOk(`Nice to meet you, ${userName}! I'll be your ${assistantName}.`);
|
|
66
|
+
} else {
|
|
67
|
+
printOk(`I'll be your ${assistantName}.`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ── Step 2: LLM Provider ──────────────────────────────────────────
|
|
71
|
+
|
|
72
|
+
printStep(2, TOTAL_STEPS, 'LLM Provider');
|
|
73
|
+
console.log(' JARVIS needs at least one AI model to function.\n');
|
|
74
|
+
|
|
75
|
+
const provider = await askChoice('Choose your primary LLM provider:', [
|
|
76
|
+
{ label: 'Anthropic (Claude)', value: 'anthropic' as const, description: 'Best quality, recommended' },
|
|
77
|
+
{ label: 'OpenAI (GPT)', value: 'openai' as const, description: 'Good alternative' },
|
|
78
|
+
{ label: 'Google (Gemini)', value: 'gemini' as const, description: 'Google AI models' },
|
|
79
|
+
{ label: 'Ollama (Local)', value: 'ollama' as const, description: 'Free, runs locally' },
|
|
80
|
+
{ label: 'OpenRouter', value: 'openrouter' as const, description: 'Access hundreds of models via single API key' },
|
|
81
|
+
{ label: 'Groq', value: 'groq' as const, description: 'Fast, OpenAI-compatible API' },
|
|
82
|
+
], config.llm.primary as any);
|
|
83
|
+
|
|
84
|
+
config.llm.primary = provider;
|
|
85
|
+
|
|
86
|
+
// Get API key and model for cloud providers
|
|
87
|
+
if (provider === 'anthropic') {
|
|
88
|
+
const existing = config.llm.anthropic?.api_key;
|
|
89
|
+
if (existing && existing.startsWith('sk-')) {
|
|
90
|
+
const keep = await askYesNo(`API key found (${existing.slice(0, 10)}...). Keep it?`, true);
|
|
91
|
+
if (!keep) {
|
|
92
|
+
const key = await askSecret('Enter your Anthropic API key');
|
|
93
|
+
if (key) config.llm.anthropic = { ...config.llm.anthropic, api_key: key };
|
|
94
|
+
}
|
|
95
|
+
} else {
|
|
96
|
+
const key = await askSecret('Enter your Anthropic API key (from console.anthropic.com)');
|
|
97
|
+
if (key) {
|
|
98
|
+
config.llm.anthropic = { ...config.llm.anthropic, api_key: key };
|
|
99
|
+
} else {
|
|
100
|
+
printWarn('No API key set. JARVIS won\'t work without one.');
|
|
101
|
+
printInfo('Set it later in ~/.jarvis/config.yaml');
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const currentModel = config.llm.anthropic?.model ?? 'claude-sonnet-4-6';
|
|
106
|
+
const anthropicModels = [
|
|
107
|
+
{ label: 'Claude Opus 4.6', value: 'claude-opus-4-6', description: 'Most capable, latest' },
|
|
108
|
+
{ label: 'Claude Sonnet 4.6', value: 'claude-sonnet-4-6', description: 'Best balance of speed & quality' },
|
|
109
|
+
{ label: 'Claude Sonnet 4.5', value: 'claude-sonnet-4-5-20250929', description: 'Previous generation' },
|
|
110
|
+
{ label: 'Claude Haiku 4.5', value: 'claude-haiku-4-5-20251001', description: 'Fastest, most affordable' },
|
|
111
|
+
{ label: 'Custom', value: 'custom', description: 'Enter model name manually' },
|
|
112
|
+
];
|
|
113
|
+
const isPreset = anthropicModels.some(m => m.value === currentModel);
|
|
114
|
+
const modelChoice = await askChoice('Choose a model:', anthropicModels, isPreset ? currentModel : 'custom');
|
|
115
|
+
const model = modelChoice === 'custom' ? await ask('Enter model name', currentModel) : modelChoice;
|
|
116
|
+
if (config.llm.anthropic) config.llm.anthropic.model = model;
|
|
117
|
+
|
|
118
|
+
} else if (provider === 'openai') {
|
|
119
|
+
const existing = config.llm.openai?.api_key;
|
|
120
|
+
const officialOpenAI = isOfficialOpenAI(config.llm.openai?.base_url);
|
|
121
|
+
const openAIKeyPrompt = officialOpenAI
|
|
122
|
+
? 'Enter your OpenAI API key (from platform.openai.com)'
|
|
123
|
+
: 'Enter your OpenAI-compatible API key';
|
|
124
|
+
|
|
125
|
+
if (existing && existing.trim().length > 0) {
|
|
126
|
+
const keep = await askYesNo(`API key found (${existing.slice(0, 10)}...). Keep it?`, true);
|
|
127
|
+
if (!keep) {
|
|
128
|
+
const key = await askSecret(openAIKeyPrompt);
|
|
129
|
+
if (key) config.llm.openai = { ...config.llm.openai, api_key: key };
|
|
130
|
+
}
|
|
131
|
+
} else {
|
|
132
|
+
const key = await askSecret(openAIKeyPrompt);
|
|
133
|
+
if (key) {
|
|
134
|
+
config.llm.openai = { ...config.llm.openai, api_key: key };
|
|
135
|
+
} else {
|
|
136
|
+
printWarn('No API key set. JARVIS won\'t work without one.');
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const currentModel = config.llm.openai?.model ?? 'gpt-5.4';
|
|
141
|
+
const openaiModels = [
|
|
142
|
+
{ label: 'GPT-5.4', value: 'gpt-5.4', description: 'Latest flagship' },
|
|
143
|
+
{ label: 'GPT-5.4 Thinking', value: 'gpt-5.4-thinking', description: 'Flagship with reasoning' },
|
|
144
|
+
{ label: 'GPT-5.4 Pro', value: 'gpt-5.4-pro', description: 'Highest capability' },
|
|
145
|
+
{ label: 'GPT-5.3 Instant', value: 'gpt-5.3-instant', description: 'Fast flagship' },
|
|
146
|
+
{ label: 'GPT-5 Mini', value: 'gpt-5-mini', description: 'Cost-efficient' },
|
|
147
|
+
{ label: 'GPT-5 Nano', value: 'gpt-5-nano', description: 'Cheapest GPT-5' },
|
|
148
|
+
{ label: 'GPT-5.1 Codex', value: 'gpt-5.1-codex', description: 'Code-focused' },
|
|
149
|
+
{ label: 'GPT-4.1', value: 'gpt-4.1', description: 'Previous gen, still solid' },
|
|
150
|
+
{ label: 'o3', value: 'o3', description: 'Reasoning model' },
|
|
151
|
+
{ label: 'o4-mini', value: 'o4-mini', description: 'Fast reasoning' },
|
|
152
|
+
{ label: 'Custom', value: 'custom', description: 'Enter model name manually' },
|
|
153
|
+
];
|
|
154
|
+
const isPreset = openaiModels.some(m => m.value === currentModel);
|
|
155
|
+
const modelChoice = await askChoice('Choose a model:', openaiModels, isPreset ? currentModel : 'custom');
|
|
156
|
+
const model = modelChoice === 'custom' ? await ask('Enter model name', currentModel) : modelChoice;
|
|
157
|
+
const openaiBaseUrl = await ask(
|
|
158
|
+
'OpenAI-compatible base URL (optional, leave blank for official OpenAI API)',
|
|
159
|
+
config.llm.openai?.base_url ?? '',
|
|
160
|
+
);
|
|
161
|
+
if (config.llm.openai) {
|
|
162
|
+
config.llm.openai.model = model;
|
|
163
|
+
config.llm.openai.base_url = openaiBaseUrl.trim() || undefined;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
} else if (provider === 'groq') {
|
|
167
|
+
const existing = config.llm.groq?.api_key;
|
|
168
|
+
if (existing && existing.length > 5) {
|
|
169
|
+
const keep = await askYesNo(`API key found (${existing.slice(0, 10)}...). Keep it?`, true);
|
|
170
|
+
if (!keep) {
|
|
171
|
+
const key = await askSecret('Enter your Groq API key');
|
|
172
|
+
if (key) config.llm.groq = { ...config.llm.groq, api_key: key };
|
|
173
|
+
}
|
|
174
|
+
} else {
|
|
175
|
+
const key = await askSecret('Enter your Groq API key (from console.groq.com)');
|
|
176
|
+
if (key) {
|
|
177
|
+
config.llm.groq = { ...config.llm.groq, api_key: key };
|
|
178
|
+
} else {
|
|
179
|
+
printWarn('No API key set. JARVIS won\'t work without one.');
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const currentModel = config.llm.groq?.model ?? 'llama-3.3-70b-versatile';
|
|
184
|
+
const groqModels = [
|
|
185
|
+
{ label: 'Llama 3.3 70B Versatile', value: 'llama-3.3-70b-versatile', description: 'Balanced capability and speed' },
|
|
186
|
+
{ label: 'Llama 3.1 8B Instant', value: 'llama-3.1-8b-instant', description: 'Fast and low latency' },
|
|
187
|
+
{ label: 'Qwen 3 32B', value: 'qwen/qwen3-32b', description: 'Strong general-purpose model' },
|
|
188
|
+
{ label: 'DeepSeek R1 Distill 70B', value: 'deepseek-r1-distill-llama-70b', description: 'Reasoning-focused model' },
|
|
189
|
+
{ label: 'Custom', value: 'custom', description: 'Enter model name manually' },
|
|
190
|
+
];
|
|
191
|
+
const isPreset = groqModels.some(m => m.value === currentModel);
|
|
192
|
+
const modelChoice = await askChoice('Choose a model:', groqModels, isPreset ? currentModel : 'custom');
|
|
193
|
+
const model = modelChoice === 'custom' ? await ask('Enter model name', currentModel) : modelChoice;
|
|
194
|
+
if (config.llm.groq) config.llm.groq.model = model;
|
|
195
|
+
|
|
196
|
+
} else if (provider === 'gemini') {
|
|
197
|
+
const existing = config.llm.gemini?.api_key;
|
|
198
|
+
if (existing && existing.length > 5) {
|
|
199
|
+
const keep = await askYesNo(`API key found (${existing.slice(0, 10)}...). Keep it?`, true);
|
|
200
|
+
if (!keep) {
|
|
201
|
+
const key = await askSecret('Enter your Google AI API key');
|
|
202
|
+
if (key) config.llm.gemini = { ...config.llm.gemini, api_key: key };
|
|
203
|
+
}
|
|
204
|
+
} else {
|
|
205
|
+
const key = await askSecret('Enter your Google AI API key (from aistudio.google.com)');
|
|
206
|
+
if (key) {
|
|
207
|
+
config.llm.gemini = { ...config.llm.gemini, api_key: key };
|
|
208
|
+
} else {
|
|
209
|
+
printWarn('No API key set. JARVIS won\'t work without one.');
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const currentModel = config.llm.gemini?.model ?? 'gemini-3-flash-preview';
|
|
214
|
+
const geminiModels = [
|
|
215
|
+
{ label: 'Gemini 3.1 Pro', value: 'gemini-3.1-pro-preview', description: 'Most intelligent, complex reasoning' },
|
|
216
|
+
{ label: 'Gemini 3 Deep Think', value: 'gemini-3-deep-think', description: 'Heavy science & research' },
|
|
217
|
+
{ label: 'Gemini 3 Flash', value: 'gemini-3-flash-preview', description: 'Fast, pro-level intelligence' },
|
|
218
|
+
{ label: 'Gemini 3.1 Flash-Lite', value: 'gemini-3-1-flash-lite-preview', description: 'Ultra-efficient, high-volume' },
|
|
219
|
+
{ label: 'Gemini 2.5 Pro', value: 'gemini-2.5-pro', description: 'Previous gen, still solid' },
|
|
220
|
+
{ label: 'Gemini 2.5 Flash', value: 'gemini-2.5-flash', description: 'Previous gen, fast' },
|
|
221
|
+
{ label: 'Custom', value: 'custom', description: 'Enter model name manually' },
|
|
222
|
+
];
|
|
223
|
+
const isPreset = geminiModels.some(m => m.value === currentModel);
|
|
224
|
+
const modelChoice = await askChoice('Choose a model:', geminiModels, isPreset ? currentModel : 'custom');
|
|
225
|
+
const model = modelChoice === 'custom' ? await ask('Enter model name', currentModel) : modelChoice;
|
|
226
|
+
if (config.llm.gemini) config.llm.gemini.model = model;
|
|
227
|
+
|
|
228
|
+
} else if (provider === 'openrouter') {
|
|
229
|
+
const existing = config.llm.openrouter?.api_key;
|
|
230
|
+
if (existing && existing.startsWith('sk-or-')) {
|
|
231
|
+
const keep = await askYesNo(`API key found (${existing.slice(0, 12)}...). Keep it?`, true);
|
|
232
|
+
if (!keep) {
|
|
233
|
+
const key = await askSecret('Enter your OpenRouter API key');
|
|
234
|
+
if (key) config.llm.openrouter = { ...config.llm.openrouter, api_key: key };
|
|
235
|
+
}
|
|
236
|
+
} else {
|
|
237
|
+
const key = await askSecret('Enter your OpenRouter API key (from openrouter.ai/keys)');
|
|
238
|
+
if (key) {
|
|
239
|
+
config.llm.openrouter = { ...config.llm.openrouter, api_key: key };
|
|
240
|
+
} else {
|
|
241
|
+
printWarn('No API key set. JARVIS won\'t work without one.');
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const currentModel = config.llm.openrouter?.model ?? 'anthropic/claude-sonnet-4';
|
|
246
|
+
const openrouterModels = [
|
|
247
|
+
{ label: 'Claude Sonnet 4', value: 'anthropic/claude-sonnet-4', description: 'Anthropic, great balance' },
|
|
248
|
+
{ label: 'Claude Opus 4', value: 'anthropic/claude-opus-4', description: 'Anthropic, most capable' },
|
|
249
|
+
{ label: 'Claude Haiku 4', value: 'anthropic/claude-haiku-4', description: 'Anthropic, fast & cheap' },
|
|
250
|
+
{ label: 'GPT-5.4', value: 'openai/gpt-5.4', description: 'OpenAI flagship' },
|
|
251
|
+
{ label: 'o3', value: 'openai/o3', description: 'OpenAI reasoning' },
|
|
252
|
+
{ label: 'Gemini 2.5 Pro', value: 'google/gemini-2.5-pro', description: 'Google, best quality' },
|
|
253
|
+
{ label: 'Gemini 2.5 Flash', value: 'google/gemini-2.5-flash', description: 'Google, fast' },
|
|
254
|
+
{ label: 'DeepSeek R1', value: 'deepseek/deepseek-r1', description: 'DeepSeek reasoning' },
|
|
255
|
+
{ label: 'Llama 4 Maverick', value: 'meta-llama/llama-4-maverick', description: 'Meta, open-weight' },
|
|
256
|
+
{ label: 'Mistral Large', value: 'mistralai/mistral-large', description: 'Mistral, strong all-round' },
|
|
257
|
+
{ label: 'Custom', value: 'custom', description: 'Enter model name manually' },
|
|
258
|
+
];
|
|
259
|
+
const isPreset = openrouterModels.some(m => m.value === currentModel);
|
|
260
|
+
const modelChoice = await askChoice('Choose a model:', openrouterModels, isPreset ? currentModel : 'custom');
|
|
261
|
+
const model = modelChoice === 'custom' ? await ask('Enter model name (provider/model format)', currentModel) : modelChoice;
|
|
262
|
+
if (config.llm.openrouter) config.llm.openrouter.model = model;
|
|
263
|
+
|
|
264
|
+
} else if (provider === 'ollama') {
|
|
265
|
+
const url = await ask('Ollama base URL', config.llm.ollama?.base_url ?? 'http://localhost:11434');
|
|
266
|
+
|
|
267
|
+
const currentModel = config.llm.ollama?.model ?? 'llama3';
|
|
268
|
+
const ollamaModels = [
|
|
269
|
+
{ label: 'Llama 3', value: 'llama3', description: 'General purpose' },
|
|
270
|
+
{ label: 'Llama 3 70B', value: 'llama3:70b', description: 'Larger, more capable' },
|
|
271
|
+
{ label: 'Mistral', value: 'mistral', description: 'Fast, good quality' },
|
|
272
|
+
{ label: 'DeepSeek Coder', value: 'deepseek-coder', description: 'Code-focused' },
|
|
273
|
+
{ label: 'CodeLlama', value: 'codellama', description: 'Code-focused' },
|
|
274
|
+
{ label: 'Custom', value: 'custom', description: 'Enter model name manually' },
|
|
275
|
+
];
|
|
276
|
+
const isPreset = ollamaModels.some(m => m.value === currentModel);
|
|
277
|
+
const modelChoice = await askChoice('Choose a model:', ollamaModels, isPreset ? currentModel : 'custom');
|
|
278
|
+
const model = modelChoice === 'custom' ? await ask('Enter model name', currentModel) : modelChoice;
|
|
279
|
+
|
|
280
|
+
config.llm.ollama = { base_url: url, model };
|
|
281
|
+
printInfo('Make sure Ollama is running: ollama serve');
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Test connectivity
|
|
285
|
+
const testConn = await askYesNo('Test LLM connectivity?', true);
|
|
286
|
+
if (testConn) {
|
|
287
|
+
const spin = startSpinner('Testing connection...');
|
|
288
|
+
try {
|
|
289
|
+
const { LLMManager, AnthropicProvider, OpenAIProvider, GroqProvider, GeminiProvider, OllamaProvider, OpenRouterProvider } = await import('../llm/index.ts');
|
|
290
|
+
const manager = new LLMManager();
|
|
291
|
+
|
|
292
|
+
if (provider === 'anthropic' && config.llm.anthropic?.api_key) {
|
|
293
|
+
manager.registerProvider(new AnthropicProvider(config.llm.anthropic.api_key, config.llm.anthropic.model));
|
|
294
|
+
} else if (provider === 'openai' && config.llm.openai?.api_key) {
|
|
295
|
+
manager.registerProvider(new OpenAIProvider(
|
|
296
|
+
config.llm.openai.api_key,
|
|
297
|
+
config.llm.openai.model,
|
|
298
|
+
config.llm.openai.base_url,
|
|
299
|
+
));
|
|
300
|
+
} else if (provider === 'groq' && config.llm.groq?.api_key) {
|
|
301
|
+
manager.registerProvider(new GroqProvider(config.llm.groq.api_key, config.llm.groq.model));
|
|
302
|
+
} else if (provider === 'gemini' && config.llm.gemini?.api_key) {
|
|
303
|
+
manager.registerProvider(new GeminiProvider(config.llm.gemini.api_key, config.llm.gemini.model));
|
|
304
|
+
} else if (provider === 'openrouter' && config.llm.openrouter?.api_key) {
|
|
305
|
+
manager.registerProvider(new OpenRouterProvider(config.llm.openrouter.api_key, config.llm.openrouter.model));
|
|
306
|
+
} else if (provider === 'ollama') {
|
|
307
|
+
manager.registerProvider(new OllamaProvider(config.llm.ollama?.base_url, config.llm.ollama?.model));
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
manager.setPrimary(provider);
|
|
311
|
+
const resp = await Promise.race([
|
|
312
|
+
manager.chat(
|
|
313
|
+
[{ role: 'user', content: 'Say "JARVIS online" in 3 words.' }],
|
|
314
|
+
{ max_tokens: 20 },
|
|
315
|
+
),
|
|
316
|
+
new Promise<never>((_, reject) =>
|
|
317
|
+
setTimeout(() => reject(new Error('Connection timed out (15s)')), 15_000),
|
|
318
|
+
),
|
|
319
|
+
]);
|
|
320
|
+
spin.stop(`Connected! Model: ${resp.model}`);
|
|
321
|
+
} catch (err) {
|
|
322
|
+
spin.stop();
|
|
323
|
+
printErr(`Connection failed: ${err}`);
|
|
324
|
+
printInfo('Check your API key and try again later.');
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// Fallback providers
|
|
329
|
+
config.llm.fallback = ['anthropic', 'openai', 'gemini', 'ollama', 'openrouter', 'groq'].filter(p => p !== provider);
|
|
330
|
+
|
|
331
|
+
// ── Step 3: Fallback API Keys ─────────────────────────────────────
|
|
332
|
+
|
|
333
|
+
printStep(3, TOTAL_STEPS, 'Fallback Providers');
|
|
334
|
+
console.log(' Optional: configure backup LLM providers.\n');
|
|
335
|
+
|
|
336
|
+
const setupFallbacks = await askYesNo('Configure fallback providers?', false);
|
|
337
|
+
if (setupFallbacks) {
|
|
338
|
+
for (const fb of config.llm.fallback) {
|
|
339
|
+
if (fb === 'anthropic' && (!config.llm.anthropic?.api_key || config.llm.anthropic.api_key === '')) {
|
|
340
|
+
const key = await askSecret('Anthropic API key (for fallback)');
|
|
341
|
+
if (key) config.llm.anthropic = { ...config.llm.anthropic, api_key: key, model: config.llm.anthropic?.model ?? 'claude-sonnet-4-6' };
|
|
342
|
+
} else if (fb === 'openai' && (!config.llm.openai?.api_key || config.llm.openai.api_key === '')) {
|
|
343
|
+
const key = await askSecret('OpenAI API key (for fallback)');
|
|
344
|
+
if (key) {
|
|
345
|
+
const baseUrl = await ask(
|
|
346
|
+
'OpenAI-compatible base URL for fallback (optional, leave blank for official OpenAI API)',
|
|
347
|
+
config.llm.openai?.base_url ?? '',
|
|
348
|
+
);
|
|
349
|
+
config.llm.openai = {
|
|
350
|
+
...config.llm.openai,
|
|
351
|
+
api_key: key,
|
|
352
|
+
model: config.llm.openai?.model ?? 'gpt-5.4',
|
|
353
|
+
base_url: baseUrl.trim() || undefined,
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
} else if (fb === 'groq' && (!config.llm.groq?.api_key || config.llm.groq.api_key === '')) {
|
|
357
|
+
const key = await askSecret('Groq API key (for fallback)');
|
|
358
|
+
if (key) config.llm.groq = { ...config.llm.groq, api_key: key, model: config.llm.groq?.model ?? 'llama-3.3-70b-versatile' };
|
|
359
|
+
} else if (fb === 'gemini' && (!config.llm.gemini?.api_key || config.llm.gemini.api_key === '')) {
|
|
360
|
+
const key = await askSecret('Google AI API key (for fallback)');
|
|
361
|
+
if (key) config.llm.gemini = { ...config.llm.gemini, api_key: key, model: config.llm.gemini?.model ?? 'gemini-3-flash-preview' };
|
|
362
|
+
} else if (fb === 'openrouter' && (!config.llm.openrouter?.api_key || config.llm.openrouter.api_key === '')) {
|
|
363
|
+
const key = await askSecret('OpenRouter API key (for fallback)');
|
|
364
|
+
if (key) config.llm.openrouter = { ...config.llm.openrouter, api_key: key, model: config.llm.openrouter?.model ?? 'anthropic/claude-sonnet-4' };
|
|
365
|
+
} else if (fb === 'ollama') {
|
|
366
|
+
const setupOllama = await askYesNo('Configure Ollama as fallback?', false);
|
|
367
|
+
if (setupOllama) {
|
|
368
|
+
const url = await ask('Ollama URL', 'http://localhost:11434');
|
|
369
|
+
const model = await ask('Ollama model', 'llama3');
|
|
370
|
+
config.llm.ollama = { base_url: url, model };
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
} else {
|
|
375
|
+
printInfo('Skipped. You can add fallback providers later in config.');
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// ── Step 4: System Dependencies ─────────────────────────────────
|
|
379
|
+
|
|
380
|
+
printStep(4, TOTAL_STEPS, 'System Dependencies');
|
|
381
|
+
console.log(' Checking for optional system tools JARVIS can use.\n');
|
|
382
|
+
|
|
383
|
+
await runDependencyCheck(config);
|
|
384
|
+
|
|
385
|
+
// ── Step 5: TTS (Text-to-Speech) ─────────────────────────────────
|
|
386
|
+
|
|
387
|
+
printStep(5, TOTAL_STEPS, 'Voice Output (TTS)');
|
|
388
|
+
console.log(' JARVIS can speak responses aloud.\n');
|
|
389
|
+
|
|
390
|
+
const enableTTS = await askYesNo('Enable text-to-speech?', false);
|
|
391
|
+
config.tts = config.tts || { enabled: false };
|
|
392
|
+
config.tts.enabled = enableTTS;
|
|
393
|
+
|
|
394
|
+
if (enableTTS) {
|
|
395
|
+
const ttsProvider = await askChoice('TTS provider:', [
|
|
396
|
+
{ label: 'Microsoft Edge TTS', value: 'edge' as const, description: 'Free, no API key needed' },
|
|
397
|
+
{ label: 'ElevenLabs', value: 'elevenlabs' as const, description: 'Premium quality, API key required' },
|
|
398
|
+
], config.tts.provider ?? 'edge');
|
|
399
|
+
|
|
400
|
+
config.tts.provider = ttsProvider;
|
|
401
|
+
|
|
402
|
+
if (ttsProvider === 'edge') {
|
|
403
|
+
const voice = await askChoice('Choose a voice:', [
|
|
404
|
+
{ label: 'Aria (US Female)', value: 'en-US-AriaNeural' },
|
|
405
|
+
{ label: 'Guy (US Male)', value: 'en-US-GuyNeural' },
|
|
406
|
+
{ label: 'Sonia (UK Female)', value: 'en-GB-SoniaNeural' },
|
|
407
|
+
{ label: 'Natasha (AU Female)', value: 'en-AU-NatashaNeural' },
|
|
408
|
+
{ label: 'Jenny (US Female)', value: 'en-US-JennyNeural' },
|
|
409
|
+
{ label: 'Davis (US Male)', value: 'en-US-DavisNeural' },
|
|
410
|
+
], config.tts.voice ?? 'en-US-AriaNeural');
|
|
411
|
+
config.tts.voice = voice;
|
|
412
|
+
} else if (ttsProvider === 'elevenlabs') {
|
|
413
|
+
const existing = config.tts.elevenlabs?.api_key;
|
|
414
|
+
let apiKey: string;
|
|
415
|
+
|
|
416
|
+
if (existing) {
|
|
417
|
+
const keep = await askYesNo('ElevenLabs API key found. Keep it?', true);
|
|
418
|
+
apiKey = keep ? existing : await askSecret('ElevenLabs API key (from elevenlabs.io)');
|
|
419
|
+
} else {
|
|
420
|
+
apiKey = await askSecret('ElevenLabs API key (from elevenlabs.io)');
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
if (apiKey) {
|
|
424
|
+
config.tts.elevenlabs = {
|
|
425
|
+
...config.tts.elevenlabs,
|
|
426
|
+
api_key: apiKey,
|
|
427
|
+
};
|
|
428
|
+
|
|
429
|
+
// Fetch available voices
|
|
430
|
+
const spin = startSpinner('Fetching available voices...');
|
|
431
|
+
try {
|
|
432
|
+
const { listElevenLabsVoices } = await import('../comms/voice.ts');
|
|
433
|
+
const voices = await Promise.race([
|
|
434
|
+
listElevenLabsVoices(apiKey),
|
|
435
|
+
new Promise<never>((_, reject) =>
|
|
436
|
+
setTimeout(() => reject(new Error('Timed out')), 10_000),
|
|
437
|
+
),
|
|
438
|
+
]);
|
|
439
|
+
|
|
440
|
+
spin.stop(`Found ${voices.length} voices`);
|
|
441
|
+
|
|
442
|
+
if (voices.length > 0) {
|
|
443
|
+
const voiceOptions = voices.slice(0, 10).map(v => ({
|
|
444
|
+
label: `${v.name} (${v.category})`,
|
|
445
|
+
value: v.voice_id,
|
|
446
|
+
}));
|
|
447
|
+
voiceOptions.push({ label: 'Custom', value: 'custom' });
|
|
448
|
+
|
|
449
|
+
const currentVoiceId = config.tts.elevenlabs.voice_id;
|
|
450
|
+
const isPreset = voiceOptions.some(v => v.value === currentVoiceId);
|
|
451
|
+
const voiceChoice = await askChoice(
|
|
452
|
+
'Choose a voice:',
|
|
453
|
+
voiceOptions,
|
|
454
|
+
isPreset ? currentVoiceId : voiceOptions[0]!.value,
|
|
455
|
+
);
|
|
456
|
+
|
|
457
|
+
if (voiceChoice === 'custom') {
|
|
458
|
+
const customId = await ask('Enter voice ID', currentVoiceId ?? '');
|
|
459
|
+
config.tts.elevenlabs.voice_id = customId || undefined;
|
|
460
|
+
} else {
|
|
461
|
+
config.tts.elevenlabs.voice_id = voiceChoice;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
} catch {
|
|
465
|
+
spin.stop();
|
|
466
|
+
printWarn('Could not fetch voices. Using default voice.');
|
|
467
|
+
const customId = await ask('Enter voice ID (optional)', config.tts.elevenlabs.voice_id ?? '');
|
|
468
|
+
if (customId) config.tts.elevenlabs.voice_id = customId;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// Model selection
|
|
472
|
+
const elModel = await askChoice('ElevenLabs model:', [
|
|
473
|
+
{ label: 'Flash v2.5', value: 'eleven_flash_v2_5', description: 'Fast, low latency' },
|
|
474
|
+
{ label: 'Multilingual v2', value: 'eleven_multilingual_v2', description: 'Higher quality' },
|
|
475
|
+
], config.tts.elevenlabs.model ?? 'eleven_flash_v2_5');
|
|
476
|
+
config.tts.elevenlabs.model = elModel;
|
|
477
|
+
} else {
|
|
478
|
+
printWarn('No API key provided. Falling back to Edge TTS.');
|
|
479
|
+
config.tts.provider = 'edge';
|
|
480
|
+
config.tts.voice = 'en-US-AriaNeural';
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
} else {
|
|
484
|
+
printInfo('Skipped. Enable later in config.');
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// ── Step 6: STT (Speech-to-Text) ─────────────────────────────────
|
|
488
|
+
|
|
489
|
+
printStep(6, TOTAL_STEPS, 'Voice Input (STT)');
|
|
490
|
+
console.log(' For voice commands via the dashboard microphone button.\n');
|
|
491
|
+
|
|
492
|
+
const setupSTT = await askYesNo('Configure speech-to-text?', false);
|
|
493
|
+
if (setupSTT) {
|
|
494
|
+
const sttProvider = await askChoice('STT provider:', [
|
|
495
|
+
{ label: 'OpenAI Whisper', value: 'openai' as const, description: 'Best accuracy, uses OpenAI API key' },
|
|
496
|
+
{ label: 'Groq Whisper', value: 'groq' as const, description: 'Fast, free tier available' },
|
|
497
|
+
{ label: 'Local Whisper', value: 'local' as const, description: 'Self-hosted, fully private' },
|
|
498
|
+
], config.stt?.provider as any ?? 'openai');
|
|
499
|
+
|
|
500
|
+
config.stt = { provider: sttProvider };
|
|
501
|
+
|
|
502
|
+
if (sttProvider === 'openai') {
|
|
503
|
+
const defaultSttBaseUrl = config.stt.openai?.base_url ?? config.llm.openai?.base_url ?? '';
|
|
504
|
+
const officialOpenAIStt = isOfficialOpenAI(defaultSttBaseUrl);
|
|
505
|
+
const sttKeyPrompt = officialOpenAIStt
|
|
506
|
+
? 'OpenAI API key for Whisper STT'
|
|
507
|
+
: 'OpenAI-compatible API key for Whisper STT';
|
|
508
|
+
|
|
509
|
+
// Reuse OpenAI API key if already set
|
|
510
|
+
if (config.llm.openai?.api_key) {
|
|
511
|
+
const reuse = await askYesNo('Reuse your OpenAI API key for STT?', true);
|
|
512
|
+
if (reuse) {
|
|
513
|
+
const baseUrl = await ask(
|
|
514
|
+
'OpenAI-compatible base URL for STT (optional, leave blank for official OpenAI API)',
|
|
515
|
+
defaultSttBaseUrl,
|
|
516
|
+
);
|
|
517
|
+
config.stt.openai = {
|
|
518
|
+
api_key: config.llm.openai.api_key,
|
|
519
|
+
base_url: baseUrl.trim() || undefined,
|
|
520
|
+
};
|
|
521
|
+
} else {
|
|
522
|
+
const key = await askSecret(sttKeyPrompt);
|
|
523
|
+
if (key) {
|
|
524
|
+
const baseUrl = await ask(
|
|
525
|
+
'OpenAI-compatible base URL for STT (optional, leave blank for official OpenAI API)',
|
|
526
|
+
defaultSttBaseUrl,
|
|
527
|
+
);
|
|
528
|
+
config.stt.openai = { api_key: key, base_url: baseUrl.trim() || undefined };
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
} else {
|
|
532
|
+
const key = await askSecret(sttKeyPrompt);
|
|
533
|
+
if (key) {
|
|
534
|
+
const baseUrl = await ask(
|
|
535
|
+
'OpenAI-compatible base URL for STT (optional, leave blank for official OpenAI API)',
|
|
536
|
+
defaultSttBaseUrl,
|
|
537
|
+
);
|
|
538
|
+
config.stt.openai = { api_key: key, base_url: baseUrl.trim() || undefined };
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
} else if (sttProvider === 'groq') {
|
|
542
|
+
const key = await askSecret('Groq API key (from console.groq.com)');
|
|
543
|
+
if (key) config.stt.groq = { api_key: key };
|
|
544
|
+
} else if (sttProvider === 'local') {
|
|
545
|
+
const endpoint = await ask('Local Whisper endpoint', 'http://localhost:8080');
|
|
546
|
+
config.stt.local = { endpoint };
|
|
547
|
+
}
|
|
548
|
+
} else {
|
|
549
|
+
printInfo('Skipped. Voice input will be disabled.');
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
// ── Step 7: Channels ──────────────────────────────────────────────
|
|
553
|
+
|
|
554
|
+
printStep(7, TOTAL_STEPS, 'Communication Channels');
|
|
555
|
+
console.log(' JARVIS can receive messages from Telegram and Discord.\n');
|
|
556
|
+
|
|
557
|
+
const setupChannels = await askYesNo('Configure messaging channels?', false);
|
|
558
|
+
if (setupChannels) {
|
|
559
|
+
// Telegram
|
|
560
|
+
const setupTG = await askYesNo('Set up Telegram?', false);
|
|
561
|
+
if (setupTG) {
|
|
562
|
+
const token = await askSecret('Telegram bot token (from @BotFather)');
|
|
563
|
+
if (token) {
|
|
564
|
+
const userId = await ask('Your Telegram user ID (numeric)');
|
|
565
|
+
config.channels = config.channels ?? {};
|
|
566
|
+
config.channels.telegram = {
|
|
567
|
+
enabled: true,
|
|
568
|
+
bot_token: token,
|
|
569
|
+
allowed_users: userId ? [parseInt(userId, 10)] : [],
|
|
570
|
+
};
|
|
571
|
+
printOk('Telegram configured.');
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
// Discord
|
|
576
|
+
const setupDC = await askYesNo('Set up Discord?', false);
|
|
577
|
+
if (setupDC) {
|
|
578
|
+
const token = await askSecret('Discord bot token (from discord.dev)');
|
|
579
|
+
if (token) {
|
|
580
|
+
const userId = await ask('Your Discord user ID');
|
|
581
|
+
config.channels = config.channels ?? {};
|
|
582
|
+
config.channels.discord = {
|
|
583
|
+
enabled: true,
|
|
584
|
+
bot_token: token,
|
|
585
|
+
allowed_users: userId ? [userId] : [],
|
|
586
|
+
};
|
|
587
|
+
printOk('Discord configured.');
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
} else {
|
|
591
|
+
printInfo('Skipped. Configure channels later for remote access.');
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// ── Step 8: Personality ───────────────────────────────────────────
|
|
595
|
+
|
|
596
|
+
printStep(8, TOTAL_STEPS, 'Personality');
|
|
597
|
+
console.log(' Customize JARVIS\'s personality traits.\n');
|
|
598
|
+
|
|
599
|
+
const customPersonality = await askYesNo('Customize personality traits?', false);
|
|
600
|
+
if (customPersonality) {
|
|
601
|
+
console.log(c.dim(' Current traits: ' + config.personality.core_traits.join(', ')));
|
|
602
|
+
const traitsInput = await ask(
|
|
603
|
+
'Enter traits (comma-separated)',
|
|
604
|
+
config.personality.core_traits.join(', ')
|
|
605
|
+
);
|
|
606
|
+
config.personality.core_traits = traitsInput.split(',').map(t => t.trim()).filter(Boolean);
|
|
607
|
+
printOk(`Traits: ${config.personality.core_traits.join(', ')}`);
|
|
608
|
+
} else {
|
|
609
|
+
printInfo(`Using defaults: ${config.personality.core_traits.join(', ')}`);
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
// ── Step 9: Authority Level ───────────────────────────────────────
|
|
613
|
+
|
|
614
|
+
printStep(9, TOTAL_STEPS, 'Authority & Safety');
|
|
615
|
+
console.log(' Controls what JARVIS can do autonomously.\n');
|
|
616
|
+
console.log(c.dim(' Level 1-3: Conservative (read-only, ask for everything)'));
|
|
617
|
+
console.log(c.dim(' Level 4-6: Moderate (browse, read/write files, run safe commands)'));
|
|
618
|
+
console.log(c.dim(' Level 7-10: Aggressive (full autonomy, sends emails, manages apps)'));
|
|
619
|
+
console.log('');
|
|
620
|
+
|
|
621
|
+
const customAuth = await askYesNo('Customize authority settings?', false);
|
|
622
|
+
if (customAuth) {
|
|
623
|
+
const levelStr = await ask('Default authority level (1-10)', String(config.authority.default_level));
|
|
624
|
+
const level = parseInt(levelStr, 10);
|
|
625
|
+
if (level >= 1 && level <= 10) {
|
|
626
|
+
config.authority.default_level = level;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
// Governed categories
|
|
630
|
+
console.log(c.dim('\n Governed categories require your approval before executing:'));
|
|
631
|
+
console.log(c.dim(' Current: ' + config.authority.governed_categories.join(', ')));
|
|
632
|
+
printInfo('Keeping default governed categories (send_email, send_message, make_payment)');
|
|
633
|
+
} else {
|
|
634
|
+
printInfo(`Using defaults: level ${config.authority.default_level}, governed: ${config.authority.governed_categories.join(', ')}`);
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
// ── Step 10: Keepalive ────────────────────────────────────────────
|
|
638
|
+
|
|
639
|
+
printStep(10, TOTAL_STEPS, 'Keepalive');
|
|
640
|
+
const platform = detectPlatform();
|
|
641
|
+
let enableKeepalive = false;
|
|
642
|
+
const keepaliveSupported = isAutostartSupported();
|
|
643
|
+
|
|
644
|
+
if (!keepaliveSupported) {
|
|
645
|
+
if (platform === 'wsl') {
|
|
646
|
+
printInfo('WSL2 detected, but the user systemd service manager is not available in this session.');
|
|
647
|
+
printInfo('Enable systemd in WSL, then rerun onboard to use 24/7 keepalive mode.');
|
|
648
|
+
} else {
|
|
649
|
+
printInfo('Keepalive mode is not supported in this environment.');
|
|
650
|
+
}
|
|
651
|
+
printInfo('Start JARVIS manually with: jarvis start');
|
|
652
|
+
} else {
|
|
653
|
+
if (process.platform === 'linux' || process.platform === 'darwin') {
|
|
654
|
+
const platformHint = platform === 'wsl' ? ' on WSL2' : '';
|
|
655
|
+
console.log(` Keepalive mode uses ${c.bold(getAutostartName())}${platformHint} to keep JARVIS running`);
|
|
656
|
+
console.log(' after you close the terminal, with automatic restart if the service exits.\n');
|
|
657
|
+
enableKeepalive = await askYesNo('Activate JARVIS keepalive mode?', false);
|
|
658
|
+
} else {
|
|
659
|
+
console.log(` Autostart mechanism: ${c.bold(getAutostartName())}\n`);
|
|
660
|
+
enableKeepalive = await askYesNo('Start JARVIS automatically?', false);
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
// ── Step 11: Know Your User ───────────────────────────────────────
|
|
665
|
+
|
|
666
|
+
printStep(11, TOTAL_STEPS, 'Know Your User');
|
|
667
|
+
console.log(' Optional: answer a richer profile wizard so JARVIS starts with context about who you are,\n' +
|
|
668
|
+
' what you care about, and how you like to work.\n');
|
|
669
|
+
|
|
670
|
+
let userProfileAnswers: Record<string, string> | null = null;
|
|
671
|
+
const runProfileWizard = await askYesNo('Answer user profile questions now?', false);
|
|
672
|
+
if (runProfileWizard) {
|
|
673
|
+
const rawAnswers: Record<string, string> = {};
|
|
674
|
+
|
|
675
|
+
for (const question of USER_PROFILE_QUESTIONS) {
|
|
676
|
+
console.log('');
|
|
677
|
+
console.log(c.bold(` ${question.step_title} · ${question.label}`));
|
|
678
|
+
console.log(c.dim(` ${question.description}`));
|
|
679
|
+
|
|
680
|
+
const defaultAnswer =
|
|
681
|
+
question.id === 'preferred_name'
|
|
682
|
+
? (config.user?.name || '')
|
|
683
|
+
: '';
|
|
684
|
+
|
|
685
|
+
const answer = await ask(question.prompt, defaultAnswer);
|
|
686
|
+
if (answer.trim()) {
|
|
687
|
+
rawAnswers[question.id] = answer.trim();
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
userProfileAnswers = normalizeUserProfileAnswers(rawAnswers) as Record<string, string>;
|
|
692
|
+
printOk(`Captured ${Object.keys(userProfileAnswers).length} profile answer(s).`);
|
|
693
|
+
} else {
|
|
694
|
+
printInfo('Skipped. You can complete the same wizard later in Settings > Profile.');
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
// ── Port (quick inline question) ──────────────────────────────────
|
|
698
|
+
|
|
699
|
+
console.log('');
|
|
700
|
+
const changePort = await askYesNo(`Dashboard will run on port ${config.daemon.port}. Change it?`, false);
|
|
701
|
+
if (changePort) {
|
|
702
|
+
const portStr = await ask('Dashboard port', String(config.daemon.port));
|
|
703
|
+
const port = parseInt(portStr, 10);
|
|
704
|
+
if (port > 0 && port < 65536) config.daemon.port = port;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
console.log('');
|
|
708
|
+
const existingDashboardPassword = config.dashboard?.password_hash;
|
|
709
|
+
if (existingDashboardPassword) {
|
|
710
|
+
const keepPassword = await askYesNo('Dashboard password is already configured. Keep it?', true);
|
|
711
|
+
if (!keepPassword) {
|
|
712
|
+
const nextPassword = await askSecret(
|
|
713
|
+
'Dashboard password (optional, leave blank to keep the panel unsecured)',
|
|
714
|
+
);
|
|
715
|
+
config.dashboard = config.dashboard ?? {};
|
|
716
|
+
config.dashboard.password_hash = nextPassword.trim()
|
|
717
|
+
? await Bun.password.hash(nextPassword.trim())
|
|
718
|
+
: undefined;
|
|
719
|
+
}
|
|
720
|
+
} else {
|
|
721
|
+
const dashboardPassword = await askSecret(
|
|
722
|
+
'Dashboard password (optional, leave blank to keep the panel unsecured)',
|
|
723
|
+
);
|
|
724
|
+
if (dashboardPassword.trim()) {
|
|
725
|
+
config.dashboard = config.dashboard ?? {};
|
|
726
|
+
config.dashboard.password_hash = await Bun.password.hash(dashboardPassword.trim());
|
|
727
|
+
} else {
|
|
728
|
+
config.dashboard = config.dashboard ?? {};
|
|
729
|
+
config.dashboard.password_hash = undefined;
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
// ── Save ──────────────────────────────────────────────────────────
|
|
734
|
+
|
|
735
|
+
console.log('\n' + c.bold('─'.repeat(50)));
|
|
736
|
+
console.log(c.bold('\nConfiguration Summary:\n'));
|
|
737
|
+
|
|
738
|
+
const ttsLabel = !config.tts?.enabled ? 'disabled'
|
|
739
|
+
: config.tts.provider === 'elevenlabs' ? 'ElevenLabs'
|
|
740
|
+
: `${config.tts.voice} (Edge)`;
|
|
741
|
+
|
|
742
|
+
const summaryItems: [string, string][] = [
|
|
743
|
+
['User', config.user?.name || c.dim('not set')],
|
|
744
|
+
['Assistant', config.personality.assistant_name ?? 'Jarvis'],
|
|
745
|
+
['LLM Provider', `${config.llm.primary} (${config.llm[config.llm.primary as keyof typeof config.llm] ? 'configured' : 'not set'})`],
|
|
746
|
+
['Fallback', config.llm.fallback.join(' -> ')],
|
|
747
|
+
['TTS', ttsLabel],
|
|
748
|
+
['STT', config.stt?.provider ?? 'not configured'],
|
|
749
|
+
['Telegram', config.channels?.telegram?.enabled ? 'enabled' : 'disabled'],
|
|
750
|
+
['Discord', config.channels?.discord?.enabled ? 'enabled' : 'disabled'],
|
|
751
|
+
['Authority', `level ${config.authority.default_level}`],
|
|
752
|
+
['Dashboard', config.dashboard?.password_hash ? 'password protected' : 'unsecured'],
|
|
753
|
+
['Keepalive', enableKeepalive ? 'enabled' : 'disabled'],
|
|
754
|
+
['Port', String(config.daemon.port)],
|
|
755
|
+
];
|
|
756
|
+
|
|
757
|
+
for (const [key, value] of summaryItems) {
|
|
758
|
+
console.log(` ${c.dim(key.padEnd(16))} ${value}`);
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
console.log('');
|
|
762
|
+
|
|
763
|
+
const doSave = await askYesNo('Save this configuration?', true);
|
|
764
|
+
let keepaliveStarted = false;
|
|
765
|
+
if (doSave) {
|
|
766
|
+
await saveConfig(config);
|
|
767
|
+
printOk(`Config saved to ${CONFIG_PATH}`);
|
|
768
|
+
|
|
769
|
+
if (enableKeepalive) {
|
|
770
|
+
const installed = await installAutostart();
|
|
771
|
+
if (installed) {
|
|
772
|
+
keepaliveStarted = await startAutostartService();
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
if (userProfileAnswers && Object.keys(userProfileAnswers).length > 0) {
|
|
777
|
+
try {
|
|
778
|
+
initDatabase(resolveOnboardDbPath(config));
|
|
779
|
+
saveUserProfile(userProfileAnswers);
|
|
780
|
+
printOk('User profile saved to the vault.');
|
|
781
|
+
} catch (err) {
|
|
782
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
783
|
+
printWarn(`Config was saved, but user profile could not be stored: ${msg}`);
|
|
784
|
+
} finally {
|
|
785
|
+
closeDb();
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
} else {
|
|
789
|
+
printWarn('Configuration not saved.');
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
// Offer to start daemon
|
|
793
|
+
console.log('');
|
|
794
|
+
const keepaliveActive = doSave && keepaliveStarted;
|
|
795
|
+
const defaultStartNow = keepaliveActive ? false : true;
|
|
796
|
+
const startNowPrompt = keepaliveActive
|
|
797
|
+
? 'Start another foreground JARVIS process now?'
|
|
798
|
+
: 'Start JARVIS now?';
|
|
799
|
+
const startNow = await askYesNo(startNowPrompt, defaultStartNow);
|
|
800
|
+
if (startNow) {
|
|
801
|
+
console.log(c.cyan('\nStarting J.A.R.V.I.S. daemon...\n'));
|
|
802
|
+
closeRL();
|
|
803
|
+
|
|
804
|
+
const { startDaemon } = await import('../daemon/index.ts');
|
|
805
|
+
await startDaemon();
|
|
806
|
+
} else {
|
|
807
|
+
if (keepaliveActive) {
|
|
808
|
+
console.log(c.dim('\nJARVIS keepalive mode is managing the daemon.\n'));
|
|
809
|
+
} else {
|
|
810
|
+
console.log(c.dim('\nStart later with: jarvis start\n'));
|
|
811
|
+
}
|
|
812
|
+
closeRL();
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
function expandHome(filepath: string): string {
|
|
817
|
+
if (filepath.startsWith('~/')) {
|
|
818
|
+
return join(homedir(), filepath.slice(2));
|
|
819
|
+
}
|
|
820
|
+
return filepath;
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
function resolveOnboardDbPath(config: JarvisConfig): string {
|
|
824
|
+
const dataDir = expandHome(config.daemon.data_dir);
|
|
825
|
+
const dbPath = expandHome(config.daemon.db_path);
|
|
826
|
+
return isAbsolute(dbPath) ? dbPath : join(dataDir, dbPath);
|
|
827
|
+
}
|