@strvmarv/total-recall 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -223,12 +223,23 @@ import * as ort from "onnxruntime-node";
223
223
 
224
224
  // src/embedding/model-manager.ts
225
225
  import { existsSync as existsSync3, mkdirSync as mkdirSync2, readdirSync } from "fs";
226
+ import { readFileSync as readFileSync2, statSync } from "fs";
226
227
  import { writeFile } from "fs/promises";
227
228
  import { join as join3 } from "path";
228
229
  var HF_BASE_URL = "https://huggingface.co";
229
- function getModelPath(modelName) {
230
+ var HF_REVISION = "main";
231
+ function getBundledModelPath(modelName) {
232
+ const distDir = new URL(".", import.meta.url).pathname;
233
+ return join3(distDir, "..", "models", modelName);
234
+ }
235
+ function getUserModelPath(modelName) {
230
236
  return join3(getDataDir(), "models", modelName);
231
237
  }
238
+ function getModelPath(modelName) {
239
+ const bundled = getBundledModelPath(modelName);
240
+ if (isModelDownloaded(bundled)) return bundled;
241
+ return getUserModelPath(modelName);
242
+ }
232
243
  function isModelDownloaded(modelPath) {
233
244
  if (!existsSync3(modelPath)) return false;
234
245
  try {
@@ -238,13 +249,36 @@ function isModelDownloaded(modelPath) {
238
249
  return false;
239
250
  }
240
251
  }
252
+ async function validateDownload(modelPath) {
253
+ const modelStat = statSync(join3(modelPath, "model.onnx"));
254
+ if (modelStat.size < 1e6) {
255
+ throw new Error("model.onnx appears corrupted (< 1MB)");
256
+ }
257
+ const tokenizerText = readFileSync2(join3(modelPath, "tokenizer.json"), "utf-8");
258
+ try {
259
+ JSON.parse(tokenizerText);
260
+ } catch {
261
+ throw new Error("tokenizer.json is not valid JSON");
262
+ }
263
+ }
241
264
  async function downloadModel(modelName) {
242
- const modelPath = getModelPath(modelName);
265
+ const modelPath = getUserModelPath(modelName);
243
266
  mkdirSync2(modelPath, { recursive: true });
244
- const files = ["model.onnx", "tokenizer.json", "tokenizer_config.json"];
245
- const repoUrl = `${HF_BASE_URL}/${modelName}/resolve/main`;
246
- for (const file of files) {
247
- const url = `${repoUrl}/${file}`;
267
+ const fileUrls = [
268
+ {
269
+ file: "model.onnx",
270
+ url: `${HF_BASE_URL}/sentence-transformers/${modelName}/resolve/${HF_REVISION}/onnx/model.onnx`
271
+ },
272
+ {
273
+ file: "tokenizer.json",
274
+ url: `${HF_BASE_URL}/sentence-transformers/${modelName}/resolve/${HF_REVISION}/tokenizer.json`
275
+ },
276
+ {
277
+ file: "tokenizer_config.json",
278
+ url: `${HF_BASE_URL}/sentence-transformers/${modelName}/resolve/${HF_REVISION}/tokenizer_config.json`
279
+ }
280
+ ];
281
+ for (const { file, url } of fileUrls) {
248
282
  const dest = join3(modelPath, file);
249
283
  const response = await fetch(url);
250
284
  if (!response.ok) {
@@ -253,8 +287,12 @@ async function downloadModel(modelName) {
253
287
  );
254
288
  }
255
289
  const buffer = await response.arrayBuffer();
290
+ if (buffer.byteLength === 0) {
291
+ throw new Error(`Downloaded ${file} is empty`);
292
+ }
256
293
  await writeFile(dest, Buffer.from(buffer));
257
294
  }
295
+ await validateDownload(modelPath);
258
296
  return modelPath;
259
297
  }
260
298
 
@@ -263,7 +301,7 @@ var CLS_TOKEN_ID = 101;
263
301
  var SEP_TOKEN_ID = 102;
264
302
  var UNK_TOKEN_ID = 100;
265
303
  var MAX_SEQ_LEN = 512;
266
- var Embedder = class _Embedder {
304
+ var Embedder = class {
267
305
  options;
268
306
  session = null;
269
307
  vocab = null;
@@ -351,39 +389,6 @@ var Embedder = class _Embedder {
351
389
  }
352
390
  return results;
353
391
  }
354
- /**
355
- * Returns a synchronous embed function suitable for use with ingestion APIs.
356
- * Uses a hash-based deterministic embedding that does not require ONNX inference,
357
- * allowing synchronous operation. Call ensureLoaded() before using this.
358
- */
359
- makeSyncEmbedFn() {
360
- const dimensions = this.options.dimensions;
361
- return (text) => {
362
- return _Embedder.hashEmbed(text, dimensions);
363
- };
364
- }
365
- /**
366
- * Hash-based deterministic embedding. Does not require the ONNX model.
367
- * Useful as a sync fallback for ingestion pipelines.
368
- */
369
- static hashEmbed(text, dimensions) {
370
- const vec = new Float32Array(dimensions);
371
- let hash = 0;
372
- for (let i = 0; i < text.length; i++) {
373
- hash = hash * 31 + text.charCodeAt(i) | 0;
374
- }
375
- for (let i = 0; i < dimensions; i++) {
376
- hash = hash * 1103515245 + 12345 | 0;
377
- vec[i] = (hash >> 16 & 32767) / 32767 - 0.5;
378
- }
379
- let norm = 0;
380
- for (let i = 0; i < dimensions; i++) norm += vec[i] * vec[i];
381
- norm = Math.sqrt(norm);
382
- if (norm > 0) {
383
- for (let i = 0; i < dimensions; i++) vec[i] = vec[i] / norm;
384
- }
385
- return vec;
386
- }
387
392
  /**
388
393
  * Deterministic embedding based on tokenization only (no ONNX inference).
389
394
  * Used as fallback when async embed cannot be awaited synchronously.
@@ -513,9 +518,23 @@ function deleteEntry(db, tier, type, id) {
513
518
  const table = tableName(tier, type);
514
519
  db.prepare(`DELETE FROM ${table} WHERE id = ?`).run(id);
515
520
  }
521
+ var ALLOWED_ORDER_COLUMNS = /* @__PURE__ */ new Set([
522
+ "created_at",
523
+ "updated_at",
524
+ "last_accessed_at",
525
+ "access_count",
526
+ "decay_score",
527
+ "content"
528
+ ]);
516
529
  function listEntries(db, tier, type, opts) {
517
530
  const table = tableName(tier, type);
518
- const orderBy = opts?.orderBy ?? "created_at DESC";
531
+ const orderParts = (opts?.orderBy ?? "created_at DESC").split(" ");
532
+ const column = orderParts[0];
533
+ const direction = orderParts[1]?.toUpperCase() === "ASC" ? "ASC" : "DESC";
534
+ if (!ALLOWED_ORDER_COLUMNS.has(column)) {
535
+ throw new Error(`Invalid orderBy column: ${column}`);
536
+ }
537
+ const orderBy = `${column} ${direction}`;
519
538
  let sql;
520
539
  let params;
521
540
  if (opts?.project !== void 0 && opts.project !== null) {
@@ -621,7 +640,7 @@ function searchByVector(db, tier, type, queryVec, opts) {
621
640
  }
622
641
 
623
642
  // src/memory/store.ts
624
- function storeMemory(db, embed, opts) {
643
+ async function storeMemory(db, embed, opts) {
625
644
  const tier = opts.tier ?? "hot";
626
645
  const contentType = opts.contentType ?? "memory";
627
646
  const id = insertEntry(db, tier, contentType, {
@@ -634,14 +653,14 @@ function storeMemory(db, embed, opts) {
634
653
  collection_id: opts.collection_id,
635
654
  metadata: opts.type ? { entry_type: opts.type } : {}
636
655
  });
637
- const embedding = embed(opts.content);
656
+ const embedding = await embed(opts.content);
638
657
  insertEmbedding(db, tier, contentType, id, embedding);
639
658
  return id;
640
659
  }
641
660
 
642
661
  // src/memory/search.ts
643
- function searchMemory(db, embed, query, opts) {
644
- const queryVec = embed(query);
662
+ async function searchMemory(db, embed, query, opts) {
663
+ const queryVec = await embed(query);
645
664
  const merged = [];
646
665
  for (const { tier, content_type } of opts.tiers) {
647
666
  const vectorResults = searchByVector(db, tier, content_type, queryVec, {
@@ -681,14 +700,14 @@ function getMemory(db, id) {
681
700
  }
682
701
 
683
702
  // src/memory/update.ts
684
- function updateMemory(db, embed, id, opts) {
703
+ async function updateMemory(db, embed, id, opts) {
685
704
  const location = getMemory(db, id);
686
705
  if (!location) return false;
687
706
  const { tier, content_type } = location;
688
707
  updateEntry(db, tier, content_type, id, opts);
689
- if (opts.content !== void 0) {
708
+ if (opts.content !== void 0 && embed !== void 0) {
690
709
  deleteEmbedding(db, tier, content_type, id);
691
- const newEmbedding = embed(opts.content);
710
+ const newEmbedding = await embed(opts.content);
692
711
  insertEmbedding(db, tier, content_type, id, newEmbedding);
693
712
  }
694
713
  return true;
@@ -698,23 +717,103 @@ function updateMemory(db, embed, id, opts) {
698
717
  function deleteMemory(db, id) {
699
718
  const location = getMemory(db, id);
700
719
  if (!location) return false;
720
+ deleteEmbedding(db, location.tier, location.content_type, id);
701
721
  deleteEntry(db, location.tier, location.content_type, id);
702
722
  return true;
703
723
  }
704
724
 
705
725
  // src/memory/promote-demote.ts
706
- function promoteEntry(db, embed, id, fromTier, fromType, toTier, toType) {
726
+ async function promoteEntry(db, embed, id, fromTier, fromType, toTier, toType) {
707
727
  const entry = getEntry(db, fromTier, fromType, id);
708
728
  if (!entry) {
709
729
  throw new Error(`Entry ${id} not found in ${fromTier}/${fromType}`);
710
730
  }
711
731
  deleteEmbedding(db, fromTier, fromType, id);
712
732
  moveEntry(db, fromTier, fromType, toTier, toType, id);
713
- const newEmbedding = embed(entry.content);
733
+ const newEmbedding = await embed(entry.content);
714
734
  insertEmbedding(db, toTier, toType, id, newEmbedding);
715
735
  }
716
- function demoteEntry(db, embed, id, fromTier, fromType, toTier, toType) {
717
- promoteEntry(db, embed, id, fromTier, fromType, toTier, toType);
736
+ async function demoteEntry(db, embed, id, fromTier, fromType, toTier, toType) {
737
+ await promoteEntry(db, embed, id, fromTier, fromType, toTier, toType);
738
+ }
739
+
740
+ // src/tools/validation.ts
741
+ import { resolve } from "path";
742
+ var VALID_TIERS = /* @__PURE__ */ new Set(["hot", "warm", "cold"]);
743
+ var VALID_CONTENT_TYPES = /* @__PURE__ */ new Set(["memory", "knowledge"]);
744
+ var VALID_ENTRY_TYPES = /* @__PURE__ */ new Set(["correction", "preference", "decision", "surfaced"]);
745
+ var MAX_CONTENT_LENGTH = 1e5;
746
+ function validateString(value, name) {
747
+ if (typeof value !== "string" || value.length === 0) {
748
+ throw new Error(`${name} must be a non-empty string`);
749
+ }
750
+ return value;
751
+ }
752
+ function validateOptionalString(value, name) {
753
+ if (value === void 0 || value === null) return void 0;
754
+ if (typeof value !== "string") throw new Error(`${name} must be a string`);
755
+ return value;
756
+ }
757
+ function validateTier(value) {
758
+ if (!VALID_TIERS.has(value)) {
759
+ throw new Error(`Invalid tier: ${String(value)}. Must be hot, warm, or cold`);
760
+ }
761
+ return value;
762
+ }
763
+ function validateContentType(value) {
764
+ if (!VALID_CONTENT_TYPES.has(value)) {
765
+ throw new Error(`Invalid content type: ${String(value)}. Must be memory or knowledge`);
766
+ }
767
+ return value;
768
+ }
769
+ function validateEntryType(value) {
770
+ if (value === void 0 || value === null) return void 0;
771
+ if (!VALID_ENTRY_TYPES.has(value)) {
772
+ throw new Error(`Invalid entry type: ${String(value)}`);
773
+ }
774
+ return value;
775
+ }
776
+ function validateContent(value) {
777
+ const content = validateString(value, "content");
778
+ if (content.length > MAX_CONTENT_LENGTH) {
779
+ throw new Error(`Content exceeds maximum length of ${MAX_CONTENT_LENGTH} characters`);
780
+ }
781
+ return content;
782
+ }
783
+ function validateNumber(value, name, min, max) {
784
+ if (typeof value !== "number" || isNaN(value)) {
785
+ throw new Error(`${name} must be a number`);
786
+ }
787
+ if (min !== void 0 && value < min) throw new Error(`${name} must be >= ${min}`);
788
+ if (max !== void 0 && value > max) throw new Error(`${name} must be <= ${max}`);
789
+ return value;
790
+ }
791
+ function validateOptionalNumber(value, name, min, max) {
792
+ if (value === void 0 || value === null) return void 0;
793
+ return validateNumber(value, name, min, max);
794
+ }
795
+ function validateTags(value) {
796
+ if (value === void 0 || value === null) return [];
797
+ if (!Array.isArray(value)) throw new Error("tags must be an array");
798
+ return value.map((v, i) => {
799
+ if (typeof v !== "string") throw new Error(`tags[${i}] must be a string`);
800
+ return v;
801
+ });
802
+ }
803
+ function validatePath(value, name) {
804
+ const path = validateString(value, name);
805
+ const resolved = resolve(path);
806
+ const dangerous = ["/etc", "/proc", "/sys", "/dev", "/var/run", "/root"];
807
+ for (const prefix of dangerous) {
808
+ if (resolved.startsWith(prefix)) {
809
+ throw new Error(`Access denied: ${name} cannot access ${prefix}`);
810
+ }
811
+ }
812
+ const basename2 = resolved.split("/").pop() ?? "";
813
+ if (basename2 === ".env" || basename2 === ".credentials.json") {
814
+ throw new Error(`Access denied: ${name} cannot access sensitive files`);
815
+ }
816
+ return resolved;
718
817
  }
719
818
 
720
819
  // src/tools/memory-tools.ts
@@ -829,23 +928,31 @@ var MEMORY_TOOLS = [
829
928
  ];
830
929
  async function handleMemoryTool(name, args, ctx) {
831
930
  if (name === "memory_store") {
832
- const content = args.content;
931
+ const content = validateContent(args.content);
932
+ const tier = args.tier !== void 0 ? validateTier(args.tier) : "hot";
933
+ const contentType = args.contentType !== void 0 ? validateContentType(args.contentType) : "memory";
934
+ const type = validateEntryType(args.entryType);
935
+ const project = validateOptionalString(args.project, "project");
936
+ const tags = validateTags(args.tags);
937
+ const source = validateOptionalString(args.source, "source");
833
938
  await ctx.embedder.ensureLoaded();
834
939
  const vec = await ctx.embedder.embed(content);
835
940
  const embedFn = () => vec;
836
- const id = storeMemory(ctx.db, embedFn, {
941
+ const id = await storeMemory(ctx.db, embedFn, {
837
942
  content,
838
- tier: args.tier ?? "hot",
839
- contentType: args.contentType ?? "memory",
840
- type: args.entryType,
841
- project: args.project,
842
- tags: args.tags,
843
- source: args.source
943
+ tier,
944
+ contentType,
945
+ type,
946
+ project,
947
+ tags,
948
+ source
844
949
  });
845
950
  return { content: [{ type: "text", text: JSON.stringify({ id }) }] };
846
951
  }
847
952
  if (name === "memory_search") {
848
- const query = args.query;
953
+ const query = validateString(args.query, "query");
954
+ const topK = validateOptionalNumber(args.topK, "topK", 1, 1e3) ?? 10;
955
+ const minScore = validateOptionalNumber(args.minScore, "minScore", 0, 1);
849
956
  await ctx.embedder.ensureLoaded();
850
957
  const vec = await ctx.embedder.embed(query);
851
958
  const embedFn = () => vec;
@@ -854,72 +961,84 @@ async function handleMemoryTool(name, args, ctx) {
854
961
  const tiers = ALL_TABLE_PAIRS.filter(
855
962
  (p) => (!tierFilter || tierFilter.includes(p.tier)) && (!typeFilter || typeFilter.includes(p.type))
856
963
  ).map((p) => ({ tier: p.tier, content_type: p.type }));
857
- const results = searchMemory(ctx.db, embedFn, query, {
964
+ const results = await searchMemory(ctx.db, embedFn, query, {
858
965
  tiers,
859
- topK: args.topK ?? 10,
860
- minScore: args.minScore
966
+ topK,
967
+ minScore
861
968
  });
862
969
  return { content: [{ type: "text", text: JSON.stringify(results) }] };
863
970
  }
864
971
  if (name === "memory_get") {
865
- const location = getMemory(ctx.db, args.id);
972
+ const id = validateString(args.id, "id");
973
+ const location = getMemory(ctx.db, id);
866
974
  return { content: [{ type: "text", text: JSON.stringify(location) }] };
867
975
  }
868
976
  if (name === "memory_update") {
977
+ const id = validateString(args.id, "id");
869
978
  await ctx.embedder.ensureLoaded();
870
- const newContent = args.content;
979
+ const newContent = args.content !== void 0 ? validateContent(args.content) : void 0;
980
+ const summary = validateOptionalString(args.summary, "summary");
981
+ const tags = args.tags !== void 0 ? validateTags(args.tags) : void 0;
982
+ const project = validateOptionalString(args.project, "project");
871
983
  let embedFn;
872
984
  if (newContent !== void 0) {
873
985
  const vec = await ctx.embedder.embed(newContent);
874
986
  embedFn = () => vec;
875
987
  }
876
- const updated = updateMemory(ctx.db, embedFn ?? (() => new Float32Array(0)), args.id, {
988
+ const updated = await updateMemory(ctx.db, embedFn, id, {
877
989
  content: newContent,
878
- summary: args.summary,
879
- tags: args.tags,
880
- project: args.project
990
+ summary,
991
+ tags,
992
+ project
881
993
  });
882
994
  return { content: [{ type: "text", text: JSON.stringify({ updated }) }] };
883
995
  }
884
996
  if (name === "memory_delete") {
885
- const deleted = deleteMemory(ctx.db, args.id);
997
+ const id = validateString(args.id, "id");
998
+ const deleted = deleteMemory(ctx.db, id);
886
999
  return { content: [{ type: "text", text: JSON.stringify({ deleted }) }] };
887
1000
  }
888
1001
  if (name === "memory_promote") {
889
- const location = getMemory(ctx.db, args.id);
1002
+ const id = validateString(args.id, "id");
1003
+ const toTier = validateTier(args.toTier);
1004
+ const toType = validateContentType(args.toType);
1005
+ const location = getMemory(ctx.db, id);
890
1006
  if (!location) {
891
1007
  return { content: [{ type: "text", text: JSON.stringify({ error: "Entry not found" }) }] };
892
1008
  }
893
1009
  await ctx.embedder.ensureLoaded();
894
1010
  const vec = await ctx.embedder.embed(location.entry.content);
895
1011
  const embedFn = () => vec;
896
- promoteEntry(
1012
+ await promoteEntry(
897
1013
  ctx.db,
898
1014
  embedFn,
899
- args.id,
1015
+ id,
900
1016
  location.tier,
901
1017
  location.content_type,
902
- args.toTier,
903
- args.toType
1018
+ toTier,
1019
+ toType
904
1020
  );
905
1021
  return { content: [{ type: "text", text: JSON.stringify({ promoted: true }) }] };
906
1022
  }
907
1023
  if (name === "memory_demote") {
908
- const location = getMemory(ctx.db, args.id);
1024
+ const id = validateString(args.id, "id");
1025
+ const toTier = validateTier(args.toTier);
1026
+ const toType = validateContentType(args.toType);
1027
+ const location = getMemory(ctx.db, id);
909
1028
  if (!location) {
910
1029
  return { content: [{ type: "text", text: JSON.stringify({ error: "Entry not found" }) }] };
911
1030
  }
912
1031
  await ctx.embedder.ensureLoaded();
913
1032
  const vec = await ctx.embedder.embed(location.entry.content);
914
1033
  const embedFn = () => vec;
915
- demoteEntry(
1034
+ await demoteEntry(
916
1035
  ctx.db,
917
1036
  embedFn,
918
- args.id,
1037
+ id,
919
1038
  location.tier,
920
1039
  location.content_type,
921
- args.toTier,
922
- args.toType
1040
+ toTier,
1041
+ toType
923
1042
  );
924
1043
  return { content: [{ type: "text", text: JSON.stringify({ demoted: true }) }] };
925
1044
  }
@@ -988,10 +1107,8 @@ function handleSystemTool(name, args, ctx) {
988
1107
  const parts = key.split(".");
989
1108
  let value = ctx.config;
990
1109
  for (const part of parts) {
991
- if (value === null || typeof value !== "object") {
992
- value = void 0;
993
- break;
994
- }
1110
+ if (value === null || typeof value !== "object") return { content: [{ type: "text", text: JSON.stringify({ error: "key not found" }) }] };
1111
+ if (!Object.prototype.hasOwnProperty.call(value, part)) return { content: [{ type: "text", text: JSON.stringify({ error: `key not found: ${key}` }) }] };
995
1112
  value = value[part];
996
1113
  }
997
1114
  return { content: [{ type: "text", text: JSON.stringify({ key, value }) }] };
@@ -1016,8 +1133,11 @@ function handleSystemTool(name, args, ctx) {
1016
1133
  return null;
1017
1134
  }
1018
1135
 
1136
+ // src/tools/kb-tools.ts
1137
+ import { statSync as statSync3 } from "fs";
1138
+
1019
1139
  // src/ingestion/ingest.ts
1020
- import { readFileSync as readFileSync2, readdirSync as readdirSync2, statSync } from "fs";
1140
+ import { readFileSync as readFileSync3, readdirSync as readdirSync2, statSync as statSync2 } from "fs";
1021
1141
  import { join as join5, dirname, basename, extname } from "path";
1022
1142
 
1023
1143
  // src/ingestion/markdown-parser.ts
@@ -1420,7 +1540,26 @@ function chunkFile(content, filePath, opts) {
1420
1540
  }
1421
1541
 
1422
1542
  // src/ingestion/hierarchical-index.ts
1423
- function createCollection(db, embed, opts) {
1543
+ function rowToEntry2(row) {
1544
+ return {
1545
+ id: row.id,
1546
+ content: row.content,
1547
+ summary: row.summary,
1548
+ source: row.source,
1549
+ source_tool: row.source_tool,
1550
+ project: row.project,
1551
+ tags: row.tags ? JSON.parse(row.tags) : [],
1552
+ created_at: row.created_at,
1553
+ updated_at: row.updated_at,
1554
+ last_accessed_at: row.last_accessed_at,
1555
+ access_count: row.access_count,
1556
+ decay_score: row.decay_score,
1557
+ parent_id: row.parent_id,
1558
+ collection_id: row.collection_id,
1559
+ metadata: row.metadata ? JSON.parse(row.metadata) : {}
1560
+ };
1561
+ }
1562
+ async function createCollection(db, embed, opts) {
1424
1563
  const content = `Collection: ${opts.name}`;
1425
1564
  const id = insertEntry(db, "cold", "knowledge", {
1426
1565
  content,
@@ -1431,11 +1570,11 @@ function createCollection(db, embed, opts) {
1431
1570
  source_path: opts.sourcePath
1432
1571
  }
1433
1572
  });
1434
- const embedding = embed(content);
1573
+ const embedding = await embed(content);
1435
1574
  insertEmbedding(db, "cold", "knowledge", id, embedding);
1436
1575
  return id;
1437
1576
  }
1438
- function addDocumentToCollection(db, embed, opts) {
1577
+ async function addDocumentToCollection(db, embed, opts) {
1439
1578
  const joined = opts.chunks.map((c) => c.content).join("\n\n");
1440
1579
  const docContent = joined.slice(0, 500);
1441
1580
  const docId = insertEntry(db, "cold", "knowledge", {
@@ -1448,7 +1587,7 @@ function addDocumentToCollection(db, embed, opts) {
1448
1587
  chunk_count: opts.chunks.length
1449
1588
  }
1450
1589
  });
1451
- const docEmbedding = embed(docContent);
1590
+ const docEmbedding = await embed(docContent);
1452
1591
  insertEmbedding(db, "cold", "knowledge", docId, docEmbedding);
1453
1592
  for (const chunk of opts.chunks) {
1454
1593
  const chunkId = insertEntry(db, "cold", "knowledge", {
@@ -1463,7 +1602,7 @@ function addDocumentToCollection(db, embed, opts) {
1463
1602
  kind: chunk.kind
1464
1603
  }
1465
1604
  });
1466
- const chunkEmbedding = embed(chunk.content);
1605
+ const chunkEmbedding = await embed(chunk.content);
1467
1606
  insertEmbedding(db, "cold", "knowledge", chunkId, chunkEmbedding);
1468
1607
  }
1469
1608
  return docId;
@@ -1471,25 +1610,9 @@ function addDocumentToCollection(db, embed, opts) {
1471
1610
  function listCollections(db) {
1472
1611
  const rows = db.prepare(`SELECT * FROM cold_knowledge WHERE json_extract(metadata, '$.type') = 'collection'`).all();
1473
1612
  return rows.map((row) => {
1474
- const metadata = row.metadata ? JSON.parse(row.metadata) : {};
1475
- return {
1476
- id: row.id,
1477
- content: row.content,
1478
- summary: row.summary,
1479
- source: row.source,
1480
- source_tool: row.source_tool,
1481
- project: row.project,
1482
- tags: row.tags ? JSON.parse(row.tags) : [],
1483
- created_at: row.created_at,
1484
- updated_at: row.updated_at,
1485
- last_accessed_at: row.last_accessed_at,
1486
- access_count: row.access_count,
1487
- decay_score: row.decay_score,
1488
- parent_id: row.parent_id,
1489
- collection_id: row.collection_id,
1490
- metadata,
1491
- name: metadata["name"]
1492
- };
1613
+ const entry = rowToEntry2(row);
1614
+ const metadata = entry.metadata;
1615
+ return { ...entry, name: metadata["name"] };
1493
1616
  });
1494
1617
  }
1495
1618
 
@@ -1518,19 +1641,19 @@ var INGESTABLE_EXTENSIONS = /* @__PURE__ */ new Set([
1518
1641
  ".yml",
1519
1642
  ".toml"
1520
1643
  ]);
1521
- function ingestFile(db, embed, filePath, collectionId) {
1522
- const content = readFileSync2(filePath, "utf-8");
1644
+ async function ingestFile(db, embed, filePath, collectionId) {
1645
+ const content = readFileSync3(filePath, "utf-8");
1523
1646
  const chunks = chunkFile(content, filePath, { maxTokens: 512, overlapTokens: 50 });
1524
1647
  let resolvedCollectionId = collectionId;
1525
1648
  if (!resolvedCollectionId) {
1526
1649
  const dirPath = dirname(filePath);
1527
1650
  const dirName = basename(dirPath);
1528
- resolvedCollectionId = createCollection(db, embed, {
1651
+ resolvedCollectionId = await createCollection(db, embed, {
1529
1652
  name: dirName,
1530
1653
  sourcePath: dirPath
1531
1654
  });
1532
1655
  }
1533
- const documentId = addDocumentToCollection(db, embed, {
1656
+ const documentId = await addDocumentToCollection(db, embed, {
1534
1657
  collectionId: resolvedCollectionId,
1535
1658
  sourcePath: filePath,
1536
1659
  chunks: chunks.map((c) => ({
@@ -1543,7 +1666,7 @@ function ingestFile(db, embed, filePath, collectionId) {
1543
1666
  let validationPassed = false;
1544
1667
  if (chunks.length > 0) {
1545
1668
  const firstChunk = chunks[0];
1546
- const queryVec = embed(firstChunk.content);
1669
+ const queryVec = await embed(firstChunk.content);
1547
1670
  const results = searchByVector(db, "cold", "knowledge", queryVec, {
1548
1671
  topK: 5,
1549
1672
  minScore: 0
@@ -1556,6 +1679,13 @@ function ingestFile(db, embed, filePath, collectionId) {
1556
1679
  validationPassed
1557
1680
  };
1558
1681
  }
1682
+ function matchesGlob(filename, glob) {
1683
+ if (glob.startsWith("*.")) {
1684
+ const ext = glob.slice(1);
1685
+ return filename.endsWith(ext);
1686
+ }
1687
+ return filename === glob;
1688
+ }
1559
1689
  function walkDirectory(dirPath) {
1560
1690
  const files = [];
1561
1691
  let entries;
@@ -1569,7 +1699,7 @@ function walkDirectory(dirPath) {
1569
1699
  const fullPath = join5(dirPath, entry);
1570
1700
  let stat;
1571
1701
  try {
1572
- stat = statSync(fullPath);
1702
+ stat = statSync2(fullPath);
1573
1703
  } catch {
1574
1704
  continue;
1575
1705
  }
@@ -1584,32 +1714,34 @@ function walkDirectory(dirPath) {
1584
1714
  }
1585
1715
  return files;
1586
1716
  }
1587
- function ingestDirectory(db, embed, dirPath, glob) {
1717
+ async function ingestDirectory(db, embed, dirPath, glob) {
1588
1718
  const dirName = basename(dirPath);
1589
- const collectionId = createCollection(db, embed, {
1719
+ const collectionId = await createCollection(db, embed, {
1590
1720
  name: dirName,
1591
1721
  sourcePath: dirPath
1592
1722
  });
1593
1723
  const files = walkDirectory(dirPath);
1594
1724
  let documentCount = 0;
1595
1725
  let totalChunks = 0;
1726
+ const errors = [];
1596
1727
  for (const filePath of files) {
1597
1728
  if (glob !== void 0) {
1598
1729
  const name = basename(filePath);
1599
- const pattern = glob.replace(/\./g, "\\.").replace(/\*/g, ".*");
1600
- if (!new RegExp(`^${pattern}$`).test(name)) continue;
1730
+ if (!matchesGlob(name, glob)) continue;
1601
1731
  }
1602
1732
  try {
1603
- const result = ingestFile(db, embed, filePath, collectionId);
1733
+ const result = await ingestFile(db, embed, filePath, collectionId);
1604
1734
  documentCount++;
1605
1735
  totalChunks += result.chunkCount;
1606
- } catch {
1736
+ } catch (err) {
1737
+ errors.push(`${filePath}: ${err instanceof Error ? err.message : String(err)}`);
1607
1738
  }
1608
1739
  }
1609
1740
  return {
1610
1741
  collectionId,
1611
1742
  documentCount,
1612
- totalChunks
1743
+ totalChunks,
1744
+ errors
1613
1745
  };
1614
1746
  }
1615
1747
 
@@ -1688,28 +1820,28 @@ var KB_TOOLS = [
1688
1820
  ];
1689
1821
  async function handleKbTool(name, args, ctx) {
1690
1822
  if (name === "kb_ingest_file") {
1691
- const filePath = args.path;
1692
- const collectionId = args.collection;
1823
+ const filePath = validatePath(args.path, "path");
1824
+ const collectionId = validateOptionalString(args.collection, "collection");
1693
1825
  await ctx.embedder.ensureLoaded();
1694
- const embedFn = ctx.embedder.makeSyncEmbedFn();
1695
- const result = ingestFile(ctx.db, embedFn, filePath, collectionId);
1826
+ const embedFn = (text) => ctx.embedder.embed(text);
1827
+ const result = await ingestFile(ctx.db, embedFn, filePath, collectionId);
1696
1828
  return { content: [{ type: "text", text: JSON.stringify(result) }] };
1697
1829
  }
1698
1830
  if (name === "kb_ingest_dir") {
1699
- const dirPath = args.path;
1700
- const glob = args.glob;
1831
+ const dirPath = validatePath(args.path, "path");
1832
+ const glob = validateOptionalString(args.glob, "glob");
1701
1833
  await ctx.embedder.ensureLoaded();
1702
- const embedFn = ctx.embedder.makeSyncEmbedFn();
1703
- const result = ingestDirectory(ctx.db, embedFn, dirPath, glob);
1834
+ const embedFn = (text) => ctx.embedder.embed(text);
1835
+ const result = await ingestDirectory(ctx.db, embedFn, dirPath, glob);
1704
1836
  return { content: [{ type: "text", text: JSON.stringify(result) }] };
1705
1837
  }
1706
1838
  if (name === "kb_search") {
1707
- const query = args.query;
1708
- const topK = args.top_k ?? 10;
1839
+ const query = validateString(args.query, "query");
1840
+ const topK = validateOptionalNumber(args.top_k, "top_k", 1, 1e3) ?? 10;
1709
1841
  await ctx.embedder.ensureLoaded();
1710
1842
  const vec = await ctx.embedder.embed(query);
1711
1843
  const embedFn = () => vec;
1712
- const results = searchMemory(ctx.db, embedFn, query, {
1844
+ const results = await searchMemory(ctx.db, embedFn, query, {
1713
1845
  tiers: [{ tier: "cold", content_type: "knowledge" }],
1714
1846
  topK
1715
1847
  });
@@ -1720,7 +1852,7 @@ async function handleKbTool(name, args, ctx) {
1720
1852
  return { content: [{ type: "text", text: JSON.stringify(collections) }] };
1721
1853
  }
1722
1854
  if (name === "kb_remove") {
1723
- const id = args.id;
1855
+ const id = validateString(args.id, "id");
1724
1856
  const cascade = args.cascade ?? false;
1725
1857
  if (cascade) {
1726
1858
  const children = listEntries(ctx.db, "cold", "knowledge").filter(
@@ -1736,15 +1868,68 @@ async function handleKbTool(name, args, ctx) {
1736
1868
  return { content: [{ type: "text", text: JSON.stringify({ removed: id, cascade }) }] };
1737
1869
  }
1738
1870
  if (name === "kb_refresh") {
1739
- const collection = args.collection;
1871
+ const collectionId = validateString(args.collection, "collection");
1872
+ const collectionEntry = getEntry(ctx.db, "cold", "knowledge", collectionId);
1873
+ if (!collectionEntry) {
1874
+ return {
1875
+ content: [
1876
+ {
1877
+ type: "text",
1878
+ text: JSON.stringify({ error: `Collection not found: ${collectionId}` })
1879
+ }
1880
+ ]
1881
+ };
1882
+ }
1883
+ const sourcePath = collectionEntry.metadata?.source_path;
1884
+ if (!sourcePath) {
1885
+ return {
1886
+ content: [
1887
+ {
1888
+ type: "text",
1889
+ text: JSON.stringify({ error: "Collection has no source_path in metadata; cannot refresh" })
1890
+ }
1891
+ ]
1892
+ };
1893
+ }
1894
+ const children = listEntries(ctx.db, "cold", "knowledge").filter(
1895
+ (e) => e.collection_id === collectionId || e.parent_id === collectionId
1896
+ );
1897
+ for (const child of children) {
1898
+ deleteEmbedding(ctx.db, "cold", "knowledge", child.id);
1899
+ deleteEntry(ctx.db, "cold", "knowledge", child.id);
1900
+ }
1901
+ deleteEmbedding(ctx.db, "cold", "knowledge", collectionId);
1902
+ deleteEntry(ctx.db, "cold", "knowledge", collectionId);
1903
+ await ctx.embedder.ensureLoaded();
1904
+ const embedFn = (text) => ctx.embedder.embed(text);
1905
+ let result;
1906
+ let isDir = false;
1907
+ try {
1908
+ isDir = statSync3(sourcePath).isDirectory();
1909
+ } catch (err) {
1910
+ return {
1911
+ content: [
1912
+ {
1913
+ type: "text",
1914
+ text: JSON.stringify({ error: `Cannot stat source path: ${String(err)}` })
1915
+ }
1916
+ ]
1917
+ };
1918
+ }
1919
+ if (isDir) {
1920
+ result = await ingestDirectory(ctx.db, embedFn, sourcePath);
1921
+ } else {
1922
+ result = await ingestFile(ctx.db, embedFn, sourcePath);
1923
+ }
1740
1924
  return {
1741
1925
  content: [
1742
1926
  {
1743
1927
  type: "text",
1744
1928
  text: JSON.stringify({
1745
- acknowledged: true,
1746
- collection,
1747
- note: "Refresh scheduled \u2014 re-ingest the source path to update"
1929
+ refreshed: true,
1930
+ source_path: sourcePath,
1931
+ deleted_children: children.length,
1932
+ ...result
1748
1933
  })
1749
1934
  }
1750
1935
  ]
@@ -1757,24 +1942,27 @@ function registerKbTools() {
1757
1942
  }
1758
1943
 
1759
1944
  // src/tools/eval-tools.ts
1760
- import { resolve } from "path";
1945
+ import { resolve as resolve2 } from "path";
1761
1946
  import { fileURLToPath } from "url";
1762
1947
 
1763
1948
  // src/eval/benchmark-runner.ts
1764
- import { readFileSync as readFileSync3 } from "fs";
1765
- function runBenchmark(db, embed, opts) {
1766
- const corpusLines = readFileSync3(opts.corpusPath, "utf-8").split("\n").filter((line) => line.trim().length > 0);
1767
- for (const line of corpusLines) {
1768
- const entry = JSON.parse(line);
1769
- storeMemory(db, embed, {
1770
- content: entry.content,
1771
- type: entry.type,
1772
- tier: "warm",
1773
- contentType: "memory",
1774
- tags: entry.tags
1775
- });
1949
+ import { readFileSync as readFileSync4 } from "fs";
1950
+ async function runBenchmark(db, embed, opts) {
1951
+ const corpusLines = readFileSync4(opts.corpusPath, "utf-8").split("\n").filter((line) => line.trim().length > 0);
1952
+ const existingWarmCount = countEntries(db, "warm", "memory");
1953
+ if (existingWarmCount < corpusLines.length) {
1954
+ for (const line of corpusLines) {
1955
+ const entry = JSON.parse(line);
1956
+ await storeMemory(db, embed, {
1957
+ content: entry.content,
1958
+ type: entry.type,
1959
+ tier: "warm",
1960
+ contentType: "memory",
1961
+ tags: entry.tags
1962
+ });
1963
+ }
1776
1964
  }
1777
- const benchmarkLines = readFileSync3(opts.benchmarkPath, "utf-8").split("\n").filter((line) => line.trim().length > 0);
1965
+ const benchmarkLines = readFileSync4(opts.benchmarkPath, "utf-8").split("\n").filter((line) => line.trim().length > 0);
1778
1966
  const queries = benchmarkLines.map((line) => JSON.parse(line));
1779
1967
  const details = [];
1780
1968
  let exactMatches = 0;
@@ -1783,7 +1971,7 @@ function runBenchmark(db, embed, opts) {
1783
1971
  let totalLatencyMs = 0;
1784
1972
  for (const bq of queries) {
1785
1973
  const start = performance.now();
1786
- const results = searchMemory(db, embed, bq.query, {
1974
+ const results = await searchMemory(db, embed, bq.query, {
1787
1975
  tiers: [{ tier: "warm", content_type: "memory" }],
1788
1976
  topK: 3
1789
1977
  });
@@ -1928,7 +2116,7 @@ function computeMetrics(events, similarityThreshold) {
1928
2116
 
1929
2117
  // src/tools/eval-tools.ts
1930
2118
  var __dirname = fileURLToPath(new URL(".", import.meta.url));
1931
- var PACKAGE_ROOT = resolve(__dirname, "..", "..");
2119
+ var PACKAGE_ROOT = resolve2(__dirname, "..", "..");
1932
2120
  var EVAL_TOOLS = [
1933
2121
  {
1934
2122
  name: "eval_benchmark",
@@ -1958,10 +2146,10 @@ var EVAL_TOOLS = [
1958
2146
  async function handleEvalTool(name, args, ctx) {
1959
2147
  if (name === "eval_benchmark") {
1960
2148
  await ctx.embedder.ensureLoaded();
1961
- const embedFn = ctx.embedder.makeSyncEmbedFn();
1962
- const corpusPath = resolve(PACKAGE_ROOT, "eval", "corpus", "memories.jsonl");
1963
- const benchmarkPath = resolve(PACKAGE_ROOT, "eval", "benchmarks", "retrieval.jsonl");
1964
- const result = runBenchmark(ctx.db, embedFn, {
2149
+ const embedFn = (text) => ctx.embedder.embed(text);
2150
+ const corpusPath = resolve2(PACKAGE_ROOT, "eval", "corpus", "memories.jsonl");
2151
+ const benchmarkPath = resolve2(PACKAGE_ROOT, "eval", "benchmarks", "retrieval.jsonl");
2152
+ const result = await runBenchmark(ctx.db, embedFn, {
1965
2153
  corpusPath,
1966
2154
  benchmarkPath
1967
2155
  });
@@ -1985,7 +2173,7 @@ function registerEvalTools() {
1985
2173
  }
1986
2174
 
1987
2175
  // src/importers/claude-code.ts
1988
- import { existsSync as existsSync4, readdirSync as readdirSync3, readFileSync as readFileSync4 } from "fs";
2176
+ import { existsSync as existsSync4, readdirSync as readdirSync3, readFileSync as readFileSync5 } from "fs";
1989
2177
  import { join as join6 } from "path";
1990
2178
  import { homedir } from "os";
1991
2179
  import { createHash } from "crypto";
@@ -2053,7 +2241,7 @@ var ClaudeCodeImporter = class {
2053
2241
  }
2054
2242
  return { memoryFiles, knowledgeFiles, sessionFiles };
2055
2243
  }
2056
- importMemories(db, embed, project) {
2244
+ async importMemories(db, embed, project) {
2057
2245
  const result = { imported: 0, skipped: 0, errors: [] };
2058
2246
  const projectsDir = join6(this.basePath, "projects");
2059
2247
  if (!existsSync4(projectsDir)) return result;
@@ -2066,7 +2254,7 @@ var ClaudeCodeImporter = class {
2066
2254
  if (!filename.endsWith(".md") || filename === "MEMORY.md") continue;
2067
2255
  const filePath = join6(memoryDir, filename);
2068
2256
  try {
2069
- const raw = readFileSync4(filePath, "utf8");
2257
+ const raw = readFileSync5(filePath, "utf8");
2070
2258
  const hash = contentHash(raw);
2071
2259
  if (isAlreadyImported(db, hash)) {
2072
2260
  result.skipped++;
@@ -2087,7 +2275,7 @@ var ClaudeCodeImporter = class {
2087
2275
  project: project ?? null,
2088
2276
  tags: frontmatter?.name ? [frontmatter.name] : []
2089
2277
  });
2090
- insertEmbedding(db, tier, type, entryId, embed(content));
2278
+ insertEmbedding(db, tier, type, entryId, await embed(content));
2091
2279
  logImport(db, "claude-code", filePath, hash, entryId, tier, type);
2092
2280
  result.imported++;
2093
2281
  } catch (err) {
@@ -2097,12 +2285,12 @@ var ClaudeCodeImporter = class {
2097
2285
  }
2098
2286
  return result;
2099
2287
  }
2100
- importKnowledge(db, embed) {
2288
+ async importKnowledge(db, embed) {
2101
2289
  const result = { imported: 0, skipped: 0, errors: [] };
2102
2290
  const claudeMdPath = join6(this.basePath, "CLAUDE.md");
2103
2291
  if (!existsSync4(claudeMdPath)) return result;
2104
2292
  try {
2105
- const raw = readFileSync4(claudeMdPath, "utf8");
2293
+ const raw = readFileSync5(claudeMdPath, "utf8");
2106
2294
  const hash = contentHash(raw);
2107
2295
  if (isAlreadyImported(db, hash)) {
2108
2296
  result.skipped++;
@@ -2115,7 +2303,7 @@ var ClaudeCodeImporter = class {
2115
2303
  source_tool: "claude-code",
2116
2304
  tags: ["pinned"]
2117
2305
  });
2118
- insertEmbedding(db, "warm", "knowledge", entryId, embed(content));
2306
+ insertEmbedding(db, "warm", "knowledge", entryId, await embed(content));
2119
2307
  logImport(db, "claude-code", claudeMdPath, hash, entryId, "warm", "knowledge");
2120
2308
  result.imported++;
2121
2309
  } catch (err) {
@@ -2128,7 +2316,7 @@ var ClaudeCodeImporter = class {
2128
2316
  };
2129
2317
 
2130
2318
  // src/importers/copilot-cli.ts
2131
- import { existsSync as existsSync5, readdirSync as readdirSync4, readFileSync as readFileSync5 } from "fs";
2319
+ import { existsSync as existsSync5, readdirSync as readdirSync4, readFileSync as readFileSync6 } from "fs";
2132
2320
  import { join as join7 } from "path";
2133
2321
  import { homedir as homedir2 } from "os";
2134
2322
  import { createHash as createHash2 } from "crypto";
@@ -2176,10 +2364,10 @@ var CopilotCliImporter = class {
2176
2364
  }
2177
2365
  return { memoryFiles: 0, knowledgeFiles, sessionFiles };
2178
2366
  }
2179
- importMemories(_db2, _embed, _project) {
2367
+ async importMemories(_db2, _embed, _project) {
2180
2368
  return { imported: 0, skipped: 0, errors: [] };
2181
2369
  }
2182
- importKnowledge(db, embed) {
2370
+ async importKnowledge(db, embed) {
2183
2371
  const result = { imported: 0, skipped: 0, errors: [] };
2184
2372
  const sessionStateDir = join7(this.basePath, "session-state");
2185
2373
  if (!existsSync5(sessionStateDir)) return result;
@@ -2188,7 +2376,7 @@ var CopilotCliImporter = class {
2188
2376
  const planPath = join7(sessionStateDir, entry.name, "plan.md");
2189
2377
  if (!existsSync5(planPath)) continue;
2190
2378
  try {
2191
- const raw = readFileSync5(planPath, "utf8");
2379
+ const raw = readFileSync6(planPath, "utf8");
2192
2380
  const hash = contentHash2(raw);
2193
2381
  if (isAlreadyImported2(db, hash)) {
2194
2382
  result.skipped++;
@@ -2199,7 +2387,7 @@ var CopilotCliImporter = class {
2199
2387
  source: planPath,
2200
2388
  source_tool: "copilot-cli"
2201
2389
  });
2202
- insertEmbedding(db, "cold", "knowledge", entryId, embed(raw));
2390
+ insertEmbedding(db, "cold", "knowledge", entryId, await embed(raw));
2203
2391
  logImport2(db, "copilot-cli", planPath, hash, entryId, "cold", "knowledge");
2204
2392
  result.imported++;
2205
2393
  } catch (err) {
@@ -2226,9 +2414,9 @@ var IMPORT_TOOLS = [
2226
2414
  ];
2227
2415
  async function handleImportTool(name, args, ctx) {
2228
2416
  if (name === "import_host") {
2229
- const source = args.source;
2417
+ const source = validateOptionalString(args.source, "source");
2230
2418
  await ctx.embedder.ensureLoaded();
2231
- const embedFn = ctx.embedder.makeSyncEmbedFn();
2419
+ const embedFn = (text) => ctx.embedder.embed(text);
2232
2420
  const importers = [
2233
2421
  new ClaudeCodeImporter(),
2234
2422
  new CopilotCliImporter()
@@ -2242,8 +2430,8 @@ async function handleImportTool(name, args, ctx) {
2242
2430
  continue;
2243
2431
  }
2244
2432
  const scan = importer.scan();
2245
- const memoriesResult = importer.importMemories(ctx.db, embedFn);
2246
- const knowledgeResult = importer.importKnowledge(ctx.db, embedFn);
2433
+ const memoriesResult = await importer.importMemories(ctx.db, embedFn);
2434
+ const knowledgeResult = await importer.importKnowledge(ctx.db, embedFn);
2247
2435
  results.push({
2248
2436
  tool: importer.name,
2249
2437
  detected: true,
@@ -2312,7 +2500,7 @@ function logCompactionEvent(db, opts) {
2312
2500
  opts.configSnapshotId
2313
2501
  );
2314
2502
  }
2315
- function compactHotTier(db, embed, config, sessionId, configSnapshotId) {
2503
+ async function compactHotTier(db, embed, config, sessionId, configSnapshotId) {
2316
2504
  const snapshotId = configSnapshotId ?? "default";
2317
2505
  const entries = listEntries(db, "hot", "memory");
2318
2506
  const now = Date.now();
@@ -2334,7 +2522,7 @@ function compactHotTier(db, embed, config, sessionId, configSnapshotId) {
2334
2522
  if (score > config.promote_threshold) {
2335
2523
  carryForward.push(entry.id);
2336
2524
  } else if (score >= config.warm_threshold) {
2337
- promoteEntry(db, embed, entry.id, "hot", "memory", "warm", "memory");
2525
+ await promoteEntry(db, embed, entry.id, "hot", "memory", "warm", "memory");
2338
2526
  promoted.push(entry.id);
2339
2527
  logCompactionEvent(db, {
2340
2528
  sessionId,
@@ -2398,7 +2586,7 @@ var SESSION_TOOLS = [
2398
2586
  async function handleSessionTool(name, args, ctx) {
2399
2587
  if (name === "session_start") {
2400
2588
  await ctx.embedder.ensureLoaded();
2401
- const embedFn = ctx.embedder.makeSyncEmbedFn();
2589
+ const embedFn = (text) => ctx.embedder.embed(text);
2402
2590
  const importers = [
2403
2591
  new ClaudeCodeImporter(),
2404
2592
  new CopilotCliImporter()
@@ -2406,8 +2594,8 @@ async function handleSessionTool(name, args, ctx) {
2406
2594
  const importSummary = [];
2407
2595
  for (const importer of importers) {
2408
2596
  if (!importer.detect()) continue;
2409
- const memResult = importer.importMemories(ctx.db, embedFn);
2410
- const kbResult = importer.importKnowledge(ctx.db, embedFn);
2597
+ const memResult = await importer.importMemories(ctx.db, embedFn);
2598
+ const kbResult = await importer.importKnowledge(ctx.db, embedFn);
2411
2599
  importSummary.push({
2412
2600
  tool: importer.name,
2413
2601
  memoriesImported: memResult.imported,
@@ -2436,9 +2624,9 @@ async function handleSessionTool(name, args, ctx) {
2436
2624
  }
2437
2625
  if (name === "session_end") {
2438
2626
  await ctx.embedder.ensureLoaded();
2439
- const embedFn = ctx.embedder.makeSyncEmbedFn();
2627
+ const embedFn = (text) => ctx.embedder.embed(text);
2440
2628
  const sessionId = ctx.sessionId ?? randomUUID4();
2441
- const result = compactHotTier(ctx.db, embedFn, ctx.config.compaction, sessionId);
2629
+ const result = await compactHotTier(ctx.db, embedFn, ctx.config.compaction, sessionId);
2442
2630
  return {
2443
2631
  content: [
2444
2632
  {
@@ -2482,6 +2670,355 @@ function registerSessionTools() {
2482
2670
  return SESSION_TOOLS;
2483
2671
  }
2484
2672
 
2673
+ // src/tools/extra-tools.ts
2674
+ import { mkdirSync as mkdirSync3, writeFileSync, readFileSync as readFileSync7 } from "fs";
2675
+ import { join as join8 } from "path";
2676
+ function registerExtraTools() {
2677
+ return [
2678
+ {
2679
+ name: "compact_now",
2680
+ description: "Force compaction of a memory tier immediately",
2681
+ inputSchema: {
2682
+ type: "object",
2683
+ properties: {
2684
+ tier: {
2685
+ type: "string",
2686
+ enum: ["hot"],
2687
+ description: "Tier to compact (currently only hot supported)"
2688
+ }
2689
+ },
2690
+ required: []
2691
+ }
2692
+ },
2693
+ {
2694
+ name: "memory_inspect",
2695
+ description: "Deep dive on a single memory entry, including its compaction history",
2696
+ inputSchema: {
2697
+ type: "object",
2698
+ properties: {
2699
+ id: { type: "string", description: "Entry ID" }
2700
+ },
2701
+ required: ["id"]
2702
+ }
2703
+ },
2704
+ {
2705
+ name: "memory_history",
2706
+ description: "List recent tier movements from compaction log",
2707
+ inputSchema: {
2708
+ type: "object",
2709
+ properties: {
2710
+ limit: { type: "number", description: "Max results (default 20)" }
2711
+ },
2712
+ required: []
2713
+ }
2714
+ },
2715
+ {
2716
+ name: "memory_lineage",
2717
+ description: "Show the full compaction ancestry tree for a memory entry",
2718
+ inputSchema: {
2719
+ type: "object",
2720
+ properties: {
2721
+ id: { type: "string", description: "Entry ID" }
2722
+ },
2723
+ required: ["id"]
2724
+ }
2725
+ },
2726
+ {
2727
+ name: "memory_export",
2728
+ description: "Export memories to a JSON file",
2729
+ inputSchema: {
2730
+ type: "object",
2731
+ properties: {
2732
+ tiers: {
2733
+ type: "array",
2734
+ items: { type: "string", enum: ["hot", "warm", "cold"] },
2735
+ description: "Tiers to export (default: all)"
2736
+ },
2737
+ content_types: {
2738
+ type: "array",
2739
+ items: { type: "string", enum: ["memory", "knowledge"] },
2740
+ description: "Content types to export (default: all)"
2741
+ },
2742
+ format: {
2743
+ type: "string",
2744
+ enum: ["json"],
2745
+ description: "Export format (default: json)"
2746
+ }
2747
+ },
2748
+ required: []
2749
+ }
2750
+ },
2751
+ {
2752
+ name: "memory_import",
2753
+ description: "Import memories from a JSON export file",
2754
+ inputSchema: {
2755
+ type: "object",
2756
+ properties: {
2757
+ path: { type: "string", description: "Path to export JSON file" }
2758
+ },
2759
+ required: ["path"]
2760
+ }
2761
+ }
2762
+ ];
2763
+ }
2764
+ function getCompactionLogForEntry(db, id) {
2765
+ const rows = db.prepare(
2766
+ `SELECT * FROM compaction_log
2767
+ WHERE target_entry_id = ?
2768
+ OR source_entry_ids LIKE ?
2769
+ ORDER BY timestamp DESC`
2770
+ ).all(id, `%"${id}"%`);
2771
+ return rows;
2772
+ }
2773
+ function buildLineage(db, id, depth) {
2774
+ if (depth >= 10) {
2775
+ return { id, sources: [] };
2776
+ }
2777
+ const row = db.prepare(`SELECT * FROM compaction_log WHERE target_entry_id = ? ORDER BY timestamp DESC LIMIT 1`).get(id);
2778
+ if (!row) {
2779
+ return { id };
2780
+ }
2781
+ let sourceIds = [];
2782
+ try {
2783
+ sourceIds = JSON.parse(row.source_entry_ids);
2784
+ } catch {
2785
+ sourceIds = [];
2786
+ }
2787
+ const sources = sourceIds.map((srcId) => buildLineage(db, srcId, depth + 1));
2788
+ return {
2789
+ id,
2790
+ compaction_log_id: row.id,
2791
+ reason: row.reason,
2792
+ timestamp: row.timestamp,
2793
+ source_tier: row.source_tier,
2794
+ target_tier: row.target_tier,
2795
+ sources
2796
+ };
2797
+ }
2798
+ async function handleExtraTool(name, args, ctx) {
2799
+ const { db } = ctx;
2800
+ if (name === "compact_now") {
2801
+ await ctx.embedder.ensureLoaded();
2802
+ const embedFn = (text) => ctx.embedder.embed(text);
2803
+ const result = await compactHotTier(
2804
+ db,
2805
+ embedFn,
2806
+ ctx.config.compaction,
2807
+ ctx.sessionId
2808
+ );
2809
+ return {
2810
+ content: [
2811
+ {
2812
+ type: "text",
2813
+ text: JSON.stringify({
2814
+ carryForward: result.carryForward.length,
2815
+ promoted: result.promoted.length,
2816
+ discarded: result.discarded.length,
2817
+ carryForwardIds: result.carryForward,
2818
+ promotedIds: result.promoted,
2819
+ discardedIds: result.discarded
2820
+ })
2821
+ }
2822
+ ]
2823
+ };
2824
+ }
2825
+ if (name === "memory_inspect") {
2826
+ const id = validateString(args.id, "id");
2827
+ const location = getMemory(db, id);
2828
+ const compactionHistory = getCompactionLogForEntry(db, id);
2829
+ return {
2830
+ content: [
2831
+ {
2832
+ type: "text",
2833
+ text: JSON.stringify({
2834
+ entry: location,
2835
+ compaction_history: compactionHistory
2836
+ })
2837
+ }
2838
+ ]
2839
+ };
2840
+ }
2841
+ if (name === "memory_history") {
2842
+ const limit = validateOptionalNumber(args.limit, "limit", 1, 1e3) ?? 20;
2843
+ const rows = db.prepare(`SELECT * FROM compaction_log ORDER BY timestamp DESC LIMIT ?`).all(limit);
2844
+ const movements = rows.map((row) => {
2845
+ let sourceIds = [];
2846
+ try {
2847
+ sourceIds = JSON.parse(row.source_entry_ids);
2848
+ } catch {
2849
+ sourceIds = [];
2850
+ }
2851
+ return {
2852
+ id: row.id,
2853
+ timestamp: row.timestamp,
2854
+ session_id: row.session_id,
2855
+ source_tier: row.source_tier,
2856
+ target_tier: row.target_tier,
2857
+ source_entry_ids: sourceIds,
2858
+ target_entry_id: row.target_entry_id,
2859
+ reason: row.reason,
2860
+ decay_scores: (() => {
2861
+ try {
2862
+ return JSON.parse(row.decay_scores);
2863
+ } catch {
2864
+ return {};
2865
+ }
2866
+ })()
2867
+ };
2868
+ });
2869
+ return {
2870
+ content: [
2871
+ {
2872
+ type: "text",
2873
+ text: JSON.stringify({ movements, count: movements.length })
2874
+ }
2875
+ ]
2876
+ };
2877
+ }
2878
+ if (name === "memory_lineage") {
2879
+ const id = validateString(args.id, "id");
2880
+ const lineage = buildLineage(db, id, 0);
2881
+ return {
2882
+ content: [
2883
+ {
2884
+ type: "text",
2885
+ text: JSON.stringify({ lineage })
2886
+ }
2887
+ ]
2888
+ };
2889
+ }
2890
+ if (name === "memory_export") {
2891
+ const tierFilter = args.tiers;
2892
+ const typeFilter = args.content_types;
2893
+ const pairs = ALL_TABLE_PAIRS.filter(
2894
+ (p) => (!tierFilter || tierFilter.includes(p.tier)) && (!typeFilter || typeFilter.includes(p.type))
2895
+ );
2896
+ const allEntries = [];
2897
+ for (const { tier, type } of pairs) {
2898
+ const entries = listEntries(db, tier, type);
2899
+ for (const entry of entries) {
2900
+ allEntries.push({
2901
+ ...entry,
2902
+ tier,
2903
+ content_type: type
2904
+ });
2905
+ }
2906
+ }
2907
+ const exportsDir = join8(getDataDir(), "exports");
2908
+ mkdirSync3(exportsDir, { recursive: true });
2909
+ const timestamp = Date.now();
2910
+ const exportPath = join8(exportsDir, `${timestamp}.json`);
2911
+ const exportData = { version: 1, exported_at: timestamp, entries: allEntries };
2912
+ const jsonStr = JSON.stringify(exportData, null, 2);
2913
+ writeFileSync(exportPath, jsonStr, "utf-8");
2914
+ return {
2915
+ content: [
2916
+ {
2917
+ type: "text",
2918
+ text: JSON.stringify({
2919
+ path: exportPath,
2920
+ entry_count: allEntries.length,
2921
+ size_bytes: Buffer.byteLength(jsonStr, "utf-8")
2922
+ })
2923
+ }
2924
+ ]
2925
+ };
2926
+ }
2927
+ if (name === "memory_import") {
2928
+ const filePath = validatePath(args.path, "path");
2929
+ let raw;
2930
+ try {
2931
+ raw = readFileSync7(filePath, "utf-8");
2932
+ } catch (err) {
2933
+ return {
2934
+ content: [
2935
+ {
2936
+ type: "text",
2937
+ text: JSON.stringify({ error: `Failed to read file: ${String(err)}` })
2938
+ }
2939
+ ]
2940
+ };
2941
+ }
2942
+ let exportData;
2943
+ try {
2944
+ exportData = JSON.parse(raw);
2945
+ } catch {
2946
+ return {
2947
+ content: [
2948
+ {
2949
+ type: "text",
2950
+ text: JSON.stringify({ error: "Invalid JSON in export file" })
2951
+ }
2952
+ ]
2953
+ };
2954
+ }
2955
+ const entries = exportData.entries ?? [];
2956
+ if (!Array.isArray(entries)) {
2957
+ return {
2958
+ content: [
2959
+ {
2960
+ type: "text",
2961
+ text: JSON.stringify({ error: "Export file missing entries array" })
2962
+ }
2963
+ ]
2964
+ };
2965
+ }
2966
+ await ctx.embedder.ensureLoaded();
2967
+ let imported = 0;
2968
+ let skipped = 0;
2969
+ const existingIds = /* @__PURE__ */ new Set();
2970
+ const existingContents = /* @__PURE__ */ new Set();
2971
+ for (const { tier, type } of ALL_TABLE_PAIRS) {
2972
+ const existing = listEntries(db, tier, type);
2973
+ for (const e of existing) {
2974
+ existingIds.add(e.id);
2975
+ existingContents.add(e.content);
2976
+ }
2977
+ }
2978
+ const seenContents = new Set(existingContents);
2979
+ for (const entry of entries) {
2980
+ if (typeof entry.content !== "string" || !entry.content) {
2981
+ skipped++;
2982
+ continue;
2983
+ }
2984
+ if (existingIds.has(entry.id)) {
2985
+ skipped++;
2986
+ continue;
2987
+ }
2988
+ if (seenContents.has(entry.content)) {
2989
+ skipped++;
2990
+ continue;
2991
+ }
2992
+ seenContents.add(entry.content);
2993
+ const tier = ["hot", "warm", "cold"].includes(entry.tier) ? entry.tier : "hot";
2994
+ const content_type = ["memory", "knowledge"].includes(entry.content_type) ? entry.content_type : "memory";
2995
+ const newId = insertEntry(db, tier, content_type, {
2996
+ content: entry.content,
2997
+ summary: entry.summary ?? null,
2998
+ source: entry.source ?? null,
2999
+ source_tool: entry.source_tool ?? null,
3000
+ project: entry.project ?? null,
3001
+ tags: Array.isArray(entry.tags) ? entry.tags : [],
3002
+ parent_id: entry.parent_id ?? null,
3003
+ collection_id: entry.collection_id ?? null,
3004
+ metadata: entry.metadata ?? {}
3005
+ });
3006
+ const vec = await ctx.embedder.embed(entry.content);
3007
+ insertEmbedding(db, tier, content_type, newId, vec);
3008
+ imported++;
3009
+ }
3010
+ return {
3011
+ content: [
3012
+ {
3013
+ type: "text",
3014
+ text: JSON.stringify({ imported, skipped })
3015
+ }
3016
+ ]
3017
+ };
3018
+ }
3019
+ return null;
3020
+ }
3021
+
2485
3022
  // src/tools/registry.ts
2486
3023
  async function startServer(ctx) {
2487
3024
  const server = new Server(
@@ -2494,7 +3031,8 @@ async function startServer(ctx) {
2494
3031
  ...registerKbTools(),
2495
3032
  ...registerEvalTools(),
2496
3033
  ...registerImportTools(),
2497
- ...registerSessionTools()
3034
+ ...registerSessionTools(),
3035
+ ...registerExtraTools()
2498
3036
  ];
2499
3037
  server.setRequestHandler(ListToolsRequestSchema, async () => {
2500
3038
  return { tools: allTools };
@@ -2514,6 +3052,8 @@ async function startServer(ctx) {
2514
3052
  if (importResult !== null) return importResult;
2515
3053
  const sessionResult = await handleSessionTool(name, args ?? {}, ctx);
2516
3054
  if (sessionResult !== null) return sessionResult;
3055
+ const extraResult = await handleExtraTool(name, args ?? {}, ctx);
3056
+ if (extraResult !== null) return extraResult;
2517
3057
  return {
2518
3058
  content: [
2519
3059
  {