nemoris 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (248) hide show
  1. package/.env.example +49 -49
  2. package/LICENSE +21 -21
  3. package/README.md +209 -209
  4. package/SECURITY.md +59 -119
  5. package/bin/nemoris +46 -46
  6. package/config/agents/agent.toml.example +28 -28
  7. package/config/agents/content.toml +23 -0
  8. package/config/agents/default.toml +22 -22
  9. package/config/agents/heartbeat.toml +35 -0
  10. package/config/agents/iris.toml +23 -0
  11. package/config/agents/lab.toml +23 -0
  12. package/config/agents/main.toml +45 -0
  13. package/config/agents/nemo.toml +21 -0
  14. package/config/agents/ops.toml +38 -0
  15. package/config/agents/orchestrator.toml +18 -18
  16. package/config/agents/revenue.toml +23 -0
  17. package/config/agents/testyboo.toml +19 -0
  18. package/config/delivery.toml +73 -73
  19. package/config/embeddings.toml +5 -5
  20. package/config/identity/content-purpose.md +11 -0
  21. package/config/identity/content-soul.md +45 -0
  22. package/config/identity/default-purpose.md +1 -1
  23. package/config/identity/default-soul.md +3 -3
  24. package/config/identity/heartbeat-purpose.md +9 -0
  25. package/config/identity/heartbeat-soul.md +16 -0
  26. package/config/identity/iris-purpose.md +17 -0
  27. package/config/identity/iris-soul.md +68 -0
  28. package/config/identity/lab-purpose.md +10 -0
  29. package/config/identity/lab-soul.md +38 -0
  30. package/config/identity/main-purpose.md +17 -0
  31. package/config/identity/main-soul.md +66 -0
  32. package/config/identity/main-user.md +22 -0
  33. package/config/identity/ops-purpose.md +9 -0
  34. package/config/identity/ops-soul.md +16 -0
  35. package/config/identity/orchestrator-purpose.md +1 -1
  36. package/config/identity/orchestrator-soul.md +1 -1
  37. package/config/identity/revenue-purpose.md +9 -0
  38. package/config/identity/revenue-soul.md +41 -0
  39. package/config/identity/testyboo-purpose.md +13 -0
  40. package/config/identity/testyboo-soul.md +20 -0
  41. package/config/improvement-targets.toml +15 -15
  42. package/config/jobs/heartbeat-check.toml +30 -30
  43. package/config/jobs/memory-rollup.toml +46 -46
  44. package/config/jobs/workspace-health.toml +63 -63
  45. package/config/mcp.toml +16 -16
  46. package/config/output-contracts.toml +17 -17
  47. package/config/peers.toml +32 -32
  48. package/config/peers.toml.example +32 -32
  49. package/config/policies/memory-default.toml +10 -10
  50. package/config/policies/memory-heartbeat.toml +5 -5
  51. package/config/policies/memory-ops.toml +10 -10
  52. package/config/policies/tools-heartbeat-minimal.toml +8 -8
  53. package/config/policies/tools-interactive-safe.toml +8 -8
  54. package/config/policies/tools-ops-bounded.toml +8 -8
  55. package/config/policies/tools-orchestrator.toml +7 -7
  56. package/config/providers/anthropic.toml +15 -15
  57. package/config/providers/ollama.toml +5 -5
  58. package/config/providers/openai-codex.toml +9 -9
  59. package/config/providers/openrouter.toml +5 -5
  60. package/config/router.toml +22 -22
  61. package/config/runtime.toml +114 -114
  62. package/config/skills/self-improvement.toml +15 -15
  63. package/config/skills/telegram-onboarding-spec.md +240 -240
  64. package/config/skills/workspace-monitor.toml +15 -15
  65. package/config/task-router.toml +42 -42
  66. package/install.sh +50 -50
  67. package/package.json +91 -90
  68. package/src/auth/auth-profiles.js +169 -169
  69. package/src/auth/openai-codex-oauth.js +285 -285
  70. package/src/battle.js +449 -449
  71. package/src/cli/help.js +265 -265
  72. package/src/cli/output-filter.js +49 -49
  73. package/src/cli/runtime-control.js +704 -704
  74. package/src/cli-main.js +2763 -2763
  75. package/src/cli.js +78 -78
  76. package/src/config/loader.js +332 -332
  77. package/src/config/schema-validator.js +214 -214
  78. package/src/config/toml-lite.js +8 -8
  79. package/src/daemon/action-handlers.js +71 -71
  80. package/src/daemon/healing-tick.js +87 -87
  81. package/src/daemon/health-probes.js +90 -90
  82. package/src/daemon/notifier.js +57 -57
  83. package/src/daemon/nurse.js +218 -218
  84. package/src/daemon/repair-log.js +106 -106
  85. package/src/daemon/rule-staging.js +90 -90
  86. package/src/daemon/rules.js +29 -29
  87. package/src/daemon/telegram-commands.js +54 -54
  88. package/src/daemon/updater.js +85 -85
  89. package/src/jobs/job-runner.js +78 -78
  90. package/src/mcp/consumer.js +129 -129
  91. package/src/memory/active-recall.js +171 -171
  92. package/src/memory/backend-manager.js +97 -97
  93. package/src/memory/backends/file-backend.js +38 -38
  94. package/src/memory/backends/qmd-backend.js +219 -219
  95. package/src/memory/embedding-guards.js +24 -24
  96. package/src/memory/embedding-index.js +118 -118
  97. package/src/memory/embedding-service.js +179 -179
  98. package/src/memory/file-index.js +177 -177
  99. package/src/memory/memory-signature.js +5 -5
  100. package/src/memory/memory-store.js +648 -648
  101. package/src/memory/retrieval-planner.js +66 -66
  102. package/src/memory/scoring.js +145 -145
  103. package/src/memory/simhash.js +78 -78
  104. package/src/memory/sqlite-active-store.js +824 -824
  105. package/src/memory/write-policy.js +36 -36
  106. package/src/onboarding/aliases.js +33 -33
  107. package/src/onboarding/auth/api-key.js +224 -224
  108. package/src/onboarding/auth/ollama-detect.js +42 -42
  109. package/src/onboarding/clack-prompter.js +77 -77
  110. package/src/onboarding/doctor.js +530 -530
  111. package/src/onboarding/lock.js +42 -42
  112. package/src/onboarding/model-catalog.js +344 -344
  113. package/src/onboarding/phases/auth.js +576 -589
  114. package/src/onboarding/phases/build.js +130 -130
  115. package/src/onboarding/phases/choose.js +82 -82
  116. package/src/onboarding/phases/detect.js +98 -98
  117. package/src/onboarding/phases/hatch.js +216 -216
  118. package/src/onboarding/phases/identity.js +79 -79
  119. package/src/onboarding/phases/ollama.js +345 -345
  120. package/src/onboarding/phases/scaffold.js +99 -99
  121. package/src/onboarding/phases/telegram.js +377 -377
  122. package/src/onboarding/phases/validate.js +204 -204
  123. package/src/onboarding/phases/verify.js +206 -206
  124. package/src/onboarding/platform.js +482 -482
  125. package/src/onboarding/status-bar.js +95 -95
  126. package/src/onboarding/templates.js +794 -794
  127. package/src/onboarding/toml-writer.js +38 -38
  128. package/src/onboarding/tui.js +250 -250
  129. package/src/onboarding/uninstall.js +153 -153
  130. package/src/onboarding/wizard.js +516 -499
  131. package/src/providers/anthropic.js +168 -168
  132. package/src/providers/base.js +247 -247
  133. package/src/providers/circuit-breaker.js +136 -136
  134. package/src/providers/ollama.js +163 -163
  135. package/src/providers/openai-codex.js +149 -149
  136. package/src/providers/openrouter.js +136 -136
  137. package/src/providers/registry.js +36 -36
  138. package/src/providers/router.js +16 -16
  139. package/src/runtime/bootstrap-cache.js +47 -47
  140. package/src/runtime/capabilities-prompt.js +25 -25
  141. package/src/runtime/completion-ping.js +99 -99
  142. package/src/runtime/config-validator.js +121 -121
  143. package/src/runtime/context-ledger.js +360 -360
  144. package/src/runtime/cutover-readiness.js +42 -42
  145. package/src/runtime/daemon.js +729 -729
  146. package/src/runtime/delivery-ack.js +195 -195
  147. package/src/runtime/delivery-adapters/local-file.js +41 -41
  148. package/src/runtime/delivery-adapters/openclaw-cli.js +94 -94
  149. package/src/runtime/delivery-adapters/openclaw-peer.js +98 -98
  150. package/src/runtime/delivery-adapters/shadow.js +13 -13
  151. package/src/runtime/delivery-adapters/standalone-http.js +98 -98
  152. package/src/runtime/delivery-adapters/telegram.js +104 -104
  153. package/src/runtime/delivery-adapters/tui.js +128 -128
  154. package/src/runtime/delivery-manager.js +807 -807
  155. package/src/runtime/delivery-store.js +168 -168
  156. package/src/runtime/dependency-health.js +118 -118
  157. package/src/runtime/envelope.js +114 -114
  158. package/src/runtime/evaluation.js +1089 -1089
  159. package/src/runtime/exec-approvals.js +216 -216
  160. package/src/runtime/executor.js +500 -500
  161. package/src/runtime/failure-ping.js +67 -67
  162. package/src/runtime/flows.js +83 -83
  163. package/src/runtime/guards.js +45 -45
  164. package/src/runtime/handoff.js +51 -51
  165. package/src/runtime/identity-cache.js +28 -28
  166. package/src/runtime/improvement-engine.js +109 -109
  167. package/src/runtime/improvement-harness.js +581 -581
  168. package/src/runtime/input-sanitiser.js +72 -72
  169. package/src/runtime/interaction-contract.js +347 -347
  170. package/src/runtime/lane-readiness.js +226 -226
  171. package/src/runtime/migration.js +323 -323
  172. package/src/runtime/model-resolution.js +78 -78
  173. package/src/runtime/network.js +64 -64
  174. package/src/runtime/notification-store.js +97 -97
  175. package/src/runtime/notifier.js +256 -256
  176. package/src/runtime/orchestrator.js +53 -53
  177. package/src/runtime/orphan-reaper.js +41 -41
  178. package/src/runtime/output-contract-schema.js +139 -139
  179. package/src/runtime/output-contract-validator.js +439 -439
  180. package/src/runtime/peer-readiness.js +69 -69
  181. package/src/runtime/peer-registry.js +133 -133
  182. package/src/runtime/pilot-status.js +108 -108
  183. package/src/runtime/prompt-builder.js +261 -261
  184. package/src/runtime/provider-attempt.js +582 -582
  185. package/src/runtime/report-fallback.js +71 -71
  186. package/src/runtime/result-normalizer.js +183 -183
  187. package/src/runtime/retention.js +74 -74
  188. package/src/runtime/review.js +244 -244
  189. package/src/runtime/route-job.js +15 -15
  190. package/src/runtime/run-store.js +38 -38
  191. package/src/runtime/schedule.js +88 -88
  192. package/src/runtime/scheduler-state.js +434 -434
  193. package/src/runtime/scheduler.js +656 -656
  194. package/src/runtime/session-compactor.js +182 -182
  195. package/src/runtime/session-search.js +155 -155
  196. package/src/runtime/slack-inbound.js +249 -249
  197. package/src/runtime/ssrf.js +102 -102
  198. package/src/runtime/status-aggregator.js +330 -330
  199. package/src/runtime/task-contract.js +140 -140
  200. package/src/runtime/task-packet.js +107 -107
  201. package/src/runtime/task-router.js +140 -140
  202. package/src/runtime/telegram-inbound.js +1565 -1565
  203. package/src/runtime/token-counter.js +134 -134
  204. package/src/runtime/token-estimator.js +59 -59
  205. package/src/runtime/tool-loop.js +200 -200
  206. package/src/runtime/transport-server.js +311 -311
  207. package/src/runtime/tui-server.js +411 -411
  208. package/src/runtime/ulid.js +44 -44
  209. package/src/security/ssrf-check.js +197 -197
  210. package/src/setup.js +369 -369
  211. package/src/shadow/bridge.js +303 -303
  212. package/src/skills/loader.js +84 -84
  213. package/src/tools/catalog.json +49 -49
  214. package/src/tools/cli-delegate.js +44 -44
  215. package/src/tools/mcp-client.js +106 -106
  216. package/src/tools/micro/cancel-task.js +6 -6
  217. package/src/tools/micro/complete-task.js +6 -6
  218. package/src/tools/micro/fail-task.js +6 -6
  219. package/src/tools/micro/http-fetch.js +74 -74
  220. package/src/tools/micro/index.js +36 -36
  221. package/src/tools/micro/lcm-recall.js +60 -60
  222. package/src/tools/micro/list-dir.js +17 -17
  223. package/src/tools/micro/list-skills.js +46 -46
  224. package/src/tools/micro/load-skill.js +38 -38
  225. package/src/tools/micro/memory-search.js +45 -45
  226. package/src/tools/micro/read-file.js +11 -11
  227. package/src/tools/micro/session-search.js +54 -54
  228. package/src/tools/micro/shell-exec.js +43 -43
  229. package/src/tools/micro/trigger-job.js +79 -79
  230. package/src/tools/micro/web-search.js +58 -58
  231. package/src/tools/micro/workspace-paths.js +39 -39
  232. package/src/tools/micro/write-file.js +14 -14
  233. package/src/tools/micro/write-memory.js +41 -41
  234. package/src/tools/registry.js +348 -348
  235. package/src/tools/tool-result-contract.js +36 -36
  236. package/src/tui/chat.js +835 -835
  237. package/src/tui/renderer.js +175 -175
  238. package/src/tui/socket-client.js +217 -217
  239. package/src/utils/canonical-json.js +29 -29
  240. package/src/utils/compaction.js +30 -30
  241. package/src/utils/env-loader.js +5 -5
  242. package/src/utils/errors.js +80 -80
  243. package/src/utils/fs.js +101 -101
  244. package/src/utils/ids.js +5 -5
  245. package/src/utils/model-context-limits.js +30 -30
  246. package/src/utils/token-budget.js +74 -74
  247. package/src/utils/usage-cost.js +25 -25
  248. package/src/utils/usage-metrics.js +14 -14
