nemoris 0.1.2 → 0.1.3

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.
@@ -13,7 +13,7 @@ adapter = "openclaw_cli"
13
13
  enabled = true
14
14
  channel = "telegram"
15
15
  account_id = "default"
16
- chat_id = "7781763328"
16
+ chat_id = "YOUR_CHAT_ID"
17
17
  silent = true
18
18
  dry_run = true
19
19
 
@@ -22,7 +22,7 @@ adapter = "openclaw_cli"
22
22
  enabled = false
23
23
  channel = "telegram"
24
24
  account_id = "default"
25
- chat_id = "7781763328"
25
+ chat_id = "YOUR_CHAT_ID"
26
26
  silent = true
27
27
  dry_run = false
28
28
 
@@ -43,7 +43,7 @@ adapter = "telegram"
43
43
  enabled = false
44
44
  channel = "telegram"
45
45
  account_id = "default"
46
- chat_id = "7781763328"
46
+ chat_id = "YOUR_CHAT_ID"
47
47
  bot_token_env = "NEMORIS_TELEGRAM_BOT_TOKEN"
48
48
 
49
49
  [profiles.standalone_operator]
@@ -102,8 +102,8 @@ allowed_failure_classes = ["timeout", "provider_loading"]
102
102
  bot_token_env = "NEMORIS_TELEGRAM_BOT_TOKEN"
103
103
  polling_mode = true
104
104
  webhook_url = ""
105
- operator_chat_id = "7781763328"
106
- authorized_chat_ids = ["7781763328"]
105
+ operator_chat_id = "YOUR_CHAT_ID"
106
+ authorized_chat_ids = ["YOUR_CHAT_ID"]
107
107
  default_agent = "main"
108
108
 
109
109
  [slack]
@@ -63,7 +63,7 @@ Check if daemon is running:
63
63
  ```
64
64
  Daemon is running. Send any message to @kodi_nemoris_bot on Telegram...
65
65
  Waiting for your message...
66
- ✓ Found you chat_id: 7781763328, @leeUsername
66
+ ✓ Found you chat_id: YOUR_CHAT_ID, @leeUsername
67
67
  ```
68
68
 
69
69
  - Poll the SQLite state store (`state/active.db`) for the first interactive job row where `source = 'telegram'`
@@ -77,7 +77,7 @@ This avoids any conflict with the daemon's getUpdates polling connection. No `de
77
77
 
78
78
  ```
79
79
  Send any message to @kodi_nemoris_bot on Telegram, then press Enter...
80
- ✓ Found you chat_id: 7781763328, @leeUsername
80
+ ✓ Found you chat_id: YOUR_CHAT_ID, @leeUsername
81
81
  ```
82
82
 
83
83
  - Reuses existing `whoami(botToken)` function (deleteWebhook → getUpdates → extract chat)
@@ -110,8 +110,8 @@ Writes the `[telegram]` section to `config/runtime.toml`:
110
110
  bot_token_env = "NEMORIS_TELEGRAM_BOT_TOKEN"
111
111
  polling_mode = true
112
112
  webhook_url = ""
113
- operator_chat_id = "7781763328"
114
- authorized_chat_ids = ["7781763328"]
113
+ operator_chat_id = "YOUR_CHAT_ID"
114
+ authorized_chat_ids = ["YOUR_CHAT_ID"]
115
115
  default_agent = "kodi"
