nemoris 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +49 -0
- package/LICENSE +21 -0
- package/README.md +209 -0
- package/SECURITY.md +119 -0
- package/bin/nemoris +46 -0
- package/config/agents/agent.toml.example +28 -0
- package/config/agents/default.toml +22 -0
- package/config/agents/orchestrator.toml +18 -0
- package/config/delivery.toml +73 -0
- package/config/embeddings.toml +5 -0
- package/config/identity/default-purpose.md +1 -0
- package/config/identity/default-soul.md +3 -0
- package/config/identity/orchestrator-purpose.md +1 -0
- package/config/identity/orchestrator-soul.md +1 -0
- package/config/improvement-targets.toml +15 -0
- package/config/jobs/heartbeat-check.toml +30 -0
- package/config/jobs/memory-rollup.toml +46 -0
- package/config/jobs/workspace-health.toml +63 -0
- package/config/mcp.toml +16 -0
- package/config/output-contracts.toml +17 -0
- package/config/peers.toml +32 -0
- package/config/peers.toml.example +32 -0
- package/config/policies/memory-default.toml +10 -0
- package/config/policies/memory-heartbeat.toml +5 -0
- package/config/policies/memory-ops.toml +10 -0
- package/config/policies/tools-heartbeat-minimal.toml +8 -0
- package/config/policies/tools-interactive-safe.toml +8 -0
- package/config/policies/tools-ops-bounded.toml +8 -0
- package/config/policies/tools-orchestrator.toml +7 -0
- package/config/providers/anthropic.toml +15 -0
- package/config/providers/ollama.toml +5 -0
- package/config/providers/openai-codex.toml +9 -0
- package/config/providers/openrouter.toml +5 -0
- package/config/router.toml +22 -0
- package/config/runtime.toml +114 -0
- package/config/skills/self-improvement.toml +15 -0
- package/config/skills/telegram-onboarding-spec.md +240 -0
- package/config/skills/workspace-monitor.toml +15 -0
- package/config/task-router.toml +42 -0
- package/install.sh +50 -0
- package/package.json +90 -0
- package/src/auth/auth-profiles.js +169 -0
- package/src/auth/openai-codex-oauth.js +285 -0
- package/src/battle.js +449 -0
- package/src/cli/help.js +265 -0
- package/src/cli/output-filter.js +49 -0
- package/src/cli/runtime-control.js +704 -0
- package/src/cli-main.js +2763 -0
- package/src/cli.js +78 -0
- package/src/config/loader.js +332 -0
- package/src/config/schema-validator.js +214 -0
- package/src/config/toml-lite.js +8 -0
- package/src/daemon/action-handlers.js +71 -0
- package/src/daemon/healing-tick.js +87 -0
- package/src/daemon/health-probes.js +90 -0
- package/src/daemon/notifier.js +57 -0
- package/src/daemon/nurse.js +218 -0
- package/src/daemon/repair-log.js +106 -0
- package/src/daemon/rule-staging.js +90 -0
- package/src/daemon/rules.js +29 -0
- package/src/daemon/telegram-commands.js +54 -0
- package/src/daemon/updater.js +85 -0
- package/src/jobs/job-runner.js +78 -0
- package/src/mcp/consumer.js +129 -0
- package/src/memory/active-recall.js +171 -0
- package/src/memory/backend-manager.js +97 -0
- package/src/memory/backends/file-backend.js +38 -0
- package/src/memory/backends/qmd-backend.js +219 -0
- package/src/memory/embedding-guards.js +24 -0
- package/src/memory/embedding-index.js +118 -0
- package/src/memory/embedding-service.js +179 -0
- package/src/memory/file-index.js +177 -0
- package/src/memory/memory-signature.js +5 -0
- package/src/memory/memory-store.js +648 -0
- package/src/memory/retrieval-planner.js +66 -0
- package/src/memory/scoring.js +145 -0
- package/src/memory/simhash.js +78 -0
- package/src/memory/sqlite-active-store.js +824 -0
- package/src/memory/write-policy.js +36 -0
- package/src/onboarding/aliases.js +33 -0
- package/src/onboarding/auth/api-key.js +224 -0
- package/src/onboarding/auth/ollama-detect.js +42 -0
- package/src/onboarding/clack-prompter.js +77 -0
- package/src/onboarding/doctor.js +530 -0
- package/src/onboarding/lock.js +42 -0
- package/src/onboarding/model-catalog.js +344 -0
- package/src/onboarding/phases/auth.js +589 -0
- package/src/onboarding/phases/build.js +130 -0
- package/src/onboarding/phases/choose.js +82 -0
- package/src/onboarding/phases/detect.js +98 -0
- package/src/onboarding/phases/hatch.js +216 -0
- package/src/onboarding/phases/identity.js +79 -0
- package/src/onboarding/phases/ollama.js +345 -0
- package/src/onboarding/phases/scaffold.js +99 -0
- package/src/onboarding/phases/telegram.js +377 -0
- package/src/onboarding/phases/validate.js +204 -0
- package/src/onboarding/phases/verify.js +206 -0
- package/src/onboarding/platform.js +482 -0
- package/src/onboarding/status-bar.js +95 -0
- package/src/onboarding/templates.js +794 -0
- package/src/onboarding/toml-writer.js +38 -0
- package/src/onboarding/tui.js +250 -0
- package/src/onboarding/uninstall.js +153 -0
- package/src/onboarding/wizard.js +499 -0
- package/src/providers/anthropic.js +168 -0
- package/src/providers/base.js +247 -0
- package/src/providers/circuit-breaker.js +136 -0
- package/src/providers/ollama.js +163 -0
- package/src/providers/openai-codex.js +149 -0
- package/src/providers/openrouter.js +136 -0
- package/src/providers/registry.js +36 -0
- package/src/providers/router.js +16 -0
- package/src/runtime/bootstrap-cache.js +47 -0
- package/src/runtime/capabilities-prompt.js +25 -0
- package/src/runtime/completion-ping.js +99 -0
- package/src/runtime/config-validator.js +121 -0
- package/src/runtime/context-ledger.js +360 -0
- package/src/runtime/cutover-readiness.js +42 -0
- package/src/runtime/daemon.js +729 -0
- package/src/runtime/delivery-ack.js +195 -0
- package/src/runtime/delivery-adapters/local-file.js +41 -0
- package/src/runtime/delivery-adapters/openclaw-cli.js +94 -0
- package/src/runtime/delivery-adapters/openclaw-peer.js +98 -0
- package/src/runtime/delivery-adapters/shadow.js +13 -0
- package/src/runtime/delivery-adapters/standalone-http.js +98 -0
- package/src/runtime/delivery-adapters/telegram.js +104 -0
- package/src/runtime/delivery-adapters/tui.js +128 -0
- package/src/runtime/delivery-manager.js +807 -0
- package/src/runtime/delivery-store.js +168 -0
- package/src/runtime/dependency-health.js +118 -0
- package/src/runtime/envelope.js +114 -0
- package/src/runtime/evaluation.js +1089 -0
- package/src/runtime/exec-approvals.js +216 -0
- package/src/runtime/executor.js +500 -0
- package/src/runtime/failure-ping.js +67 -0
- package/src/runtime/flows.js +83 -0
- package/src/runtime/guards.js +45 -0
- package/src/runtime/handoff.js +51 -0
- package/src/runtime/identity-cache.js +28 -0
- package/src/runtime/improvement-engine.js +109 -0
- package/src/runtime/improvement-harness.js +581 -0
- package/src/runtime/input-sanitiser.js +72 -0
- package/src/runtime/interaction-contract.js +347 -0
- package/src/runtime/lane-readiness.js +226 -0
- package/src/runtime/migration.js +323 -0
- package/src/runtime/model-resolution.js +78 -0
- package/src/runtime/network.js +64 -0
- package/src/runtime/notification-store.js +97 -0
- package/src/runtime/notifier.js +256 -0
- package/src/runtime/orchestrator.js +53 -0
- package/src/runtime/orphan-reaper.js +41 -0
- package/src/runtime/output-contract-schema.js +139 -0
- package/src/runtime/output-contract-validator.js +439 -0
- package/src/runtime/peer-readiness.js +69 -0
- package/src/runtime/peer-registry.js +133 -0
- package/src/runtime/pilot-status.js +108 -0
- package/src/runtime/prompt-builder.js +261 -0
- package/src/runtime/provider-attempt.js +582 -0
- package/src/runtime/report-fallback.js +71 -0
- package/src/runtime/result-normalizer.js +183 -0
- package/src/runtime/retention.js +74 -0
- package/src/runtime/review.js +244 -0
- package/src/runtime/route-job.js +15 -0
- package/src/runtime/run-store.js +38 -0
- package/src/runtime/schedule.js +88 -0
- package/src/runtime/scheduler-state.js +434 -0
- package/src/runtime/scheduler.js +656 -0
- package/src/runtime/session-compactor.js +182 -0
- package/src/runtime/session-search.js +155 -0
- package/src/runtime/slack-inbound.js +249 -0
- package/src/runtime/ssrf.js +102 -0
- package/src/runtime/status-aggregator.js +330 -0
- package/src/runtime/task-contract.js +140 -0
- package/src/runtime/task-packet.js +107 -0
- package/src/runtime/task-router.js +140 -0
- package/src/runtime/telegram-inbound.js +1565 -0
- package/src/runtime/token-counter.js +134 -0
- package/src/runtime/token-estimator.js +59 -0
- package/src/runtime/tool-loop.js +200 -0
- package/src/runtime/transport-server.js +311 -0
- package/src/runtime/tui-server.js +411 -0
- package/src/runtime/ulid.js +44 -0
- package/src/security/ssrf-check.js +197 -0
- package/src/setup.js +369 -0
- package/src/shadow/bridge.js +303 -0
- package/src/skills/loader.js +84 -0
- package/src/tools/catalog.json +49 -0
- package/src/tools/cli-delegate.js +44 -0
- package/src/tools/mcp-client.js +106 -0
- package/src/tools/micro/cancel-task.js +6 -0
- package/src/tools/micro/complete-task.js +6 -0
- package/src/tools/micro/fail-task.js +6 -0
- package/src/tools/micro/http-fetch.js +74 -0
- package/src/tools/micro/index.js +36 -0
- package/src/tools/micro/lcm-recall.js +60 -0
- package/src/tools/micro/list-dir.js +17 -0
- package/src/tools/micro/list-skills.js +46 -0
- package/src/tools/micro/load-skill.js +38 -0
- package/src/tools/micro/memory-search.js +45 -0
- package/src/tools/micro/read-file.js +11 -0
- package/src/tools/micro/session-search.js +54 -0
- package/src/tools/micro/shell-exec.js +43 -0
- package/src/tools/micro/trigger-job.js +79 -0
- package/src/tools/micro/web-search.js +58 -0
- package/src/tools/micro/workspace-paths.js +39 -0
- package/src/tools/micro/write-file.js +14 -0
- package/src/tools/micro/write-memory.js +41 -0
- package/src/tools/registry.js +348 -0
- package/src/tools/tool-result-contract.js +36 -0
- package/src/tui/chat.js +835 -0
- package/src/tui/renderer.js +175 -0
- package/src/tui/socket-client.js +217 -0
- package/src/utils/canonical-json.js +29 -0
- package/src/utils/compaction.js +30 -0
- package/src/utils/env-loader.js +5 -0
- package/src/utils/errors.js +80 -0
- package/src/utils/fs.js +101 -0
- package/src/utils/ids.js +5 -0
- package/src/utils/model-context-limits.js +30 -0
- package/src/utils/token-budget.js +74 -0
- package/src/utils/usage-cost.js +25 -0
- package/src/utils/usage-metrics.js +14 -0
- package/vendor/smol-toml-1.5.2.tgz +0 -0
|
@@ -0,0 +1,530 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Nemoris Doctor — standalone health report command.
|
|
3
|
+
*
|
|
4
|
+
* Pure functions (checkPlatform, formatDoctorReport) are unit-tested.
|
|
5
|
+
* runDoctor orchestrates all checks and is tested in integration.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import fs from "node:fs";
|
|
9
|
+
import os from "node:os";
|
|
10
|
+
import { execFile } from "node:child_process";
|
|
11
|
+
import { promisify } from "node:util";
|
|
12
|
+
import { getDaemonPaths, listServeDaemonPids } from "../cli/runtime-control.js";
|
|
13
|
+
|
|
14
|
+
const execFileAsync = promisify(execFile);
|
|
15
|
+
|
|
16
|
+
// ── Minimum supported Node.js major version ─────────────────────────
|
|
17
|
+
const MIN_NODE_MAJOR = 22;
|
|
18
|
+
|
|
19
|
+
// ── Platform check ───────────────────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Returns basic platform information.
|
|
23
|
+
* @returns {{ os: string, arch: string, nodeVersion: string, nodeOk: boolean }}
|
|
24
|
+
*/
|
|
25
|
+
export function checkPlatform() {
|
|
26
|
+
const platform = os.platform();
|
|
27
|
+
const arch = os.arch();
|
|
28
|
+
const nodeVersion = process.version;
|
|
29
|
+
const major = parseInt(nodeVersion.replace(/^v/, "").split(".")[0], 10);
|
|
30
|
+
const nodeOk = major >= MIN_NODE_MAJOR;
|
|
31
|
+
|
|
32
|
+
// Map os.platform() to a human-readable OS name
|
|
33
|
+
const osNames = {
|
|
34
|
+
darwin: "macOS",
|
|
35
|
+
linux: "Linux",
|
|
36
|
+
win32: "Windows",
|
|
37
|
+
};
|
|
38
|
+
const osDisplay = osNames[platform] || platform;
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
os: osDisplay,
|
|
42
|
+
arch,
|
|
43
|
+
nodeVersion,
|
|
44
|
+
nodeOk,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ── Dependency checks ────────────────────────────────────────────────
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Checks for required and optional CLI dependencies.
|
|
52
|
+
* @returns {Promise<Array<{ name: string, found: boolean, required: boolean, installHint: string }>>}
|
|
53
|
+
*/
|
|
54
|
+
export async function checkDependencies() {
|
|
55
|
+
const platform = os.platform();
|
|
56
|
+
const whichCmd = platform === "win32" ? "where" : "which";
|
|
57
|
+
const deps = [];
|
|
58
|
+
|
|
59
|
+
async function check(name, required, hint) {
|
|
60
|
+
try {
|
|
61
|
+
await execFileAsync(whichCmd, [name], { timeout: 3000 });
|
|
62
|
+
deps.push({ name, found: true, required, installHint: hint });
|
|
63
|
+
} catch {
|
|
64
|
+
deps.push({ name, found: false, required, installHint: hint });
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
await check("git", true, "https://git-scm.com");
|
|
69
|
+
await check("ollama", false, "https://ollama.ai");
|
|
70
|
+
await check("gh", false, "https://cli.github.com (optional)");
|
|
71
|
+
|
|
72
|
+
if (platform === "win32") {
|
|
73
|
+
await check("pm2", false, "npm install -g pm2 (https://pm2.io)");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return deps;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ── Format helpers ───────────────────────────────────────────────────
|
|
80
|
+
|
|
81
|
+
function _stripAnsi(str) {
|
|
82
|
+
// Strip ANSI escape codes for length calculations
|
|
83
|
+
// eslint-disable-next-line no-control-regex
|
|
84
|
+
return str.replace(/\x1b\[[0-9;]*m/g, "");
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function checkIcon(ok) {
|
|
88
|
+
return ok ? "\x1b[32m✓\x1b[0m" : "\x1b[31m✗\x1b[0m";
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function warnIcon() {
|
|
92
|
+
return "\x1b[33m!\x1b[0m";
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function bold(text) {
|
|
96
|
+
return `\x1b[1m${text}\x1b[0m`;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function dim(text) {
|
|
100
|
+
return `\x1b[2m${text}\x1b[0m`;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function formatProviderReason(status) {
|
|
104
|
+
if (status === null || status === undefined || status === "") {
|
|
105
|
+
return "";
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (typeof status === "number") {
|
|
109
|
+
return `HTTP ${status}`;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const text = String(status).trim();
|
|
113
|
+
const missingToken = text.match(/\(env:([A-Z0-9_]+)\)/);
|
|
114
|
+
if (missingToken) {
|
|
115
|
+
return `missing ${missingToken[1]}`;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return text;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function row(label, value, ok) {
|
|
122
|
+
const icon = ok === null ? warnIcon() : checkIcon(ok);
|
|
123
|
+
const labelPad = label.padEnd(16);
|
|
124
|
+
const valuePad = (value || "").toString().padEnd(22);
|
|
125
|
+
return ` ${labelPad}${valuePad}${icon}`;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// ── formatDoctorReport ───────────────────────────────────────────────
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Renders a human-readable doctor report string.
|
|
132
|
+
*
|
|
133
|
+
* @param {{
|
|
134
|
+
* platform: { os: string, arch: string, nodeVersion: string, nodeOk: boolean },
|
|
135
|
+
* installDir: string,
|
|
136
|
+
* configValid: boolean,
|
|
137
|
+
* configErrors: string[],
|
|
138
|
+
* providers: Array<{ id: string, healthy: boolean, status?: string }>,
|
|
139
|
+
* lanes: Array<{ id: string, ready: boolean }>,
|
|
140
|
+
* jobs: Array<{ id: string, healthy: boolean }>,
|
|
141
|
+
* delivery: Array<{ id: string, ready: boolean }>,
|
|
142
|
+
* daemon?: { pids?: number[], stalePidFile?: boolean },
|
|
143
|
+
* suggestions: string[],
|
|
144
|
+
* exitCode?: number,
|
|
145
|
+
* }} results
|
|
146
|
+
* @returns {string}
|
|
147
|
+
*/
|
|
148
|
+
export function formatDoctorReport(results) {
|
|
149
|
+
const {
|
|
150
|
+
platform,
|
|
151
|
+
installDir,
|
|
152
|
+
configValid,
|
|
153
|
+
configErrors = [],
|
|
154
|
+
dependencies = [],
|
|
155
|
+
providers = [],
|
|
156
|
+
lanes = [],
|
|
157
|
+
jobs = [],
|
|
158
|
+
delivery = [],
|
|
159
|
+
mcpServers = [],
|
|
160
|
+
daemon = {},
|
|
161
|
+
suggestions = [],
|
|
162
|
+
} = results;
|
|
163
|
+
|
|
164
|
+
const lines = [];
|
|
165
|
+
|
|
166
|
+
// Header
|
|
167
|
+
const headerLine = "─".repeat(40);
|
|
168
|
+
lines.push(`\n${bold(`── Nemoris Doctor `)}${bold(headerLine.slice(19))}\n`);
|
|
169
|
+
|
|
170
|
+
// Platform section
|
|
171
|
+
const osLabel = `${platform.os} (${platform.arch})`;
|
|
172
|
+
lines.push(row("Platform", osLabel, true));
|
|
173
|
+
lines.push(row("Node.js", platform.nodeVersion, platform.nodeOk));
|
|
174
|
+
|
|
175
|
+
// Install dir — shorten to ~ if in home
|
|
176
|
+
const homeDir = os.homedir();
|
|
177
|
+
const displayDir = installDir
|
|
178
|
+
? installDir.startsWith(homeDir)
|
|
179
|
+
? installDir.replace(homeDir, "~")
|
|
180
|
+
: installDir
|
|
181
|
+
: "(unknown)";
|
|
182
|
+
lines.push(row("Install", displayDir, true));
|
|
183
|
+
|
|
184
|
+
const allLanesReady = lanes.every((lane) => lane.ready);
|
|
185
|
+
|
|
186
|
+
// Dependencies section
|
|
187
|
+
if (dependencies.length > 0) {
|
|
188
|
+
lines.push(`\n${bold(" Dependencies")}`);
|
|
189
|
+
for (let i = 0; i < dependencies.length; i++) {
|
|
190
|
+
const d = dependencies[i];
|
|
191
|
+
const prefix = i === dependencies.length - 1 ? " └─" : " ├─";
|
|
192
|
+
if (d.found) {
|
|
193
|
+
lines.push(`${prefix} ${d.name.padEnd(14)} ${dim("found")} ${checkIcon(true)}`);
|
|
194
|
+
} else if (d.required) {
|
|
195
|
+
lines.push(`${prefix} ${d.name.padEnd(14)} \x1b[31mmissing\x1b[0m ${checkIcon(false)} ${dim(d.installHint)}`);
|
|
196
|
+
} else {
|
|
197
|
+
lines.push(`${prefix} ${d.name.padEnd(14)} ${dim("missing")} \x1b[33m!\x1b[0m ${dim(d.installHint)}`);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Providers section
|
|
203
|
+
if (providers.length > 0) {
|
|
204
|
+
lines.push(`\n${bold(" Providers")}`);
|
|
205
|
+
for (let i = 0; i < providers.length; i++) {
|
|
206
|
+
const p = providers[i];
|
|
207
|
+
const prefix = i === providers.length - 1 ? " └─" : " ├─";
|
|
208
|
+
if (p.healthy) {
|
|
209
|
+
lines.push(`${prefix} ${p.id.padEnd(14)} ${dim("healthy")} ${checkIcon(true)}`);
|
|
210
|
+
} else if (allLanesReady) {
|
|
211
|
+
const reasonText = formatProviderReason(p.status);
|
|
212
|
+
const reason = reasonText ? ` ${dim(`(${reasonText})`)}` : "";
|
|
213
|
+
lines.push(`${prefix} ${p.id.padEnd(14)} ${dim("not configured")} —${reason}`);
|
|
214
|
+
} else {
|
|
215
|
+
const reasonText = formatProviderReason(p.status);
|
|
216
|
+
const reason = reasonText ? dim(`(${reasonText})`) : "";
|
|
217
|
+
lines.push(`${prefix} ${p.id.padEnd(14)} \x1b[31munhealthy\x1b[0m ${checkIcon(false)} ${reason}`);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Config section
|
|
223
|
+
const configNote = configValid
|
|
224
|
+
? `${configErrors.length === 0 ? "all manifests valid" : `${configErrors.length} error(s)`}`
|
|
225
|
+
: `${configErrors.length} error(s)`;
|
|
226
|
+
lines.push(`\n${row("Config", configNote, configValid)}`);
|
|
227
|
+
|
|
228
|
+
if (configErrors.length > 0) {
|
|
229
|
+
for (const err of configErrors) {
|
|
230
|
+
lines.push(` ${dim("• " + err)}`);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Lanes section
|
|
235
|
+
if (lanes.length > 0) {
|
|
236
|
+
lines.push(`\n${bold(" Lanes")}`);
|
|
237
|
+
for (let i = 0; i < lanes.length; i++) {
|
|
238
|
+
const l = lanes[i];
|
|
239
|
+
const prefix = i === lanes.length - 1 ? " └─" : " ├─";
|
|
240
|
+
lines.push(`${prefix} ${l.id.padEnd(30)} ${checkIcon(l.ready)}`);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Jobs section
|
|
245
|
+
if (jobs.length > 0) {
|
|
246
|
+
lines.push(`\n${bold(" Jobs")}`);
|
|
247
|
+
for (let i = 0; i < jobs.length; i++) {
|
|
248
|
+
const j = jobs[i];
|
|
249
|
+
const prefix = i === jobs.length - 1 ? " └─" : " ├─";
|
|
250
|
+
lines.push(`${prefix} ${j.id.padEnd(30)} ${checkIcon(j.healthy)}`);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Delivery section
|
|
255
|
+
if (delivery.length > 0) {
|
|
256
|
+
lines.push(`\n${bold(" Delivery")}`);
|
|
257
|
+
for (let i = 0; i < delivery.length; i++) {
|
|
258
|
+
const d = delivery[i];
|
|
259
|
+
const prefix = i === delivery.length - 1 ? " └─" : " ├─";
|
|
260
|
+
lines.push(`${prefix} ${d.id.padEnd(30)} ${checkIcon(d.ready)}`);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// MCP Servers section
|
|
265
|
+
if (mcpServers.length > 0) {
|
|
266
|
+
lines.push(`\n${bold(" MCP Servers")}`);
|
|
267
|
+
for (let i = 0; i < mcpServers.length; i++) {
|
|
268
|
+
const s = mcpServers[i];
|
|
269
|
+
const prefix = i === mcpServers.length - 1 ? " └─" : " ├─";
|
|
270
|
+
if (s.alive) {
|
|
271
|
+
lines.push(`${prefix} ${s.id.padEnd(14)} ${dim("running")} ${checkIcon(true)}`);
|
|
272
|
+
} else {
|
|
273
|
+
lines.push(`${prefix} ${s.id.padEnd(14)} ${dim("stopped")} ${checkIcon(false)}`);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if ((daemon.pids || []).length > 1 || daemon.stalePidFile) {
|
|
279
|
+
lines.push(`\n${bold(" Daemon")}`);
|
|
280
|
+
if ((daemon.pids || []).length > 1) {
|
|
281
|
+
lines.push(` ⚠️ Multiple daemon processes detected (pids: ${daemon.pids.join(", ")}). Run nemoris stop && nemoris start to fix.`);
|
|
282
|
+
}
|
|
283
|
+
if (daemon.stalePidFile) {
|
|
284
|
+
lines.push(" ⚠️ Stale PID file found. Daemon is not running.");
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Overall status
|
|
289
|
+
// An unhealthy provider is only critical if lanes can't resolve without it.
|
|
290
|
+
// If all lanes are ready, unhealthy providers are warnings (unused/optional).
|
|
291
|
+
const allDeliveryReady = delivery.every((d) => d.ready);
|
|
292
|
+
const allJobsHealthy = jobs.every((j) => j.healthy);
|
|
293
|
+
const allProviderHealthy = providers.every((p) => p.healthy);
|
|
294
|
+
const overallHealthy = configValid && (allProviderHealthy || allLanesReady) && allDeliveryReady && allJobsHealthy;
|
|
295
|
+
|
|
296
|
+
const suggCount = suggestions.length;
|
|
297
|
+
const suggNote = suggCount > 0 ? ` (${suggCount} suggestion${suggCount === 1 ? "" : "s"})` : "";
|
|
298
|
+
const overallLabel = overallHealthy ? bold("healthy") : bold("degraded");
|
|
299
|
+
lines.push(`\n Overall: ${overallLabel}${suggNote}\n`);
|
|
300
|
+
|
|
301
|
+
// Suggestions
|
|
302
|
+
if (suggestions.length > 0) {
|
|
303
|
+
for (const s of suggestions) {
|
|
304
|
+
lines.push(` \x1b[33m💡\x1b[0m ${s}`);
|
|
305
|
+
}
|
|
306
|
+
lines.push("");
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
return lines.join("\n");
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// ── runDoctor ────────────────────────────────────────────────────────
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Orchestrates all health checks and returns a results object.
|
|
316
|
+
*
|
|
317
|
+
* @param {string} installDir — path to the nemoris install root
|
|
318
|
+
* @param {{
|
|
319
|
+
* execFileImpl?: Function,
|
|
320
|
+
* fsImpl?: typeof import("node:fs"),
|
|
321
|
+
* listServeDaemonPidsImpl?: Function,
|
|
322
|
+
* }} options
|
|
323
|
+
* @returns {Promise<object>} — results suitable for formatDoctorReport + exitCode
|
|
324
|
+
*/
|
|
325
|
+
export async function runDoctor(
|
|
326
|
+
installDir,
|
|
327
|
+
{
|
|
328
|
+
execFileImpl = execFileAsync,
|
|
329
|
+
fsImpl = fs,
|
|
330
|
+
listServeDaemonPidsImpl = listServeDaemonPids,
|
|
331
|
+
} = {},
|
|
332
|
+
) {
|
|
333
|
+
const results = {
|
|
334
|
+
platform: checkPlatform(),
|
|
335
|
+
installDir,
|
|
336
|
+
configValid: true,
|
|
337
|
+
configErrors: [],
|
|
338
|
+
dependencies: [],
|
|
339
|
+
providers: [],
|
|
340
|
+
lanes: [],
|
|
341
|
+
jobs: [],
|
|
342
|
+
delivery: [],
|
|
343
|
+
mcpServers: [],
|
|
344
|
+
daemon: {
|
|
345
|
+
pids: [],
|
|
346
|
+
stalePidFile: false,
|
|
347
|
+
},
|
|
348
|
+
suggestions: [],
|
|
349
|
+
exitCode: 0,
|
|
350
|
+
};
|
|
351
|
+
|
|
352
|
+
// ── 0. Dependency checks ──────────────────────────────────────────
|
|
353
|
+
try {
|
|
354
|
+
results.dependencies = await checkDependencies();
|
|
355
|
+
} catch {
|
|
356
|
+
// Dependency check failure is non-fatal
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// ── 1. Config validation ─────────────────────────────────────────
|
|
360
|
+
try {
|
|
361
|
+
const { ConfigLoader } = await import("../config/loader.js");
|
|
362
|
+
const { validateAllConfigs } = await import("../config/schema-validator.js");
|
|
363
|
+
const configDir = `${installDir}/config`;
|
|
364
|
+
const loader = new ConfigLoader({ rootDir: configDir });
|
|
365
|
+
const config = await loader.loadAll();
|
|
366
|
+
const validation = validateAllConfigs(config, configDir);
|
|
367
|
+
results.configValid = validation.ok;
|
|
368
|
+
results.configErrors = validation.errors || [];
|
|
369
|
+
results._config = config;
|
|
370
|
+
} catch (err) {
|
|
371
|
+
// Gracefully degrade — config may not exist yet
|
|
372
|
+
results.configValid = false;
|
|
373
|
+
results.configErrors = [err.message || "Failed to load config"];
|
|
374
|
+
results._config = null;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// ── 2. Provider health ───────────────────────────────────────────
|
|
378
|
+
try {
|
|
379
|
+
const { ProviderRegistry } = await import("../providers/registry.js");
|
|
380
|
+
const config = results._config;
|
|
381
|
+
if (config?.providers) {
|
|
382
|
+
const registry = new ProviderRegistry({ fetchImpl: fetch });
|
|
383
|
+
for (const [id, providerConfig] of Object.entries(config.providers)) {
|
|
384
|
+
try {
|
|
385
|
+
const adapter = registry.create(providerConfig);
|
|
386
|
+
let healthy = true;
|
|
387
|
+
let status = null;
|
|
388
|
+
if (typeof adapter.healthCheck === "function") {
|
|
389
|
+
const h = await adapter.healthCheck();
|
|
390
|
+
healthy = Boolean(h.ok);
|
|
391
|
+
status = h.status ?? null;
|
|
392
|
+
}
|
|
393
|
+
results.providers.push({ id, healthy, status });
|
|
394
|
+
} catch (err) {
|
|
395
|
+
results.providers.push({ id, healthy: false, status: err.message });
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
} catch {
|
|
400
|
+
// Provider module unavailable — skip
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// ── 3. Lane readiness ────────────────────────────────────────────
|
|
404
|
+
try {
|
|
405
|
+
const { buildLaneReadiness } = await import("../runtime/lane-readiness.js");
|
|
406
|
+
const config = results._config;
|
|
407
|
+
if (config?.router) {
|
|
408
|
+
for (const [laneId] of Object.entries(config.router)) {
|
|
409
|
+
try {
|
|
410
|
+
const readiness = buildLaneReadiness({ jobId: laneId });
|
|
411
|
+
results.lanes.push({ id: laneId, ready: readiness.ready !== false });
|
|
412
|
+
} catch {
|
|
413
|
+
results.lanes.push({ id: laneId, ready: false });
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
} catch {
|
|
418
|
+
// Lane readiness module unavailable — skip
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// ── 4. Job dependency health ─────────────────────────────────────
|
|
422
|
+
try {
|
|
423
|
+
const { DependencyHealth } = await import("../runtime/dependency-health.js");
|
|
424
|
+
const config = results._config;
|
|
425
|
+
if (config?.jobs) {
|
|
426
|
+
const dh = new DependencyHealth({
|
|
427
|
+
projectRoot: installDir,
|
|
428
|
+
liveRoot: installDir,
|
|
429
|
+
fetchImpl: fetch,
|
|
430
|
+
execFileImpl,
|
|
431
|
+
});
|
|
432
|
+
for (const [jobId] of Object.entries(config.jobs)) {
|
|
433
|
+
try {
|
|
434
|
+
let healthy = true;
|
|
435
|
+
if (typeof dh.forJob === "function") {
|
|
436
|
+
const jobHealth = await dh.forJob(jobId);
|
|
437
|
+
healthy = Boolean(jobHealth?.ok !== false);
|
|
438
|
+
}
|
|
439
|
+
results.jobs.push({ id: jobId, healthy });
|
|
440
|
+
} catch {
|
|
441
|
+
results.jobs.push({ id: jobId, healthy: false });
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
} catch {
|
|
446
|
+
// Dependency health module unavailable — skip
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// ── 5. Delivery channel readiness ───────────────────────────────
|
|
450
|
+
try {
|
|
451
|
+
const config = results._config;
|
|
452
|
+
if (config?.delivery?.profiles) {
|
|
453
|
+
for (const [id, profile] of Object.entries(config.delivery.profiles)) {
|
|
454
|
+
// Basic check: profile exists and has an adapter
|
|
455
|
+
const ready = Boolean(profile?.adapter);
|
|
456
|
+
results.delivery.push({ id, ready });
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
} catch {
|
|
460
|
+
// Delivery config unavailable — skip
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// ── 5b. MCP server health ────────────────────────────────────────────────────
|
|
464
|
+
try {
|
|
465
|
+
const config = results._config;
|
|
466
|
+
if (config?.mcp?.servers) {
|
|
467
|
+
for (const id of Object.keys(config.mcp.servers)) {
|
|
468
|
+
// MCP servers are lazy — at doctor time they're not running.
|
|
469
|
+
// Report them as configured but stopped (informational only).
|
|
470
|
+
results.mcpServers.push({ id, alive: false });
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
} catch {
|
|
474
|
+
// MCP config unavailable — skip
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// ── 5c. Daemon process health ────────────────────────────────────────────────
|
|
478
|
+
try {
|
|
479
|
+
const { pidFile } = getDaemonPaths(installDir);
|
|
480
|
+
results.daemon.pids = await listServeDaemonPidsImpl({ execFileImpl });
|
|
481
|
+
results.daemon.stalePidFile = results.daemon.pids.length === 0 && fsImpl.existsSync(pidFile);
|
|
482
|
+
} catch {
|
|
483
|
+
// Daemon process checks are best-effort only.
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// ── 6. Suggestions ───────────────────────────────────────────────
|
|
487
|
+
try {
|
|
488
|
+
const config = results._config;
|
|
489
|
+
// Suggest Telegram if no delivery configured
|
|
490
|
+
if (!config?.delivery?.profiles || Object.keys(config.delivery?.profiles || {}).length === 0) {
|
|
491
|
+
results.suggestions.push("Connect Telegram for delivery: nemoris channel add telegram");
|
|
492
|
+
}
|
|
493
|
+
// Suggest enabling embeddings if no embedding config
|
|
494
|
+
if (config && !config.runtime?.embeddings?.enabled) {
|
|
495
|
+
// Only suggest if runtime loaded but embeddings not explicitly enabled
|
|
496
|
+
if (config.runtime && config.runtime.embeddings === undefined) {
|
|
497
|
+
results.suggestions.push("Enable embeddings for semantic memory search: set runtime.embeddings.enabled = true");
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
} catch {
|
|
501
|
+
// Skip suggestions on error
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// ── 7. Determine exit code ───────────────────────────────────────
|
|
505
|
+
// Unhealthy providers are only critical if lanes can't resolve without them.
|
|
506
|
+
const allLanesOk = results.lanes.every((l) => l.ready);
|
|
507
|
+
const providerIssuesCritical = results.providers.some((p) => !p.healthy) && !allLanesOk;
|
|
508
|
+
const hasCritical = !results.configValid ||
|
|
509
|
+
providerIssuesCritical ||
|
|
510
|
+
results.delivery.some((d) => !d.ready);
|
|
511
|
+
const hasWarning = results.suggestions.length > 0 ||
|
|
512
|
+
results.providers.some((p) => !p.healthy) ||
|
|
513
|
+
results.jobs.some((j) => !j.healthy) ||
|
|
514
|
+
results.lanes.some((l) => !l.ready) ||
|
|
515
|
+
results.daemon.pids.length > 1 ||
|
|
516
|
+
results.daemon.stalePidFile;
|
|
517
|
+
|
|
518
|
+
if (hasCritical) {
|
|
519
|
+
results.exitCode = 2;
|
|
520
|
+
} else if (hasWarning) {
|
|
521
|
+
results.exitCode = 1;
|
|
522
|
+
} else {
|
|
523
|
+
results.exitCode = 0;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// Clean up internal fields
|
|
527
|
+
delete results._config;
|
|
528
|
+
|
|
529
|
+
return results;
|
|
530
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
export const PHASES = ["detect", "choose", "build", "validate", "verify"];
|
|
5
|
+
|
|
6
|
+
const LOCK_FILENAME = "nemoris.lock";
|
|
7
|
+
const OLD_PHASE_NAMES = ["scaffold", "identity", "auth", "hatch"];
|
|
8
|
+
|
|
9
|
+
function lockPath(installDir) {
|
|
10
|
+
return path.join(installDir, LOCK_FILENAME);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function writeLock(installDir, state) {
|
|
14
|
+
fs.writeFileSync(lockPath(installDir), JSON.stringify(state, null, 2), "utf8");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function readLock(installDir) {
|
|
18
|
+
const p = lockPath(installDir);
|
|
19
|
+
try {
|
|
20
|
+
const raw = fs.readFileSync(p, "utf8");
|
|
21
|
+
const parsed = JSON.parse(raw);
|
|
22
|
+
const completed = parsed.completed || [];
|
|
23
|
+
const isOldFormat = completed.some((name) => OLD_PHASE_NAMES.includes(name));
|
|
24
|
+
if (isOldFormat) {
|
|
25
|
+
console.log(" Previous partial install detected — restarting with new wizard.");
|
|
26
|
+
try { fs.unlinkSync(p); } catch {}
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
return parsed;
|
|
30
|
+
} catch {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function deleteLock(installDir) {
|
|
36
|
+
const p = lockPath(installDir);
|
|
37
|
+
try {
|
|
38
|
+
fs.unlinkSync(p);
|
|
39
|
+
} catch {
|
|
40
|
+
// ignore if already gone
|
|
41
|
+
}
|
|
42
|
+
}
|