@usejarvis/brain 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (266) hide show
  1. package/LICENSE +153 -0
  2. package/README.md +278 -0
  3. package/bin/jarvis.ts +413 -0
  4. package/package.json +74 -0
  5. package/scripts/ensure-bun.cjs +8 -0
  6. package/src/actions/README.md +421 -0
  7. package/src/actions/app-control/desktop-controller.test.ts +26 -0
  8. package/src/actions/app-control/desktop-controller.ts +438 -0
  9. package/src/actions/app-control/interface.ts +64 -0
  10. package/src/actions/app-control/linux.ts +273 -0
  11. package/src/actions/app-control/macos.ts +54 -0
  12. package/src/actions/app-control/sidecar-launcher.test.ts +23 -0
  13. package/src/actions/app-control/sidecar-launcher.ts +286 -0
  14. package/src/actions/app-control/windows.ts +44 -0
  15. package/src/actions/browser/cdp.ts +138 -0
  16. package/src/actions/browser/chrome-launcher.ts +252 -0
  17. package/src/actions/browser/session.ts +437 -0
  18. package/src/actions/browser/stealth.ts +49 -0
  19. package/src/actions/index.ts +20 -0
  20. package/src/actions/terminal/executor.ts +157 -0
  21. package/src/actions/terminal/wsl-bridge.ts +126 -0
  22. package/src/actions/test.ts +93 -0
  23. package/src/actions/tools/agents.ts +321 -0
  24. package/src/actions/tools/builtin.ts +846 -0
  25. package/src/actions/tools/commitments.ts +192 -0
  26. package/src/actions/tools/content.ts +217 -0
  27. package/src/actions/tools/delegate.ts +147 -0
  28. package/src/actions/tools/desktop.test.ts +55 -0
  29. package/src/actions/tools/desktop.ts +305 -0
  30. package/src/actions/tools/goals.ts +376 -0
  31. package/src/actions/tools/local-tools-guard.ts +20 -0
  32. package/src/actions/tools/registry.ts +171 -0
  33. package/src/actions/tools/research.ts +111 -0
  34. package/src/actions/tools/sidecar-list.ts +57 -0
  35. package/src/actions/tools/sidecar-route.ts +105 -0
  36. package/src/actions/tools/workflows.ts +216 -0
  37. package/src/agents/agent.ts +132 -0
  38. package/src/agents/delegation.ts +107 -0
  39. package/src/agents/hierarchy.ts +113 -0
  40. package/src/agents/index.ts +19 -0
  41. package/src/agents/messaging.ts +125 -0
  42. package/src/agents/orchestrator.ts +576 -0
  43. package/src/agents/role-discovery.ts +61 -0
  44. package/src/agents/sub-agent-runner.ts +307 -0
  45. package/src/agents/task-manager.ts +151 -0
  46. package/src/authority/approval-delivery.ts +59 -0
  47. package/src/authority/approval.ts +196 -0
  48. package/src/authority/audit.ts +158 -0
  49. package/src/authority/authority.test.ts +519 -0
  50. package/src/authority/deferred-executor.ts +103 -0
  51. package/src/authority/emergency.ts +66 -0
  52. package/src/authority/engine.ts +297 -0
  53. package/src/authority/index.ts +12 -0
  54. package/src/authority/learning.ts +111 -0
  55. package/src/authority/tool-action-map.ts +74 -0
  56. package/src/awareness/analytics.ts +466 -0
  57. package/src/awareness/awareness.test.ts +332 -0
  58. package/src/awareness/capture-engine.ts +305 -0
  59. package/src/awareness/context-graph.ts +130 -0
  60. package/src/awareness/context-tracker.ts +349 -0
  61. package/src/awareness/index.ts +25 -0
  62. package/src/awareness/intelligence.ts +321 -0
  63. package/src/awareness/ocr-engine.ts +88 -0
  64. package/src/awareness/service.ts +528 -0
  65. package/src/awareness/struggle-detector.ts +342 -0
  66. package/src/awareness/suggestion-engine.ts +476 -0
  67. package/src/awareness/types.ts +201 -0
  68. package/src/cli/autostart.ts +241 -0
  69. package/src/cli/deps.ts +449 -0
  70. package/src/cli/doctor.ts +230 -0
  71. package/src/cli/helpers.ts +401 -0
  72. package/src/cli/onboard.ts +580 -0
  73. package/src/comms/README.md +329 -0
  74. package/src/comms/auth-error.html +48 -0
  75. package/src/comms/channels/discord.ts +228 -0
  76. package/src/comms/channels/signal.ts +56 -0
  77. package/src/comms/channels/telegram.ts +316 -0
  78. package/src/comms/channels/whatsapp.ts +60 -0
  79. package/src/comms/channels.test.ts +173 -0
  80. package/src/comms/desktop-notify.ts +114 -0
  81. package/src/comms/example.ts +129 -0
  82. package/src/comms/index.ts +129 -0
  83. package/src/comms/streaming.ts +142 -0
  84. package/src/comms/voice.test.ts +152 -0
  85. package/src/comms/voice.ts +291 -0
  86. package/src/comms/websocket.test.ts +409 -0
  87. package/src/comms/websocket.ts +473 -0
  88. package/src/config/README.md +387 -0
  89. package/src/config/index.ts +6 -0
  90. package/src/config/loader.test.ts +137 -0
  91. package/src/config/loader.ts +142 -0
  92. package/src/config/types.ts +260 -0
  93. package/src/daemon/README.md +232 -0
  94. package/src/daemon/agent-service-interface.ts +9 -0
  95. package/src/daemon/agent-service.ts +600 -0
  96. package/src/daemon/api-routes.ts +2119 -0
  97. package/src/daemon/background-agent-service.ts +396 -0
  98. package/src/daemon/background-agent.test.ts +78 -0
  99. package/src/daemon/channel-service.ts +201 -0
  100. package/src/daemon/commitment-executor.ts +297 -0
  101. package/src/daemon/event-classifier.ts +239 -0
  102. package/src/daemon/event-coalescer.ts +123 -0
  103. package/src/daemon/event-reactor.ts +214 -0
  104. package/src/daemon/health.ts +220 -0
  105. package/src/daemon/index.ts +1004 -0
  106. package/src/daemon/llm-settings.ts +316 -0
  107. package/src/daemon/observer-service.ts +150 -0
  108. package/src/daemon/pid.ts +98 -0
  109. package/src/daemon/research-queue.ts +155 -0
  110. package/src/daemon/services.ts +175 -0
  111. package/src/daemon/ws-service.ts +788 -0
  112. package/src/goals/accountability.ts +240 -0
  113. package/src/goals/awareness-bridge.ts +185 -0
  114. package/src/goals/estimator.ts +185 -0
  115. package/src/goals/events.ts +28 -0
  116. package/src/goals/goals.test.ts +400 -0
  117. package/src/goals/integration.test.ts +329 -0
  118. package/src/goals/nl-builder.test.ts +220 -0
  119. package/src/goals/nl-builder.ts +256 -0
  120. package/src/goals/rhythm.test.ts +177 -0
  121. package/src/goals/rhythm.ts +275 -0
  122. package/src/goals/service.test.ts +135 -0
  123. package/src/goals/service.ts +348 -0
  124. package/src/goals/types.ts +106 -0
  125. package/src/goals/workflow-bridge.ts +96 -0
  126. package/src/integrations/google-api.ts +134 -0
  127. package/src/integrations/google-auth.ts +175 -0
  128. package/src/llm/README.md +291 -0
  129. package/src/llm/anthropic.ts +386 -0
  130. package/src/llm/gemini.ts +371 -0
  131. package/src/llm/index.ts +19 -0
  132. package/src/llm/manager.ts +153 -0
  133. package/src/llm/ollama.ts +307 -0
  134. package/src/llm/openai.ts +350 -0
  135. package/src/llm/provider.test.ts +231 -0
  136. package/src/llm/provider.ts +60 -0
  137. package/src/llm/test.ts +87 -0
  138. package/src/observers/README.md +278 -0
  139. package/src/observers/calendar.ts +113 -0
  140. package/src/observers/clipboard.ts +136 -0
  141. package/src/observers/email.ts +109 -0
  142. package/src/observers/example.ts +58 -0
  143. package/src/observers/file-watcher.ts +124 -0
  144. package/src/observers/index.ts +159 -0
  145. package/src/observers/notifications.ts +197 -0
  146. package/src/observers/observers.test.ts +203 -0
  147. package/src/observers/processes.ts +225 -0
  148. package/src/personality/README.md +61 -0
  149. package/src/personality/adapter.ts +196 -0
  150. package/src/personality/index.ts +20 -0
  151. package/src/personality/learner.ts +209 -0
  152. package/src/personality/model.ts +132 -0
  153. package/src/personality/personality.test.ts +236 -0
  154. package/src/roles/README.md +252 -0
  155. package/src/roles/authority.ts +119 -0
  156. package/src/roles/example-usage.ts +198 -0
  157. package/src/roles/index.ts +42 -0
  158. package/src/roles/loader.ts +143 -0
  159. package/src/roles/prompt-builder.ts +194 -0
  160. package/src/roles/test-multi.ts +102 -0
  161. package/src/roles/test-role.yaml +77 -0
  162. package/src/roles/test-utils.ts +93 -0
  163. package/src/roles/test.ts +106 -0
  164. package/src/roles/tool-guide.ts +190 -0
  165. package/src/roles/types.ts +36 -0
  166. package/src/roles/utils.ts +200 -0
  167. package/src/scripts/google-setup.ts +168 -0
  168. package/src/sidecar/connection.ts +179 -0
  169. package/src/sidecar/index.ts +6 -0
  170. package/src/sidecar/manager.ts +542 -0
  171. package/src/sidecar/protocol.ts +85 -0
  172. package/src/sidecar/rpc.ts +161 -0
  173. package/src/sidecar/scheduler.ts +136 -0
  174. package/src/sidecar/types.ts +112 -0
  175. package/src/sidecar/validator.ts +144 -0
  176. package/src/vault/README.md +110 -0
  177. package/src/vault/awareness.ts +341 -0
  178. package/src/vault/commitments.ts +299 -0
  179. package/src/vault/content-pipeline.ts +260 -0
  180. package/src/vault/conversations.ts +173 -0
  181. package/src/vault/entities.ts +180 -0
  182. package/src/vault/extractor.test.ts +356 -0
  183. package/src/vault/extractor.ts +345 -0
  184. package/src/vault/facts.ts +190 -0
  185. package/src/vault/goals.ts +477 -0
  186. package/src/vault/index.ts +87 -0
  187. package/src/vault/keychain.ts +99 -0
  188. package/src/vault/observations.ts +115 -0
  189. package/src/vault/relationships.ts +178 -0
  190. package/src/vault/retrieval.test.ts +126 -0
  191. package/src/vault/retrieval.ts +227 -0
  192. package/src/vault/schema.ts +658 -0
  193. package/src/vault/settings.ts +38 -0
  194. package/src/vault/vectors.ts +92 -0
  195. package/src/vault/workflows.ts +403 -0
  196. package/src/workflows/auto-suggest.ts +290 -0
  197. package/src/workflows/engine.ts +366 -0
  198. package/src/workflows/events.ts +24 -0
  199. package/src/workflows/executor.ts +207 -0
  200. package/src/workflows/nl-builder.ts +198 -0
  201. package/src/workflows/nodes/actions/agent-task.ts +73 -0
  202. package/src/workflows/nodes/actions/calendar-action.ts +85 -0
  203. package/src/workflows/nodes/actions/code-execution.ts +73 -0
  204. package/src/workflows/nodes/actions/discord.ts +77 -0
  205. package/src/workflows/nodes/actions/file-write.ts +73 -0
  206. package/src/workflows/nodes/actions/gmail.ts +69 -0
  207. package/src/workflows/nodes/actions/http-request.ts +117 -0
  208. package/src/workflows/nodes/actions/notification.ts +85 -0
  209. package/src/workflows/nodes/actions/run-tool.ts +55 -0
  210. package/src/workflows/nodes/actions/send-message.ts +82 -0
  211. package/src/workflows/nodes/actions/shell-command.ts +76 -0
  212. package/src/workflows/nodes/actions/telegram.ts +60 -0
  213. package/src/workflows/nodes/builtin.ts +119 -0
  214. package/src/workflows/nodes/error/error-handler.ts +37 -0
  215. package/src/workflows/nodes/error/fallback.ts +47 -0
  216. package/src/workflows/nodes/error/retry.ts +82 -0
  217. package/src/workflows/nodes/logic/delay.ts +42 -0
  218. package/src/workflows/nodes/logic/if-else.ts +41 -0
  219. package/src/workflows/nodes/logic/loop.ts +90 -0
  220. package/src/workflows/nodes/logic/merge.ts +38 -0
  221. package/src/workflows/nodes/logic/race.ts +40 -0
  222. package/src/workflows/nodes/logic/switch.ts +59 -0
  223. package/src/workflows/nodes/logic/template-render.ts +53 -0
  224. package/src/workflows/nodes/logic/variable-get.ts +37 -0
  225. package/src/workflows/nodes/logic/variable-set.ts +59 -0
  226. package/src/workflows/nodes/registry.ts +99 -0
  227. package/src/workflows/nodes/transform/aggregate.ts +99 -0
  228. package/src/workflows/nodes/transform/csv-parse.ts +70 -0
  229. package/src/workflows/nodes/transform/json-parse.ts +63 -0
  230. package/src/workflows/nodes/transform/map-filter.ts +84 -0
  231. package/src/workflows/nodes/transform/regex-match.ts +89 -0
  232. package/src/workflows/nodes/triggers/calendar.ts +33 -0
  233. package/src/workflows/nodes/triggers/clipboard.ts +32 -0
  234. package/src/workflows/nodes/triggers/cron.ts +40 -0
  235. package/src/workflows/nodes/triggers/email.ts +40 -0
  236. package/src/workflows/nodes/triggers/file-change.ts +45 -0
  237. package/src/workflows/nodes/triggers/git.ts +46 -0
  238. package/src/workflows/nodes/triggers/manual.ts +23 -0
  239. package/src/workflows/nodes/triggers/poll.ts +81 -0
  240. package/src/workflows/nodes/triggers/process.ts +44 -0
  241. package/src/workflows/nodes/triggers/screen-event.ts +37 -0
  242. package/src/workflows/nodes/triggers/webhook.ts +39 -0
  243. package/src/workflows/safe-eval.ts +139 -0
  244. package/src/workflows/template.ts +118 -0
  245. package/src/workflows/triggers/cron.ts +311 -0
  246. package/src/workflows/triggers/manager.ts +285 -0
  247. package/src/workflows/triggers/observer-bridge.ts +172 -0
  248. package/src/workflows/triggers/poller.ts +201 -0
  249. package/src/workflows/triggers/screen-condition.ts +218 -0
  250. package/src/workflows/triggers/triggers.test.ts +740 -0
  251. package/src/workflows/triggers/webhook.ts +191 -0
  252. package/src/workflows/types.ts +133 -0
  253. package/src/workflows/variables.ts +72 -0
  254. package/src/workflows/workflows.test.ts +383 -0
  255. package/src/workflows/yaml.ts +104 -0
  256. package/ui/dist/index-j75njzc1.css +1199 -0
  257. package/ui/dist/index-p2zh407q.js +80603 -0
  258. package/ui/dist/index.html +13 -0
  259. package/ui/public/openwakeword/models/embedding_model.onnx +0 -0
  260. package/ui/public/openwakeword/models/hey_jarvis_v0.1.onnx +0 -0
  261. package/ui/public/openwakeword/models/melspectrogram.onnx +0 -0
  262. package/ui/public/openwakeword/models/silero_vad.onnx +0 -0
  263. package/ui/public/ort/ort-wasm-simd-threaded.jsep.mjs +106 -0
  264. package/ui/public/ort/ort-wasm-simd-threaded.jsep.wasm +0 -0
  265. package/ui/public/ort/ort-wasm-simd-threaded.mjs +59 -0
  266. package/ui/public/ort/ort-wasm-simd-threaded.wasm +0 -0
