nemoris 0.1.5 → 0.1.7

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/SECURITY.md CHANGED
@@ -23,7 +23,7 @@ You will receive an acknowledgement within 48 hours. We aim to provide a substan
23
23
 
24
24
  Nemoris runs locally on your machine. Key security properties:
25
25
 
26
- - **API keys** are stored in `~/.nemoris/.env` with `0600` permissions (owner read/write only)
26
+ - **API keys** are stored in `~/.nemoris-data/.env` with `0600` permissions (owner read/write only)
27
27
  - **Keys are never logged** — the runtime redacts secrets from all log output
28
28
  - **Exec approval gates** require human confirmation before shell commands execute
29
29
  - **SSRF protection** on all URL-intake surfaces
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nemoris",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
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",
@@ -11,16 +11,21 @@ function dedupe(items = []) {
11
11
 
12
12
  export function resolveInstallDir({ env = process.env, cwd: _cwd = process.cwd() } = {}) {
13
13
  if (env.NEMORIS_INSTALL_DIR) {
14
- return env.NEMORIS_INSTALL_DIR;
15
- }
16
- if (_cwd) {
17
- const hasPackageJson = fs.existsSync(path.join(_cwd, "package.json"));
18
- const hasCliEntry = fs.existsSync(path.join(_cwd, "src", "cli.js"));
19
- if (hasPackageJson && hasCliEntry) {
20
- return _cwd;
14
+ const resolved = env.NEMORIS_INSTALL_DIR;
15
+ if (fs.existsSync(path.join(resolved, "package.json"))) {
16
+ throw new Error(
17
+ `NEMORIS_INSTALL_DIR "${resolved}" looks like a source repo.\n` +
18
+ `Set NEMORIS_INSTALL_DIR to a clean data directory (e.g. ~/.nemoris-data).`
19
+ );
21
20
  }
21
+ return resolved;
22
+ }
23
+ // Dev mode: when CWD is the source repo, use it as install dir
24
+ if (_cwd && fs.existsSync(path.join(_cwd, "package.json")) &&
25
+ fs.existsSync(path.join(_cwd, "src", "cli.js"))) {
26
+ return _cwd;
22
27
  }
23
- return path.join(os.homedir(), ".nemoris");
28
+ return path.join(os.homedir(), ".nemoris-data");
24
29
  }
25
30
 
26
31
  export function resolveAuthProfilesPath({ env = process.env, cwd = process.cwd() } = {}) {
package/src/cli-main.js CHANGED
@@ -53,51 +53,52 @@ import {
53
53
  import { resolveAuthProfilesPath, resolveInstallDir } from "./auth/auth-profiles.js";
54
54
 
55
55
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
56
- const projectRoot = path.join(__dirname, "..");
57
- const stateRoot = path.join(projectRoot, "state", "memory");
58
- const liveRoot = resolveLiveRoot(projectRoot);
56
+ const projectRoot = path.join(__dirname, ".."); // npm package dir — only for package.json reads
57
+ const installDir = resolveRuntimeInstallDir(); // user data dir — all runtime state lives here
58
+ const stateRoot = path.join(installDir, "state", "memory");
59
+ const liveRoot = resolveLiveRoot(installDir);
59
60
  const scheduler = new Scheduler({
60
- projectRoot,
61
+ projectRoot: installDir,
61
62
  liveRoot,
62
- stateRoot: path.join(projectRoot, "state")
63
+ stateRoot: path.join(installDir, "state")
63
64
  });
64
65
  const executor = new Executor({
65
- projectRoot,
66
+ projectRoot: installDir,
66
67
  liveRoot,
67
- stateRoot: path.join(projectRoot, "state")
68
+ stateRoot: path.join(installDir, "state")
68
69
  });
69
70
  const daemon = new SchedulerDaemon({
70
- projectRoot,
71
+ projectRoot: installDir,
71
72
  liveRoot,
72
- stateRoot: path.join(projectRoot, "state")
73
+ stateRoot: path.join(installDir, "state")
73
74
  });
74
75
  const reviewer = new RunReviewer({
75
- stateRoot: path.join(projectRoot, "state")
76
+ stateRoot: path.join(installDir, "state")
76
77
  });
77
78
  const deliveryManager = new DeliveryManager({
78
- projectRoot,
79
+ projectRoot: installDir,
79
80
  liveRoot,
80
- stateRoot: path.join(projectRoot, "state")
81
+ stateRoot: path.join(installDir, "state")
81
82
  });
82
83
  const evaluator = new Evaluator({
83
- projectRoot,
84
+ projectRoot: installDir,
84
85
  liveRoot,
85
- stateRoot: path.join(projectRoot, "state")
86
+ stateRoot: path.join(installDir, "state")
86
87
  });
87
88
  const improvementHarness = new ImprovementHarness({
88
- projectRoot,
89
- stateRoot: path.join(projectRoot, "state"),
89
+ projectRoot: installDir,
90
+ stateRoot: path.join(installDir, "state"),
90
91
  executor,
91
92
  evaluator
92
93
  });
93
94
  const dependencyHealth = new DependencyHealth({
94
- projectRoot,
95
+ projectRoot: installDir,
95
96
  liveRoot
96
97
  });
97
98
  const peerReadinessProbe = new PeerReadinessProbe({ liveRoot });
98
- const embeddingService = new EmbeddingService({ projectRoot });
99
+ const embeddingService = new EmbeddingService({ projectRoot: installDir });
99
100
  const embeddingIndex = new EmbeddingIndex({
100
- rootDir: path.join(projectRoot, "state", "memory"),
101
+ rootDir: path.join(installDir, "state", "memory"),
101
102
  embeddingService
102
103
  });
103
104
 
@@ -465,7 +466,7 @@ async function deliverNotificationFile(mode = "shadow", notificationFilePath = n
465
466
  }
466
467
  const resolvedPath = path.isAbsolute(notificationFilePath)
467
468
  ? notificationFilePath
468
- : path.resolve(projectRoot, notificationFilePath);
469
+ : path.resolve(installDir, notificationFilePath);
469
470
  const result = await deliveryManager.deliverPending({
470
471
  mode,
471
472
  limit: 1,
@@ -506,7 +507,7 @@ async function chooseHandoffPeer(notificationFilePath, peerId, deliveryProfile =
506
507
  }
507
508
  const resolvedPath = path.isAbsolute(notificationFilePath)
508
509
  ? notificationFilePath
509
- : path.resolve(projectRoot, notificationFilePath);
510
+ : path.resolve(installDir, notificationFilePath);
510
511
  const result = await deliveryManager.chooseHandoffPeer(resolvedPath, peerId, {
511
512
  deliveryProfile
512
513
  });
@@ -519,7 +520,7 @@ async function chooseTopHandoffPeer(notificationFilePath, deliveryProfile = null
519
520
  }
520
521
  const resolvedPath = path.isAbsolute(notificationFilePath)
521
522
  ? notificationFilePath
522
- : path.resolve(projectRoot, notificationFilePath);
523
+ : path.resolve(installDir, notificationFilePath);
523
524
  const result = await deliveryManager.chooseTopSuggestedPeer(resolvedPath, {
524
525
  deliveryProfile
525
526
  });
@@ -530,7 +531,7 @@ async function serveTransport(port = "4318") {
530
531
  const authToken = process.env.NEMORIS_TRANSPORT_TOKEN || null;
531
532
  const runtime = await scheduler.loadRuntime();
532
533
  const server = new StandaloneTransportServer({
533
- stateRoot: path.join(projectRoot, "state"),
534
+ stateRoot: path.join(installDir, "state"),
534
535
  authToken,
535
536
  retention: runtime.runtime?.retention?.transportInbox || {},
536
537
  requestTimeoutMs: runtime.runtime?.shutdown?.transportShutdownTimeoutMs || 5000
@@ -1383,7 +1384,7 @@ async function serveDaemon(mode = "dry-run", intervalMs = "30000", installDir =
1383
1384
 
1384
1385
  async function pruneState(scope = "all") {
1385
1386
  const runtime = await scheduler.loadRuntime();
1386
- const statePath = path.join(projectRoot, "state");
1387
+ const statePath = path.join(installDir, "state");
1387
1388
  const operations = [];
1388
1389
 
1389
1390
  if (scope === "all" || scope === "runs") {
@@ -1428,7 +1429,7 @@ async function pruneState(scope = "all") {
1428
1429
 
1429
1430
  async function reviewInbox(limit = "10") {
1430
1431
  const server = new StandaloneTransportServer({
1431
- stateRoot: path.join(projectRoot, "state")
1432
+ stateRoot: path.join(installDir, "state")
1432
1433
  });
1433
1434
  const items = await server.listInbox(Number(limit) || 10);
1434
1435
  console.log(JSON.stringify(items, null, 2));
@@ -1606,7 +1607,7 @@ async function reportFallbackReadiness(jobId = "workspace-health") {
1606
1607
 
1607
1608
  async function configValidate() {
1608
1609
  const config = await scheduler.loadRuntime();
1609
- const schemaResult = validateAllConfigs(config, path.join(projectRoot, "config"));
1610
+ const schemaResult = validateAllConfigs(config, path.join(installDir, "config"));
1610
1611
  console.log(
1611
1612
  JSON.stringify(
1612
1613
  {
@@ -2246,7 +2247,7 @@ async function runtimeStatus(jsonFlag = false) {
2246
2247
 
2247
2248
  async function mcpList() {
2248
2249
  const { ConfigLoader } = await import("./config/loader.js");
2249
- const loader = new ConfigLoader({ rootDir: path.join(projectRoot, "config") });
2250
+ const loader = new ConfigLoader({ rootDir: path.join(installDir, "config") });
2250
2251
  const mcp = await loader.loadMcp();
2251
2252
 
2252
2253
  if (!mcp.servers || Object.keys(mcp.servers).length === 0) {
@@ -2283,9 +2284,9 @@ async function listTools() {
2283
2284
  const { ToolRegistry } = await import("./tools/registry.js");
2284
2285
  const { registerMicroToolHandlers } = await import("./tools/micro/index.js");
2285
2286
  const registry = new ToolRegistry();
2286
- registerMicroToolHandlers(registry, { workspaceRoot: projectRoot });
2287
+ registerMicroToolHandlers(registry, { workspaceRoot: installDir });
2287
2288
  try {
2288
- const toolsDir = path.join(projectRoot, "config", "tools");
2289
+ const toolsDir = path.join(installDir, "config", "tools");
2289
2290
  await registry.loadTools(toolsDir);
2290
2291
  } catch { /* no tools dir is fine */ }
2291
2292
  console.log("Configured tools:\n");
@@ -2306,7 +2307,7 @@ async function addTool(toolId) {
2306
2307
  process.exitCode = 1;
2307
2308
  return;
2308
2309
  }
2309
- const toolsDir = path.join(projectRoot, "config", "tools");
2310
+ const toolsDir = path.join(installDir, "config", "tools");
2310
2311
  await mkdirAsync(toolsDir, { recursive: true });
2311
2312
  const lines = [
2312
2313
  `[tool]`,
@@ -2336,7 +2337,7 @@ async function addTool(toolId) {
2336
2337
  async function listSkills() {
2337
2338
  const { SkillsLoader } = await import("./skills/loader.js");
2338
2339
  const loader = new SkillsLoader();
2339
- const skillsDir = path.join(projectRoot, "config", "skills");
2340
+ const skillsDir = path.join(installDir, "config", "skills");
2340
2341
  await loader.loadSkills(skillsDir);
2341
2342
  const all = loader.listAll();
2342
2343
  if (all.length === 0) {
@@ -2353,8 +2354,8 @@ async function listSkills() {
2353
2354
  async function listImprovements() {
2354
2355
  const { readdir: readdirAsync } = await import("node:fs/promises");
2355
2356
  const { ImprovementEngine } = await import("./runtime/improvement-engine.js");
2356
- const engine = new ImprovementEngine({ stateRoot: path.join(projectRoot, "state") });
2357
- const tuningsRoot = path.join(projectRoot, "state", "tunings");
2357
+ const engine = new ImprovementEngine({ stateRoot: path.join(installDir, "state") });
2358
+ const tuningsRoot = path.join(installDir, "state", "tunings");
2358
2359
  try {
2359
2360
  const jobDirs = await readdirAsync(tuningsRoot);
2360
2361
  let found = false;
@@ -2449,7 +2450,17 @@ export async function main(argv = process.argv) {
2449
2450
  const { pidFile } = getDaemonPaths(installDir);
2450
2451
  const trackedPid = readTrackedPid(pidFile);
2451
2452
  const hasTrackedDaemon = isPidRunning(trackedPid);
2452
- const runningPids = hasTrackedDaemon ? [trackedPid] : await listServeDaemonPids();
2453
+ let runningPids = hasTrackedDaemon ? [trackedPid] : await listServeDaemonPids();
2454
+ // Fallback: check launchd/systemd service if no PID found
2455
+ if (runningPids.length === 0) {
2456
+ try {
2457
+ const { isDaemonRunning } = await import("./onboarding/platform.js");
2458
+ if (isDaemonRunning()) {
2459
+ // Service is running but PID file is missing — proceed anyway
2460
+ runningPids = [0];
2461
+ }
2462
+ } catch { /* platform check unavailable */ }
2463
+ }
2453
2464
  if (runningPids.length === 0) {
2454
2465
  writeOutput(process.stderr, "Daemon not running. Start with: nemoris start");
2455
2466
  process.exitCode = 1;
@@ -2557,7 +2568,7 @@ export async function main(argv = process.argv) {
2557
2568
  const { runMigration } = await import("./runtime/migration.js");
2558
2569
  const dryRun = rest.includes("--dry-run");
2559
2570
  const liveRoot = process.env.NEMORIS_LIVE_ROOT || path.join(process.env.HOME || os.homedir(), ".openclaw");
2560
- const result = await runMigration({ installDir: projectRoot, liveRoot, dryRun });
2571
+ const result = await runMigration({ installDir, liveRoot, dryRun });
2561
2572
  console.log(JSON.stringify(result, null, 2));
2562
2573
  return 0;
2563
2574
  } else if (command === "review-inbox") {
@@ -20,7 +20,7 @@ function defaultSearchPaths() {
20
20
  const installDir = resolveInstallDir();
21
21
  return [
22
22
  path.join(installDir, ".env"),
23
- path.join(os.homedir(), ".nemoris", ".env"),
23
+ path.join(os.homedir(), ".nemoris-data", ".env"),
24
24
  path.join(os.homedir(), ".openclaw", ".env"),
25
25
  ];
26
26
  }
@@ -78,37 +78,66 @@ const PROVIDER_MODEL_PRESETS = {
78
78
  },
79
79
  ],
80
80
  openrouter: [
81
- {
82
- key: "haiku",
83
- id: "openrouter/anthropic/claude-haiku-4-5",
84
- label: "Claude Haiku 4.5",
85
- description: "Lowest-cost OpenRouter default.",
86
- },
81
+ // Anthropic via OpenRouter
87
82
  {
88
83
  key: "sonnet",
89
84
  id: "openrouter/anthropic/claude-sonnet-4-6",
90
- label: "Claude Sonnet 4.6",
85
+ label: "Sonnet 4.6 (Anthropic)",
91
86
  description: "Strong general-purpose default.",
87
+ group: "Anthropic",
92
88
  },
93
89
  {
94
- key: "gpt4o",
95
- id: "openrouter/openai/gpt-4o",
96
- label: "GPT-4o",
97
- description: "Fast multimodal OpenAI route through OpenRouter.",
90
+ key: "haiku",
91
+ id: "openrouter/anthropic/claude-haiku-4-5",
92
+ label: "Haiku 4.5 (Anthropic)",
93
+ description: "Fastest and cheapest Anthropic model.",
94
+ group: "Anthropic",
98
95
  },
99
96
  {
100
97
  key: "opus",
101
98
  id: "openrouter/anthropic/claude-opus-4-6",
102
- label: "Claude Opus 4.6",
99
+ label: "Opus 4.6 (Anthropic)",
103
100
  description: "Highest quality. Use when Sonnet isn't enough.",
101
+ group: "Anthropic",
102
+ },
103
+ // OpenAI via OpenRouter
104
+ {
105
+ key: "gpt4o",
106
+ id: "openrouter/openai/gpt-4o",
107
+ label: "GPT-4o (OpenAI)",
108
+ description: "Fast multimodal OpenAI model.",
109
+ group: "OpenAI",
110
+ },
111
+ {
112
+ key: "gpt4omini",
113
+ id: "openrouter/openai/gpt-4o-mini",
114
+ label: "GPT-4o mini (OpenAI)",
115
+ description: "Cheapest OpenAI option.",
116
+ group: "OpenAI",
117
+ },
118
+ // Google via OpenRouter
119
+ {
120
+ key: "gemini31pro",
121
+ id: "openrouter/google/gemini-3.1-pro",
122
+ label: "Gemini 3.1 Pro (Google)",
123
+ description: "Frontier Google model, strong benchmarks.",
124
+ group: "Google",
125
+ },
126
+ // Meta via OpenRouter
127
+ {
128
+ key: "llama4scout",
129
+ id: "openrouter/meta-llama/llama-4-scout",
130
+ label: "Llama 4 Scout (Meta)",
131
+ description: "Open-weight Meta model.",
132
+ group: "Meta",
104
133
  },
105
134
  ],
106
135
  openai: [
107
136
  {
108
- key: "gpt41",
109
- id: "openai-codex/gpt-4.1",
110
- label: "GPT-4.1",
111
- description: "Latest large-context default.",
137
+ key: "gpt54",
138
+ id: "openai-codex/gpt-5.4",
139
+ label: "GPT-5.4",
140
+ description: "Latest flagship model.",
112
141
  },
113
142
  {
114
143
  key: "gpt4o",
@@ -116,18 +145,18 @@ const PROVIDER_MODEL_PRESETS = {
116
145
  label: "GPT-4o",
117
146
  description: "Fast multimodal fallback.",
118
147
  },
148
+ {
149
+ key: "gpt4omini",
150
+ id: "openai-codex/gpt-4o-mini",
151
+ label: "GPT-4o mini",
152
+ description: "Cheapest and fastest.",
153
+ },
119
154
  {
120
155
  key: "o4mini",
121
156
  id: "openai-codex/o4-mini",
122
157
  label: "o4-mini",
123
158
  description: "Efficient reasoning path.",
124
159
  },
125
- {
126
- key: "o3",
127
- id: "openai-codex/o3",
128
- label: "o3",
129
- description: "Most capable reasoning option.",
130
- },
131
160
  ],
132
161
  };
133
162
 
@@ -275,9 +304,26 @@ async function buildProviderSelectionOptions(provider, key, { fetchImpl = global
275
304
  : curated;
276
305
  const curatedIds = new Set(curatedAvailable.map((item) => item.id));
277
306
 
278
- // Remaining fetched models not already shown as curated
307
+ // Remaining fetched models not already shown as curated.
308
+ // For providers with huge catalogs (OpenRouter: 1000+ models), filter to
309
+ // well-known vendors and cap the list to avoid an unusable wall of options.
310
+ const KNOWN_VENDORS = new Set([
311
+ "anthropic", "openai", "google", "meta-llama", "mistralai",
312
+ "deepseek", "cohere", "qwen", "microsoft", "nvidia",
313
+ ]);
314
+ const MAX_EXTRA = 20;
279
315
  const extra = fetchedModels
280
- .filter((m) => !curatedIds.has(m.id))
316
+ .filter((m) => {
317
+ if (curatedIds.has(m.id)) return false;
318
+ // For large catalogs, only show models from known vendors
319
+ if (fetchedModels.length > 50) {
320
+ const rawId = m.id.replace(/^openrouter\//, "").replace(/^openai-codex\//, "").replace(/^anthropic\//, "");
321
+ const vendor = rawId.split("/")[0];
322
+ return KNOWN_VENDORS.has(vendor);
323
+ }
324
+ return true;
325
+ })
326
+ .slice(0, MAX_EXTRA)
281
327
  .map((m) => {
282
328
  const displayId = m.id
283
329
  .replace(/^openrouter\//, "")
@@ -288,25 +334,49 @@ async function buildProviderSelectionOptions(provider, key, { fetchImpl = global
288
334
  return { value: m.id, label: displayId, description: desc };
289
335
  });
290
336
 
337
+ // Detect new models matching known patterns that aren't in the curated list
338
+ const NEW_MODEL_PATTERNS = [
339
+ /claude-.*-[5-9]-/, // future Claude major versions
340
+ /gpt-[6-9]/, // future GPT major versions
341
+ /gpt-5\.[5-9]/, // future GPT-5.x minor versions
342
+ /gemini-[4-9]/, // future Gemini major versions
343
+ /llama-[5-9]/, // future Llama major versions
344
+ ];
345
+ const newModels = fetchedModels
346
+ .filter((m) => {
347
+ if (curatedIds.has(m.id)) return false;
348
+ const rawId = stripProviderPrefix(provider, m.id);
349
+ return NEW_MODEL_PATTERNS.some((p) => p.test(rawId));
350
+ })
351
+ .map((m) => {
352
+ const displayId = stripProviderPrefix(provider, m.id);
353
+ const ctxStr = formatContextWindow(m.contextWindow);
354
+ const desc = ctxStr ? `New model \u00b7 ${ctxStr}` : "New model";
355
+ return { value: m.id, label: `\u2605 ${m.name || displayId}`, description: desc };
356
+ });
357
+
291
358
  const options = [
359
+ ...newModels,
292
360
  ...curatedAvailable.map((item) => {
293
361
  const fetched = fetchedMap.get(item.id);
294
362
  const ctxStr = fetched ? formatContextWindow(fetched.contextWindow) : "";
295
363
  const desc = ctxStr ? `${item.description} \u00b7 ${ctxStr}` : item.description;
296
364
  return { value: item.id, label: item.label, description: desc };
297
365
  }),
298
- ...extra,
366
+ { value: "__show_all__", label: "\u2193 Show all models", description: `Browse all ${fetchedModels.length} models from provider.` },
299
367
  { value: "__custom__", label: "Enter a different model name...", description: "Use a specific model id not shown in the list." },
300
368
  ];
301
369
 
302
- return { options, fetchedModels };
370
+ // Extra models only shown when "Show all" is selected
371
+ return { options, fetchedModels, extra };
303
372
  }
304
373
 
305
374
  async function promptForProviderModels(provider, key, tui, { fetchImpl = globalThis.fetch } = {}) {
306
375
  const { select, prompt, dim, cyan } = tui;
307
376
  if (!select) return { chosen: [], fetchedModels: [] };
308
377
 
309
- const { options, fetchedModels } = await buildProviderSelectionOptions(provider, key, { fetchImpl });
378
+ const { options, fetchedModels, extra } = await buildProviderSelectionOptions(provider, key, { fetchImpl });
379
+ let activeOptions = options;
310
380
  const chosen = [];
311
381
  const manualOptionValue = "__custom__";
312
382
  const defaultModelValue = options.find((item) => !String(item.value).startsWith("__"))?.value || "";
@@ -315,7 +385,7 @@ async function promptForProviderModels(provider, key, tui, { fetchImpl = globalT
315
385
  console.log(` ${dim("Pick up to three models. The first is your default. The others are fallbacks when the default is slow or unavailable.")}`);
316
386
 
317
387
  while (chosen.length < 3) {
318
- const remaining = options.filter((item) => !chosen.includes(item.value));
388
+ const remaining = activeOptions.filter((item) => !chosen.includes(item.value));
319
389
  const pickerOptions = remaining.map((item) => ({
320
390
  label: item.label,
321
391
  value: item.value,
@@ -342,6 +412,16 @@ async function promptForProviderModels(provider, key, tui, { fetchImpl = globalT
342
412
  break;
343
413
  }
344
414
 
415
+ if (picked === "__show_all__") {
416
+ // Expand to full filtered list + custom entry
417
+ activeOptions = [
418
+ ...activeOptions.filter((o) => !o.value.startsWith("__")),
419
+ ...(extra || []),
420
+ { value: "__custom__", label: "Enter a different model name...", description: "Use a specific model id not shown in the list." },
421
+ ];
422
+ continue;
423
+ }
424
+
345
425
  let modelId = picked;
346
426
  if (picked === manualOptionValue) {
347
427
  const custom = await prompt("Model id", stripProviderPrefix(provider, defaultModelValue));
@@ -354,11 +354,18 @@ export function isDaemonRunning() {
354
354
 
355
355
  if (platform === "darwin") {
356
356
  try {
357
- const { status } = spawnSync(
357
+ const { status, stdout } = spawnSync(
358
358
  "launchctl", ["list", "ai.nemoris.daemon"],
359
- { timeout: 3000, stdio: "pipe" },
359
+ { timeout: 3000, stdio: "pipe", encoding: "utf8" },
360
360
  );
361
- return status === 0;
361
+ if (status !== 0) return false;
362
+ // Service is loaded — verify the process is actually alive.
363
+ // launchctl list <label> includes "PID" = <number> when running.
364
+ const pidMatch = String(stdout || "").match(/"PID"\s*=\s*(\d+)/);
365
+ if (!pidMatch) return false;
366
+ const pid = Number.parseInt(pidMatch[1], 10);
367
+ if (!pid || pid <= 0) return false;
368
+ try { process.kill(pid, 0); return true; } catch { return false; }
362
369
  } catch {
363
370
  return false;
364
371
  }
@@ -200,6 +200,20 @@ async function launchChat() {
200
200
  async function runFastPathWizard({ installDir }) {
201
201
  const prompter = createClackPrompter();
202
202
 
203
+ // ASCII banner
204
+ const BRAND = "\x1b[38;2;45;212;191m";
205
+ const RESET = "\x1b[0m";
206
+ console.log([
207
+ "",
208
+ `${BRAND} ███╗ ██╗███████╗███╗ ███╗ ██████╗ ██████╗ ██╗███████╗${RESET}`,
209
+ `${BRAND} ████╗ ██║██╔════╝████╗ ████║██╔═══██╗██╔══██╗██║██╔════╝${RESET}`,
210
+ `${BRAND} ██╔██╗ ██║█████╗ ██╔████╔██║██║ ██║██████╔╝██║███████╗${RESET}`,
211
+ `${BRAND} ██║╚██╗██║██╔══╝ ██║╚██╔╝██║██║ ██║██╔══██╗██║╚════██║${RESET}`,
212
+ `${BRAND} ██║ ╚████║███████╗██║ ╚═╝ ██║╚██████╔╝██║ ██║██║███████║${RESET}`,
213
+ `${BRAND} ╚═╝ ╚═══╝╚══════╝╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚═╝╚══════╝${RESET}`,
214
+ "",
215
+ ].join("\n"));
216
+
203
217
  await prompter.intro("nemoris setup");
204
218
 
205
219
  // D2 security gate — brief consent
@@ -266,6 +280,20 @@ async function runFastPathWizard({ installDir }) {
266
280
  });
267
281
  }
268
282
 
283
+ // Ask consent to use detected API key
284
+ const detectedKey = detection.apiKeys?.[provider];
285
+ if (detectedKey) {
286
+ const masked = detectedKey.slice(0, 6) + "..." + detectedKey.slice(-4);
287
+ const useDetected = await prompter.confirm({
288
+ message: `Found ${provider} key in env (${masked}) — use it?`,
289
+ initialValue: true,
290
+ });
291
+ if (!useDetected) {
292
+ // Clear detected key so auth phase will prompt for a new one
293
+ delete detection.apiKeys[provider];
294
+ }
295
+ }
296
+
269
297
  // Step 4: Auth + model selection
270
298
  writeIdentity({
271
299
  installDir,
@@ -286,15 +314,29 @@ async function runFastPathWizard({ installDir }) {
286
314
  });
287
315
  }
288
316
 
289
- // Auto-start daemon (best-effort)
290
- try {
291
- await writeDaemonUnit(installDir);
292
- await loadDaemon(installDir);
293
- } catch {
294
- // Daemon start failure is non-fatal in fast path
295
- }
317
+ // Post-setup guidance
318
+ const { buildSetupChecklist, formatSetupChecklist } = await import("./setup-checklist.js");
319
+ const checklist = buildSetupChecklist(installDir);
320
+ const allConfigured = Object.values(checklist).every((c) => c.configured);
321
+
322
+ await prompter.note(
323
+ [
324
+ `Agent "${agentName}" is ready.`,
325
+ "",
326
+ "Next steps:",
327
+ " nemoris start Start the daemon",
328
+ " nemoris chat Open interactive chat",
329
+ ...(allConfigured ? [] : [
330
+ "",
331
+ "Optional:",
332
+ ...(!checklist.telegram.configured ? [" nemoris setup telegram Connect Telegram"] : []),
333
+ ...(!checklist.ollama.configured ? [" nemoris setup ollama Add local models"] : []),
334
+ ]),
335
+ ].join("\n"),
336
+ "Setup Complete"
337
+ );
296
338
 
297
- await prompter.outro(`Agent "${agentName}" is ready.\n Run: nemoris chat`);
339
+ await prompter.outro("Run: nemoris start");
298
340
 
299
341
  try {
300
342
  const pkg = JSON.parse(fs.readFileSync(new URL("../../package.json", import.meta.url), "utf8"));
@@ -7,7 +7,7 @@ export function resolveSocketPath({ platform = process.platform, stateDir, homed
7
7
  if (platform === "win32") {
8
8
  return "\\\\.\\pipe\\nemoris-daemon";
9
9
  }
10
- const root = stateDir || path.join(homedir(), ".nemoris", "state");
10
+ const root = stateDir || path.join(homedir(), ".nemoris-data", "state");
11
11
  return path.join(root, "daemon.sock");
12
12
  }
13
13