context-vault 2.2.0 → 2.3.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.
Files changed (28) hide show
  1. package/bin/cli.js +354 -32
  2. package/node_modules/@context-vault/core/package.json +36 -0
  3. package/{src → node_modules/@context-vault/core/src}/core/categories.js +1 -0
  4. package/{src → node_modules/@context-vault/core/src}/core/files.js +1 -0
  5. package/{src → node_modules/@context-vault/core/src}/index/embed.js +10 -1
  6. package/node_modules/@context-vault/core/src/index.js +29 -0
  7. package/{src → node_modules/@context-vault/core/src}/server/tools.js +107 -26
  8. package/package.json +7 -14
  9. package/src/server/index.js +21 -4
  10. package/ui/serve.js +7 -6
  11. package/LICENSE +0 -21
  12. package/README.md +0 -431
  13. package/smithery.yaml +0 -10
  14. package/src/capture/README.md +0 -23
  15. package/src/core/README.md +0 -20
  16. package/src/index/README.md +0 -28
  17. package/src/retrieve/README.md +0 -19
  18. package/src/server/README.md +0 -44
  19. /package/{src → node_modules/@context-vault/core/src}/capture/file-ops.js +0 -0
  20. /package/{src → node_modules/@context-vault/core/src}/capture/formatters.js +0 -0
  21. /package/{src → node_modules/@context-vault/core/src}/capture/index.js +0 -0
  22. /package/{src → node_modules/@context-vault/core/src}/core/config.js +0 -0
  23. /package/{src → node_modules/@context-vault/core/src}/core/frontmatter.js +0 -0
  24. /package/{src → node_modules/@context-vault/core/src}/core/status.js +0 -0
  25. /package/{src → node_modules/@context-vault/core/src}/index/db.js +0 -0
  26. /package/{src → node_modules/@context-vault/core/src}/index/index.js +0 -0
  27. /package/{src → node_modules/@context-vault/core/src}/retrieve/index.js +0 -0
  28. /package/{src → node_modules/@context-vault/core/src}/server/helpers.js +0 -0
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * tools.js — MCP tool registrations
3
3
  *
4
- * Five tools: save_context (write/update), get_context (search), list_context (browse),
5
- * delete_context (remove), context_status (diag).
4
+ * Six tools: save_context (write/update), get_context (search), list_context (browse),
5
+ * delete_context (remove), submit_feedback (bug/feature reports), context_status (diag).
6
6
  * Auto-reindex runs transparently on first tool call per session.
7
7
  */
8
8
 
