nemoris 0.1.6 → 0.1.9
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/runtime-control.js +30 -5
- package/src/cli-main.js +26 -2
- package/src/onboarding/phases/auth.js +108 -28
- package/src/onboarding/platform.js +10 -3
- package/src/onboarding/setup-checklist.js +18 -4
- package/src/onboarding/wizard.js +70 -8
- package/src/utils/env-loader.js +28 -0
package/package.json
CHANGED
|
@@ -487,24 +487,49 @@ export async function cleanupOrphanedDaemonProcesses({
|
|
|
487
487
|
|
|
488
488
|
export async function startDirectDaemon({
|
|
489
489
|
projectRoot,
|
|
490
|
-
cliEntryPath
|
|
490
|
+
cliEntryPath,
|
|
491
491
|
fsImpl = fs,
|
|
492
492
|
spawnImpl = spawn,
|
|
493
493
|
processImpl = process,
|
|
494
494
|
} = {}) {
|
|
495
|
+
// Resolve CLI entry: explicit path > source repo > which nemoris > package __dirname
|
|
496
|
+
if (!cliEntryPath) {
|
|
497
|
+
const srcCliPath = path.join(projectRoot, "src", "cli.js");
|
|
498
|
+
if (fsImpl.existsSync(srcCliPath)) {
|
|
499
|
+
cliEntryPath = srcCliPath;
|
|
500
|
+
} else {
|
|
501
|
+
try {
|
|
502
|
+
const { execFileSync } = await import("node:child_process");
|
|
503
|
+
const whichOut = execFileSync("which", ["nemoris"], { encoding: "utf8", timeout: 3000 }).trim();
|
|
504
|
+
if (whichOut) {
|
|
505
|
+
cliEntryPath = whichOut;
|
|
506
|
+
}
|
|
507
|
+
} catch { /* which not available */ }
|
|
508
|
+
}
|
|
509
|
+
if (!cliEntryPath) {
|
|
510
|
+
// Fallback: resolve from this module's own package
|
|
511
|
+
const __dirname = path.dirname(new URL(import.meta.url).pathname);
|
|
512
|
+
cliEntryPath = path.join(__dirname, "..", "cli.js");
|
|
513
|
+
}
|
|
514
|
+
}
|
|
495
515
|
const { stateDir, pidFile, stdoutLog, stderrLog } = getDaemonPaths(projectRoot);
|
|
496
516
|
fsImpl.mkdirSync(stateDir, { recursive: true });
|
|
497
517
|
const stdoutFd = fsImpl.openSync(stdoutLog, "a");
|
|
498
518
|
const stderrFd = fsImpl.openSync(stderrLog, "a");
|
|
499
519
|
|
|
500
520
|
try {
|
|
521
|
+
// Only set NEMORIS_INSTALL_DIR for the child when the path is a clean
|
|
522
|
+
// data directory. If it's the source repo (has package.json), the child
|
|
523
|
+
// will detect it via CWD detection instead — avoids the source repo guard.
|
|
524
|
+
const childEnv = { ...processImpl.env };
|
|
525
|
+
const looksLikeSourceRepo = fsImpl.existsSync(path.join(projectRoot, "package.json"));
|
|
526
|
+
if (!looksLikeSourceRepo) {
|
|
527
|
+
childEnv.NEMORIS_INSTALL_DIR = projectRoot;
|
|
528
|
+
}
|
|
501
529
|
const child = spawnImpl(processImpl.execPath, [cliEntryPath, "serve-daemon", "live"], {
|
|
502
530
|
cwd: projectRoot,
|
|
503
531
|
detached: true,
|
|
504
|
-
env:
|
|
505
|
-
...processImpl.env,
|
|
506
|
-
NEMORIS_INSTALL_DIR: projectRoot,
|
|
507
|
-
},
|
|
532
|
+
env: childEnv,
|
|
508
533
|
stdio: ["ignore", stdoutFd, stderrFd],
|
|
509
534
|
});
|
|
510
535
|
child.unref?.();
|
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;
|
|
@@ -2735,13 +2745,27 @@ export async function main(argv = process.argv) {
|
|
|
2735
2745
|
});
|
|
2736
2746
|
} else if (command === "doctor") {
|
|
2737
2747
|
const { runDoctor, formatDoctorReport } = await import("./onboarding/doctor.js");
|
|
2738
|
-
const
|
|
2748
|
+
const doctorInstallDir = resolveRuntimeInstallDir();
|
|
2749
|
+
const results = await runDoctor(doctorInstallDir);
|
|
2739
2750
|
const json = rest.includes("--json");
|
|
2751
|
+
const fix = rest.includes("--fix");
|
|
2740
2752
|
if (json) {
|
|
2741
2753
|
console.log(JSON.stringify(results, null, 2));
|
|
2742
2754
|
} else {
|
|
2743
2755
|
console.log(formatDoctorReport(results));
|
|
2744
2756
|
}
|
|
2757
|
+
// Auto-repair: clean stale PID file
|
|
2758
|
+
if (fix && results.daemon?.stalePidFile) {
|
|
2759
|
+
const { pidFile } = getDaemonPaths(doctorInstallDir);
|
|
2760
|
+
try {
|
|
2761
|
+
fs.unlinkSync(pidFile);
|
|
2762
|
+
console.log("\n \u2705 Cleaned stale PID file. Run: nemoris start");
|
|
2763
|
+
} catch {
|
|
2764
|
+
console.log("\n \u274c Could not remove stale PID file.");
|
|
2765
|
+
}
|
|
2766
|
+
} else if (!fix && results.daemon?.stalePidFile) {
|
|
2767
|
+
console.log("\n Tip: run nemoris doctor --fix to clean the stale PID file.");
|
|
2768
|
+
}
|
|
2745
2769
|
return results.exitCode;
|
|
2746
2770
|
} else if (command === "battle") {
|
|
2747
2771
|
const { runBattle, parseBattleFlags } = await import("./battle.js");
|
|
@@ -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
|
}
|
|
@@ -41,7 +41,18 @@ function detectProvider(installDir) {
|
|
|
41
41
|
const files = fs.readdirSync(providersDir).filter((f) => f.endsWith(".toml"));
|
|
42
42
|
if (files.length === 0) return { configured: false };
|
|
43
43
|
const name = files[0].replace(/\.toml$/, "");
|
|
44
|
-
|
|
44
|
+
// Read primary model from the first provider config
|
|
45
|
+
let primaryModel = "";
|
|
46
|
+
try {
|
|
47
|
+
const content = fs.readFileSync(path.join(providersDir, files[0]), "utf8");
|
|
48
|
+
const config = parseToml(content);
|
|
49
|
+
const models = config.models || {};
|
|
50
|
+
const firstModelKey = Object.keys(models)[0];
|
|
51
|
+
if (firstModelKey && models[firstModelKey]?.id) {
|
|
52
|
+
primaryModel = models[firstModelKey].id;
|
|
53
|
+
}
|
|
54
|
+
} catch { /* non-fatal */ }
|
|
55
|
+
return { configured: true, name, count: files.length, primaryModel };
|
|
45
56
|
} catch {
|
|
46
57
|
return { configured: false };
|
|
47
58
|
}
|
|
@@ -96,9 +107,12 @@ export function formatSetupChecklist(checklist) {
|
|
|
96
107
|
: "Identity";
|
|
97
108
|
item(checklist.identity, idLabel, "nemoris setup");
|
|
98
109
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
110
|
+
let provLabel = "Provider";
|
|
111
|
+
if (checklist.provider.configured) {
|
|
112
|
+
const model = checklist.provider.primaryModel || "";
|
|
113
|
+
const modelSuffix = model ? ` \u00b7 ${model}` : "";
|
|
114
|
+
provLabel = `Provider ${checklist.provider.name}${modelSuffix}`;
|
|
115
|
+
}
|
|
102
116
|
item(checklist.provider, provLabel, "nemoris setup");
|
|
103
117
|
|
|
104
118
|
item(checklist.telegram, "Telegram", "nemoris setup telegram");
|
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,24 +314,58 @@ async function runFastPathWizard({ installDir }) {
|
|
|
286
314
|
});
|
|
287
315
|
}
|
|
288
316
|
|
|
289
|
-
//
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
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
|
+
);
|
|
338
|
+
|
|
339
|
+
// Offer Telegram setup inline
|
|
340
|
+
if (!checklist.telegram.configured) {
|
|
341
|
+
const wantTelegram = await prompter.confirm({
|
|
342
|
+
message: "Set up Telegram now?",
|
|
343
|
+
initialValue: false,
|
|
344
|
+
});
|
|
345
|
+
if (wantTelegram) {
|
|
346
|
+
await runTelegramPhase({
|
|
347
|
+
installDir,
|
|
348
|
+
agentId,
|
|
349
|
+
});
|
|
350
|
+
}
|
|
295
351
|
}
|
|
296
352
|
|
|
297
|
-
await prompter.outro(
|
|
353
|
+
await prompter.outro("Run: nemoris start");
|
|
298
354
|
|
|
299
355
|
try {
|
|
300
356
|
const pkg = JSON.parse(fs.readFileSync(new URL("../../package.json", import.meta.url), "utf8"));
|
|
357
|
+
const caps = { identity: true, provider: true };
|
|
358
|
+
// Re-check telegram after possible inline setup
|
|
359
|
+
const updatedChecklist = buildSetupChecklist(installDir);
|
|
360
|
+
if (updatedChecklist.telegram.configured || updatedChecklist.telegram.pending) {
|
|
361
|
+
caps.telegram = true;
|
|
362
|
+
}
|
|
301
363
|
writeWizardMetadata(installDir, {
|
|
302
364
|
lastRunVersion: pkg.version || "",
|
|
303
365
|
lastRunCommand: "setup",
|
|
304
366
|
lastRunMode: "fast",
|
|
305
367
|
lastRunFlow: "interactive",
|
|
306
|
-
capabilities:
|
|
368
|
+
capabilities: caps,
|
|
307
369
|
});
|
|
308
370
|
} catch {
|
|
309
371
|
// Non-fatal
|
package/src/utils/env-loader.js
CHANGED
|
@@ -1,5 +1,33 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
1
2
|
import path from "node:path";
|
|
3
|
+
import os from "node:os";
|
|
2
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Resolve the .env file path. Priority:
|
|
7
|
+
* 1. NEMORIS_INSTALL_DIR/.env (explicit data dir)
|
|
8
|
+
* 2. CWD/.env (dev mode — running from source repo)
|
|
9
|
+
* 3. ~/.nemoris-data/.env (default install dir)
|
|
10
|
+
* 4. package dir/.env (legacy fallback)
|
|
11
|
+
*/
|
|
3
12
|
export function resolveEnvPath(srcDir) {
|
|
13
|
+
// 1. Explicit install dir
|
|
14
|
+
if (process.env.NEMORIS_INSTALL_DIR) {
|
|
15
|
+
return path.join(process.env.NEMORIS_INSTALL_DIR, ".env");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// 2. Dev mode — CWD is source repo
|
|
19
|
+
const cwd = process.cwd();
|
|
20
|
+
if (fs.existsSync(path.join(cwd, "package.json")) &&
|
|
21
|
+
fs.existsSync(path.join(cwd, "src", "cli.js"))) {
|
|
22
|
+
return path.join(cwd, ".env");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// 3. Default data dir
|
|
26
|
+
const defaultEnv = path.join(os.homedir(), ".nemoris-data", ".env");
|
|
27
|
+
if (fs.existsSync(defaultEnv)) {
|
|
28
|
+
return defaultEnv;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// 4. Legacy: package dir
|
|
4
32
|
return path.resolve(srcDir, "..", ".env");
|
|
5
33
|
}
|