@cognistore/mcp-server 1.0.9 → 1.0.13

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.
Files changed (2) hide show
  1. package/dist/index.js +374 -55
  2. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -44,8 +44,8 @@ var TaskPriority;
44
44
  })(TaskPriority || (TaskPriority = {}));
45
45
 
46
46
  // ../../packages/shared/dist/constants/defaults.js
47
- var DEFAULT_EMBEDDING_MODEL = "all-minilm";
48
- var DEFAULT_EMBEDDING_DIMENSIONS = 384;
47
+ var DEFAULT_EMBEDDING_MODEL = "nomic-embed-text";
48
+ var DEFAULT_EMBEDDING_DIMENSIONS = 256;
49
49
  var DEFAULT_SIMILARITY_THRESHOLD = 0.3;
50
50
  var DEFAULT_SEARCH_LIMIT = 10;
51
51
  var DEFAULT_OLLAMA_HOST = "http://localhost:11434";
@@ -58,12 +58,13 @@ var knowledgeTypeSchema = z.nativeEnum(KnowledgeType);
58
58
  var knowledgeStatusSchema = z.nativeEnum(KnowledgeStatus);
59
59
  var taskStatusSchema = z.nativeEnum(TaskStatus);
60
60
  var taskPrioritySchema = z.nativeEnum(TaskPriority);
