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 +1 -1
- package/src/cli-main.js +11 -1
- package/src/onboarding/phases/auth.js +108 -28
- package/src/onboarding/platform.js +10 -3
- package/src/onboarding/wizard.js +50 -8
package/package.json
CHANGED
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
|
-
|
|
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: "
|
|
85
|
+
label: "Sonnet 4.6 (Anthropic)",
|
|
91
86
|
description: "Strong general-purpose default.",
|
|
87
|
+
group: "Anthropic",
|
|
92
88
|
},
|
|
93
89
|
{
|
|
94
|
-
key: "
|
|
95
|
-
id: "openrouter/
|
|
96
|
-
label: "
|
|
97
|
-
description: "
|
|
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: "
|
|
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: "
|
|
109
|
-
id: "openai-codex/gpt-4
|
|
110
|
-
label: "GPT-4
|
|
111
|
-
description: "Latest
|
|
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) =>
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
}
|
package/src/onboarding/wizard.js
CHANGED
|
@@ -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
|
-
//
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
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(
|
|
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"));
|