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.
- package/.env.example +49 -49
- package/LICENSE +21 -21
- package/README.md +209 -209
- package/SECURITY.md +59 -119
- package/bin/nemoris +46 -46
- package/config/agents/agent.toml.example +28 -28
- package/config/agents/content.toml +23 -0
- package/config/agents/default.toml +22 -22
- package/config/agents/heartbeat.toml +35 -0
- package/config/agents/iris.toml +23 -0
- package/config/agents/lab.toml +23 -0
- package/config/agents/main.toml +45 -0
- package/config/agents/nemo.toml +21 -0
- package/config/agents/ops.toml +38 -0
- package/config/agents/orchestrator.toml +18 -18
- package/config/agents/revenue.toml +23 -0
- package/config/agents/testyboo.toml +19 -0
- package/config/delivery.toml +73 -73
- package/config/embeddings.toml +5 -5
- package/config/identity/content-purpose.md +11 -0
- package/config/identity/content-soul.md +45 -0
- package/config/identity/default-purpose.md +1 -1
- package/config/identity/default-soul.md +3 -3
- package/config/identity/heartbeat-purpose.md +9 -0
- package/config/identity/heartbeat-soul.md +16 -0
- package/config/identity/iris-purpose.md +17 -0
- package/config/identity/iris-soul.md +68 -0
- package/config/identity/lab-purpose.md +10 -0
- package/config/identity/lab-soul.md +38 -0
- package/config/identity/main-purpose.md +17 -0
- package/config/identity/main-soul.md +66 -0
- package/config/identity/main-user.md +22 -0
- package/config/identity/ops-purpose.md +9 -0
- package/config/identity/ops-soul.md +16 -0
- package/config/identity/orchestrator-purpose.md +1 -1
- package/config/identity/orchestrator-soul.md +1 -1
- package/config/identity/revenue-purpose.md +9 -0
- package/config/identity/revenue-soul.md +41 -0
- package/config/identity/testyboo-purpose.md +13 -0
- package/config/identity/testyboo-soul.md +20 -0
- package/config/improvement-targets.toml +15 -15
- package/config/jobs/heartbeat-check.toml +30 -30
- package/config/jobs/memory-rollup.toml +46 -46
- package/config/jobs/workspace-health.toml +63 -63
- package/config/mcp.toml +16 -16
- package/config/output-contracts.toml +17 -17
- package/config/peers.toml +32 -32
- package/config/peers.toml.example +32 -32
- package/config/policies/memory-default.toml +10 -10
- package/config/policies/memory-heartbeat.toml +5 -5
- package/config/policies/memory-ops.toml +10 -10
- package/config/policies/tools-heartbeat-minimal.toml +8 -8
- package/config/policies/tools-interactive-safe.toml +8 -8
- package/config/policies/tools-ops-bounded.toml +8 -8
- package/config/policies/tools-orchestrator.toml +7 -7
- package/config/providers/anthropic.toml +15 -15
- package/config/providers/ollama.toml +5 -5
- package/config/providers/openai-codex.toml +9 -9
- package/config/providers/openrouter.toml +5 -5
- package/config/router.toml +22 -22
- package/config/runtime.toml +114 -114
- package/config/skills/self-improvement.toml +15 -15
- package/config/skills/telegram-onboarding-spec.md +240 -240
- package/config/skills/workspace-monitor.toml +15 -15
- package/config/task-router.toml +42 -42
- package/install.sh +50 -50
- package/package.json +91 -90
- package/src/auth/auth-profiles.js +169 -169
- package/src/auth/openai-codex-oauth.js +285 -285
- package/src/battle.js +449 -449
- package/src/cli/help.js +265 -265
- package/src/cli/output-filter.js +49 -49
- package/src/cli/runtime-control.js +704 -704
- package/src/cli-main.js +2763 -2763
- package/src/cli.js +78 -78
- package/src/config/loader.js +332 -332
- package/src/config/schema-validator.js +214 -214
- package/src/config/toml-lite.js +8 -8
- package/src/daemon/action-handlers.js +71 -71
- package/src/daemon/healing-tick.js +87 -87
- package/src/daemon/health-probes.js +90 -90
- package/src/daemon/notifier.js +57 -57
- package/src/daemon/nurse.js +218 -218
- package/src/daemon/repair-log.js +106 -106
- package/src/daemon/rule-staging.js +90 -90
- package/src/daemon/rules.js +29 -29
- package/src/daemon/telegram-commands.js +54 -54
- package/src/daemon/updater.js +85 -85
- package/src/jobs/job-runner.js +78 -78
- package/src/mcp/consumer.js +129 -129
- package/src/memory/active-recall.js +171 -171
- package/src/memory/backend-manager.js +97 -97
- package/src/memory/backends/file-backend.js +38 -38
- package/src/memory/backends/qmd-backend.js +219 -219
- package/src/memory/embedding-guards.js +24 -24
- package/src/memory/embedding-index.js +118 -118
- package/src/memory/embedding-service.js +179 -179
- package/src/memory/file-index.js +177 -177
- package/src/memory/memory-signature.js +5 -5
- package/src/memory/memory-store.js +648 -648
- package/src/memory/retrieval-planner.js +66 -66
- package/src/memory/scoring.js +145 -145
- package/src/memory/simhash.js +78 -78
- package/src/memory/sqlite-active-store.js +824 -824
- package/src/memory/write-policy.js +36 -36
- package/src/onboarding/aliases.js +33 -33
- package/src/onboarding/auth/api-key.js +224 -224
- package/src/onboarding/auth/ollama-detect.js +42 -42
- package/src/onboarding/clack-prompter.js +77 -77
- package/src/onboarding/doctor.js +530 -530
- package/src/onboarding/lock.js +42 -42
- package/src/onboarding/model-catalog.js +344 -344
- package/src/onboarding/phases/auth.js +576 -589
- package/src/onboarding/phases/build.js +130 -130
- package/src/onboarding/phases/choose.js +82 -82
- package/src/onboarding/phases/detect.js +98 -98
- package/src/onboarding/phases/hatch.js +216 -216
- package/src/onboarding/phases/identity.js +79 -79
- package/src/onboarding/phases/ollama.js +345 -345
- package/src/onboarding/phases/scaffold.js +99 -99
- package/src/onboarding/phases/telegram.js +377 -377
- package/src/onboarding/phases/validate.js +204 -204
- package/src/onboarding/phases/verify.js +206 -206
- package/src/onboarding/platform.js +482 -482
- package/src/onboarding/status-bar.js +95 -95
- package/src/onboarding/templates.js +794 -794
- package/src/onboarding/toml-writer.js +38 -38
- package/src/onboarding/tui.js +250 -250
- package/src/onboarding/uninstall.js +153 -153
- package/src/onboarding/wizard.js +516 -499
- package/src/providers/anthropic.js +168 -168
- package/src/providers/base.js +247 -247
- package/src/providers/circuit-breaker.js +136 -136
- package/src/providers/ollama.js +163 -163
- package/src/providers/openai-codex.js +149 -149
- package/src/providers/openrouter.js +136 -136
- package/src/providers/registry.js +36 -36
- package/src/providers/router.js +16 -16
- package/src/runtime/bootstrap-cache.js +47 -47
- package/src/runtime/capabilities-prompt.js +25 -25
- package/src/runtime/completion-ping.js +99 -99
- package/src/runtime/config-validator.js +121 -121
- package/src/runtime/context-ledger.js +360 -360
- package/src/runtime/cutover-readiness.js +42 -42
- package/src/runtime/daemon.js +729 -729
- package/src/runtime/delivery-ack.js +195 -195
- package/src/runtime/delivery-adapters/local-file.js +41 -41
- package/src/runtime/delivery-adapters/openclaw-cli.js +94 -94
- package/src/runtime/delivery-adapters/openclaw-peer.js +98 -98
- package/src/runtime/delivery-adapters/shadow.js +13 -13
- package/src/runtime/delivery-adapters/standalone-http.js +98 -98
- package/src/runtime/delivery-adapters/telegram.js +104 -104
- package/src/runtime/delivery-adapters/tui.js +128 -128
- package/src/runtime/delivery-manager.js +807 -807
- package/src/runtime/delivery-store.js +168 -168
- package/src/runtime/dependency-health.js +118 -118
- package/src/runtime/envelope.js +114 -114
- package/src/runtime/evaluation.js +1089 -1089
- package/src/runtime/exec-approvals.js +216 -216
- package/src/runtime/executor.js +500 -500
- package/src/runtime/failure-ping.js +67 -67
- package/src/runtime/flows.js +83 -83
- package/src/runtime/guards.js +45 -45
- package/src/runtime/handoff.js +51 -51
- package/src/runtime/identity-cache.js +28 -28
- package/src/runtime/improvement-engine.js +109 -109
- package/src/runtime/improvement-harness.js +581 -581
- package/src/runtime/input-sanitiser.js +72 -72
- package/src/runtime/interaction-contract.js +347 -347
- package/src/runtime/lane-readiness.js +226 -226
- package/src/runtime/migration.js +323 -323
- package/src/runtime/model-resolution.js +78 -78
- package/src/runtime/network.js +64 -64
- package/src/runtime/notification-store.js +97 -97
- package/src/runtime/notifier.js +256 -256
- package/src/runtime/orchestrator.js +53 -53
- package/src/runtime/orphan-reaper.js +41 -41
- package/src/runtime/output-contract-schema.js +139 -139
- package/src/runtime/output-contract-validator.js +439 -439
- package/src/runtime/peer-readiness.js +69 -69
- package/src/runtime/peer-registry.js +133 -133
- package/src/runtime/pilot-status.js +108 -108
- package/src/runtime/prompt-builder.js +261 -261
- package/src/runtime/provider-attempt.js +582 -582
- package/src/runtime/report-fallback.js +71 -71
- package/src/runtime/result-normalizer.js +183 -183
- package/src/runtime/retention.js +74 -74
- package/src/runtime/review.js +244 -244
- package/src/runtime/route-job.js +15 -15
- package/src/runtime/run-store.js +38 -38
- package/src/runtime/schedule.js +88 -88
- package/src/runtime/scheduler-state.js +434 -434
- package/src/runtime/scheduler.js +656 -656
- package/src/runtime/session-compactor.js +182 -182
- package/src/runtime/session-search.js +155 -155
- package/src/runtime/slack-inbound.js +249 -249
- package/src/runtime/ssrf.js +102 -102
- package/src/runtime/status-aggregator.js +330 -330
- package/src/runtime/task-contract.js +140 -140
- package/src/runtime/task-packet.js +107 -107
- package/src/runtime/task-router.js +140 -140
- package/src/runtime/telegram-inbound.js +1565 -1565
- package/src/runtime/token-counter.js +134 -134
- package/src/runtime/token-estimator.js +59 -59
- package/src/runtime/tool-loop.js +200 -200
- package/src/runtime/transport-server.js +311 -311
- package/src/runtime/tui-server.js +411 -411
- package/src/runtime/ulid.js +44 -44
- package/src/security/ssrf-check.js +197 -197
- package/src/setup.js +369 -369
- package/src/shadow/bridge.js +303 -303
- package/src/skills/loader.js +84 -84
- package/src/tools/catalog.json +49 -49
- package/src/tools/cli-delegate.js +44 -44
- package/src/tools/mcp-client.js +106 -106
- package/src/tools/micro/cancel-task.js +6 -6
- package/src/tools/micro/complete-task.js +6 -6
- package/src/tools/micro/fail-task.js +6 -6
- package/src/tools/micro/http-fetch.js +74 -74
- package/src/tools/micro/index.js +36 -36
- package/src/tools/micro/lcm-recall.js +60 -60
- package/src/tools/micro/list-dir.js +17 -17
- package/src/tools/micro/list-skills.js +46 -46
- package/src/tools/micro/load-skill.js +38 -38
- package/src/tools/micro/memory-search.js +45 -45
- package/src/tools/micro/read-file.js +11 -11
- package/src/tools/micro/session-search.js +54 -54
- package/src/tools/micro/shell-exec.js +43 -43
- package/src/tools/micro/trigger-job.js +79 -79
- package/src/tools/micro/web-search.js +58 -58
- package/src/tools/micro/workspace-paths.js +39 -39
- package/src/tools/micro/write-file.js +14 -14
- package/src/tools/micro/write-memory.js +41 -41
- package/src/tools/registry.js +348 -348
- package/src/tools/tool-result-contract.js +36 -36
- package/src/tui/chat.js +835 -835
- package/src/tui/renderer.js +175 -175
- package/src/tui/socket-client.js +217 -217
- package/src/utils/canonical-json.js +29 -29
- package/src/utils/compaction.js +30 -30
- package/src/utils/env-loader.js +5 -5
- package/src/utils/errors.js +80 -80
- package/src/utils/fs.js +101 -101
- package/src/utils/ids.js +5 -5
- package/src/utils/model-context-limits.js +30 -30
- package/src/utils/token-budget.js +74 -74
- package/src/utils/usage-cost.js +25 -25
- package/src/utils/usage-metrics.js +14 -14
package/src/onboarding/wizard.js
CHANGED
|
@@ -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
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
"
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
"
|
|
241
|
-
"
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
]
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
? "
|
|
387
|
-
: "
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
if (
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
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
|
+
}
|