nlm-memory 0.5.0 → 0.5.1
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 +72 -34
- package/dist/cli/nlm.js +2 -1
- package/dist/cli/nlm.js.map +1 -1
- package/dist/http/app.js +2 -1
- package/dist/http/app.js.map +1 -1
- package/dist/mcp/server.js +20 -1
- package/dist/mcp/server.js.map +1 -1
- package/dist/ui/assets/{index-C8cpwbYJ.css → index-Beo8psd-.css} +1 -1
- package/dist/ui/assets/{index-CB50QnL-.js → index-CSPTTeeM.js} +8 -8
- package/dist/ui/index.html +2 -2
- package/package.json +26 -1
- package/.agents/plugins/marketplace.json +0 -20
- package/.github/workflows/ci.yml +0 -30
- package/docs/methodology/re-derivation-rate.md +0 -112
- package/docs/methodology/useful-hit-rate.md +0 -79
- package/docs/plans/2026-05-20-fts5-lexical-recall.md +0 -1088
- package/docs/plans/2026-05-20-recall-daemon-wedge-fix.md +0 -662
- package/docs/plans/2026-05-20-recall-hook-design.md +0 -131
- package/docs/plans/2026-05-20-recall-hook-implementation.md +0 -1222
- package/docs/plans/desktop-product.md +0 -69
- package/docs/plans/factstore-design.md +0 -236
- package/logs/CHANGELOG/CHANGELOG-2026.md +0 -1575
- package/logs/CHANGELOG/CHANGELOG.md +0 -209
- package/migrations/000_initial_schema.sql +0 -174
- package/migrations/001_entity_type_rename.sql +0 -17
- package/migrations/002_adapter_state_extend.sql +0 -12
- package/migrations/003_session_embeddings.sql +0 -11
- package/migrations/004_facts.sql +0 -46
- package/migrations/005_sources.sql +0 -31
- package/migrations/006_providers.sql +0 -33
- package/migrations/007_source_tokens.sql +0 -17
- package/migrations/008_fts_rebuild.sql +0 -9
- package/migrations/009_session_embedding_chunks.sql +0 -46
- package/migrations/010_sources_opencode.sql +0 -30
- package/migrations/011_sources_hermes_agent.sql +0 -30
- package/migrations/012_sources_aider.sql +0 -30
- package/migrations/013_adapter_state_failure_count.sql +0 -12
- package/migrations/014_sources_cursor.sql +0 -30
- package/migrations/015_sources_windsurf.sql +0 -30
- package/plugin-hermes-agent/README.md +0 -49
- package/plugin-hermes-agent/__init__.py +0 -75
- package/plugin-hermes-agent/plugin.yaml +0 -15
- package/scripts/backfill-citations.mjs +0 -0
- package/scripts/build-codex-plugin.mjs +0 -61
- package/scripts/deepseek-probe.mjs +0 -67
- package/scripts/extract-triples.mjs +0 -207
- package/scripts/longmemeval/embedding-cache.ts +0 -77
- package/scripts/longmemeval/fetch-dataset.sh +0 -25
- package/scripts/longmemeval/run-harness.ts +0 -315
- package/scripts/longmemeval/scorer.ts +0 -99
- package/scripts/longmemeval/tsconfig.json +0 -9
- package/scripts/longmemeval/types.ts +0 -35
- package/scripts/nlm-daily-digest.py +0 -239
- package/scripts/nlm-daily-digest.sh +0 -28
- package/src/cli/classify-parity.ts +0 -257
- package/src/cli/launchctl-helpers.ts +0 -49
- package/src/cli/nlm.ts +0 -1078
- package/src/core/actions/actions-log.ts +0 -118
- package/src/core/actions/overlay.ts +0 -117
- package/src/core/adapters/aider.ts +0 -205
- package/src/core/adapters/claude-code.ts +0 -293
- package/src/core/adapters/common.ts +0 -54
- package/src/core/adapters/cursor.ts +0 -486
- package/src/core/adapters/from-source.ts +0 -67
- package/src/core/adapters/hermes-agent.ts +0 -240
- package/src/core/adapters/hermes.ts +0 -277
- package/src/core/adapters/jsonl-generic.ts +0 -208
- package/src/core/adapters/opencode.ts +0 -281
- package/src/core/adapters/pi.ts +0 -264
- package/src/core/adapters/windsurf.ts +0 -386
- package/src/core/classifier/prompt.ts +0 -200
- package/src/core/dataset/build-dataset.ts +0 -463
- package/src/core/embedding/chunk-body.ts +0 -76
- package/src/core/embedding/embed-backfill.ts +0 -210
- package/src/core/embedding/embed-normalize.ts +0 -135
- package/src/core/facts/backfill-facts.ts +0 -254
- package/src/core/facts/extract-facts.ts +0 -50
- package/src/core/hook/citation-detect.ts +0 -124
- package/src/core/hook/cite-memo.ts +0 -68
- package/src/core/hook/claude-settings.ts +0 -187
- package/src/core/hook/gate.ts +0 -25
- package/src/core/hook/hook-log.ts +0 -41
- package/src/core/hook/memo-sweep.ts +0 -164
- package/src/core/hook/memo.ts +0 -67
- package/src/core/hook/pointer-block.ts +0 -26
- package/src/core/hook/select.ts +0 -32
- package/src/core/hook/transcript.ts +0 -121
- package/src/core/ingest/ingest-session.ts +0 -111
- package/src/core/providers/provider-models.ts +0 -100
- package/src/core/providers/provider-registry.ts +0 -196
- package/src/core/recall/citation-log.ts +0 -108
- package/src/core/recall/filter.ts +0 -27
- package/src/core/recall/index.ts +0 -6
- package/src/core/recall/match-fields.ts +0 -40
- package/src/core/recall/query-log.ts +0 -149
- package/src/core/recall/query-shape.ts +0 -66
- package/src/core/recall/recall-service.ts +0 -320
- package/src/core/recall/recent-log.ts +0 -59
- package/src/core/recall/tokenize.ts +0 -18
- package/src/core/recall/useful-scan.ts +0 -336
- package/src/core/recall-facts/fact-query-log.ts +0 -150
- package/src/core/recall-facts/fact-recall-service.ts +0 -327
- package/src/core/scheduler/scan-once.ts +0 -142
- package/src/core/scheduler/scheduler.ts +0 -225
- package/src/core/sources/source-registry.ts +0 -278
- package/src/core/storage/db-restore.ts +0 -133
- package/src/core/storage/live-status.ts +0 -45
- package/src/core/storage/migrate.ts +0 -72
- package/src/core/storage/sqlite-fact-store.ts +0 -304
- package/src/core/storage/sqlite-session-store.ts +0 -810
- package/src/hook/hook-auth.ts +0 -18
- package/src/hook/prompt-recall-hook.ts +0 -180
- package/src/hook/session-end-hook.ts +0 -81
- package/src/hook/session-start-hook.ts +0 -168
- package/src/hook/stop-hook.ts +0 -239
- package/src/http/app.ts +0 -1215
- package/src/install/claude-code.ts +0 -128
- package/src/install/codex.ts +0 -367
- package/src/install/cursor.ts +0 -68
- package/src/install/hermes-agent.ts +0 -76
- package/src/install/hermes.ts +0 -78
- package/src/install/nlm-dir-perms.ts +0 -55
- package/src/install/ollama.ts +0 -284
- package/src/install/setup.ts +0 -489
- package/src/install/windsurf.ts +0 -68
- package/src/llm/classifier-box.ts +0 -64
- package/src/llm/deepseek-client.ts +0 -150
- package/src/llm/env-autoload.ts +0 -55
- package/src/llm/ollama-client.ts +0 -189
- package/src/mcp/server.ts +0 -534
- package/src/ports/fact-store.ts +0 -102
- package/src/ports/llm-client.ts +0 -52
- package/src/ports/logger.ts +0 -16
- package/src/ports/session-store.ts +0 -45
- package/src/ports/transcript-adapter.ts +0 -55
- package/src/shared/types.ts +0 -149
- package/src/ui/App.tsx +0 -58
- package/src/ui/components/PromoteOpenButton.tsx +0 -65
- package/src/ui/components/SessionDrawer.tsx +0 -199
- package/src/ui/components/SideNav.tsx +0 -162
- package/src/ui/components/Skeleton.tsx +0 -107
- package/src/ui/index.html +0 -13
- package/src/ui/lib/actions.ts +0 -30
- package/src/ui/lib/api.ts +0 -92
- package/src/ui/lib/dataset.ts +0 -141
- package/src/ui/lib/registries.ts +0 -155
- package/src/ui/lib/view-settings.ts +0 -41
- package/src/ui/main.tsx +0 -15
- package/src/ui/pages/Live.tsx +0 -229
- package/src/ui/pages/Pulse.tsx +0 -415
- package/src/ui/pages/Recall.tsx +0 -190
- package/src/ui/pages/River.tsx +0 -354
- package/src/ui/pages/Search.tsx +0 -386
- package/src/ui/pages/Stub.tsx +0 -9
- package/src/ui/pages/Thread.tsx +0 -473
- package/src/ui/pages/settings/Classifier.tsx +0 -227
- package/src/ui/pages/settings/Data.tsx +0 -190
- package/src/ui/pages/settings/Index.tsx +0 -65
- package/src/ui/pages/settings/Labels.tsx +0 -224
- package/src/ui/pages/settings/Providers.tsx +0 -305
- package/src/ui/pages/settings/SettingsSubnav.tsx +0 -28
- package/src/ui/pages/settings/Sources.tsx +0 -326
- package/src/ui/pages/settings/Views.tsx +0 -96
- package/src/ui/styles.css +0 -1890
- package/src/ui/tsconfig.json +0 -21
- package/src/ui/vite.config.ts +0 -19
- package/tests/fixtures/claude_code/short_session.jsonl +0 -2
- package/tests/fixtures/claude_code/standard_iso.jsonl +0 -4
- package/tests/fixtures/claude_code/tool_heavy.jsonl +0 -8
- package/tests/fixtures/claude_code/with_subagent.jsonl +0 -7
- package/tests/fixtures/facts.ts +0 -17
- package/tests/fixtures/golden-corpus.ts +0 -85
- package/tests/fixtures/hermes/paired_request_dump.json +0 -24
- package/tests/fixtures/hermes/paired_session.json +0 -23
- package/tests/fixtures/hermes/request_dump.json +0 -28
- package/tests/fixtures/hermes/session_iso.json +0 -38
- package/tests/fixtures/hermes/session_unix.json +0 -38
- package/tests/fixtures/hermes/system_only.json +0 -18
- package/tests/fixtures/pi/error-connection-abort.jsonl +0 -8
- package/tests/fixtures/pi/short-successful.jsonl +0 -5
- package/tests/fixtures/pi/with-custom-message.jsonl +0 -6
- package/tests/fixtures/sessions.ts +0 -22
- package/tests/integration/backfill-facts.test.ts +0 -362
- package/tests/integration/citation-explicit.test.ts +0 -111
- package/tests/integration/cite-event.test.ts +0 -169
- package/tests/integration/cite-memo.test.ts +0 -87
- package/tests/integration/db-restore.test.ts +0 -153
- package/tests/integration/embed-backfill.test.ts +0 -176
- package/tests/integration/fact-supersedence.test.ts +0 -313
- package/tests/integration/fts-index.test.ts +0 -60
- package/tests/integration/getbyids-sqlite.test.ts +0 -100
- package/tests/integration/hermes-agent-hooks.test.ts +0 -248
- package/tests/integration/hook-claude-settings.test.ts +0 -218
- package/tests/integration/hook-log.test.ts +0 -54
- package/tests/integration/hook-memo.test.ts +0 -68
- package/tests/integration/hook-pre-compact.test.ts +0 -105
- package/tests/integration/hook-subagent-start.test.ts +0 -102
- package/tests/integration/http.test.ts +0 -401
- package/tests/integration/keyword-search-fts.test.ts +0 -66
- package/tests/integration/mcp-recall-logging.test.ts +0 -88
- package/tests/integration/mcp.test.ts +0 -260
- package/tests/integration/memo-sweep.test.ts +0 -91
- package/tests/integration/prompt-recall-hook.test.ts +0 -88
- package/tests/integration/provider-registry.test.ts +0 -107
- package/tests/integration/recall-golden.test.ts +0 -59
- package/tests/integration/recall-sqlite.test.ts +0 -169
- package/tests/integration/scheduler.test.ts +0 -391
- package/tests/integration/session-end-hook.test.ts +0 -48
- package/tests/integration/session-start-hook.test.ts +0 -126
- package/tests/integration/source-registry.test.ts +0 -122
- package/tests/integration/sqlite-fact-store.test.ts +0 -346
- package/tests/integration/stop-hook.test.ts +0 -560
- package/tests/integration/wal-checkpoint.test.ts +0 -49
- package/tests/unit/cli/launchctl-helpers.test.ts +0 -60
- package/tests/unit/core/adapters/aider.test.ts +0 -230
- package/tests/unit/core/adapters/claude-code.test.ts +0 -118
- package/tests/unit/core/adapters/cursor.test.ts +0 -485
- package/tests/unit/core/adapters/hermes-agent.test.ts +0 -329
- package/tests/unit/core/adapters/hermes.test.ts +0 -81
- package/tests/unit/core/adapters/jsonl-generic.test.ts +0 -142
- package/tests/unit/core/adapters/opencode.test.ts +0 -354
- package/tests/unit/core/adapters/pi.test.ts +0 -110
- package/tests/unit/core/adapters/windsurf.test.ts +0 -416
- package/tests/unit/core/classifier/prompt.test.ts +0 -126
- package/tests/unit/core/embedding/chunk-body.test.ts +0 -100
- package/tests/unit/core/facts/extract-facts.test.ts +0 -117
- package/tests/unit/core/filter.test.ts +0 -40
- package/tests/unit/core/hook/citation-detect-cite-session.test.ts +0 -96
- package/tests/unit/core/hook/citation-detect.test.ts +0 -124
- package/tests/unit/core/hook/gate.test.ts +0 -29
- package/tests/unit/core/hook/pointer-block.test.ts +0 -22
- package/tests/unit/core/hook/select.test.ts +0 -66
- package/tests/unit/core/match-fields.test.ts +0 -39
- package/tests/unit/core/mcp-cite-session.test.ts +0 -51
- package/tests/unit/core/providers/provider-models.test.ts +0 -101
- package/tests/unit/core/query-shape.test.ts +0 -92
- package/tests/unit/core/recall-facts/fact-recall-service.test.ts +0 -258
- package/tests/unit/core/recall-service.test.ts +0 -200
- package/tests/unit/core/storage/live-status.test.ts +0 -54
- package/tests/unit/core/tokenize.test.ts +0 -32
- package/tests/unit/core/useful-scan.test.ts +0 -537
- package/tests/unit/llm/embed.test.ts +0 -93
- package/tests/unit/llm/ollama-client.test.ts +0 -124
- package/tests/unit/scripts/longmemeval-scorer.test.ts +0 -114
- package/tsconfig.json +0 -31
- package/tsconfig.test.json +0 -11
- package/vitest.config.ts +0 -22
|
@@ -1,278 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* SourceRegistry — CRUD over the `sources` table.
|
|
3
|
-
*
|
|
4
|
-
* A "source" is any transcript origin the daemon scans (Claude Code's
|
|
5
|
-
* projects dir, Hermes's sessions dir, pi.dev, a user-defined JSONL
|
|
6
|
-
* directory, or a webhook).
|
|
7
|
-
*
|
|
8
|
-
* The three legacy adapters (claude-code, hermes, pi) seed as preset rows
|
|
9
|
-
* pointing at fixed `path_or_url` values. The generic JSONL adapter and
|
|
10
|
-
* webhook ingest piggy-back on this same table — the scheduler chooses
|
|
11
|
-
* which adapter to dispatch by reading `kind`.
|
|
12
|
-
*
|
|
13
|
-
* See docs/plans/desktop-product.md (Phase 0).
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
import { randomBytes } from "node:crypto";
|
|
17
|
-
import { existsSync } from "node:fs";
|
|
18
|
-
import { homedir } from "node:os";
|
|
19
|
-
import { join } from "node:path";
|
|
20
|
-
import type Database from "better-sqlite3";
|
|
21
|
-
import { defaultHistoryFile as defaultAiderHistoryFile } from "../adapters/aider.js";
|
|
22
|
-
import { defaultDbPath as defaultCursorDbPath } from "../adapters/cursor.js";
|
|
23
|
-
import { defaultDbPath as defaultHermesAgentDbPath } from "../adapters/hermes-agent.js";
|
|
24
|
-
import { defaultDbPath as defaultOpenCodeDbPath } from "../adapters/opencode.js";
|
|
25
|
-
import { defaultUserDir as defaultWindsurfUserDir } from "../adapters/windsurf.js";
|
|
26
|
-
|
|
27
|
-
export type SourceKind = "claude-code" | "hermes" | "hermes-agent" | "aider" | "cursor" | "windsurf" | "opencode" | "pi" | "jsonl-generic" | "webhook";
|
|
28
|
-
|
|
29
|
-
export interface SourceRow {
|
|
30
|
-
readonly id: number;
|
|
31
|
-
readonly kind: SourceKind;
|
|
32
|
-
readonly name: string;
|
|
33
|
-
readonly pathOrUrl: string | null;
|
|
34
|
-
readonly runtimeLabel: string;
|
|
35
|
-
readonly parseConfig: Record<string, unknown>;
|
|
36
|
-
readonly enabled: boolean;
|
|
37
|
-
/** Only populated on the response from `insert()` for webhook sources.
|
|
38
|
-
* Always `null` from `list()` / `get()`. Use `getToken()` inside the daemon. */
|
|
39
|
-
readonly token: string | null;
|
|
40
|
-
readonly hasToken: boolean;
|
|
41
|
-
readonly createdAt: string;
|
|
42
|
-
readonly updatedAt: string;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export interface SourceInsert {
|
|
46
|
-
readonly kind: SourceKind;
|
|
47
|
-
readonly name: string;
|
|
48
|
-
readonly pathOrUrl?: string | null;
|
|
49
|
-
readonly runtimeLabel: string;
|
|
50
|
-
readonly parseConfig?: Record<string, unknown>;
|
|
51
|
-
readonly enabled?: boolean;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export interface SourceUpdate {
|
|
55
|
-
readonly name?: string;
|
|
56
|
-
readonly pathOrUrl?: string | null;
|
|
57
|
-
readonly runtimeLabel?: string;
|
|
58
|
-
readonly parseConfig?: Record<string, unknown>;
|
|
59
|
-
readonly enabled?: boolean;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
interface SourceDbRow {
|
|
63
|
-
id: number;
|
|
64
|
-
kind: string;
|
|
65
|
-
name: string;
|
|
66
|
-
path_or_url: string | null;
|
|
67
|
-
runtime_label: string;
|
|
68
|
-
parse_config: string;
|
|
69
|
-
enabled: number;
|
|
70
|
-
token: string | null;
|
|
71
|
-
created_at: string;
|
|
72
|
-
updated_at: string;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function rowFromDb(r: SourceDbRow, revealedToken: string | null = null): SourceRow {
|
|
76
|
-
let parsed: Record<string, unknown> = {};
|
|
77
|
-
try {
|
|
78
|
-
parsed = r.parse_config ? (JSON.parse(r.parse_config) as Record<string, unknown>) : {};
|
|
79
|
-
} catch {
|
|
80
|
-
parsed = {};
|
|
81
|
-
}
|
|
82
|
-
return {
|
|
83
|
-
id: r.id,
|
|
84
|
-
kind: r.kind as SourceKind,
|
|
85
|
-
name: r.name,
|
|
86
|
-
pathOrUrl: r.path_or_url,
|
|
87
|
-
runtimeLabel: r.runtime_label,
|
|
88
|
-
parseConfig: parsed,
|
|
89
|
-
enabled: r.enabled === 1,
|
|
90
|
-
token: revealedToken,
|
|
91
|
-
hasToken: r.token !== null && r.token.length > 0,
|
|
92
|
-
createdAt: r.created_at,
|
|
93
|
-
updatedAt: r.updated_at,
|
|
94
|
-
};
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
function mintToken(): string {
|
|
98
|
-
return `nlm_${randomBytes(24).toString("hex")}`;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
export class SourceRegistry {
|
|
102
|
-
constructor(private readonly db: Database.Database) {}
|
|
103
|
-
|
|
104
|
-
list(): SourceRow[] {
|
|
105
|
-
const rows = this.db.prepare<[], SourceDbRow>(
|
|
106
|
-
`SELECT * FROM sources ORDER BY id ASC`,
|
|
107
|
-
).all();
|
|
108
|
-
return rows.map((r) => rowFromDb(r));
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
get(id: number): SourceRow | null {
|
|
112
|
-
const row = this.db.prepare<[number], SourceDbRow>(
|
|
113
|
-
`SELECT * FROM sources WHERE id = ?`,
|
|
114
|
-
).get(id);
|
|
115
|
-
return row ? rowFromDb(row) : null;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
getByName(name: string): SourceRow | null {
|
|
119
|
-
const row = this.db.prepare<[string], SourceDbRow>(
|
|
120
|
-
`SELECT * FROM sources WHERE name = ?`,
|
|
121
|
-
).get(name);
|
|
122
|
-
return row ? rowFromDb(row) : null;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
insert(input: SourceInsert): SourceRow {
|
|
126
|
-
const token = input.kind === "webhook" ? mintToken() : null;
|
|
127
|
-
const stmt = this.db.prepare(`
|
|
128
|
-
INSERT INTO sources (kind, name, path_or_url, runtime_label, parse_config, enabled, token)
|
|
129
|
-
VALUES (@kind, @name, @path_or_url, @runtime_label, @parse_config, @enabled, @token)
|
|
130
|
-
`);
|
|
131
|
-
const result = stmt.run({
|
|
132
|
-
kind: input.kind,
|
|
133
|
-
name: input.name,
|
|
134
|
-
path_or_url: input.pathOrUrl ?? null,
|
|
135
|
-
runtime_label: input.runtimeLabel,
|
|
136
|
-
parse_config: JSON.stringify(input.parseConfig ?? {}),
|
|
137
|
-
enabled: input.enabled === false ? 0 : 1,
|
|
138
|
-
token,
|
|
139
|
-
});
|
|
140
|
-
const id = Number(result.lastInsertRowid);
|
|
141
|
-
const dbRow = this.db.prepare<[number], SourceDbRow>(
|
|
142
|
-
`SELECT * FROM sources WHERE id = ?`,
|
|
143
|
-
).get(id);
|
|
144
|
-
if (!dbRow) throw new Error(`SourceRegistry.insert: row ${id} not found after insert`);
|
|
145
|
-
// Reveal the token on the insert response only — this is the user's
|
|
146
|
-
// one chance to copy it. Subsequent list/get redact.
|
|
147
|
-
return rowFromDb(dbRow, token);
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
/** Daemon-internal: resolve a bearer token to its owning source. */
|
|
151
|
-
findByToken(token: string): SourceRow | null {
|
|
152
|
-
if (!token) return null;
|
|
153
|
-
const row = this.db.prepare<[string], SourceDbRow>(
|
|
154
|
-
`SELECT * FROM sources WHERE token = ?`,
|
|
155
|
-
).get(token);
|
|
156
|
-
return row ? rowFromDb(row) : null;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
/** Daemon-internal: returns the raw token. Never echo to HTTP responses. */
|
|
160
|
-
getToken(id: number): string | null {
|
|
161
|
-
const row = this.db.prepare<[number], SourceDbRow>(
|
|
162
|
-
`SELECT token FROM sources WHERE id = ?`,
|
|
163
|
-
).get(id);
|
|
164
|
-
return row?.token ?? null;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
/** Mint a fresh token, invalidating any previous one. */
|
|
168
|
-
regenerateToken(id: number): string | null {
|
|
169
|
-
const current = this.get(id);
|
|
170
|
-
if (!current || current.kind !== "webhook") return null;
|
|
171
|
-
const token = mintToken();
|
|
172
|
-
this.db.prepare(`UPDATE sources SET token = ?, updated_at = datetime('now') WHERE id = ?`)
|
|
173
|
-
.run(token, id);
|
|
174
|
-
return token;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
update(id: number, patch: SourceUpdate): SourceRow | null {
|
|
178
|
-
const fields: string[] = [];
|
|
179
|
-
const params: Record<string, unknown> = { id };
|
|
180
|
-
if (patch.name !== undefined) { fields.push("name = @name"); params["name"] = patch.name; }
|
|
181
|
-
if (patch.pathOrUrl !== undefined) { fields.push("path_or_url = @path"); params["path"] = patch.pathOrUrl; }
|
|
182
|
-
if (patch.runtimeLabel !== undefined) { fields.push("runtime_label = @rt"); params["rt"] = patch.runtimeLabel; }
|
|
183
|
-
if (patch.parseConfig !== undefined) { fields.push("parse_config = @cfg"); params["cfg"] = JSON.stringify(patch.parseConfig); }
|
|
184
|
-
if (patch.enabled !== undefined) { fields.push("enabled = @en"); params["en"] = patch.enabled ? 1 : 0; }
|
|
185
|
-
if (fields.length === 0) return this.get(id);
|
|
186
|
-
fields.push("updated_at = datetime('now')");
|
|
187
|
-
this.db.prepare(`UPDATE sources SET ${fields.join(", ")} WHERE id = @id`).run(params);
|
|
188
|
-
return this.get(id);
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
delete(id: number): boolean {
|
|
192
|
-
const result = this.db.prepare(`DELETE FROM sources WHERE id = ?`).run(id);
|
|
193
|
-
return result.changes > 0;
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
/**
|
|
197
|
-
* Seed the three legacy adapter presets on first boot of an empty
|
|
198
|
-
* registry. Subsequent boots are no-ops. Respects per-runtime env
|
|
199
|
-
* overrides so existing installs don't lose their custom paths.
|
|
200
|
-
*/
|
|
201
|
-
seedDefaults(): void {
|
|
202
|
-
const count = this.db.prepare<[], { c: number }>(`SELECT COUNT(*) AS c FROM sources`).get();
|
|
203
|
-
if ((count?.c ?? 0) > 0) return;
|
|
204
|
-
|
|
205
|
-
const claudePath = process.env["NLM_CLAUDE_PROJECTS_PATH"]
|
|
206
|
-
?? join(homedir(), ".claude", "projects");
|
|
207
|
-
const hermesPath = process.env["NLM_HERMES_SESSIONS_PATH"]
|
|
208
|
-
?? join(homedir(), ".hermes", "sessions");
|
|
209
|
-
const piPath = process.env["PI_SESSIONS_PATH"]
|
|
210
|
-
?? join(homedir(), ".pi", "agent", "sessions");
|
|
211
|
-
|
|
212
|
-
const openCodeDbPath = defaultOpenCodeDbPath();
|
|
213
|
-
const hermesAgentDbPath = defaultHermesAgentDbPath();
|
|
214
|
-
const aiderHistoryFile = defaultAiderHistoryFile();
|
|
215
|
-
const cursorDbPath = defaultCursorDbPath();
|
|
216
|
-
const windsurfUserDir = defaultWindsurfUserDir();
|
|
217
|
-
|
|
218
|
-
const presets: SourceInsert[] = [
|
|
219
|
-
{
|
|
220
|
-
kind: "claude-code",
|
|
221
|
-
name: "Claude Code",
|
|
222
|
-
pathOrUrl: claudePath,
|
|
223
|
-
runtimeLabel: "claude-code/1.0",
|
|
224
|
-
enabled: existsSync(claudePath),
|
|
225
|
-
},
|
|
226
|
-
{
|
|
227
|
-
kind: "hermes",
|
|
228
|
-
name: "Hermes",
|
|
229
|
-
pathOrUrl: hermesPath,
|
|
230
|
-
runtimeLabel: "hermes/1.0",
|
|
231
|
-
enabled: existsSync(hermesPath),
|
|
232
|
-
},
|
|
233
|
-
{
|
|
234
|
-
kind: "hermes-agent",
|
|
235
|
-
name: "Hermes Agent",
|
|
236
|
-
pathOrUrl: hermesAgentDbPath,
|
|
237
|
-
runtimeLabel: "hermes-agent/1.0",
|
|
238
|
-
enabled: existsSync(hermesAgentDbPath),
|
|
239
|
-
},
|
|
240
|
-
{
|
|
241
|
-
kind: "aider",
|
|
242
|
-
name: "Aider",
|
|
243
|
-
pathOrUrl: aiderHistoryFile,
|
|
244
|
-
runtimeLabel: "aider/1.0",
|
|
245
|
-
enabled: existsSync(aiderHistoryFile),
|
|
246
|
-
},
|
|
247
|
-
{
|
|
248
|
-
kind: "cursor",
|
|
249
|
-
name: "Cursor",
|
|
250
|
-
pathOrUrl: cursorDbPath,
|
|
251
|
-
runtimeLabel: "cursor/1.0",
|
|
252
|
-
enabled: existsSync(cursorDbPath),
|
|
253
|
-
},
|
|
254
|
-
{
|
|
255
|
-
kind: "windsurf",
|
|
256
|
-
name: "Windsurf",
|
|
257
|
-
pathOrUrl: windsurfUserDir,
|
|
258
|
-
runtimeLabel: "windsurf/1.0",
|
|
259
|
-
enabled: existsSync(windsurfUserDir),
|
|
260
|
-
},
|
|
261
|
-
{
|
|
262
|
-
kind: "opencode",
|
|
263
|
-
name: "OpenCode",
|
|
264
|
-
pathOrUrl: openCodeDbPath,
|
|
265
|
-
runtimeLabel: "opencode/1.0",
|
|
266
|
-
enabled: existsSync(openCodeDbPath),
|
|
267
|
-
},
|
|
268
|
-
{
|
|
269
|
-
kind: "pi",
|
|
270
|
-
name: "pi.dev",
|
|
271
|
-
pathOrUrl: piPath,
|
|
272
|
-
runtimeLabel: "pi/1.0",
|
|
273
|
-
enabled: existsSync(piPath),
|
|
274
|
-
},
|
|
275
|
-
];
|
|
276
|
-
for (const p of presets) this.insert(p);
|
|
277
|
-
}
|
|
278
|
-
}
|
|
@@ -1,133 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Backup + restore for the canonical SQLite store.
|
|
3
|
-
*
|
|
4
|
-
* Backup is live-safe: `VACUUM INTO` takes a read lock and writes a clean,
|
|
5
|
-
* defragmented, single-file snapshot — no WAL sidecars, consistent even
|
|
6
|
-
* while the daemon is ingesting.
|
|
7
|
-
*
|
|
8
|
-
* Restore cannot swap a file the daemon holds open. Instead the uploaded
|
|
9
|
-
* DB is validated and parked at `<dbPath>.restore-pending`; the next daemon
|
|
10
|
-
* boot calls `applyPendingRestore()` before opening the store, moving the
|
|
11
|
-
* current DB aside to `<dbPath>.pre-restore-<ts>` and promoting the pending
|
|
12
|
-
* file. The desktop shell turns "restart required" into one click.
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
import Database from "better-sqlite3";
|
|
16
|
-
import { existsSync, renameSync, rmSync, statSync } from "node:fs";
|
|
17
|
-
import { dirname, join } from "node:path";
|
|
18
|
-
|
|
19
|
-
export const PENDING_SUFFIX = ".restore-pending";
|
|
20
|
-
|
|
21
|
-
export interface RestoreValidation {
|
|
22
|
-
ok: boolean;
|
|
23
|
-
error?: string;
|
|
24
|
-
sessions?: number;
|
|
25
|
-
schemaVersion?: number;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Validate that `filePath` is a usable canonical store: passes integrity
|
|
30
|
-
* check and carries the `sessions` + `schema_migrations` tables. Opened
|
|
31
|
-
* read-only and without the sqlite-vec extension — we never touch the
|
|
32
|
-
* vec virtual tables here, so the extension isn't needed.
|
|
33
|
-
*/
|
|
34
|
-
export function validateRestoreCandidate(filePath: string): RestoreValidation {
|
|
35
|
-
let db: Database.Database | null = null;
|
|
36
|
-
try {
|
|
37
|
-
db = new Database(filePath, { readonly: true, fileMustExist: true });
|
|
38
|
-
const integrity = db.pragma("integrity_check", { simple: true });
|
|
39
|
-
if (integrity !== "ok") {
|
|
40
|
-
return { ok: false, error: `integrity check failed: ${String(integrity)}` };
|
|
41
|
-
}
|
|
42
|
-
const tables = db
|
|
43
|
-
.prepare<[], { name: string }>(
|
|
44
|
-
"SELECT name FROM sqlite_master WHERE type='table' AND name IN ('sessions','schema_migrations')",
|
|
45
|
-
)
|
|
46
|
-
.all()
|
|
47
|
-
.map((r) => r.name);
|
|
48
|
-
if (!tables.includes("sessions") || !tables.includes("schema_migrations")) {
|
|
49
|
-
return { ok: false, error: "not an nlm-memory database (missing sessions/schema_migrations)" };
|
|
50
|
-
}
|
|
51
|
-
const sessions = db
|
|
52
|
-
.prepare<[], { n: number }>("SELECT COUNT(*) AS n FROM sessions")
|
|
53
|
-
.get();
|
|
54
|
-
const version = db
|
|
55
|
-
.prepare<[], { v: number | null }>("SELECT MAX(version) AS v FROM schema_migrations")
|
|
56
|
-
.get();
|
|
57
|
-
return {
|
|
58
|
-
ok: true,
|
|
59
|
-
sessions: sessions?.n ?? 0,
|
|
60
|
-
schemaVersion: version?.v ?? 0,
|
|
61
|
-
};
|
|
62
|
-
} catch (e) {
|
|
63
|
-
return { ok: false, error: e instanceof Error ? e.message : String(e) };
|
|
64
|
-
} finally {
|
|
65
|
-
db?.close();
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* Park an already-written candidate file as the pending restore for
|
|
71
|
-
* `dbPath`. Validates first; on failure the candidate is removed and the
|
|
72
|
-
* validation error returned. On success the candidate is renamed to
|
|
73
|
-
* `<dbPath>.restore-pending` (same directory, so the rename is atomic).
|
|
74
|
-
*/
|
|
75
|
-
export function stageRestore(dbPath: string, candidatePath: string): RestoreValidation {
|
|
76
|
-
const validation = validateRestoreCandidate(candidatePath);
|
|
77
|
-
if (!validation.ok) {
|
|
78
|
-
rmSync(candidatePath, { force: true });
|
|
79
|
-
return validation;
|
|
80
|
-
}
|
|
81
|
-
const pending = dbPath + PENDING_SUFFIX;
|
|
82
|
-
rmSync(pending, { force: true });
|
|
83
|
-
renameSync(candidatePath, pending);
|
|
84
|
-
return validation;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
export interface PendingRestoreResult {
|
|
88
|
-
applied: boolean;
|
|
89
|
-
archivedTo?: string;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* If a pending restore exists for `dbPath`, promote it: move the current
|
|
94
|
-
* DB (and its WAL/SHM sidecars) aside, then rename the pending file into
|
|
95
|
-
* place. Call once at boot, before the store is opened.
|
|
96
|
-
*/
|
|
97
|
-
export function applyPendingRestore(dbPath: string): PendingRestoreResult {
|
|
98
|
-
const pending = dbPath + PENDING_SUFFIX;
|
|
99
|
-
if (!existsSync(pending)) return { applied: false };
|
|
100
|
-
|
|
101
|
-
const stamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
102
|
-
const archived = `${dbPath}.pre-restore-${stamp}`;
|
|
103
|
-
|
|
104
|
-
if (existsSync(dbPath)) {
|
|
105
|
-
renameSync(dbPath, archived);
|
|
106
|
-
}
|
|
107
|
-
// The archived DB's WAL/SHM belong to it — drop the live sidecars so the
|
|
108
|
-
// promoted file isn't paired with a stale WAL.
|
|
109
|
-
for (const sidecar of [`${dbPath}-wal`, `${dbPath}-shm`]) {
|
|
110
|
-
rmSync(sidecar, { force: true });
|
|
111
|
-
}
|
|
112
|
-
renameSync(pending, dbPath);
|
|
113
|
-
|
|
114
|
-
return existsSync(archived)
|
|
115
|
-
? { applied: true, archivedTo: archived }
|
|
116
|
-
: { applied: true };
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
/**
|
|
120
|
-
* Write a live-consistent snapshot of `db` to a fresh file via
|
|
121
|
-
* `VACUUM INTO`. The destination must not already exist. Returns the
|
|
122
|
-
* snapshot's size in bytes.
|
|
123
|
-
*/
|
|
124
|
-
export function vacuumSnapshot(db: Database.Database, destPath: string): number {
|
|
125
|
-
rmSync(destPath, { force: true });
|
|
126
|
-
db.prepare("VACUUM INTO ?").run(destPath);
|
|
127
|
-
return statSync(destPath).size;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
/** Scratch path for a backup snapshot, alongside the DB so rename stays atomic. */
|
|
131
|
-
export function snapshotScratchPath(dbPath: string): string {
|
|
132
|
-
return join(dirname(dbPath), `.backup-${process.pid}-${Date.now()}.sqlite`);
|
|
133
|
-
}
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* live-status — derive the three-tier session status (active / idle / closed)
|
|
3
|
-
* from a transcript file's mtime. Mirrors the Python daemon's
|
|
4
|
-
* live_session_status(): explicit supersedence wins; missing file → closed;
|
|
5
|
-
* otherwise bucketed by age.
|
|
6
|
-
*
|
|
7
|
-
* Thresholds match Python exactly:
|
|
8
|
-
* < 15 min → active
|
|
9
|
-
* 15 min – 24 h → idle
|
|
10
|
-
* ≥ 24 h → closed
|
|
11
|
-
*
|
|
12
|
-
* Pure function over filesystem mtime. Tested with synthetic file ages.
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
import { statSync } from "node:fs";
|
|
16
|
-
import { join } from "node:path";
|
|
17
|
-
import { homedir } from "node:os";
|
|
18
|
-
import type { SessionStatus } from "@shared/types.js";
|
|
19
|
-
|
|
20
|
-
const ACTIVE_THRESHOLD_MS = 15 * 60 * 1000;
|
|
21
|
-
const IDLE_THRESHOLD_MS = 24 * 60 * 60 * 1000;
|
|
22
|
-
|
|
23
|
-
function expandHome(path: string): string {
|
|
24
|
-
if (path.startsWith("~/")) return join(homedir(), path.slice(2));
|
|
25
|
-
return path;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export function liveSessionStatus(
|
|
29
|
-
transcriptPath: string | null,
|
|
30
|
-
persistedStatus: SessionStatus | "active" | "closed" | "superseded",
|
|
31
|
-
now: number = Date.now(),
|
|
32
|
-
): SessionStatus {
|
|
33
|
-
if (persistedStatus === "superseded") return "superseded";
|
|
34
|
-
if (!transcriptPath) return "closed";
|
|
35
|
-
try {
|
|
36
|
-
const expanded = expandHome(transcriptPath);
|
|
37
|
-
const st = statSync(expanded);
|
|
38
|
-
const ageMs = now - st.mtimeMs;
|
|
39
|
-
if (ageMs < ACTIVE_THRESHOLD_MS) return "active";
|
|
40
|
-
if (ageMs < IDLE_THRESHOLD_MS) return "idle";
|
|
41
|
-
return "closed";
|
|
42
|
-
} catch {
|
|
43
|
-
return "closed";
|
|
44
|
-
}
|
|
45
|
-
}
|
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Migration runner. Reads versioned *.sql files from a directory, applies any
|
|
3
|
-
* whose integer prefix is not yet in schema_migrations, and returns the list
|
|
4
|
-
* of newly applied versions. Idempotent: re-running on an up-to-date database
|
|
5
|
-
* is a no-op. Each migration file is expected to end with its own
|
|
6
|
-
* `INSERT OR IGNORE INTO schema_migrations (...) VALUES (...)`; the runner
|
|
7
|
-
* also defensively upserts the row in case a file forgets.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import { readFileSync, readdirSync } from "node:fs";
|
|
11
|
-
import { join } from "node:path";
|
|
12
|
-
import type Database from "better-sqlite3";
|
|
13
|
-
|
|
14
|
-
export interface AppliedMigration {
|
|
15
|
-
readonly version: number;
|
|
16
|
-
readonly name: string;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
const FILE_PATTERN = /^(\d+)_([a-z0-9_-]+)\.sql$/i;
|
|
20
|
-
|
|
21
|
-
export function runMigrations(
|
|
22
|
-
db: Database.Database,
|
|
23
|
-
migrationsDir: string,
|
|
24
|
-
): ReadonlyArray<AppliedMigration> {
|
|
25
|
-
db.exec(`
|
|
26
|
-
CREATE TABLE IF NOT EXISTS schema_migrations (
|
|
27
|
-
version INTEGER PRIMARY KEY,
|
|
28
|
-
name TEXT NOT NULL,
|
|
29
|
-
applied_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
30
|
-
);
|
|
31
|
-
`);
|
|
32
|
-
|
|
33
|
-
const applied = new Set<number>(
|
|
34
|
-
db
|
|
35
|
-
.prepare<[], { version: number }>("SELECT version FROM schema_migrations")
|
|
36
|
-
.all()
|
|
37
|
-
.map((r) => r.version),
|
|
38
|
-
);
|
|
39
|
-
|
|
40
|
-
const files = readdirSync(migrationsDir)
|
|
41
|
-
.filter((f) => FILE_PATTERN.test(f))
|
|
42
|
-
.sort();
|
|
43
|
-
|
|
44
|
-
const newlyApplied: AppliedMigration[] = [];
|
|
45
|
-
const upsert = db.prepare(
|
|
46
|
-
"INSERT OR IGNORE INTO schema_migrations (version, name) VALUES (?, ?)",
|
|
47
|
-
);
|
|
48
|
-
|
|
49
|
-
for (const file of files) {
|
|
50
|
-
const match = FILE_PATTERN.exec(file);
|
|
51
|
-
if (!match) continue;
|
|
52
|
-
const version = Number(match[1]);
|
|
53
|
-
const name = match[2] ?? file;
|
|
54
|
-
if (applied.has(version)) continue;
|
|
55
|
-
|
|
56
|
-
const sql = readFileSync(join(migrationsDir, file), "utf8");
|
|
57
|
-
db.exec("BEGIN");
|
|
58
|
-
try {
|
|
59
|
-
db.exec(sql);
|
|
60
|
-
upsert.run(version, name);
|
|
61
|
-
db.exec("COMMIT");
|
|
62
|
-
} catch (err) {
|
|
63
|
-
db.exec("ROLLBACK");
|
|
64
|
-
throw new Error(
|
|
65
|
-
`Migration ${file} failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
66
|
-
);
|
|
67
|
-
}
|
|
68
|
-
newlyApplied.push({ version, name });
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
return newlyApplied;
|
|
72
|
-
}
|