@@ -0,0 +1,580 @@
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 { 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 { installAutostart, getAutostartName } from './autostart.ts';
19
+ import { runDependencyCheck } from './deps.ts';
20
+
21
+ const JARVIS_DIR = join(homedir(), '.jarvis');
22
+ const CONFIG_PATH = join(JARVIS_DIR, 'config.yaml');
23
+ const TOTAL_STEPS = 10;
24
+
25
+ export async function runOnboard(): Promise<void> {
26
+ printBanner();
27
+ console.log(c.bold('Welcome to the J.A.R.V.I.S. setup wizard!\n'));
28
+ console.log('This wizard will configure your personal AI assistant.');
29
+ console.log(c.dim('Most steps can be skipped and configured later.\n'));
30
+
31
+ // Load existing config or start with defaults
32
+ let config: JarvisConfig;
33
+ if (existsSync(CONFIG_PATH)) {
34
+ console.log(c.dim(`Found existing config at ${CONFIG_PATH}`));
35
+ const useExisting = await askYesNo('Use existing config as base?', true);
36
+ config = useExisting ? await loadConfig() : structuredClone(DEFAULT_CONFIG);
37
+ } else {
38
+ config = structuredClone(DEFAULT_CONFIG);
39
+ }
40
+
41
+ // Ensure data directory
42
+ if (!existsSync(JARVIS_DIR)) {
43
+ mkdirSync(JARVIS_DIR, { recursive: true });
44
+ }
45
+
46
+ // ── Step 1: About You ──────────────────────────────────────────────
47
+
48
+ printStep(1, TOTAL_STEPS, 'About You');
49
+ console.log(' Let\'s get to know each other.\n');
50
+
51
+ const userName = await ask('What\'s your name?', config.user?.name || '');
52
+ config.user = { name: userName };
53
+
54
+ const assistantName = await ask(
55
+ 'What would you like to call your assistant?',
56
+ config.personality.assistant_name ?? 'Jarvis',
57
+ );
58
+ config.personality.assistant_name = assistantName;
59
+
60
+ if (userName) {
61
+ printOk(`Nice to meet you, ${userName}! I'll be your ${assistantName}.`);
62
+ } else {
63
+ printOk(`I'll be your ${assistantName}.`);
64
+ }
65
+
66
+ // ── Step 2: LLM Provider ──────────────────────────────────────────
67
+
68
+ printStep(2, TOTAL_STEPS, 'LLM Provider');
69
+ console.log(' JARVIS needs at least one AI model to function.\n');
70
+
71
+ const provider = await askChoice('Choose your primary LLM provider:', [
72
+ { label: 'Anthropic (Claude)', value: 'anthropic' as const, description: 'Best quality, recommended' },
73
+ { label: 'OpenAI (GPT)', value: 'openai' as const, description: 'Good alternative' },
74
+ { label: 'Google (Gemini)', value: 'gemini' as const, description: 'Google AI models' },
75
+ { label: 'Ollama (Local)', value: 'ollama' as const, description: 'Free, runs locally' },
76
+ ], config.llm.primary as any);
77
+
78
+ config.llm.primary = provider;
79
+
80
+ // Get API key and model for cloud providers
81
+ if (provider === 'anthropic') {
82
+ const existing = config.llm.anthropic?.api_key;
83
+ if (existing && existing.startsWith('sk-')) {
84
+ const keep = await askYesNo(`API key found (${existing.slice(0, 10)}...). Keep it?`, true);
85
+ if (!keep) {
86
+ const key = await askSecret('Enter your Anthropic API key');
87
+ if (key) config.llm.anthropic = { ...config.llm.anthropic, api_key: key };
88
+ }
89
+ } else {
90
+ const key = await askSecret('Enter your Anthropic API key (from console.anthropic.com)');
91
+ if (key) {
92
+ config.llm.anthropic = { ...config.llm.anthropic, api_key: key };
93
+ } else {
94
+ printWarn('No API key set. JARVIS won\'t work without one.');
95
+ printInfo('Set it later in ~/.jarvis/config.yaml');
96
+ }
97
+ }
98
+
99
+ const currentModel = config.llm.anthropic?.model ?? 'claude-sonnet-4-6';
100
+ const anthropicModels = [
101
+ { label: 'Claude Opus 4.6', value: 'claude-opus-4-6', description: 'Most capable, latest' },
102
+ { label: 'Claude Sonnet 4.6', value: 'claude-sonnet-4-6', description: 'Best balance of speed & quality' },
103
+ { label: 'Claude Sonnet 4.5', value: 'claude-sonnet-4-5-20250929', description: 'Previous generation' },
104
+ { label: 'Claude Haiku 4.5', value: 'claude-haiku-4-5-20251001', description: 'Fastest, most affordable' },
105
+ { label: 'Custom', value: 'custom', description: 'Enter model name manually' },
106
+ ];
107
+ const isPreset = anthropicModels.some(m => m.value === currentModel);
108
+ const modelChoice = await askChoice('Choose a model:', anthropicModels, isPreset ? currentModel : 'custom');
109
+ const model = modelChoice === 'custom' ? await ask('Enter model name', currentModel) : modelChoice;
110
+ if (config.llm.anthropic) config.llm.anthropic.model = model;
111
+
112
+ } else if (provider === 'openai') {
113
+ const existing = config.llm.openai?.api_key;
114
+ if (existing && existing.startsWith('sk-')) {
115
+ const keep = await askYesNo(`API key found (${existing.slice(0, 10)}...). Keep it?`, true);
116
+ if (!keep) {
117
+ const key = await askSecret('Enter your OpenAI API key');
118
+ if (key) config.llm.openai = { ...config.llm.openai, api_key: key };
119
+ }
120
+ } else {
121
+ const key = await askSecret('Enter your OpenAI API key (from platform.openai.com)');
122
+ if (key) {
123
+ config.llm.openai = { ...config.llm.openai, api_key: key };
124
+ } else {
125
+ printWarn('No API key set. JARVIS won\'t work without one.');
126
+ }
127
+ }
128
+
129
+ const currentModel = config.llm.openai?.model ?? 'gpt-5.4';
130
+ const openaiModels = [
131
+ { label: 'GPT-5.4', value: 'gpt-5.4', description: 'Latest flagship' },
132
+ { label: 'GPT-5.4 Thinking', value: 'gpt-5.4-thinking', description: 'Flagship with reasoning' },
133
+ { label: 'GPT-5.4 Pro', value: 'gpt-5.4-pro', description: 'Highest capability' },
134
+ { label: 'GPT-5.3 Instant', value: 'gpt-5.3-instant', description: 'Fast flagship' },
135
+ { label: 'GPT-5 Mini', value: 'gpt-5-mini', description: 'Cost-efficient' },
136
+ { label: 'GPT-5 Nano', value: 'gpt-5-nano', description: 'Cheapest GPT-5' },
137
+ { label: 'GPT-5.1 Codex', value: 'gpt-5.1-codex', description: 'Code-focused' },
138
+ { label: 'GPT-4.1', value: 'gpt-4.1', description: 'Previous gen, still solid' },
139
+ { label: 'o3', value: 'o3', description: 'Reasoning model' },
140
+ { label: 'o4-mini', value: 'o4-mini', description: 'Fast reasoning' },
141
+ { label: 'Custom', value: 'custom', description: 'Enter model name manually' },
142
+ ];
143
+ const isPreset = openaiModels.some(m => m.value === currentModel);
144
+ const modelChoice = await askChoice('Choose a model:', openaiModels, isPreset ? currentModel : 'custom');
145
+ const model = modelChoice === 'custom' ? await ask('Enter model name', currentModel) : modelChoice;
146
+ if (config.llm.openai) config.llm.openai.model = model;
147
+
148
+ } else if (provider === 'gemini') {
149
+ const existing = config.llm.gemini?.api_key;
150
+ if (existing && existing.length > 5) {
151
+ const keep = await askYesNo(`API key found (${existing.slice(0, 10)}...). Keep it?`, true);
152
+ if (!keep) {
153
+ const key = await askSecret('Enter your Google AI API key');
154
+ if (key) config.llm.gemini = { ...config.llm.gemini, api_key: key };
155
+ }
156
+ } else {
157
+ const key = await askSecret('Enter your Google AI API key (from aistudio.google.com)');
158
+ if (key) {
159
+ config.llm.gemini = { ...config.llm.gemini, api_key: key };
160
+ } else {
161
+ printWarn('No API key set. JARVIS won\'t work without one.');
162
+ }
163
+ }
164
+
165
+ const currentModel = config.llm.gemini?.model ?? 'gemini-3-flash-preview';
166
+ const geminiModels = [
167
+ { label: 'Gemini 3.1 Pro', value: 'gemini-3.1-pro-preview', description: 'Most intelligent, complex reasoning' },
168
+ { label: 'Gemini 3 Deep Think', value: 'gemini-3-deep-think', description: 'Heavy science & research' },
169
+ { label: 'Gemini 3 Flash', value: 'gemini-3-flash-preview', description: 'Fast, pro-level intelligence' },
170
+ { label: 'Gemini 3.1 Flash-Lite', value: 'gemini-3-1-flash-lite-preview', description: 'Ultra-efficient, high-volume' },
171
+ { label: 'Gemini 2.5 Pro', value: 'gemini-2.5-pro', description: 'Previous gen, still solid' },
172
+ { label: 'Gemini 2.5 Flash', value: 'gemini-2.5-flash', description: 'Previous gen, fast' },
173
+ { label: 'Custom', value: 'custom', description: 'Enter model name manually' },
174
+ ];
175
+ const isPreset = geminiModels.some(m => m.value === currentModel);
176
+ const modelChoice = await askChoice('Choose a model:', geminiModels, isPreset ? currentModel : 'custom');
177
+ const model = modelChoice === 'custom' ? await ask('Enter model name', currentModel) : modelChoice;
178
+ if (config.llm.gemini) config.llm.gemini.model = model;
179
+
180
+ } else if (provider === 'ollama') {
181
+ const url = await ask('Ollama base URL', config.llm.ollama?.base_url ?? 'http://localhost:11434');
182
+
183
+ const currentModel = config.llm.ollama?.model ?? 'llama3';
184
+ const ollamaModels = [
185
+ { label: 'Llama 3', value: 'llama3', description: 'General purpose' },
186
+ { label: 'Llama 3 70B', value: 'llama3:70b', description: 'Larger, more capable' },
187
+ { label: 'Mistral', value: 'mistral', description: 'Fast, good quality' },
188
+ { label: 'DeepSeek Coder', value: 'deepseek-coder', description: 'Code-focused' },
189
+ { label: 'CodeLlama', value: 'codellama', description: 'Code-focused' },
190
+ { label: 'Custom', value: 'custom', description: 'Enter model name manually' },
191
+ ];
192
+ const isPreset = ollamaModels.some(m => m.value === currentModel);
193
+ const modelChoice = await askChoice('Choose a model:', ollamaModels, isPreset ? currentModel : 'custom');
194
+ const model = modelChoice === 'custom' ? await ask('Enter model name', currentModel) : modelChoice;
195
+
196
+ config.llm.ollama = { base_url: url, model };
197
+ printInfo('Make sure Ollama is running: ollama serve');
198
+ }
199
+
200
+ // Test connectivity
201
+ const testConn = await askYesNo('Test LLM connectivity?', true);
202
+ if (testConn) {
203
+ const spin = startSpinner('Testing connection...');
204
+ try {
205
+ const { LLMManager, AnthropicProvider, OpenAIProvider, GeminiProvider, OllamaProvider } = await import('../llm/index.ts');
206
+ const manager = new LLMManager();
207
+
208
+ if (provider === 'anthropic' && config.llm.anthropic?.api_key) {
209
+ manager.registerProvider(new AnthropicProvider(config.llm.anthropic.api_key, config.llm.anthropic.model));
210
+ } else if (provider === 'openai' && config.llm.openai?.api_key) {
211
+ manager.registerProvider(new OpenAIProvider(config.llm.openai.api_key, config.llm.openai.model));
212
+ } else if (provider === 'gemini' && config.llm.gemini?.api_key) {
213
+ manager.registerProvider(new GeminiProvider(config.llm.gemini.api_key, config.llm.gemini.model));
214
+ } else if (provider === 'ollama') {
215
+ manager.registerProvider(new OllamaProvider(config.llm.ollama?.base_url, config.llm.ollama?.model));
216
+ }
217
+
218
+ manager.setPrimary(provider);
219
+ const resp = await Promise.race([
220
+ manager.chat(
221
+ [{ role: 'user', content: 'Say "JARVIS online" in 3 words.' }],
222
+ { max_tokens: 20 },
223
+ ),
224
+ new Promise<never>((_, reject) =>
225
+ setTimeout(() => reject(new Error('Connection timed out (15s)')), 15_000),
226
+ ),
227
+ ]);
228
+ spin.stop(`Connected! Model: ${resp.model}`);
229
+ } catch (err) {
230
+ spin.stop();
231
+ printErr(`Connection failed: ${err}`);
232
+ printInfo('Check your API key and try again later.');
233
+ }
234
+ }
235
+
236
+ // Fallback providers
237
+ config.llm.fallback = ['anthropic', 'openai', 'gemini', 'ollama'].filter(p => p !== provider);
238
+
239
+ // ── Step 3: Fallback API Keys ─────────────────────────────────────
240
+
241
+ printStep(3, TOTAL_STEPS, 'Fallback Providers');
242
+ console.log(' Optional: configure backup LLM providers.\n');
243
+
244
+ const setupFallbacks = await askYesNo('Configure fallback providers?', false);
245
+ if (setupFallbacks) {
246
+ for (const fb of config.llm.fallback) {
247
+ if (fb === 'anthropic' && (!config.llm.anthropic?.api_key || config.llm.anthropic.api_key === '')) {
248
+ const key = await askSecret('Anthropic API key (for fallback)');
249
+ if (key) config.llm.anthropic = { ...config.llm.anthropic, api_key: key, model: config.llm.anthropic?.model ?? 'claude-sonnet-4-6' };
250
+ } else if (fb === 'openai' && (!config.llm.openai?.api_key || config.llm.openai.api_key === '')) {
251
+ const key = await askSecret('OpenAI API key (for fallback)');
252
+ if (key) config.llm.openai = { ...config.llm.openai, api_key: key, model: config.llm.openai?.model ?? 'gpt-5.4' };
253
+ } else if (fb === 'gemini' && (!config.llm.gemini?.api_key || config.llm.gemini.api_key === '')) {
254
+ const key = await askSecret('Google AI API key (for fallback)');
255
+ if (key) config.llm.gemini = { ...config.llm.gemini, api_key: key, model: config.llm.gemini?.model ?? 'gemini-3-flash-preview' };
256
+ } else if (fb === 'ollama') {
257
+ const setupOllama = await askYesNo('Configure Ollama as fallback?', false);
258
+ if (setupOllama) {
259
+ const url = await ask('Ollama URL', 'http://localhost:11434');
260
+ const model = await ask('Ollama model', 'llama3');
261
+ config.llm.ollama = { base_url: url, model };
262
+ }
263
+ }
264
+ }
265
+ } else {
266
+ printInfo('Skipped. You can add fallback providers later in config.');
267
+ }
268
+
269
+ // ── Step 4: System Dependencies ─────────────────────────────────
270
+
271
+ printStep(4, TOTAL_STEPS, 'System Dependencies');
272
+ console.log(' Checking for optional system tools JARVIS can use.\n');
273
+
274
+ await runDependencyCheck(config);
275
+
276
+ // ── Step 5: TTS (Text-to-Speech) ─────────────────────────────────
277
+
278
+ printStep(5, TOTAL_STEPS, 'Voice Output (TTS)');
279
+ console.log(' JARVIS can speak responses aloud.\n');
280
+
281
+ const enableTTS = await askYesNo('Enable text-to-speech?', false);
282
+ config.tts = config.tts || { enabled: false };
283
+ config.tts.enabled = enableTTS;
284
+
285
+ if (enableTTS) {
286
+ const ttsProvider = await askChoice('TTS provider:', [
287
+ { label: 'Microsoft Edge TTS', value: 'edge' as const, description: 'Free, no API key needed' },
288
+ { label: 'ElevenLabs', value: 'elevenlabs' as const, description: 'Premium quality, API key required' },
289
+ ], config.tts.provider ?? 'edge');
290
+
291
+ config.tts.provider = ttsProvider;
292
+
293
+ if (ttsProvider === 'edge') {
294
+ const voice = await askChoice('Choose a voice:', [
295
+ { label: 'Aria (US Female)', value: 'en-US-AriaNeural' },
296
+ { label: 'Guy (US Male)', value: 'en-US-GuyNeural' },
297
+ { label: 'Sonia (UK Female)', value: 'en-GB-SoniaNeural' },
298
+ { label: 'Natasha (AU Female)', value: 'en-AU-NatashaNeural' },
299
+ { label: 'Jenny (US Female)', value: 'en-US-JennyNeural' },
300
+ { label: 'Davis (US Male)', value: 'en-US-DavisNeural' },
301
+ ], config.tts.voice ?? 'en-US-AriaNeural');
302
+ config.tts.voice = voice;
303
+ } else if (ttsProvider === 'elevenlabs') {
304
+ const existing = config.tts.elevenlabs?.api_key;
305
+ let apiKey: string;
306
+
307
+ if (existing) {
308
+ const keep = await askYesNo('ElevenLabs API key found. Keep it?', true);
309
+ apiKey = keep ? existing : await askSecret('ElevenLabs API key (from elevenlabs.io)');
310
+ } else {
311
+ apiKey = await askSecret('ElevenLabs API key (from elevenlabs.io)');
312
+ }
313
+
314
+ if (apiKey) {
315
+ config.tts.elevenlabs = {
316
+ ...config.tts.elevenlabs,
317
+ api_key: apiKey,
318
+ };
319
+
320
+ // Fetch available voices
321
+ const spin = startSpinner('Fetching available voices...');
322
+ try {
323
+ const { listElevenLabsVoices } = await import('../comms/voice.ts');
324
+ const voices = await Promise.race([
325
+ listElevenLabsVoices(apiKey),
326
+ new Promise<never>((_, reject) =>
327
+ setTimeout(() => reject(new Error('Timed out')), 10_000),
328
+ ),
329
+ ]);
330
+
331
+ spin.stop(`Found ${voices.length} voices`);
332
+
333
+ if (voices.length > 0) {
334
+ const voiceOptions = voices.slice(0, 10).map(v => ({
335
+ label: `${v.name} (${v.category})`,
336
+ value: v.voice_id,
337
+ }));
338
+ voiceOptions.push({ label: 'Custom', value: 'custom' });
339
+
340
+ const currentVoiceId = config.tts.elevenlabs.voice_id;
341
+ const isPreset = voiceOptions.some(v => v.value === currentVoiceId);
342
+ const voiceChoice = await askChoice(
343
+ 'Choose a voice:',
344
+ voiceOptions,
345
+ isPreset ? currentVoiceId : voiceOptions[0]!.value,
346
+ );
347
+
348
+ if (voiceChoice === 'custom') {
349
+ const customId = await ask('Enter voice ID', currentVoiceId ?? '');
350
+ config.tts.elevenlabs.voice_id = customId || undefined;
351
+ } else {
352
+ config.tts.elevenlabs.voice_id = voiceChoice;
353
+ }
354
+ }
355
+ } catch {
356
+ spin.stop();
357
+ printWarn('Could not fetch voices. Using default voice.');
358
+ const customId = await ask('Enter voice ID (optional)', config.tts.elevenlabs.voice_id ?? '');
359
+ if (customId) config.tts.elevenlabs.voice_id = customId;
360
+ }
361
+
362
+ // Model selection
363
+ const elModel = await askChoice('ElevenLabs model:', [
364
+ { label: 'Flash v2.5', value: 'eleven_flash_v2_5', description: 'Fast, low latency' },
365
+ { label: 'Multilingual v2', value: 'eleven_multilingual_v2', description: 'Higher quality' },
366
+ ], config.tts.elevenlabs.model ?? 'eleven_flash_v2_5');
367
+ config.tts.elevenlabs.model = elModel;
368
+ } else {
369
+ printWarn('No API key provided. Falling back to Edge TTS.');
370
+ config.tts.provider = 'edge';
371
+ config.tts.voice = 'en-US-AriaNeural';
372
+ }
373
+ }
374
+ } else {
375
+ printInfo('Skipped. Enable later in config.');
376
+ }
377
+
378
+ // ── Step 6: STT (Speech-to-Text) ─────────────────────────────────
379
+
380
+ printStep(6, TOTAL_STEPS, 'Voice Input (STT)');
381
+ console.log(' For voice commands via the dashboard microphone button.\n');
382
+
383
+ const setupSTT = await askYesNo('Configure speech-to-text?', false);
384
+ if (setupSTT) {
385
+ const sttProvider = await askChoice('STT provider:', [
386
+ { label: 'OpenAI Whisper', value: 'openai' as const, description: 'Best accuracy, uses OpenAI API key' },
387
+ { label: 'Groq Whisper', value: 'groq' as const, description: 'Fast, free tier available' },
388
+ { label: 'Local Whisper', value: 'local' as const, description: 'Self-hosted, fully private' },
389
+ ], config.stt?.provider as any ?? 'openai');
390
+
391
+ config.stt = { provider: sttProvider };
392
+
393
+ if (sttProvider === 'openai') {
394
+ // Reuse OpenAI API key if already set
395
+ if (config.llm.openai?.api_key) {
396
+ const reuse = await askYesNo('Reuse your OpenAI API key for STT?', true);
397
+ if (reuse) {
398
+ config.stt.openai = { api_key: config.llm.openai.api_key };
399
+ } else {
400
+ const key = await askSecret('OpenAI API key for STT');
401
+ if (key) config.stt.openai = { api_key: key };
402
+ }
403
+ } else {
404
+ const key = await askSecret('OpenAI API key for Whisper STT');
405
+ if (key) config.stt.openai = { api_key: key };
406
+ }
407
+ } else if (sttProvider === 'groq') {
408
+ const key = await askSecret('Groq API key (from console.groq.com)');
409
+ if (key) config.stt.groq = { api_key: key };
410
+ } else if (sttProvider === 'local') {
411
+ const endpoint = await ask('Local Whisper endpoint', 'http://localhost:8080');
412
+ config.stt.local = { endpoint };
413
+ }
414
+ } else {
415
+ printInfo('Skipped. Voice input will be disabled.');
416
+ }
417
+
418
+ // ── Step 7: Channels ──────────────────────────────────────────────
419
+
420
+ printStep(7, TOTAL_STEPS, 'Communication Channels');
421
+ console.log(' JARVIS can receive messages from Telegram and Discord.\n');
422
+
423
+ const setupChannels = await askYesNo('Configure messaging channels?', false);
424
+ if (setupChannels) {
425
+ // Telegram
426
+ const setupTG = await askYesNo('Set up Telegram?', false);
427
+ if (setupTG) {
428
+ const token = await askSecret('Telegram bot token (from @BotFather)');
429
+ if (token) {
430
+ const userId = await ask('Your Telegram user ID (numeric)');
431
+ config.channels = config.channels ?? {};
432
+ config.channels.telegram = {
433
+ enabled: true,
434
+ bot_token: token,
435
+ allowed_users: userId ? [parseInt(userId, 10)] : [],
436
+ };
437
+ printOk('Telegram configured.');
438
+ }
439
+ }
440
+
441
+ // Discord
442
+ const setupDC = await askYesNo('Set up Discord?', false);
443
+ if (setupDC) {
444
+ const token = await askSecret('Discord bot token (from discord.dev)');
445
+ if (token) {
446
+ const userId = await ask('Your Discord user ID');
447
+ config.channels = config.channels ?? {};
448
+ config.channels.discord = {
449
+ enabled: true,
450
+ bot_token: token,
451
+ allowed_users: userId ? [userId] : [],
452
+ };
453
+ printOk('Discord configured.');
454
+ }
455
+ }
456
+ } else {
457
+ printInfo('Skipped. Configure channels later for remote access.');
458
+ }
459
+
460
+ // ── Step 8: Personality ───────────────────────────────────────────
461
+
462
+ printStep(8, TOTAL_STEPS, 'Personality');
463
+ console.log(' Customize JARVIS\'s personality traits.\n');
464
+
465
+ const customPersonality = await askYesNo('Customize personality traits?', false);
466
+ if (customPersonality) {
467
+ console.log(c.dim(' Current traits: ' + config.personality.core_traits.join(', ')));
468
+ const traitsInput = await ask(
469
+ 'Enter traits (comma-separated)',
470
+ config.personality.core_traits.join(', ')
471
+ );
472
+ config.personality.core_traits = traitsInput.split(',').map(t => t.trim()).filter(Boolean);
473
+ printOk(`Traits: ${config.personality.core_traits.join(', ')}`);
474
+ } else {
475
+ printInfo(`Using defaults: ${config.personality.core_traits.join(', ')}`);
476
+ }
477
+
478
+ // ── Step 9: Authority Level ───────────────────────────────────────
479
+
480
+ printStep(9, TOTAL_STEPS, 'Authority & Safety');
481
+ console.log(' Controls what JARVIS can do autonomously.\n');
482
+ console.log(c.dim(' Level 1-3: Conservative (read-only, ask for everything)'));
483
+ console.log(c.dim(' Level 4-6: Moderate (browse, read/write files, run safe commands)'));
484
+ console.log(c.dim(' Level 7-10: Aggressive (full autonomy, sends emails, manages apps)'));
485
+ console.log('');
486
+
487
+ const customAuth = await askYesNo('Customize authority settings?', false);
488
+ if (customAuth) {
489
+ const levelStr = await ask('Default authority level (1-10)', String(config.authority.default_level));
490
+ const level = parseInt(levelStr, 10);
491
+ if (level >= 1 && level <= 10) {
492
+ config.authority.default_level = level;
493
+ }
494
+
495
+ // Governed categories
496
+ console.log(c.dim('\n Governed categories require your approval before executing:'));
497
+ console.log(c.dim(' Current: ' + config.authority.governed_categories.join(', ')));
498
+ printInfo('Keeping default governed categories (send_email, send_message, make_payment)');
499
+ } else {
500
+ printInfo(`Using defaults: level ${config.authority.default_level}, governed: ${config.authority.governed_categories.join(', ')}`);
501
+ }
502
+
503
+ // ── Step 10: Autostart ────────────────────────────────────────────
504
+
505
+ printStep(10, TOTAL_STEPS, 'Autostart');
506
+ const platform = detectPlatform();
507
+
508
+ if (platform === 'wsl') {
509
+ printInfo('WSL detected. Autostart is not supported in WSL.');
510
+ printInfo('Start JARVIS manually with: jarvis start');
511
+ } else {
512
+ console.log(` Autostart mechanism: ${c.bold(getAutostartName())}\n`);
513
+ const setupAutostart = await askYesNo('Start JARVIS automatically on login?', false);
514
+ if (setupAutostart) {
515
+ await installAutostart();
516
+ } else {
517
+ printInfo('Skipped. Start manually with: jarvis start');
518
+ }
519
+ }
520
+
521
+ // ── Port (quick inline question) ──────────────────────────────────
522
+
523
+ console.log('');
524
+ const changePort = await askYesNo(`Dashboard will run on port ${config.daemon.port}. Change it?`, false);
525
+ if (changePort) {
526
+ const portStr = await ask('Dashboard port', String(config.daemon.port));
527
+ const port = parseInt(portStr, 10);
528
+ if (port > 0 && port < 65536) config.daemon.port = port;
529
+ }
530
+
531
+ // ── Save ──────────────────────────────────────────────────────────
532
+
533
+ console.log('\n' + c.bold('─'.repeat(50)));
534
+ console.log(c.bold('\nConfiguration Summary:\n'));
535
+
536
+ const ttsLabel = !config.tts?.enabled ? 'disabled'
537
+ : config.tts.provider === 'elevenlabs' ? 'ElevenLabs'
538
+ : `${config.tts.voice} (Edge)`;
539
+
540
+ const summaryItems: [string, string][] = [
541
+ ['User', config.user?.name || c.dim('not set')],
542
+ ['Assistant', config.personality.assistant_name ?? 'Jarvis'],
543
+ ['LLM Provider', `${config.llm.primary} (${config.llm[config.llm.primary as keyof typeof config.llm] ? 'configured' : 'not set'})`],
544
+ ['Fallback', config.llm.fallback.join(' -> ')],
545
+ ['TTS', ttsLabel],
546
+ ['STT', config.stt?.provider ?? 'not configured'],
547
+ ['Telegram', config.channels?.telegram?.enabled ? 'enabled' : 'disabled'],
548
+ ['Discord', config.channels?.discord?.enabled ? 'enabled' : 'disabled'],
549
+ ['Authority', `level ${config.authority.default_level}`],
550
+ ['Port', String(config.daemon.port)],
551
+ ];
552
+
553
+ for (const [key, value] of summaryItems) {
554
+ console.log(` ${c.dim(key.padEnd(16))} ${value}`);
555
+ }
556
+
557
+ console.log('');
558
+
559
+ const doSave = await askYesNo('Save this configuration?', true);
560
+ if (doSave) {
561
+ await saveConfig(config);
562
+ printOk(`Config saved to ${CONFIG_PATH}`);
563
+ } else {
564
+ printWarn('Configuration not saved.');
565
+ }
566
+
567
+ // Offer to start daemon
568
+ console.log('');
569
+ const startNow = await askYesNo('Start JARVIS now?', true);
570
+ if (startNow) {
571
+ console.log(c.cyan('\nStarting J.A.R.V.I.S. daemon...\n'));
572
+ closeRL();
573
+
574
+ const { startDaemon } = await import('../daemon/index.ts');
575
+ await startDaemon();
576
+ } else {
577
+ console.log(c.dim('\nStart later with: jarvis start\n'));
578
+ closeRL();
579
+ }
580
+ }