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 +1 -1
- package/package.json +1 -1
- package/src/auth/auth-profiles.js +13 -8
- package/src/cli-main.js +46 -35
- package/src/onboarding/auth/api-key.js +1 -1
- package/src/onboarding/phases/auth.js +108 -28
- package/src/onboarding/platform.js +10 -3
- package/src/onboarding/wizard.js +50 -8
- package/src/tui/socket-client.js +1 -1
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
|
@@ -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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
|
58
|
-
const
|
|
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(
|
|
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(
|
|
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(
|
|
73
|
+
stateRoot: path.join(installDir, "state")
|
|
73
74
|
});
|
|
74
75
|
const reviewer = new RunReviewer({
|
|
75
|
-
stateRoot: path.join(
|
|
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(
|
|
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(
|
|
86
|
+
stateRoot: path.join(installDir, "state")
|
|
86
87
|
});
|
|
87
88
|
const improvementHarness = new ImprovementHarness({
|
|
88
|
-
projectRoot,
|
|
89
|
-
stateRoot: path.join(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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:
|
|
2287
|
+
registerMicroToolHandlers(registry, { workspaceRoot: installDir });
|
|
2287
2288
|
try {
|
|
2288
|
-
const toolsDir = path.join(
|
|
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(
|
|
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(
|
|
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(
|
|
2357
|
-
const tuningsRoot = path.join(
|
|
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
|
-
|
|
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
|
|
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: "
|
|
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"));
|
package/src/tui/socket-client.js
CHANGED
|
@@ -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
|
|