@coreyuan/vector-mind 1.0.17 → 1.0.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -12,7 +12,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
12
12
  import { toJsonSchemaCompat } from "@modelcontextprotocol/sdk/server/zod-json-schema-compat.js";
13
13
  import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
14
14
  const SERVER_NAME = "vector-mind";
15
- const SERVER_VERSION = "1.0.17";
15
+ const SERVER_VERSION = "1.0.19";
16
16
  const rootFromEnv = process.env.VECTORMIND_ROOT?.trim() ?? "";
17
17
  const prettyJsonOutput = ["1", "true", "on", "yes"].includes((process.env.VECTORMIND_PRETTY_JSON ?? "").trim().toLowerCase());
18
18
  const debugLogEnabled = ["1", "true", "on", "yes"].includes((process.env.VECTORMIND_DEBUG_LOG ?? "").trim().toLowerCase());
@@ -61,6 +61,36 @@ const PENDING_PRUNE_EVERY = (() => {
61
61
  return 500;
62
62
  return n;
63
63
  })();
64
+ const INDEX_MAX_CODE_BYTES = (() => {
65
+ const raw = process.env.VECTORMIND_INDEX_MAX_CODE_BYTES?.trim();
66
+ if (!raw)
67
+ return 400_000;
68
+ const n = Number.parseInt(raw, 10);
69
+ if (!Number.isFinite(n) || n <= 0)
70
+ return 400_000;
71
+ return n;
72
+ })();
73
+ const INDEX_MAX_DOC_BYTES = (() => {
74
+ const raw = process.env.VECTORMIND_INDEX_MAX_DOC_BYTES?.trim();
75
+ if (!raw)
76
+ return 600_000;
77
+ const n = Number.parseInt(raw, 10);
78
+ if (!Number.isFinite(n) || n <= 0)
79
+ return 600_000;
80
+ return n;
81
+ })();
82
+ const INDEX_SKIP_MINIFIED = (() => {
83
+ const raw = (process.env.VECTORMIND_INDEX_SKIP_MINIFIED ?? "").trim().toLowerCase();
84
+ if (!raw)
85
+ return true;
86
+ return ["1", "true", "on", "yes"].includes(raw);
87
+ })();
88
+ const INDEX_AUTO_PRUNE_IGNORED = (() => {
89
+ const raw = (process.env.VECTORMIND_INDEX_AUTO_PRUNE_IGNORED ?? "").trim().toLowerCase();
90
+ if (!raw)
91
+ return true;
92
+ return ["1", "true", "on", "yes"].includes(raw);
93
+ })();
64
94
  const ROOTS_LIST_TIMEOUT_MS = (() => {
65
95
  const raw = process.env.VECTORMIND_ROOTS_TIMEOUT_MS?.trim();
66
96
  if (!raw)
@@ -99,6 +129,10 @@ let insertChangeLogStmt;
99
129
  let insertMemoryItemStmt;
100
130
  let getMemoryItemByIdStmt;
101
131
  let getRequirementMemoryItemIdStmt;
132
+ let getConventionByKeyStmt;
133
+ let insertConventionStmt;
134
+ let updateConventionByIdStmt;
135
+ let listConventionsStmt;
102
136
  let upsertProjectSummaryStmt;
103
137
  let getProjectSummaryStmt;
104
138
  let listRecentNotesStmt;
@@ -119,6 +153,37 @@ let searchSymbolsStmt;
119
153
  let indexFileSymbolsTx = null;
120
154
  let activitySeq = 0;
121
155
  const activityLog = [];
156
+ function sanitizeForLog(value, depth = 0) {
157
+ if (depth > 4)
158
+ return "[max-depth]";
159
+ if (value === null || value === undefined)
160
+ return value;
161
+ if (typeof value === "string") {
162
+ return value.length > 500 ? `${value.slice(0, 500)}...` : value;
163
+ }
164
+ if (typeof value === "number" || typeof value === "boolean")
165
+ return value;
166
+ if (Array.isArray(value)) {
167
+ const sliced = value.slice(0, 20).map((v) => sanitizeForLog(v, depth + 1));
168
+ return value.length > 20 ? [...sliced, `[+${value.length - 20} more]`] : sliced;
169
+ }
170
+ if (typeof value === "object") {
171
+ const obj = value;
172
+ const keys = Object.keys(obj).slice(0, 40);
173
+ const out = {};
174
+ for (const k of keys)
175
+ out[k] = sanitizeForLog(obj[k], depth + 1);
176
+ if (Object.keys(obj).length > 40)
177
+ out["__more_keys__"] = Object.keys(obj).length - 40;
178
+ return out;
179
+ }
180
+ try {
181
+ return String(value);
182
+ }
183
+ catch {
184
+ return "[unserializable]";
185
+ }
186
+ }
122
187
  function logActivity(type, data) {
123
188
  if (!debugLogEnabled)
124
189
  return;
@@ -127,7 +192,7 @@ function logActivity(type, data) {
127
192
  ts: new Date().toISOString(),
128
193
  type,
129
194
  project_root: projectRoot || "",
130
- data,
195
+ data: sanitizeForLog(data),
131
196
  });
132
197
  while (activityLog.length > debugLogMaxEntries)
133
198
  activityLog.shift();
@@ -143,6 +208,37 @@ function clearActivityLog() {
143
208
  activityLog.length = 0;
144
209
  activitySeq = 0;
145
210
  }
211
+ function summarizeActivityEvent(e) {
212
+ const d = e.data ?? {};
213
+ switch (e.type) {
214
+ case "index_file":
215
+ return `index ${String(d.file_path ?? "")} reason=${String(d.reason ?? "")} symbols=${String(d.symbols ?? "")} chunks=${String(d.chunks ?? "")}`;
216
+ case "remove_file":
217
+ return `remove ${String(d.file_path ?? "")}`;
218
+ case "pending_flush":
219
+ return `pending_flush entries=${String(d.entries ?? "")}`;
220
+ case "pending_prune":
221
+ return `pending_prune ${String(d.before ?? "")}->${String(d.after ?? "")}`;
222
+ case "bootstrap_context":
223
+ return `bootstrap q=${String(d.query ?? "")} pending=${String(d.pending_returned ?? "")}/${String(d.pending_total ?? "")} reqs=${String(d.requirements_returned ?? "")} semantic=${String(d.semantic_mode ?? "")}+${String(d.semantic_matches ?? "")}`;
224
+ case "get_brain_dump":
225
+ return `brain_dump pending=${String(d.pending_returned ?? "")}/${String(d.pending_total ?? "")} reqs=${String(d.requirements_returned ?? "")} notes=${String(d.notes_returned ?? "")}`;
226
+ case "get_pending_changes":
227
+ return `pending_list returned=${String(d.returned ?? "")} total=${String(d.total ?? "")}`;
228
+ case "semantic_search":
229
+ return `semantic_search mode=${String(d.mode ?? "")} q=${String(d.query ?? "")} matches=${String(d.matches ?? "")}`;
230
+ case "query_codebase":
231
+ return `query_codebase q=${String(d.query ?? "")} matches=${String(d.matches ?? "")}`;
232
+ case "start_requirement":
233
+ return `start_requirement #${String(d.req_id ?? "")} ${String(d.title ?? "")}`;
234
+ case "sync_change_intent":
235
+ return `sync_change_intent #${String(d.req_id ?? "")} files=${String(d.files_total ?? "")}`;
236
+ case "complete_requirement":
237
+ return `complete_requirement ${String(d.all_active ? "all_active" : d.req_id ?? "")}`;
238
+ default:
239
+ return e.type;
240
+ }
241
+ }
146
242
  const FTS_TABLE_NAME = "memory_items_fts";
147
243
  let ftsAvailable = false;
148
244
  function isProbablyVscodeInstallDir(dir) {
@@ -340,6 +436,10 @@ const IGNORED_PATH_SEGMENTS = new Set([
340
436
  ".next",
341
437
  ".nuxt",
342
438
  ".svelte-kit",
439
+ ".turbo",
440
+ ".nx",
441
+ ".cache",
442
+ ".parcel-cache",
343
443
  // .NET / VS build artifacts
344
444
  "bin",
345
445
  "obj",
@@ -348,9 +448,11 @@ const IGNORED_PATH_SEGMENTS = new Set([
348
448
  // General build outputs
349
449
  "dist",
350
450
  "build",
451
+ "buildfiles",
351
452
  "out",
352
453
  "target",
353
454
  "coverage",
455
+ "artifacts",
354
456
  // Python caches/venvs
355
457
  "__pycache__",
356
458
  ".pytest_cache",
@@ -399,7 +501,9 @@ function pruneIgnoredPendingChanges() {
399
501
  try {
400
502
  if (!IGNORED_LIKE_PATTERNS.length)
401
503
  return;
402
- const where = IGNORED_LIKE_PATTERNS.map(() => "LOWER(file_path) LIKE ?").join(" OR ");
504
+ const where = IGNORED_LIKE_PATTERNS
505
+ .map(() => "LOWER(REPLACE(file_path, '\\\\', '/')) LIKE ?")
506
+ .join(" OR ");
403
507
  db.prepare(`DELETE FROM pending_changes WHERE ${where}`).run(...IGNORED_LIKE_PATTERNS);
404
508
  }
405
509
  catch (err) {
@@ -432,6 +536,102 @@ function prunePendingChanges() {
432
536
  console.error("[vectormind] prune pending_changes failed:", err);
433
537
  }
434
538
  }
539
+ function pruneIgnoredIndexesByPathPatterns() {
540
+ if (!db)
541
+ return { chunks_deleted: 0, symbols_deleted: 0 };
542
+ try {
543
+ if (!IGNORED_LIKE_PATTERNS.length)
544
+ return { chunks_deleted: 0, symbols_deleted: 0 };
545
+ const where = IGNORED_LIKE_PATTERNS
546
+ .map(() => "LOWER(REPLACE(file_path, '\\\\', '/')) LIKE ?")
547
+ .join(" OR ");
548
+ const chunksDeleted = db
549
+ .prepare(`DELETE FROM memory_items
550
+ WHERE file_path IS NOT NULL
551
+ AND (kind = 'code_chunk' OR kind = 'doc_chunk')
552
+ AND (${where})`)
553
+ .run(...IGNORED_LIKE_PATTERNS).changes;
554
+ const symbolsDeleted = db
555
+ .prepare(`DELETE FROM symbols
556
+ WHERE file_path IS NOT NULL
557
+ AND (${where})`)
558
+ .run(...IGNORED_LIKE_PATTERNS).changes;
559
+ if (chunksDeleted || symbolsDeleted) {
560
+ logActivity("index_prune", {
561
+ reason: "ignored_paths",
562
+ chunks_deleted: chunksDeleted,
563
+ symbols_deleted: symbolsDeleted,
564
+ });
565
+ }
566
+ return { chunks_deleted: chunksDeleted, symbols_deleted: symbolsDeleted };
567
+ }
568
+ catch (err) {
569
+ console.error("[vectormind] prune indexes failed:", err);
570
+ return { chunks_deleted: 0, symbols_deleted: 0 };
571
+ }
572
+ }
573
+ function pruneFilenameNoiseIndexes() {
574
+ if (!db)
575
+ return { chunks_deleted: 0, symbols_deleted: 0 };
576
+ const suffixes = [
577
+ ".min.js",
578
+ ".min.css",
579
+ ".bundle.js",
580
+ ".bundle.css",
581
+ ".chunk.js",
582
+ ".chunk.css",
583
+ ];
584
+ const baseNames = [
585
+ "package-lock.json",
586
+ "pnpm-lock.yaml",
587
+ "yarn.lock",
588
+ "bun.lockb",
589
+ "cargo.lock",
590
+ "composer.lock",
591
+ ];
592
+ try {
593
+ const suffixWhere = suffixes.map(() => "LOWER(file_path) LIKE ?").join(" OR ");
594
+ const baseWhere = baseNames.map(() => "LOWER(file_path) LIKE ?").join(" OR ");
595
+ const suffixArgs = suffixes.map((s) => `%${s}`);
596
+ const baseArgs = baseNames.map((n) => `%/${n}`);
597
+ const whereParts = [];
598
+ const args = [];
599
+ if (suffixWhere) {
600
+ whereParts.push(`(${suffixWhere})`);
601
+ args.push(...suffixArgs);
602
+ }
603
+ if (baseWhere) {
604
+ whereParts.push(`(${baseWhere})`);
605
+ args.push(...baseArgs);
606
+ }
607
+ if (!whereParts.length)
608
+ return { chunks_deleted: 0, symbols_deleted: 0 };
609
+ const where = whereParts.join(" OR ");
610
+ const chunksDeleted = db
611
+ .prepare(`DELETE FROM memory_items
612
+ WHERE file_path IS NOT NULL
613
+ AND (kind = 'code_chunk' OR kind = 'doc_chunk')
614
+ AND (${where})`)
615
+ .run(...args).changes;
616
+ const symbolsDeleted = db
617
+ .prepare(`DELETE FROM symbols
618
+ WHERE file_path IS NOT NULL
619
+ AND (${where})`)
620
+ .run(...args).changes;
621
+ if (chunksDeleted || symbolsDeleted) {
622
+ logActivity("index_prune", {
623
+ reason: "filename_noise",
624
+ chunks_deleted: chunksDeleted,
625
+ symbols_deleted: symbolsDeleted,
626
+ });
627
+ }
628
+ return { chunks_deleted: chunksDeleted, symbols_deleted: symbolsDeleted };
629
+ }
630
+ catch (err) {
631
+ console.error("[vectormind] prune filename noise failed:", err);
632
+ return { chunks_deleted: 0, symbols_deleted: 0 };
633
+ }
634
+ }
435
635
  function shouldIgnorePath(inputPath) {
436
636
  const normalizedAbs = path.resolve(inputPath);
437
637
  const rel = path.relative(projectRoot, normalizedAbs);
@@ -449,6 +649,8 @@ function shouldIgnorePath(inputPath) {
449
649
  return false;
450
650
  }
451
651
  function isSymbolIndexableFile(filePath) {
652
+ if (shouldIgnoreContentFile(filePath))
653
+ return false;
452
654
  const ext = path.extname(filePath).toLowerCase();
453
655
  const allowed = new Set([
454
656
  ".ts",
@@ -485,6 +687,56 @@ function shouldIgnoreContentFile(filePath) {
485
687
  return true;
486
688
  if (base.endsWith(".min.js") || base.endsWith(".min.css"))
487
689
  return true;
690
+ if (base.endsWith(".bundle.js") || base.endsWith(".bundle.css"))
691
+ return true;
692
+ if (base.endsWith(".chunk.js") || base.endsWith(".chunk.css"))
693
+ return true;
694
+ return false;
695
+ }
696
+ function looksLikeGeneratedFile(content) {
697
+ const head = content.slice(0, 4000).toLowerCase();
698
+ if (head.includes("@generated"))
699
+ return true;
700
+ if (head.includes("do not edit") && (head.includes("generated") || head.includes("auto-generated"))) {
701
+ return true;
702
+ }
703
+ if (head.includes("this file was generated") && head.includes("do not edit"))
704
+ return true;
705
+ return false;
706
+ }
707
+ function looksLikeMinifiedBundle(content) {
708
+ if (content.length < 30_000)
709
+ return false;
710
+ let lines = 1;
711
+ let currentLen = 0;
712
+ let maxLineLen = 0;
713
+ let longLines = 0;
714
+ for (let i = 0; i < content.length; i++) {
715
+ const code = content.charCodeAt(i);
716
+ if (code === 10 /* \\n */) {
717
+ if (currentLen > maxLineLen)
718
+ maxLineLen = currentLen;
719
+ if (currentLen >= 800)
720
+ longLines += 1;
721
+ currentLen = 0;
722
+ lines += 1;
723
+ continue;
724
+ }
725
+ currentLen += 1;
726
+ }
727
+ if (currentLen > maxLineLen)
728
+ maxLineLen = currentLen;
729
+ if (currentLen >= 800)
730
+ longLines += 1;
731
+ const avgLineLen = content.length / Math.max(1, lines);
732
+ if (lines <= 2 && maxLineLen >= 2000)
733
+ return true;
734
+ if (maxLineLen >= 6000)
735
+ return true;
736
+ if (avgLineLen >= 900)
737
+ return true;
738
+ if (lines <= 10 && longLines >= Math.ceil(lines * 0.6))
739
+ return true;
488
740
  return false;
489
741
  }
490
742
  function getContentChunkKind(filePath) {
@@ -787,6 +1039,9 @@ function indexFile(absPath, reason) {
787
1039
  const indexContent = isContentIndexableFile(absPath);
788
1040
  if (!indexSymbols && !indexContent)
789
1041
  return;
1042
+ const kind = getContentChunkKind(absPath);
1043
+ if (!kind)
1044
+ return;
790
1045
  let stat;
791
1046
  try {
792
1047
  stat = fs.statSync(absPath);
@@ -796,7 +1051,8 @@ function indexFile(absPath, reason) {
796
1051
  }
797
1052
  if (!stat.isFile())
798
1053
  return;
799
- if (stat.size > 1_000_000)
1054
+ const maxBytes = kind === "code_chunk" ? INDEX_MAX_CODE_BYTES : INDEX_MAX_DOC_BYTES;
1055
+ if (maxBytes > 0 && stat.size > maxBytes)
800
1056
  return;
801
1057
  let content;
802
1058
  try {
@@ -807,7 +1063,19 @@ function indexFile(absPath, reason) {
807
1063
  }
808
1064
  if (content.includes("\u0000"))
809
1065
  return;
1066
+ const ext = path.extname(absPath).toLowerCase();
810
1067
  const filePath = normalizeToDbPath(absPath);
1068
+ if (INDEX_SKIP_MINIFIED &&
1069
+ kind === "code_chunk" &&
1070
+ (ext === ".js" || ext === ".mjs" || ext === ".cjs" || ext === ".css") &&
1071
+ looksLikeMinifiedBundle(content)) {
1072
+ logActivity("index_skip", { file_path: filePath, reason: "minified_bundle", bytes: stat.size });
1073
+ return;
1074
+ }
1075
+ if (kind === "code_chunk" && stat.size >= 20_000 && looksLikeGeneratedFile(content)) {
1076
+ logActivity("index_skip", { file_path: filePath, reason: "generated_file", bytes: stat.size });
1077
+ return;
1078
+ }
811
1079
  let symbolCount = 0;
812
1080
  let chunkCount = 0;
813
1081
  if (indexSymbols) {
@@ -873,6 +1141,18 @@ const AddNoteArgsSchema = ProjectRootArgSchema.merge(z.object({
873
1141
  content: z.string().min(1),
874
1142
  tags: z.array(z.string().min(1)).optional(),
875
1143
  }));
1144
+ const PruneIndexArgsSchema = ProjectRootArgSchema.merge(z.object({
1145
+ dry_run: z.boolean().optional().default(true),
1146
+ prune_ignored_paths: z.boolean().optional().default(true),
1147
+ prune_minified_bundles: z.boolean().optional().default(false),
1148
+ max_files: z.number().int().min(1).max(50_000).optional().default(2000),
1149
+ vacuum: z.boolean().optional().default(false),
1150
+ }));
1151
+ const UpsertConventionArgsSchema = ProjectRootArgSchema.merge(z.object({
1152
+ key: z.string().min(1),
1153
+ content: z.string().min(1),
1154
+ tags: z.array(z.string().min(1)).optional(),
1155
+ }));
876
1156
  const DEFAULT_PENDING_LIMIT = 50;
877
1157
  const MAX_PENDING_LIMIT = 2000;
878
1158
  const PendingPagingSchema = z.object({
@@ -890,10 +1170,12 @@ const ContentMaxSchema = z.object({
890
1170
  const DEFAULT_RECENT_REQUIREMENTS = 3;
891
1171
  const DEFAULT_RECENT_CHANGES_PER_REQ = 5;
892
1172
  const DEFAULT_RECENT_NOTES = 5;
1173
+ const DEFAULT_CONVENTIONS_LIMIT = 20;
893
1174
  const BrainDumpLimitsSchema = z.object({
894
1175
  requirements_limit: z.number().int().min(1).max(20).optional().default(DEFAULT_RECENT_REQUIREMENTS),
895
1176
  changes_limit: z.number().int().min(1).max(100).optional().default(DEFAULT_RECENT_CHANGES_PER_REQ),
896
1177
  notes_limit: z.number().int().min(0).max(50).optional().default(DEFAULT_RECENT_NOTES),
1178
+ conventions_limit: z.number().int().min(0).max(200).optional().default(DEFAULT_CONVENTIONS_LIMIT),
897
1179
  });
898
1180
  const GetPendingChangesArgsSchema = ProjectRootArgSchema.merge(z.object({
899
1181
  offset: z.number().int().min(0).optional().default(0),
@@ -905,7 +1187,12 @@ const CompleteRequirementArgsSchema = ProjectRootArgSchema.merge(z.object({
905
1187
  }));
906
1188
  const GetActivityLogArgsSchema = ProjectRootArgSchema.merge(z.object({
907
1189
  since_id: z.number().int().min(0).optional().default(0),
908
- limit: z.number().int().min(1).max(500).optional().default(50),
1190
+ limit: z.number().int().min(1).max(500).optional().default(30),
1191
+ verbose: z.boolean().optional().default(false),
1192
+ }));
1193
+ const GetActivitySummaryArgsSchema = ProjectRootArgSchema.merge(z.object({
1194
+ since_id: z.number().int().min(0).optional().default(0),
1195
+ max_files: z.number().int().min(0).max(200).optional().default(20),
909
1196
  }));
910
1197
  const ClearActivityLogArgsSchema = ProjectRootArgSchema;
911
1198
  const GetBrainDumpArgsSchema = ProjectRootArgSchema.merge(PendingPagingSchema)
@@ -1415,6 +1702,7 @@ const server = new Server({ name: SERVER_NAME, version: SERVER_VERSION }, {
1415
1702
  "- BEFORE editing code: call start_requirement(title, background) to set the active requirement.",
1416
1703
  "- AFTER editing + saving: call get_pending_changes() to see unsynced files, then call sync_change_intent(intent, files). (You can omit files to auto-link all pending changes.)",
1417
1704
  "- After major milestones/decisions: call upsert_project_summary(summary) and/or add_note(...) to persist durable context locally.",
1705
+ "- If the user states a durable project convention (build commands, frameworks, naming rules, output paths): call upsert_convention(key, content, tags) so it is applied in future sessions.",
1418
1706
  "- When you need full text for a specific note/summary/match: call read_memory_item(id, offset, limit) and page through it.",
1419
1707
  "- When asked to locate code (class/function/type): call query_codebase(query) instead of guessing.",
1420
1708
  "- When you need to recall relevant context from history/code/docs: call semantic_search(query, ...) instead of guessing.",
@@ -1629,6 +1917,9 @@ function initDatabase() {
1629
1917
  CREATE UNIQUE INDEX IF NOT EXISTS idx_memory_items_project_summary
1630
1918
  ON memory_items(kind) WHERE kind = 'project_summary';
1631
1919
 
1920
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_memory_items_convention_key
1921
+ ON memory_items(kind, title) WHERE kind = 'convention';
1922
+
1632
1923
  CREATE INDEX IF NOT EXISTS idx_memory_items_kind_updated_at
1633
1924
  ON memory_items(kind, updated_at DESC);
1634
1925
 
@@ -1688,12 +1979,27 @@ function initDatabase() {
1688
1979
  LIMIT ?`);
1689
1980
  insertChangeLogStmt = db.prepare(`INSERT INTO change_logs (req_id, file_path, intent_summary) VALUES (?, ?, ?)`);
1690
1981
  insertMemoryItemStmt = db.prepare(`INSERT INTO memory_items
1691
- (kind, title, content, file_path, start_line, end_line, req_id, metadata_json, content_hash)
1982
+ (kind, title, content, file_path, start_line, end_line, req_id, metadata_json, content_hash)
1692
1983
  VALUES
1693
- (?, ?, ?, ?, ?, ?, ?, ?, ?)`);
1984
+ (?, ?, ?, ?, ?, ?, ?, ?, ?)`);
1694
1985
  getMemoryItemByIdStmt = db.prepare(`SELECT id, kind, title, content, file_path, start_line, end_line, req_id, metadata_json, content_hash, created_at, updated_at
1695
1986
  FROM memory_items
1696
1987
  WHERE id = ?`);
1988
+ getConventionByKeyStmt = db.prepare(`SELECT id, kind, title, content, file_path, start_line, end_line, req_id, metadata_json, content_hash, created_at, updated_at
1989
+ FROM memory_items
1990
+ WHERE kind = 'convention' AND title = ?
1991
+ ORDER BY updated_at DESC, id DESC
1992
+ LIMIT 1`);
1993
+ insertConventionStmt = db.prepare(`INSERT INTO memory_items (kind, title, content, metadata_json, content_hash)
1994
+ VALUES ('convention', ?, ?, ?, ?)`);
1995
+ updateConventionByIdStmt = db.prepare(`UPDATE memory_items
1996
+ SET content = ?, metadata_json = ?, content_hash = ?, updated_at = CURRENT_TIMESTAMP
1997
+ WHERE id = ?`);
1998
+ listConventionsStmt = db.prepare(`SELECT id, kind, title, content, file_path, start_line, end_line, req_id, metadata_json, content_hash, created_at, updated_at
1999
+ FROM memory_items
2000
+ WHERE kind = 'convention'
2001
+ ORDER BY updated_at DESC, id DESC
2002
+ LIMIT ?`);
1697
2003
  getRequirementMemoryItemIdStmt = db.prepare(`SELECT id
1698
2004
  FROM memory_items
1699
2005
  WHERE kind = 'requirement' AND req_id = ?
@@ -1773,6 +2079,13 @@ function initDatabase() {
1773
2079
  });
1774
2080
  // Clean up noisy pending changes recorded by older versions (build artifacts, node_modules, etc).
1775
2081
  prunePendingChanges();
2082
+ // Clean up noisy indexes recorded by older versions (build artifacts, etc).
2083
+ if (INDEX_AUTO_PRUNE_IGNORED) {
2084
+ pruneIgnoredIndexesByPathPatterns();
2085
+ }
2086
+ // Clean up common "file name noise" recorded by older versions.
2087
+ // (These files are ignored by current index rules; keep the DB consistent automatically.)
2088
+ pruneFilenameNoiseIndexes();
1776
2089
  }
1777
2090
  function initWatcher() {
1778
2091
  watcherReady = false;
@@ -1942,6 +2255,11 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
1942
2255
  description: "Get recent debug activity (indexing/search/pending) for troubleshooting. Enable logging with VECTORMIND_DEBUG_LOG=1. Use since_id/limit to page.",
1943
2256
  inputSchema: toJsonSchemaCompat(GetActivityLogArgsSchema),
1944
2257
  },
2258
+ {
2259
+ name: "get_activity_summary",
2260
+ description: "Get a compact summary of recent debug activity (counts + small samples). Enable logging with VECTORMIND_DEBUG_LOG=1. Use since_id to get incremental summaries.",
2261
+ inputSchema: toJsonSchemaCompat(GetActivitySummaryArgsSchema),
2262
+ },
1945
2263
  {
1946
2264
  name: "clear_activity_log",
1947
2265
  description: "Clear the in-memory debug activity log. Enable logging with VECTORMIND_DEBUG_LOG=1.",
@@ -1962,11 +2280,21 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
1962
2280
  description: "Save a durable project note (decision, constraint, TODO, architecture detail). Use this to persist important context locally instead of relying on chat memory.",
1963
2281
  inputSchema: toJsonSchemaCompat(AddNoteArgsSchema),
1964
2282
  },
2283
+ {
2284
+ name: "upsert_convention",
2285
+ description: "Save/update a project convention (framework choice, build command, naming rules, etc). Conventions are durable and should be applied automatically in future sessions.",
2286
+ inputSchema: toJsonSchemaCompat(UpsertConventionArgsSchema),
2287
+ },
1965
2288
  {
1966
2289
  name: "semantic_search",
1967
2290
  description: "Semantic search across the local memory store (requirements, change intents, notes, project summary, and indexed code/doc chunks). Use this to retrieve relevant context instead of guessing.",
1968
2291
  inputSchema: toJsonSchemaCompat(SemanticSearchArgsSchema),
1969
2292
  },
2293
+ {
2294
+ name: "prune_index",
2295
+ description: "Prune noisy auto-indexed items (code_chunk/doc_chunk + symbols). Useful after tightening ignore rules to shrink the index and improve search relevance.",
2296
+ inputSchema: toJsonSchemaCompat(PruneIndexArgsSchema),
2297
+ },
1970
2298
  ],
1971
2299
  };
1972
2300
  });
@@ -2013,6 +2341,142 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
2013
2341
  ],
2014
2342
  };
2015
2343
  }
2344
+ if (toolName === "prune_index") {
2345
+ const args = PruneIndexArgsSchema.parse(rawArgs);
2346
+ flushPendingChangeBuffer();
2347
+ const result = {
2348
+ ok: true,
2349
+ dry_run: args.dry_run,
2350
+ config: {
2351
+ index_max_code_bytes: INDEX_MAX_CODE_BYTES,
2352
+ index_max_doc_bytes: INDEX_MAX_DOC_BYTES,
2353
+ index_skip_minified: INDEX_SKIP_MINIFIED,
2354
+ index_auto_prune_ignored: INDEX_AUTO_PRUNE_IGNORED,
2355
+ },
2356
+ pruned: {
2357
+ ignored_paths: { chunks_deleted: 0, symbols_deleted: 0 },
2358
+ minified_bundles: { files_matched: 0, chunks_deleted: 0, symbols_deleted: 0 },
2359
+ },
2360
+ };
2361
+ if (args.prune_ignored_paths) {
2362
+ if (!IGNORED_LIKE_PATTERNS.length) {
2363
+ result.pruned.ignored_paths = { chunks_deleted: 0, symbols_deleted: 0 };
2364
+ }
2365
+ else if (args.dry_run) {
2366
+ const where = IGNORED_LIKE_PATTERNS
2367
+ .map(() => "LOWER(REPLACE(file_path, '\\\\', '/')) LIKE ?")
2368
+ .join(" OR ");
2369
+ const chunksWould = Number(db
2370
+ .prepare(`SELECT COUNT(1) AS c
2371
+ FROM memory_items
2372
+ WHERE file_path IS NOT NULL
2373
+ AND (kind = 'code_chunk' OR kind = 'doc_chunk')
2374
+ AND (${where})`)
2375
+ .get(...IGNORED_LIKE_PATTERNS)?.c ?? 0);
2376
+ const symbolsWould = Number(db
2377
+ .prepare(`SELECT COUNT(1) AS c
2378
+ FROM symbols
2379
+ WHERE file_path IS NOT NULL
2380
+ AND (${where})`)
2381
+ .get(...IGNORED_LIKE_PATTERNS)?.c ?? 0);
2382
+ result.pruned.ignored_paths = { chunks_deleted: chunksWould, symbols_deleted: symbolsWould };
2383
+ }
2384
+ else {
2385
+ result.pruned.ignored_paths = pruneIgnoredIndexesByPathPatterns();
2386
+ }
2387
+ }
2388
+ if (args.prune_minified_bundles) {
2389
+ const maxFiles = args.max_files;
2390
+ const candidates = db
2391
+ .prepare(`SELECT file_path, content
2392
+ FROM memory_items
2393
+ WHERE kind = 'code_chunk'
2394
+ AND file_path IS NOT NULL
2395
+ AND (
2396
+ LOWER(file_path) LIKE '%.js'
2397
+ OR LOWER(file_path) LIKE '%.mjs'
2398
+ OR LOWER(file_path) LIKE '%.cjs'
2399
+ OR LOWER(file_path) LIKE '%.css'
2400
+ )
2401
+ ORDER BY updated_at DESC, id DESC
2402
+ LIMIT ?`)
2403
+ .all(Math.min(50_000, maxFiles * 5));
2404
+ const matched = new Set();
2405
+ for (const row of candidates) {
2406
+ if (matched.size >= maxFiles)
2407
+ break;
2408
+ const fp = row.file_path;
2409
+ if (!fp || matched.has(fp))
2410
+ continue;
2411
+ if (looksLikeMinifiedBundle(row.content))
2412
+ matched.add(fp);
2413
+ }
2414
+ if (args.dry_run) {
2415
+ let chunksWould = 0;
2416
+ let symbolsWould = 0;
2417
+ const countChunksStmt = db.prepare(`SELECT COUNT(1) AS c
2418
+ FROM memory_items
2419
+ WHERE file_path = ?
2420
+ AND (kind = 'code_chunk' OR kind = 'doc_chunk')`);
2421
+ const countSymbolsStmt = db.prepare(`SELECT COUNT(1) AS c FROM symbols WHERE file_path = ?`);
2422
+ for (const fp of matched) {
2423
+ chunksWould += Number(countChunksStmt.get(fp)?.c ?? 0);
2424
+ symbolsWould += Number(countSymbolsStmt.get(fp)?.c ?? 0);
2425
+ }
2426
+ result.pruned.minified_bundles = {
2427
+ files_matched: matched.size,
2428
+ chunks_deleted: chunksWould,
2429
+ symbols_deleted: symbolsWould,
2430
+ };
2431
+ }
2432
+ else {
2433
+ let chunksDeleted = 0;
2434
+ let symbolsDeleted = 0;
2435
+ const tx = db.transaction(() => {
2436
+ for (const fp of matched) {
2437
+ chunksDeleted += deleteFileChunkItemsStmt.run(fp).changes;
2438
+ symbolsDeleted += deleteSymbolsForFileStmt.run(fp).changes;
2439
+ }
2440
+ });
2441
+ try {
2442
+ tx();
2443
+ }
2444
+ catch (err) {
2445
+ console.error("[vectormind] prune minified bundles failed:", err);
2446
+ }
2447
+ if (matched.size) {
2448
+ logActivity("index_prune", {
2449
+ reason: "minified_bundles",
2450
+ files_matched: matched.size,
2451
+ chunks_deleted: chunksDeleted,
2452
+ symbols_deleted: symbolsDeleted,
2453
+ });
2454
+ }
2455
+ result.pruned.minified_bundles = {
2456
+ files_matched: matched.size,
2457
+ chunks_deleted: chunksDeleted,
2458
+ symbols_deleted: symbolsDeleted,
2459
+ };
2460
+ }
2461
+ }
2462
+ if (!args.dry_run && args.vacuum) {
2463
+ try {
2464
+ db.exec("VACUUM");
2465
+ logActivity("index_prune", { reason: "vacuum" });
2466
+ }
2467
+ catch (err) {
2468
+ console.error("[vectormind] vacuum failed:", err);
2469
+ }
2470
+ }
2471
+ return {
2472
+ content: [
2473
+ {
2474
+ type: "text",
2475
+ text: toolJson(result),
2476
+ },
2477
+ ],
2478
+ };
2479
+ }
2016
2480
  if (toolName === "sync_change_intent") {
2017
2481
  const args = SyncChangeIntentArgsSchema.parse(rawArgs);
2018
2482
  flushPendingChangeBuffer();
@@ -2132,6 +2596,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
2132
2596
  const requirementsLimit = args.requirements_limit;
2133
2597
  const changesLimit = args.changes_limit;
2134
2598
  const notesLimit = args.notes_limit;
2599
+ const conventionsLimit = args.conventions_limit;
2135
2600
  const recent = listRecentRequirementsStmt.all(requirementsLimit);
2136
2601
  const items = recent.map((req) => {
2137
2602
  const changes = listChangeLogsForRequirementStmt.all(req.id, changesLimit);
@@ -2145,6 +2610,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
2145
2610
  ? toMemoryItemPreview(projectSummaryRow, includeContent, previewChars, contentMaxChars)
2146
2611
  : null;
2147
2612
  const recent_notes = listRecentNotesStmt.all(notesLimit).map((n) => toMemoryItemPreview(n, includeContent, previewChars, contentMaxChars));
2613
+ const conventions = listConventionsStmt.all(conventionsLimit).map((c) => toMemoryItemPreview(c, false, previewChars, contentMaxChars));
2148
2614
  const pending_total = Number(countPendingChangesStmt.get()?.total ?? 0);
2149
2615
  const pending_offset = args.pending_offset;
2150
2616
  const pending_limit = args.pending_limit;
@@ -2172,6 +2638,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
2172
2638
  pending_total,
2173
2639
  pending_returned: pending_changes.length,
2174
2640
  requirements_returned: items.length,
2641
+ conventions_returned: conventions.length,
2175
2642
  semantic_mode: semantic?.mode ?? null,
2176
2643
  semantic_matches: semantic?.matches?.length ?? 0,
2177
2644
  });
@@ -2199,8 +2666,10 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
2199
2666
  requirements_limit: requirementsLimit,
2200
2667
  changes_limit: changesLimit,
2201
2668
  notes_limit: notesLimit,
2669
+ conventions_limit: conventionsLimit,
2202
2670
  },
2203
2671
  project_summary,
2672
+ conventions,
2204
2673
  recent_notes,
2205
2674
  pending_total,
2206
2675
  pending_offset,
@@ -2223,6 +2692,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
2223
2692
  const requirementsLimit = args.requirements_limit;
2224
2693
  const changesLimit = args.changes_limit;
2225
2694
  const notesLimit = args.notes_limit;
2695
+ const conventionsLimit = args.conventions_limit;
2226
2696
  const recent = listRecentRequirementsStmt.all(requirementsLimit);
2227
2697
  const items = recent.map((req) => {
2228
2698
  const changes = listChangeLogsForRequirementStmt.all(req.id, changesLimit);
@@ -2236,6 +2706,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
2236
2706
  ? toMemoryItemPreview(projectSummaryRow, includeContent, previewChars, contentMaxChars)
2237
2707
  : null;
2238
2708
  const recent_notes = listRecentNotesStmt.all(notesLimit).map((n) => toMemoryItemPreview(n, includeContent, previewChars, contentMaxChars));
2709
+ const conventions = listConventionsStmt.all(conventionsLimit).map((c) => toMemoryItemPreview(c, false, previewChars, contentMaxChars));
2239
2710
  const pending_total = Number(countPendingChangesStmt.get()?.total ?? 0);
2240
2711
  const pending_offset = args.pending_offset;
2241
2712
  const pending_limit = args.pending_limit;
@@ -2246,6 +2717,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
2246
2717
  pending_returned: pending_changes.length,
2247
2718
  requirements_returned: items.length,
2248
2719
  notes_returned: recent_notes.length,
2720
+ conventions_returned: conventions.length,
2249
2721
  });
2250
2722
  return {
2251
2723
  content: [
@@ -2271,8 +2743,10 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
2271
2743
  requirements_limit: requirementsLimit,
2272
2744
  changes_limit: changesLimit,
2273
2745
  notes_limit: notesLimit,
2746
+ conventions_limit: conventionsLimit,
2274
2747
  },
2275
2748
  project_summary,
2749
+ conventions,
2276
2750
  recent_notes,
2277
2751
  pending_total,
2278
2752
  pending_offset,
@@ -2384,6 +2858,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
2384
2858
  const args = GetActivityLogArgsSchema.parse(rawArgs);
2385
2859
  flushPendingChangeBuffer();
2386
2860
  const { events, last_id } = snapshotActivityLog({ sinceId: args.since_id, limit: args.limit });
2861
+ const outEvents = args.verbose
2862
+ ? events
2863
+ : events.map((e) => ({ id: e.id, ts: e.ts, type: e.type, summary: summarizeActivityEvent(e) }));
2387
2864
  return {
2388
2865
  content: [
2389
2866
  {
@@ -2393,7 +2870,61 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
2393
2870
  enabled: debugLogEnabled,
2394
2871
  max_entries: debugLogMaxEntries,
2395
2872
  last_id,
2396
- events,
2873
+ events: outEvents,
2874
+ }),
2875
+ },
2876
+ ],
2877
+ };
2878
+ }
2879
+ if (toolName === "get_activity_summary") {
2880
+ const args = GetActivitySummaryArgsSchema.parse(rawArgs);
2881
+ flushPendingChangeBuffer();
2882
+ const { events, last_id } = snapshotActivityLog({ sinceId: args.since_id, limit: 500 });
2883
+ const counts = {};
2884
+ const indexedFiles = new Set();
2885
+ let semanticCount = 0;
2886
+ let queryCodebaseCount = 0;
2887
+ let pendingFlushes = 0;
2888
+ let pendingPrunes = 0;
2889
+ let lastSemantic = null;
2890
+ let lastQueryCodebase = null;
2891
+ for (const e of events) {
2892
+ counts[e.type] = (counts[e.type] ?? 0) + 1;
2893
+ if (e.type === "index_file") {
2894
+ const fp = String(e.data.file_path ?? "");
2895
+ if (fp)
2896
+ indexedFiles.add(fp);
2897
+ }
2898
+ if (e.type === "semantic_search") {
2899
+ semanticCount += 1;
2900
+ lastSemantic = e.data;
2901
+ }
2902
+ if (e.type === "query_codebase") {
2903
+ queryCodebaseCount += 1;
2904
+ lastQueryCodebase = e.data;
2905
+ }
2906
+ if (e.type === "pending_flush")
2907
+ pendingFlushes += 1;
2908
+ if (e.type === "pending_prune")
2909
+ pendingPrunes += 1;
2910
+ }
2911
+ const sampleFiles = Array.from(indexedFiles).slice(0, args.max_files);
2912
+ return {
2913
+ content: [
2914
+ {
2915
+ type: "text",
2916
+ text: toolJson({
2917
+ ok: true,
2918
+ enabled: debugLogEnabled,
2919
+ last_id,
2920
+ since_id: args.since_id,
2921
+ counts,
2922
+ indexed_files: { unique: indexedFiles.size, sample: sampleFiles },
2923
+ searches: {
2924
+ semantic_search: { count: semanticCount, last: lastSemantic },
2925
+ query_codebase: { count: queryCodebaseCount, last: lastQueryCodebase },
2926
+ },
2927
+ pending: { flushes: pendingFlushes, prunes: pendingPrunes },
2397
2928
  }),
2398
2929
  },
2399
2930
  ],
@@ -2461,6 +2992,42 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
2461
2992
  ],
2462
2993
  };
2463
2994
  }
2995
+ if (toolName === "upsert_convention") {
2996
+ const args = UpsertConventionArgsSchema.parse(rawArgs);
2997
+ const key = args.key.trim();
2998
+ const content = args.content.trim();
2999
+ const contentHash = sha256Hex(content);
3000
+ const meta = safeJson({ tags: args.tags ?? [] });
3001
+ const existing = getConventionByKeyStmt.get(key);
3002
+ if (existing) {
3003
+ updateConventionByIdStmt.run(content, meta, contentHash, existing.id);
3004
+ }
3005
+ else {
3006
+ insertConventionStmt.run(key, content, meta, contentHash);
3007
+ }
3008
+ const row = getConventionByKeyStmt.get(key);
3009
+ if (row)
3010
+ enqueueEmbedding(row.id);
3011
+ logActivity("upsert_convention", { key, content_preview: makePreviewText(content, 200) });
3012
+ return {
3013
+ content: [
3014
+ {
3015
+ type: "text",
3016
+ text: toolJson({
3017
+ ok: true,
3018
+ convention: row
3019
+ ? {
3020
+ id: row.id,
3021
+ key: row.title,
3022
+ updated_at: row.updated_at,
3023
+ preview: makePreviewText(row.content, DEFAULT_PREVIEW_CHARS),
3024
+ }
3025
+ : null,
3026
+ }),
3027
+ },
3028
+ ],
3029
+ };
3030
+ }
2464
3031
  if (toolName === "semantic_search") {
2465
3032
  const args = SemanticSearchArgsSchema.parse(rawArgs);
2466
3033
  const result = await semanticSearchHybridInternal({