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,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
|
+
await prompter.intro("Nemoris setup");
|
|
168
|
+
|
|
169
|
+
await prompter.note([
|
|
170
|
+
"Security warning — please read.",
|
|
171
|
+
"",
|
|
172
|
+
"Nemoris runs as a background daemon on your machine.",
|
|
173
|
+
"It can execute shell commands, read files, and make network requests — always acting on your instructions, never autonomously.",
|
|
174
|
+
"",
|
|
175
|
+
"This is open-source software. Review the code at:",
|
|
176
|
+
"https://github.com/amzer24/nemoris",
|
|
177
|
+
"",
|
|
178
|
+
"Run as a personal agent — one trusted operator boundary.",
|
|
179
|
+
"Do not expose to the internet without hardening first.",
|
|
180
|
+
].join("\n"), "Security");
|
|
181
|
+
|
|
182
|
+
const proceed = await prompter.confirm({
|
|
183
|
+
message: "I understand. Continue?",
|
|
184
|
+
initialValue: false,
|
|
185
|
+
});
|
|
186
|
+
if (!proceed) {
|
|
187
|
+
await prompter.cancel("Cancelled.");
|
|
188
|
+
return 0;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (fs.existsSync(path.join(installDir, "config", "runtime.toml"))) {
|
|
192
|
+
await prompter.note(summarizeExistingConfig(installDir), "Existing config detected");
|
|
193
|
+
const action = await prompter.select({
|
|
194
|
+
message: "Config handling",
|
|
195
|
+
options: [
|
|
196
|
+
{ value: "keep", label: "Use existing values" },
|
|
197
|
+
{ value: "update", label: "Update values" },
|
|
198
|
+
{ value: "reset", label: "Reset everything" },
|
|
199
|
+
],
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
if (action === "keep") {
|
|
203
|
+
await prompter.outro("Config unchanged. Run nemoris status to check what's running.");
|
|
204
|
+
return 0;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (action === "reset") {
|
|
208
|
+
const scope = await prompter.select({
|
|
209
|
+
message: "Reset scope",
|
|
210
|
+
options: [
|
|
211
|
+
{ value: "config", label: "Config only" },
|
|
212
|
+
{ value: "config+state", label: "Config + state (memory, runs, scheduler)" },
|
|
213
|
+
{ value: "full", label: "Full reset (everything)" },
|
|
214
|
+
],
|
|
215
|
+
});
|
|
216
|
+
resetInstallArtifacts(installDir, scope);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const flow = flowOverride || await prompter.select({
|
|
221
|
+
message: "Setup mode",
|
|
222
|
+
options: [
|
|
223
|
+
{ value: "quickstart", label: "QuickStart", hint: "Running in 2 minutes, configure details later with: nemoris setup" },
|
|
224
|
+
{ value: "manual", label: "Manual", hint: "Full configuration now" },
|
|
225
|
+
],
|
|
226
|
+
initialValue: "quickstart",
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
if (flow === "quickstart") {
|
|
230
|
+
await prompter.note([
|
|
231
|
+
"QuickStart defaults:",
|
|
232
|
+
`State directory: ${installDir}`,
|
|
233
|
+
"Polling: long-poll (no tunnel needed)",
|
|
234
|
+
"Daemon: auto-installed",
|
|
235
|
+
].join("\n"), "QuickStart");
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (process.platform === "win32") {
|
|
239
|
+
await prompter.note([
|
|
240
|
+
"Windows detected — Nemoris runs best on WSL2.",
|
|
241
|
+
"Native Windows support is experimental.",
|
|
242
|
+
"Quick setup: wsl --install (one command, one reboot)",
|
|
243
|
+
"Guide: https://github.com/amzer24/nemoris#windows",
|
|
244
|
+
].join("\n"), "Windows");
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const detection = await detect(installDir);
|
|
248
|
+
await scaffold({ installDir });
|
|
249
|
+
|
|
250
|
+
const userName = await prompter.text({
|
|
251
|
+
message: "Your name",
|
|
252
|
+
initialValue: process.env.NEMORIS_USER_NAME || process.env.USER || "",
|
|
253
|
+
placeholder: os.userInfo().username || "",
|
|
254
|
+
});
|
|
255
|
+
const defaultAgentName = process.env.NEMORIS_AGENT_NAME || resolveDefaultAgentName();
|
|
256
|
+
const agentName = await prompter.text({
|
|
257
|
+
message: "What should your agent be called?",
|
|
258
|
+
initialValue: defaultAgentName,
|
|
259
|
+
validate: (value) => String(value || "").trim() ? undefined : "Agent name is required.",
|
|
260
|
+
});
|
|
261
|
+
const agentId = String(agentName).toLowerCase().replace(/[^a-z0-9-]/g, "-");
|
|
262
|
+
writeIdentity({
|
|
263
|
+
installDir,
|
|
264
|
+
userName: userName || process.env.USER || "operator",
|
|
265
|
+
agentName,
|
|
266
|
+
agentId,
|
|
267
|
+
userGoal: "build software",
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
const provider = await prompter.select({
|
|
271
|
+
message: "Choose your AI provider",
|
|
272
|
+
options: detectProviderOptions({
|
|
273
|
+
env: process.env,
|
|
274
|
+
ollamaResult: detection.ollama ? { ok: true, models: detection.ollama.models } : { ok: false, models: [] },
|
|
275
|
+
}).map((option) => ({
|
|
276
|
+
value: option.value,
|
|
277
|
+
label: option.label,
|
|
278
|
+
hint: option.hint,
|
|
279
|
+
})),
|
|
280
|
+
initialValue: detectPreferredProvider({
|
|
281
|
+
env: process.env,
|
|
282
|
+
ollamaResult: detection.ollama ? { ok: true } : { ok: false },
|
|
283
|
+
}),
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
let authResult = { providers: [], providerFlags: {}, selectedModels: {} };
|
|
287
|
+
let telegramResult = { configured: false, verified: false };
|
|
288
|
+
let ollamaResult = { configured: false, verified: false, models: [] };
|
|
289
|
+
|
|
290
|
+
if (provider !== "skip" && provider !== "ollama") {
|
|
291
|
+
const authSpin = prompter.spinner();
|
|
292
|
+
authSpin.start("Configuring provider auth...");
|
|
293
|
+
authResult = await runAuthPhase(installDir, {
|
|
294
|
+
tui: createLegacyPromptAdapter(prompter),
|
|
295
|
+
detectionCache: {
|
|
296
|
+
rawKeys: detection.apiKeys,
|
|
297
|
+
ollamaResult: detection.ollama ? { ok: true, models: detection.ollama.models } : { ok: false, models: [] },
|
|
298
|
+
},
|
|
299
|
+
providerOrder: [provider],
|
|
300
|
+
enableOpenAIOAuthChoice: provider === "openai",
|
|
301
|
+
});
|
|
302
|
+
authSpin.stop("Provider configured");
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const connectTelegram = await prompter.confirm({
|
|
306
|
+
message: "Connect via Telegram?",
|
|
307
|
+
initialValue: true,
|
|
308
|
+
});
|
|
309
|
+
if (connectTelegram) {
|
|
310
|
+
telegramResult = await runTelegramPhase({ installDir, agentId, nonInteractive: false, skipGate: true });
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const useOllama = provider === "ollama" || await prompter.confirm({
|
|
314
|
+
message: "Add Ollama local models too?",
|
|
315
|
+
initialValue: flow === "quickstart",
|
|
316
|
+
});
|
|
317
|
+
if (useOllama) {
|
|
318
|
+
ollamaResult = await runOllamaPhase({ installDir, nonInteractive: false });
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const installDaemon = flow === "quickstart" || await prompter.confirm({
|
|
322
|
+
message: "Install background service (recommended)",
|
|
323
|
+
initialValue: true,
|
|
324
|
+
});
|
|
325
|
+
let daemonHealthy = false;
|
|
326
|
+
|
|
327
|
+
if (installDaemon) {
|
|
328
|
+
const spin = prompter.spinner();
|
|
329
|
+
spin.start("Installing daemon...");
|
|
330
|
+
await writeDaemonUnit(installDir);
|
|
331
|
+
const loaded = await loadDaemon(installDir);
|
|
332
|
+
spin.stop(loaded.ok ? "Daemon installed" : `Daemon install failed: ${loaded.message || "check logs with nemoris logs"}`);
|
|
333
|
+
daemonHealthy = loaded.ok ? await waitForDaemonHealthy({ timeoutMs: 15000 }) : false;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
if (!daemonHealthy) {
|
|
337
|
+
await prompter.note("Run `nemoris start` to start it manually.\nDocs: https://github.com/amzer24/nemoris#troubleshooting", "Daemon");
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
if (daemonHealthy) {
|
|
341
|
+
const hatch = await prompter.select({
|
|
342
|
+
message: "How do you want to start?",
|
|
343
|
+
options: [
|
|
344
|
+
{ value: "chat", label: "Open TUI chat (recommended)" },
|
|
345
|
+
{ value: "later", label: "Do this later" },
|
|
346
|
+
],
|
|
347
|
+
initialValue: "chat",
|
|
348
|
+
});
|
|
349
|
+
if (hatch === "chat") {
|
|
350
|
+
await launchChat();
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const installCompletion = flow === "quickstart" || await prompter.confirm({
|
|
355
|
+
message: "Enable shell completion?",
|
|
356
|
+
initialValue: true,
|
|
357
|
+
});
|
|
358
|
+
if (installCompletion) {
|
|
359
|
+
installShellCompletion();
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
const modelSummary = summarizeSelectedModels([
|
|
363
|
+
...(authResult.selectedModels?.[provider] || []),
|
|
364
|
+
...(ollamaResult.models || []).map((model) => `ollama/${model}`),
|
|
365
|
+
]);
|
|
366
|
+
const authMethod = provider === "ollama"
|
|
367
|
+
? "local"
|
|
368
|
+
: authResult.providers.length > 0
|
|
369
|
+
? "api_key"
|
|
370
|
+
: "skipped";
|
|
371
|
+
|
|
372
|
+
await prompter.note([
|
|
373
|
+
"Auth overview:",
|
|
374
|
+
` ${provider} ${provider === "skip" ? "skip" : "✓"} ${authMethod}`,
|
|
375
|
+
"",
|
|
376
|
+
"Models:",
|
|
377
|
+
` Default : ${modelSummary.defaultModel || "not configured"}`,
|
|
378
|
+
` Fallback : ${modelSummary.fallbackModel || "not configured"}`,
|
|
379
|
+
"",
|
|
380
|
+
"Docs: https://github.com/amzer24/nemoris",
|
|
381
|
+
"Issues: https://github.com/amzer24/nemoris/issues",
|
|
382
|
+
].join("\n"), "Ready");
|
|
383
|
+
|
|
384
|
+
await prompter.outro(
|
|
385
|
+
telegramResult.verified
|
|
386
|
+
? "Nemoris is running. Message your bot to get started."
|
|
387
|
+
: "Nemoris is running. Start with: nemoris chat"
|
|
388
|
+
);
|
|
389
|
+
return 0;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
async function runNonInteractiveWizard({
|
|
393
|
+
installDir,
|
|
394
|
+
acceptRisk = false,
|
|
395
|
+
flow = "quickstart",
|
|
396
|
+
anthropicKey = null,
|
|
397
|
+
openaiKey = null,
|
|
398
|
+
openrouterKey = null,
|
|
399
|
+
}) {
|
|
400
|
+
if (!acceptRisk) {
|
|
401
|
+
console.error("Non-interactive setup requires --accept-risk.");
|
|
402
|
+
return 1;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
const detectionCache = await detect(installDir);
|
|
406
|
+
detectionCache.apiKeys = {
|
|
407
|
+
...detectionCache.apiKeys,
|
|
408
|
+
anthropic: anthropicKey || detectionCache.apiKeys?.anthropic || null,
|
|
409
|
+
openai: openaiKey || detectionCache.apiKeys?.openai || null,
|
|
410
|
+
openrouter: openrouterKey || detectionCache.apiKeys?.openrouter || null,
|
|
411
|
+
};
|
|
412
|
+
|
|
413
|
+
const { mode } = await choose({ detectionCache, nonInteractive: true });
|
|
414
|
+
const userName = process.env.NEMORIS_USER_NAME || process.env.USER || "operator";
|
|
415
|
+
|
|
416
|
+
let buildResult;
|
|
417
|
+
if (mode === "shadow") {
|
|
418
|
+
buildResult = await buildShadow({
|
|
419
|
+
installDir,
|
|
420
|
+
userName,
|
|
421
|
+
detectionCache,
|
|
422
|
+
nonInteractive: true,
|
|
423
|
+
});
|
|
424
|
+
} else {
|
|
425
|
+
const requestedAgentName = String(process.env.NEMORIS_AGENT_NAME || "").trim() || "Nemo";
|
|
426
|
+
buildResult = await buildFresh({
|
|
427
|
+
installDir,
|
|
428
|
+
agentName: requestedAgentName,
|
|
429
|
+
userName,
|
|
430
|
+
userGoal: "build software",
|
|
431
|
+
detectionCache,
|
|
432
|
+
nonInteractive: true,
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
const result = await verify({
|
|
437
|
+
installDir,
|
|
438
|
+
agentName: buildResult.agentName,
|
|
439
|
+
userName: buildResult.userName,
|
|
440
|
+
agentId: buildResult.agentId,
|
|
441
|
+
mode,
|
|
442
|
+
providers: buildResult.providers || [],
|
|
443
|
+
providerFlags: buildResult.providerFlags || {},
|
|
444
|
+
nonInteractive: true,
|
|
445
|
+
skipHealthcheck: process.env.NEMORIS_SKIP_HEALTHCHECK === "true",
|
|
446
|
+
telegramConfigured: buildResult.telegramConfigured,
|
|
447
|
+
telegramVerified: buildResult.telegramVerified,
|
|
448
|
+
telegramBotUsername: buildResult.telegramBotUsername,
|
|
449
|
+
telegramBotToken: buildResult.botToken,
|
|
450
|
+
telegramOperatorChatId: buildResult.operatorChatId,
|
|
451
|
+
userGoal: buildResult.userGoal,
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
if (result.status === "warning") {
|
|
455
|
+
return 1;
|
|
456
|
+
}
|
|
457
|
+
if (flow !== "quickstart" && buildResult.ollamaConfigured) {
|
|
458
|
+
// Keep manual mode deterministic in CI while preserving quickstart defaults.
|
|
459
|
+
return 0;
|
|
460
|
+
}
|
|
461
|
+
return 0;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
export async function runWizard({
|
|
465
|
+
installDir,
|
|
466
|
+
nonInteractive = false,
|
|
467
|
+
acceptRisk = false,
|
|
468
|
+
flow = null,
|
|
469
|
+
anthropicKey = null,
|
|
470
|
+
openaiKey = null,
|
|
471
|
+
openrouterKey = null,
|
|
472
|
+
}) {
|
|
473
|
+
if (!checkNodeVersion()) {
|
|
474
|
+
console.error(`Node.js v${MIN_NODE_MAJOR}+ is required.`);
|
|
475
|
+
return 1;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
try {
|
|
479
|
+
return nonInteractive
|
|
480
|
+
? await runNonInteractiveWizard({
|
|
481
|
+
installDir,
|
|
482
|
+
acceptRisk,
|
|
483
|
+
flow: flow || "quickstart",
|
|
484
|
+
anthropicKey,
|
|
485
|
+
openaiKey,
|
|
486
|
+
openrouterKey,
|
|
487
|
+
})
|
|
488
|
+
: await runInteractiveWizard({
|
|
489
|
+
installDir,
|
|
490
|
+
flowOverride: flow,
|
|
491
|
+
});
|
|
492
|
+
} catch (error) {
|
|
493
|
+
if (error instanceof SetupCancelledError) {
|
|
494
|
+
return 0;
|
|
495
|
+
}
|
|
496
|
+
console.error(`Setup failed: ${error.message}`);
|
|
497
|
+
return 1;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { ProviderAdapter } from "./base.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Build correct auth headers for Anthropic API.
|
|
5
|
+
* OAuth/setup-tokens (sk-ant-oat-*) use Authorization: Bearer.
|
|
6
|
+
* API keys (sk-ant-api-* or sk-ant-*) use x-api-key.
|
|
7
|
+
*/
|
|
8
|
+
export function buildAnthropicAuthHeaders(token) {
|
|
9
|
+
if (token && token.startsWith("sk-ant-oat")) {
|
|
10
|
+
return { "Authorization": `Bearer ${token}`, "anthropic-version": "2023-06-01" };
|
|
11
|
+
}
|
|
12
|
+
return { "x-api-key": token, "anthropic-version": "2023-06-01" };
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export class AnthropicAdapter extends ProviderAdapter {
|
|
16
|
+
static providerId = "anthropic";
|
|
17
|
+
|
|
18
|
+
normalizeModelId(model) {
|
|
19
|
+
return typeof model === "string" && model.startsWith("anthropic/")
|
|
20
|
+
? model.slice("anthropic/".length)
|
|
21
|
+
: model;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
getCapabilities() {
|
|
25
|
+
return {
|
|
26
|
+
supportsToolDefinitions: true,
|
|
27
|
+
supportsToolCalls: true,
|
|
28
|
+
structuredOutputMode: "forced_tool",
|
|
29
|
+
supportsReasoningSchema: true,
|
|
30
|
+
tokenCountingMode: "exact",
|
|
31
|
+
toolReliabilityTier: "high",
|
|
32
|
+
supportsVision: true
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
buildStructuredOutputTool(input) {
|
|
37
|
+
if (!input?.responseSchema?.schema) return null;
|
|
38
|
+
return {
|
|
39
|
+
name: input.responseSchema.name || "record_structured_output",
|
|
40
|
+
description: "Return the final structured job result using the required schema.",
|
|
41
|
+
input_schema: input.responseSchema.schema
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
buildInvokePayload(input) {
|
|
46
|
+
const { model: rawModel, system, messages, maxTokens = 2048, tools } = input;
|
|
47
|
+
const model = this.normalizeModelId(rawModel);
|
|
48
|
+
const structuredOutputTool = this.buildStructuredOutputTool(input);
|
|
49
|
+
const combinedTools = [...(tools || [])];
|
|
50
|
+
if (structuredOutputTool) {
|
|
51
|
+
combinedTools.push(structuredOutputTool);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const payload = {
|
|
55
|
+
model,
|
|
56
|
+
max_tokens: maxTokens,
|
|
57
|
+
messages
|
|
58
|
+
};
|
|
59
|
+
if (system) {
|
|
60
|
+
payload.system = [{
|
|
61
|
+
type: "text",
|
|
62
|
+
text: typeof system === "string" ? system : JSON.stringify(system),
|
|
63
|
+
cache_control: { type: "ephemeral" }
|
|
64
|
+
}];
|
|
65
|
+
}
|
|
66
|
+
if (combinedTools.length) payload.tools = combinedTools;
|
|
67
|
+
if (structuredOutputTool) {
|
|
68
|
+
payload.tool_choice = {
|
|
69
|
+
type: "tool",
|
|
70
|
+
name: structuredOutputTool.name
|
|
71
|
+
};
|
|
72
|
+
payload.disable_parallel_tool_use = true;
|
|
73
|
+
}
|
|
74
|
+
return payload;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async invoke(input) {
|
|
78
|
+
const token = this.ensureAuthToken();
|
|
79
|
+
return this.postJson(
|
|
80
|
+
"v1/messages",
|
|
81
|
+
this.buildInvokePayload(input),
|
|
82
|
+
buildAnthropicAuthHeaders(token),
|
|
83
|
+
{
|
|
84
|
+
timeoutMs: input.timeoutMs
|
|
85
|
+
}
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
normalizeResponse(raw) {
|
|
90
|
+
const toolUseBlock =
|
|
91
|
+
raw?.content?.find((item) => item?.type === "tool_use" && item.input && typeof item.input === "object") || null;
|
|
92
|
+
if (toolUseBlock?.input) {
|
|
93
|
+
return {
|
|
94
|
+
summary: toolUseBlock.input.summary || "Provider returned structured tool output.",
|
|
95
|
+
output: toolUseBlock.input.output ?? toolUseBlock.input,
|
|
96
|
+
nextActions: Array.isArray(toolUseBlock.input.nextActions) ? toolUseBlock.input.nextActions : [],
|
|
97
|
+
reasoning: toolUseBlock.input.analysis || null,
|
|
98
|
+
raw
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return super.normalizeResponse(raw);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
buildCountTokensPayload(input) {
|
|
106
|
+
const { model: rawModel, system, messages, tools } = input;
|
|
107
|
+
const model = this.normalizeModelId(rawModel);
|
|
108
|
+
const structuredOutputTool = this.buildStructuredOutputTool(input);
|
|
109
|
+
const combinedTools = [...(tools || [])];
|
|
110
|
+
if (structuredOutputTool) {
|
|
111
|
+
combinedTools.push(structuredOutputTool);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const payload = {
|
|
115
|
+
model,
|
|
116
|
+
messages
|
|
117
|
+
};
|
|
118
|
+
if (system) {
|
|
119
|
+
payload.system = [{
|
|
120
|
+
type: "text",
|
|
121
|
+
text: typeof system === "string" ? system : JSON.stringify(system),
|
|
122
|
+
cache_control: { type: "ephemeral" }
|
|
123
|
+
}];
|
|
124
|
+
}
|
|
125
|
+
if (combinedTools.length) payload.tools = combinedTools;
|
|
126
|
+
if (structuredOutputTool) {
|
|
127
|
+
payload.tool_choice = {
|
|
128
|
+
type: "tool",
|
|
129
|
+
name: structuredOutputTool.name
|
|
130
|
+
};
|
|
131
|
+
payload.disable_parallel_tool_use = true;
|
|
132
|
+
}
|
|
133
|
+
return payload;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async countTokens(input) {
|
|
137
|
+
const token = this.ensureAuthToken();
|
|
138
|
+
const result = await this.postJson(
|
|
139
|
+
"v1/messages/count_tokens",
|
|
140
|
+
this.buildCountTokensPayload(input),
|
|
141
|
+
buildAnthropicAuthHeaders(token),
|
|
142
|
+
{
|
|
143
|
+
timeoutMs: input.timeoutMs
|
|
144
|
+
}
|
|
145
|
+
);
|
|
146
|
+
return Number(result?.input_tokens ?? result?.inputTokens ?? 0);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async healthCheck() {
|
|
150
|
+
const token = this.ensureAuthToken();
|
|
151
|
+
// OAuth tokens (sk-ant-oat*) don't have access to GET /v1/models.
|
|
152
|
+
// Use a minimal messages.count_tokens probe instead — works with all token types.
|
|
153
|
+
// The configured baseUrl still goes through the shared SSRF policy.
|
|
154
|
+
try {
|
|
155
|
+
const response = await this.fetchResponse("v1/messages/count_tokens", {
|
|
156
|
+
method: "POST",
|
|
157
|
+
headers: { "content-type": "application/json", ...buildAnthropicAuthHeaders(token) },
|
|
158
|
+
body: JSON.stringify({
|
|
159
|
+
model: "claude-haiku-4-5",
|
|
160
|
+
messages: [{ role: "user", content: "ping" }]
|
|
161
|
+
})
|
|
162
|
+
});
|
|
163
|
+
return { ok: response.ok, status: response.status };
|
|
164
|
+
} catch {
|
|
165
|
+
return { ok: false, status: 0 };
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|