nlm-memory 0.5.0 → 0.5.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 +89 -34
- package/dist/cli/digest.d.ts +20 -0
- package/dist/cli/digest.js +142 -0
- package/dist/cli/digest.js.map +1 -0
- package/dist/cli/nlm.d.ts +1 -0
- package/dist/cli/nlm.js +25 -1
- package/dist/cli/nlm.js.map +1 -1
- package/dist/core/digest/compose.d.ts +38 -0
- package/dist/core/digest/compose.js +93 -0
- package/dist/core/digest/compose.js.map +1 -0
- package/dist/core/digest/hook-liveness.d.ts +32 -0
- package/dist/core/digest/hook-liveness.js +54 -0
- package/dist/core/digest/hook-liveness.js.map +1 -0
- 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
package/src/core/hook/memo.ts
DELETED
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Per-conversation dedup memo for the recall hook. One JSON file per
|
|
3
|
-
* conversation holds the set of session ids already surfaced, so each is
|
|
4
|
-
* surfaced at most once per conversation.
|
|
5
|
-
*
|
|
6
|
-
* State dir defaults to ~/.nlm/hook-state/, overridable via
|
|
7
|
-
* NLM_HOOK_STATE_DIR (testability — mirrors query-log.ts).
|
|
8
|
-
*
|
|
9
|
-
* Every function is defensive: a missing or corrupt file yields an empty
|
|
10
|
-
* memo, and a write failure is swallowed. The hook must never break on memo
|
|
11
|
-
* I/O.
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
15
|
-
import { homedir } from "node:os";
|
|
16
|
-
import { join } from "node:path";
|
|
17
|
-
|
|
18
|
-
function stateDir(): string {
|
|
19
|
-
return process.env["NLM_HOOK_STATE_DIR"] ?? join(homedir(), ".nlm", "hook-state");
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function memoPath(conversationId: string): string {
|
|
23
|
-
const safe = conversationId.replace(/[^A-Za-z0-9_-]/g, "_") || "unknown";
|
|
24
|
-
return join(stateDir(), `${safe}.json`);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export function loadSurfaced(conversationId: string): Set<string> {
|
|
28
|
-
try {
|
|
29
|
-
const path = memoPath(conversationId);
|
|
30
|
-
if (!existsSync(path)) return new Set();
|
|
31
|
-
const parsed: unknown = JSON.parse(readFileSync(path, "utf8"));
|
|
32
|
-
if (!Array.isArray(parsed)) return new Set();
|
|
33
|
-
return new Set(parsed.filter((x): x is string => typeof x === "string"));
|
|
34
|
-
} catch {
|
|
35
|
-
return new Set();
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export function recordSurfaced(
|
|
40
|
-
conversationId: string,
|
|
41
|
-
ids: ReadonlyArray<string>,
|
|
42
|
-
): void {
|
|
43
|
-
try {
|
|
44
|
-
const merged = loadSurfaced(conversationId);
|
|
45
|
-
for (const id of ids) merged.add(id);
|
|
46
|
-
mkdirSync(stateDir(), { recursive: true });
|
|
47
|
-
writeFileSync(memoPath(conversationId), JSON.stringify([...merged]), "utf8");
|
|
48
|
-
} catch {
|
|
49
|
-
// Memo write failure must never break the hook.
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Delete the memo file for a closed conversation. Called by the SessionEnd
|
|
55
|
-
* hook so memo files don't accumulate forever. Returns true if a file was
|
|
56
|
-
* removed, false otherwise — callers may want to log the outcome.
|
|
57
|
-
*/
|
|
58
|
-
export function clearSurfaced(conversationId: string): boolean {
|
|
59
|
-
try {
|
|
60
|
-
const path = memoPath(conversationId);
|
|
61
|
-
if (!existsSync(path)) return false;
|
|
62
|
-
rmSync(path);
|
|
63
|
-
return true;
|
|
64
|
-
} catch {
|
|
65
|
-
return false;
|
|
66
|
-
}
|
|
67
|
-
}
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Renders the pointer block injected by the recall hook in live mode. Pure.
|
|
3
|
-
* Pointer-only by design: ids + labels, no session content. The footer
|
|
4
|
-
* names all four NLM MCP tools because the pointer block is the only
|
|
5
|
-
* cross-runtime distribution surface for teaching the tool inventory —
|
|
6
|
-
* fresh-install users never edit a prompt or settings file, so anything
|
|
7
|
-
* we want the agent to know about the tool surface ships here.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
export interface PointerHit {
|
|
11
|
-
readonly id: string;
|
|
12
|
-
readonly label: string;
|
|
13
|
-
readonly startedAt: string;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export function formatPointerBlock(hits: ReadonlyArray<PointerHit>): string {
|
|
17
|
-
if (hits.length === 0) return "";
|
|
18
|
-
const lines = hits.map(
|
|
19
|
-
(h) => `- ${h.id} · ${h.label} (${h.startedAt.slice(0, 10)})`,
|
|
20
|
-
);
|
|
21
|
-
return [
|
|
22
|
-
"## Possibly-relevant prior sessions (nlm-memory)",
|
|
23
|
-
...lines,
|
|
24
|
-
"NLM tools: recall_sessions (search), get_session (full transcript), recall_facts (prior decisions), get_fact_history (how a decision evolved).",
|
|
25
|
-
].join("\n");
|
|
26
|
-
}
|
package/src/core/hook/select.ts
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Selects which recall hits the hook surfaces. Pure — no I/O.
|
|
3
|
-
*
|
|
4
|
-
* Order of filtering: score threshold, then dedup against ids already
|
|
5
|
-
* surfaced in this conversation, then the per-fire cap bounded by the
|
|
6
|
-
* remaining per-conversation budget. Hits are assumed pre-ranked best-first.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
export interface RecallHitInput {
|
|
10
|
-
readonly id: string;
|
|
11
|
-
readonly label: string;
|
|
12
|
-
readonly startedAt: string;
|
|
13
|
-
readonly matchScore: number;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export interface SelectParams {
|
|
17
|
-
readonly hits: ReadonlyArray<RecallHitInput>;
|
|
18
|
-
readonly surfaced: ReadonlySet<string>;
|
|
19
|
-
readonly scoreThreshold: number;
|
|
20
|
-
readonly perFireCap: number;
|
|
21
|
-
readonly perConversationCap: number;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export function selectHits(params: SelectParams): ReadonlyArray<RecallHitInput> {
|
|
25
|
-
const { hits, surfaced, scoreThreshold, perFireCap, perConversationCap } = params;
|
|
26
|
-
const eligible = hits.filter(
|
|
27
|
-
(h) => h.matchScore >= scoreThreshold && !surfaced.has(h.id),
|
|
28
|
-
);
|
|
29
|
-
const budget = Math.max(0, perConversationCap - surfaced.size);
|
|
30
|
-
const limit = Math.min(perFireCap, budget);
|
|
31
|
-
return eligible.slice(0, limit);
|
|
32
|
-
}
|
|
@@ -1,121 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Read assistant messages from a Claude Code transcript JSONL.
|
|
3
|
-
*
|
|
4
|
-
* Claude Code passes `transcript_path` in the Stop hook payload. Each line is
|
|
5
|
-
* a JSON object; assistant turns have `type:"assistant"` and a `message`
|
|
6
|
-
* object whose `content` is an array of blocks (`{type:"text", text:...}` for
|
|
7
|
-
* prose; `{type:"tool_use", name, input}` for tool invocations).
|
|
8
|
-
*
|
|
9
|
-
* Stop-hook citation detection needs the union of ALL assistant turns in the
|
|
10
|
-
* transcript, not just the last one: the model typically calls a tool, reads
|
|
11
|
-
* the result on the next user turn (tool_result), then writes a prose summary
|
|
12
|
-
* as a separate assistant turn. Scanning only the last turn misses the
|
|
13
|
-
* tool_use entirely. `readAllAssistantTurns` returns every assistant turn in
|
|
14
|
-
* order so the detector can fire across the whole conversation; cross-firing
|
|
15
|
-
* dedup happens upstream via the per-conversation cited memo.
|
|
16
|
-
*
|
|
17
|
-
* Fail-quiet: a malformed file yields nulls/empty rather than throwing —
|
|
18
|
-
* the Stop hook must never break on transcript I/O.
|
|
19
|
-
*/
|
|
20
|
-
|
|
21
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
22
|
-
|
|
23
|
-
interface ContentBlock {
|
|
24
|
-
readonly type: string;
|
|
25
|
-
readonly text?: string;
|
|
26
|
-
readonly name?: string;
|
|
27
|
-
readonly input?: unknown;
|
|
28
|
-
}
|
|
29
|
-
interface AssistantMessage {
|
|
30
|
-
readonly content?: ReadonlyArray<ContentBlock> | string;
|
|
31
|
-
}
|
|
32
|
-
interface TranscriptLine {
|
|
33
|
-
readonly type?: string;
|
|
34
|
-
readonly message?: AssistantMessage;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export interface ToolUseBlock {
|
|
38
|
-
readonly name: string;
|
|
39
|
-
readonly input: unknown;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export interface AssistantTurn {
|
|
43
|
-
readonly text: string;
|
|
44
|
-
readonly toolUses: ReadonlyArray<ToolUseBlock>;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
const EMPTY_TURN: AssistantTurn = { text: "", toolUses: [] };
|
|
48
|
-
|
|
49
|
-
function parseTurn(parsed: TranscriptLine): AssistantTurn | null {
|
|
50
|
-
if (parsed.type !== "assistant" || !parsed.message) return null;
|
|
51
|
-
const content = parsed.message.content;
|
|
52
|
-
if (typeof content === "string") {
|
|
53
|
-
return content ? { text: content, toolUses: [] } : null;
|
|
54
|
-
}
|
|
55
|
-
if (!Array.isArray(content)) return null;
|
|
56
|
-
const textParts: string[] = [];
|
|
57
|
-
const toolUses: ToolUseBlock[] = [];
|
|
58
|
-
for (const block of content) {
|
|
59
|
-
if (block.type === "text" && typeof block.text === "string") {
|
|
60
|
-
textParts.push(block.text);
|
|
61
|
-
} else if (block.type === "tool_use" && typeof block.name === "string") {
|
|
62
|
-
toolUses.push({ name: block.name, input: block.input });
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
if (textParts.length === 0 && toolUses.length === 0) return null;
|
|
66
|
-
return { text: textParts.join("\n"), toolUses };
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function readLines(transcriptPath: string): string[] | null {
|
|
70
|
-
if (!transcriptPath || !existsSync(transcriptPath)) return null;
|
|
71
|
-
try {
|
|
72
|
-
return readFileSync(transcriptPath, "utf8").split("\n");
|
|
73
|
-
} catch {
|
|
74
|
-
return null;
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
export function readAllAssistantTurns(
|
|
79
|
-
transcriptPath: string,
|
|
80
|
-
): ReadonlyArray<AssistantTurn> {
|
|
81
|
-
const lines = readLines(transcriptPath);
|
|
82
|
-
if (!lines) return [];
|
|
83
|
-
const turns: AssistantTurn[] = [];
|
|
84
|
-
for (const raw of lines) {
|
|
85
|
-
const line = raw.trim();
|
|
86
|
-
if (!line) continue;
|
|
87
|
-
let parsed: TranscriptLine;
|
|
88
|
-
try {
|
|
89
|
-
parsed = JSON.parse(line) as TranscriptLine;
|
|
90
|
-
} catch {
|
|
91
|
-
continue;
|
|
92
|
-
}
|
|
93
|
-
const turn = parseTurn(parsed);
|
|
94
|
-
if (turn) turns.push(turn);
|
|
95
|
-
}
|
|
96
|
-
return turns;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
export function readLastAssistantTurn(transcriptPath: string): AssistantTurn {
|
|
100
|
-
const lines = readLines(transcriptPath);
|
|
101
|
-
if (!lines) return EMPTY_TURN;
|
|
102
|
-
for (let i = lines.length - 1; i >= 0; i--) {
|
|
103
|
-
const line = lines[i]?.trim();
|
|
104
|
-
if (!line) continue;
|
|
105
|
-
let parsed: TranscriptLine;
|
|
106
|
-
try {
|
|
107
|
-
parsed = JSON.parse(line) as TranscriptLine;
|
|
108
|
-
} catch {
|
|
109
|
-
continue;
|
|
110
|
-
}
|
|
111
|
-
const turn = parseTurn(parsed);
|
|
112
|
-
if (turn) return turn;
|
|
113
|
-
}
|
|
114
|
-
return EMPTY_TURN;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
/** Back-compat shim for callers that only need prose. */
|
|
118
|
-
export function readLastAssistantText(transcriptPath: string): string | null {
|
|
119
|
-
const turn = readLastAssistantTurn(transcriptPath);
|
|
120
|
-
return turn.text || null;
|
|
121
|
-
}
|
|
@@ -1,111 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ingestSession — push a single externally-supplied session through the
|
|
3
|
-
* normal classifier → embedder → store pipeline.
|
|
4
|
-
*
|
|
5
|
-
* Shared by the webhook endpoint (POST /api/ingest) and anything else
|
|
6
|
-
* that wants to push without going through a TranscriptAdapter. Mirrors
|
|
7
|
-
* the inner loop of ScanScheduler.runOnce but accepts a pre-built chunk.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import { createHash } from "node:crypto";
|
|
11
|
-
import { extractFacts } from "@core/facts/extract-facts.js";
|
|
12
|
-
import type { SqliteFactStore } from "@core/storage/sqlite-fact-store.js";
|
|
13
|
-
import type { IngestRecord, SqliteSessionStore } from "@core/storage/sqlite-session-store.js";
|
|
14
|
-
import type { LLMClient } from "@ports/llm-client.js";
|
|
15
|
-
|
|
16
|
-
const BODY_CAP = 200_000;
|
|
17
|
-
const CONFIDENCE_FLOOR = 0.3;
|
|
18
|
-
|
|
19
|
-
export interface IngestInput {
|
|
20
|
-
/** Optional — if omitted, derived from a hash of (runtime + startedAt + text). */
|
|
21
|
-
readonly id?: string;
|
|
22
|
-
readonly runtime: string;
|
|
23
|
-
readonly runtimeSessionId?: string | null;
|
|
24
|
-
readonly text: string;
|
|
25
|
-
readonly startedAt?: string;
|
|
26
|
-
readonly endedAt?: string | null;
|
|
27
|
-
readonly transcriptPath?: string | null;
|
|
28
|
-
/** Webhook id when the source is webhook-pushed; null for generic. */
|
|
29
|
-
readonly sourceId?: number | null;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export interface IngestDeps {
|
|
33
|
-
readonly classifier: LLMClient;
|
|
34
|
-
readonly embedder: LLMClient;
|
|
35
|
-
readonly store: SqliteSessionStore;
|
|
36
|
-
readonly factStore?: SqliteFactStore;
|
|
37
|
-
/** Optional logger — defaults to console.error. */
|
|
38
|
-
readonly log?: (msg: string) => void;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export interface IngestResult {
|
|
42
|
-
readonly id: string;
|
|
43
|
-
readonly status: "ingested" | "low_confidence" | "classifier_failed";
|
|
44
|
-
readonly latencyMs: number;
|
|
45
|
-
readonly confidence?: number;
|
|
46
|
-
readonly error?: string;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export function deriveSessionId(runtime: string, startedAt: string, text: string): string {
|
|
50
|
-
const hash = createHash("sha256")
|
|
51
|
-
.update(runtime)
|
|
52
|
-
.update("|")
|
|
53
|
-
.update(startedAt)
|
|
54
|
-
.update("|")
|
|
55
|
-
.update(text.slice(0, 4_000))
|
|
56
|
-
.digest("hex")
|
|
57
|
-
.slice(0, 16);
|
|
58
|
-
return `webhook_${hash}`;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
export async function ingestSession(input: IngestInput, deps: IngestDeps): Promise<IngestResult> {
|
|
62
|
-
const startedAt = input.startedAt ?? new Date().toISOString();
|
|
63
|
-
const id = input.id ?? deriveSessionId(input.runtime, startedAt, input.text);
|
|
64
|
-
const log = deps.log ?? ((m: string) => console.error(m));
|
|
65
|
-
const t0 = Date.now();
|
|
66
|
-
|
|
67
|
-
let classification;
|
|
68
|
-
try {
|
|
69
|
-
classification = await deps.classifier.classify(input.text);
|
|
70
|
-
} catch (e) {
|
|
71
|
-
const error = e instanceof Error ? e.message : String(e);
|
|
72
|
-
log(`[ingest] classifier failed for ${id}: ${error}`);
|
|
73
|
-
return { id, status: "classifier_failed", latencyMs: Date.now() - t0, error };
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
if (classification.confidence < CONFIDENCE_FLOOR) {
|
|
77
|
-
return {
|
|
78
|
-
id,
|
|
79
|
-
status: "low_confidence",
|
|
80
|
-
latencyMs: Date.now() - t0,
|
|
81
|
-
confidence: classification.confidence,
|
|
82
|
-
};
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
const record: IngestRecord = {
|
|
86
|
-
id,
|
|
87
|
-
runtime: input.runtime,
|
|
88
|
-
runtimeSessionId: input.runtimeSessionId ?? null,
|
|
89
|
-
startedAt,
|
|
90
|
-
endedAt: input.endedAt ?? null,
|
|
91
|
-
durationMin: null,
|
|
92
|
-
label: classification.label,
|
|
93
|
-
summary: classification.summary,
|
|
94
|
-
body: input.text.slice(0, BODY_CAP),
|
|
95
|
-
status: "closed",
|
|
96
|
-
transcriptKind: "webhook",
|
|
97
|
-
transcriptPath: input.transcriptPath ?? null,
|
|
98
|
-
transcriptOffset: null,
|
|
99
|
-
transcriptLength: null,
|
|
100
|
-
entities: classification.entities,
|
|
101
|
-
decisions: classification.decisions,
|
|
102
|
-
openQuestions: classification.open,
|
|
103
|
-
};
|
|
104
|
-
|
|
105
|
-
const factSink = deps.factStore
|
|
106
|
-
? { factStore: deps.factStore, facts: extractFacts(classification, id, startedAt) }
|
|
107
|
-
: null;
|
|
108
|
-
|
|
109
|
-
await deps.store.insertSession(record, deps.embedder, null, factSink);
|
|
110
|
-
return { id, status: "ingested", latencyMs: Date.now() - t0, confidence: classification.confidence };
|
|
111
|
-
}
|
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Provider model discovery — runtime lookup of available models.
|
|
3
|
-
*
|
|
4
|
-
* Per-kind strategy:
|
|
5
|
-
* - ollama: GET {baseUrl}/api/tags
|
|
6
|
-
* - openai: GET {baseUrl}/models with Bearer key
|
|
7
|
-
* - openrouter: GET {baseUrl}/models with Bearer key
|
|
8
|
-
* - openai-compatible: GET {baseUrl}/models, key optional
|
|
9
|
-
* - deepseek: hardcoded (no public list endpoint)
|
|
10
|
-
* - anthropic: hardcoded (their /v1/models exists but
|
|
11
|
-
* requires beta header + returns subsets;
|
|
12
|
-
* a hardcoded list is more reliable)
|
|
13
|
-
*
|
|
14
|
-
* Returns a flat `string[]`. Errors throw — callers (the HTTP endpoint
|
|
15
|
-
* and connection-test) catch and surface to the user.
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
import type { ProviderKind, ProviderRow } from "./provider-registry.js";
|
|
19
|
-
|
|
20
|
-
export type FetchImpl = typeof fetch;
|
|
21
|
-
|
|
22
|
-
const HARDCODED_MODELS: Partial<Record<ProviderKind, string[]>> = {
|
|
23
|
-
deepseek: ["deepseek-v4-flash", "deepseek-v4-pro", "deepseek-chat"],
|
|
24
|
-
anthropic: [
|
|
25
|
-
"claude-opus-4-7",
|
|
26
|
-
"claude-sonnet-4-6",
|
|
27
|
-
"claude-haiku-4-5-20251001",
|
|
28
|
-
],
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
interface OllamaTagsResponse {
|
|
32
|
-
readonly models?: ReadonlyArray<{ readonly name?: string }>;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
interface OpenAIModelsResponse {
|
|
36
|
-
readonly data?: ReadonlyArray<{ readonly id?: string }>;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export interface ListModelsOptions {
|
|
40
|
-
readonly apiKey?: string | null;
|
|
41
|
-
readonly fetchImpl?: FetchImpl;
|
|
42
|
-
readonly timeoutMs?: number;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export async function listModels(
|
|
46
|
-
provider: ProviderRow,
|
|
47
|
-
opts: ListModelsOptions = {},
|
|
48
|
-
): Promise<string[]> {
|
|
49
|
-
const hardcoded = HARDCODED_MODELS[provider.kind];
|
|
50
|
-
if (hardcoded) return [...hardcoded];
|
|
51
|
-
|
|
52
|
-
const fetchImpl = opts.fetchImpl ?? fetch;
|
|
53
|
-
const timeoutMs = opts.timeoutMs ?? 10_000;
|
|
54
|
-
const baseUrl = (provider.baseUrl ?? "").replace(/\/+$/, "");
|
|
55
|
-
if (!baseUrl) throw new Error(`${provider.name}: baseUrl not configured`);
|
|
56
|
-
|
|
57
|
-
if (provider.kind === "ollama") {
|
|
58
|
-
return fetchOllamaModels(baseUrl, fetchImpl, timeoutMs);
|
|
59
|
-
}
|
|
60
|
-
return fetchOpenAIModels(baseUrl, opts.apiKey ?? null, fetchImpl, timeoutMs);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
async function fetchOllamaModels(
|
|
64
|
-
baseUrl: string,
|
|
65
|
-
fetchImpl: FetchImpl,
|
|
66
|
-
timeoutMs: number,
|
|
67
|
-
): Promise<string[]> {
|
|
68
|
-
const controller = new AbortController();
|
|
69
|
-
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
70
|
-
try {
|
|
71
|
-
const res = await fetchImpl(`${baseUrl}/api/tags`, { signal: controller.signal });
|
|
72
|
-
if (!res.ok) throw new Error(`Ollama returned ${res.status}`);
|
|
73
|
-
const data = (await res.json()) as OllamaTagsResponse;
|
|
74
|
-
const names = (data.models ?? []).map((m) => m.name).filter((n): n is string => typeof n === "string");
|
|
75
|
-
return names.sort();
|
|
76
|
-
} finally {
|
|
77
|
-
clearTimeout(timer);
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
async function fetchOpenAIModels(
|
|
82
|
-
baseUrl: string,
|
|
83
|
-
apiKey: string | null,
|
|
84
|
-
fetchImpl: FetchImpl,
|
|
85
|
-
timeoutMs: number,
|
|
86
|
-
): Promise<string[]> {
|
|
87
|
-
const controller = new AbortController();
|
|
88
|
-
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
89
|
-
try {
|
|
90
|
-
const headers: Record<string, string> = {};
|
|
91
|
-
if (apiKey) headers["Authorization"] = `Bearer ${apiKey}`;
|
|
92
|
-
const res = await fetchImpl(`${baseUrl}/models`, { signal: controller.signal, headers });
|
|
93
|
-
if (!res.ok) throw new Error(`${res.status} ${res.statusText}`);
|
|
94
|
-
const data = (await res.json()) as OpenAIModelsResponse;
|
|
95
|
-
const ids = (data.data ?? []).map((m) => m.id).filter((s): s is string => typeof s === "string");
|
|
96
|
-
return ids.sort();
|
|
97
|
-
} finally {
|
|
98
|
-
clearTimeout(timer);
|
|
99
|
-
}
|
|
100
|
-
}
|
|
@@ -1,196 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ProviderRegistry — CRUD over the `providers` table.
|
|
3
|
-
*
|
|
4
|
-
* One row per LLM endpoint the user has configured. The classifier reads
|
|
5
|
-
* this at boot to pick a provider/model; the UI lets users add their own.
|
|
6
|
-
*
|
|
7
|
-
* API keys live in the `api_key` column today. Phase 2 (Tauri shell)
|
|
8
|
-
* migrates them to the OS keychain; the API shape stays identical so this
|
|
9
|
-
* module's consumers don't change.
|
|
10
|
-
*
|
|
11
|
-
* `redact()` strips secrets on the way out — every HTTP response sends
|
|
12
|
-
* redacted rows, with the key only retrievable via getSecret() inside the
|
|
13
|
-
* daemon process.
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
import type Database from "better-sqlite3";
|
|
17
|
-
|
|
18
|
-
export type ProviderKind =
|
|
19
|
-
| "deepseek"
|
|
20
|
-
| "ollama"
|
|
21
|
-
| "openai"
|
|
22
|
-
| "anthropic"
|
|
23
|
-
| "openrouter"
|
|
24
|
-
| "openai-compatible";
|
|
25
|
-
|
|
26
|
-
export interface ProviderRow {
|
|
27
|
-
readonly id: number;
|
|
28
|
-
readonly kind: ProviderKind;
|
|
29
|
-
readonly name: string;
|
|
30
|
-
readonly baseUrl: string | null;
|
|
31
|
-
/** Always `null` on rows returned by `list()` / `get()`. Use `getSecret()`. */
|
|
32
|
-
readonly apiKey: string | null;
|
|
33
|
-
readonly hasApiKey: boolean;
|
|
34
|
-
readonly defaultModel: string | null;
|
|
35
|
-
readonly enabled: boolean;
|
|
36
|
-
readonly createdAt: string;
|
|
37
|
-
readonly updatedAt: string;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export interface ProviderInsert {
|
|
41
|
-
readonly kind: ProviderKind;
|
|
42
|
-
readonly name: string;
|
|
43
|
-
readonly baseUrl?: string | null;
|
|
44
|
-
readonly apiKey?: string | null;
|
|
45
|
-
readonly defaultModel?: string | null;
|
|
46
|
-
readonly enabled?: boolean;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export interface ProviderUpdate {
|
|
50
|
-
readonly name?: string;
|
|
51
|
-
readonly baseUrl?: string | null;
|
|
52
|
-
readonly apiKey?: string | null;
|
|
53
|
-
readonly defaultModel?: string | null;
|
|
54
|
-
readonly enabled?: boolean;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
interface ProviderDbRow {
|
|
58
|
-
id: number;
|
|
59
|
-
kind: string;
|
|
60
|
-
name: string;
|
|
61
|
-
base_url: string | null;
|
|
62
|
-
api_key: string | null;
|
|
63
|
-
default_model: string | null;
|
|
64
|
-
enabled: number;
|
|
65
|
-
created_at: string;
|
|
66
|
-
updated_at: string;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function rowFromDb(r: ProviderDbRow, includeSecret: boolean): ProviderRow {
|
|
70
|
-
return {
|
|
71
|
-
id: r.id,
|
|
72
|
-
kind: r.kind as ProviderKind,
|
|
73
|
-
name: r.name,
|
|
74
|
-
baseUrl: r.base_url,
|
|
75
|
-
apiKey: includeSecret ? r.api_key : null,
|
|
76
|
-
hasApiKey: r.api_key !== null && r.api_key.length > 0,
|
|
77
|
-
defaultModel: r.default_model,
|
|
78
|
-
enabled: r.enabled === 1,
|
|
79
|
-
createdAt: r.created_at,
|
|
80
|
-
updatedAt: r.updated_at,
|
|
81
|
-
};
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
const DEFAULT_BASE_URLS: Record<ProviderKind, string | null> = {
|
|
85
|
-
deepseek: "https://api.deepseek.com",
|
|
86
|
-
ollama: "http://localhost:11434",
|
|
87
|
-
openai: "https://api.openai.com/v1",
|
|
88
|
-
anthropic: "https://api.anthropic.com",
|
|
89
|
-
openrouter: "https://openrouter.ai/api/v1",
|
|
90
|
-
"openai-compatible": null,
|
|
91
|
-
};
|
|
92
|
-
|
|
93
|
-
const DEFAULT_MODELS: Record<ProviderKind, string | null> = {
|
|
94
|
-
deepseek: "deepseek-v4-flash",
|
|
95
|
-
ollama: "phi4-mini:latest",
|
|
96
|
-
openai: "gpt-4o-mini",
|
|
97
|
-
anthropic: "claude-haiku-4-5-20251001",
|
|
98
|
-
openrouter: "anthropic/claude-haiku-4-5",
|
|
99
|
-
"openai-compatible": null,
|
|
100
|
-
};
|
|
101
|
-
|
|
102
|
-
export class ProviderRegistry {
|
|
103
|
-
constructor(private readonly db: Database.Database) {}
|
|
104
|
-
|
|
105
|
-
list(): ProviderRow[] {
|
|
106
|
-
const rows = this.db.prepare<[], ProviderDbRow>(
|
|
107
|
-
`SELECT * FROM providers ORDER BY id ASC`,
|
|
108
|
-
).all();
|
|
109
|
-
return rows.map((r) => rowFromDb(r, false));
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
get(id: number): ProviderRow | null {
|
|
113
|
-
const row = this.db.prepare<[number], ProviderDbRow>(
|
|
114
|
-
`SELECT * FROM providers WHERE id = ?`,
|
|
115
|
-
).get(id);
|
|
116
|
-
return row ? rowFromDb(row, false) : null;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
getByName(name: string): ProviderRow | null {
|
|
120
|
-
const row = this.db.prepare<[string], ProviderDbRow>(
|
|
121
|
-
`SELECT * FROM providers WHERE name = ?`,
|
|
122
|
-
).get(name);
|
|
123
|
-
return row ? rowFromDb(row, false) : null;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
/** Returns the secret. Use only inside the daemon — never echo to HTTP. */
|
|
127
|
-
getSecret(id: number): string | null {
|
|
128
|
-
const row = this.db.prepare<[number], ProviderDbRow>(
|
|
129
|
-
`SELECT * FROM providers WHERE id = ?`,
|
|
130
|
-
).get(id);
|
|
131
|
-
return row?.api_key ?? null;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
insert(input: ProviderInsert): ProviderRow {
|
|
135
|
-
const baseUrl = input.baseUrl ?? DEFAULT_BASE_URLS[input.kind];
|
|
136
|
-
const defaultModel = input.defaultModel ?? DEFAULT_MODELS[input.kind];
|
|
137
|
-
const result = this.db.prepare(`
|
|
138
|
-
INSERT INTO providers (kind, name, base_url, api_key, default_model, enabled)
|
|
139
|
-
VALUES (@kind, @name, @base_url, @api_key, @default_model, @enabled)
|
|
140
|
-
`).run({
|
|
141
|
-
kind: input.kind,
|
|
142
|
-
name: input.name,
|
|
143
|
-
base_url: baseUrl ?? null,
|
|
144
|
-
api_key: input.apiKey ?? null,
|
|
145
|
-
default_model: defaultModel ?? null,
|
|
146
|
-
enabled: input.enabled === false ? 0 : 1,
|
|
147
|
-
});
|
|
148
|
-
const id = Number(result.lastInsertRowid);
|
|
149
|
-
const row = this.get(id);
|
|
150
|
-
if (!row) throw new Error(`ProviderRegistry.insert: row ${id} not found after insert`);
|
|
151
|
-
return row;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
update(id: number, patch: ProviderUpdate): ProviderRow | null {
|
|
155
|
-
const fields: string[] = [];
|
|
156
|
-
const params: Record<string, unknown> = { id };
|
|
157
|
-
if (patch.name !== undefined) { fields.push("name = @name"); params["name"] = patch.name; }
|
|
158
|
-
if (patch.baseUrl !== undefined) { fields.push("base_url = @url"); params["url"] = patch.baseUrl; }
|
|
159
|
-
if (patch.apiKey !== undefined) { fields.push("api_key = @key"); params["key"] = patch.apiKey; }
|
|
160
|
-
if (patch.defaultModel !== undefined) { fields.push("default_model = @m"); params["m"] = patch.defaultModel; }
|
|
161
|
-
if (patch.enabled !== undefined) { fields.push("enabled = @en"); params["en"] = patch.enabled ? 1 : 0; }
|
|
162
|
-
if (fields.length === 0) return this.get(id);
|
|
163
|
-
fields.push("updated_at = datetime('now')");
|
|
164
|
-
this.db.prepare(`UPDATE providers SET ${fields.join(", ")} WHERE id = @id`).run(params);
|
|
165
|
-
return this.get(id);
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
delete(id: number): boolean {
|
|
169
|
-
const result = this.db.prepare(`DELETE FROM providers WHERE id = ?`).run(id);
|
|
170
|
-
return result.changes > 0;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
/**
|
|
174
|
-
* Seed defaults on an empty registry. Bridges from the legacy env-var
|
|
175
|
-
* setup: if DEEPSEEK_API_KEY is present, the DeepSeek row carries it
|
|
176
|
-
* forward; Ollama is always seeded since it needs no key.
|
|
177
|
-
*/
|
|
178
|
-
seedDefaults(): void {
|
|
179
|
-
const count = this.db.prepare<[], { c: number }>(`SELECT COUNT(*) AS c FROM providers`).get();
|
|
180
|
-
if ((count?.c ?? 0) > 0) return;
|
|
181
|
-
|
|
182
|
-
this.insert({
|
|
183
|
-
kind: "ollama",
|
|
184
|
-
name: "Ollama (local)",
|
|
185
|
-
baseUrl: process.env["NLM_OLLAMA_URL"] ?? "http://localhost:11434",
|
|
186
|
-
});
|
|
187
|
-
|
|
188
|
-
const deepseekKey = process.env["DEEPSEEK_API_KEY"];
|
|
189
|
-
this.insert({
|
|
190
|
-
kind: "deepseek",
|
|
191
|
-
name: "DeepSeek",
|
|
192
|
-
apiKey: deepseekKey ?? null,
|
|
193
|
-
enabled: Boolean(deepseekKey),
|
|
194
|
-
});
|
|
195
|
-
}
|
|
196
|
-
}
|