@@ -1,36 +1,36 @@
1
- export function evaluateDurableWrite(candidate, policy, writesThisRun = 0) {
2
- const reasons = [];
3
-
4
- if (!policy?.allow_durable_writes) {
5
- reasons.push("durable writes disabled");
6
- }
7
-
8
- if (policy?.max_writes_per_run != null && writesThisRun >= policy.max_writes_per_run) {
9
- reasons.push("write budget exhausted");
10
- }
11
-
12
- const category = candidate.category || "unknown";
13
- const allowed = policy?.categories?.allowed;
14
- const blocked = policy?.categories?.blocked || [];
15
-
16
- if (Array.isArray(allowed) && !allowed.includes(category)) {
17
- reasons.push(`category not allowed: ${category}`);
18
- }
19
-
20
- if (blocked.includes(category)) {
21
- reasons.push(`category blocked: ${category}`);
22
- }
23
-
24
- if (policy?.require_source_reference && (!candidate.sourceRefs || candidate.sourceRefs.length === 0)) {
25
- reasons.push("source reference required");
26
- }
27
-
28
- if (policy?.require_write_reason && !candidate.reason) {
29
- reasons.push("write reason required");
30
- }
31
-
32
- return {
33
- accepted: reasons.length === 0,
34
- reasons
35
- };
36
- }
1
+ export function evaluateDurableWrite(candidate, policy, writesThisRun = 0) {
2
+ const reasons = [];
3
+
4
+ if (!policy?.allow_durable_writes) {
5
+ reasons.push("durable writes disabled");
6
+ }
7
+
8
+ if (policy?.max_writes_per_run != null && writesThisRun >= policy.max_writes_per_run) {
9
+ reasons.push("write budget exhausted");
10
+ }
11
+
12
+ const category = candidate.category || "unknown";
13
+ const allowed = policy?.categories?.allowed;
14
+ const blocked = policy?.categories?.blocked || [];
15
+
16
+ if (Array.isArray(allowed) && !allowed.includes(category)) {
17
+ reasons.push(`category not allowed: ${category}`);
18
+ }
19
+
20
+ if (blocked.includes(category)) {
21
+ reasons.push(`category blocked: ${category}`);
22
+ }
23
+
24
+ if (policy?.require_source_reference && (!candidate.sourceRefs || candidate.sourceRefs.length === 0)) {
25
+ reasons.push("source reference required");
26
+ }
27
+
28
+ if (policy?.require_write_reason && !candidate.reason) {
29
+ reasons.push("write reason required");
30
+ }
31
+
32
+ return {
33
+ accepted: reasons.length === 0,
34
+ reasons
35
+ };
36
+ }
@@ -1,33 +1,33 @@
1
- /**
2
- * CLI command alias resolver.
3
- *
4
- * resolveAlias(command, args) returns the resolved args array for known
5
- * aliases, or null for unknown commands.
6
- *
7
- * Real command handlers in cli-main.js now own start/stop/logs/restart.
8
- * This alias layer only maps legacy convenience shorthands.
9
- */
10
-
11
- /**
12
- * @param {string} command
13
- * @param {string[]} args
14
- * @returns {string[] | null}
15
- */
16
- export function resolveAlias(command, args) {
17
- switch (command) {
18
- case "status":
19
- return ["runtime-status"];
20
-
21
- case "run": {
22
- const [jobName, ...rest] = args;
23
- if (!jobName) return ["execute-job", "provider"];
24
- return ["execute-job", jobName, "provider", ...rest];
25
- }
26
-
27
- case "runs":
28
- return ["review-runs", "10"];
29
-
30
- default:
31
- return null;
32
- }
33
- }
1
+ /**
2
+ * CLI command alias resolver.
3
+ *
4
+ * resolveAlias(command, args) returns the resolved args array for known
5
+ * aliases, or null for unknown commands.
6
+ *
7
+ * Real command handlers in cli-main.js now own start/stop/logs/restart.
8
+ * This alias layer only maps legacy convenience shorthands.
9
+ */
10
+
11
+ /**
12
+ * @param {string} command
13
+ * @param {string[]} args
14
+ * @returns {string[] | null}
15
+ */
16
+ export function resolveAlias(command, args) {
17
+ switch (command) {
18
+ case "status":
19
+ return ["runtime-status"];
20
+
21
+ case "run": {
22
+ const [jobName, ...rest] = args;
23
+ if (!jobName) return ["execute-job", "provider"];
24
+ return ["execute-job", jobName, "provider", ...rest];
25
+ }
26
+
27
+ case "runs":
28
+ return ["review-runs", "10"];
29
+
30
+ default:
31
+ return null;
32
+ }
33
+ }
@@ -1,224 +1,224 @@
1
- /**
2
- * Auth handlers: API key detection, validation, .env writing, and provider resolution.
3
- */
4
-
5
- import fs from "node:fs";
6
- import path from "node:path";
7
- import os from "node:os";
8
- import { buildAnthropicAuthHeaders } from "../../providers/anthropic.js";
9
- import { resolveInstallDir } from "../../auth/auth-profiles.js";
10
-
11
- // Env var names for each provider
12
- const ENV_VAR_MAP = {
13
- anthropic: ["NEMORIS_ANTHROPIC_API_KEY", "ANTHROPIC_API_KEY"],
14
- openai: ["NEMORIS_OPENAI_API_KEY", "OPENAI_API_KEY"],
15
- openrouter: ["OPENROUTER_API_KEY"],
16
- };
17
-
18
- // Default paths to search for .env files (in priority order)
19
- function defaultSearchPaths() {
20
- const installDir = resolveInstallDir();
21
- return [
22
- path.join(installDir, ".env"),
23
- path.join(os.homedir(), ".nemoris", ".env"),
24
- path.join(os.homedir(), ".openclaw", ".env"),
25
- ];
26
- }
27
-
28
- /**
29
- * Parse a .env file into a key/value map.
30
- * Handles lines of the form KEY=value, ignoring comments and blank lines.
31
- *
32
- * @param {string} filePath
33
- * @returns {Record<string, string>}
34
- */
35
- function parseEnvFile(filePath) {
36
- try {
37
- const content = fs.readFileSync(filePath, "utf8");
38
- const result = {};
39
- for (const line of content.split(/\r?\n/)) {
40
- const trimmed = line.trim();
41
- if (!trimmed || trimmed.startsWith("#")) continue;
42
- const eqIdx = trimmed.indexOf("=");
43
- if (eqIdx === -1) continue;
44
- const key = trimmed.slice(0, eqIdx).trim();
45
- const value = trimmed.slice(eqIdx + 1).trim();
46
- if (key) result[key] = value;
47
- }
48
- return result;
49
- } catch {
50
- return {};
51
- }
52
- }
53
-
54
- /**
55
- * Detect existing API keys from environment variables and optional .env file search paths.
56
- * Environment variables take priority over file values.
57
- *
58
- * @param {object} [options]
59
- * @param {string[]} [options.searchPaths] - Ordered list of .env file paths to check
60
- * @returns {{ anthropic: string|null, openai: string|null, openrouter: string|null }}
61
- */
62
- export function detectExistingKeys({ searchPaths } = {}) {
63
- const paths = searchPaths ?? defaultSearchPaths();
64
- const fileEnv = {};
65
- for (let i = paths.length - 1; i >= 0; i--) {
66
- const parsed = parseEnvFile(paths[i]);
67
- Object.assign(fileEnv, parsed);
68
- }
69
-
70
- const result = {};
71
- for (const [provider, envVars] of Object.entries(ENV_VAR_MAP)) {
72
- result[provider] = null;
73
- for (const envVar of envVars) {
74
- const value = process.env[envVar] || fileEnv[envVar] || null;
75
- if (value) {
76
- result[provider] = value;
77
- break;
78
- }
79
- }
80
- }
81
- return result;
82
- }
83
-
84
- export function validateApiKeyFormat(provider, key) {
85
- const token = String(key || "").trim();
86
- if (!token) {
87
- return { ok: false, error: "missing token" };
88
- }
89
-
90
- const rules = {
91
- anthropic: {
92
- prefixes: ["sk-ant-oat-", "sk-ant-api-", "sk-ant-"],
93
- minLength: 16,
94
- label: "Anthropic token"
95
- },
96
- openai: {
97
- prefixes: ["sk-"],
98
- minLength: 12,
99
- label: "OpenAI API key"
100
- },
101
- openrouter: {
102
- prefixes: ["sk-or-"],
103
- minLength: 12,
104
- label: "OpenRouter API key"
105
- }
106
- };
107
-
108
- const rule = rules[provider];
109
- if (!rule) {
110
- return { ok: true };
111
- }
112
-
113
- if (!rule.prefixes.some((prefix) => token.startsWith(prefix))) {
114
- return {
115
- ok: false,
116
- error: `${rule.label} must start with ${rule.prefixes.join(" or ")}`
117
- };
118
- }
119
-
120
- if (token.length < rule.minLength) {
121
- return {
122
- ok: false,
123
- error: `${rule.label} looks too short`
124
- };
125
- }
126
-
127
- return { ok: true };
128
- }
129
-
130
- /**
131
- * Validate an API key against a provider's health endpoint.
132
- * Ollama is not validated here (no auth needed) — use ollama-detect.js instead.
133
- *
134
- * @param {"anthropic"|"openai"|"openrouter"} provider
135
- * @param {string} key
136
- * @param {object} [options]
137
- * @param {Function} [options.fetchImpl]
138
- * @returns {Promise<{ ok: boolean, status?: number, error?: string }>}
139
- */
140
- export async function validateApiKey(provider, key, options = {}) {
141
- if (provider === "ollama") {
142
- return { ok: true };
143
- }
144
-
145
- const fetch = options.fetchImpl || globalThis.fetch;
146
- const providerTargets = {
147
- anthropic: {
148
- url: "https://api.anthropic.com/v1/messages/count_tokens",
149
- method: "POST",
150
- headers: {
151
- "content-type": "application/json",
152
- ...buildAnthropicAuthHeaders(key)
153
- },
154
- body: JSON.stringify({
155
- model: "claude-haiku-4-5",
156
- messages: [{ role: "user", content: "ping" }]
157
- })
158
- },
159
- openai: {
160
- url: "https://api.openai.com/v1/models",
161
- method: "GET",
162
- headers: {
163
- Authorization: `Bearer ${key}`
164
- }
165
- },
166
- openrouter: {
167
- url: "https://openrouter.ai/api/v1/models",
168
- method: "GET",
169
- headers: {
170
- Authorization: `Bearer ${key}`
171
- }
172
- }
173
- };
174
-
175
- const target = providerTargets[provider];
176
- if (!target) {
177
- throw new Error(`Unknown provider: ${provider}`);
178
- }
179
-
180
- try {
181
- const response = await fetch(target.url, {
182
- method: target.method,
183
- headers: target.headers,
184
- body: target.body,
185
- signal: AbortSignal.timeout(10000)
186
- });
187
- return {
188
- ok: response.ok,
189
- status: response.status
190
- };
191
- } catch (error) {
192
- return { ok: false, error: error.message };
193
- }
194
- }
195
-
196
- /**
197
- * Write or merge keys into an .env file at installDir/.env.
198
- * Existing keys not being overwritten are preserved.
199
- *
200
- * @param {string} installDir - Directory where .env will be written
201
- * @param {Record<string, string>} keys - Key/value pairs to write
202
- */
203
- export function writeEnvFile(installDir, keys) {
204
- const envPath = path.join(installDir, ".env");
205
- const existing = parseEnvFile(envPath);
206
- const merged = { ...existing, ...keys };
207
- const lines = Object.entries(merged).map(([k, v]) => `${k}=${v}`);
208
- fs.writeFileSync(envPath, lines.join("\n") + "\n", { encoding: "utf8", mode: 0o600 });
209
- }
210
-
211
- /**
212
- * Determine which provider IDs to generate TOML configs for based on available keys.
213
- *
214
- * @param {{ anthropic?: string|null, openrouter?: string|null, openai?: string|null, ollama?: boolean }} keys
215
- * @returns {string[]} Array of provider IDs (e.g. ["anthropic", "openrouter", "ollama"])
216
- */
217
- export function resolveProviders(keys) {
218
- const providers = [];
219
- if (keys.anthropic) providers.push("anthropic");
220
- if (keys.openrouter) providers.push("openrouter");
221
- if (keys.openai) providers.push("openai");
222
- if (keys.ollama) providers.push("ollama");
223
- return providers;
224
- }
1
+ /**
2
+ * Auth handlers: API key detection, validation, .env writing, and provider resolution.
3
+ */
4
+
5
+ import fs from "node:fs";
6
+ import path from "node:path";
7
+ import os from "node:os";
8
+ import { buildAnthropicAuthHeaders } from "../../providers/anthropic.js";
9
+ import { resolveInstallDir } from "../../auth/auth-profiles.js";
10
+
11
+ // Env var names for each provider
12
+ const ENV_VAR_MAP = {
13
+ anthropic: ["NEMORIS_ANTHROPIC_API_KEY", "ANTHROPIC_API_KEY"],
14
+ openai: ["NEMORIS_OPENAI_API_KEY", "OPENAI_API_KEY"],
15
+ openrouter: ["OPENROUTER_API_KEY"],
16
+ };
17
+
18
+ // Default paths to search for .env files (in priority order)
19
+ function defaultSearchPaths() {
20
+ const installDir = resolveInstallDir();
21
+ return [
22
+ path.join(installDir, ".env"),
23
+ path.join(os.homedir(), ".nemoris", ".env"),
24
+ path.join(os.homedir(), ".openclaw", ".env"),
25
+ ];
26
+ }
27
+
28
+ /**
29
+ * Parse a .env file into a key/value map.
30
+ * Handles lines of the form KEY=value, ignoring comments and blank lines.
31
+ *
32
+ * @param {string} filePath
33
+ * @returns {Record<string, string>}
34
+ */
35
+ function parseEnvFile(filePath) {
36
+ try {
37
+ const content = fs.readFileSync(filePath, "utf8");
38
+ const result = {};
39
+ for (const line of content.split(/\r?\n/)) {
40
+ const trimmed = line.trim();
41
+ if (!trimmed || trimmed.startsWith("#")) continue;
42
+ const eqIdx = trimmed.indexOf("=");
43
+ if (eqIdx === -1) continue;
44
+ const key = trimmed.slice(0, eqIdx).trim();
45
+ const value = trimmed.slice(eqIdx + 1).trim();
46
+ if (key) result[key] = value;
47
+ }
48
+ return result;
49
+ } catch {
50
+ return {};
51
+ }
52
+ }
53
+
54
+ /**
55
+ * Detect existing API keys from environment variables and optional .env file search paths.
56
+ * Environment variables take priority over file values.
57
+ *
58
+ * @param {object} [options]
59
+ * @param {string[]} [options.searchPaths] - Ordered list of .env file paths to check
60
+ * @returns {{ anthropic: string|null, openai: string|null, openrouter: string|null }}
61
+ */
62
+ export function detectExistingKeys({ searchPaths } = {}) {
63
+ const paths = searchPaths ?? defaultSearchPaths();
64
+ const fileEnv = {};
65
+ for (let i = paths.length - 1; i >= 0; i--) {
66
+ const parsed = parseEnvFile(paths[i]);
67
+ Object.assign(fileEnv, parsed);
68
+ }
69
+
70
+ const result = {};
71
+ for (const [provider, envVars] of Object.entries(ENV_VAR_MAP)) {
72
+ result[provider] = null;
73
+ for (const envVar of envVars) {
74
+ const value = process.env[envVar] || fileEnv[envVar] || null;
75
+ if (value) {
76
+ result[provider] = value;
77
+ break;
78
+ }
79
+ }
80
+ }
81
+ return result;
82
+ }
83
+
84
+ export function validateApiKeyFormat(provider, key) {
85
+ const token = String(key || "").trim();
86
+ if (!token) {
87
+ return { ok: false, error: "missing token" };
88
+ }
89
+
90
+ const rules = {
91
+ anthropic: {
92
+ prefixes: ["sk-ant-oat-", "sk-ant-api-", "sk-ant-"],
93
+ minLength: 16,
94
+ label: "Anthropic token"
95
+ },
96
+ openai: {
97
+ prefixes: ["sk-"],
98
+ minLength: 12,
99
+ label: "OpenAI API key"
100
+ },
101
+ openrouter: {
102
+ prefixes: ["sk-or-"],
103
+ minLength: 12,
104
+ label: "OpenRouter API key"
105
+ }
106
+ };
107
+
108
+ const rule = rules[provider];
109
+ if (!rule) {
110
+ return { ok: true };
111
+ }
112
+
113
+ if (!rule.prefixes.some((prefix) => token.startsWith(prefix))) {
114
+ return {
115
+ ok: false,
116
+ error: `${rule.label} must start with ${rule.prefixes.join(" or ")}`
117
+ };
118
+ }
119
+
120
+ if (token.length < rule.minLength) {
121
+ return {
122
+ ok: false,
123
+ error: `${rule.label} looks too short`
124
+ };
125
+ }
126
+
127
+ return { ok: true };
128
+ }
129
+
130
+ /**
131
+ * Validate an API key against a provider's health endpoint.
132
+ * Ollama is not validated here (no auth needed) — use ollama-detect.js instead.
133
+ *
134
+ * @param {"anthropic"|"openai"|"openrouter"} provider
135
+ * @param {string} key
136
+ * @param {object} [options]
137
+ * @param {Function} [options.fetchImpl]
138
+ * @returns {Promise<{ ok: boolean, status?: number, error?: string }>}
139
+ */
140
+ export async function validateApiKey(provider, key, options = {}) {
141
+ if (provider === "ollama") {
142
+ return { ok: true };
143
+ }
144
+
145
+ const fetch = options.fetchImpl || globalThis.fetch;
146
+ const providerTargets = {
147
+ anthropic: {
148
+ url: "https://api.anthropic.com/v1/messages/count_tokens",
149
+ method: "POST",
150
+ headers: {
151
+ "content-type": "application/json",
152
+ ...buildAnthropicAuthHeaders(key)
153
+ },
154
+ body: JSON.stringify({
155
+ model: "claude-haiku-4-5",
156
+ messages: [{ role: "user", content: "ping" }]
157
+ })
158
+ },
159
+ openai: {
160
+ url: "https://api.openai.com/v1/models",
161
+ method: "GET",
162
+ headers: {
163
+ Authorization: `Bearer ${key}`
164
+ }
165
+ },
166
+ openrouter: {
167
+ url: "https://openrouter.ai/api/v1/models",
168
+ method: "GET",
169
+ headers: {
170
+ Authorization: `Bearer ${key}`
171
+ }
172
+ }
173
+ };
174
+
175
+ const target = providerTargets[provider];
176
+ if (!target) {
177
+ throw new Error(`Unknown provider: ${provider}`);
178
+ }
179
+
180
+ try {
181
+ const response = await fetch(target.url, {
182
+ method: target.method,
183
+ headers: target.headers,
184
+ body: target.body,
185
+ signal: AbortSignal.timeout(10000)
186
+ });
187
+ return {
188
+ ok: response.ok,
189
+ status: response.status
190
+ };
191
+ } catch (error) {
192
+ return { ok: false, error: error.message };
193
+ }
194
+ }
195
+
196
+ /**
197
+ * Write or merge keys into an .env file at installDir/.env.
198
+ * Existing keys not being overwritten are preserved.
199
+ *
200
+ * @param {string} installDir - Directory where .env will be written
201
+ * @param {Record<string, string>} keys - Key/value pairs to write
202
+ */
203
+ export function writeEnvFile(installDir, keys) {
204
+ const envPath = path.join(installDir, ".env");
205
+ const existing = parseEnvFile(envPath);
206
+ const merged = { ...existing, ...keys };
207
+ const lines = Object.entries(merged).map(([k, v]) => `${k}=${v}`);
208
+ fs.writeFileSync(envPath, lines.join("\n") + "\n", { encoding: "utf8", mode: 0o600 });
209
+ }
210
+
211
+ /**
212
+ * Determine which provider IDs to generate TOML configs for based on available keys.
213
+ *
214
+ * @param {{ anthropic?: string|null, openrouter?: string|null, openai?: string|null, ollama?: boolean }} keys
215
+ * @returns {string[]} Array of provider IDs (e.g. ["anthropic", "openrouter", "ollama"])
216
+ */
217
+ export function resolveProviders(keys) {
218
+ const providers = [];
219
+ if (keys.anthropic) providers.push("anthropic");
220
+ if (keys.openrouter) providers.push("openrouter");
221
+ if (keys.openai) providers.push("openai");
222
+ if (keys.ollama) providers.push("ollama");
223
+ return providers;
224
+ }
@@ -1,42 +1,42 @@
1
- /**
2
- * Ollama local instance detection.
3
- * Probes localhost:11434/api/tags and returns model list metadata.
4
- */
5
-
6
- import { inspectOutboundUrl, OUTBOUND_ADDRESS_POLICY } from "../../security/ssrf-check.js";
7
-
8
- /**
9
- * Detect a running Ollama instance and enumerate available models.
10
- *
11
- * @param {object} [options]
12
- * @param {Function} [options.fetchImpl] - Injectable fetch implementation (defaults to globalThis.fetch)
13
- * @param {Function} [options.lookupImpl] - Injectable DNS lookup used for loopback verification
14
- * @returns {Promise<{ ok: boolean, modelCount: number, models: string[] }>}
15
- */
16
- export async function detectOllama({ fetchImpl, lookupImpl } = {}) {
17
- const fetch = fetchImpl || globalThis.fetch;
18
- const baseUrl = process.env.OLLAMA_HOST || "http://localhost:11434";
19
- const url = `${baseUrl}/api/tags`;
20
-
21
- try {
22
- const inspection = await inspectOutboundUrl(url, {
23
- lookupImpl,
24
- addressPolicy: OUTBOUND_ADDRESS_POLICY.REQUIRE_LOOPBACK,
25
- loopbackOnlyMessage: `Ollama base URL must resolve to loopback only; refusing ${url}.`,
26
- });
27
- if (!inspection.ok) {
28
- return { ok: false, modelCount: 0, models: [], error: inspection.reason };
29
- }
30
- const response = await fetch(url);
31
- if (!response.ok) {
32
- return { ok: false, modelCount: 0, models: [] };
33
- }
34
- const data = await response.json();
35
- const models = Array.isArray(data?.models)
36
- ? data.models.map((m) => m?.name || m?.model || null).filter(Boolean)
37
- : [];
38
- return { ok: true, modelCount: models.length, models };
39
- } catch {
40
- return { ok: false, modelCount: 0, models: [] };
41
- }
42
- }
1
+ /**
2
+ * Ollama local instance detection.
3
+ * Probes localhost:11434/api/tags and returns model list metadata.
4
+ */
5
+
6
+ import { inspectOutboundUrl, OUTBOUND_ADDRESS_POLICY } from "../../security/ssrf-check.js";
7
+
8
+ /**
9
+ * Detect a running Ollama instance and enumerate available models.
10
+ *
11
+ * @param {object} [options]
12
+ * @param {Function} [options.fetchImpl] - Injectable fetch implementation (defaults to globalThis.fetch)
13
+ * @param {Function} [options.lookupImpl] - Injectable DNS lookup used for loopback verification
14
+ * @returns {Promise<{ ok: boolean, modelCount: number, models: string[] }>}
15
+ */
16
+ export async function detectOllama({ fetchImpl, lookupImpl } = {}) {
17
+ const fetch = fetchImpl || globalThis.fetch;
18
+ const baseUrl = process.env.OLLAMA_HOST || "http://localhost:11434";
19
+ const url = `${baseUrl}/api/tags`;
20
+
21
+ try {
22
+ const inspection = await inspectOutboundUrl(url, {
23
+ lookupImpl,
24
+ addressPolicy: OUTBOUND_ADDRESS_POLICY.REQUIRE_LOOPBACK,
25
+ loopbackOnlyMessage: `Ollama base URL must resolve to loopback only; refusing ${url}.`,
26
+ });
27
+ if (!inspection.ok) {
28
+ return { ok: false, modelCount: 0, models: [], error: inspection.reason };
29
+ }
30
+ const response = await fetch(url);
31
+ if (!response.ok) {
32
+ return { ok: false, modelCount: 0, models: [] };
33
+ }
34
+ const data = await response.json();
35
+ const models = Array.isArray(data?.models)
36
+ ? data.models.map((m) => m?.name || m?.model || null).filter(Boolean)
37
+ : [];
38
+ return { ok: true, modelCount: models.length, models };
39
+ } catch {
40
+ return { ok: false, modelCount: 0, models: [] };
41
+ }
42
+ }