nemoris 0.1.18 → 0.1.20

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.18",
3
+ "version": "0.1.20",
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",
@@ -73,7 +73,9 @@
73
73
  "publish:check": "node scripts/check-publish-dry-run.js",
74
74
  "status": "node src/cli.js runtime-status",
75
75
  "test": "node --test",
76
- "test:e2e": "node tests/e2e/run-report.js"
76
+ "test:e2e": "node tests/e2e/run-report.js",
77
+ "test:e2e:interactive": "node --test tests/e2e/onboarding-interactive.test.js",
78
+ "test:e2e:setup-sweep": "node scripts/e2e-setup-sweep.js"
77
79
  },
78
80
  "engines": {
79
81
  "node": ">=22.5.0"
@@ -20,12 +20,17 @@ export function resolveInstallDir({ env = process.env, cwd: _cwd = process.cwd()
20
20
  }
21
21
  return resolved;
22
22
  }
23
- // Dev mode: when CWD is the source repo, use it as install dir
23
+ // Default data dir prefer this if it exists with actual config
24
+ const defaultDataDir = path.join(os.homedir(), ".nemoris-data");
25
+ if (fs.existsSync(path.join(defaultDataDir, "config"))) {
26
+ return defaultDataDir;
27
+ }
28
+ // Dev mode: when CWD is the source repo and no ~/.nemoris-data config exists
24
29
  if (_cwd && fs.existsSync(path.join(_cwd, "package.json")) &&
25
30
  fs.existsSync(path.join(_cwd, "src", "cli.js"))) {
26
31
  return _cwd;
27
32
  }
28
- return path.join(os.homedir(), ".nemoris-data");
33
+ return defaultDataDir;
29
34
  }
30
35
 
