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/setup.js
CHANGED
|
@@ -1,369 +1,369 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* First-run setup script for Nemoris V2.
|
|
3
|
-
*
|
|
4
|
-
* Validates the environment, detects API keys, checks external services,
|
|
5
|
-
* validates config, and ensures state directories exist.
|
|
6
|
-
*
|
|
7
|
-
* No npm dependencies — uses native fetch, native fs, ANSI codes only.
|
|
8
|
-
* Idempotent and non-interactive (no prompts).
|
|
9
|
-
* Exit code 0 if all critical checks pass, 1 otherwise.
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import fs from "node:fs";
|
|
13
|
-
import os from "node:os";
|
|
14
|
-
import path from "node:path";
|
|
15
|
-
import { fileURLToPath } from "node:url";
|
|
16
|
-
|
|
17
|
-
// ── ANSI helpers ────────────────────────────────────────────────────
|
|
18
|
-
|
|
19
|
-
const BOLD = "\x1b[1m";
|
|
20
|
-
const RESET = "\x1b[0m";
|
|
21
|
-
const GREEN = "\x1b[32m";
|
|
22
|
-
const RED = "\x1b[31m";
|
|
23
|
-
const YELLOW = "\x1b[33m";
|
|
24
|
-
const DIM = "\x1b[2m";
|
|
25
|
-
|
|
26
|
-
const PASS = `${GREEN}\u2713${RESET}`;
|
|
27
|
-
const FAIL = `${RED}\u2717${RESET}`;
|
|
28
|
-
const WARN = `${YELLOW}!${RESET}`;
|
|
29
|
-
|
|
30
|
-
function bold(text) {
|
|
31
|
-
return `${BOLD}${text}${RESET}`;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
// ── Path resolution ─────────────────────────────────────────────────
|
|
35
|
-
|
|
36
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
37
|
-
const projectRoot = path.resolve(__dirname, "..");
|
|
38
|
-
const configRoot = path.join(projectRoot, "config");
|
|
39
|
-
const stateRoot = path.join(projectRoot, "state", "memory");
|
|
40
|
-
|
|
41
|
-
// ── Env loader (mirrors cli.js) ─────────────────────────────────────
|
|
42
|
-
|
|
43
|
-
function loadParentEnv(envPath) {
|
|
44
|
-
try {
|
|
45
|
-
const content = fs.readFileSync(envPath, "utf8");
|
|
46
|
-
for (const line of content.split("\n")) {
|
|
47
|
-
const trimmed = line.trim();
|
|
48
|
-
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
49
|
-
const eqIdx = trimmed.indexOf("=");
|
|
50
|
-
if (eqIdx < 1) continue;
|
|
51
|
-
const key = trimmed.slice(0, eqIdx).trim();
|
|
52
|
-
if (process.env[key] !== undefined) continue;
|
|
53
|
-
let value = trimmed.slice(eqIdx + 1).trim();
|
|
54
|
-
if (
|
|
55
|
-
(value.startsWith('"') && value.endsWith('"')) ||
|
|
56
|
-
(value.startsWith("'") && value.endsWith("'"))
|
|
57
|
-
) {
|
|
58
|
-
value = value.slice(1, -1);
|
|
59
|
-
}
|
|
60
|
-
process.env[key] = value;
|
|
61
|
-
}
|
|
62
|
-
} catch {
|
|
63
|
-
// .env file is optional
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// ── Helpers ─────────────────────────────────────────────────────────
|
|
68
|
-
|
|
69
|
-
function maskKey(key) {
|
|
70
|
-
if (!key || key.length < 7) return "***";
|
|
71
|
-
return key.slice(0, 6) + "...";
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
function readPackageJson() {
|
|
75
|
-
const raw = fs.readFileSync(path.join(projectRoot, "package.json"), "utf8");
|
|
76
|
-
return JSON.parse(raw);
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// ── Step 1: Welcome banner ──────────────────────────────────────────
|
|
80
|
-
|
|
81
|
-
function printBanner(pkg) {
|
|
82
|
-
console.log("");
|
|
83
|
-
console.log(
|
|
84
|
-
bold("── Nemoris V2 Setup ") +
|
|
85
|
-
DIM +
|
|
86
|
-
`v${pkg.version}` +
|
|
87
|
-
RESET +
|
|
88
|
-
bold(" ─────────────────────")
|
|
89
|
-
);
|
|
90
|
-
console.log("");
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
// ── Step 2: Node.js version check ───────────────────────────────────
|
|
94
|
-
|
|
95
|
-
function checkNodeVersion() {
|
|
96
|
-
const full = process.version; // e.g. "v25.8.0"
|
|
97
|
-
const major = parseInt(full.slice(1), 10);
|
|
98
|
-
const ok = major >= 22;
|
|
99
|
-
if (ok) {
|
|
100
|
-
console.log(` Node.js ${full} ${PASS}`);
|
|
101
|
-
} else {
|
|
102
|
-
console.log(
|
|
103
|
-
` Node.js ${full} ${FAIL} (need >= 22.5)`
|
|
104
|
-
);
|
|
105
|
-
}
|
|
106
|
-
return ok;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
// ── Step 3: Detect API keys ─────────────────────────────────────────
|
|
110
|
-
|
|
111
|
-
function detectApiKeys() {
|
|
112
|
-
// Load parent .env first
|
|
113
|
-
const parentEnvPath = path.resolve(
|
|
114
|
-
process.env.HOME || os.homedir(),
|
|
115
|
-
".openclaw",
|
|
116
|
-
".env"
|
|
117
|
-
);
|
|
118
|
-
loadParentEnv(parentEnvPath);
|
|
119
|
-
|
|
120
|
-
const keys = {};
|
|
121
|
-
|
|
122
|
-
// OpenRouter (primary for battle-testing)
|
|
123
|
-
const orKey = process.env.OPENROUTER_API_KEY;
|
|
124
|
-
if (orKey) {
|
|
125
|
-
console.log(` OpenRouter ${maskKey(orKey)} ${PASS} (primary)`);
|
|
126
|
-
keys.openrouter = orKey;
|
|
127
|
-
} else {
|
|
128
|
-
console.log(` OpenRouter not found ${FAIL}`);
|
|
129
|
-
keys.openrouter = null;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
// Anthropic (optional — routes through OpenRouter when unavailable)
|
|
133
|
-
const antKey = process.env.NEMORIS_ANTHROPIC_API_KEY;
|
|
134
|
-
if (antKey) {
|
|
135
|
-
console.log(` Anthropic ${maskKey(antKey)} ${PASS}`);
|
|
136
|
-
keys.anthropic = antKey;
|
|
137
|
-
} else {
|
|
138
|
-
console.log(` Anthropic not found ${WARN} (optional)`);
|
|
139
|
-
keys.anthropic = null;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
return keys;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
// ── Step 4: Validate OpenRouter API key ─────────────────────────────
|
|
146
|
-
|
|
147
|
-
async function validateOpenRouterKey(apiKey) {
|
|
148
|
-
if (!apiKey) return false;
|
|
149
|
-
|
|
150
|
-
try {
|
|
151
|
-
const res = await fetch("https://openrouter.ai/api/v1/models", {
|
|
152
|
-
headers: {
|
|
153
|
-
"Authorization": `Bearer ${apiKey}`,
|
|
154
|
-
},
|
|
155
|
-
signal: AbortSignal.timeout(10000),
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
if (res.ok) {
|
|
159
|
-
console.log(` API check verified ${PASS}`);
|
|
160
|
-
return true;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
console.log(` API check invalid ${FAIL} (HTTP ${res.status})`);
|
|
164
|
-
return false;
|
|
165
|
-
} catch (err) {
|
|
166
|
-
console.log(` API check error ${FAIL} (${err.message})`);
|
|
167
|
-
return false;
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
// ── Step 5: Check Ollama ────────────────────────────────────────────
|
|
172
|
-
|
|
173
|
-
async function checkOllama() {
|
|
174
|
-
try {
|
|
175
|
-
const res = await fetch("http://localhost:11434/api/tags", {
|
|
176
|
-
signal: AbortSignal.timeout(5000),
|
|
177
|
-
});
|
|
178
|
-
if (!res.ok) {
|
|
179
|
-
console.log(
|
|
180
|
-
` Ollama unreachable ${WARN} (HTTP ${res.status})`
|
|
181
|
-
);
|
|
182
|
-
return { ok: false, modelCount: 0 };
|
|
183
|
-
}
|
|
184
|
-
const data = await res.json();
|
|
185
|
-
const models = data.models || [];
|
|
186
|
-
if (models.length === 0) {
|
|
187
|
-
console.log(` Ollama 0 models ${WARN} (no models pulled)`);
|
|
188
|
-
} else {
|
|
189
|
-
console.log(
|
|
190
|
-
` Ollama ${models.length} model${models.length === 1 ? "" : "s"}${models.length < 10 ? " " : " "} ${PASS}`
|
|
191
|
-
);
|
|
192
|
-
for (const m of models) {
|
|
193
|
-
console.log(`${DIM} - ${m.name}${RESET}`);
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
return { ok: true, modelCount: models.length };
|
|
197
|
-
} catch {
|
|
198
|
-
console.log(
|
|
199
|
-
` Ollama not running ${WARN} (remote providers still work)`
|
|
200
|
-
);
|
|
201
|
-
return { ok: false, modelCount: 0 };
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
// ── Step 6: Validate config ─────────────────────────────────────────
|
|
206
|
-
|
|
207
|
-
async function checkConfig() {
|
|
208
|
-
try {
|
|
209
|
-
const { ConfigLoader } = await import("./config/loader.js");
|
|
210
|
-
const { validateAllConfigs } = await import(
|
|
211
|
-
"./config/schema-validator.js"
|
|
212
|
-
);
|
|
213
|
-
|
|
214
|
-
const loader = new ConfigLoader({ rootDir: configRoot });
|
|
215
|
-
const config = await loader.loadAll();
|
|
216
|
-
const result = validateAllConfigs(config, configRoot);
|
|
217
|
-
|
|
218
|
-
if (result.ok) {
|
|
219
|
-
console.log(` Config all valid ${PASS}`);
|
|
220
|
-
} else {
|
|
221
|
-
console.log(
|
|
222
|
-
` Config ${result.errors.length} error${result.errors.length === 1 ? "" : "s"} ${FAIL}`
|
|
223
|
-
);
|
|
224
|
-
for (const e of result.errors) {
|
|
225
|
-
console.log(`${DIM} - ${e}${RESET}`);
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
return result.ok;
|
|
229
|
-
} catch (err) {
|
|
230
|
-
console.log(
|
|
231
|
-
` Config load error ${FAIL} (${err.message})`
|
|
232
|
-
);
|
|
233
|
-
return false;
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
// ── Step 7: Ensure state directories ────────────────────────────────
|
|
238
|
-
|
|
239
|
-
async function ensureStateDirs() {
|
|
240
|
-
try {
|
|
241
|
-
// Read agent configs to know which dirs are needed
|
|
242
|
-
const agentsDir = path.join(configRoot, "agents");
|
|
243
|
-
const agentFiles = fs.readdirSync(agentsDir).filter((f) => f.endsWith(".toml"));
|
|
244
|
-
const agentIds = agentFiles.map((f) => path.basename(f, ".toml"));
|
|
245
|
-
|
|
246
|
-
let created = 0;
|
|
247
|
-
let _existed = 0;
|
|
248
|
-
|
|
249
|
-
for (const id of agentIds) {
|
|
250
|
-
const dir = path.join(stateRoot, id);
|
|
251
|
-
if (fs.existsSync(dir)) {
|
|
252
|
-
_existed++;
|
|
253
|
-
} else {
|
|
254
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
255
|
-
created++;
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
if (created > 0) {
|
|
260
|
-
console.log(
|
|
261
|
-
` State dirs ${created} created ${PASS}`
|
|
262
|
-
);
|
|
263
|
-
} else {
|
|
264
|
-
console.log(
|
|
265
|
-
` State dirs all present ${PASS}`
|
|
266
|
-
);
|
|
267
|
-
}
|
|
268
|
-
return true;
|
|
269
|
-
} catch (err) {
|
|
270
|
-
console.log(
|
|
271
|
-
` State dirs error ${FAIL} (${err.message})`
|
|
272
|
-
);
|
|
273
|
-
return false;
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
// ── Summary ─────────────────────────────────────────────────────────
|
|
278
|
-
|
|
279
|
-
function printSummary(results) {
|
|
280
|
-
console.log("");
|
|
281
|
-
console.log(bold("── Setup Complete ") + bold("─".repeat(30)));
|
|
282
|
-
|
|
283
|
-
const nodeLabel = results.nodeVersion
|
|
284
|
-
? `${process.version}`
|
|
285
|
-
: process.version;
|
|
286
|
-
console.log(
|
|
287
|
-
` Node.js: ${nodeLabel.padEnd(17)}${results.nodeVersion ? PASS : FAIL}`
|
|
288
|
-
);
|
|
289
|
-
|
|
290
|
-
const orLabel = results.keys.openrouter
|
|
291
|
-
? `${maskKey(results.keys.openrouter)}`
|
|
292
|
-
: "missing";
|
|
293
|
-
const orStatus = results.apiKeyValid
|
|
294
|
-
? `${PASS} (verified)`
|
|
295
|
-
: results.keys.openrouter
|
|
296
|
-
? `${FAIL} (unverified)`
|
|
297
|
-
: FAIL;
|
|
298
|
-
console.log(` OpenRouter: ${orLabel.padEnd(17)}${orStatus}`);
|
|
299
|
-
|
|
300
|
-
const antLabel = results.keys.anthropic
|
|
301
|
-
? `${maskKey(results.keys.anthropic)}`
|
|
302
|
-
: "not set";
|
|
303
|
-
const antStatus = results.keys.anthropic ? PASS : `${WARN} (optional)`;
|
|
304
|
-
console.log(` Anthropic: ${antLabel.padEnd(17)}${antStatus}`);
|
|
305
|
-
|
|
306
|
-
const ollamaLabel = results.ollama.ok
|
|
307
|
-
? `${results.ollama.modelCount} model${results.ollama.modelCount === 1 ? "" : "s"}`
|
|
308
|
-
: "offline";
|
|
309
|
-
const ollamaStatus = results.ollama.ok ? PASS : WARN;
|
|
310
|
-
console.log(
|
|
311
|
-
` Ollama: ${ollamaLabel.padEnd(17)}${ollamaStatus}`
|
|
312
|
-
);
|
|
313
|
-
|
|
314
|
-
const configLabel = results.configValid ? "all valid" : "errors";
|
|
315
|
-
console.log(
|
|
316
|
-
` Config: ${configLabel.padEnd(17)}${results.configValid ? PASS : FAIL}`
|
|
317
|
-
);
|
|
318
|
-
|
|
319
|
-
const stateLabel = results.stateDirs ? "ready" : "error";
|
|
320
|
-
console.log(
|
|
321
|
-
` State dirs: ${stateLabel.padEnd(17)}${results.stateDirs ? PASS : FAIL}`
|
|
322
|
-
);
|
|
323
|
-
|
|
324
|
-
console.log("");
|
|
325
|
-
console.log(bold(" Ready to run:"));
|
|
326
|
-
console.log(
|
|
327
|
-
` ${DIM}npm run run:heartbeat${RESET} # dry-run (no API calls)`
|
|
328
|
-
);
|
|
329
|
-
console.log(
|
|
330
|
-
` ${DIM}npm run run:heartbeat:provider${RESET} # live run (uses API)`
|
|
331
|
-
);
|
|
332
|
-
console.log("");
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
// ── Main ────────────────────────────────────────────────────────────
|
|
336
|
-
|
|
337
|
-
export async function runSetup() {
|
|
338
|
-
const pkg = readPackageJson();
|
|
339
|
-
printBanner(pkg);
|
|
340
|
-
|
|
341
|
-
// Critical checks
|
|
342
|
-
const nodeVersion = checkNodeVersion();
|
|
343
|
-
const keys = detectApiKeys();
|
|
344
|
-
const apiKeyValid = await validateOpenRouterKey(keys.openrouter);
|
|
345
|
-
|
|
346
|
-
// Non-blocking check
|
|
347
|
-
const ollama = await checkOllama();
|
|
348
|
-
|
|
349
|
-
// Critical checks (continued)
|
|
350
|
-
const configValid = await checkConfig();
|
|
351
|
-
const stateDirs = await ensureStateDirs();
|
|
352
|
-
|
|
353
|
-
const results = {
|
|
354
|
-
nodeVersion,
|
|
355
|
-
keys,
|
|
356
|
-
apiKeyValid,
|
|
357
|
-
ollama,
|
|
358
|
-
configValid,
|
|
359
|
-
stateDirs,
|
|
360
|
-
};
|
|
361
|
-
|
|
362
|
-
printSummary(results);
|
|
363
|
-
|
|
364
|
-
// Exit code: fail if any critical check failed
|
|
365
|
-
const criticalPass =
|
|
366
|
-
nodeVersion && keys.openrouter && apiKeyValid && configValid && stateDirs;
|
|
367
|
-
|
|
368
|
-
return criticalPass ? 0 : 1;
|
|
369
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* First-run setup script for Nemoris V2.
|
|
3
|
+
*
|
|
4
|
+
* Validates the environment, detects API keys, checks external services,
|
|
5
|
+
* validates config, and ensures state directories exist.
|
|
6
|
+
*
|
|
7
|
+
* No npm dependencies — uses native fetch, native fs, ANSI codes only.
|
|
8
|
+
* Idempotent and non-interactive (no prompts).
|
|
9
|
+
* Exit code 0 if all critical checks pass, 1 otherwise.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import fs from "node:fs";
|
|
13
|
+
import os from "node:os";
|
|
14
|
+
import path from "node:path";
|
|
15
|
+
import { fileURLToPath } from "node:url";
|
|
16
|
+
|
|
17
|
+
// ── ANSI helpers ────────────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
const BOLD = "\x1b[1m";
|
|
20
|
+
const RESET = "\x1b[0m";
|
|
21
|
+
const GREEN = "\x1b[32m";
|
|
22
|
+
const RED = "\x1b[31m";
|
|
23
|
+
const YELLOW = "\x1b[33m";
|
|
24
|
+
const DIM = "\x1b[2m";
|
|
25
|
+
|
|
26
|
+
const PASS = `${GREEN}\u2713${RESET}`;
|
|
27
|
+
const FAIL = `${RED}\u2717${RESET}`;
|
|
28
|
+
const WARN = `${YELLOW}!${RESET}`;
|
|
29
|
+
|
|
30
|
+
function bold(text) {
|
|
31
|
+
return `${BOLD}${text}${RESET}`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// ── Path resolution ─────────────────────────────────────────────────
|
|
35
|
+
|
|
36
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
37
|
+
const projectRoot = path.resolve(__dirname, "..");
|
|
38
|
+
const configRoot = path.join(projectRoot, "config");
|
|
39
|
+
const stateRoot = path.join(projectRoot, "state", "memory");
|
|
40
|
+
|
|
41
|
+
// ── Env loader (mirrors cli.js) ─────────────────────────────────────
|
|
42
|
+
|
|
43
|
+
function loadParentEnv(envPath) {
|
|
44
|
+
try {
|
|
45
|
+
const content = fs.readFileSync(envPath, "utf8");
|
|
46
|
+
for (const line of content.split("\n")) {
|
|
47
|
+
const trimmed = line.trim();
|
|
48
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
49
|
+
const eqIdx = trimmed.indexOf("=");
|
|
50
|
+
if (eqIdx < 1) continue;
|
|
51
|
+
const key = trimmed.slice(0, eqIdx).trim();
|
|
52
|
+
if (process.env[key] !== undefined) continue;
|
|
53
|
+
let value = trimmed.slice(eqIdx + 1).trim();
|
|
54
|
+
if (
|
|
55
|
+
(value.startsWith('"') && value.endsWith('"')) ||
|
|
56
|
+
(value.startsWith("'") && value.endsWith("'"))
|
|
57
|
+
) {
|
|
58
|
+
value = value.slice(1, -1);
|
|
59
|
+
}
|
|
60
|
+
process.env[key] = value;
|
|
61
|
+
}
|
|
62
|
+
} catch {
|
|
63
|
+
// .env file is optional
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ── Helpers ─────────────────────────────────────────────────────────
|
|
68
|
+
|
|
69
|
+
function maskKey(key) {
|
|
70
|
+
if (!key || key.length < 7) return "***";
|
|
71
|
+
return key.slice(0, 6) + "...";
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function readPackageJson() {
|
|
75
|
+
const raw = fs.readFileSync(path.join(projectRoot, "package.json"), "utf8");
|
|
76
|
+
return JSON.parse(raw);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ── Step 1: Welcome banner ──────────────────────────────────────────
|
|
80
|
+
|
|
81
|
+
function printBanner(pkg) {
|
|
82
|
+
console.log("");
|
|
83
|
+
console.log(
|
|
84
|
+
bold("── Nemoris V2 Setup ") +
|
|
85
|
+
DIM +
|
|
86
|
+
`v${pkg.version}` +
|
|
87
|
+
RESET +
|
|
88
|
+
bold(" ─────────────────────")
|
|
89
|
+
);
|
|
90
|
+
console.log("");
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ── Step 2: Node.js version check ───────────────────────────────────
|
|
94
|
+
|
|
95
|
+
function checkNodeVersion() {
|
|
96
|
+
const full = process.version; // e.g. "v25.8.0"
|
|
97
|
+
const major = parseInt(full.slice(1), 10);
|
|
98
|
+
const ok = major >= 22;
|
|
99
|
+
if (ok) {
|
|
100
|
+
console.log(` Node.js ${full} ${PASS}`);
|
|
101
|
+
} else {
|
|
102
|
+
console.log(
|
|
103
|
+
` Node.js ${full} ${FAIL} (need >= 22.5)`
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
return ok;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ── Step 3: Detect API keys ─────────────────────────────────────────
|
|
110
|
+
|
|
111
|
+
function detectApiKeys() {
|
|
112
|
+
// Load parent .env first
|
|
113
|
+
const parentEnvPath = path.resolve(
|
|
114
|
+
process.env.HOME || os.homedir(),
|
|
115
|
+
".openclaw",
|
|
116
|
+
".env"
|
|
117
|
+
);
|
|
118
|
+
loadParentEnv(parentEnvPath);
|
|
119
|
+
|
|
120
|
+
const keys = {};
|
|
121
|
+
|
|
122
|
+
// OpenRouter (primary for battle-testing)
|
|
123
|
+
const orKey = process.env.OPENROUTER_API_KEY;
|
|
124
|
+
if (orKey) {
|
|
125
|
+
console.log(` OpenRouter ${maskKey(orKey)} ${PASS} (primary)`);
|
|
126
|
+
keys.openrouter = orKey;
|
|
127
|
+
} else {
|
|
128
|
+
console.log(` OpenRouter not found ${FAIL}`);
|
|
129
|
+
keys.openrouter = null;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Anthropic (optional — routes through OpenRouter when unavailable)
|
|
133
|
+
const antKey = process.env.NEMORIS_ANTHROPIC_API_KEY;
|
|
134
|
+
if (antKey) {
|
|
135
|
+
console.log(` Anthropic ${maskKey(antKey)} ${PASS}`);
|
|
136
|
+
keys.anthropic = antKey;
|
|
137
|
+
} else {
|
|
138
|
+
console.log(` Anthropic not found ${WARN} (optional)`);
|
|
139
|
+
keys.anthropic = null;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return keys;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ── Step 4: Validate OpenRouter API key ─────────────────────────────
|
|
146
|
+
|
|
147
|
+
async function validateOpenRouterKey(apiKey) {
|
|
148
|
+
if (!apiKey) return false;
|
|
149
|
+
|
|
150
|
+
try {
|
|
151
|
+
const res = await fetch("https://openrouter.ai/api/v1/models", {
|
|
152
|
+
headers: {
|
|
153
|
+
"Authorization": `Bearer ${apiKey}`,
|
|
154
|
+
},
|
|
155
|
+
signal: AbortSignal.timeout(10000),
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
if (res.ok) {
|
|
159
|
+
console.log(` API check verified ${PASS}`);
|
|
160
|
+
return true;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
console.log(` API check invalid ${FAIL} (HTTP ${res.status})`);
|
|
164
|
+
return false;
|
|
165
|
+
} catch (err) {
|
|
166
|
+
console.log(` API check error ${FAIL} (${err.message})`);
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// ── Step 5: Check Ollama ────────────────────────────────────────────
|
|
172
|
+
|
|
173
|
+
async function checkOllama() {
|
|
174
|
+
try {
|
|
175
|
+
const res = await fetch("http://localhost:11434/api/tags", {
|
|
176
|
+
signal: AbortSignal.timeout(5000),
|
|
177
|
+
});
|
|
178
|
+
if (!res.ok) {
|
|
179
|
+
console.log(
|
|
180
|
+
` Ollama unreachable ${WARN} (HTTP ${res.status})`
|
|
181
|
+
);
|
|
182
|
+
return { ok: false, modelCount: 0 };
|
|
183
|
+
}
|
|
184
|
+
const data = await res.json();
|
|
185
|
+
const models = data.models || [];
|
|
186
|
+
if (models.length === 0) {
|
|
187
|
+
console.log(` Ollama 0 models ${WARN} (no models pulled)`);
|
|
188
|
+
} else {
|
|
189
|
+
console.log(
|
|
190
|
+
` Ollama ${models.length} model${models.length === 1 ? "" : "s"}${models.length < 10 ? " " : " "} ${PASS}`
|
|
191
|
+
);
|
|
192
|
+
for (const m of models) {
|
|
193
|
+
console.log(`${DIM} - ${m.name}${RESET}`);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return { ok: true, modelCount: models.length };
|
|
197
|
+
} catch {
|
|
198
|
+
console.log(
|
|
199
|
+
` Ollama not running ${WARN} (remote providers still work)`
|
|
200
|
+
);
|
|
201
|
+
return { ok: false, modelCount: 0 };
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// ── Step 6: Validate config ─────────────────────────────────────────
|
|
206
|
+
|
|
207
|
+
async function checkConfig() {
|
|
208
|
+
try {
|
|
209
|
+
const { ConfigLoader } = await import("./config/loader.js");
|
|
210
|
+
const { validateAllConfigs } = await import(
|
|
211
|
+
"./config/schema-validator.js"
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
const loader = new ConfigLoader({ rootDir: configRoot });
|
|
215
|
+
const config = await loader.loadAll();
|
|
216
|
+
const result = validateAllConfigs(config, configRoot);
|
|
217
|
+
|
|
218
|
+
if (result.ok) {
|
|
219
|
+
console.log(` Config all valid ${PASS}`);
|
|
220
|
+
} else {
|
|
221
|
+
console.log(
|
|
222
|
+
` Config ${result.errors.length} error${result.errors.length === 1 ? "" : "s"} ${FAIL}`
|
|
223
|
+
);
|
|
224
|
+
for (const e of result.errors) {
|
|
225
|
+
console.log(`${DIM} - ${e}${RESET}`);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return result.ok;
|
|
229
|
+
} catch (err) {
|
|
230
|
+
console.log(
|
|
231
|
+
` Config load error ${FAIL} (${err.message})`
|
|
232
|
+
);
|
|
233
|
+
return false;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// ── Step 7: Ensure state directories ────────────────────────────────
|
|
238
|
+
|
|
239
|
+
async function ensureStateDirs() {
|
|
240
|
+
try {
|
|
241
|
+
// Read agent configs to know which dirs are needed
|
|
242
|
+
const agentsDir = path.join(configRoot, "agents");
|
|
243
|
+
const agentFiles = fs.readdirSync(agentsDir).filter((f) => f.endsWith(".toml"));
|
|
244
|
+
const agentIds = agentFiles.map((f) => path.basename(f, ".toml"));
|
|
245
|
+
|
|
246
|
+
let created = 0;
|
|
247
|
+
let _existed = 0;
|
|
248
|
+
|
|
249
|
+
for (const id of agentIds) {
|
|
250
|
+
const dir = path.join(stateRoot, id);
|
|
251
|
+
if (fs.existsSync(dir)) {
|
|
252
|
+
_existed++;
|
|
253
|
+
} else {
|
|
254
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
255
|
+
created++;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (created > 0) {
|
|
260
|
+
console.log(
|
|
261
|
+
` State dirs ${created} created ${PASS}`
|
|
262
|
+
);
|
|
263
|
+
} else {
|
|
264
|
+
console.log(
|
|
265
|
+
` State dirs all present ${PASS}`
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
return true;
|
|
269
|
+
} catch (err) {
|
|
270
|
+
console.log(
|
|
271
|
+
` State dirs error ${FAIL} (${err.message})`
|
|
272
|
+
);
|
|
273
|
+
return false;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// ── Summary ─────────────────────────────────────────────────────────
|
|
278
|
+
|
|
279
|
+
function printSummary(results) {
|
|
280
|
+
console.log("");
|
|
281
|
+
console.log(bold("── Setup Complete ") + bold("─".repeat(30)));
|
|
282
|
+
|
|
283
|
+
const nodeLabel = results.nodeVersion
|
|
284
|
+
? `${process.version}`
|
|
285
|
+
: process.version;
|
|
286
|
+
console.log(
|
|
287
|
+
` Node.js: ${nodeLabel.padEnd(17)}${results.nodeVersion ? PASS : FAIL}`
|
|
288
|
+
);
|
|
289
|
+
|
|
290
|
+
const orLabel = results.keys.openrouter
|
|
291
|
+
? `${maskKey(results.keys.openrouter)}`
|
|
292
|
+
: "missing";
|
|
293
|
+
const orStatus = results.apiKeyValid
|
|
294
|
+
? `${PASS} (verified)`
|
|
295
|
+
: results.keys.openrouter
|
|
296
|
+
? `${FAIL} (unverified)`
|
|
297
|
+
: FAIL;
|
|
298
|
+
console.log(` OpenRouter: ${orLabel.padEnd(17)}${orStatus}`);
|
|
299
|
+
|
|
300
|
+
const antLabel = results.keys.anthropic
|
|
301
|
+
? `${maskKey(results.keys.anthropic)}`
|
|
302
|
+
: "not set";
|
|
303
|
+
const antStatus = results.keys.anthropic ? PASS : `${WARN} (optional)`;
|
|
304
|
+
console.log(` Anthropic: ${antLabel.padEnd(17)}${antStatus}`);
|
|
305
|
+
|
|
306
|
+
const ollamaLabel = results.ollama.ok
|
|
307
|
+
? `${results.ollama.modelCount} model${results.ollama.modelCount === 1 ? "" : "s"}`
|
|
308
|
+
: "offline";
|
|
309
|
+
const ollamaStatus = results.ollama.ok ? PASS : WARN;
|
|
310
|
+
console.log(
|
|
311
|
+
` Ollama: ${ollamaLabel.padEnd(17)}${ollamaStatus}`
|
|
312
|
+
);
|
|
313
|
+
|
|
314
|
+
const configLabel = results.configValid ? "all valid" : "errors";
|
|
315
|
+
console.log(
|
|
316
|
+
` Config: ${configLabel.padEnd(17)}${results.configValid ? PASS : FAIL}`
|
|
317
|
+
);
|
|
318
|
+
|
|
319
|
+
const stateLabel = results.stateDirs ? "ready" : "error";
|
|
320
|
+
console.log(
|
|
321
|
+
` State dirs: ${stateLabel.padEnd(17)}${results.stateDirs ? PASS : FAIL}`
|
|
322
|
+
);
|
|
323
|
+
|
|
324
|
+
console.log("");
|
|
325
|
+
console.log(bold(" Ready to run:"));
|
|
326
|
+
console.log(
|
|
327
|
+
` ${DIM}npm run run:heartbeat${RESET} # dry-run (no API calls)`
|
|
328
|
+
);
|
|
329
|
+
console.log(
|
|
330
|
+
` ${DIM}npm run run:heartbeat:provider${RESET} # live run (uses API)`
|
|
331
|
+
);
|
|
332
|
+
console.log("");
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// ── Main ────────────────────────────────────────────────────────────
|
|
336
|
+
|
|
337
|
+
export async function runSetup() {
|
|
338
|
+
const pkg = readPackageJson();
|
|
339
|
+
printBanner(pkg);
|
|
340
|
+
|
|
341
|
+
// Critical checks
|
|
342
|
+
const nodeVersion = checkNodeVersion();
|
|
343
|
+
const keys = detectApiKeys();
|
|
344
|
+
const apiKeyValid = await validateOpenRouterKey(keys.openrouter);
|
|
345
|
+
|
|
346
|
+
// Non-blocking check
|
|
347
|
+
const ollama = await checkOllama();
|
|
348
|
+
|
|
349
|
+
// Critical checks (continued)
|
|
350
|
+
const configValid = await checkConfig();
|
|
351
|
+
const stateDirs = await ensureStateDirs();
|
|
352
|
+
|
|
353
|
+
const results = {
|
|
354
|
+
nodeVersion,
|
|
355
|
+
keys,
|
|
356
|
+
apiKeyValid,
|
|
357
|
+
ollama,
|
|
358
|
+
configValid,
|
|
359
|
+
stateDirs,
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
printSummary(results);
|
|
363
|
+
|
|
364
|
+
// Exit code: fail if any critical check failed
|
|
365
|
+
const criticalPass =
|
|
366
|
+
nodeVersion && keys.openrouter && apiKeyValid && configValid && stateDirs;
|
|
367
|
+
|
|
368
|
+
return criticalPass ? 0 : 1;
|
|
369
|
+
}
|