61
+ var scopeSchema = z.string().regex(/^(global|workspace:[a-zA-Z0-9._-]+)$/, 'Scope must be "global" or "workspace:<project-name>" (alphanumeric, dots, hyphens, underscores)');
61
62
  var createKnowledgeSchema = z.object({
62
63
  title: z.string().min(1, "Title is required"),
63
64
  content: z.string().min(1, "Content is required"),
64
65
  tags: z.array(z.string().min(1)).min(1, "At least one tag is required"),
65
66
  type: knowledgeTypeSchema,
66
- scope: z.string().min(1, "Scope is required"),
67
+ scope: scopeSchema,
67
68
  source: z.string().min(1, "Source is required"),
68
69
  confidenceScore: z.number().min(0).max(1).optional().default(1),
69
70
  expiresAt: z.date().nullable().optional().default(null),
@@ -75,7 +76,7 @@ var updateKnowledgeSchema = z.object({
75
76
  content: z.string().min(1).optional(),
76
77
  tags: z.array(z.string().min(1)).min(1).optional(),
77
78
  type: knowledgeTypeSchema.optional(),
78
- scope: z.string().min(1).optional(),
79
+ scope: scopeSchema.optional(),
79
80
  source: z.string().min(1).optional(),
80
81
  confidenceScore: z.number().min(0).max(1).optional(),
81
82
  expiresAt: z.date().nullable().optional(),
@@ -85,7 +86,7 @@ var updateKnowledgeSchema = z.object({
85
86
  var searchOptionsSchema = z.object({
86
87
  tags: z.array(z.string()).optional(),
87
88
  type: knowledgeTypeSchema.optional(),
88
- scope: z.string().optional(),
89
+ scope: scopeSchema.optional(),
89
90
  limit: z.number().int().min(1).max(100).optional().default(DEFAULT_SEARCH_LIMIT),
90
91
  threshold: z.number().min(0).max(1).optional().default(DEFAULT_SIMILARITY_THRESHOLD)
91
92
  });
@@ -93,15 +94,19 @@ var createPlanSchema = z.object({
93
94
  title: z.string().min(1, "Title is required"),
94
95
  content: z.string().min(1, "Content is required"),
95
96
  tags: z.array(z.string().min(1)).min(1, "At least one tag is required"),
96
- scope: z.string().min(1, "Scope is required"),
97
+ scope: scopeSchema,
97
98
  source: z.string().min(1, "Source is required"),
98
- status: knowledgeStatusSchema.optional().default(KnowledgeStatus.DRAFT)
99
+ status: knowledgeStatusSchema.optional().default(KnowledgeStatus.DRAFT),
100
+ tasks: z.array(z.object({
101
+ description: z.string().min(1),
102
+ priority: z.enum(["low", "medium", "high"]).optional()
103
+ })).optional()
99
104
  });
100
105
  var updatePlanSchema = z.object({
101
106
  title: z.string().min(1).optional(),
102
107
  content: z.string().min(1).optional(),
103
108
  tags: z.array(z.string().min(1)).min(1).optional(),
104
- scope: z.string().min(1).optional(),
109
+ scope: scopeSchema.optional(),
105
110
  status: knowledgeStatusSchema.optional(),
106
111
  source: z.string().min(1).optional()
107
112
  });
@@ -126,11 +131,15 @@ var schema_exports = {};
126
131
  __export(schema_exports, {
127
132
  createEmbeddingsTable: () => createEmbeddingsTable,
128
133
  deleteEmbedding: () => deleteEmbedding,
134
+ deletePlanEmbedding: () => deletePlanEmbedding,
129
135
  insertEmbedding: () => insertEmbedding,
136
+ insertPlanEmbedding: () => insertPlanEmbedding,
130
137
  knowledgeEntries: () => knowledgeEntries,
131
138
  knowledgeTypeEnum: () => knowledgeTypeEnum,
132
139
  searchKnn: () => searchKnn,
133
- updateEmbedding: () => updateEmbedding
140
+ searchPlansKnn: () => searchPlansKnn,
141
+ updateEmbedding: () => updateEmbedding,
142
+ updatePlanEmbedding: () => updatePlanEmbedding
134
143
  });
135
144
 
136
145
  // ../../packages/core/dist/db/schema/knowledge.js
@@ -158,7 +167,7 @@ var knowledgeEntries = sqliteTable("knowledge_entries", {
158
167
 
159
168
  // ../../packages/core/dist/db/schema/sqlite-vec.js
160
169
  var VIRTUAL_TABLE_NAME = "knowledge_embeddings";
161
- function createEmbeddingsTable(sqlite, dimensions = 384) {
170
+ function createEmbeddingsTable(sqlite, dimensions = DEFAULT_EMBEDDING_DIMENSIONS) {
162
171
  sqlite.exec(`
163
172
  CREATE VIRTUAL TABLE IF NOT EXISTS ${VIRTUAL_TABLE_NAME} USING vec0(
164
173
  id TEXT PRIMARY KEY,
@@ -187,6 +196,25 @@ function searchKnn(sqlite, queryEmbedding, k) {
187
196
  `);
188
197
  return stmt.all(Buffer.from(new Float32Array(queryEmbedding).buffer), k);
189
198
  }
199
+ var PLANS_TABLE_NAME = "plans_embeddings";
200
+ function insertPlanEmbedding(sqlite, id, embedding) {
201
+ sqlite.prepare(`INSERT INTO ${PLANS_TABLE_NAME}(id, embedding) VALUES (?, ?)`).run(id, Buffer.from(new Float32Array(embedding).buffer));
202
+ }
203
+ function updatePlanEmbedding(sqlite, id, embedding) {
204
+ sqlite.prepare(`UPDATE ${PLANS_TABLE_NAME} SET embedding = ? WHERE id = ?`).run(Buffer.from(new Float32Array(embedding).buffer), id);
205
+ }
206
+ function deletePlanEmbedding(sqlite, id) {
207
+ sqlite.prepare(`DELETE FROM ${PLANS_TABLE_NAME} WHERE id = ?`).run(id);
208
+ }
209
+ function searchPlansKnn(sqlite, queryEmbedding, k) {
210
+ const stmt = sqlite.prepare(`
211
+ SELECT id, distance
212
+ FROM ${PLANS_TABLE_NAME}
213
+ WHERE embedding MATCH ?
214
+ AND k = ?
215
+ `);
216
+ return stmt.all(Buffer.from(new Float32Array(queryEmbedding).buffer), k);
217
+ }
190
218
 
191
219
  // ../../packages/core/dist/db/migrate.js
192
220
  import { readdirSync, readFileSync, existsSync } from "fs";
@@ -275,7 +303,7 @@ function runMigrations(sqlite, migrationsDir) {
275
303
  if (tableExists) {
276
304
  sqlite.prepare("INSERT INTO schema_version (version, applied_at) VALUES (?, ?)").run("0.8.0", (/* @__PURE__ */ new Date()).toISOString());
277
305
  applied.add("0.8.0");
278
- console.log("Migration: bootstrapped existing DB as v0.8.0");
306
+ console.error("Migration: bootstrapped existing DB as v0.8.0");
279
307
  }
280
308
  }
281
309
  let migrations;
@@ -287,7 +315,7 @@ function runMigrations(sqlite, migrationsDir) {
287
315
  }));
288
316
  } else {
289
317
  migrations = Object.entries(EMBEDDED_MIGRATIONS).map(([version, sql2]) => ({ version, sql: sql2 })).sort((a, b) => compareSemver(a.version, b.version));
290
- console.log("Migration: using embedded migrations (bundled mode)");
318
+ console.error("Migration: using embedded migrations (bundled mode)");
291
319
  }
292
320
  for (const { version, sql: sql2 } of migrations) {
293
321
  if (applied.has(version))
@@ -306,7 +334,7 @@ function runMigrations(sqlite, migrationsDir) {
306
334
  }
307
335
  }
308
336
  sqlite.prepare("INSERT INTO schema_version (version, applied_at) VALUES (?, ?)").run(version, (/* @__PURE__ */ new Date()).toISOString());
309
- console.log(`Migration: ${version} applied`);
337
+ console.error(`Migration: ${version} applied`);
310
338
  }
311
339
  }
312
340
  function runSeeds(sqlite, seedsDir, isFreshInstall) {
@@ -319,7 +347,7 @@ function runSeeds(sqlite, seedsDir, isFreshInstall) {
319
347
  const sqlPath = resolve(seedsDir, file);
320
348
  const sqlContent = readFileSync(sqlPath, "utf-8");
321
349
  sqlite.exec(sqlContent);
322
- console.log(`Seed: ${file} applied`);
350
+ console.error(`Seed: ${file} applied`);
323
351
  }
324
352
  }
325
353
  function compareSemver(a, b) {
@@ -359,17 +387,18 @@ function createDbClient(dbPath) {
359
387
  const seedsDir = resolve2(__dirname, "seeds");
360
388
  runMigrations(sqlite, migrationsDir);
361
389
  runSeeds(sqlite, seedsDir, isFreshInstall);
362
- createEmbeddingsTable(sqlite);
363
- createPlansEmbeddingsTable(sqlite);
390
+ const dims = Number(process.env.EMBEDDING_DIMENSIONS) || DEFAULT_EMBEDDING_DIMENSIONS;
391
+ createEmbeddingsTable(sqlite, dims);
392
+ createPlansEmbeddingsTable(sqlite, dims);
364
393
  const db = drizzle(sqlite, { schema: schema_exports });
365
394
  return { db, sqlite };
366
395
  }
367
- function createPlansEmbeddingsTable(sqlite) {
396
+ function createPlansEmbeddingsTable(sqlite, dimensions = DEFAULT_EMBEDDING_DIMENSIONS) {
368
397
  try {
369
398
  sqlite.exec(`
370
399
  CREATE VIRTUAL TABLE IF NOT EXISTS plans_embeddings USING vec0(
371
400
  id TEXT PRIMARY KEY,
372
- embedding float[384] distance_metric=cosine
401
+ embedding float[${dimensions}] distance_metric=cosine
373
402
  );
374
403
  `);
375
404
  } catch {
@@ -525,7 +554,7 @@ var KnowledgeRepository = class {
525
554
  const now = (/* @__PURE__ */ new Date()).toISOString();
526
555
  this.sqlite.prepare("INSERT INTO plans (id, title, content, tags, scope, status, source, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)").run(id, input.title, input.content, JSON.stringify(input.tags), input.scope, input.status ?? "draft", input.source, now, now);
527
556
  try {
528
- this.sqlite.prepare("INSERT INTO plans_embeddings (id, embedding) VALUES (?, ?)").run(id, new Float32Array(input.embedding).buffer);
557
+ insertPlanEmbedding(this.sqlite, id, input.embedding);
529
558
  } catch {
530
559
  }
531
560
  return this.getPlanById(id);
@@ -554,7 +583,7 @@ var KnowledgeRepository = class {
554
583
  deletePlan(id) {
555
584
  const result = this.sqlite.prepare("DELETE FROM plans WHERE id = ?").run(id);
556
585
  try {
557
- this.sqlite.prepare("DELETE FROM plans_embeddings WHERE id = ?").run(id);
586
+ deletePlanEmbedding(this.sqlite, id);
558
587
  } catch {
559
588
  }
560
589
  return result.changes > 0;
@@ -562,11 +591,62 @@ var KnowledgeRepository = class {
562
591
  listAllPlans() {
563
592
  return this.sqlite.prepare("SELECT * FROM plans ORDER BY created_at DESC").all();
564
593
  }
565
- listPlans(limit = 20, status) {
594
+ listPlans(limit = 20, status, scope) {
595
+ const conditions = [];
596
+ const params = [];
566
597
  if (status) {
567
- return this.sqlite.prepare("SELECT * FROM plans WHERE status = ? ORDER BY created_at DESC LIMIT ?").all(status, limit);
598
+ conditions.push("status = ?");
599
+ params.push(status);
600
+ }
601
+ if (scope) {
602
+ conditions.push("scope = ?");
603
+ params.push(scope);
568
604
  }
569
- return this.sqlite.prepare("SELECT * FROM plans ORDER BY created_at DESC LIMIT ?").all(limit);
605
+ const where = conditions.length ? "WHERE " + conditions.join(" AND ") : "";
606
+ params.push(limit);
607
+ return this.sqlite.prepare(`SELECT * FROM plans ${where} ORDER BY created_at DESC LIMIT ?`).all(...params);
608
+ }
609
+ findSimilarActivePlans(embedding, scope, threshold = 0.5) {
610
+ try {
611
+ const candidates = this.sqlite.prepare("SELECT id FROM plans WHERE scope = ? AND status IN ('draft', 'active')").all(scope);
612
+ if (!candidates.length)
613
+ return [];
614
+ const ids = candidates.map((c) => c.id);
615
+ const placeholders = ids.map(() => "?").join(",");
616
+ const rows = this.sqlite.prepare(`SELECT id, embedding FROM plans_embeddings WHERE id IN (${placeholders})`).all(...ids);
617
+ if (!rows.length)
618
+ return [];
619
+ const queryVec = new Float32Array(embedding);
620
+ return rows.map((row) => {
621
+ const vec = new Float32Array(row.embedding.buffer, row.embedding.byteOffset, row.embedding.byteLength / 4);
622
+ const sim = cosineSimilarity(queryVec, vec);
623
+ return { id: row.id, similarity: sim };
624
+ }).filter((r) => r.similarity >= threshold).sort((a, b) => b.similarity - a.similarity).map((r) => ({
625
+ plan: this.sqlite.prepare("SELECT * FROM plans WHERE id = ?").get(r.id),
626
+ similarity: r.similarity
627
+ }));
628
+ } catch {
629
+ return [];
630
+ }
631
+ }
632
+ archiveStaleDrafts(maxAgeHours = 24) {
633
+ const cutoff = new Date(Date.now() - maxAgeHours * 60 * 60 * 1e3).toISOString();
634
+ return this.sqlite.prepare("UPDATE plans SET status = 'archived', updated_at = ? WHERE status = 'draft' AND updated_at < ?").run((/* @__PURE__ */ new Date()).toISOString(), cutoff).changes;
635
+ }
636
+ deletePlanTasks(planId) {
637
+ return this.sqlite.prepare("DELETE FROM plan_tasks WHERE plan_id = ?").run(planId).changes;
638
+ }
639
+ updatePlanEmbeddingById(id, embedding) {
640
+ try {
641
+ updatePlanEmbedding(this.sqlite, id, embedding);
642
+ } catch {
643
+ }
644
+ }
645
+ insertEmbeddingById(id, embedding) {
646
+ insertEmbedding(this.sqlite, id, embedding);
647
+ }
648
+ insertPlanEmbeddingById(id, embedding) {
649
+ insertPlanEmbedding(this.sqlite, id, embedding);
570
650
  }
571
651
  // ─── Plan Relations ─────────────────────────────────────────
572
652
  addPlanRelation(planId, knowledgeId, relationType) {
@@ -659,6 +739,17 @@ var KnowledgeRepository = class {
659
739
  logOperation(operation) {
660
740
  this.sqlite.prepare("INSERT INTO operations_log (operation, created_at) VALUES (?, ?)").run(operation, (/* @__PURE__ */ new Date()).toISOString());
661
741
  }
742
+ logOperationBatch(operation, count) {
743
+ if (count <= 0)
744
+ return;
745
+ const now = (/* @__PURE__ */ new Date()).toISOString();
746
+ const stmt = this.sqlite.prepare("INSERT INTO operations_log (operation, created_at) VALUES (?, ?)");
747
+ const insertMany = this.sqlite.transaction((n) => {
748
+ for (let i = 0; i < n; i++)
749
+ stmt.run(operation, now);
750
+ });
751
+ insertMany(count);
752
+ }
662
753
  getOperationCounts() {
663
754
  const now = /* @__PURE__ */ new Date();
664
755
  const oneHourAgo = new Date(now.getTime() - 60 * 60 * 1e3).toISOString();
@@ -708,33 +799,85 @@ var KnowledgeRepository = class {
708
799
  const cutoff = new Date(Date.now() - 7 * 24 * 60 * 60 * 1e3).toISOString();
709
800
  return this.sqlite.prepare("DELETE FROM operations_log WHERE created_at < ?").run(cutoff).changes;
710
801
  }
802
+ /**
803
+ * Delete embeddings for completed/archived plans older than maxAgeDays.
804
+ * These plans are never searched semantically (only draft/active are).
805
+ */
806
+ cleanupCompletedPlanEmbeddings(maxAgeDays = 30) {
807
+ const cutoff = new Date(Date.now() - maxAgeDays * 24 * 60 * 60 * 1e3).toISOString();
808
+ const oldPlans = this.sqlite.prepare(`SELECT id FROM plans WHERE status IN ('completed', 'archived') AND updated_at < ?`).all(cutoff);
809
+ if (oldPlans.length === 0)
810
+ return 0;
811
+ let removed = 0;
812
+ const deleteStmt = this.sqlite.prepare("DELETE FROM plans_embeddings WHERE id = ?");
813
+ for (const plan of oldPlans) {
814
+ const result = deleteStmt.run(plan.id);
815
+ removed += result.changes;
816
+ }
817
+ return removed;
818
+ }
711
819
  };
820
+ function cosineSimilarity(a, b) {
821
+ let dot = 0, magA = 0, magB = 0;
822
+ for (let i = 0; i < a.length; i++) {
823
+ dot += a[i] * b[i];
824
+ magA += a[i] * a[i];
825
+ magB += b[i] * b[i];
826
+ }
827
+ const denom = Math.sqrt(magA) * Math.sqrt(magB);
828
+ return denom === 0 ? 0 : dot / denom;
829
+ }
712
830
 
713
831
  // ../../packages/core/dist/services/knowledge.service.js
714
832
  var KnowledgeService = class {
715
833
  repository;
716
834
  embeddingProvider;
835
+ lastArchiveRunMs = 0;
717
836
  constructor(repository, embeddingProvider) {
718
837
  this.repository = repository;
719
838
  this.embeddingProvider = embeddingProvider;
720
839
  }
721
- logOp(op) {
840
+ logOp(op, count = 1) {
722
841
  try {
723
- this.repository.logOperation(op);
842
+ if (count <= 1)
843
+ this.repository.logOperation(op);
844
+ else
845
+ this.repository.logOperationBatch(op, count);
724
846
  } catch {
725
847
  }
726
848
  }
849
+ buildEmbeddingText(title, content, tags) {
850
+ return `${title} ${content} ${tags.join(" ")}`;
851
+ }
727
852
  async add(input) {
728
- const tagsText = input.tags.join(" ");
729
- const embedding = await this.embeddingProvider.embed(tagsText);
730
- const entry = await this.repository.create({ ...input, embedding });
853
+ const { skipDedup, ...rest } = input;
854
+ const embeddingText = this.buildEmbeddingText(rest.title, rest.content, rest.tags);
855
+ const embedding = await this.embeddingProvider.embed(embeddingText);
856
+ if (!skipDedup) {
857
+ try {
858
+ const similar = await this.repository.searchBySimilarity(embedding, {
859
+ scope: rest.scope,
860
+ type: rest.type,
861
+ limit: 1,
862
+ threshold: 0.85
863
+ });
864
+ if (similar.length > 0) {
865
+ const existing = similar[0].entry;
866
+ const updated = await this.repository.update(existing.id, { ...rest, embedding });
867
+ this.logOp("write");
868
+ return { ...this.toKnowledgeEntry(updated), deduplicated: true };
869
+ }
870
+ } catch {
871
+ }
872
+ }
873
+ const entry = await this.repository.create({ ...rest, embedding });
731
874
  this.logOp("write");
732
875
  return this.toKnowledgeEntry(entry);
733
876
  }
734
877
  async search(query, options) {
735
878
  const queryEmbedding = await this.embeddingProvider.embed(query);
736
879
  const results = await this.repository.searchBySimilarity(queryEmbedding, options);
737
- this.logOp("read");
880
+ this.logOp("read", results.length);
738
881
  return results.map((r) => ({
739
882
  entry: this.toKnowledgeEntry(r.entry),
740
883
  similarity: r.similarity
@@ -746,8 +889,12 @@ var KnowledgeService = class {
746
889
  }
747
890
  async update(id, updates) {
748
891
  let embedding;
749
- if (updates.tags && updates.tags.length > 0) {
750
- embedding = await this.embeddingProvider.embed(updates.tags.join(" "));
892
+ if (updates.content || updates.title || updates.tags) {
893
+ const current = await this.repository.findById(id);
894
+ const title = updates.title || current?.title || "";
895
+ const content = updates.content || current?.content || "";
896
+ const tags = updates.tags || (current?.tags ?? []);
897
+ embedding = await this.embeddingProvider.embed(this.buildEmbeddingText(title, content, tags));
751
898
  }
752
899
  const entry = await this.repository.update(id, { ...updates, embedding });
753
900
  if (entry)
@@ -764,6 +911,43 @@ var KnowledgeService = class {
764
911
  const entries = await this.repository.listAll();
765
912
  return entries.map((e) => this.toKnowledgeEntry(e));
766
913
  }
914
+ /**
915
+ * Re-embed all knowledge entries and plans with the current embedding provider.
916
+ * Used when switching embedding models (e.g. all-minilm 384d → nomic-embed-text 768d).
917
+ * Assumes vec tables have already been dropped and recreated with new dimensions.
918
+ */
919
+ async reembedAll() {
920
+ let count = 0;
921
+ const entries = await this.repository.listAll();
922
+ for (const entry of entries) {
923
+ try {
924
+ const tags = Array.isArray(entry.tags) ? entry.tags : JSON.parse(entry.tags ?? "[]");
925
+ const text2 = this.buildEmbeddingText(entry.title, entry.content, tags);
926
+ const embedding = await this.embeddingProvider.embed(text2);
927
+ try {
928
+ this.repository.insertEmbeddingById(entry.id, embedding);
929
+ } catch {
930
+ }
931
+ count++;
932
+ } catch (e) {
933
+ console.warn(`[CogniStore] Re-embed failed for entry ${entry.id}:`, e);
934
+ }
935
+ }
936
+ const plans = this.repository.listAllPlans();
937
+ for (const plan of plans) {
938
+ try {
939
+ const embedding = await this.embeddingProvider.embed(`${plan.title} ${plan.content}`);
940
+ try {
941
+ this.repository.insertPlanEmbeddingById(plan.id, embedding);
942
+ } catch {
943
+ }
944
+ count++;
945
+ } catch (e) {
946
+ console.warn(`[CogniStore] Re-embed failed for plan ${plan.id}:`, e);
947
+ }
948
+ }
949
+ return count;
950
+ }
767
951
  async listScopes() {
768
952
  return this.repository.listScopes();
769
953
  }
@@ -847,8 +1031,50 @@ var KnowledgeService = class {
847
1031
  }
848
1032
  // ─── Plans (separate entity) ────────────────────────────────
849
1033
  async createPlan(input) {
850
- const { tasks, ...planInput } = input;
851
- const embedding = await this.embeddingProvider.embed(input.tags.join(" "));
1034
+ const { tasks, skipDedup, ...planInput } = input;
1035
+ const embedding = await this.embeddingProvider.embed(`${input.title} ${input.content}`);
1036
+ const now = Date.now();
1037
+ if (now - this.lastArchiveRunMs > 36e5) {
1038
+ try {
1039
+ this.repository.archiveStaleDrafts(24);
1040
+ this.lastArchiveRunMs = now;
1041
+ } catch {
1042
+ }
1043
+ }
1044
+ const similarPlans = skipDedup ? [] : this.repository.findSimilarActivePlans(embedding, input.scope, 0.5);
1045
+ if (similarPlans.length > 0) {
1046
+ const { plan: existingRow } = similarPlans[0];
1047
+ const isActive = existingRow.status === "active";
1048
+ if (isActive) {
1049
+ if (tasks && tasks.length > 0) {
1050
+ for (const task of tasks) {
1051
+ this.repository.createPlanTask({ planId: existingRow.id, description: task.description, priority: task.priority });
1052
+ }
1053
+ }
1054
+ const plan2 = this.toPlan(existingRow);
1055
+ return { ...plan2, deduplicated: true, deduplicatedAction: "tasks_added_to_active_plan" };
1056
+ } else {
1057
+ this.repository.updatePlan(existingRow.id, {
1058
+ title: input.title,
1059
+ content: input.content,
1060
+ tags: input.tags,
1061
+ source: input.source
1062
+ });
1063
+ if (tasks && tasks.length > 0) {
1064
+ this.repository.deletePlanTasks(existingRow.id);
1065
+ for (let i = 0; i < tasks.length; i++) {
1066
+ this.repository.createPlanTask({ planId: existingRow.id, description: tasks[i].description, priority: tasks[i].priority, position: i });
1067
+ }
1068
+ }
1069
+ try {
1070
+ this.repository.updatePlanEmbeddingById(existingRow.id, embedding);
1071
+ } catch {
1072
+ }
1073
+ const updated = this.repository.getPlanById(existingRow.id);
1074
+ const plan2 = this.toPlan(updated);
1075
+ return { ...plan2, deduplicated: true, deduplicatedAction: "draft_plan_updated" };
1076
+ }
1077
+ }
852
1078
  const row = this.repository.createPlan({ ...planInput, embedding });
853
1079
  const plan = this.toPlan(row);
854
1080
  if (tasks && tasks.length > 0) {
@@ -888,10 +1114,13 @@ var KnowledgeService = class {
888
1114
  const rows = this.repository.listAllPlans();
889
1115
  return rows.map((r) => this.toPlan(r));
890
1116
  }
891
- listPlans(limit = 20, status) {
892
- const rows = this.repository.listPlans(limit, status);
1117
+ listPlans(limit = 20, status, scope) {
1118
+ const rows = this.repository.listPlans(limit, status, scope);
893
1119
  return rows.map((r) => this.toPlan(r));
894
1120
  }
1121
+ archiveStaleDrafts(maxAgeHours = 24) {
1122
+ return this.repository.archiveStaleDrafts(maxAgeHours);
1123
+ }
895
1124
  async importPlans(plans) {
896
1125
  let imported = 0;
897
1126
  let skipped = 0;
@@ -1007,6 +1236,9 @@ var KnowledgeService = class {
1007
1236
  cleanupOldOperations() {
1008
1237
  return this.repository.cleanupOldOperations();
1009
1238
  }
1239
+ cleanupCompletedPlanEmbeddings(maxAgeDays = 30) {
1240
+ return this.repository.cleanupCompletedPlanEmbeddings(maxAgeDays);
1241
+ }
1010
1242
  // ─── Converters ─────────────────────────────────────────────
1011
1243
  toKnowledgeEntry(row) {
1012
1244
  return {
@@ -1061,16 +1293,25 @@ var OllamaEmbeddingClient = class {
1061
1293
  model;
1062
1294
  dimensions;
1063
1295
  maxRetries;
1296
+ maxInputChars;
1064
1297
  constructor(config) {
1065
1298
  this.host = config?.host ?? (process.env.OLLAMA_HOST ?? DEFAULT_OLLAMA_HOST);
1066
1299
  this.model = config?.model ?? (process.env.OLLAMA_MODEL ?? DEFAULT_EMBEDDING_MODEL);
1067
1300
  this.dimensions = config?.dimensions ?? (Number(process.env.EMBEDDING_DIMENSIONS) || DEFAULT_EMBEDDING_DIMENSIONS);
1068
1301
  this.maxRetries = config?.maxRetries ?? 3;
1302
+ this.maxInputChars = config?.maxInputChars ?? 2e3;
1303
+ }
1304
+ truncateText(text2, maxChars) {
1305
+ if (text2.length <= maxChars)
1306
+ return text2;
1307
+ const truncated = text2.slice(0, maxChars);
1308
+ const lastSpace = truncated.lastIndexOf(" ");
1309
+ return lastSpace > maxChars * 0.5 ? truncated.slice(0, lastSpace) : truncated;
1069
1310
  }
1070
1311
  async embed(text2) {
1071
1312
  const body = {
1072
1313
  model: this.model,
1073
- prompt: text2
1314
+ prompt: this.truncateText(text2, this.maxInputChars)
1074
1315
  };
1075
1316
  const response = await this.fetchWithRetry(`${this.host}/api/embeddings`, {
1076
1317
  method: "POST",
@@ -1085,8 +1326,11 @@ var OllamaEmbeddingClient = class {
1085
1326
  if (!data.embedding || !Array.isArray(data.embedding)) {
1086
1327
  throw new Error("Invalid embedding response from Ollama");
1087
1328
  }
1088
- if (data.embedding.length !== this.dimensions) {
1089
- throw new Error(`Embedding dimension mismatch: expected ${this.dimensions}, got ${data.embedding.length}. Check that OLLAMA_MODEL and EMBEDDING_DIMENSIONS are compatible.`);
1329
+ if (data.embedding.length < this.dimensions) {
1330
+ throw new Error(`Embedding dimension mismatch: expected at least ${this.dimensions}, got ${data.embedding.length}. Check that OLLAMA_MODEL and EMBEDDING_DIMENSIONS are compatible.`);
1331
+ }
1332
+ if (data.embedding.length > this.dimensions) {
1333
+ return this.truncateAndNormalize(data.embedding, this.dimensions);
1090
1334
  }
1091
1335
  return data.embedding;
1092
1336
  }
@@ -1149,6 +1393,17 @@ var OllamaEmbeddingClient = class {
1149
1393
  dimensions: this.dimensions
1150
1394
  };
1151
1395
  }
1396
+ /**
1397
+ * Matryoshka truncation: slice to first targetDims dimensions, then L2-normalize.
1398
+ * L2 normalization is critical for cosine similarity quality after truncation.
1399
+ */
1400
+ truncateAndNormalize(embedding, targetDims) {
1401
+ const truncated = embedding.slice(0, targetDims);
1402
+ const norm = Math.sqrt(truncated.reduce((sum, v) => sum + v * v, 0));
1403
+ if (norm === 0)
1404
+ return truncated;
1405
+ return truncated.map((v) => v / norm);
1406
+ }
1152
1407
  async fetchWithRetry(url, init) {
1153
1408
  let lastError;
1154
1409
  for (let attempt = 0; attempt < this.maxRetries; attempt++) {
@@ -1263,6 +1518,7 @@ var KnowledgeSDK = class {
1263
1518
  } catch (error) {
1264
1519
  throw new EmbeddingError(`Failed to ensure embedding model: ${error instanceof Error ? error.message : String(error)}`);
1265
1520
  }
1521
+ await this.detectAndMigrateDimensions();
1266
1522
  const repository = new KnowledgeRepository(this.db, this.sqlite);
1267
1523
  this.service = new KnowledgeService(repository, this.ollamaClient);
1268
1524
  this.initialized = true;
@@ -1418,13 +1674,13 @@ var KnowledgeSDK = class {
1418
1674
  // ─── Plans (separate entity) ─────────────────────────────────
1419
1675
  async createPlan(input) {
1420
1676
  this.ensureInitialized();
1421
- const { relatedKnowledgeIds, tasks, ...rest } = input;
1677
+ const { relatedKnowledgeIds, ...rest } = input;
1422
1678
  const parsed = createPlanSchema.safeParse(rest);
1423
1679
  if (!parsed.success) {
1424
1680
  throw new ValidationError(`Invalid plan input: ${parsed.error.message}`);
1425
1681
  }
1426
1682
  try {
1427
- const plan = await this.service.createPlan({ ...parsed.data, tasks });
1683
+ const plan = await this.service.createPlan(parsed.data);
1428
1684
  if (relatedKnowledgeIds) {
1429
1685
  for (const kid of relatedKnowledgeIds) {
1430
1686
  try {
@@ -1450,9 +1706,9 @@ var KnowledgeSDK = class {
1450
1706
  this.ensureInitialized();
1451
1707
  return this.service.deletePlan(id);
1452
1708
  }
1453
- listPlans(limit = 20, status) {
1709
+ listPlans(limit = 20, status, scope) {
1454
1710
  this.ensureInitialized();
1455
- return this.service.listPlans(limit, status);
1711
+ return this.service.listPlans(limit, status, scope);
1456
1712
  }
1457
1713
  async addPlanRelation(planId, knowledgeId, relationType) {
1458
1714
  this.ensureInitialized();
@@ -1501,6 +1757,14 @@ var KnowledgeSDK = class {
1501
1757
  this.ensureInitialized();
1502
1758
  return this.service.getPlanTaskStats();
1503
1759
  }
1760
+ /**
1761
+ * Re-embed all knowledge entries and plans with the current embedding model.
1762
+ * Used during upgrade when switching embedding models (dimensions change).
1763
+ */
1764
+ async reembedAll() {
1765
+ this.ensureInitialized();
1766
+ return this.service.reembedAll();
1767
+ }
1504
1768
  // ─── Operations ─────────────────────────────────────────────
1505
1769
  getOperationCounts() {
1506
1770
  this.ensureInitialized();
@@ -1515,6 +1779,11 @@ var KnowledgeSDK = class {
1515
1779
  return 0;
1516
1780
  return this.service.cleanupOldOperations();
1517
1781
  }
1782
+ cleanupCompletedPlanEmbeddings(maxAgeDays = 30) {
1783
+ if (!this.initialized || !this.service)
1784
+ return 0;
1785
+ return this.service.cleanupCompletedPlanEmbeddings(maxAgeDays);
1786
+ }
1518
1787
  async healthCheck() {
1519
1788
  const ollamaHealth = await checkOllamaHealth(this.ollamaClient);
1520
1789
  let dbConnected = false;
@@ -1563,6 +1832,14 @@ var KnowledgeSDK = class {
1563
1832
  throw this.wrapError(error, "Failed to cleanup database");
1564
1833
  }
1565
1834
  }
1835
+ /**
1836
+ * Run a passive WAL checkpoint to keep the WAL file small.
1837
+ * PASSIVE mode does not block readers or writers.
1838
+ */
1839
+ walCheckpoint() {
1840
+ this.ensureInitialized();
1841
+ this.sqlite.pragma("wal_checkpoint(PASSIVE)");
1842
+ }
1566
1843
  ensureInitialized() {
1567
1844
  if (!this.initialized || !this.service) {
1568
1845
  throw new ConnectionError("SDK not initialized. Call initialize() first.");
@@ -1579,6 +1856,38 @@ var KnowledgeSDK = class {
1579
1856
  this.db = null;
1580
1857
  this.service = null;
1581
1858
  }
1859
+ /**
1860
+ * Detect if stored embeddings have different dimensions than config.
1861
+ * If mismatch found (e.g., 768→256 Matryoshka migration), drop vec tables,
1862
+ * recreate at new dimensions, and re-embed all entries.
1863
+ */
1864
+ async detectAndMigrateDimensions() {
1865
+ const targetDims = this.config.ollama.dimensions;
1866
+ try {
1867
+ const sampleRow = this.sqlite.prepare("SELECT embedding FROM knowledge_embeddings LIMIT 1").get();
1868
+ if (!sampleRow)
1869
+ return;
1870
+ const currentDims = sampleRow.embedding.byteLength / 4;
1871
+ if (currentDims === targetDims)
1872
+ return;
1873
+ console.error(`[CogniStore] Embedding dimension mismatch: DB has ${currentDims}d, config wants ${targetDims}d. Re-embedding all entries...`);
1874
+ this.sqlite.exec("DROP TABLE IF EXISTS knowledge_embeddings");
1875
+ this.sqlite.exec("DROP TABLE IF EXISTS plans_embeddings");
1876
+ createEmbeddingsTable(this.sqlite, targetDims);
1877
+ this.sqlite.exec(`
1878
+ CREATE VIRTUAL TABLE IF NOT EXISTS plans_embeddings USING vec0(
1879
+ id TEXT PRIMARY KEY,
1880
+ embedding float[${targetDims}] distance_metric=cosine
1881
+ )
1882
+ `);
1883
+ const repository = new KnowledgeRepository(this.db, this.sqlite);
1884
+ const tempService = new KnowledgeService(repository, this.ollamaClient);
1885
+ const count = await tempService.reembedAll();
1886
+ console.error(`[CogniStore] Re-embedded ${count} entries at ${targetDims} dimensions`);
1887
+ } catch (error) {
1888
+ console.error(`[CogniStore] Dimension migration failed: ${error instanceof Error ? error.message : String(error)}`);
1889
+ }
1890
+ }
1582
1891
  wrapError(error, context) {
1583
1892
  if (error instanceof Error && error.name.endsWith("Error")) {
1584
1893
  return error;
@@ -1685,19 +1994,26 @@ function createServer(sdk) {
1685
1994
  });
1686
1995
  lastSearchResultIds = results.map((r) => r.entry.id);
1687
1996
  const response = { results };
1688
- try {
1689
- const activePlans = sdk.listPlans(1, "active");
1690
- const draftPlans = sdk.listPlans(1, "draft");
1691
- const currentPlan = activePlans[0] || draftPlans[0];
1692
- if (currentPlan) {
1693
- response.activePlan = {
1694
- id: currentPlan.id,
1695
- title: currentPlan.title,
1696
- status: currentPlan.status,
1697
- hint: "Pass this planId to addKnowledge calls for output linking."
1698
- };
1997
+ if (params.scope) {
1998
+ try {
1999
+ const activePlans = sdk.listPlans(1, "active", params.scope);
2000
+ const draftPlans = sdk.listPlans(1, "draft", params.scope);
2001
+ const currentPlan = activePlans[0] || draftPlans[0];
2002
+ if (currentPlan) {
2003
+ const tasks = sdk.listPlanTasks(currentPlan.id);
2004
+ const completedTasks = tasks.filter((t) => t.status === "completed").length;
2005
+ response.activePlan = {
2006
+ id: currentPlan.id,
2007
+ title: currentPlan.title,
2008
+ status: currentPlan.status,
2009
+ scope: currentPlan.scope,
2010
+ taskCount: tasks.length,
2011
+ completedTasks,
2012
+ hint: `You have an active plan (${completedTasks}/${tasks.length} tasks done). Use updatePlan(planId, ...) to modify it or updatePlanTask() to track progress. Do NOT call createPlan() \u2014 it will auto-deduplicate into this plan.`
2013
+ };
2014
+ }
2015
+ } catch {
1699
2016
  }
1700
- } catch {
1701
2017
  }
1702
2018
  return { content: [{ type: "text", text: JSON.stringify(response, null, 2) }] };
1703
2019
  }
@@ -1795,9 +2111,12 @@ function createServer(sdk) {
1795
2111
  tasks: params.tasks
1796
2112
  });
1797
2113
  lastSearchResultIds = [];
2114
+ const deduplicated = result.deduplicated === true;
2115
+ const deduplicatedAction = result.deduplicatedAction;
2116
+ const reminder = deduplicated ? `Existing plan "${result.title}" was reused (${deduplicatedAction === "tasks_added_to_active_plan" ? "new tasks added to active plan" : "draft plan updated"}). Plan ID: "${result.id}". Pass this planId to addKnowledge calls.` : `Your plan ID is "${result.id}". Pass planId: "${result.id}" to every addKnowledge call for output linking. Plan auto-activates when you start the first task.`;
1798
2117
  const response = {
1799
2118
  ...result,
1800
- reminder: `Your plan ID is "${result.id}". Pass planId: "${result.id}" to every addKnowledge call for output linking. Plan auto-activates when you start the first task.`
2119
+ reminder
1801
2120
  };
1802
2121
  return { content: [{ type: "text", text: JSON.stringify(response, null, 2) }] };
1803
2122
  }
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "@cognistore/mcp-server",
3
- "version": "1.0.9",
3
+ "version": "1.0.13",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "MCP server for CogniStore — integrates with Claude Code and GitHub Copilot",
7
7
  "license": "BUSL-1.1",
8
8
  "repository": {
9
9
  "type": "git",
10
- "url": "https://github.com/Sithion/cognistore",
10
+ "url": "git+https://github.com/Sithion/cognistore.git",
11
11
  "directory": "apps/mcp-server"
12
12
  },
13
13
  "main": "./dist/index.js",