nlm-memory 0.12.0 → 0.13.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 +1 -1
- package/dist/cli/classify-parity.js +1 -1
- package/dist/cli/classify-parity.js.map +1 -1
- package/dist/cli/nlm.js +327 -56
- package/dist/cli/nlm.js.map +1 -1
- package/dist/core/classifier/prompt.d.ts +70 -0
- package/dist/core/classifier/prompt.js +71 -2
- package/dist/core/classifier/prompt.js.map +1 -1
- package/dist/core/exemplars/extract-exemplar.d.ts +36 -0
- package/dist/core/exemplars/extract-exemplar.js +177 -0
- package/dist/core/exemplars/extract-exemplar.js.map +1 -0
- package/dist/core/exemplars/ingest-exemplar.d.ts +30 -0
- package/dist/core/exemplars/ingest-exemplar.js +71 -0
- package/dist/core/exemplars/ingest-exemplar.js.map +1 -0
- package/dist/core/exemplars/recall-code.d.ts +25 -0
- package/dist/core/exemplars/recall-code.js +49 -0
- package/dist/core/exemplars/recall-code.js.map +1 -0
- package/dist/core/facts/backfill-facts.d.ts +4 -2
- package/dist/core/facts/backfill-facts.js +18 -27
- package/dist/core/facts/backfill-facts.js.map +1 -1
- package/dist/core/hook/strip-injected-context.d.ts +24 -0
- package/dist/core/hook/strip-injected-context.js +73 -0
- package/dist/core/hook/strip-injected-context.js.map +1 -0
- package/dist/core/ingest/ingest-session.d.ts +4 -2
- package/dist/core/ingest/ingest-session.js +12 -4
- package/dist/core/ingest/ingest-session.js.map +1 -1
- package/dist/core/providers/provider-registry.d.ts +31 -11
- package/dist/core/providers/provider-registry.js +27 -13
- package/dist/core/providers/provider-registry.js.map +1 -1
- package/dist/core/recall-facts/fact-recall-service.js +75 -28
- package/dist/core/recall-facts/fact-recall-service.js.map +1 -1
- package/dist/core/scheduler/scheduler.d.ts +4 -2
- package/dist/core/scheduler/scheduler.js +26 -15
- package/dist/core/scheduler/scheduler.js.map +1 -1
- package/dist/core/sources/source-registry.d.ts +36 -12
- package/dist/core/sources/source-registry.js +59 -44
- package/dist/core/sources/source-registry.js.map +1 -1
- package/dist/core/storage/backup-rotation.d.ts +38 -0
- package/dist/core/storage/backup-rotation.js +82 -0
- package/dist/core/storage/backup-rotation.js.map +1 -0
- package/dist/core/storage/null-code-exemplar-store.d.ts +21 -0
- package/dist/core/storage/null-code-exemplar-store.js +19 -0
- package/dist/core/storage/null-code-exemplar-store.js.map +1 -0
- package/dist/core/storage/pg-fact-store.d.ts +2 -0
- package/dist/core/storage/pg-fact-store.js +34 -3
- package/dist/core/storage/pg-fact-store.js.map +1 -1
- package/dist/core/storage/pg-session-store.d.ts +14 -4
- package/dist/core/storage/pg-session-store.js +123 -2
- package/dist/core/storage/pg-session-store.js.map +1 -1
- package/dist/core/storage/pg-storage.d.ts +11 -2
- package/dist/core/storage/pg-storage.js +14 -2
- package/dist/core/storage/pg-storage.js.map +1 -1
- package/dist/core/storage/pg-tx-context.d.ts +2 -0
- package/dist/core/storage/pg-tx-context.js +12 -0
- package/dist/core/storage/pg-tx-context.js.map +1 -1
- package/dist/core/storage/sqlite-code-exemplar-store.d.ts +40 -0
- package/dist/core/storage/sqlite-code-exemplar-store.js +247 -0
- package/dist/core/storage/sqlite-code-exemplar-store.js.map +1 -0
- package/dist/core/storage/sqlite-fact-store.d.ts +2 -0
- package/dist/core/storage/sqlite-fact-store.js +30 -2
- package/dist/core/storage/sqlite-fact-store.js.map +1 -1
- package/dist/core/storage/sqlite-session-store.d.ts +21 -0
- package/dist/core/storage/sqlite-session-store.js +23 -0
- package/dist/core/storage/sqlite-session-store.js.map +1 -1
- package/dist/core/storage/sqlite-storage.d.ts +6 -0
- package/dist/core/storage/sqlite-storage.js +14 -2
- package/dist/core/storage/sqlite-storage.js.map +1 -1
- package/dist/core/storage/supersedence-log.d.ts +8 -0
- package/dist/core/storage/supersedence-log.js +19 -0
- package/dist/core/storage/supersedence-log.js.map +1 -1
- package/dist/hook/prompt-recall-hook.d.ts +5 -4
- package/dist/hook/prompt-recall-hook.js +12 -7
- package/dist/hook/prompt-recall-hook.js.map +1 -1
- package/dist/http/app.d.ts +7 -1
- package/dist/http/app.js +226 -83
- package/dist/http/app.js.map +1 -1
- package/dist/install/codex.d.ts +29 -1
- package/dist/install/codex.js +117 -15
- package/dist/install/codex.js.map +1 -1
- package/dist/install/cursor.d.ts +4 -4
- package/dist/install/cursor.js +7 -7
- package/dist/install/cursor.js.map +1 -1
- package/dist/install/health.d.ts +56 -0
- package/dist/install/health.js +133 -0
- package/dist/install/health.js.map +1 -0
- package/dist/install/setup.js +1 -1
- package/dist/install/setup.js.map +1 -1
- package/dist/install/windsurf.d.ts +4 -4
- package/dist/install/windsurf.js +7 -7
- package/dist/install/windsurf.js.map +1 -1
- package/dist/llm/classifier-box.d.ts +4 -0
- package/dist/llm/classifier-box.js +9 -2
- package/dist/llm/classifier-box.js.map +1 -1
- package/dist/llm/ollama-client.d.ts +19 -1
- package/dist/llm/ollama-client.js +14 -6
- package/dist/llm/ollama-client.js.map +1 -1
- package/dist/llm/ollama-code-embedder.d.ts +28 -0
- package/dist/llm/ollama-code-embedder.js +73 -0
- package/dist/llm/ollama-code-embedder.js.map +1 -0
- package/dist/mcp/server.d.ts +10 -0
- package/dist/mcp/server.js +132 -1
- package/dist/mcp/server.js.map +1 -1
- package/dist/ports/code-embedder.d.ts +27 -0
- package/dist/ports/code-embedder.js +15 -0
- package/dist/ports/code-embedder.js.map +1 -0
- package/dist/ports/code-exemplar-store.d.ts +45 -0
- package/dist/ports/code-exemplar-store.js +11 -0
- package/dist/ports/code-exemplar-store.js.map +1 -0
- package/dist/ports/fact-store.d.ts +0 -0
- package/dist/ports/fact-store.js.map +1 -1
- package/dist/ports/storage.d.ts +8 -0
- package/dist/shared/types.d.ts +35 -0
- package/dist/ui/assets/{index-Fr5AMEPj.js → index-CVo7KKw2.js} +1 -1
- package/dist/ui/assets/{index-C74N8APQ.css → index-DCuQrzJH.css} +1 -1
- package/dist/ui/index.html +2 -2
- package/migrations/021_code_exemplars.sql +51 -0
- package/migrations/022_facts_retired_at.sql +17 -0
- package/migrations/pg/021_code_exemplars.sql +45 -0
- package/migrations/pg/022_facts_retired_at.sql +12 -0
- package/nlm/index.js +6 -1
- package/package.json +3 -2
- package/plugin/.codex-plugin/plugin.json +2 -2
- package/plugin/hooks/hooks.json +13 -2
- package/plugin/scripts/prompt-recall-hook.mjs +7 -1
- package/plugin/scripts/session-start-hook.mjs +275 -0
package/dist/http/app.js
CHANGED
|
@@ -45,8 +45,12 @@ import { SourceRegistry, PgSourceRegistry, } from "../core/sources/source-regist
|
|
|
45
45
|
import { ProviderRegistry, PgProviderRegistry, } from "../core/providers/provider-registry.js";
|
|
46
46
|
import { listModels } from "../core/providers/provider-models.js";
|
|
47
47
|
import { ingestSession, deriveSessionId } from "../core/ingest/ingest-session.js";
|
|
48
|
-
import { listActions, undoAction, writeAction, writeActionsBatch, } from "../core/actions/actions-log.js";
|
|
48
|
+
import { listActions, listActionsPg, undoAction, undoActionPg, writeAction, writeActionPg, writeActionsBatch, writeActionsBatchPg, } from "../core/actions/actions-log.js";
|
|
49
|
+
import { PgSessionStore } from "../core/storage/pg-session-store.js";
|
|
49
50
|
import { normalizeSignal } from "../core/signals/ingest-signal.js";
|
|
51
|
+
import { normalizeExemplar } from "../core/exemplars/ingest-exemplar.js";
|
|
52
|
+
import { extractExemplar } from "../core/exemplars/extract-exemplar.js";
|
|
53
|
+
import { recallCode } from "../core/exemplars/recall-code.js";
|
|
50
54
|
import { buildFailureModeBlock } from "../core/signals/failure-mode-recall.js";
|
|
51
55
|
import { aggregateFailureModes } from "../core/signals/aggregate.js";
|
|
52
56
|
const MIME_TYPES = {
|
|
@@ -73,6 +77,81 @@ const DATA_STAT_TABLES = [
|
|
|
73
77
|
"sources",
|
|
74
78
|
"providers",
|
|
75
79
|
];
|
|
80
|
+
function sqliteDataStats(db, dbPath) {
|
|
81
|
+
const countOf = (table) => {
|
|
82
|
+
try {
|
|
83
|
+
const row = db.prepare(`SELECT COUNT(*) AS n FROM ${table}`).get();
|
|
84
|
+
return row?.n ?? 0;
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
return 0;
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
const tables = DATA_STAT_TABLES.map((name) => ({ name, rows: countOf(name) }));
|
|
91
|
+
const migrations = db
|
|
92
|
+
.prepare("SELECT version, name, applied_at FROM schema_migrations ORDER BY version")
|
|
93
|
+
.all();
|
|
94
|
+
const runtimes = db
|
|
95
|
+
.prepare("SELECT runtime, COUNT(*) AS n FROM sessions GROUP BY runtime ORDER BY n DESC")
|
|
96
|
+
.all();
|
|
97
|
+
let dbBytes = 0;
|
|
98
|
+
let dbPresent = false;
|
|
99
|
+
try {
|
|
100
|
+
dbBytes = statSync(dbPath).size;
|
|
101
|
+
dbPresent = true;
|
|
102
|
+
}
|
|
103
|
+
catch { /* file absent */ }
|
|
104
|
+
for (const sidecar of [`${dbPath}-wal`, `${dbPath}-shm`]) {
|
|
105
|
+
try {
|
|
106
|
+
dbBytes += statSync(sidecar).size;
|
|
107
|
+
}
|
|
108
|
+
catch { /* no sidecar */ }
|
|
109
|
+
}
|
|
110
|
+
return {
|
|
111
|
+
dbPath,
|
|
112
|
+
dbBytes,
|
|
113
|
+
dbPresent,
|
|
114
|
+
schemaVersion: migrations.length > 0 ? migrations[migrations.length - 1].version : null,
|
|
115
|
+
migrations,
|
|
116
|
+
tables,
|
|
117
|
+
runtimes,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
async function pgDataStats(pool) {
|
|
121
|
+
const countOf = async (table) => {
|
|
122
|
+
try {
|
|
123
|
+
const r = await pool.query(`SELECT COUNT(*) AS n FROM ${table}`);
|
|
124
|
+
return Number(r.rows[0]?.n ?? 0);
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
return 0;
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
const tables = await Promise.all(DATA_STAT_TABLES.map(async (name) => ({ name, rows: await countOf(name) })));
|
|
131
|
+
const migrationsResult = await pool.query("SELECT version, name, applied_at FROM schema_migrations ORDER BY version");
|
|
132
|
+
const migrations = migrationsResult.rows.map((r) => ({
|
|
133
|
+
version: r.version,
|
|
134
|
+
name: r.name,
|
|
135
|
+
applied_at: String(r.applied_at),
|
|
136
|
+
}));
|
|
137
|
+
const runtimesResult = await pool.query("SELECT runtime, COUNT(*) AS n FROM sessions GROUP BY runtime ORDER BY n DESC");
|
|
138
|
+
const runtimes = runtimesResult.rows.map((r) => ({ runtime: r.runtime, n: Number(r.n) }));
|
|
139
|
+
let dbBytes = 0;
|
|
140
|
+
try {
|
|
141
|
+
const sizeResult = await pool.query("SELECT pg_database_size(current_database()) AS bytes");
|
|
142
|
+
dbBytes = Number(sizeResult.rows[0]?.bytes ?? 0);
|
|
143
|
+
}
|
|
144
|
+
catch { /* size unavailable */ }
|
|
145
|
+
return {
|
|
146
|
+
dbPath: "postgresql",
|
|
147
|
+
dbBytes,
|
|
148
|
+
dbPresent: true,
|
|
149
|
+
schemaVersion: migrations.length > 0 ? migrations[migrations.length - 1].version : null,
|
|
150
|
+
migrations,
|
|
151
|
+
tables,
|
|
152
|
+
runtimes,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
76
155
|
function parseLimit(raw, fallback, max) {
|
|
77
156
|
if (raw === undefined)
|
|
78
157
|
return fallback;
|
|
@@ -787,50 +866,14 @@ function registerDatasetRoute(app, deps) {
|
|
|
787
866
|
// ── Data management ─────────────────────────────────────────────
|
|
788
867
|
// Storage stats, live-safe backup snapshot, and staged restore.
|
|
789
868
|
function registerDataManagementRoutes(app, deps) {
|
|
790
|
-
app.get("/api/data/stats", (c) => {
|
|
869
|
+
app.get("/api/data/stats", async (c) => {
|
|
791
870
|
if (!deps.liveStore || !deps.dbPath) {
|
|
792
871
|
return c.json({ error: "data stats require liveStore + dbPath" }, 503);
|
|
793
872
|
}
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
const row = db.prepare(`SELECT COUNT(*) AS n FROM ${table}`).get();
|
|
799
|
-
return row?.n ?? 0;
|
|
800
|
-
}
|
|
801
|
-
catch {
|
|
802
|
-
return 0;
|
|
803
|
-
}
|
|
804
|
-
};
|
|
805
|
-
const tables = DATA_STAT_TABLES.map((name) => ({ name, rows: countOf(name) }));
|
|
806
|
-
const migrations = db
|
|
807
|
-
.prepare("SELECT version, name, applied_at FROM schema_migrations ORDER BY version")
|
|
808
|
-
.all();
|
|
809
|
-
const runtimes = db
|
|
810
|
-
.prepare("SELECT runtime, COUNT(*) AS n FROM sessions GROUP BY runtime ORDER BY n DESC")
|
|
811
|
-
.all();
|
|
812
|
-
let dbBytes = 0;
|
|
813
|
-
let dbPresent = false;
|
|
814
|
-
try {
|
|
815
|
-
dbBytes = statSync(deps.dbPath).size;
|
|
816
|
-
dbPresent = true;
|
|
817
|
-
}
|
|
818
|
-
catch { /* file absent */ }
|
|
819
|
-
for (const sidecar of [`${deps.dbPath}-wal`, `${deps.dbPath}-shm`]) {
|
|
820
|
-
try {
|
|
821
|
-
dbBytes += statSync(sidecar).size;
|
|
822
|
-
}
|
|
823
|
-
catch { /* no sidecar */ }
|
|
824
|
-
}
|
|
825
|
-
return c.json({
|
|
826
|
-
dbPath: deps.dbPath,
|
|
827
|
-
dbBytes,
|
|
828
|
-
dbPresent,
|
|
829
|
-
schemaVersion: migrations.length > 0 ? migrations[migrations.length - 1].version : null,
|
|
830
|
-
migrations,
|
|
831
|
-
tables,
|
|
832
|
-
runtimes,
|
|
833
|
-
});
|
|
873
|
+
const stats = deps.liveStore instanceof PgSessionStore
|
|
874
|
+
? await pgDataStats(deps.liveStore.pool)
|
|
875
|
+
: sqliteDataStats(deps.liveStore.rawDb(), deps.dbPath);
|
|
876
|
+
return c.json(stats);
|
|
834
877
|
});
|
|
835
878
|
app.get("/api/data/backup", (c) => {
|
|
836
879
|
const adminToken = process.env["NLM_MCP_TOKEN"];
|
|
@@ -846,9 +889,16 @@ function registerDataManagementRoutes(app, deps) {
|
|
|
846
889
|
if (!deps.liveStore || !deps.dbPath) {
|
|
847
890
|
return c.json({ error: "backup requires liveStore + dbPath" }, 503);
|
|
848
891
|
}
|
|
892
|
+
// VACUUM INTO is a SQLite-only mechanism. PostgreSQL backups are delegated
|
|
893
|
+
// to the database layer (pg_dump / managed-PG snapshots / WAL archiving),
|
|
894
|
+
// not produced in-process by the daemon.
|
|
895
|
+
if (deps.liveStore instanceof PgSessionStore) {
|
|
896
|
+
return c.json({
|
|
897
|
+
error: "daemon backup is SQLite-only; for PostgreSQL use pg_dump or your managed-PG snapshot facility",
|
|
898
|
+
}, 501);
|
|
899
|
+
}
|
|
849
900
|
const scratch = snapshotScratchPath(deps.dbPath);
|
|
850
901
|
try {
|
|
851
|
-
// TODO(#215a): replace rawDb() with port methods; cast until then
|
|
852
902
|
vacuumSnapshot(deps.liveStore.rawDb(), scratch);
|
|
853
903
|
const bytes = readFileSync(scratch);
|
|
854
904
|
const stamp = new Date().toISOString().slice(0, 10);
|
|
@@ -876,6 +926,13 @@ function registerDataManagementRoutes(app, deps) {
|
|
|
876
926
|
}
|
|
877
927
|
if (!deps.dbPath)
|
|
878
928
|
return c.json({ error: "restore requires dbPath" }, 503);
|
|
929
|
+
// Staged restore swaps the SQLite file on restart — SQLite-only. PG restores
|
|
930
|
+
// go through pg_restore / managed-PG point-in-time recovery.
|
|
931
|
+
if (deps.liveStore instanceof PgSessionStore) {
|
|
932
|
+
return c.json({
|
|
933
|
+
error: "daemon restore is SQLite-only; for PostgreSQL use pg_restore or your managed-PG recovery facility",
|
|
934
|
+
}, 501);
|
|
935
|
+
}
|
|
879
936
|
const form = await c.req.parseBody().catch(() => null);
|
|
880
937
|
const file = form?.["file"];
|
|
881
938
|
if (!(file instanceof File)) {
|
|
@@ -913,8 +970,10 @@ function registerActionRoutes(app, deps) {
|
|
|
913
970
|
const parsed = parseActionInput(body);
|
|
914
971
|
if (!parsed)
|
|
915
972
|
return c.json({ error: "invalid action payload" }, 400);
|
|
916
|
-
|
|
917
|
-
const id =
|
|
973
|
+
const store = deps.liveStore;
|
|
974
|
+
const id = store instanceof PgSessionStore
|
|
975
|
+
? await writeActionPg(store.pool, parsed)
|
|
976
|
+
: writeAction(store.rawDb(), parsed);
|
|
918
977
|
return c.json({ id, timestamp: new Date().toISOString() });
|
|
919
978
|
});
|
|
920
979
|
app.post("/api/action/batch", async (c) => {
|
|
@@ -928,39 +987,46 @@ function registerActionRoutes(app, deps) {
|
|
|
928
987
|
.filter((x) => x !== null);
|
|
929
988
|
if (inputs.length === 0)
|
|
930
989
|
return c.json({ accepted: 0, ids: [] });
|
|
931
|
-
|
|
932
|
-
const ids =
|
|
990
|
+
const store = deps.liveStore;
|
|
991
|
+
const ids = store instanceof PgSessionStore
|
|
992
|
+
? await writeActionsBatchPg(store.pool, inputs)
|
|
993
|
+
: writeActionsBatch(store.rawDb(), inputs);
|
|
933
994
|
return c.json({ accepted: ids.length, ids });
|
|
934
995
|
});
|
|
935
|
-
app.post("/api/action/:id/undo", (c) => {
|
|
996
|
+
app.post("/api/action/:id/undo", async (c) => {
|
|
936
997
|
if (!deps.liveStore)
|
|
937
998
|
return c.json({ error: "actions require liveStore" }, 503);
|
|
938
|
-
|
|
939
|
-
const result =
|
|
999
|
+
const store = deps.liveStore;
|
|
1000
|
+
const result = store instanceof PgSessionStore
|
|
1001
|
+
? await undoActionPg(store.pool, c.req.param("id"))
|
|
1002
|
+
: undoAction(store.rawDb(), c.req.param("id"));
|
|
940
1003
|
if (!result)
|
|
941
1004
|
return c.json({ error: "action not found or already undone" }, 404);
|
|
942
1005
|
return c.json({ id: result.undoId, timestamp: new Date().toISOString() });
|
|
943
1006
|
});
|
|
944
|
-
app.get("/api/actions", (c) => {
|
|
1007
|
+
app.get("/api/actions", async (c) => {
|
|
945
1008
|
if (!deps.liveStore)
|
|
946
1009
|
return c.json({ actions: [] });
|
|
947
1010
|
const limitRaw = c.req.query("limit");
|
|
948
1011
|
const subjectId = c.req.query("subject_id");
|
|
949
1012
|
const kind = c.req.query("kind");
|
|
950
1013
|
const limit = limitRaw ? Math.max(1, Math.min(500, Number.parseInt(limitRaw, 10))) : 100;
|
|
951
|
-
|
|
952
|
-
const rows = listActions(deps.liveStore.rawDb(), {
|
|
1014
|
+
const opts = {
|
|
953
1015
|
limit,
|
|
954
1016
|
...(subjectId ? { subjectId } : {}),
|
|
955
1017
|
...(kind ? { kind } : {}),
|
|
956
|
-
}
|
|
1018
|
+
};
|
|
1019
|
+
const store = deps.liveStore;
|
|
1020
|
+
const rows = store instanceof PgSessionStore
|
|
1021
|
+
? await listActionsPg(store.pool, opts)
|
|
1022
|
+
: listActions(store.rawDb(), opts);
|
|
957
1023
|
return c.json({ actions: rows });
|
|
958
1024
|
});
|
|
959
1025
|
}
|
|
960
1026
|
function registerClassifierRoutes(app, deps) {
|
|
961
1027
|
app.get("/api/classifier/info", (c) => {
|
|
962
1028
|
const provider = deps.classifier?.provider ?? "ollama";
|
|
963
|
-
const model = deps.classifier?.model ?? "qwen3:4b
|
|
1029
|
+
const model = deps.classifier?.model ?? "qwen3.5:4b";
|
|
964
1030
|
return c.json({
|
|
965
1031
|
provider,
|
|
966
1032
|
model,
|
|
@@ -971,7 +1037,7 @@ function registerClassifierRoutes(app, deps) {
|
|
|
971
1037
|
},
|
|
972
1038
|
default_models: {
|
|
973
1039
|
deepseek: ["deepseek-v4-flash", "deepseek-v4-pro", "deepseek-chat"],
|
|
974
|
-
ollama: ["qwen3:4b-instruct-2507-q4_K_M", "qwen2.5:3b-instruct", "llama3.2:3b", "mistral:7b"],
|
|
1040
|
+
ollama: ["qwen3.5:4b", "qwen3:4b-instruct-2507-q4_K_M", "qwen2.5:3b-instruct", "llama3.2:3b", "mistral:7b"],
|
|
975
1041
|
},
|
|
976
1042
|
embedder: deps.embedderInfo ?? { provider: "ollama", model: "nomic-embed-text", dims: 768 },
|
|
977
1043
|
});
|
|
@@ -999,10 +1065,10 @@ function registerClassifierRoutes(app, deps) {
|
|
|
999
1065
|
// Each row = one transcript origin the daemon scans. UI uses these
|
|
1000
1066
|
// endpoints to surface existing sources + let users add custom ones.
|
|
1001
1067
|
function registerSourceRoutes(app, deps) {
|
|
1002
|
-
app.get("/api/sources", (c) => {
|
|
1068
|
+
app.get("/api/sources", async (c) => {
|
|
1003
1069
|
if (!deps.sources)
|
|
1004
1070
|
return c.json({ sources: [] });
|
|
1005
|
-
return c.json({ sources: deps.sources.list() });
|
|
1071
|
+
return c.json({ sources: await deps.sources.list() });
|
|
1006
1072
|
});
|
|
1007
1073
|
app.post("/api/sources", async (c) => {
|
|
1008
1074
|
if (!deps.sources)
|
|
@@ -1011,11 +1077,10 @@ function registerSourceRoutes(app, deps) {
|
|
|
1011
1077
|
const parsed = parseSourceInsert(body);
|
|
1012
1078
|
if (!parsed)
|
|
1013
1079
|
return c.json({ error: "invalid source payload" }, 400);
|
|
1014
|
-
|
|
1015
|
-
if (deps.sources.getByName(parsed.name)) {
|
|
1080
|
+
if (await deps.sources.getByName(parsed.name)) {
|
|
1016
1081
|
return c.json({ error: `source named '${parsed.name}' already exists` }, 409);
|
|
1017
1082
|
}
|
|
1018
|
-
return c.json(deps.sources.insert(parsed), 201);
|
|
1083
|
+
return c.json(await deps.sources.insert(parsed), 201);
|
|
1019
1084
|
});
|
|
1020
1085
|
app.patch("/api/sources/:id", async (c) => {
|
|
1021
1086
|
if (!deps.sources)
|
|
@@ -1027,30 +1092,29 @@ function registerSourceRoutes(app, deps) {
|
|
|
1027
1092
|
const patch = parseSourceUpdate(body);
|
|
1028
1093
|
if (!patch)
|
|
1029
1094
|
return c.json({ error: "invalid patch payload" }, 400);
|
|
1030
|
-
const updated = deps.sources.update(id, patch);
|
|
1095
|
+
const updated = await deps.sources.update(id, patch);
|
|
1031
1096
|
if (!updated)
|
|
1032
1097
|
return c.json({ error: `source ${id} not found` }, 404);
|
|
1033
1098
|
return c.json(updated);
|
|
1034
1099
|
});
|
|
1035
|
-
app.delete("/api/sources/:id", (c) => {
|
|
1100
|
+
app.delete("/api/sources/:id", async (c) => {
|
|
1036
1101
|
if (!deps.sources)
|
|
1037
1102
|
return c.json({ error: "sources registry unavailable" }, 503);
|
|
1038
1103
|
const id = Number.parseInt(c.req.param("id"), 10);
|
|
1039
1104
|
if (!Number.isFinite(id))
|
|
1040
1105
|
return c.json({ error: "invalid id" }, 400);
|
|
1041
|
-
const ok = deps.sources.delete(id);
|
|
1106
|
+
const ok = await deps.sources.delete(id);
|
|
1042
1107
|
if (!ok)
|
|
1043
1108
|
return c.json({ error: `source ${id} not found` }, 404);
|
|
1044
1109
|
return c.json({ deleted: id });
|
|
1045
1110
|
});
|
|
1046
|
-
app.post("/api/sources/:id/regenerate-token", (c) => {
|
|
1111
|
+
app.post("/api/sources/:id/regenerate-token", async (c) => {
|
|
1047
1112
|
if (!deps.sources)
|
|
1048
1113
|
return c.json({ error: "sources registry unavailable" }, 503);
|
|
1049
1114
|
const id = Number.parseInt(c.req.param("id"), 10);
|
|
1050
1115
|
if (!Number.isFinite(id))
|
|
1051
1116
|
return c.json({ error: "invalid id" }, 400);
|
|
1052
|
-
|
|
1053
|
-
const token = deps.sources.regenerateToken(id);
|
|
1117
|
+
const token = await deps.sources.regenerateToken(id);
|
|
1054
1118
|
if (!token)
|
|
1055
1119
|
return c.json({ error: "regenerate-token only applies to webhook sources" }, 400);
|
|
1056
1120
|
return c.json({ token });
|
|
@@ -1067,8 +1131,7 @@ function registerIngestRoute(app, deps) {
|
|
|
1067
1131
|
const match = /^Bearer\s+(\S+)$/i.exec(auth);
|
|
1068
1132
|
if (!match || !match[1])
|
|
1069
1133
|
return c.json({ error: "missing or malformed bearer token" }, 401);
|
|
1070
|
-
|
|
1071
|
-
const source = deps.sources.findByToken(match[1]);
|
|
1134
|
+
const source = await deps.sources.findByToken(match[1]);
|
|
1072
1135
|
if (!source || source.kind !== "webhook")
|
|
1073
1136
|
return c.json({ error: "invalid token" }, 401);
|
|
1074
1137
|
if (!source.enabled)
|
|
@@ -1103,10 +1166,10 @@ function registerIngestRoute(app, deps) {
|
|
|
1103
1166
|
// Each row = one LLM endpoint. Keys are redacted on every response
|
|
1104
1167
|
// (rows carry hasApiKey:boolean instead).
|
|
1105
1168
|
function registerProviderRoutes(app, deps) {
|
|
1106
|
-
app.get("/api/providers", (c) => {
|
|
1169
|
+
app.get("/api/providers", async (c) => {
|
|
1107
1170
|
if (!deps.providers)
|
|
1108
1171
|
return c.json({ providers: [] });
|
|
1109
|
-
return c.json({ providers: deps.providers.list() });
|
|
1172
|
+
return c.json({ providers: await deps.providers.list() });
|
|
1110
1173
|
});
|
|
1111
1174
|
app.post("/api/providers", async (c) => {
|
|
1112
1175
|
if (!deps.providers)
|
|
@@ -1115,11 +1178,10 @@ function registerProviderRoutes(app, deps) {
|
|
|
1115
1178
|
const parsed = parseProviderInsert(body);
|
|
1116
1179
|
if (!parsed)
|
|
1117
1180
|
return c.json({ error: "invalid provider payload" }, 400);
|
|
1118
|
-
|
|
1119
|
-
if (deps.providers.getByName(parsed.name)) {
|
|
1181
|
+
if (await deps.providers.getByName(parsed.name)) {
|
|
1120
1182
|
return c.json({ error: `provider named '${parsed.name}' already exists` }, 409);
|
|
1121
1183
|
}
|
|
1122
|
-
return c.json(deps.providers.insert(parsed), 201);
|
|
1184
|
+
return c.json(await deps.providers.insert(parsed), 201);
|
|
1123
1185
|
});
|
|
1124
1186
|
app.patch("/api/providers/:id", async (c) => {
|
|
1125
1187
|
if (!deps.providers)
|
|
@@ -1131,18 +1193,18 @@ function registerProviderRoutes(app, deps) {
|
|
|
1131
1193
|
const patch = parseProviderUpdate(body);
|
|
1132
1194
|
if (!patch)
|
|
1133
1195
|
return c.json({ error: "invalid patch payload" }, 400);
|
|
1134
|
-
const updated = deps.providers.update(id, patch);
|
|
1196
|
+
const updated = await deps.providers.update(id, patch);
|
|
1135
1197
|
if (!updated)
|
|
1136
1198
|
return c.json({ error: `provider ${id} not found` }, 404);
|
|
1137
1199
|
return c.json(updated);
|
|
1138
1200
|
});
|
|
1139
|
-
app.delete("/api/providers/:id", (c) => {
|
|
1201
|
+
app.delete("/api/providers/:id", async (c) => {
|
|
1140
1202
|
if (!deps.providers)
|
|
1141
1203
|
return c.json({ error: "providers registry unavailable" }, 503);
|
|
1142
1204
|
const id = Number.parseInt(c.req.param("id"), 10);
|
|
1143
1205
|
if (!Number.isFinite(id))
|
|
1144
1206
|
return c.json({ error: "invalid id" }, 400);
|
|
1145
|
-
const ok = deps.providers.delete(id);
|
|
1207
|
+
const ok = await deps.providers.delete(id);
|
|
1146
1208
|
if (!ok)
|
|
1147
1209
|
return c.json({ error: `provider ${id} not found` }, 404);
|
|
1148
1210
|
return c.json({ deleted: id });
|
|
@@ -1153,11 +1215,10 @@ function registerProviderRoutes(app, deps) {
|
|
|
1153
1215
|
const id = Number.parseInt(c.req.param("id"), 10);
|
|
1154
1216
|
if (!Number.isFinite(id))
|
|
1155
1217
|
return c.json({ error: "invalid id" }, 400);
|
|
1156
|
-
|
|
1157
|
-
const provider = deps.providers.get(id);
|
|
1218
|
+
const provider = await deps.providers.get(id);
|
|
1158
1219
|
if (!provider)
|
|
1159
1220
|
return c.json({ error: `provider ${id} not found` }, 404);
|
|
1160
|
-
const key = deps.providers.getSecret(id);
|
|
1221
|
+
const key = await deps.providers.getSecret(id);
|
|
1161
1222
|
try {
|
|
1162
1223
|
const models = await listModels(provider, { apiKey: key });
|
|
1163
1224
|
return c.json({ models });
|
|
@@ -1173,11 +1234,10 @@ function registerProviderRoutes(app, deps) {
|
|
|
1173
1234
|
const id = Number.parseInt(c.req.param("id"), 10);
|
|
1174
1235
|
if (!Number.isFinite(id))
|
|
1175
1236
|
return c.json({ error: "invalid id" }, 400);
|
|
1176
|
-
|
|
1177
|
-
const provider = deps.providers.get(id);
|
|
1237
|
+
const provider = await deps.providers.get(id);
|
|
1178
1238
|
if (!provider)
|
|
1179
1239
|
return c.json({ error: `provider ${id} not found` }, 404);
|
|
1180
|
-
const key = deps.providers.getSecret(id);
|
|
1240
|
+
const key = await deps.providers.getSecret(id);
|
|
1181
1241
|
const startedAt = Date.now();
|
|
1182
1242
|
try {
|
|
1183
1243
|
const models = await listModels(provider, { apiKey: key });
|
|
@@ -1412,6 +1472,28 @@ function registerSignalRoutes(app, deps) {
|
|
|
1412
1472
|
catch {
|
|
1413
1473
|
return c.json({ error: "signal insert failed" }, 500);
|
|
1414
1474
|
}
|
|
1475
|
+
// Best-effort code-exemplar capture: a code-bearing signal (git_sha or
|
|
1476
|
+
// detail.code) becomes a retrievable exemplar. Gated by the flag and
|
|
1477
|
+
// never allowed to fail the signal write — extractExemplar returns null
|
|
1478
|
+
// when there's no code, and embedding is fire-and-forget.
|
|
1479
|
+
if (process.env["NLM_CODE_EXEMPLARS_ENABLED"] === "1" &&
|
|
1480
|
+
deps.exemplarStore &&
|
|
1481
|
+
deps.installScope !== undefined) {
|
|
1482
|
+
const exemplarStore = deps.exemplarStore;
|
|
1483
|
+
try {
|
|
1484
|
+
const exemplar = extractExemplar(signal, { installScope: deps.installScope });
|
|
1485
|
+
if (exemplar) {
|
|
1486
|
+
const { id, skipped } = await exemplarStore.insert(exemplar);
|
|
1487
|
+
if (!skipped && deps.codeEmbedder) {
|
|
1488
|
+
void deps.codeEmbedder
|
|
1489
|
+
.embed(exemplar.taskContext + "\n" + exemplar.code, "document")
|
|
1490
|
+
.then((r) => exemplarStore.upsertEmbedding(id, r.vector))
|
|
1491
|
+
.catch(() => { });
|
|
1492
|
+
}
|
|
1493
|
+
}
|
|
1494
|
+
}
|
|
1495
|
+
catch { /* capture is best-effort; never blocks signal ingest */ }
|
|
1496
|
+
}
|
|
1415
1497
|
return c.json({ id: signal.id, status: "accepted" }, 202);
|
|
1416
1498
|
});
|
|
1417
1499
|
app.get("/api/signals/failure-modes", async (c) => {
|
|
@@ -1439,6 +1521,67 @@ function registerSignalRoutes(app, deps) {
|
|
|
1439
1521
|
const modes = aggregateFailureModes(rows);
|
|
1440
1522
|
return c.json({ days, total: rows.length, modes });
|
|
1441
1523
|
});
|
|
1524
|
+
// ── Code exemplar routes (NLM_CODE_EXEMPLARS_ENABLED=1) ───────────────────
|
|
1525
|
+
app.post("/api/exemplar", async (c) => {
|
|
1526
|
+
if (!deps.exemplarStore || deps.installScope === undefined) {
|
|
1527
|
+
return c.json({ error: "exemplar store not wired in this deployment" }, 503);
|
|
1528
|
+
}
|
|
1529
|
+
if (process.env["NLM_CODE_EXEMPLARS_ENABLED"] !== "1") {
|
|
1530
|
+
return c.json({ error: "code exemplars disabled" }, 403);
|
|
1531
|
+
}
|
|
1532
|
+
let body;
|
|
1533
|
+
try {
|
|
1534
|
+
body = await c.req.json();
|
|
1535
|
+
}
|
|
1536
|
+
catch {
|
|
1537
|
+
return c.json({ error: "body must be JSON" }, 400);
|
|
1538
|
+
}
|
|
1539
|
+
if (!body || typeof body !== "object" || Array.isArray(body)) {
|
|
1540
|
+
return c.json({ error: "body must be a JSON object" }, 400);
|
|
1541
|
+
}
|
|
1542
|
+
let inp;
|
|
1543
|
+
try {
|
|
1544
|
+
inp = normalizeExemplar({ ...body, installScope: deps.installScope });
|
|
1545
|
+
}
|
|
1546
|
+
catch (e) {
|
|
1547
|
+
return c.json({ error: e instanceof Error ? e.message : "invalid exemplar" }, 400);
|
|
1548
|
+
}
|
|
1549
|
+
const { id, skipped } = await deps.exemplarStore.insert(inp);
|
|
1550
|
+
if (!skipped && deps.codeEmbedder) {
|
|
1551
|
+
// Best-effort embedding: fire-and-forget, never blocks the response.
|
|
1552
|
+
deps.codeEmbedder.embed(inp.taskContext + "\n" + inp.code, "document")
|
|
1553
|
+
.then((r) => deps.exemplarStore.upsertEmbedding(id, r.vector))
|
|
1554
|
+
.catch(() => { });
|
|
1555
|
+
}
|
|
1556
|
+
return c.json({ id, skipped, status: "accepted" }, 202);
|
|
1557
|
+
});
|
|
1558
|
+
app.get("/api/recall-code", async (c) => {
|
|
1559
|
+
if (!deps.exemplarStore || deps.installScope === undefined) {
|
|
1560
|
+
return c.json({ error: "exemplar store not wired in this deployment" }, 503);
|
|
1561
|
+
}
|
|
1562
|
+
if (process.env["NLM_CODE_EXEMPLARS_ENABLED"] !== "1") {
|
|
1563
|
+
return c.json({ error: "code exemplars disabled" }, 403);
|
|
1564
|
+
}
|
|
1565
|
+
const query = c.req.query("q");
|
|
1566
|
+
if (!query)
|
|
1567
|
+
return c.json({ error: "q (query) is required" }, 400);
|
|
1568
|
+
const kStr = c.req.query("k") ?? "5";
|
|
1569
|
+
const k = Math.max(1, Math.min(20, Number.parseInt(kStr, 10) || 5));
|
|
1570
|
+
const repo = c.req.query("repo");
|
|
1571
|
+
const lang = c.req.query("lang");
|
|
1572
|
+
const model = c.req.query("model");
|
|
1573
|
+
const includeNegatives = c.req.query("negatives") !== "0";
|
|
1574
|
+
const result = await recallCode({
|
|
1575
|
+
query,
|
|
1576
|
+
installScope: deps.installScope,
|
|
1577
|
+
...(repo ? { repo } : {}),
|
|
1578
|
+
...(lang ? { lang } : {}),
|
|
1579
|
+
...(model ? { model } : {}),
|
|
1580
|
+
includeNegatives,
|
|
1581
|
+
k,
|
|
1582
|
+
}, deps.exemplarStore, deps.codeEmbedder ?? null, null);
|
|
1583
|
+
return c.json(result);
|
|
1584
|
+
});
|
|
1442
1585
|
}
|
|
1443
1586
|
function mountSpa(app, dist) {
|
|
1444
1587
|
const indexHtml = join(dist, "index.html");
|