@@ -63,9 +63,9 @@ export function registerTools(server, ctx) {
63
63
 
64
64
  server.tool(
65
65
  "get_context",
66
- "Search your knowledge vault. Returns entries ranked by relevance using hybrid full-text + semantic search. Use this to find insights, decisions, patterns, or any saved context.",
66
+ "Search your knowledge vault. Returns entries ranked by relevance using hybrid full-text + semantic search. Use this to find insights, decisions, patterns, or any saved context. Each result includes an `id` you can use with save_context or delete_context.",
67
67
  {
68
- query: z.string().describe("Search query (natural language or keywords)"),
68
+ query: z.string().optional().describe("Search query (natural language or keywords). Optional if filters (tags, kind, category) are provided."),
69
69
  kind: z.string().optional().describe("Filter by kind (e.g. 'insight', 'decision', 'pattern')"),
70
70
  category: z.enum(["knowledge", "entity", "event"]).optional().describe("Filter by category"),
71
71
  tags: z.array(z.string()).optional().describe("Filter by tags (entries must match at least one)"),
@@ -74,38 +74,69 @@ export function registerTools(server, ctx) {
74
74
  limit: z.number().optional().describe("Max results to return (default 10)"),
75
75
  },
76
76
  async ({ query, kind, category, tags, since, until, limit }) => {
77
- if (!query?.trim()) return err("Required: query (non-empty string)", "INVALID_INPUT");
77
+ const hasQuery = query?.trim();
78
+ const hasFilters = kind || category || tags?.length || since || until;
79
+ if (!hasQuery && !hasFilters) return err("Required: query or at least one filter (kind, category, tags, since, until)", "INVALID_INPUT");
78
80
  await ensureIndexed();
79
81
 
80
82
  const kindFilter = kind ? normalizeKind(kind) : null;
81
- const sorted = await hybridSearch(ctx, query, {
82
- kindFilter,
83
- categoryFilter: category || null,
84
- since: since || null,
85
- until: until || null,
86
- limit: limit || 10,
87
- });
88
83
 
89
- // Post-filter by tags if provided
90
- const filtered = tags?.length
91
- ? sorted.filter((r) => {
92
- const entryTags = r.tags ? JSON.parse(r.tags) : [];
93
- return tags.some((t) => entryTags.includes(t));
94
- })
95
- : sorted;
84
+ let filtered;
85
+ if (hasQuery) {
86
+ // Hybrid search mode
87
+ const sorted = await hybridSearch(ctx, query, {
88
+ kindFilter,
89
+ categoryFilter: category || null,
90
+ since: since || null,
91
+ until: until || null,
92
+ limit: limit || 10,
93
+ });
94
+
95
+ // Post-filter by tags if provided
96
+ filtered = tags?.length
97
+ ? sorted.filter((r) => {
98
+ const entryTags = r.tags ? JSON.parse(r.tags) : [];
99
+ return tags.some((t) => entryTags.includes(t));
100
+ })
101
+ : sorted;
102
+ } else {
103
+ // Filter-only mode (no query, use SQL directly)
104
+ const clauses = [];
105
+ const params = [];
106
+ if (kindFilter) { clauses.push("kind = ?"); params.push(kindFilter); }
107
+ if (category) { clauses.push("category = ?"); params.push(category); }
108
+ if (since) { clauses.push("created_at >= ?"); params.push(since); }
109
+ if (until) { clauses.push("created_at <= ?"); params.push(until); }
110
+ clauses.push("(expires_at IS NULL OR expires_at > datetime('now'))");
111
+ const where = clauses.length ? `WHERE ${clauses.join(" AND ")}` : "";
112
+ const effectiveLimit = limit || 10;
113
+ params.push(effectiveLimit);
114
+ const rows = ctx.db.prepare(`SELECT * FROM vault ${where} ORDER BY created_at DESC LIMIT ?`).all(...params);
115
+
116
+ filtered = tags?.length
117
+ ? rows.filter((r) => {
118
+ const entryTags = r.tags ? JSON.parse(r.tags) : [];
119
+ return tags.some((t) => entryTags.includes(t));
120
+ })
121
+ : rows;
122
+
123
+ // Add score field for consistent output
124
+ for (const r of filtered) r.score = 0;
125
+ }
96
126
 
97
- if (!filtered.length) return ok("No results found for: " + query);
127
+ if (!filtered.length) return ok(hasQuery ? "No results found for: " + query : "No entries found matching the given filters.");
98
128
 
99
129
  const lines = [];
100
130
  if (reindexFailed) lines.push(`> **Warning:** Auto-reindex failed. Results may be stale. Run \`context-mcp reindex\` to fix.\n`);
101
- lines.push(`## Results for "${query}" (${filtered.length} matches)\n`);
131
+ const heading = hasQuery ? `Results for "${query}"` : "Filtered entries";
132
+ lines.push(`## ${heading} (${filtered.length} matches)\n`);
102
133
  for (let i = 0; i < filtered.length; i++) {
103
134
  const r = filtered[i];
104
135
  const entryTags = r.tags ? JSON.parse(r.tags) : [];
105
136
  const tagStr = entryTags.length ? entryTags.join(", ") : "none";
106
137
  const relPath = r.file_path && config.vaultDir ? r.file_path.replace(config.vaultDir + "/", "") : r.file_path || "n/a";
107
138
  lines.push(`### [${i + 1}/${filtered.length}] ${r.title || "(untitled)"} [${r.kind}/${r.category}]`);
108
- lines.push(`${r.score.toFixed(3)} · ${tagStr} · ${relPath}`);
139
+ lines.push(`${r.score.toFixed(3)} · ${tagStr} · ${relPath} · id: \`${r.id}\``);
109
140
  lines.push(r.body?.slice(0, 300) + (r.body?.length > 300 ? "..." : ""));
110
141
  lines.push("");
111
142
  }
@@ -117,7 +148,7 @@ export function registerTools(server, ctx) {
117
148
 
118
149
  server.tool(
119
150
  "save_context",
120
- "Save knowledge to your vault. Creates a .md file and indexes it for search. Use for any kind of context: insights, decisions, patterns, references, or any custom kind.",
151
+ "Save knowledge to your vault. Creates a .md file and indexes it for search. Use for any kind of context: insights, decisions, patterns, references, or any custom kind. To update an existing entry, pass its `id` — omitted fields are preserved.",
121
152
  {
122
153
  id: z.string().optional().describe("Entry ULID to update. When provided, updates the existing entry instead of creating new. Omitted fields are preserved."),
123
154
  kind: z.string().optional().describe("Entry kind — determines folder (e.g. 'insight', 'decision', 'pattern', 'reference', or any custom kind). Required for new entries."),
@@ -155,6 +186,7 @@ export function registerTools(server, ctx) {
155
186
  if (entry.title) parts.push(` title: ${entry.title}`);
156
187
  const entryTags = entry.tags || [];
157
188
  if (entryTags.length) parts.push(` tags: ${entryTags.join(", ")}`);
189
+ parts.push("", "_Search with get_context to verify changes._");
158
190
  return ok(parts.join("\n"));
159
191
  }
160
192
 
@@ -179,6 +211,7 @@ export function registerTools(server, ctx) {
179
211
  const parts = [`✓ Saved ${kind} → ${relPath}`, ` id: ${entry.id}`];
180
212
  if (title) parts.push(` title: ${title}`);
181
213
  if (tags?.length) parts.push(` tags: ${tags.join(", ")}`);
214
+ parts.push("", "_Use this id to update or delete later._");
182
215
  return ok(parts.join("\n"));
183
216
  }
184
217
  );
@@ -187,7 +220,7 @@ export function registerTools(server, ctx) {
187
220
 
188
221
  server.tool(
189
222
  "list_context",
190
- "Browse vault entries without a search query. Returns id, title, kind, category, tags, created_at. Use get_context with a query for semantic search.",
223
+ "Browse vault entries without a search query. Returns id, title, kind, category, tags, created_at. Use get_context with a query for semantic search. Use this to browse by tags or find recent entries.",
191
224
  {
192
225
  kind: z.string().optional().describe("Filter by kind (e.g. 'insight', 'decision', 'pattern')"),
193
226
  category: z.enum(["knowledge", "entity", "event"]).optional().describe("Filter by category"),
@@ -229,7 +262,7 @@ export function registerTools(server, ctx) {
229
262
  const total = ctx.db.prepare(`SELECT COUNT(*) as c FROM vault ${where}`).get(...countParams).c;
230
263
 
231
264
  params.push(effectiveLimit, effectiveOffset);
232
- const rows = ctx.db.prepare(`SELECT id, title, kind, category, tags, created_at FROM vault ${where} ORDER BY created_at DESC LIMIT ? OFFSET ?`).all(...params);
265
+ const rows = ctx.db.prepare(`SELECT id, title, kind, category, tags, created_at, SUBSTR(body, 1, 120) as preview FROM vault ${where} ORDER BY created_at DESC LIMIT ? OFFSET ?`).all(...params);
233
266
 
234
267
  // Post-filter by tags if provided
235
268
  const filtered = tags?.length
@@ -246,6 +279,7 @@ export function registerTools(server, ctx) {
246
279
  const entryTags = r.tags ? JSON.parse(r.tags) : [];
247
280
  const tagStr = entryTags.length ? entryTags.join(", ") : "none";
248
281
  lines.push(`- **${r.title || "(untitled)"}** [${r.kind}/${r.category}] — ${tagStr} — ${r.created_at} — \`${r.id}\``);
282
+ if (r.preview) lines.push(` ${r.preview.replace(/\n+/g, " ").trim()}${r.preview.length >= 120 ? "…" : ""}`);
249
283
  }
250
284
 
251
285
  if (effectiveOffset + effectiveLimit < total) {
@@ -289,11 +323,47 @@ export function registerTools(server, ctx) {
289
323
  }
290
324
  );
291
325
 
326
+ // ─── submit_feedback (bug/feature reports) ────────────────────────────────
327
+
328
+ server.tool(
329
+ "submit_feedback",
330
+ "Report a bug, request a feature, or suggest an improvement. Feedback is stored in the vault and triaged by the development pipeline.",
331
+ {
332
+ type: z.enum(["bug", "feature", "improvement"]).describe("Type of feedback"),
333
+ title: z.string().describe("Short summary of the feedback"),
334
+ body: z.string().describe("Detailed description"),
335
+ severity: z.enum(["low", "medium", "high"]).optional().describe("Severity level (default: medium)"),
336
+ },
337
+ async ({ type, title, body, severity }) => {
338
+ const vaultErr = ensureVaultExists(config);
339
+ if (vaultErr) return vaultErr;
340
+
341
+ await ensureIndexed();
342
+
343
+ const effectiveSeverity = severity || "medium";
344
+ const entry = await captureAndIndex(
345
+ ctx,
346
+ {
347
+ kind: "feedback",
348
+ title,
349
+ body,
350
+ tags: [type, effectiveSeverity],
351
+ source: "submit_feedback",
352
+ meta: { feedback_type: type, severity: effectiveSeverity, status: "new" },
353
+ },
354
+ indexEntry
355
+ );
356
+
357
+ const relPath = entry.filePath ? entry.filePath.replace(config.vaultDir + "/", "") : entry.filePath;
358
+ return ok(`Feedback submitted: ${type} [${effectiveSeverity}] → ${relPath}\n id: ${entry.id}\n title: ${title}`);
359
+ }
360
+ );
361
+
292
362
  // ─── context_status (diagnostics) ──────────────────────────────────────────
293
363
 
294
364
  server.tool(
295
365
  "context_status",
296
- "Show vault health: resolved config, file counts per kind, database size, and any issues. Use to verify setup or troubleshoot.",
366
+ "Show vault health: resolved config, file counts per kind, database size, and any issues. Use to verify setup or troubleshoot. Call this when a user asks about their vault or to debug search issues.",
297
367
  {},
298
368
  () => {
299
369
  const status = gatherVaultStatus(ctx);
@@ -346,6 +416,17 @@ export function registerTools(server, ctx) {
346
416
  lines.push(`Auto-reindex will fix this on next search or save.`);
347
417
  }
348
418
 
419
+ // Suggested actions
420
+ const actions = [];
421
+ if (status.stalePaths) actions.push("- Run `context-mcp reindex` to fix stale paths");
422
+ if (status.embeddingStatus?.missing > 0) actions.push("- Run `context-mcp reindex` to generate missing embeddings");
423
+ if (!config.vaultDirExists) actions.push("- Run `context-mcp setup` to create the vault directory");
424
+ if (status.kindCounts.length === 0 && config.vaultDirExists) actions.push("- Use `save_context` to add your first entry");
425
+
426
+ if (actions.length) {
427
+ lines.push("", "### Suggested Actions", ...actions);
428
+ }
429
+
349
430
  return ok(lines.join("\n"));
350
431
  }
351
432
  );
package/package.json CHANGED
@@ -1,19 +1,21 @@
1
1
  {
2
2
  "name": "context-vault",
3
- "version": "2.2.0",
3
+ "version": "2.3.0",
4
4
  "type": "module",
5
5
  "description": "Persistent memory for AI agents — saves and searches knowledge across sessions",
6
6
  "bin": {
7
7
  "context-mcp": "bin/cli.js"
8
8
  },
9
9
  "main": "src/server/index.js",
10
+ "scripts": {
11
+ "prepack": "mkdir -p node_modules/@context-vault && rm -rf node_modules/@context-vault/core && cp -rL ../core node_modules/@context-vault/core && rm -rf node_modules/@context-vault/core/node_modules"
12
+ },
10
13
  "files": [
11
14
  "bin/",
12
15
  "src/",
13
16
  "ui/",
14
17
  "README.md",
15
- "LICENSE",
16
- "smithery.yaml"
18
+ "LICENSE"
17
19
  ],
18
20
  "license": "MIT",
19
21
  "engines": { "node": ">=20" },
@@ -21,17 +23,8 @@
21
23
  "repository": { "type": "git", "url": "https://github.com/fellanH/context-mcp.git" },
22
24
  "homepage": "https://github.com/fellanH/context-mcp",
23
25
  "keywords": ["mcp", "model-context-protocol", "ai", "knowledge-base", "knowledge-management", "vault", "rag", "sqlite", "embeddings", "claude", "cursor", "cline", "windsurf"],
24
- "scripts": {
25
- "test": "vitest run",
26
- "test:watch": "vitest"
27
- },
28
- "devDependencies": {
29
- "vitest": "^3.0.0"
30
- },
26
+ "bundledDependencies": ["@context-vault/core"],
31
27
  "dependencies": {
32
- "@huggingface/transformers": "^3.0.0",
33
- "@modelcontextprotocol/sdk": "^1.26.0",
34
- "better-sqlite3": "^12.6.2",
35
- "sqlite-vec": "^0.1.0"
28
+ "@context-vault/core": "^2.3.0"
36
29
  }
37
30
  }
@@ -9,10 +9,10 @@ import { fileURLToPath } from "node:url";
9
9
  const __dirname = dirname(fileURLToPath(import.meta.url));
10
10
  const pkg = JSON.parse(readFileSync(join(__dirname, "..", "..", "package.json"), "utf-8"));
11
11
 
12
- import { resolveConfig } from "../core/config.js";
13
- import { embed } from "../index/embed.js";
14
- import { initDatabase, prepareStatements, insertVec, deleteVec } from "../index/db.js";
15
- import { registerTools } from "./tools.js";
12
+ import { resolveConfig } from "@context-vault/core/core/config";
13
+ import { embed } from "@context-vault/core/index/embed";
14
+ import { initDatabase, prepareStatements, insertVec, deleteVec } from "@context-vault/core/index/db";
15
+ import { registerTools } from "@context-vault/core/server/tools";
16
16
 
17
17
  // ─── Config Resolution ──────────────────────────────────────────────────────
18
18
 
@@ -80,3 +80,20 @@ process.on("SIGTERM", shutdown);
80
80
 
81
81
  const transport = new StdioServerTransport();
82
82
  await server.connect(transport);
83
+
84
+ // ─── Non-blocking Update Check ──────────────────────────────────────────────
85
+
86
+ setTimeout(() => {
87
+ import("node:child_process").then(({ execSync }) => {
88
+ try {
89
+ const latest = execSync("npm view context-vault version", {
90
+ encoding: "utf-8",
91
+ timeout: 5000,
92
+ stdio: ["pipe", "pipe", "pipe"],
93
+ }).trim();
94
+ if (latest && latest !== pkg.version) {
95
+ console.error(`[context-mcp] Update available: v${pkg.version} → v${latest}. Run: context-mcp update`);
96
+ }
97
+ } catch {}
98
+ }).catch(() => {});
99
+ }, 3000);
package/ui/serve.js CHANGED
@@ -12,10 +12,10 @@ import { createServer } from "node:http";
12
12
  import { readFileSync, writeFileSync, existsSync, statSync, readdirSync } from "node:fs";
13
13
  import { resolve, dirname, join, basename } from "node:path";
14
14
  import { fileURLToPath } from "node:url";
15
- import { resolveConfig } from "../src/config.js";
16
- import { initDatabase } from "../src/db.js";
17
- import { embed } from "../src/embed.js";
18
- import { hybridSearch } from "../src/search.js";
15
+ import { resolveConfig } from "@context-vault/core/core/config";
16
+ import { initDatabase } from "@context-vault/core/index/db";
17
+ import { embed } from "@context-vault/core/index/embed";
18
+ import { hybridSearch } from "@context-vault/core/retrieve";
19
19
 
20
20
  const __dirname = dirname(fileURLToPath(import.meta.url));
21
21
 
@@ -348,8 +348,9 @@ async function handleSearch(res, url) {
348
348
 
349
349
  if (!query) return jsonResponse(res, { results: [] });
350
350
 
351
- const sorted = await hybridSearch(db, embed, query, kindFilter);
352
- jsonResponse(res, { query, results: sorted });
351
+ const searchCtx = { db, config, embed };
352
+ const results = await hybridSearch(searchCtx, query, { kindFilter });
353
+ jsonResponse(res, { query, results });
353
354
  }
354
355
 
355
356
  // ─── Config Handlers ─────────────────────────────────────────────────────────
package/LICENSE DELETED
@@ -1,21 +0,0 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 Felix Hellstrom
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.