nemoris 0.1.6 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nemoris",
3
- "version": "0.1.6",
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",
package/src/cli-main.js CHANGED
@@ -2450,7 +2450,17 @@ export async function main(argv = process.argv) {
2450
2450
  const { pidFile } = getDaemonPaths(installDir);
2451
2451
  const trackedPid = readTrackedPid(pidFile);
2452
2452
  const hasTrackedDaemon = isPidRunning(trackedPid);
2453
- 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
+ }
2454
2464
  if (runningPids.length === 0) {
2455
2465
  writeOutput(process.stderr, "Daemon not running. Start with: nemoris start");
2456
2466
  process.exitCode = 1;
@@ -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"));