116
116
  ```
117
117
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nemoris",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "type": "module",
5
5
  "description": "Personal AI agent runtime — persistent memory, delivery guarantees, task contracts, self-healing. Local-first, no cloud.",
6
6
  "license": "MIT",
@@ -6,6 +6,7 @@
6
6
  */
7
7
 
8
8
  import fs from "node:fs";
9
+ import path from "node:path";
9
10
  import os from "node:os";
10
11
  import { execFile } from "node:child_process";
11
12
  import { promisify } from "node:util";
@@ -528,3 +529,80 @@ export async function runDoctor(
528
529
 
529
530
  return results;
530
531
  }
532
+
533
+ export function quickValidateConfig(installDir) {
534
+ const errors = [];
535
+ const suggestions = [];
536
+ const configDir = path.join(installDir, "config");
537
+
538
+ if (!fs.existsSync(path.join(configDir, "runtime.toml"))) {
539
+ errors.push("Missing config/runtime.toml");
540
+ suggestions.push("Run `nemoris setup` to regenerate config files.");
541
+ }
542
+
543
+ if (!fs.existsSync(path.join(configDir, "router.toml"))) {
544
+ errors.push("Missing config/router.toml");
545
+ suggestions.push("Run `nemoris setup` to regenerate the router.");
546
+ }
547
+
548
+ const providersDir = path.join(configDir, "providers");
549
+ if (!fs.existsSync(providersDir)) {
550
+ errors.push("No provider configs found");
551
+ suggestions.push("Run `nemoris setup` to configure a provider.");
552
+ } else {
553
+ const files = fs.readdirSync(providersDir).filter((f) => f.endsWith(".toml"));
554
+ if (files.length === 0) {
555
+ errors.push("No provider configs found in config/providers/");
556
+ suggestions.push("Run `nemoris setup` to configure a provider.");
557
+ }
558
+ for (const file of files) {
559
+ try {
560
+ const content = fs.readFileSync(path.join(providersDir, file), "utf8");
561
+ const authRefMatch = content.match(/auth_ref\s*=\s*"([^"]+)"/);
562
+ if (authRefMatch) {
563
+ const authRef = authRefMatch[1];
564
+ if (!authRef.startsWith("env:") && !authRef.startsWith("profile:")) {
565
+ errors.push(`Invalid auth_ref in ${file}: "${authRef}" (must start with env: or profile:)`);
566
+ suggestions.push(`Fix auth_ref in config/providers/${file}`);
567
+ } else if (authRef.startsWith("env:")) {
568
+ const envName = authRef.slice(4);
569
+ if (!process.env[envName]) {
570
+ errors.push(`Missing env var ${envName} referenced by ${file}`);
571
+ suggestions.push(`Set ${envName} in your .env file or environment.`);
572
+ }
573
+ }
574
+ }
575
+ } catch {
576
+ errors.push(`Cannot read ${file}`);
577
+ }
578
+ }
579
+ }
580
+
581
+ if (!fs.existsSync(path.join(configDir, "identity"))) {
582
+ errors.push("Missing config/identity/ directory");
583
+ suggestions.push("Run `nemoris setup` to create identity files.");
584
+ }
585
+
586
+ return { valid: errors.length === 0, errors, suggestions };
587
+ }
588
+
589
+ export function suggestRepairs(results) {
590
+ const repairs = [];
591
+ for (const error of results.errors) {
592
+ if (error.includes("No provider configs")) {
593
+ repairs.push({ error, fix: "nemoris setup", description: "Run setup to configure a provider" });
594
+ } else if (error.includes("Missing config/runtime.toml")) {
595
+ repairs.push({ error, fix: "nemoris setup", description: "Run setup to regenerate config files" });
596
+ } else if (error.includes("Missing config/router.toml")) {
597
+ repairs.push({ error, fix: "nemoris setup", description: "Run setup to regenerate the router" });
598
+ } else if (error.includes("Invalid auth_ref")) {
599
+ repairs.push({ error, fix: "Edit the provider TOML file", description: "Fix auth_ref to use env: or profile: prefix" });
600
+ } else if (error.includes("Missing env var")) {
601
+ const envVar = error.match(/Missing env var (\S+)/)?.[1] || "the referenced variable";
602
+ repairs.push({ error, fix: `Set ${envVar} in .env`, description: `Add ${envVar}=your_key to your .env file` });
603
+ } else if (error.includes("Missing config/identity")) {
604
+ repairs.push({ error, fix: "nemoris setup", description: "Run setup to create identity files" });
605
+ }
606
+ }
607
+ return repairs;
608
+ }
@@ -32,6 +32,19 @@ export function readLock(installDir) {
32
32
  }
33
33
  }
34
34
 
35
+ export function writeWizardMetadata(installDir, metadata) {
36
+ const lock = readLock(installDir) || {};
37
+ const updated = {
38
+ ...lock,
39
+ lastRunAt: metadata.lastRunAt || new Date().toISOString(),
40
+ lastRunVersion: metadata.lastRunVersion || "",
41
+ lastRunCommand: metadata.lastRunCommand || "setup",
42
+ lastRunMode: metadata.lastRunMode || "quickstart",
43
+ lastRunFlow: metadata.lastRunFlow || "interactive",
44
+ };
45
+ writeLock(installDir, updated);
46
+ }
47
+
35
48
  export function deleteLock(installDir) {
36
49
  const p = lockPath(installDir);
37
50
  try {
@@ -0,0 +1,83 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+
4
+ /**
5
+ * Writes the model catalog to state/models.json.
6
+ * @param {string} installDir
7
+ * @param {string} agentId
8
+ * @param {Array<object>} models
9
+ */
10
+ export function writeModelsCatalog(installDir, agentId, models) {
11
+ const stateDir = path.join(installDir, "state");
12
+ fs.mkdirSync(stateDir, { recursive: true, mode: 0o700 });
13
+ const catalogPath = path.join(stateDir, "models.json");
14
+ const catalog = {
15
+ version: 1,
16
+ agentId,
17
+ updatedAt: new Date().toISOString(),
18
+ models: models.map((m) => ({
19
+ id: String(m.id || ""),
20
+ provider: String(m.provider || ""),
21
+ displayName: String(m.displayName || m.name || m.id || ""),
22
+ contextWindow: typeof m.contextWindow === "number" && m.contextWindow > 0 ? m.contextWindow : undefined,
23
+ reasoning: typeof m.reasoning === "boolean" ? m.reasoning : undefined,
24
+ inputTypes: Array.isArray(m.inputTypes) ? m.inputTypes : undefined,
25
+ costTier: m.costTier || undefined,
26
+ role: m.role || undefined,
27
+ })),
28
+ };
29
+ const content = JSON.stringify(catalog, null, 2) + "\n";
30
+ const tempPath = `${catalogPath}.${process.pid}.${Date.now()}.tmp`;
31
+ fs.writeFileSync(tempPath, content, { encoding: "utf8", mode: 0o600 });
32
+ fs.renameSync(tempPath, catalogPath);
33
+ }
34
+
35
+ /**
36
+ * Reads the model catalog from state/models.json.
37
+ * @param {string} installDir
38
+ * @returns {{ version: number, agentId: string, updatedAt: string, models: Array<object> } | null}
39
+ */
40
+ export function readModelsCatalog(installDir) {
41
+ const catalogPath = path.join(installDir, "state", "models.json");
42
+ try {
43
+ const raw = fs.readFileSync(catalogPath, "utf8");
44
+ const parsed = JSON.parse(raw);
45
+ if (!parsed || !Array.isArray(parsed.models)) return null;
46
+ return parsed;
47
+ } catch {
48
+ return null;
49
+ }
50
+ }
51
+
52
+ /**
53
+ * Merges curated preset metadata with selected model IDs.
54
+ * @param {Array<object>} presets - curated presets with { id, label, description }
55
+ * @param {Array<object>} fetchedModels - fetched models with { id, contextWindow?, name? }
56
+ * @param {Array<string>} selectedIds - user-selected model IDs
57
+ * @param {string} provider - provider name
58
+ * @returns {Array<object>} enriched model entries
59
+ */
60
+ export function mergeModelMetadata(presets, fetchedModels, selectedIds, provider) {
61
+ const presetMap = new Map(presets.map((p) => [p.id, p]));
62
+ const fetchedMap = new Map(fetchedModels.map((m) => [m.id, m]));
63
+ const MODEL_ROLE_ORDER = ["cheap_interactive", "fallback", "manual_bump"];
64
+
65
+ return selectedIds.map((id, index) => {
66
+ const preset = presetMap.get(id);
67
+ const fetched = fetchedMap.get(id);
68
+ const displayId = id
69
+ .replace(/^openrouter\//, "")
70
+ .replace(/^openai-codex\//, "")
71
+ .replace(/^anthropic\//, "");
72
+
73
+ return {
74
+ id,
75
+ provider,
76
+ displayName: preset?.label || fetched?.name || displayId,
77
+ contextWindow: fetched?.contextWindow || undefined,
78
+ reasoning: fetched?.reasoning || undefined,
79
+ inputTypes: fetched?.inputTypes || undefined,
80
+ role: MODEL_ROLE_ORDER[index] || MODEL_ROLE_ORDER.at(-1),
81
+ };
82
+ });
83
+ }
@@ -169,20 +169,31 @@ export function detectProviderOptions({ env = process.env, ollamaResult = { ok:
169
169
  {
170
170
  value: "anthropic",
171
171
  label: "Anthropic",
172
- hint: anthropic.value ? "API key found in env" : "API key required",
172
+ hint: anthropic.value ? "API key found in env" : "Setup token or API key",
173
173
  detected: Boolean(anthropic.value),
174
+ authMethods: [
175
+ { value: "token", label: "Setup Token", hint: "From `claude setup-token`" },
176
+ { value: "api_key", label: "API Key", hint: "From console.anthropic.com" },
177
+ ],
174
178
  },
175
179
  {
176
180
  value: "openai",
177
181
  label: "OpenAI",
178
182
  hint: openai.value ? "API key found in env" : "API key or ChatGPT OAuth",
179
183
  detected: Boolean(openai.value),
184
+ authMethods: [
185
+ { value: "api_key", label: "API Key", hint: "From platform.openai.com" },
186
+ { value: "oauth", label: "ChatGPT OAuth", hint: "Browser login, refreshable token" },
187
+ ],
180
188
  },
181
189
  {
182
190
  value: "openrouter",
183
191
  label: "OpenRouter",
184
192
  hint: openrouter.value ? "API key found" : "One key, 100+ models — openrouter.ai",
185
193
  detected: Boolean(openrouter.value),
194
+ authMethods: [
195
+ { value: "api_key", label: "API Key", hint: "From openrouter.ai/keys" },
196
+ ],
186
197
  },
187
198
  {
188
199
  value: "ollama",
@@ -191,12 +202,16 @@ export function detectProviderOptions({ env = process.env, ollamaResult = { ok:
191
202
  ? `Local, free — ${ollamaResult.models?.length || 0} models installed`
192
203
  : "Local, free — not detected",
193
204
  detected: Boolean(ollamaResult.ok),
205
+ authMethods: [
206
+ { value: "local", label: "Local", hint: "No auth needed" },
207
+ ],
194
208
  },
195
209
  {
196
210
  value: "skip",
197
211
  label: "Skip for now",
198
212
  hint: "Finish setup without provider auth.",
199
213
  detected: false,
214
+ authMethods: [],
200
215
  },
201
216
  ];
202
217
  }
@@ -24,6 +24,7 @@ import {
24
24
  resolveOpenAICodexAccess,
25
25
  } from "../../auth/openai-codex-oauth.js";
26
26
  import { getAuthProfile, resolveAuthProfilesPath } from "../../auth/auth-profiles.js";
27
+ import { writeModelsCatalog, mergeModelMetadata } from "../model-catalog-writer.js";
27
28
 
28
29
  const PROVIDER_CONFIGS = {
29
30
  anthropic: {
@@ -177,7 +178,26 @@ function providerDisplayName(provider) {
177
178
  return provider;
178
179
  }
179
180
 
180
- async function fetchProviderModelIds(provider, key, { fetchImpl = globalThis.fetch } = {}) {
181
+ const OPENAI_CONTEXT_WINDOWS = {
182
+ "gpt-4.1": 1000000,
183
+ "gpt-4o": 128000,
184
+ "gpt-4o-mini": 128000,
185
+ "o4-mini": 200000,
186
+ "o3": 200000,
187
+ "o3-mini": 200000,
188
+ "o1": 200000,
189
+ "o1-mini": 128000,
190
+ "gpt-4-turbo": 128000,
191
+ "gpt-3.5-turbo": 16385,
192
+ };
193
+
194
+ function formatContextWindow(tokens) {
195
+ if (!tokens || tokens <= 0) return "";
196
+ if (tokens >= 1000000) return `ctx ${(tokens / 1000000).toFixed(0)}M`;
197
+ return `ctx ${Math.round(tokens / 1000).toFixed(0)}k`;
198
+ }
199
+
200
+ async function fetchProviderModels(provider, key, { fetchImpl = globalThis.fetch } = {}) {
181
201
  const targets = {
182
202
  anthropic: {
183
203
  url: "https://api.anthropic.com/v1/models",
@@ -203,50 +223,90 @@ async function fetchProviderModelIds(provider, key, { fetchImpl = globalThis.fet
203
223
  signal: AbortSignal.timeout(10000),
204
224
  });
205
225
  const data = await response.json();
206
- if (!response.ok) return PROVIDER_MODEL_PRESETS[provider]?.map((item) => item.id) ?? [];
207
- const ids = Array.isArray(data?.data)
208
- ? data.data.map((item) => item?.id).filter(Boolean)
209
- : [];
210
- return ids.map((id) => ensureProviderModelPrefix(provider, id));
226
+ if (!response.ok) {
227
+ return (PROVIDER_MODEL_PRESETS[provider] || []).map((item) => ({
228
+ id: item.id,
229
+ name: item.label,
230
+ }));
231
+ }
232
+ const items = Array.isArray(data?.data) ? data.data.filter(Boolean) : [];
233
+ return items.map((item) => {
234
+ const rawId = item.id;
235
+ const prefixedId = ensureProviderModelPrefix(provider, rawId);
236
+ let contextWindow;
237
+ let name;
238
+ let reasoning;
239
+
240
+ if (provider === "anthropic") {
241
+ name = item.display_name || undefined;
242
+ contextWindow = item.context_window || undefined;
243
+ } else if (provider === "openrouter") {
244
+ name = item.name || undefined;
245
+ contextWindow = item.context_length || undefined;
246
+ } else if (provider === "openai") {
247
+ name = undefined;
248
+ contextWindow = OPENAI_CONTEXT_WINDOWS[rawId] || undefined;
249
+ }
250
+
251
+ return {
252
+ id: prefixedId,
253
+ contextWindow,
254
+ name,
255
+ reasoning,
256
+ };
257
+ });
211
258
  } catch {
212
- return PROVIDER_MODEL_PRESETS[provider]?.map((item) => item.id) ?? [];
259
+ return (PROVIDER_MODEL_PRESETS[provider] || []).map((item) => ({
260
+ id: item.id,
261
+ name: item.label,
262
+ }));
213
263
  }
214
264
  }
215
265
 
216
266
  async function buildProviderSelectionOptions(provider, key, { fetchImpl = globalThis.fetch } = {}) {
217
267
  const curated = PROVIDER_MODEL_PRESETS[provider] || [];
218
- const fetched = await fetchProviderModelIds(provider, key, { fetchImpl });
219
- const fetchedSet = new Set(fetched);
268
+ const fetchedModels = await fetchProviderModels(provider, key, { fetchImpl });
269
+ const fetchedIds = new Set(fetchedModels.map((m) => m.id));
270
+ const fetchedMap = new Map(fetchedModels.map((m) => [m.id, m]));
220
271
 
221
272
  // Curated models that exist in the fetched list (fall back to all curated if fetch failed)
222
- const curatedAvailable = fetched.length > 0
223
- ? curated.filter((item) => fetchedSet.has(item.id))
273
+ const curatedAvailable = fetchedModels.length > 0
274
+ ? curated.filter((item) => fetchedIds.has(item.id))
224
275
  : curated;
225
276
  const curatedIds = new Set(curatedAvailable.map((item) => item.id));
226
277
 
227
278
  // Remaining fetched models not already shown as curated
228
- const extra = fetched
229
- .filter((id) => !curatedIds.has(id))
230
- .map((id) => {
231
- const displayId = id
279
+ const extra = fetchedModels
280
+ .filter((m) => !curatedIds.has(m.id))
281
+ .map((m) => {
282
+ const displayId = m.id
232
283
  .replace(/^openrouter\//, "")
233
284
  .replace(/^openai-codex\//, "")
234
285
  .replace(/^anthropic\//, "");
235
- return { value: id, label: displayId, description: "available from provider" };
286
+ const ctxStr = formatContextWindow(m.contextWindow);
287
+ const desc = ctxStr ? `available from provider \u00b7 ${ctxStr}` : "available from provider";
288
+ return { value: m.id, label: displayId, description: desc };
236
289
  });
237
290
 
238
- return [
239
- ...curatedAvailable.map((item) => ({ value: item.id, label: item.label, description: item.description })),
291
+ const options = [
292
+ ...curatedAvailable.map((item) => {
293
+ const fetched = fetchedMap.get(item.id);
294
+ const ctxStr = fetched ? formatContextWindow(fetched.contextWindow) : "";
295
+ const desc = ctxStr ? `${item.description} \u00b7 ${ctxStr}` : item.description;
296
+ return { value: item.id, label: item.label, description: desc };
297
+ }),
240
298
  ...extra,
241
299
  { value: "__custom__", label: "Enter a different model name...", description: "Use a specific model id not shown in the list." },
242
300
  ];
301
+
302
+ return { options, fetchedModels };
243
303
  }
244
304
 
245
305
  async function promptForProviderModels(provider, key, tui, { fetchImpl = globalThis.fetch } = {}) {
246
306
  const { select, prompt, dim, cyan } = tui;
247
- if (!select) return [];
307
+ if (!select) return { chosen: [], fetchedModels: [] };
248
308
 
249
- const options = await buildProviderSelectionOptions(provider, key, { fetchImpl });
309
+ const { options, fetchedModels } = await buildProviderSelectionOptions(provider, key, { fetchImpl });
250
310
  const chosen = [];
251
311
  const manualOptionValue = "__custom__";
252
312
  const defaultModelValue = options.find((item) => !String(item.value).startsWith("__"))?.value || "";
@@ -296,7 +356,7 @@ async function promptForProviderModels(provider, key, tui, { fetchImpl = globalT
296
356
  }
297
357
  }
298
358
 
299
- return chosen;
359
+ return { chosen, fetchedModels };
300
360
  }
301
361
 
302
362
  export function writeProviderConfigs(installDir, providerInput) {
@@ -538,11 +598,31 @@ export async function runAuthPhase(installDir, options = {}) {
538
598
  providerFlags.openai = true;
539
599
  }
540
600
 
601
+ const fetchedModelsCache = {};
541
602
  if (tui) {
542
603
  for (const provider of ["openrouter", "anthropic", "openai"]) {
543
604
  const providerToken = providerSecrets[provider];
544
605
  if (!providerToken) continue;
545
- selectedModels[provider] = await promptForProviderModels(provider, providerToken, tui, { fetchImpl });
606
+ const { chosen, fetchedModels } = await promptForProviderModels(provider, providerToken, tui, { fetchImpl });
607
+ selectedModels[provider] = chosen;
608
+ fetchedModelsCache[provider] = fetchedModels;
609
+ }
610
+ }
611
+
612
+ // Persist model catalog with metadata
613
+ const allCatalogModels = [];
614
+ for (const [provider, modelIds] of Object.entries(selectedModels)) {
615
+ if (!modelIds?.length) continue;
616
+ const presets = PROVIDER_MODEL_PRESETS[provider] || [];
617
+ const fetched = fetchedModelsCache[provider] || [];
618
+ const enriched = mergeModelMetadata(presets, fetched, modelIds, provider);
619
+ allCatalogModels.push(...enriched);
620
+ }
621
+ if (allCatalogModels.length > 0) {
622
+ try {
623
+ writeModelsCatalog(installDir, "default", allCatalogModels);
624
+ } catch {
625
+ // Non-fatal — catalog is optional
546
626
  }
547
627
  }
548
628
 
@@ -17,6 +17,7 @@ import { verify } from "./phases/verify.js";
17
17
  import { createClackPrompter, SetupCancelledError } from "./clack-prompter.js";
18
18
  import { detectPreferredProvider, detectProviderOptions, summarizeSelectedModels } from "./model-catalog.js";
19
19
  import { isDaemonRunning, loadDaemon, writeDaemonUnit } from "./platform.js";
20
+ import { writeWizardMetadata } from "./lock.js";
20
21
 
21
22
  const MIN_NODE_MAJOR = 22;
22
23
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
@@ -63,6 +64,43 @@ function readRuntimeConfig(installDir) {
63
64
  }
64
65
  }
65
66
 
67
+ function readExistingIdentity(installDir) {
68
+ const result = { userName: "", agentName: "", agentId: "", provider: "" };
69
+ try {
70
+ const agentsDir = path.join(installDir, "config", "agents");
71
+ if (fs.existsSync(agentsDir)) {
72
+ const files = fs.readdirSync(agentsDir).filter((f) => f.endsWith(".toml"));
73
+ if (files.length > 0) {
74
+ const agentToml = parseToml(fs.readFileSync(path.join(agentsDir, files[0]), "utf8"));
75
+ result.agentId = agentToml?.id || files[0].replace(/\.toml$/, "");
76
+ }
77
+ }
78
+ const identityDir = path.join(installDir, "config", "identity");
79
+ if (fs.existsSync(identityDir)) {
80
+ const soulFiles = fs.readdirSync(identityDir).filter((f) => f.endsWith("-soul.md"));
81
+ for (const file of soulFiles) {
82
+ const content = fs.readFileSync(path.join(identityDir, file), "utf8");
83
+ const nameMatch = content.match(/^# Soul — (.+)$/m);
84
+ if (nameMatch) result.agentName = nameMatch[1].trim();
85
+ const operatorMatch = content.match(/^Operator:\s*(.+)$/m);
86
+ if (operatorMatch) result.userName = operatorMatch[1].trim();
87
+ if (result.agentName) break;
88
+ }
89
+ }
90
+ const providersDir = path.join(installDir, "config", "providers");
91
+ if (fs.existsSync(providersDir)) {
92
+ const providerFiles = fs.readdirSync(providersDir).filter((f) => f.endsWith(".toml") && f !== "ollama.toml");
93
+ if (providerFiles.length > 0) {
94
+ const name = providerFiles[0].replace(/\.toml$/, "").replace(/-codex$/, "");
95
+ result.provider = name === "openai" ? "openai" : name;
96
+ }
97
+ }
98
+ } catch {
99
+ // Non-fatal — fall back to defaults
100
+ }
101
+ return result;
102
+ }
103
+
66
104
  function summarizeExistingConfig(installDir) {
67
105
  const runtime = readRuntimeConfig(installDir);
68
106
  const providersDir = path.join(installDir, "config", "providers");
@@ -184,16 +222,22 @@ async function runInteractiveWizard({
184
222
  await prompter.intro("Nemoris setup");
185
223
 
186
224
  await prompter.note([
187
- "Security warning please read.",
225
+ "Nemoris is a personal AI agent runtime.",
226
+ "",
227
+ "Before continuing, please understand:",
188
228
  "",
189
- "Nemoris runs as a background daemon on your machine.",
190
- "It can execute shell commands, read files, and make network requests always acting on your instructions, never autonomously.",
229
+ " Your agent can execute system commands if tools are enabled.",
230
+ " It can read and write files within its workspace directory.",
231
+ " It can make network requests to configured providers.",
232
+ " A malicious prompt could trick it into unsafe actions.",
191
233
  "",
192
- "This is open-source software. Review the code at:",
193
- "https://github.com/amzer24/nemoris",
234
+ "Recommended baseline:",
235
+ " - Keep API keys out of the agent's reachable workspace.",
236
+ " - Use the strongest available model for tool-enabled agents.",
237
+ " - Review agent actions in the run log regularly.",
238
+ " - Do not expose the daemon to untrusted networks.",
194
239
  "",
195
- "Run as a personal agent — one trusted operator boundary.",
196
- "Do not expose to the internet without hardening first.",
240
+ "More: https://github.com/amzer24/nemoris#security",
197
241
  ].join("\n"), "Security");
198
242
 
199
243
  const proceed = await prompter.confirm({
@@ -264,12 +308,13 @@ async function runInteractiveWizard({
264
308
  const detection = await detect(installDir);
265
309
  await scaffold({ installDir });
266
310
 
311
+ const existing = readExistingIdentity(installDir);
267
312
  const userName = await prompter.text({
268
313
  message: "Your name",
269
- initialValue: process.env.NEMORIS_USER_NAME || process.env.USER || "",
314
+ initialValue: existing.userName || process.env.NEMORIS_USER_NAME || process.env.USER || "",
270
315
  placeholder: os.userInfo().username || "",
271
316
  });
272
- const defaultAgentName = process.env.NEMORIS_AGENT_NAME || resolveDefaultAgentName();
317
+ const defaultAgentName = existing.agentName || process.env.NEMORIS_AGENT_NAME || resolveDefaultAgentName();
273
318
  const agentName = await prompter.text({
274
319
  message: "What should your agent be called?",
275
320
  initialValue: defaultAgentName,
@@ -294,12 +339,30 @@ async function runInteractiveWizard({
294
339
  label: option.label,
295
340
  hint: option.hint,
296
341
  })),
297
- initialValue: detectPreferredProvider({
342
+ initialValue: existing.provider || detectPreferredProvider({
298
343
  env: process.env,
299
344
  ollamaResult: detection.ollama ? { ok: true } : { ok: false },
300
345
  }),
301
346
  });
302
347
 
348
+ // Select auth method if provider has multiple options
349
+ let selectedAuthMethod = "api_key";
350
+ const providerOptions = detectProviderOptions({
351
+ env: process.env,
352
+ ollamaResult: detection.ollama ? { ok: true, models: detection.ollama.models } : { ok: false, models: [] },
353
+ });
354
+ const selectedProvider = providerOptions.find((o) => o.value === provider);
355
+ if (selectedProvider?.authMethods?.length > 1) {
356
+ selectedAuthMethod = await prompter.select({
357
+ message: `${selectedProvider.label} auth method:`,
358
+ options: selectedProvider.authMethods.map((m) => ({
359
+ value: m.value,
360
+ label: m.label,
361
+ hint: m.hint,
362
+ })),
363
+ });
364
+ }
365
+
303
366
  let authResult = { providers: [], providerFlags: {}, selectedModels: {} };
304
367
  let telegramResult = { configured: false, verified: false };
305
368
  let ollamaResult = { configured: false, verified: false, models: [] };
@@ -314,7 +377,7 @@ async function runInteractiveWizard({
314
377
  ollamaResult: detection.ollama ? { ok: true, models: detection.ollama.models } : { ok: false, models: [] },
315
378
  },
316
379
  providerOrder: [provider],
317
- enableOpenAIOAuthChoice: provider === "openai",
380
+ authMethod: selectedAuthMethod,
318
381
  });
319
382
  authSpin.stop("Provider configured");
320
383
  }
@@ -376,27 +439,55 @@ async function runInteractiveWizard({
376
439
  installShellCompletion();
377
440
  }
378
441
 
379
- const modelSummary = summarizeSelectedModels([
380
- ...(authResult.selectedModels?.[provider] || []),
381
- ...(ollamaResult.models || []).map((model) => `ollama/${model}`),
382
- ]);
383
- const authMethod = provider === "ollama"
384
- ? "local"
385
- : authResult.providers.length > 0
386
- ? "api_key"
387
- : "skipped";
442
+ const summaryLines = [];
443
+
444
+ // Providers
445
+ const providerNames = [];
446
+ if (authResult.providerFlags?.anthropic) providerNames.push("Anthropic");
447
+ if (authResult.providerFlags?.openai) providerNames.push("OpenAI");
448
+ if (authResult.providerFlags?.openrouter) providerNames.push("OpenRouter");
449
+ if (ollamaResult.configured || authResult.providerFlags?.ollama) providerNames.push("Ollama");
450
+ summaryLines.push(`Providers: ${providerNames.length > 0 ? providerNames.join(", ") : "none configured"}`);
451
+
452
+ // Models
453
+ for (const [prov, models] of Object.entries(authResult.selectedModels || {})) {
454
+ if (!models?.length) continue;
455
+ const stripped = models.map((m) => m.replace(/^(openrouter|openai-codex|anthropic)\//, ""));
456
+ summaryLines.push(` ${prov}: ${stripped.join(", ")}`);
457
+ }
388
458
 
389
- await prompter.note([
390
- "Auth overview:",
391
- ` ${provider} ${provider === "skip" ? "skip" : "✓"} ${authMethod}`,
392
- "",
393
- "Models:",
394
- ` Default : ${modelSummary.defaultModel || "not configured"}`,
395
- ` Fallback : ${modelSummary.fallbackModel || "not configured"}`,
396
- "",
397
- "Docs: https://github.com/amzer24/nemoris",
398
- "Issues: https://github.com/amzer24/nemoris/issues",
399
- ].join("\n"), "Ready");
459
+ // Telegram
460
+ if (telegramResult.verified) {
461
+ summaryLines.push("Telegram: connected");
462
+ } else if (telegramResult.configured) {
463
+ summaryLines.push("Telegram: configured (not verified)");
464
+ }
465
+
466
+ // Ollama
467
+ if (ollamaResult.configured) {
468
+ summaryLines.push(`Ollama: ${ollamaResult.models?.length || 0} models`);
469
+ }
470
+
471
+ // Next steps
472
+ summaryLines.push("");
473
+ summaryLines.push("Next steps:");
474
+ summaryLines.push(" nemoris start Start the daemon");
475
+ summaryLines.push(" nemoris status Check runtime health");
476
+ summaryLines.push(" nemoris chat Open TUI chat");
477
+
478
+ await prompter.note(summaryLines.join("\n"), "Setup complete");
479
+
480
+ try {
481
+ const pkg = JSON.parse(fs.readFileSync(new URL("../../package.json", import.meta.url), "utf8"));
482
+ writeWizardMetadata(installDir, {
483
+ lastRunVersion: pkg.version || "",
484
+ lastRunCommand: "setup",
485
+ lastRunMode: flow === "manual" ? "manual" : "quickstart",
486
+ lastRunFlow: "interactive",
487
+ });
488
+ } catch {
489
+ // Non-fatal
490
+ }
400
491
 
401
492
  await prompter.outro(
402
493
  telegramResult.verified
@@ -471,6 +562,19 @@ async function runNonInteractiveWizard({
471
562
  if (result.status === "warning") {
472
563
  return 1;
473
564
  }
565
+
566
+ try {
567
+ const pkg = JSON.parse(fs.readFileSync(new URL("../../package.json", import.meta.url), "utf8"));
568
+ writeWizardMetadata(installDir, {
569
+ lastRunVersion: pkg.version || "",
570
+ lastRunCommand: "setup",
571
+ lastRunMode: flow === "quickstart" ? "quickstart" : "manual",
572
+ lastRunFlow: "non-interactive",
573
+ });
574
+ } catch {
575
+ // Non-fatal
576
+ }
577
+
474
578
  if (flow !== "quickstart" && buildResult.ollamaConfigured) {
475
579
  // Keep manual mode deterministic in CI while preserving quickstart defaults.
476
580
  return 0;