nlm-memory 0.4.1 → 0.5.0
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/dist/cli/nlm.js +221 -32
- package/dist/cli/nlm.js.map +1 -1
- package/dist/core/adapters/cursor.d.ts +45 -0
- package/dist/core/adapters/cursor.js +397 -0
- package/dist/core/adapters/cursor.js.map +1 -0
- package/dist/core/adapters/from-source.js +10 -0
- package/dist/core/adapters/from-source.js.map +1 -1
- package/dist/core/adapters/windsurf.d.ts +44 -0
- package/dist/core/adapters/windsurf.js +299 -0
- package/dist/core/adapters/windsurf.js.map +1 -0
- package/dist/core/hook/claude-settings.d.ts +12 -5
- package/dist/core/hook/claude-settings.js +21 -6
- package/dist/core/hook/claude-settings.js.map +1 -1
- package/dist/core/sources/source-registry.d.ts +1 -1
- package/dist/core/sources/source-registry.js +18 -0
- package/dist/core/sources/source-registry.js.map +1 -1
- package/dist/core/storage/sqlite-session-store.d.ts +2 -0
- package/dist/core/storage/sqlite-session-store.js +38 -2
- package/dist/core/storage/sqlite-session-store.js.map +1 -1
- package/dist/hook/hook-auth.d.ts +13 -0
- package/dist/hook/hook-auth.js +19 -0
- package/dist/hook/hook-auth.js.map +1 -0
- package/dist/hook/prompt-recall-hook.js +7 -1
- package/dist/hook/prompt-recall-hook.js.map +1 -1
- package/dist/hook/session-start-hook.js +4 -1
- package/dist/hook/session-start-hook.js.map +1 -1
- package/dist/hook/stop-hook.js +4 -1
- package/dist/hook/stop-hook.js.map +1 -1
- package/dist/http/app.d.ts +2 -0
- package/dist/http/app.js +74 -0
- package/dist/http/app.js.map +1 -1
- package/dist/install/claude-code.js +1 -1
- package/dist/install/claude-code.js.map +1 -1
- package/dist/install/cursor.d.ts +25 -0
- package/dist/install/cursor.js +43 -0
- package/dist/install/cursor.js.map +1 -0
- package/dist/install/nlm-dir-perms.d.ts +19 -0
- package/dist/install/nlm-dir-perms.js +43 -0
- package/dist/install/nlm-dir-perms.js.map +1 -0
- package/dist/install/ollama.d.ts +18 -1
- package/dist/install/ollama.js +68 -10
- package/dist/install/ollama.js.map +1 -1
- package/dist/install/setup.d.ts +4 -0
- package/dist/install/setup.js +141 -18
- package/dist/install/setup.js.map +1 -1
- package/dist/install/windsurf.d.ts +25 -0
- package/dist/install/windsurf.js +43 -0
- package/dist/install/windsurf.js.map +1 -0
- package/dist/shared/types.d.ts +4 -0
- package/dist/ui/assets/{index-BA6IpU8g.css → index-C8cpwbYJ.css} +1 -1
- package/dist/ui/assets/index-CB50QnL-.js +69 -0
- package/dist/ui/index.html +2 -2
- package/logs/CHANGELOG/CHANGELOG-2026.md +186 -0
- package/logs/CHANGELOG/CHANGELOG.md +107 -235
- package/migrations/014_sources_cursor.sql +30 -0
- package/migrations/015_sources_windsurf.sql +30 -0
- package/package.json +1 -1
- package/plugin/scripts/prompt-recall-hook.mjs +55 -4
- package/plugin/scripts/stop-hook.mjs +57 -6
- package/src/cli/nlm.ts +224 -31
- package/src/core/adapters/cursor.ts +486 -0
- package/src/core/adapters/from-source.ts +10 -0
- package/src/core/adapters/windsurf.ts +386 -0
- package/src/core/hook/claude-settings.ts +30 -9
- package/src/core/sources/source-registry.ts +19 -1
- package/src/core/storage/sqlite-session-store.ts +46 -1
- package/src/hook/hook-auth.ts +18 -0
- package/src/hook/prompt-recall-hook.ts +7 -1
- package/src/hook/session-start-hook.ts +4 -1
- package/src/hook/stop-hook.ts +4 -1
- package/src/http/app.ts +78 -0
- package/src/install/claude-code.ts +1 -1
- package/src/install/cursor.ts +68 -0
- package/src/install/nlm-dir-perms.ts +55 -0
- package/src/install/ollama.ts +86 -10
- package/src/install/setup.ts +138 -17
- package/src/install/windsurf.ts +68 -0
- package/src/shared/types.ts +4 -0
- package/src/ui/components/SessionDrawer.tsx +97 -34
- package/src/ui/pages/River.tsx +90 -44
- package/src/ui/pages/Search.tsx +357 -64
- package/src/ui/pages/Thread.tsx +267 -56
- package/src/ui/styles.css +129 -5
- package/tests/integration/getbyids-sqlite.test.ts +40 -0
- package/tests/integration/hook-claude-settings.test.ts +14 -1
- package/tests/integration/mcp.test.ts +12 -0
- package/tests/integration/source-registry.test.ts +5 -3
- package/tests/unit/core/adapters/cursor.test.ts +485 -0
- package/tests/unit/core/adapters/windsurf.test.ts +416 -0
- package/dist/ui/assets/index-B_qIVV0k.js +0 -69
|
@@ -43,7 +43,7 @@ export function installClaudeCodeHooks(opts) {
|
|
|
43
43
|
const installed = [];
|
|
44
44
|
for (const spec of opts.hooks) {
|
|
45
45
|
try {
|
|
46
|
-
const command = opts.buildHookCommand(opts.nodeExecPath, spec.script, "
|
|
46
|
+
const command = opts.buildHookCommand(opts.nodeExecPath, spec.script, "live");
|
|
47
47
|
opts.addHook(opts.settingsPath, command, spec.event);
|
|
48
48
|
const smoke = opts.smokeTestHookCommand(command, opts.hookLogPath);
|
|
49
49
|
if (!smoke.ok) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"claude-code.js","sourceRoot":"","sources":["../../src/install/claude-code.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAC7E,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAsB1C,MAAM,UAAU,aAAa;IAC3B,OAAO,OAAO,CAAC,GAAG,CAAC,gBAAgB,CAAC,IAAI,IAAI,CAAC,OAAO,EAAE,EAAE,WAAW,CAAC,CAAC;AACvE,CAAC;AAED,SAAS,UAAU,CAAC,IAAY;IAC9B,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC;QAAE,OAAO,EAAE,CAAC;IACjC,IAAI,CAAC;QACH,OAAO,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,CAA4B,CAAC;IAC3E,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,IAAI,KAAK,CAAC,GAAG,IAAI,gFAAgF,CAAC,CAAC;IAC3G,CAAC;AACH,CAAC;AAED,MAAM,UAAU,iBAAiB,CAAC,IAA8B;IAC9D,MAAM,UAAU,GAAG,aAAa,EAAE,CAAC;IACnC,MAAM,MAAM,GAAG,UAAU,CAAC,UAAU,CAAC,CAAC;IACtC,MAAM,UAAU,GAAG,CAAC,MAAM,CAAC,YAAY,CAAC,IAAI,EAAE,CAA4B,CAAC;IAC3E,MAAM,cAAc,GAAG,YAAY,IAAI,UAAU,CAAC;IAElD,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC;QACjB,UAAU,CAAC,YAAY,CAAC,GAAG;YACzB,OAAO,EAAE,IAAI,CAAC,YAAY;YAC1B,IAAI,EAAE,CAAC,IAAI,CAAC,UAAU,EAAE,KAAK,CAAC;SAC/B,CAAC;QACF,MAAM,CAAC,YAAY,CAAC,GAAG,UAAU,CAAC;QAClC,SAAS,CAAC,OAAO,CAAC,UAAU,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QACpD,aAAa,CAAC,UAAU,EAAE,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,GAAG,IAAI,EAAE,MAAM,CAAC,CAAC;IAC5E,CAAC;IAED,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,cAAc,EAAE,OAAO,EAAE,CAAC,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,IAAI,KAAK,EAAE,CAAC;AAC5G,CAAC;AA4BD,MAAM,UAAU,sBAAsB,CAAC,IAAwB;IAC7D,MAAM,SAAS,GAAe,EAAE,CAAC;IACjC,KAAK,MAAM,IAAI,IAAI,IAAI,CAAC,KAAK,EAAE,CAAC;QAC9B,IAAI,CAAC;YACH,MAAM,OAAO,GAAG,IAAI,CAAC,gBAAgB,CAAC,IAAI,CAAC,YAAY,EAAE,IAAI,CAAC,MAAM,EAAE,
|
|
1
|
+
{"version":3,"file":"claude-code.js","sourceRoot":"","sources":["../../src/install/claude-code.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `nlm connect cursor` / `nlm disconnect cursor` — registers or removes the
|
|
3
|
+
* Cursor adapter source in the NLM source registry.
|
|
4
|
+
*
|
|
5
|
+
* Unlike plugin-based runtimes (hermes-agent, codex), Cursor needs no file
|
|
6
|
+
* to be installed. NLM reads Cursor's existing state.vscdb directly. The
|
|
7
|
+
* connect operation only registers the source row so the daemon scans it.
|
|
8
|
+
*/
|
|
9
|
+
import type { SourceRegistry } from "../core/sources/source-registry.js";
|
|
10
|
+
export interface ConnectCursorOptions {
|
|
11
|
+
readonly dbPath?: string;
|
|
12
|
+
readonly dryRun?: boolean;
|
|
13
|
+
}
|
|
14
|
+
export interface ConnectCursorReport {
|
|
15
|
+
readonly adapterDbPath: string;
|
|
16
|
+
readonly adapterExists: boolean;
|
|
17
|
+
readonly action: "created" | "enabled" | "already-active" | "dry-run";
|
|
18
|
+
}
|
|
19
|
+
export interface DisconnectCursorReport {
|
|
20
|
+
readonly action: "disabled" | "not-found" | "dry-run";
|
|
21
|
+
}
|
|
22
|
+
export declare function connectCursor(registry: SourceRegistry, opts?: ConnectCursorOptions): ConnectCursorReport;
|
|
23
|
+
export declare function disconnectCursor(registry: SourceRegistry, opts?: {
|
|
24
|
+
dryRun?: boolean;
|
|
25
|
+
}): DisconnectCursorReport;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `nlm connect cursor` / `nlm disconnect cursor` — registers or removes the
|
|
3
|
+
* Cursor adapter source in the NLM source registry.
|
|
4
|
+
*
|
|
5
|
+
* Unlike plugin-based runtimes (hermes-agent, codex), Cursor needs no file
|
|
6
|
+
* to be installed. NLM reads Cursor's existing state.vscdb directly. The
|
|
7
|
+
* connect operation only registers the source row so the daemon scans it.
|
|
8
|
+
*/
|
|
9
|
+
import { existsSync } from "node:fs";
|
|
10
|
+
import { defaultDbPath } from "../core/adapters/cursor.js";
|
|
11
|
+
export function connectCursor(registry, opts = {}) {
|
|
12
|
+
const adapterDbPath = opts.dbPath ?? defaultDbPath();
|
|
13
|
+
const adapterExists = existsSync(adapterDbPath);
|
|
14
|
+
if (opts.dryRun) {
|
|
15
|
+
return { adapterDbPath, adapterExists, action: "dry-run" };
|
|
16
|
+
}
|
|
17
|
+
const existing = registry.getByName("Cursor");
|
|
18
|
+
if (existing) {
|
|
19
|
+
if (existing.enabled && existing.pathOrUrl === adapterDbPath) {
|
|
20
|
+
return { adapterDbPath, adapterExists, action: "already-active" };
|
|
21
|
+
}
|
|
22
|
+
registry.update(existing.id, { enabled: true, pathOrUrl: adapterDbPath });
|
|
23
|
+
return { adapterDbPath, adapterExists, action: "enabled" };
|
|
24
|
+
}
|
|
25
|
+
registry.insert({
|
|
26
|
+
kind: "cursor",
|
|
27
|
+
name: "Cursor",
|
|
28
|
+
pathOrUrl: adapterDbPath,
|
|
29
|
+
runtimeLabel: "cursor/1.0",
|
|
30
|
+
enabled: adapterExists,
|
|
31
|
+
});
|
|
32
|
+
return { adapterDbPath, adapterExists, action: "created" };
|
|
33
|
+
}
|
|
34
|
+
export function disconnectCursor(registry, opts = {}) {
|
|
35
|
+
if (opts.dryRun)
|
|
36
|
+
return { action: "dry-run" };
|
|
37
|
+
const existing = registry.getByName("Cursor");
|
|
38
|
+
if (!existing)
|
|
39
|
+
return { action: "not-found" };
|
|
40
|
+
registry.update(existing.id, { enabled: false });
|
|
41
|
+
return { action: "disabled" };
|
|
42
|
+
}
|
|
43
|
+
//# sourceMappingURL=cursor.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cursor.js","sourceRoot":"","sources":["../../src/install/cursor.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAkB3D,MAAM,UAAU,aAAa,CAC3B,QAAwB,EACxB,OAA6B,EAAE;IAE/B,MAAM,aAAa,GAAG,IAAI,CAAC,MAAM,IAAI,aAAa,EAAE,CAAC;IACrD,MAAM,aAAa,GAAG,UAAU,CAAC,aAAa,CAAC,CAAC;IAEhD,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC;QAChB,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC;IAC7D,CAAC;IAED,MAAM,QAAQ,GAAG,QAAQ,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC;IAC9C,IAAI,QAAQ,EAAE,CAAC;QACb,IAAI,QAAQ,CAAC,OAAO,IAAI,QAAQ,CAAC,SAAS,KAAK,aAAa,EAAE,CAAC;YAC7D,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,EAAE,gBAAgB,EAAE,CAAC;QACpE,CAAC;QACD,QAAQ,CAAC,MAAM,CAAC,QAAQ,CAAC,EAAE,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,aAAa,EAAE,CAAC,CAAC;QAC1E,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC;IAC7D,CAAC;IAED,QAAQ,CAAC,MAAM,CAAC;QACd,IAAI,EAAE,QAAQ;QACd,IAAI,EAAE,QAAQ;QACd,SAAS,EAAE,aAAa;QACxB,YAAY,EAAE,YAAY;QAC1B,OAAO,EAAE,aAAa;KACvB,CAAC,CAAC;IACH,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC;AAC7D,CAAC;AAED,MAAM,UAAU,gBAAgB,CAC9B,QAAwB,EACxB,OAA6B,EAAE;IAE/B,IAAI,IAAI,CAAC,MAAM;QAAE,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC;IAC9C,MAAM,QAAQ,GAAG,QAAQ,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC;IAC9C,IAAI,CAAC,QAAQ;QAAE,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,CAAC;IAC9C,QAAQ,CAAC,MAAM,CAAC,QAAQ,CAAC,EAAE,EAAE,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC,CAAC;IACjD,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,CAAC;AAChC,CAAC"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Idempotent permission hardening for ~/.nlm/.
|
|
3
|
+
*
|
|
4
|
+
* Recursively sets owner-only perms on the daemon's working directory:
|
|
5
|
+
* directories → 0o700
|
|
6
|
+
* files → 0o600
|
|
7
|
+
*
|
|
8
|
+
* Run at every `nlm setup`, `nlm install`, and `nlm start` so installs
|
|
9
|
+
* predating v0.4.2 (when explicit chmod was added) self-heal on next
|
|
10
|
+
* launch. No-op on Windows — ACLs are the POSIX equivalent and out of
|
|
11
|
+
* scope here.
|
|
12
|
+
*/
|
|
13
|
+
export interface PermsHardenResult {
|
|
14
|
+
readonly nlmDir: string;
|
|
15
|
+
readonly filesHardened: number;
|
|
16
|
+
readonly dirsHardened: number;
|
|
17
|
+
readonly skipped: number;
|
|
18
|
+
}
|
|
19
|
+
export declare function hardenNlmDirPermissions(nlmDir?: string): PermsHardenResult;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Idempotent permission hardening for ~/.nlm/.
|
|
3
|
+
*
|
|
4
|
+
* Recursively sets owner-only perms on the daemon's working directory:
|
|
5
|
+
* directories → 0o700
|
|
6
|
+
* files → 0o600
|
|
7
|
+
*
|
|
8
|
+
* Run at every `nlm setup`, `nlm install`, and `nlm start` so installs
|
|
9
|
+
* predating v0.4.2 (when explicit chmod was added) self-heal on next
|
|
10
|
+
* launch. No-op on Windows — ACLs are the POSIX equivalent and out of
|
|
11
|
+
* scope here.
|
|
12
|
+
*/
|
|
13
|
+
import { chmodSync, existsSync, readdirSync, statSync } from "node:fs";
|
|
14
|
+
import { homedir } from "node:os";
|
|
15
|
+
import { join } from "node:path";
|
|
16
|
+
export function hardenNlmDirPermissions(nlmDir = join(homedir(), ".nlm")) {
|
|
17
|
+
const result = { nlmDir, filesHardened: 0, dirsHardened: 0, skipped: 0 };
|
|
18
|
+
if (process.platform === "win32")
|
|
19
|
+
return result;
|
|
20
|
+
if (!existsSync(nlmDir))
|
|
21
|
+
return result;
|
|
22
|
+
walk(nlmDir, result);
|
|
23
|
+
return result;
|
|
24
|
+
}
|
|
25
|
+
function walk(path, r) {
|
|
26
|
+
try {
|
|
27
|
+
const s = statSync(path);
|
|
28
|
+
if (s.isDirectory()) {
|
|
29
|
+
chmodSync(path, 0o700);
|
|
30
|
+
r.dirsHardened += 1;
|
|
31
|
+
for (const name of readdirSync(path))
|
|
32
|
+
walk(join(path, name), r);
|
|
33
|
+
}
|
|
34
|
+
else if (s.isFile()) {
|
|
35
|
+
chmodSync(path, 0o600);
|
|
36
|
+
r.filesHardened += 1;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
r.skipped += 1;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
//# sourceMappingURL=nlm-dir-perms.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"nlm-dir-perms.js","sourceRoot":"","sources":["../../src/install/nlm-dir-perms.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AACvE,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AASjC,MAAM,UAAU,uBAAuB,CACrC,SAAiB,IAAI,CAAC,OAAO,EAAE,EAAE,MAAM,CAAC;IAExC,MAAM,MAAM,GAAG,EAAE,MAAM,EAAE,aAAa,EAAE,CAAC,EAAE,YAAY,EAAE,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,CAAC;IACzE,IAAI,OAAO,CAAC,QAAQ,KAAK,OAAO;QAAE,OAAO,MAAM,CAAC;IAChD,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC;QAAE,OAAO,MAAM,CAAC;IACvC,IAAI,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACrB,OAAO,MAAM,CAAC;AAChB,CAAC;AAQD,SAAS,IAAI,CAAC,IAAY,EAAE,CAAgB;IAC1C,IAAI,CAAC;QACH,MAAM,CAAC,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC;QACzB,IAAI,CAAC,CAAC,WAAW,EAAE,EAAE,CAAC;YACpB,SAAS,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;YACvB,CAAC,CAAC,YAAY,IAAI,CAAC,CAAC;YACpB,KAAK,MAAM,IAAI,IAAI,WAAW,CAAC,IAAI,CAAC;gBAAE,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC;QAClE,CAAC;aAAM,IAAI,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC;YACtB,SAAS,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;YACvB,CAAC,CAAC,aAAa,IAAI,CAAC,CAAC;QACvB,CAAC;IACH,CAAC;IAAC,MAAM,CAAC;QACP,CAAC,CAAC,OAAO,IAAI,CAAC,CAAC;IACjB,CAAC;AACH,CAAC"}
|
package/dist/install/ollama.d.ts
CHANGED
|
@@ -47,8 +47,25 @@ export declare function waitForOllamaServer(maxAttempts?: number, intervalMs?: n
|
|
|
47
47
|
*/
|
|
48
48
|
export declare function pullEmbeddingModel(): OllamaResult;
|
|
49
49
|
export type ClassifierChoice = "deepseek" | "ollama-offline";
|
|
50
|
+
export interface ClassifierConfigInput {
|
|
51
|
+
readonly choice: ClassifierChoice;
|
|
52
|
+
readonly model?: string;
|
|
53
|
+
readonly apiKey?: string;
|
|
54
|
+
}
|
|
50
55
|
/**
|
|
51
56
|
* Write classifier config to ~/.nlm/.env. Merges into the existing file —
|
|
52
57
|
* only the lines we manage are updated; anything the user added by hand stays.
|
|
58
|
+
*
|
|
59
|
+
* Manages three keys: DEEPSEEK_API_KEY, NLM_CLASSIFIER, NLM_CLASSIFIER_MODEL.
|
|
60
|
+
* Backwards-compatible: passing positional (choice, apiKey) still works.
|
|
61
|
+
*/
|
|
62
|
+
export declare function writeClassifierConfig(choiceOrInput: ClassifierChoice | ClassifierConfigInput, apiKey?: string): void;
|
|
63
|
+
/**
|
|
64
|
+
* Generate and persist an NLM_MCP_TOKEN if one isn't already set. Returns
|
|
65
|
+
* the token that's active for this process. Called during setup and on
|
|
66
|
+
* `nlm start` so installs that pre-date token-gated /api/* still get
|
|
67
|
+
* Bearer-protected without operator intervention.
|
|
68
|
+
*
|
|
69
|
+
* Token is hex-encoded crypto.randomBytes — 64 chars, 256 bits of entropy.
|
|
53
70
|
*/
|
|
54
|
-
export declare function
|
|
71
|
+
export declare function ensureMcpToken(): string;
|
package/dist/install/ollama.js
CHANGED
|
@@ -11,10 +11,11 @@
|
|
|
11
11
|
* Linux — official install.sh / systemctl / detached spawn
|
|
12
12
|
* Windows — winget install / detached spawn
|
|
13
13
|
*/
|
|
14
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
14
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
15
15
|
import { homedir, platform } from "node:os";
|
|
16
16
|
import { join } from "node:path";
|
|
17
17
|
import { spawnSync, spawn } from "node:child_process";
|
|
18
|
+
import { randomBytes } from "node:crypto";
|
|
18
19
|
export const EMBEDDING_MODEL = "nomic-embed-text";
|
|
19
20
|
const OS = platform();
|
|
20
21
|
// ── Detection ─────────────────────────────────────────────────────────────
|
|
@@ -154,25 +155,82 @@ export function pullEmbeddingModel() {
|
|
|
154
155
|
/**
|
|
155
156
|
* Write classifier config to ~/.nlm/.env. Merges into the existing file —
|
|
156
157
|
* only the lines we manage are updated; anything the user added by hand stays.
|
|
158
|
+
*
|
|
159
|
+
* Manages three keys: DEEPSEEK_API_KEY, NLM_CLASSIFIER, NLM_CLASSIFIER_MODEL.
|
|
160
|
+
* Backwards-compatible: passing positional (choice, apiKey) still works.
|
|
157
161
|
*/
|
|
158
|
-
export function writeClassifierConfig(
|
|
162
|
+
export function writeClassifierConfig(choiceOrInput, apiKey) {
|
|
163
|
+
const input = typeof choiceOrInput === "string"
|
|
164
|
+
? { choice: choiceOrInput, ...(apiKey !== undefined ? { apiKey } : {}) }
|
|
165
|
+
: choiceOrInput;
|
|
159
166
|
const envPath = join(homedir(), ".nlm", ".env");
|
|
160
|
-
|
|
167
|
+
const nlmDir = join(homedir(), ".nlm");
|
|
168
|
+
mkdirSync(nlmDir, { recursive: true, mode: 0o700 });
|
|
169
|
+
chmodSync(nlmDir, 0o700);
|
|
161
170
|
const existing = existsSync(envPath) ? readFileSync(envPath, "utf8") : "";
|
|
162
171
|
const kept = existing
|
|
163
172
|
.split("\n")
|
|
164
|
-
.filter((l) => !l.startsWith("DEEPSEEK_API_KEY=") &&
|
|
173
|
+
.filter((l) => !l.startsWith("DEEPSEEK_API_KEY=") &&
|
|
174
|
+
!l.startsWith("NLM_CLASSIFIER=") &&
|
|
175
|
+
!l.startsWith("NLM_CLASSIFIER_MODEL="))
|
|
165
176
|
.join("\n")
|
|
166
177
|
.replace(/\n{3,}/g, "\n\n")
|
|
167
178
|
.trim();
|
|
168
179
|
const additions = [];
|
|
169
|
-
if (choice === "deepseek"
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
180
|
+
if (input.choice === "deepseek") {
|
|
181
|
+
additions.push("NLM_CLASSIFIER=deepseek");
|
|
182
|
+
if (input.apiKey) {
|
|
183
|
+
// Strip newlines that clipboard paste can introduce.
|
|
184
|
+
const sanitized = input.apiKey.replace(/[\r\n]/g, "").trim();
|
|
185
|
+
additions.push(`DEEPSEEK_API_KEY=${sanitized}`);
|
|
186
|
+
}
|
|
173
187
|
}
|
|
174
|
-
if (choice === "ollama-offline")
|
|
188
|
+
if (input.choice === "ollama-offline")
|
|
175
189
|
additions.push("NLM_CLASSIFIER=ollama");
|
|
176
|
-
|
|
190
|
+
if (input.model)
|
|
191
|
+
additions.push(`NLM_CLASSIFIER_MODEL=${input.model}`);
|
|
192
|
+
writeFileSync(envPath, [kept, ...additions].filter(Boolean).join("\n") + "\n", { mode: 0o600 });
|
|
193
|
+
chmodSync(envPath, 0o600);
|
|
194
|
+
}
|
|
195
|
+
const TOKEN_BYTES = 32;
|
|
196
|
+
/**
|
|
197
|
+
* Generate and persist an NLM_MCP_TOKEN if one isn't already set. Returns
|
|
198
|
+
* the token that's active for this process. Called during setup and on
|
|
199
|
+
* `nlm start` so installs that pre-date token-gated /api/* still get
|
|
200
|
+
* Bearer-protected without operator intervention.
|
|
201
|
+
*
|
|
202
|
+
* Token is hex-encoded crypto.randomBytes — 64 chars, 256 bits of entropy.
|
|
203
|
+
*/
|
|
204
|
+
export function ensureMcpToken() {
|
|
205
|
+
const existing = process.env["NLM_MCP_TOKEN"];
|
|
206
|
+
if (existing)
|
|
207
|
+
return existing;
|
|
208
|
+
const token = randomBytes(TOKEN_BYTES).toString("hex");
|
|
209
|
+
const envPath = join(homedir(), ".nlm", ".env");
|
|
210
|
+
const nlmDir = join(homedir(), ".nlm");
|
|
211
|
+
mkdirSync(nlmDir, { recursive: true, mode: 0o700 });
|
|
212
|
+
chmodSync(nlmDir, 0o700);
|
|
213
|
+
const fileExisting = existsSync(envPath) ? readFileSync(envPath, "utf8") : "";
|
|
214
|
+
// Idempotent re-read: another setup run could have written the token
|
|
215
|
+
// between our env check and now. Prefer the persisted value.
|
|
216
|
+
for (const line of fileExisting.split("\n")) {
|
|
217
|
+
if (line.startsWith("NLM_MCP_TOKEN=")) {
|
|
218
|
+
const persisted = line.slice("NLM_MCP_TOKEN=".length).trim();
|
|
219
|
+
if (persisted) {
|
|
220
|
+
process.env["NLM_MCP_TOKEN"] = persisted;
|
|
221
|
+
return persisted;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
const kept = fileExisting
|
|
226
|
+
.split("\n")
|
|
227
|
+
.filter((l) => !l.startsWith("NLM_MCP_TOKEN="))
|
|
228
|
+
.join("\n")
|
|
229
|
+
.replace(/\n{3,}/g, "\n\n")
|
|
230
|
+
.trim();
|
|
231
|
+
writeFileSync(envPath, [kept, `NLM_MCP_TOKEN=${token}`].filter(Boolean).join("\n") + "\n", { mode: 0o600 });
|
|
232
|
+
chmodSync(envPath, 0o600);
|
|
233
|
+
process.env["NLM_MCP_TOKEN"] = token;
|
|
234
|
+
return token;
|
|
177
235
|
}
|
|
178
236
|
//# sourceMappingURL=ollama.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ollama.js","sourceRoot":"","sources":["../../src/install/ollama.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"ollama.js","sourceRoot":"","sources":["../../src/install/ollama.ts"],"names":[],"mappings":"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"}
|
package/dist/install/setup.d.ts
CHANGED
|
@@ -19,6 +19,10 @@ export interface SetupOptions {
|
|
|
19
19
|
readonly launchAgentLabel: string;
|
|
20
20
|
readonly launchAgentPlist: string;
|
|
21
21
|
readonly buildPlist: (nodeExec: string, binPath: string) => string;
|
|
22
|
+
readonly linuxSystemdUnitName: string;
|
|
23
|
+
readonly linuxSystemdUnitPath: string;
|
|
24
|
+
readonly buildSystemdUnit: (nodeExec: string, binPath: string) => string;
|
|
25
|
+
readonly linuxSystemdUserAvailable: () => boolean;
|
|
22
26
|
readonly claudeSettingsPath: string;
|
|
23
27
|
readonly allHooks: ReadonlyArray<{
|
|
24
28
|
event: ClaudeHookEvent;
|
package/dist/install/setup.js
CHANGED
|
@@ -12,15 +12,41 @@
|
|
|
12
12
|
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
13
13
|
import { execFileSync } from "node:child_process";
|
|
14
14
|
import { homedir, platform } from "node:os";
|
|
15
|
-
import { join } from "node:path";
|
|
15
|
+
import { dirname, join } from "node:path";
|
|
16
16
|
import { cancel, confirm, intro, isCancel, log, multiselect, outro, password, select, spinner, } from "@clack/prompts";
|
|
17
17
|
import { connectClaudeCode } from "./claude-code.js";
|
|
18
18
|
import { connectHermes } from "./hermes.js";
|
|
19
19
|
import { codexBinaryAvailable, connectCodex, pluginScriptsDir } from "./codex.js";
|
|
20
20
|
import { defaultDbPath as openCodeDefaultDbPath } from "../core/adapters/opencode.js";
|
|
21
|
-
import { EMBEDDING_MODEL, embeddingModelPresent, installOllama, ollamaBinaryAvailable, ollamaServerRunning, pullEmbeddingModel, startOllamaServer, waitForOllamaServer, writeClassifierConfig, } from "./ollama.js";
|
|
21
|
+
import { EMBEDDING_MODEL, embeddingModelPresent, ensureMcpToken, installOllama, ollamaBinaryAvailable, ollamaServerRunning, pullEmbeddingModel, startOllamaServer, waitForOllamaServer, writeClassifierConfig, } from "./ollama.js";
|
|
22
22
|
import { installClaudeCodeHooks } from "./claude-code.js";
|
|
23
|
+
import { hardenNlmDirPermissions } from "./nlm-dir-perms.js";
|
|
23
24
|
const OS = platform();
|
|
25
|
+
// Embedding-only tags shouldn't be offered as classifier models — they
|
|
26
|
+
// can't run chat completions and the call would fail at first ingest.
|
|
27
|
+
const EMBEDDING_MODEL_PREFIXES = ["nomic-embed", "mxbai-embed", "snowflake-arctic-embed", "bge-"];
|
|
28
|
+
async function fetchOllamaChatModels(timeoutMs = 5000) {
|
|
29
|
+
const baseUrl = process.env["NLM_OLLAMA_URL"] ?? "http://localhost:11434";
|
|
30
|
+
const controller = new AbortController();
|
|
31
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
32
|
+
try {
|
|
33
|
+
const res = await fetch(`${baseUrl}/api/tags`, { signal: controller.signal });
|
|
34
|
+
if (!res.ok)
|
|
35
|
+
return [];
|
|
36
|
+
const data = (await res.json());
|
|
37
|
+
return (data.models ?? [])
|
|
38
|
+
.map((m) => m.name)
|
|
39
|
+
.filter((n) => typeof n === "string")
|
|
40
|
+
.filter((n) => !EMBEDDING_MODEL_PREFIXES.some((p) => n.startsWith(p)))
|
|
41
|
+
.sort();
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return [];
|
|
45
|
+
}
|
|
46
|
+
finally {
|
|
47
|
+
clearTimeout(timer);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
24
50
|
function detectRuntimes() {
|
|
25
51
|
const claudeProjectsPath = process.env["NLM_CLAUDE_PROJECTS_PATH"]
|
|
26
52
|
?? join(homedir(), ".claude", "projects");
|
|
@@ -151,18 +177,28 @@ export async function runSetup(opts) {
|
|
|
151
177
|
if (ollamaBinaryAvailable() && embeddingModelPresent()) {
|
|
152
178
|
log.success(`Ollama ready — ${EMBEDDING_MODEL} present`);
|
|
153
179
|
}
|
|
154
|
-
// ── Step 3: classifier
|
|
155
|
-
const
|
|
156
|
-
|
|
180
|
+
// ── Step 3: classifier (provider + model + key) ───────────────────────
|
|
181
|
+
const wantConfigure = await confirm({
|
|
182
|
+
message: "Configure the session classifier? (controls how new sessions are tagged)",
|
|
183
|
+
});
|
|
184
|
+
if (isCancel(wantConfigure)) {
|
|
157
185
|
cancel("Setup cancelled.");
|
|
158
186
|
process.exit(0);
|
|
159
187
|
}
|
|
160
|
-
if (
|
|
188
|
+
if (wantConfigure) {
|
|
161
189
|
const classifierChoice = await select({
|
|
162
|
-
message: "Which classifier?",
|
|
190
|
+
message: "Which classifier provider?",
|
|
163
191
|
options: [
|
|
164
|
-
{
|
|
165
|
-
|
|
192
|
+
{
|
|
193
|
+
value: "deepseek",
|
|
194
|
+
label: "DeepSeek (cloud)",
|
|
195
|
+
hint: "fast, cheap (~$0.002/session). Transcripts are sent to api.deepseek.com.",
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
value: "ollama-offline",
|
|
199
|
+
label: "Ollama (local)",
|
|
200
|
+
hint: "private — runs on this machine via your local Ollama. Slower; needs a chat model pulled.",
|
|
201
|
+
},
|
|
166
202
|
],
|
|
167
203
|
});
|
|
168
204
|
if (isCancel(classifierChoice)) {
|
|
@@ -170,24 +206,71 @@ export async function runSetup(opts) {
|
|
|
170
206
|
process.exit(0);
|
|
171
207
|
}
|
|
172
208
|
if (classifierChoice === "deepseek") {
|
|
209
|
+
log.info("Heads up: DeepSeek classification sends up to 30K chars of each session transcript to api.deepseek.com.");
|
|
210
|
+
log.info(" Anything in a transcript (pasted keys, client names, internal URLs) leaves this machine.");
|
|
211
|
+
log.info(" Pick Ollama (local) above if that's not acceptable.");
|
|
212
|
+
const model = await select({
|
|
213
|
+
message: "Which DeepSeek model?",
|
|
214
|
+
options: [
|
|
215
|
+
{ value: "deepseek-v4-flash", label: "deepseek-v4-flash", hint: "recommended — fast + cheap, ~$0.002/session" },
|
|
216
|
+
{ value: "deepseek-v4-pro", label: "deepseek-v4-pro", hint: "higher quality, ~10× cost" },
|
|
217
|
+
{ value: "deepseek-chat", label: "deepseek-chat", hint: "legacy chat model" },
|
|
218
|
+
],
|
|
219
|
+
});
|
|
220
|
+
if (isCancel(model)) {
|
|
221
|
+
cancel("Setup cancelled.");
|
|
222
|
+
process.exit(0);
|
|
223
|
+
}
|
|
173
224
|
const key = await password({ message: "DeepSeek API key (get one at platform.deepseek.com):" });
|
|
174
225
|
if (isCancel(key)) {
|
|
175
226
|
cancel("Setup cancelled.");
|
|
176
227
|
process.exit(0);
|
|
177
228
|
}
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
229
|
+
const apiKey = key && key.trim() ? key.trim() : undefined;
|
|
230
|
+
writeClassifierConfig(apiKey !== undefined
|
|
231
|
+
? { choice: "deepseek", model: model, apiKey }
|
|
232
|
+
: { choice: "deepseek", model: model });
|
|
233
|
+
if (apiKey) {
|
|
234
|
+
log.success(`DeepSeek (${model}) configured — credentials saved to ~/.nlm/.env`);
|
|
181
235
|
}
|
|
182
236
|
else {
|
|
183
|
-
log.warn(
|
|
237
|
+
log.warn(`DeepSeek (${model}) configured — set DEEPSEEK_API_KEY in ~/.nlm/.env before running.`);
|
|
184
238
|
}
|
|
185
239
|
}
|
|
186
240
|
else {
|
|
187
|
-
|
|
188
|
-
|
|
241
|
+
const ollamaModels = await fetchOllamaChatModels();
|
|
242
|
+
let modelValue = "phi4-mini:latest";
|
|
243
|
+
if (ollamaModels.length > 0) {
|
|
244
|
+
const model = await select({
|
|
245
|
+
message: "Which Ollama chat model?",
|
|
246
|
+
options: ollamaModels.map((m) => ({
|
|
247
|
+
value: m,
|
|
248
|
+
label: m,
|
|
249
|
+
hint: m === "phi4-mini:latest" ? "recommended default — small, fast" : undefined,
|
|
250
|
+
})),
|
|
251
|
+
});
|
|
252
|
+
if (isCancel(model)) {
|
|
253
|
+
cancel("Setup cancelled.");
|
|
254
|
+
process.exit(0);
|
|
255
|
+
}
|
|
256
|
+
modelValue = model;
|
|
257
|
+
}
|
|
258
|
+
else {
|
|
259
|
+
log.warn("No Ollama chat models detected. Defaulting to phi4-mini:latest.");
|
|
260
|
+
log.warn(" Pull a model with: ollama pull phi4-mini (or any chat model you prefer)");
|
|
261
|
+
}
|
|
262
|
+
writeClassifierConfig({ choice: "ollama-offline", model: modelValue });
|
|
263
|
+
log.success(`Ollama classifier (${modelValue}) saved to ~/.nlm/.env`);
|
|
189
264
|
}
|
|
190
265
|
}
|
|
266
|
+
// ── Step 3.5: HTTP API auth token ─────────────────────────────────────
|
|
267
|
+
// Generate a token if one isn't set so /api/* gets Bearer-protected for
|
|
268
|
+
// non-browser callers (curl, port-forwarded clients). The UI still works
|
|
269
|
+
// because browsers send Origin and we exempt loopback origins.
|
|
270
|
+
const token = ensureMcpToken();
|
|
271
|
+
if (token === process.env["NLM_MCP_TOKEN"] && token.length === 64) {
|
|
272
|
+
log.success("HTTP API auth token saved to ~/.nlm/.env (NLM_MCP_TOKEN)");
|
|
273
|
+
}
|
|
191
274
|
// ── Step 4: migrations ────────────────────────────────────────────────
|
|
192
275
|
const ms = spinner();
|
|
193
276
|
ms.start("Running database migrations");
|
|
@@ -202,6 +285,13 @@ export async function runSetup(opts) {
|
|
|
202
285
|
log.error(`${e instanceof Error ? e.message : String(e)}`);
|
|
203
286
|
process.exit(1);
|
|
204
287
|
}
|
|
288
|
+
// ── Step 4.5: harden ~/.nlm permissions ────────────────────────────────
|
|
289
|
+
// Idempotent. Covers upgrade from pre-v0.4.2 installs where files were
|
|
290
|
+
// written without explicit chmod, leaving secrets world-readable.
|
|
291
|
+
const perms = hardenNlmDirPermissions();
|
|
292
|
+
if (perms.filesHardened + perms.dirsHardened > 0) {
|
|
293
|
+
log.success(`Hardened perms on ${perms.dirsHardened} dirs and ${perms.filesHardened} files in ${perms.nlmDir}`);
|
|
294
|
+
}
|
|
205
295
|
// ── Step 5: daemon ────────────────────────────────────────────────────
|
|
206
296
|
if (OS === "darwin") {
|
|
207
297
|
const installDaemon = await confirm({ message: "Install macOS LaunchAgent (auto-start on login)?" });
|
|
@@ -233,9 +323,38 @@ export async function runSetup(opts) {
|
|
|
233
323
|
}
|
|
234
324
|
}
|
|
235
325
|
else if (OS === "linux") {
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
326
|
+
if (opts.linuxSystemdUserAvailable()) {
|
|
327
|
+
const installDaemon = await confirm({ message: "Install systemd user unit (auto-start on login)?" });
|
|
328
|
+
if (isCancel(installDaemon)) {
|
|
329
|
+
cancel("Setup cancelled.");
|
|
330
|
+
process.exit(0);
|
|
331
|
+
}
|
|
332
|
+
if (installDaemon) {
|
|
333
|
+
const ds = spinner();
|
|
334
|
+
ds.start("Installing systemd user unit");
|
|
335
|
+
try {
|
|
336
|
+
mkdirSync(dirname(opts.linuxSystemdUnitPath), { recursive: true });
|
|
337
|
+
mkdirSync(join(homedir(), ".nlm", "logs"), { recursive: true });
|
|
338
|
+
writeFileSync(opts.linuxSystemdUnitPath, opts.buildSystemdUnit(opts.nodeExecPath, opts.nlmBinPath), "utf8");
|
|
339
|
+
execFileSync("systemctl", ["--user", "daemon-reload"]);
|
|
340
|
+
execFileSync("systemctl", ["--user", "enable", "--now", opts.linuxSystemdUnitName]);
|
|
341
|
+
ds.stop("systemd user unit installed — daemon running");
|
|
342
|
+
log.info(` Status: systemctl --user status ${opts.linuxSystemdUnitName}`);
|
|
343
|
+
log.info(" Headless? Run `sudo loginctl enable-linger $USER` so the daemon survives logout.");
|
|
344
|
+
}
|
|
345
|
+
catch (e) {
|
|
346
|
+
ds.stop("systemd install failed");
|
|
347
|
+
log.error(`${e instanceof Error ? e.message : String(e)}`);
|
|
348
|
+
log.warn("Run `nlm install` manually later, or start now with: nlm start &");
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
else {
|
|
353
|
+
log.info("systemd user instance not available (no XDG_RUNTIME_DIR or `systemctl --user`).");
|
|
354
|
+
log.info(" Common on headless servers — start manually with: nlm start &");
|
|
355
|
+
log.info(" Or enable lingering, then re-run `nlm install`:");
|
|
356
|
+
log.info(" sudo loginctl enable-linger $USER");
|
|
357
|
+
}
|
|
239
358
|
}
|
|
240
359
|
else if (OS === "win32") {
|
|
241
360
|
log.info("Windows daemon: run `nlm start` at login via Task Scheduler.");
|
|
@@ -324,6 +443,10 @@ export async function runSetup(opts) {
|
|
|
324
443
|
case "pi":
|
|
325
444
|
log.success("pi.dev: session scanning enabled (passive — no extra config needed)");
|
|
326
445
|
break;
|
|
446
|
+
default: {
|
|
447
|
+
const _ = id;
|
|
448
|
+
log.warn(`Unknown runtime: ${_} — skipping.`);
|
|
449
|
+
}
|
|
327
450
|
}
|
|
328
451
|
}
|
|
329
452
|
// ── Summary ───────────────────────────────────────────────────────────
|