31
36
  export function resolveAuthProfilesPath({ env = process.env, cwd = process.cwd() } = {}) {
@@ -143,19 +143,59 @@ export async function validateApiKey(provider, key, options = {}) {
143
143
  }
144
144
 
145
145
  const fetch = options.fetchImpl || globalThis.fetch;
146
- const providerTargets = {
147
- anthropic: {
148
- url: "https://api.anthropic.com/v1/messages/count_tokens",
149
- method: "POST",
150
- headers: {
151
- "content-type": "application/json",
152
- ...buildAnthropicAuthHeaders(key)
146
+ if (provider === "anthropic") {
147
+ const probes = [
148
+ {
149
+ url: "https://api.anthropic.com/v1/models",
150
+ method: "GET",
151
+ headers: {
152
+ "x-api-key": key,
153
+ "anthropic-version": "2023-06-01"
154
+ }
153
155
  },
154
- body: JSON.stringify({
155
- model: "claude-haiku-4-5",
156
- messages: [{ role: "user", content: "ping" }]
157
- })
158
- },
156
+ {
157
+ url: "https://api.anthropic.com/v1/models",
158
+ method: "GET",
159
+ headers: {
160
+ Authorization: `Bearer ${key}`,
161
+ "anthropic-version": "2023-06-01"
162
+ }
163
+ },
164
+ {
165
+ url: "https://api.anthropic.com/v1/messages/count_tokens",
166
+ method: "POST",
167
+ headers: {
168
+ "content-type": "application/json",
169
+ ...buildAnthropicAuthHeaders(key)
170
+ },
171
+ body: JSON.stringify({
172
+ model: "claude-haiku-4-5",
173
+ messages: [{ role: "user", content: "ping" }]
174
+ })
175
+ }
176
+ ];
177
+
178
+ let lastFailure = { ok: false, error: "request failed" };
179
+ for (const probe of probes) {
180
+ try {
181
+ const response = await fetch(probe.url, {
182
+ method: probe.method,
183
+ headers: probe.headers,
184
+ body: probe.body,
185
+ signal: AbortSignal.timeout(10000)
186
+ });
187
+ if (response.ok) {
188
+ return { ok: true, status: response.status };
189
+ }
190
+ lastFailure = { ok: false, status: response.status };
191
+ } catch (error) {
192
+ lastFailure = { ok: false, error: error.message };
193
+ }
194
+ }
195
+ return lastFailure;
196
+ }
197
+
198
+ const providerTargets = {
159
199
  openai: {
160
200
  url: "https://api.openai.com/v1/models",
161
201
  method: "GET",
@@ -23,6 +23,27 @@ function normalizeOptions(options = []) {
23
23
  }));
24
24
  }
25
25
 
26
+ function normalizeSearchTokens(search = "") {
27
+ return String(search)
28
+ .toLowerCase()
29
+ .split(/\s+/)
30
+ .map((token) => token.trim())
31
+ .filter(Boolean);
32
+ }
33
+
34
+ function buildOptionSearchText(option = {}) {
35
+ return `${option.label || ""} ${option.hint || ""} ${option.value || ""}`.toLowerCase();
36
+ }
37
+
38
+ function tokenizedOptionFilter(search, option) {
39
+ const tokens = normalizeSearchTokens(search);
40
+ if (tokens.length === 0) {
41
+ return true;
42
+ }
43
+ const haystack = buildOptionSearchText(option);
44
+ return tokens.every((token) => haystack.includes(token));
45
+ }
46
+
26
47
  export function createClackPrompter() {
27
48
  return {
28
49
  intro(message) {
@@ -48,12 +69,20 @@ export function createClackPrompter() {
48
69
  }));
49
70
  },
50
71
  async multiselect({ message, options, initialValues = [], required = false }) {
51
- const value = guardCancel(await p.multiselect({
52
- message,
53
- options: normalizeOptions(options),
54
- initialValues,
55
- required,
56
- }));
72
+ const normalized = normalizeOptions(options);
73
+ const value = guardCancel(await (required || !p.autocompleteMultiselect
74
+ ? p.multiselect({
75
+ message,
76
+ options: normalized,
77
+ initialValues,
78
+ required,
79
+ })
80
+ : p.autocompleteMultiselect({
81
+ message,
82
+ options: normalized,
83
+ initialValues,
84
+ filter: tokenizedOptionFilter,
85
+ })));
57
86
  return Array.isArray(value) ? value : [];
58
87
  },
59
88
  async text({ message, placeholder, initialValue, validate }) {
@@ -10,18 +10,18 @@ export const PROVIDER_ENV_KEYS = {
10
10
  export const CURATED_MODELS = {
11
11
  anthropic: [
12
12
  {
13
- value: "anthropic/claude-haiku-4-5",
14
- label: "claude-haiku-4-5",
15
- hint: "ctx 200k · fast · cheapest",
13
+ value: "anthropic/claude-sonnet-4.6",
14
+ label: "claude-sonnet-4.6",
15
+ hint: "ctx 200k · balanced · recommended",
16
16
  },
17
17
  {
18
- value: "anthropic/claude-sonnet-4-6",
19
- label: "claude-sonnet-4-6",
20
- hint: "ctx 200k · balanced · recommended",
18
+ value: "anthropic/claude-haiku-4.5",
19
+ label: "claude-haiku-4.5",
20
+ hint: "ctx 200k · fast · cheapest",
21
21
  },
22
22
  {
23
- value: "anthropic/claude-opus-4-6",
24
- label: "claude-opus-4-6",
23
+ value: "anthropic/claude-opus-4.6",
24
+ label: "claude-opus-4.6",
25
25
  hint: "ctx 200k · most capable · expensive",
26
26
  },
27
27
  ],
@@ -50,13 +50,13 @@ export const CURATED_MODELS = {
50
50
  openrouter: [
51
51
  // ── Top picks (most popular on OpenRouter) ──
52
52
  {
53
- value: "openrouter/anthropic/claude-sonnet-4-6",
54
- label: "anthropic/claude-sonnet-4-6",
53
+ value: "openrouter/anthropic/claude-sonnet-4.6",
54
+ label: "anthropic/claude-sonnet-4.6",
55
55
  hint: "ctx 200k · balanced · recommended",
56
56
  },
57
57
  {
58
- value: "openrouter/anthropic/claude-haiku-4-5",
59
- label: "anthropic/claude-haiku-4-5",
58
+ value: "openrouter/anthropic/claude-haiku-4.5",
59
+ label: "anthropic/claude-haiku-4.5",
60
60
  hint: "ctx 200k · fast · cheap",
61
61
  },
62
62
  {
@@ -70,8 +70,8 @@ export const CURATED_MODELS = {
70
70
  hint: "ctx 164k · strong · very cheap",
71
71
  },
72
72
  {
73
- value: "openrouter/anthropic/claude-opus-4-6",
74
- label: "anthropic/claude-opus-4-6",
73
+ value: "openrouter/anthropic/claude-opus-4.6",
74
+ label: "anthropic/claude-opus-4.6",
75
75
  hint: "ctx 200k · most capable · expensive",
76
76
  },
77
77
  // ── More options ──