nemoris 0.1.1 → 0.1.2

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/README.md CHANGED
@@ -184,7 +184,7 @@ Requires Node.js >= 22.
184
184
  git clone https://github.com/amzer24/nemoris.git
185
185
  cd nemoris
186
186
  npm install
187
- npm test # 1179 tests
187
+ npm test # 1403 tests
188
188
  npm run test:e2e # End-to-end tests
189
189
  ```
190
190
 
package/SECURITY.md CHANGED
@@ -38,6 +38,22 @@ We follow responsible disclosure practices. If you report a vulnerability:
38
38
  - We will credit you in the release notes (unless you prefer anonymity)
39
39
  - We will coordinate disclosure timing with you
40
40
 
41
+ ## Deployment Boundaries
42
+
43
+ Nemoris is a single operator, single-user runtime designed to run on your own machine.
44
+
45
+ It is not designed to be:
46
+
47
+ - a hardened multi-tenant sandbox
48
+ - a public-facing web service
49
+ - a shared server runtime
50
+
51
+ For recovery procedures, see [docs/RECOVERY-FLOWS.md](docs/RECOVERY-FLOWS.md).
52
+
53
+ ## Vulnerability Tracking
54
+
55
+ Known vulnerabilities are tracked via GitHub Security Advisories on this repository.
56
+
41
57
  ## Dependencies
42
58
 
43
59
  Nemoris keeps dependencies minimal by design. We monitor for known vulnerabilities via `npm audit` and update promptly.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nemoris",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
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",
@@ -24,7 +24,6 @@ import {
24
24
  resolveOpenAICodexAccess,
25
25
  } from "../../auth/openai-codex-oauth.js";
26
26
  import { getAuthProfile, resolveAuthProfilesPath } from "../../auth/auth-profiles.js";
27
- import { buildModelSelectionOptions, fetchOpenAIModels } from "../model-catalog.js";
28
27
 
29
28
  const PROVIDER_CONFIGS = {
30
29
  anthropic: {
@@ -179,25 +178,23 @@ function providerDisplayName(provider) {
179
178
  }
180
179
 
181
180
  async function fetchProviderModelIds(provider, key, { fetchImpl = globalThis.fetch } = {}) {
182
- if (provider === "anthropic") {
183
- return PROVIDER_MODEL_PRESETS.anthropic.map((item) => item.id);
184
- }
185
-
186
- const target = provider === "openrouter"
187
- ? {
188
- url: "https://openrouter.ai/api/v1/models",
189
- headers: { authorization: `Bearer ${key}` },
190
- }
191
- : provider === "openai"
192
- ? {
193
- url: "https://api.openai.com/v1/models",
194
- headers: { authorization: `Bearer ${key}` },
195
- }
196
- : null;
181
+ const targets = {
182
+ anthropic: {
183
+ url: "https://api.anthropic.com/v1/models",
184
+ headers: { "x-api-key": key, "anthropic-version": "2023-06-01" },
185
+ },
186
+ openrouter: {
187
+ url: "https://openrouter.ai/api/v1/models",
188
+ headers: { authorization: `Bearer ${key}` },
189
+ },
190
+ openai: {
191
+ url: "https://api.openai.com/v1/models",
192
+ headers: { authorization: `Bearer ${key}` },
193
+ },
194
+ };
197
195
 
198
- if (!target) {
199
- return [];
200
- }
196
+ const target = targets[provider];
197
+ if (!target) return [];
201
198
 
202
199
  try {
203
200
  const response = await fetchImpl(target.url, {
@@ -206,48 +203,43 @@ async function fetchProviderModelIds(provider, key, { fetchImpl = globalThis.fet
206
203
  signal: AbortSignal.timeout(10000),
207
204
  });
208
205
  const data = await response.json();
209
- if (!response.ok) {
210
- return [];
211
- }
206
+ if (!response.ok) return PROVIDER_MODEL_PRESETS[provider]?.map((item) => item.id) ?? [];
212
207
  const ids = Array.isArray(data?.data)
213
208
  ? data.data.map((item) => item?.id).filter(Boolean)
214
209
  : [];
215
- if (provider === "openrouter") {
216
- return ids.map((id) => ensureProviderModelPrefix(provider, id));
217
- }
218
- if (provider === "openai") {
219
- return ids.map((id) => ensureProviderModelPrefix(provider, id));
220
- }
221
- return ids;
210
+ return ids.map((id) => ensureProviderModelPrefix(provider, id));
222
211
  } catch {
223
- return [];
212
+ return PROVIDER_MODEL_PRESETS[provider]?.map((item) => item.id) ?? [];
224
213
  }
225
214
  }
226
215
 
227
216
  async function buildProviderSelectionOptions(provider, key, { fetchImpl = globalThis.fetch } = {}) {
228
- if (provider === "openai") {
229
- const discoveredModels = await fetchOpenAIModels(key, { fetchImpl });
230
- return buildModelSelectionOptions({
231
- provider,
232
- discoveredModels,
233
- includeKeep: false,
234
- includeManual: true,
235
- }).map((entry) => ({
236
- value: entry.value,
237
- label: entry.label,
238
- description: entry.hint,
239
- }));
240
- }
241
-
242
217
  const curated = PROVIDER_MODEL_PRESETS[provider] || [];
243
- const available = new Set(await fetchProviderModelIds(provider, key, { fetchImpl }));
244
- const selectable = curated.filter((item) => available.size === 0 || available.has(item.id));
245
- const options = selectable.length > 0 ? selectable : curated;
246
- return options.map((item) => ({
247
- value: item.id,
248
- label: item.label,
249
- description: item.description,
250
- }));
218
+ const fetched = await fetchProviderModelIds(provider, key, { fetchImpl });
219
+ const fetchedSet = new Set(fetched);
220
+
221
+ // Curated models that exist in the fetched list (fall back to all curated if fetch failed)
222
+ const curatedAvailable = fetched.length > 0
223
+ ? curated.filter((item) => fetchedSet.has(item.id))
224
+ : curated;
225
+ const curatedIds = new Set(curatedAvailable.map((item) => item.id));
226
+
227
+ // Remaining fetched models not already shown as curated
228
+ const extra = fetched
229
+ .filter((id) => !curatedIds.has(id))
230
+ .map((id) => {
231
+ const displayId = id
232
+ .replace(/^openrouter\//, "")
233
+ .replace(/^openai-codex\//, "")
234
+ .replace(/^anthropic\//, "");
235
+ return { value: id, label: displayId, description: "available from provider" };
236
+ });
237
+
238
+ return [
239
+ ...curatedAvailable.map((item) => ({ value: item.id, label: item.label, description: item.description })),
240
+ ...extra,
241
+ { value: "__custom__", label: "Enter a different model name...", description: "Use a specific model id not shown in the list." },
242
+ ];
251
243
  }
252
244
 
253
245
  async function promptForProviderModels(provider, key, tui, { fetchImpl = globalThis.fetch } = {}) {
@@ -256,7 +248,7 @@ async function promptForProviderModels(provider, key, tui, { fetchImpl = globalT
256
248
 
257
249
  const options = await buildProviderSelectionOptions(provider, key, { fetchImpl });
258
250
  const chosen = [];
259
- const manualOptionValue = provider === "openai" ? "__manual__" : "__custom__";
251
+ const manualOptionValue = "__custom__";
260
252
  const defaultModelValue = options.find((item) => !String(item.value).startsWith("__"))?.value || "";
261
253
 
262
254
  console.log(`\n ${cyan(`Choose ${provider === "openrouter" ? "OpenRouter" : provider === "openai" ? "OpenAI" : "Anthropic"} models`)}`);
@@ -271,19 +263,14 @@ async function promptForProviderModels(provider, key, tui, { fetchImpl = globalT
271
263
  }));
272
264
 
273
265
  if (chosen.length > 0) {
274
- pickerOptions.push({
275
- label: "Done",
276
- value: "__done__",
277
- description: "Continue setup with the models already selected.",
278
- });
279
- }
280
-
281
- if (provider !== "openai") {
282
- pickerOptions.push({
283
- label: "Enter a different model name...",
284
- value: manualOptionValue,
285
- description: "Use a specific model id not shown in the curated list.",
286
- });
266
+ // Insert "Done" before the custom-entry option at the end
267
+ const customIndex = pickerOptions.findIndex((item) => item.value === "__custom__");
268
+ const doneOption = { label: "Done", value: "__done__", description: "Continue setup with the models already selected." };
269
+ if (customIndex >= 0) {
270
+ pickerOptions.splice(customIndex, 0, doneOption);
271
+ } else {
272
+ pickerOptions.push(doneOption);
273
+ }
287
274
  }
288
275
 
289
276
  const picked = await select(
@@ -164,6 +164,23 @@ async function runInteractiveWizard({
164
164
  flowOverride = null,
165
165
  }) {
166
166
  const prompter = createClackPrompter();
167
+
168
+ // ASCII banner — ANSI Shadow font, brand accent colour
169
+ const BRAND = "\x1b[38;2;45;212;191m";
170
+ const RESET = "\x1b[0m";
171
+ const DIM = "\x1b[2m";
172
+ const ascii = [
173
+ "",
174
+ `${BRAND} ███╗ ██╗███████╗███╗ ███╗ ██████╗ ██████╗ ██╗███████╗${RESET}`,
175
+ `${BRAND} ████╗ ██║██╔════╝████╗ ████║██╔═══██╗██╔══██╗██║██╔════╝${RESET}`,
176
+ `${BRAND} ██╔██╗ ██║█████╗ ██╔████╔██║██║ ██║██████╔╝██║███████╗${RESET}`,
177
+ `${BRAND} ██║╚██╗██║██╔══╝ ██║╚██╔╝██║██║ ██║██╔══██╗██║╚════██║${RESET}`,
178
+ `${BRAND} ██║ ╚████║███████╗██║ ╚═╝ ██║╚██████╔╝██║ ██║██║███████║${RESET}`,
179
+ `${BRAND} ╚═╝ ╚═══╝╚══════╝╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚═╝╚══════╝${RESET}`,
180
+ "",
181
+ ].join("\n");
182
+ console.log(ascii);
183
+
167
184
  await prompter.intro("Nemoris setup");
168
185
 
169
186
  await prompter.note([
@@ -193,9 +210,9 @@ async function runInteractiveWizard({
193
210
  const action = await prompter.select({
194
211
  message: "Config handling",
195
212
  options: [
196
- { value: "keep", label: "Use existing values" },
197
- { value: "update", label: "Update values" },
198
- { value: "reset", label: "Reset everything" },
213
+ { value: "keep", label: "Use existing values", hint: "Skip setup — keep current config and start running" },
214
+ { value: "update", label: "Update values", hint: "Walk through setup again, pre-filled with current config" },
215
+ { value: "reset", label: "Reset everything", hint: "Wipe and start fresh" },
199
216
  ],
200
217
  });
201
218
 
@@ -208,9 +225,9 @@ async function runInteractiveWizard({
208
225
  const scope = await prompter.select({
209
226
  message: "Reset scope",
210
227
  options: [
211
- { value: "config", label: "Config only" },
212
- { value: "config+state", label: "Config + state (memory, runs, scheduler)" },
213
- { value: "full", label: "Full reset (everything)" },
228
+ { value: "config", label: "Config only", hint: "Rewrites agents, router, and provider config. Keeps memory and history." },
229
+ { value: "config+state", label: "Config + state", hint: "Config + wipes memory, run history, and scheduler data. Agent identities kept." },
230
+ { value: "full", label: "Full reset", hint: "Deletes everything and starts completely fresh. Like a first install." },
214
231
  ],
215
232
  });
216
233
  resetInstallArtifacts(installDir, scope);