nemoris 0.1.0 → 0.1.2

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 (248) hide show
  1. package/.env.example +49 -49
  2. package/LICENSE +21 -21
  3. package/README.md +209 -209
  4. package/SECURITY.md +59 -119
  5. package/bin/nemoris +46 -46
  6. package/config/agents/agent.toml.example +28 -28
  7. package/config/agents/content.toml +23 -0
  8. package/config/agents/default.toml +22 -22
  9. package/config/agents/heartbeat.toml +35 -0
  10. package/config/agents/iris.toml +23 -0
  11. package/config/agents/lab.toml +23 -0
  12. package/config/agents/main.toml +45 -0
  13. package/config/agents/nemo.toml +21 -0
  14. package/config/agents/ops.toml +38 -0
  15. package/config/agents/orchestrator.toml +18 -18
  16. package/config/agents/revenue.toml +23 -0
  17. package/config/agents/testyboo.toml +19 -0
  18. package/config/delivery.toml +73 -73
  19. package/config/embeddings.toml +5 -5
  20. package/config/identity/content-purpose.md +11 -0
  21. package/config/identity/content-soul.md +45 -0
  22. package/config/identity/default-purpose.md +1 -1
  23. package/config/identity/default-soul.md +3 -3
  24. package/config/identity/heartbeat-purpose.md +9 -0
  25. package/config/identity/heartbeat-soul.md +16 -0
  26. package/config/identity/iris-purpose.md +17 -0
  27. package/config/identity/iris-soul.md +68 -0
  28. package/config/identity/lab-purpose.md +10 -0
  29. package/config/identity/lab-soul.md +38 -0
  30. package/config/identity/main-purpose.md +17 -0
  31. package/config/identity/main-soul.md +66 -0
  32. package/config/identity/main-user.md +22 -0
  33. package/config/identity/ops-purpose.md +9 -0
  34. package/config/identity/ops-soul.md +16 -0
  35. package/config/identity/orchestrator-purpose.md +1 -1
  36. package/config/identity/orchestrator-soul.md +1 -1
  37. package/config/identity/revenue-purpose.md +9 -0
  38. package/config/identity/revenue-soul.md +41 -0
  39. package/config/identity/testyboo-purpose.md +13 -0
  40. package/config/identity/testyboo-soul.md +20 -0
  41. package/config/improvement-targets.toml +15 -15
  42. package/config/jobs/heartbeat-check.toml +30 -30
  43. package/config/jobs/memory-rollup.toml +46 -46
  44. package/config/jobs/workspace-health.toml +63 -63
  45. package/config/mcp.toml +16 -16
  46. package/config/output-contracts.toml +17 -17
  47. package/config/peers.toml +32 -32
  48. package/config/peers.toml.example +32 -32
  49. package/config/policies/memory-default.toml +10 -10
  50. package/config/policies/memory-heartbeat.toml +5 -5
  51. package/config/policies/memory-ops.toml +10 -10
  52. package/config/policies/tools-heartbeat-minimal.toml +8 -8
  53. package/config/policies/tools-interactive-safe.toml +8 -8
  54. package/config/policies/tools-ops-bounded.toml +8 -8
  55. package/config/policies/tools-orchestrator.toml +7 -7
  56. package/config/providers/anthropic.toml +15 -15
  57. package/config/providers/ollama.toml +5 -5
  58. package/config/providers/openai-codex.toml +9 -9
  59. package/config/providers/openrouter.toml +5 -5
  60. package/config/router.toml +22 -22
  61. package/config/runtime.toml +114 -114
  62. package/config/skills/self-improvement.toml +15 -15
  63. package/config/skills/telegram-onboarding-spec.md +240 -240
  64. package/config/skills/workspace-monitor.toml +15 -15
  65. package/config/task-router.toml +42 -42
  66. package/install.sh +50 -50
  67. package/package.json +91 -90
  68. package/src/auth/auth-profiles.js +169 -169
  69. package/src/auth/openai-codex-oauth.js +285 -285
  70. package/src/battle.js +449 -449
  71. package/src/cli/help.js +265 -265
  72. package/src/cli/output-filter.js +49 -49
  73. package/src/cli/runtime-control.js +704 -704
  74. package/src/cli-main.js +2763 -2763
  75. package/src/cli.js +78 -78
  76. package/src/config/loader.js +332 -332
  77. package/src/config/schema-validator.js +214 -214
  78. package/src/config/toml-lite.js +8 -8
  79. package/src/daemon/action-handlers.js +71 -71
  80. package/src/daemon/healing-tick.js +87 -87
  81. package/src/daemon/health-probes.js +90 -90
  82. package/src/daemon/notifier.js +57 -57
  83. package/src/daemon/nurse.js +218 -218
  84. package/src/daemon/repair-log.js +106 -106
  85. package/src/daemon/rule-staging.js +90 -90
  86. package/src/daemon/rules.js +29 -29
  87. package/src/daemon/telegram-commands.js +54 -54
  88. package/src/daemon/updater.js +85 -85
  89. package/src/jobs/job-runner.js +78 -78
  90. package/src/mcp/consumer.js +129 -129
  91. package/src/memory/active-recall.js +171 -171
  92. package/src/memory/backend-manager.js +97 -97
  93. package/src/memory/backends/file-backend.js +38 -38
  94. package/src/memory/backends/qmd-backend.js +219 -219
  95. package/src/memory/embedding-guards.js +24 -24
  96. package/src/memory/embedding-index.js +118 -118
  97. package/src/memory/embedding-service.js +179 -179
  98. package/src/memory/file-index.js +177 -177
  99. package/src/memory/memory-signature.js +5 -5
  100. package/src/memory/memory-store.js +648 -648
  101. package/src/memory/retrieval-planner.js +66 -66
  102. package/src/memory/scoring.js +145 -145
  103. package/src/memory/simhash.js +78 -78
  104. package/src/memory/sqlite-active-store.js +824 -824
  105. package/src/memory/write-policy.js +36 -36
  106. package/src/onboarding/aliases.js +33 -33
  107. package/src/onboarding/auth/api-key.js +224 -224
  108. package/src/onboarding/auth/ollama-detect.js +42 -42
  109. package/src/onboarding/clack-prompter.js +77 -77
  110. package/src/onboarding/doctor.js +530 -530
  111. package/src/onboarding/lock.js +42 -42
  112. package/src/onboarding/model-catalog.js +344 -344
  113. package/src/onboarding/phases/auth.js +576 -589
  114. package/src/onboarding/phases/build.js +130 -130
  115. package/src/onboarding/phases/choose.js +82 -82
  116. package/src/onboarding/phases/detect.js +98 -98
  117. package/src/onboarding/phases/hatch.js +216 -216
  118. package/src/onboarding/phases/identity.js +79 -79
  119. package/src/onboarding/phases/ollama.js +345 -345
  120. package/src/onboarding/phases/scaffold.js +99 -99
  121. package/src/onboarding/phases/telegram.js +377 -377
  122. package/src/onboarding/phases/validate.js +204 -204
  123. package/src/onboarding/phases/verify.js +206 -206
  124. package/src/onboarding/platform.js +482 -482
  125. package/src/onboarding/status-bar.js +95 -95
  126. package/src/onboarding/templates.js +794 -794
  127. package/src/onboarding/toml-writer.js +38 -38
  128. package/src/onboarding/tui.js +250 -250
  129. package/src/onboarding/uninstall.js +153 -153
  130. package/src/onboarding/wizard.js +516 -499
  131. package/src/providers/anthropic.js +168 -168
  132. package/src/providers/base.js +247 -247
  133. package/src/providers/circuit-breaker.js +136 -136
  134. package/src/providers/ollama.js +163 -163
  135. package/src/providers/openai-codex.js +149 -149
  136. package/src/providers/openrouter.js +136 -136
  137. package/src/providers/registry.js +36 -36
  138. package/src/providers/router.js +16 -16
  139. package/src/runtime/bootstrap-cache.js +47 -47
  140. package/src/runtime/capabilities-prompt.js +25 -25
  141. package/src/runtime/completion-ping.js +99 -99
  142. package/src/runtime/config-validator.js +121 -121
  143. package/src/runtime/context-ledger.js +360 -360
  144. package/src/runtime/cutover-readiness.js +42 -42
  145. package/src/runtime/daemon.js +729 -729
  146. package/src/runtime/delivery-ack.js +195 -195
  147. package/src/runtime/delivery-adapters/local-file.js +41 -41
  148. package/src/runtime/delivery-adapters/openclaw-cli.js +94 -94
  149. package/src/runtime/delivery-adapters/openclaw-peer.js +98 -98
  150. package/src/runtime/delivery-adapters/shadow.js +13 -13
  151. package/src/runtime/delivery-adapters/standalone-http.js +98 -98
  152. package/src/runtime/delivery-adapters/telegram.js +104 -104
  153. package/src/runtime/delivery-adapters/tui.js +128 -128
  154. package/src/runtime/delivery-manager.js +807 -807
  155. package/src/runtime/delivery-store.js +168 -168
  156. package/src/runtime/dependency-health.js +118 -118
  157. package/src/runtime/envelope.js +114 -114
  158. package/src/runtime/evaluation.js +1089 -1089
  159. package/src/runtime/exec-approvals.js +216 -216
  160. package/src/runtime/executor.js +500 -500
  161. package/src/runtime/failure-ping.js +67 -67
  162. package/src/runtime/flows.js +83 -83
  163. package/src/runtime/guards.js +45 -45
  164. package/src/runtime/handoff.js +51 -51
  165. package/src/runtime/identity-cache.js +28 -28
  166. package/src/runtime/improvement-engine.js +109 -109
  167. package/src/runtime/improvement-harness.js +581 -581
  168. package/src/runtime/input-sanitiser.js +72 -72
  169. package/src/runtime/interaction-contract.js +347 -347
  170. package/src/runtime/lane-readiness.js +226 -226
  171. package/src/runtime/migration.js +323 -323
  172. package/src/runtime/model-resolution.js +78 -78
  173. package/src/runtime/network.js +64 -64
  174. package/src/runtime/notification-store.js +97 -97
  175. package/src/runtime/notifier.js +256 -256
  176. package/src/runtime/orchestrator.js +53 -53
  177. package/src/runtime/orphan-reaper.js +41 -41
  178. package/src/runtime/output-contract-schema.js +139 -139
  179. package/src/runtime/output-contract-validator.js +439 -439
  180. package/src/runtime/peer-readiness.js +69 -69
  181. package/src/runtime/peer-registry.js +133 -133
  182. package/src/runtime/pilot-status.js +108 -108
  183. package/src/runtime/prompt-builder.js +261 -261
  184. package/src/runtime/provider-attempt.js +582 -582
  185. package/src/runtime/report-fallback.js +71 -71
  186. package/src/runtime/result-normalizer.js +183 -183
  187. package/src/runtime/retention.js +74 -74
  188. package/src/runtime/review.js +244 -244
  189. package/src/runtime/route-job.js +15 -15
  190. package/src/runtime/run-store.js +38 -38
  191. package/src/runtime/schedule.js +88 -88
  192. package/src/runtime/scheduler-state.js +434 -434
  193. package/src/runtime/scheduler.js +656 -656
  194. package/src/runtime/session-compactor.js +182 -182
  195. package/src/runtime/session-search.js +155 -155
  196. package/src/runtime/slack-inbound.js +249 -249
  197. package/src/runtime/ssrf.js +102 -102
  198. package/src/runtime/status-aggregator.js +330 -330
  199. package/src/runtime/task-contract.js +140 -140
  200. package/src/runtime/task-packet.js +107 -107
  201. package/src/runtime/task-router.js +140 -140
  202. package/src/runtime/telegram-inbound.js +1565 -1565
  203. package/src/runtime/token-counter.js +134 -134
  204. package/src/runtime/token-estimator.js +59 -59
  205. package/src/runtime/tool-loop.js +200 -200
  206. package/src/runtime/transport-server.js +311 -311
  207. package/src/runtime/tui-server.js +411 -411
  208. package/src/runtime/ulid.js +44 -44
  209. package/src/security/ssrf-check.js +197 -197
  210. package/src/setup.js +369 -369
  211. package/src/shadow/bridge.js +303 -303
  212. package/src/skills/loader.js +84 -84
  213. package/src/tools/catalog.json +49 -49
  214. package/src/tools/cli-delegate.js +44 -44
  215. package/src/tools/mcp-client.js +106 -106
  216. package/src/tools/micro/cancel-task.js +6 -6
  217. package/src/tools/micro/complete-task.js +6 -6
  218. package/src/tools/micro/fail-task.js +6 -6
  219. package/src/tools/micro/http-fetch.js +74 -74
  220. package/src/tools/micro/index.js +36 -36
  221. package/src/tools/micro/lcm-recall.js +60 -60
  222. package/src/tools/micro/list-dir.js +17 -17
  223. package/src/tools/micro/list-skills.js +46 -46
  224. package/src/tools/micro/load-skill.js +38 -38
  225. package/src/tools/micro/memory-search.js +45 -45
  226. package/src/tools/micro/read-file.js +11 -11
  227. package/src/tools/micro/session-search.js +54 -54
  228. package/src/tools/micro/shell-exec.js +43 -43
  229. package/src/tools/micro/trigger-job.js +79 -79
  230. package/src/tools/micro/web-search.js +58 -58
  231. package/src/tools/micro/workspace-paths.js +39 -39
  232. package/src/tools/micro/write-file.js +14 -14
  233. package/src/tools/micro/write-memory.js +41 -41
  234. package/src/tools/registry.js +348 -348
  235. package/src/tools/tool-result-contract.js +36 -36
  236. package/src/tui/chat.js +835 -835
  237. package/src/tui/renderer.js +175 -175
  238. package/src/tui/socket-client.js +217 -217
  239. package/src/utils/canonical-json.js +29 -29
  240. package/src/utils/compaction.js +30 -30
  241. package/src/utils/env-loader.js +5 -5
  242. package/src/utils/errors.js +80 -80
  243. package/src/utils/fs.js +101 -101
  244. package/src/utils/ids.js +5 -5
  245. package/src/utils/model-context-limits.js +30 -30
  246. package/src/utils/token-budget.js +74 -74
  247. package/src/utils/usage-cost.js +25 -25
  248. package/src/utils/usage-metrics.js +14 -14
