@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/.claude-plugin/plugin.json +12 -11
- package/.copilot-plugin/plugin.json +2 -2
- package/.cursor-plugin/plugin.json +2 -2
- package/.mcp.json +8 -0
- package/README.md +18 -21
- package/bin/total-recall.sh +70 -0
- package/dist/defaults.toml +28 -0
- package/dist/index.js +723 -183
- package/package.json +4 -1
- package/skills/status/SKILL.md +7 -0
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
|
-
|
|
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 =
|
|
265
|
+
const modelPath = getUserModelPath(modelName);
|
|
243
266
|
mkdirSync2(modelPath, { recursive: true });
|
|
244
|
-
const
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
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
|
|
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
|
|
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
|
|
839
|
-
contentType
|
|
840
|
-
type
|
|
841
|
-
project
|
|
842
|
-
tags
|
|
843
|
-
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
|
|
860
|
-
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
|
|
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
|
|
988
|
+
const updated = await updateMemory(ctx.db, embedFn, id, {
|
|
877
989
|
content: newContent,
|
|
878
|
-
summary
|
|
879
|
-
tags
|
|
880
|
-
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
|
|
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
|
|
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
|
-
|
|
1015
|
+
id,
|
|
900
1016
|
location.tier,
|
|
901
1017
|
location.content_type,
|
|
902
|
-
|
|
903
|
-
|
|
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
|
|
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
|
-
|
|
1037
|
+
id,
|
|
919
1038
|
location.tier,
|
|
920
1039
|
location.content_type,
|
|
921
|
-
|
|
922
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
1475
|
-
|
|
1476
|
-
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
|
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
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
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
|
|
1765
|
-
function runBenchmark(db, embed, opts) {
|
|
1766
|
-
const corpusLines =
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
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 =
|
|
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 =
|
|
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.
|
|
1962
|
-
const corpusPath =
|
|
1963
|
-
const benchmarkPath =
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
|
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 =
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
{
|