@@ -1,499 +1,516 @@
1
- import fs from "node:fs";
2
- import os from "node:os";
3
- import path from "node:path";
4
- import { spawn } from "node:child_process";
5
- import { fileURLToPath } from "node:url";
6
- import { parseToml } from "../config/toml-lite.js";
7
- import { detect } from "./phases/detect.js";
8
- import { scaffold } from "./phases/scaffold.js";
9
- import { resolveDefaultAgentName, writeIdentity } from "./phases/identity.js";
10
- import { runAuthPhase } from "./phases/auth.js";
11
- import { runTelegramPhase } from "./phases/telegram.js";
12
- import { runOllamaPhase } from "./phases/ollama.js";
13
- import { validateScaffold } from "./phases/validate.js";
14
- import { choose } from "./phases/choose.js";
15
- import { buildFresh, buildShadow } from "./phases/build.js";
16
- import { verify } from "./phases/verify.js";
17
- import { createClackPrompter, SetupCancelledError } from "./clack-prompter.js";
18
- import { detectPreferredProvider, detectProviderOptions, summarizeSelectedModels } from "./model-catalog.js";
19
- import { isDaemonRunning, loadDaemon, writeDaemonUnit } from "./platform.js";
20
-
21
- const MIN_NODE_MAJOR = 22;
22
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
23
- const CLI_ENTRY = path.join(__dirname, "..", "cli.js");
24
- export const SETUP_RETRY_MESSAGE = "Fix: re-run `nemoris setup` to retry.";
25
-
26
- function checkNodeVersion() {
27
- const [major] = process.versions.node.split(".").map(Number);
28
- return major >= MIN_NODE_MAJOR;
29
- }
30
-
31
- function stripAnsi(value) {
32
- return String(value || "").replace(/\x1b\[[0-9;]*m/g, "");
33
- }
34
-
35
- function createLegacyPromptAdapter(prompter) {
36
- return {
37
- promptSecret: async (message) => prompter.password({ message }),
38
- prompt: async (message, initialValue = "") => prompter.text({ message, initialValue }),
39
- select: async (message, options) => prompter.select({
40
- message,
41
- options: options.map((option) => ({
42
- value: option.value,
43
- label: stripAnsi(option.label),
44
- hint: option.description ? stripAnsi(option.description) : undefined,
45
- })),
46
- }),
47
- confirm: async (message, initialValue = false) => prompter.confirm({ message, initialValue }),
48
- bold: (value) => value,
49
- dim: (value) => value,
50
- cyan: (value) => value,
51
- green: (value) => value,
52
- red: (value) => value,
53
- yellow: (value) => value,
54
- };
55
- }
56
-
57
- function readRuntimeConfig(installDir) {
58
- const runtimePath = path.join(installDir, "config", "runtime.toml");
59
- try {
60
- return parseToml(fs.readFileSync(runtimePath, "utf8"));
61
- } catch {
62
- return null;
63
- }
64
- }
65
-
66
- function summarizeExistingConfig(installDir) {
67
- const runtime = readRuntimeConfig(installDir);
68
- const providersDir = path.join(installDir, "config", "providers");
69
- const providers = fs.existsSync(providersDir)
70
- ? fs.readdirSync(providersDir).filter((file) => file.endsWith(".toml")).map((file) => path.basename(file, ".toml"))
71
- : [];
72
- const lines = [
73
- `Install dir: ${installDir}`,
74
- `Providers: ${providers.length > 0 ? providers.join(", ") : "none"}`,
75
- ];
76
- if (runtime?.telegram?.default_agent) {
77
- lines.push(`Telegram: ${runtime.telegram.default_agent}${runtime.telegram.operator_chat_id ? ` · chat ${runtime.telegram.operator_chat_id}` : ""}`);
78
- }
79
- if (runtime?.telegram?.polling_mode !== undefined) {
80
- lines.push(`Transport: ${runtime.telegram.polling_mode ? "long-poll" : "webhook"}`);
81
- }
82
- return lines.join("\n");
83
- }
84
-
85
- function resetInstallArtifacts(installDir, scope) {
86
- const targets = scope === "config"
87
- ? ["config"]
88
- : scope === "config+state"
89
- ? ["config", "state", ".env", "nemoris.lock"]
90
- : ["config", "state", ".env", "nemoris.lock", "workspace"];
91
-
92
- for (const target of targets) {
93
- fs.rmSync(path.join(installDir, target), { recursive: true, force: true });
94
- }
95
- }
96
-
97
- async function waitForDaemonHealthy({ timeoutMs = 15000 } = {}) {
98
- const deadline = Date.now() + timeoutMs;
99
- while (Date.now() < deadline) {
100
- if (isDaemonRunning()) {
101
- return true;
102
- }
103
- await new Promise((resolve) => setTimeout(resolve, 1000));
104
- }
105
- return isDaemonRunning();
106
- }
107
-
108
- function installShellCompletion() {
109
- const shell = path.basename(process.env.SHELL || "");
110
- const home = process.env.HOME || os.homedir();
111
- const commands = [
112
- "setup", "start", "stop", "restart", "status", "logs", "chat", "models",
113
- "doctor", "migrate", "mcp", "init", "telegram", "tools", "skills",
114
- "improvements", "uninstall", "run", "runs", "internal",
115
- ];
116
-
117
- if (shell === "zsh") {
118
- const zshrc = path.join(home, ".zshrc");
119
- const marker = "# nemoris completion";
120
- const snippet = `${marker}
121
- autoload -U compinit
122
- compinit
123
- compctl -k "(${commands.join(" ")})" nemoris
124
- `;
125
- const content = fs.existsSync(zshrc) ? fs.readFileSync(zshrc, "utf8") : "";
126
- if (!content.includes(marker)) {
127
- fs.appendFileSync(zshrc, `\n${snippet}`);
128
- }
129
- return true;
130
- }
131
-
132
- if (shell === "bash") {
133
- const bashrc = path.join(home, ".bashrc");
134
- const marker = "# nemoris completion";
135
- const snippet = `${marker}
136
- _nemoris_complete() {
137
- COMPREPLY=( $(compgen -W "${commands.join(" ")}" -- "\${COMP_WORDS[1]}") )
138
- }
139
- complete -F _nemoris_complete nemoris
140
- `;
141
- const content = fs.existsSync(bashrc) ? fs.readFileSync(bashrc, "utf8") : "";
142
- if (!content.includes(marker)) {
143
- fs.appendFileSync(bashrc, `\n${snippet}`);
144
- }
145
- return true;
146
- }
147
-
148
- return false;
149
- }
150
-
151
- async function launchChat() {
152
- await new Promise((resolve) => {
153
- const child = spawn(process.execPath, [CLI_ENTRY, "chat"], {
154
- stdio: "inherit",
155
- env: process.env,
156
- });
157
- child.once("exit", () => resolve());
158
- child.once("error", () => resolve());
159
- });
160
- }
161
-
162
- async function runInteractiveWizard({
163
- installDir,
164
- flowOverride = null,
165
- }) {
166
- const prompter = createClackPrompter();
167
- await prompter.intro("Nemoris setup");
168
-
169
- await prompter.note([
170
- "Security warning please read.",
171
- "",
172
- "Nemoris runs as a background daemon on your machine.",
173
- "It can execute shell commands, read files, and make network requests — always acting on your instructions, never autonomously.",
174
- "",
175
- "This is open-source software. Review the code at:",
176
- "https://github.com/amzer24/nemoris",
177
- "",
178
- "Run as a personal agent — one trusted operator boundary.",
179
- "Do not expose to the internet without hardening first.",
180
- ].join("\n"), "Security");
181
-
182
- const proceed = await prompter.confirm({
183
- message: "I understand. Continue?",
184
- initialValue: false,
185
- });
186
- if (!proceed) {
187
- await prompter.cancel("Cancelled.");
188
- return 0;
189
- }
190
-
191
- if (fs.existsSync(path.join(installDir, "config", "runtime.toml"))) {
192
- await prompter.note(summarizeExistingConfig(installDir), "Existing config detected");
193
- const action = await prompter.select({
194
- message: "Config handling",
195
- options: [
196
- { value: "keep", label: "Use existing values" },
197
- { value: "update", label: "Update values" },
198
- { value: "reset", label: "Reset everything" },
199
- ],
200
- });
201
-
202
- if (action === "keep") {
203
- await prompter.outro("Config unchanged. Run nemoris status to check what's running.");
204
- return 0;
205
- }
206
-
207
- if (action === "reset") {
208
- const scope = await prompter.select({
209
- message: "Reset scope",
210
- options: [
211
- { value: "config", label: "Config only" },
212
- { value: "config+state", label: "Config + state (memory, runs, scheduler)" },
213
- { value: "full", label: "Full reset (everything)" },
214
- ],
215
- });
216
- resetInstallArtifacts(installDir, scope);
217
- }
218
- }
219
-
220
- const flow = flowOverride || await prompter.select({
221
- message: "Setup mode",
222
- options: [
223
- { value: "quickstart", label: "QuickStart", hint: "Running in 2 minutes, configure details later with: nemoris setup" },
224
- { value: "manual", label: "Manual", hint: "Full configuration now" },
225
- ],
226
- initialValue: "quickstart",
227
- });
228
-
229
- if (flow === "quickstart") {
230
- await prompter.note([
231
- "QuickStart defaults:",
232
- `State directory: ${installDir}`,
233
- "Polling: long-poll (no tunnel needed)",
234
- "Daemon: auto-installed",
235
- ].join("\n"), "QuickStart");
236
- }
237
-
238
- if (process.platform === "win32") {
239
- await prompter.note([
240
- "Windows detected Nemoris runs best on WSL2.",
241
- "Native Windows support is experimental.",
242
- "Quick setup: wsl --install (one command, one reboot)",
243
- "Guide: https://github.com/amzer24/nemoris#windows",
244
- ].join("\n"), "Windows");
245
- }
246
-
247
- const detection = await detect(installDir);
248
- await scaffold({ installDir });
249
-
250
- const userName = await prompter.text({
251
- message: "Your name",
252
- initialValue: process.env.NEMORIS_USER_NAME || process.env.USER || "",
253
- placeholder: os.userInfo().username || "",
254
- });
255
- const defaultAgentName = process.env.NEMORIS_AGENT_NAME || resolveDefaultAgentName();
256
- const agentName = await prompter.text({
257
- message: "What should your agent be called?",
258
- initialValue: defaultAgentName,
259
- validate: (value) => String(value || "").trim() ? undefined : "Agent name is required.",
260
- });
261
- const agentId = String(agentName).toLowerCase().replace(/[^a-z0-9-]/g, "-");
262
- writeIdentity({
263
- installDir,
264
- userName: userName || process.env.USER || "operator",
265
- agentName,
266
- agentId,
267
- userGoal: "build software",
268
- });
269
-
270
- const provider = await prompter.select({
271
- message: "Choose your AI provider",
272
- options: detectProviderOptions({
273
- env: process.env,
274
- ollamaResult: detection.ollama ? { ok: true, models: detection.ollama.models } : { ok: false, models: [] },
275
- }).map((option) => ({
276
- value: option.value,
277
- label: option.label,
278
- hint: option.hint,
279
- })),
280
- initialValue: detectPreferredProvider({
281
- env: process.env,
282
- ollamaResult: detection.ollama ? { ok: true } : { ok: false },
283
- }),
284
- });
285
-
286
- let authResult = { providers: [], providerFlags: {}, selectedModels: {} };
287
- let telegramResult = { configured: false, verified: false };
288
- let ollamaResult = { configured: false, verified: false, models: [] };
289
-
290
- if (provider !== "skip" && provider !== "ollama") {
291
- const authSpin = prompter.spinner();
292
- authSpin.start("Configuring provider auth...");
293
- authResult = await runAuthPhase(installDir, {
294
- tui: createLegacyPromptAdapter(prompter),
295
- detectionCache: {
296
- rawKeys: detection.apiKeys,
297
- ollamaResult: detection.ollama ? { ok: true, models: detection.ollama.models } : { ok: false, models: [] },
298
- },
299
- providerOrder: [provider],
300
- enableOpenAIOAuthChoice: provider === "openai",
301
- });
302
- authSpin.stop("Provider configured");
303
- }
304
-
305
- const connectTelegram = await prompter.confirm({
306
- message: "Connect via Telegram?",
307
- initialValue: true,
308
- });
309
- if (connectTelegram) {
310
- telegramResult = await runTelegramPhase({ installDir, agentId, nonInteractive: false, skipGate: true });
311
- }
312
-
313
- const useOllama = provider === "ollama" || await prompter.confirm({
314
- message: "Add Ollama local models too?",
315
- initialValue: flow === "quickstart",
316
- });
317
- if (useOllama) {
318
- ollamaResult = await runOllamaPhase({ installDir, nonInteractive: false });
319
- }
320
-
321
- const installDaemon = flow === "quickstart" || await prompter.confirm({
322
- message: "Install background service (recommended)",
323
- initialValue: true,
324
- });
325
- let daemonHealthy = false;
326
-
327
- if (installDaemon) {
328
- const spin = prompter.spinner();
329
- spin.start("Installing daemon...");
330
- await writeDaemonUnit(installDir);
331
- const loaded = await loadDaemon(installDir);
332
- spin.stop(loaded.ok ? "Daemon installed" : `Daemon install failed: ${loaded.message || "check logs with nemoris logs"}`);
333
- daemonHealthy = loaded.ok ? await waitForDaemonHealthy({ timeoutMs: 15000 }) : false;
334
- }
335
-
336
- if (!daemonHealthy) {
337
- await prompter.note("Run `nemoris start` to start it manually.\nDocs: https://github.com/amzer24/nemoris#troubleshooting", "Daemon");
338
- }
339
-
340
- if (daemonHealthy) {
341
- const hatch = await prompter.select({
342
- message: "How do you want to start?",
343
- options: [
344
- { value: "chat", label: "Open TUI chat (recommended)" },
345
- { value: "later", label: "Do this later" },
346
- ],
347
- initialValue: "chat",
348
- });
349
- if (hatch === "chat") {
350
- await launchChat();
351
- }
352
- }
353
-
354
- const installCompletion = flow === "quickstart" || await prompter.confirm({
355
- message: "Enable shell completion?",
356
- initialValue: true,
357
- });
358
- if (installCompletion) {
359
- installShellCompletion();
360
- }
361
-
362
- const modelSummary = summarizeSelectedModels([
363
- ...(authResult.selectedModels?.[provider] || []),
364
- ...(ollamaResult.models || []).map((model) => `ollama/${model}`),
365
- ]);
366
- const authMethod = provider === "ollama"
367
- ? "local"
368
- : authResult.providers.length > 0
369
- ? "api_key"
370
- : "skipped";
371
-
372
- await prompter.note([
373
- "Auth overview:",
374
- ` ${provider} ${provider === "skip" ? "skip" : "✓"} ${authMethod}`,
375
- "",
376
- "Models:",
377
- ` Default : ${modelSummary.defaultModel || "not configured"}`,
378
- ` Fallback : ${modelSummary.fallbackModel || "not configured"}`,
379
- "",
380
- "Docs: https://github.com/amzer24/nemoris",
381
- "Issues: https://github.com/amzer24/nemoris/issues",
382
- ].join("\n"), "Ready");
383
-
384
- await prompter.outro(
385
- telegramResult.verified
386
- ? "Nemoris is running. Message your bot to get started."
387
- : "Nemoris is running. Start with: nemoris chat"
388
- );
389
- return 0;
390
- }
391
-
392
- async function runNonInteractiveWizard({
393
- installDir,
394
- acceptRisk = false,
395
- flow = "quickstart",
396
- anthropicKey = null,
397
- openaiKey = null,
398
- openrouterKey = null,
399
- }) {
400
- if (!acceptRisk) {
401
- console.error("Non-interactive setup requires --accept-risk.");
402
- return 1;
403
- }
404
-
405
- const detectionCache = await detect(installDir);
406
- detectionCache.apiKeys = {
407
- ...detectionCache.apiKeys,
408
- anthropic: anthropicKey || detectionCache.apiKeys?.anthropic || null,
409
- openai: openaiKey || detectionCache.apiKeys?.openai || null,
410
- openrouter: openrouterKey || detectionCache.apiKeys?.openrouter || null,
411
- };
412
-
413
- const { mode } = await choose({ detectionCache, nonInteractive: true });
414
- const userName = process.env.NEMORIS_USER_NAME || process.env.USER || "operator";
415
-
416
- let buildResult;
417
- if (mode === "shadow") {
418
- buildResult = await buildShadow({
419
- installDir,
420
- userName,
421
- detectionCache,
422
- nonInteractive: true,
423
- });
424
- } else {
425
- const requestedAgentName = String(process.env.NEMORIS_AGENT_NAME || "").trim() || "Nemo";
426
- buildResult = await buildFresh({
427
- installDir,
428
- agentName: requestedAgentName,
429
- userName,
430
- userGoal: "build software",
431
- detectionCache,
432
- nonInteractive: true,
433
- });
434
- }
435
-
436
- const result = await verify({
437
- installDir,
438
- agentName: buildResult.agentName,
439
- userName: buildResult.userName,
440
- agentId: buildResult.agentId,
441
- mode,
442
- providers: buildResult.providers || [],
443
- providerFlags: buildResult.providerFlags || {},
444
- nonInteractive: true,
445
- skipHealthcheck: process.env.NEMORIS_SKIP_HEALTHCHECK === "true",
446
- telegramConfigured: buildResult.telegramConfigured,
447
- telegramVerified: buildResult.telegramVerified,
448
- telegramBotUsername: buildResult.telegramBotUsername,
449
- telegramBotToken: buildResult.botToken,
450
- telegramOperatorChatId: buildResult.operatorChatId,
451
- userGoal: buildResult.userGoal,
452
- });
453
-
454
- if (result.status === "warning") {
455
- return 1;
456
- }
457
- if (flow !== "quickstart" && buildResult.ollamaConfigured) {
458
- // Keep manual mode deterministic in CI while preserving quickstart defaults.
459
- return 0;
460
- }
461
- return 0;
462
- }
463
-
464
- export async function runWizard({
465
- installDir,
466
- nonInteractive = false,
467
- acceptRisk = false,
468
- flow = null,
469
- anthropicKey = null,
470
- openaiKey = null,
471
- openrouterKey = null,
472
- }) {
473
- if (!checkNodeVersion()) {
474
- console.error(`Node.js v${MIN_NODE_MAJOR}+ is required.`);
475
- return 1;
476
- }
477
-
478
- try {
479
- return nonInteractive
480
- ? await runNonInteractiveWizard({
481
- installDir,
482
- acceptRisk,
483
- flow: flow || "quickstart",
484
- anthropicKey,
485
- openaiKey,
486
- openrouterKey,
487
- })
488
- : await runInteractiveWizard({
489
- installDir,
490
- flowOverride: flow,
491
- });
492
- } catch (error) {
493
- if (error instanceof SetupCancelledError) {
494
- return 0;
495
- }
496
- console.error(`Setup failed: ${error.message}`);
497
- return 1;
498
- }
499
- }
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { spawn } from "node:child_process";
5
+ import { fileURLToPath } from "node:url";
6
+ import { parseToml } from "../config/toml-lite.js";
7
+ import { detect } from "./phases/detect.js";
8
+ import { scaffold } from "./phases/scaffold.js";
9
+ import { resolveDefaultAgentName, writeIdentity } from "./phases/identity.js";
10
+ import { runAuthPhase } from "./phases/auth.js";
11
+ import { runTelegramPhase } from "./phases/telegram.js";
12
+ import { runOllamaPhase } from "./phases/ollama.js";
13
+ import { validateScaffold } from "./phases/validate.js";
14
+ import { choose } from "./phases/choose.js";
15
+ import { buildFresh, buildShadow } from "./phases/build.js";
16
+ import { verify } from "./phases/verify.js";
17
+ import { createClackPrompter, SetupCancelledError } from "./clack-prompter.js";
18
+ import { detectPreferredProvider, detectProviderOptions, summarizeSelectedModels } from "./model-catalog.js";
19
+ import { isDaemonRunning, loadDaemon, writeDaemonUnit } from "./platform.js";
20
+
21
+ const MIN_NODE_MAJOR = 22;
22
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
23
+ const CLI_ENTRY = path.join(__dirname, "..", "cli.js");
24
+ export const SETUP_RETRY_MESSAGE = "Fix: re-run `nemoris setup` to retry.";
25
+
26
+ function checkNodeVersion() {
27
+ const [major] = process.versions.node.split(".").map(Number);
28
+ return major >= MIN_NODE_MAJOR;
29
+ }
30
+
31
+ function stripAnsi(value) {
32
+ return String(value || "").replace(/\x1b\[[0-9;]*m/g, "");
33
+ }
34
+
35
+ function createLegacyPromptAdapter(prompter) {
36
+ return {
37
+ promptSecret: async (message) => prompter.password({ message }),
38
+ prompt: async (message, initialValue = "") => prompter.text({ message, initialValue }),
39
+ select: async (message, options) => prompter.select({
40
+ message,
41
+ options: options.map((option) => ({
42
+ value: option.value,
43
+ label: stripAnsi(option.label),
44
+ hint: option.description ? stripAnsi(option.description) : undefined,
45
+ })),
46
+ }),
47
+ confirm: async (message, initialValue = false) => prompter.confirm({ message, initialValue }),
48
+ bold: (value) => value,
49
+ dim: (value) => value,
50
+ cyan: (value) => value,
51
+ green: (value) => value,
52
+ red: (value) => value,
53
+ yellow: (value) => value,
54
+ };
55
+ }
56
+
57
+ function readRuntimeConfig(installDir) {
58
+ const runtimePath = path.join(installDir, "config", "runtime.toml");
59
+ try {
60
+ return parseToml(fs.readFileSync(runtimePath, "utf8"));
61
+ } catch {
62
+ return null;
63
+ }
64
+ }
65
+
66
+ function summarizeExistingConfig(installDir) {
67
+ const runtime = readRuntimeConfig(installDir);
68
+ const providersDir = path.join(installDir, "config", "providers");
69
+ const providers = fs.existsSync(providersDir)
70
+ ? fs.readdirSync(providersDir).filter((file) => file.endsWith(".toml")).map((file) => path.basename(file, ".toml"))
71
+ : [];
72
+ const lines = [
73
+ `Install dir: ${installDir}`,
74
+ `Providers: ${providers.length > 0 ? providers.join(", ") : "none"}`,
75
+ ];
76
+ if (runtime?.telegram?.default_agent) {
77
+ lines.push(`Telegram: ${runtime.telegram.default_agent}${runtime.telegram.operator_chat_id ? ` · chat ${runtime.telegram.operator_chat_id}` : ""}`);
78
+ }
79
+ if (runtime?.telegram?.polling_mode !== undefined) {
80
+ lines.push(`Transport: ${runtime.telegram.polling_mode ? "long-poll" : "webhook"}`);
81
+ }
82
+ return lines.join("\n");
83
+ }
84
+
85
+ function resetInstallArtifacts(installDir, scope) {
86
+ const targets = scope === "config"
87
+ ? ["config"]
88
+ : scope === "config+state"
89
+ ? ["config", "state", ".env", "nemoris.lock"]
90
+ : ["config", "state", ".env", "nemoris.lock", "workspace"];
91
+
92
+ for (const target of targets) {
93
+ fs.rmSync(path.join(installDir, target), { recursive: true, force: true });
94
+ }
95
+ }
96
+
97
+ async function waitForDaemonHealthy({ timeoutMs = 15000 } = {}) {
98
+ const deadline = Date.now() + timeoutMs;
99
+ while (Date.now() < deadline) {
100
+ if (isDaemonRunning()) {
101
+ return true;
102
+ }
103
+ await new Promise((resolve) => setTimeout(resolve, 1000));
104
+ }
105
+ return isDaemonRunning();
106
+ }
107
+
108
+ function installShellCompletion() {
109
+ const shell = path.basename(process.env.SHELL || "");
110
+ const home = process.env.HOME || os.homedir();
111
+ const commands = [
112
+ "setup", "start", "stop", "restart", "status", "logs", "chat", "models",
113
+ "doctor", "migrate", "mcp", "init", "telegram", "tools", "skills",
114
+ "improvements", "uninstall", "run", "runs", "internal",
115
+ ];
116
+
117
+ if (shell === "zsh") {
118
+ const zshrc = path.join(home, ".zshrc");
119
+ const marker = "# nemoris completion";
120
+ const snippet = `${marker}
121
+ autoload -U compinit
122
+ compinit
123
+ compctl -k "(${commands.join(" ")})" nemoris
124
+ `;
125
+ const content = fs.existsSync(zshrc) ? fs.readFileSync(zshrc, "utf8") : "";
126
+ if (!content.includes(marker)) {
127
+ fs.appendFileSync(zshrc, `\n${snippet}`);
128
+ }
129
+ return true;
130
+ }
131
+
132
+ if (shell === "bash") {
133
+ const bashrc = path.join(home, ".bashrc");
134
+ const marker = "# nemoris completion";
135
+ const snippet = `${marker}
136
+ _nemoris_complete() {
137
+ COMPREPLY=( $(compgen -W "${commands.join(" ")}" -- "\${COMP_WORDS[1]}") )
138
+ }
139
+ complete -F _nemoris_complete nemoris
140
+ `;
141
+ const content = fs.existsSync(bashrc) ? fs.readFileSync(bashrc, "utf8") : "";
142
+ if (!content.includes(marker)) {
143
+ fs.appendFileSync(bashrc, `\n${snippet}`);
144
+ }
145
+ return true;
146
+ }
147
+
148
+ return false;
149
+ }
150
+
151
+ async function launchChat() {
152
+ await new Promise((resolve) => {
153
+ const child = spawn(process.execPath, [CLI_ENTRY, "chat"], {
154
+ stdio: "inherit",
155
+ env: process.env,
156
+ });
157
+ child.once("exit", () => resolve());
158
+ child.once("error", () => resolve());
159
+ });
160
+ }
161
+
162
+ async function runInteractiveWizard({
163
+ installDir,
164
+ flowOverride = null,
165
+ }) {
166
+ const prompter = createClackPrompter();
167
+
168
+ // ASCII banner — ANSI Shadow font, brand accent colour
169
+ const BRAND = "\x1b[38;2;45;212;191m";
170
+ const RESET = "\x1b[0m";
171
+ const DIM = "\x1b[2m";
172
+ const ascii = [
173
+ "",
174
+ `${BRAND} ███╗ ██╗███████╗███╗ ███╗ ██████╗ ██████╗ ██╗███████╗${RESET}`,
175
+ `${BRAND} ████╗ ██║██╔════╝████╗ ████║██╔═══██╗██╔══██╗██║██╔════╝${RESET}`,
176
+ `${BRAND} ██╔██╗ ██║█████╗ ██╔████╔██║██║ ██║██████╔╝██║███████╗${RESET}`,
177
+ `${BRAND} ██║╚██╗██║██╔══╝ ██║╚██╔╝██║██║ ██║██╔══██╗██║╚════██║${RESET}`,
178
+ `${BRAND} ██║ ╚████║███████╗██║ ╚═╝ ██║╚██████╔╝██║ ██║██║███████║${RESET}`,
179
+ `${BRAND} ╚═╝ ╚═══╝╚══════╝╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚═╝╚══════╝${RESET}`,
180
+ "",
181
+ ].join("\n");
182
+ console.log(ascii);
183
+
184
+ await prompter.intro("Nemoris setup");
185
+
186
+ await prompter.note([
187
+ "Security warning — please read.",
188
+ "",
189
+ "Nemoris runs as a background daemon on your machine.",
190
+ "It can execute shell commands, read files, and make network requests — always acting on your instructions, never autonomously.",
191
+ "",
192
+ "This is open-source software. Review the code at:",
193
+ "https://github.com/amzer24/nemoris",
194
+ "",
195
+ "Run as a personal agent — one trusted operator boundary.",
196
+ "Do not expose to the internet without hardening first.",
197
+ ].join("\n"), "Security");
198
+
199
+ const proceed = await prompter.confirm({
200
+ message: "I understand. Continue?",
201
+ initialValue: false,
202
+ });
203
+ if (!proceed) {
204
+ await prompter.cancel("Cancelled.");
205
+ return 0;
206
+ }
207
+
208
+ if (fs.existsSync(path.join(installDir, "config", "runtime.toml"))) {
209
+ await prompter.note(summarizeExistingConfig(installDir), "Existing config detected");
210
+ const action = await prompter.select({
211
+ message: "Config handling",
212
+ options: [
213
+ { value: "keep", label: "Use existing values", hint: "Skip setup — keep current config and start running" },
214
+ { value: "update", label: "Update values", hint: "Walk through setup again, pre-filled with current config" },
215
+ { value: "reset", label: "Reset everything", hint: "Wipe and start fresh" },
216
+ ],
217
+ });
218
+
219
+ if (action === "keep") {
220
+ await prompter.outro("Config unchanged. Run nemoris status to check what's running.");
221
+ return 0;
222
+ }
223
+
224
+ if (action === "reset") {
225
+ const scope = await prompter.select({
226
+ message: "Reset scope",
227
+ options: [
228
+ { value: "config", label: "Config only", hint: "Rewrites agents, router, and provider config. Keeps memory and history." },
229
+ { value: "config+state", label: "Config + state", hint: "Config + wipes memory, run history, and scheduler data. Agent identities kept." },
230
+ { value: "full", label: "Full reset", hint: "Deletes everything and starts completely fresh. Like a first install." },
231
+ ],
232
+ });
233
+ resetInstallArtifacts(installDir, scope);
234
+ }
235
+ }
236
+
237
+ const flow = flowOverride || await prompter.select({
238
+ message: "Setup mode",
239
+ options: [
240
+ { value: "quickstart", label: "QuickStart", hint: "Running in 2 minutes, configure details later with: nemoris setup" },
241
+ { value: "manual", label: "Manual", hint: "Full configuration now" },
242
+ ],
243
+ initialValue: "quickstart",
244
+ });
245
+
246
+ if (flow === "quickstart") {
247
+ await prompter.note([
248
+ "QuickStart defaults:",
249
+ `State directory: ${installDir}`,
250
+ "Polling: long-poll (no tunnel needed)",
251
+ "Daemon: auto-installed",
252
+ ].join("\n"), "QuickStart");
253
+ }
254
+
255
+ if (process.platform === "win32") {
256
+ await prompter.note([
257
+ "Windows detected Nemoris runs best on WSL2.",
258
+ "Native Windows support is experimental.",
259
+ "Quick setup: wsl --install (one command, one reboot)",
260
+ "Guide: https://github.com/amzer24/nemoris#windows",
261
+ ].join("\n"), "Windows");
262
+ }
263
+
264
+ const detection = await detect(installDir);
265
+ await scaffold({ installDir });
266
+
267
+ const userName = await prompter.text({
268
+ message: "Your name",
269
+ initialValue: process.env.NEMORIS_USER_NAME || process.env.USER || "",
270
+ placeholder: os.userInfo().username || "",
271
+ });
272
+ const defaultAgentName = process.env.NEMORIS_AGENT_NAME || resolveDefaultAgentName();
273
+ const agentName = await prompter.text({
274
+ message: "What should your agent be called?",
275
+ initialValue: defaultAgentName,
276
+ validate: (value) => String(value || "").trim() ? undefined : "Agent name is required.",
277
+ });
278
+ const agentId = String(agentName).toLowerCase().replace(/[^a-z0-9-]/g, "-");
279
+ writeIdentity({
280
+ installDir,
281
+ userName: userName || process.env.USER || "operator",
282
+ agentName,
283
+ agentId,
284
+ userGoal: "build software",
285
+ });
286
+
287
+ const provider = await prompter.select({
288
+ message: "Choose your AI provider",
289
+ options: detectProviderOptions({
290
+ env: process.env,
291
+ ollamaResult: detection.ollama ? { ok: true, models: detection.ollama.models } : { ok: false, models: [] },
292
+ }).map((option) => ({
293
+ value: option.value,
294
+ label: option.label,
295
+ hint: option.hint,
296
+ })),
297
+ initialValue: detectPreferredProvider({
298
+ env: process.env,
299
+ ollamaResult: detection.ollama ? { ok: true } : { ok: false },
300
+ }),
301
+ });
302
+
303
+ let authResult = { providers: [], providerFlags: {}, selectedModels: {} };
304
+ let telegramResult = { configured: false, verified: false };
305
+ let ollamaResult = { configured: false, verified: false, models: [] };
306
+
307
+ if (provider !== "skip" && provider !== "ollama") {
308
+ const authSpin = prompter.spinner();
309
+ authSpin.start("Configuring provider auth...");
310
+ authResult = await runAuthPhase(installDir, {
311
+ tui: createLegacyPromptAdapter(prompter),
312
+ detectionCache: {
313
+ rawKeys: detection.apiKeys,
314
+ ollamaResult: detection.ollama ? { ok: true, models: detection.ollama.models } : { ok: false, models: [] },
315
+ },
316
+ providerOrder: [provider],
317
+ enableOpenAIOAuthChoice: provider === "openai",
318
+ });
319
+ authSpin.stop("Provider configured");
320
+ }
321
+
322
+ const connectTelegram = await prompter.confirm({
323
+ message: "Connect via Telegram?",
324
+ initialValue: true,
325
+ });
326
+ if (connectTelegram) {
327
+ telegramResult = await runTelegramPhase({ installDir, agentId, nonInteractive: false, skipGate: true });
328
+ }
329
+
330
+ const useOllama = provider === "ollama" || await prompter.confirm({
331
+ message: "Add Ollama local models too?",
332
+ initialValue: flow === "quickstart",
333
+ });
334
+ if (useOllama) {
335
+ ollamaResult = await runOllamaPhase({ installDir, nonInteractive: false });
336
+ }
337
+
338
+ const installDaemon = flow === "quickstart" || await prompter.confirm({
339
+ message: "Install background service (recommended)",
340
+ initialValue: true,
341
+ });
342
+ let daemonHealthy = false;
343
+
344
+ if (installDaemon) {
345
+ const spin = prompter.spinner();
346
+ spin.start("Installing daemon...");
347
+ await writeDaemonUnit(installDir);
348
+ const loaded = await loadDaemon(installDir);
349
+ spin.stop(loaded.ok ? "Daemon installed" : `Daemon install failed: ${loaded.message || "check logs with nemoris logs"}`);
350
+ daemonHealthy = loaded.ok ? await waitForDaemonHealthy({ timeoutMs: 15000 }) : false;
351
+ }
352
+
353
+ if (!daemonHealthy) {
354
+ await prompter.note("Run `nemoris start` to start it manually.\nDocs: https://github.com/amzer24/nemoris#troubleshooting", "Daemon");
355
+ }
356
+
357
+ if (daemonHealthy) {
358
+ const hatch = await prompter.select({
359
+ message: "How do you want to start?",
360
+ options: [
361
+ { value: "chat", label: "Open TUI chat (recommended)" },
362
+ { value: "later", label: "Do this later" },
363
+ ],
364
+ initialValue: "chat",
365
+ });
366
+ if (hatch === "chat") {
367
+ await launchChat();
368
+ }
369
+ }
370
+
371
+ const installCompletion = flow === "quickstart" || await prompter.confirm({
372
+ message: "Enable shell completion?",
373
+ initialValue: true,
374
+ });
375
+ if (installCompletion) {
376
+ installShellCompletion();
377
+ }
378
+
379
+ const modelSummary = summarizeSelectedModels([
380
+ ...(authResult.selectedModels?.[provider] || []),
381
+ ...(ollamaResult.models || []).map((model) => `ollama/${model}`),
382
+ ]);
383
+ const authMethod = provider === "ollama"
384
+ ? "local"
385
+ : authResult.providers.length > 0
386
+ ? "api_key"
387
+ : "skipped";
388
+
389
+ await prompter.note([
390
+ "Auth overview:",
391
+ ` ${provider} ${provider === "skip" ? "skip" : "✓"} ${authMethod}`,
392
+ "",
393
+ "Models:",
394
+ ` Default : ${modelSummary.defaultModel || "not configured"}`,
395
+ ` Fallback : ${modelSummary.fallbackModel || "not configured"}`,
396
+ "",
397
+ "Docs: https://github.com/amzer24/nemoris",
398
+ "Issues: https://github.com/amzer24/nemoris/issues",
399
+ ].join("\n"), "Ready");
400
+
401
+ await prompter.outro(
402
+ telegramResult.verified
403
+ ? "Nemoris is running. Message your bot to get started."
404
+ : "Nemoris is running. Start with: nemoris chat"
405
+ );
406
+ return 0;
407
+ }
408
+
409
+ async function runNonInteractiveWizard({
410
+ installDir,
411
+ acceptRisk = false,
412
+ flow = "quickstart",
413
+ anthropicKey = null,
414
+ openaiKey = null,
415
+ openrouterKey = null,
416
+ }) {
417
+ if (!acceptRisk) {
418
+ console.error("Non-interactive setup requires --accept-risk.");
419
+ return 1;
420
+ }
421
+
422
+ const detectionCache = await detect(installDir);
423
+ detectionCache.apiKeys = {
424
+ ...detectionCache.apiKeys,
425
+ anthropic: anthropicKey || detectionCache.apiKeys?.anthropic || null,
426
+ openai: openaiKey || detectionCache.apiKeys?.openai || null,
427
+ openrouter: openrouterKey || detectionCache.apiKeys?.openrouter || null,
428
+ };
429
+
430
+ const { mode } = await choose({ detectionCache, nonInteractive: true });
431
+ const userName = process.env.NEMORIS_USER_NAME || process.env.USER || "operator";
432
+
433
+ let buildResult;
434
+ if (mode === "shadow") {
435
+ buildResult = await buildShadow({
436
+ installDir,
437
+ userName,
438
+ detectionCache,
439
+ nonInteractive: true,
440
+ });
441
+ } else {
442
+ const requestedAgentName = String(process.env.NEMORIS_AGENT_NAME || "").trim() || "Nemo";
443
+ buildResult = await buildFresh({
444
+ installDir,
445
+ agentName: requestedAgentName,
446
+ userName,
447
+ userGoal: "build software",
448
+ detectionCache,
449
+ nonInteractive: true,
450
+ });
451
+ }
452
+
453
+ const result = await verify({
454
+ installDir,
455
+ agentName: buildResult.agentName,
456
+ userName: buildResult.userName,
457
+ agentId: buildResult.agentId,
458
+ mode,
459
+ providers: buildResult.providers || [],
460
+ providerFlags: buildResult.providerFlags || {},
461
+ nonInteractive: true,
462
+ skipHealthcheck: process.env.NEMORIS_SKIP_HEALTHCHECK === "true",
463
+ telegramConfigured: buildResult.telegramConfigured,
464
+ telegramVerified: buildResult.telegramVerified,
465
+ telegramBotUsername: buildResult.telegramBotUsername,
466
+ telegramBotToken: buildResult.botToken,
467
+ telegramOperatorChatId: buildResult.operatorChatId,
468
+ userGoal: buildResult.userGoal,
469
+ });
470
+
471
+ if (result.status === "warning") {
472
+ return 1;
473
+ }
474
+ if (flow !== "quickstart" && buildResult.ollamaConfigured) {
475
+ // Keep manual mode deterministic in CI while preserving quickstart defaults.
476
+ return 0;
477
+ }
478
+ return 0;
479
+ }
480
+
481
+ export async function runWizard({
482
+ installDir,
483
+ nonInteractive = false,
484
+ acceptRisk = false,
485
+ flow = null,
486
+ anthropicKey = null,
487
+ openaiKey = null,
488
+ openrouterKey = null,
489
+ }) {
490
+ if (!checkNodeVersion()) {
491
+ console.error(`Node.js v${MIN_NODE_MAJOR}+ is required.`);
492
+ return 1;
493
+ }
494
+
495
+ try {
496
+ return nonInteractive
497
+ ? await runNonInteractiveWizard({
498
+ installDir,
499
+ acceptRisk,
500
+ flow: flow || "quickstart",
501
+ anthropicKey,
502
+ openaiKey,
503
+ openrouterKey,
504
+ })
505
+ : await runInteractiveWizard({
506
+ installDir,
507
+ flowOverride: flow,
508
+ });
509
+ } catch (error) {
510
+ if (error instanceof SetupCancelledError) {
511
+ return 0;
512
+ }
513
+ console.error(`Setup failed: ${error.message}`);
514
+ return 1;
515
+ }
516
+ }