context-vault 2.7.1 → 2.8.3

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 (37) hide show
  1. package/README.md +39 -375
  2. package/bin/cli.js +373 -230
  3. package/node_modules/@context-vault/core/package.json +7 -3
  4. package/node_modules/@context-vault/core/src/capture/file-ops.js +20 -2
  5. package/node_modules/@context-vault/core/src/capture/import-pipeline.js +11 -50
  6. package/node_modules/@context-vault/core/src/capture/importers.js +64 -37
  7. package/node_modules/@context-vault/core/src/capture/index.js +57 -15
  8. package/node_modules/@context-vault/core/src/capture/ingest-url.js +80 -44
  9. package/node_modules/@context-vault/core/src/constants.js +8 -0
  10. package/node_modules/@context-vault/core/src/core/config.js +65 -29
  11. package/node_modules/@context-vault/core/src/core/files.js +8 -15
  12. package/node_modules/@context-vault/core/src/core/frontmatter.js +22 -10
  13. package/node_modules/@context-vault/core/src/core/status.js +32 -15
  14. package/node_modules/@context-vault/core/src/index/db.js +47 -34
  15. package/node_modules/@context-vault/core/src/index/embed.js +15 -5
  16. package/node_modules/@context-vault/core/src/index/index.js +206 -52
  17. package/node_modules/@context-vault/core/src/index.js +39 -6
  18. package/node_modules/@context-vault/core/src/retrieve/index.js +40 -8
  19. package/node_modules/@context-vault/core/src/server/helpers.js +8 -6
  20. package/node_modules/@context-vault/core/src/server/tools/context-status.js +24 -10
  21. package/node_modules/@context-vault/core/src/server/tools/delete-context.js +8 -3
  22. package/node_modules/@context-vault/core/src/server/tools/get-context.js +117 -35
  23. package/node_modules/@context-vault/core/src/server/tools/ingest-url.js +34 -15
  24. package/node_modules/@context-vault/core/src/server/tools/list-context.js +59 -18
  25. package/node_modules/@context-vault/core/src/server/tools/save-context.js +164 -40
  26. package/node_modules/@context-vault/core/src/server/tools/submit-feedback.js +24 -18
  27. package/node_modules/@context-vault/core/src/server/tools.js +24 -28
  28. package/node_modules/@context-vault/core/src/sync/sync.js +24 -19
  29. package/package.json +2 -2
  30. package/scripts/local-server.js +334 -122
  31. package/scripts/postinstall.js +25 -10
  32. package/scripts/prepack.js +18 -15
  33. package/src/server/index.js +78 -29
  34. package/app-dist/assets/index-DjXoWapE.css +0 -1
  35. package/app-dist/assets/index-R4n9Qz4U.js +0 -380
  36. package/app-dist/index.html +0 -16
  37. package/node_modules/@context-vault/core/src/server/types.js +0 -78
@@ -19,7 +19,8 @@ export const inputSchema = {
19
19
  export async function handler({ id }, ctx, { ensureIndexed }) {
20
20
  const userId = ctx.userId !== undefined ? ctx.userId : undefined;
21
21
 
22
- if (!id?.trim()) return err("Required: id (non-empty string)", "INVALID_INPUT");
22
+ if (!id?.trim())
23
+ return err("Required: id (non-empty string)", "INVALID_INPUT");
23
24
  await ensureIndexed();
24
25
 
25
26
  const entry = ctx.stmts.getEntryById.get(id);
@@ -32,13 +33,17 @@ export async function handler({ id }, ctx, { ensureIndexed }) {
32
33
 
33
34
  // Delete file from disk first (source of truth)
34
35
  if (entry.file_path) {
35
- try { unlinkSync(entry.file_path); } catch {}
36
+ try {
37
+ unlinkSync(entry.file_path);
38
+ } catch {}
36
39
  }
37
40
 
38
41
  // Delete vector embedding
39
42
  const rowidResult = ctx.stmts.getRowid.get(id);
40
43
  if (rowidResult?.rowid) {
41
- try { ctx.deleteVec(Number(rowidResult.rowid)); } catch {}
44
+ try {
45
+ ctx.deleteVec(Number(rowidResult.rowid));
46
+ } catch {}
42
47
  }
43
48
 
44
49
  // Delete DB row (FTS trigger handles FTS cleanup)
@@ -11,13 +11,36 @@ export const description =
11
11
  "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.";
12
12
 
13
13
  export const inputSchema = {
14
- query: z.string().optional().describe("Search query (natural language or keywords). Optional if filters (tags, kind, category) are provided."),
15
- kind: z.string().optional().describe("Filter by kind (e.g. 'insight', 'decision', 'pattern')"),
16
- category: z.enum(["knowledge", "entity", "event"]).optional().describe("Filter by category"),
17
- identity_key: z.string().optional().describe("For entity lookup: exact match on identity key. Requires kind."),
18
- tags: z.array(z.string()).optional().describe("Filter by tags (entries must match at least one)"),
19
- since: z.string().optional().describe("ISO date, return entries created after this"),
20
- until: z.string().optional().describe("ISO date, return entries created before this"),
14
+ query: z
15
+ .string()
16
+ .optional()
17
+ .describe(
18
+ "Search query (natural language or keywords). Optional if filters (tags, kind, category) are provided.",
19
+ ),
20
+ kind: z
21
+ .string()
22
+ .optional()
23
+ .describe("Filter by kind (e.g. 'insight', 'decision', 'pattern')"),
24
+ category: z
25
+ .enum(["knowledge", "entity", "event"])
26
+ .optional()
27
+ .describe("Filter by category"),
28
+ identity_key: z
29
+ .string()
30
+ .optional()
31
+ .describe("For entity lookup: exact match on identity key. Requires kind."),
32
+ tags: z
33
+ .array(z.string())
34
+ .optional()
35
+ .describe("Filter by tags (entries must match at least one)"),
36
+ since: z
37
+ .string()
38
+ .optional()
39
+ .describe("ISO date, return entries created after this"),
40
+ until: z
41
+ .string()
42
+ .optional()
43
+ .describe("ISO date, return entries created before this"),
21
44
  limit: z.number().optional().describe("Max results to return (default 10)"),
22
45
  };
23
46
 
@@ -26,25 +49,42 @@ export const inputSchema = {
26
49
  * @param {import('../types.js').BaseCtx & Partial<import('../types.js').HostedCtxExtensions>} ctx
27
50
  * @param {import('../types.js').ToolShared} shared
28
51
  */
29
- export async function handler({ query, kind, category, identity_key, tags, since, until, limit }, ctx, { ensureIndexed, reindexFailed }) {
52
+ export async function handler(
53
+ { query, kind, category, identity_key, tags, since, until, limit },
54
+ ctx,
55
+ { ensureIndexed, reindexFailed },
56
+ ) {
30
57
  const { config } = ctx;
31
58
  const userId = ctx.userId !== undefined ? ctx.userId : undefined;
32
59
 
33
60
  const hasQuery = query?.trim();
34
- const hasFilters = kind || category || tags?.length || since || until || identity_key;
35
- if (!hasQuery && !hasFilters) return err("Required: query or at least one filter (kind, category, tags, since, until, identity_key)", "INVALID_INPUT");
61
+ const hasFilters =
62
+ kind || category || tags?.length || since || until || identity_key;
63
+ if (!hasQuery && !hasFilters)
64
+ return err(
65
+ "Required: query or at least one filter (kind, category, tags, since, until, identity_key)",
66
+ "INVALID_INPUT",
67
+ );
36
68
  await ensureIndexed();
37
69
 
38
70
  const kindFilter = kind ? normalizeKind(kind) : null;
39
71
 
40
72
  // Gap 1: Entity exact-match by identity_key
41
73
  if (identity_key) {
42
- if (!kindFilter) return err("identity_key requires kind to be specified", "INVALID_INPUT");
43
- const match = ctx.stmts.getByIdentityKey.get(kindFilter, identity_key, userId !== undefined ? userId : null);
74
+ if (!kindFilter)
75
+ return err("identity_key requires kind to be specified", "INVALID_INPUT");
76
+ const match = ctx.stmts.getByIdentityKey.get(
77
+ kindFilter,
78
+ identity_key,
79
+ userId !== undefined ? userId : null,
80
+ );
44
81
  if (match) {
45
82
  const entryTags = match.tags ? JSON.parse(match.tags) : [];
46
83
  const tagStr = entryTags.length ? entryTags.join(", ") : "none";
47
- const relPath = match.file_path && config.vaultDir ? match.file_path.replace(config.vaultDir + "/", "") : match.file_path || "n/a";
84
+ const relPath =
85
+ match.file_path && config.vaultDir
86
+ ? match.file_path.replace(config.vaultDir + "/", "")
87
+ : match.file_path || "n/a";
48
88
  const lines = [
49
89
  `## Entity Match (exact)\n`,
50
90
  `### ${match.title || "(untitled)"} [${match.kind}/${match.category}]`,
@@ -57,7 +97,8 @@ export async function handler({ query, kind, category, identity_key, tags, since
57
97
  }
58
98
 
59
99
  // Gap 2: Event default time-window
60
- const effectiveCategory = category || (kindFilter ? categoryFor(kindFilter) : null);
100
+ const effectiveCategory =
101
+ category || (kindFilter ? categoryFor(kindFilter) : null);
61
102
  let effectiveSince = since || null;
62
103
  let effectiveUntil = until || null;
63
104
  let autoWindowed = false;
@@ -86,38 +127,64 @@ export async function handler({ query, kind, category, identity_key, tags, since
86
127
 
87
128
  // Post-filter by tags if provided, then apply requested limit
88
129
  filtered = tags?.length
89
- ? sorted.filter((r) => {
90
- const entryTags = r.tags ? JSON.parse(r.tags) : [];
91
- return tags.some((t) => entryTags.includes(t));
92
- }).slice(0, effectiveLimit)
130
+ ? sorted
131
+ .filter((r) => {
132
+ const entryTags = r.tags ? JSON.parse(r.tags) : [];
133
+ return tags.some((t) => entryTags.includes(t));
134
+ })
135
+ .slice(0, effectiveLimit)
93
136
  : sorted;
94
137
  } else {
95
138
  // Filter-only mode (no query, use SQL directly)
96
139
  const clauses = [];
97
140
  const params = [];
98
- if (userId !== undefined) { clauses.push("user_id = ?"); params.push(userId); }
99
- if (kindFilter) { clauses.push("kind = ?"); params.push(kindFilter); }
100
- if (category) { clauses.push("category = ?"); params.push(category); }
101
- if (effectiveSince) { clauses.push("created_at >= ?"); params.push(effectiveSince); }
102
- if (effectiveUntil) { clauses.push("created_at <= ?"); params.push(effectiveUntil); }
141
+ if (userId !== undefined) {
142
+ clauses.push("user_id = ?");
143
+ params.push(userId);
144
+ }
145
+ if (kindFilter) {
146
+ clauses.push("kind = ?");
147
+ params.push(kindFilter);
148
+ }
149
+ if (category) {
150
+ clauses.push("category = ?");
151
+ params.push(category);
152
+ }
153
+ if (effectiveSince) {
154
+ clauses.push("created_at >= ?");
155
+ params.push(effectiveSince);
156
+ }
157
+ if (effectiveUntil) {
158
+ clauses.push("created_at <= ?");
159
+ params.push(effectiveUntil);
160
+ }
103
161
  clauses.push("(expires_at IS NULL OR expires_at > datetime('now'))");
104
162
  const where = clauses.length ? `WHERE ${clauses.join(" AND ")}` : "";
105
163
  params.push(fetchLimit);
106
- const rows = ctx.db.prepare(`SELECT * FROM vault ${where} ORDER BY created_at DESC LIMIT ?`).all(...params);
164
+ const rows = ctx.db
165
+ .prepare(`SELECT * FROM vault ${where} ORDER BY created_at DESC LIMIT ?`)
166
+ .all(...params);
107
167
 
108
168
  // Post-filter by tags if provided, then apply requested limit
109
169
  filtered = tags?.length
110
- ? rows.filter((r) => {
111
- const entryTags = r.tags ? JSON.parse(r.tags) : [];
112
- return tags.some((t) => entryTags.includes(t));
113
- }).slice(0, effectiveLimit)
170
+ ? rows
171
+ .filter((r) => {
172
+ const entryTags = r.tags ? JSON.parse(r.tags) : [];
173
+ return tags.some((t) => entryTags.includes(t));
174
+ })
175
+ .slice(0, effectiveLimit)
114
176
  : rows;
115
177
 
116
178
  // Add score field for consistent output
117
179
  for (const r of filtered) r.score = 0;
118
180
  }
119
181
 
120
- if (!filtered.length) return ok(hasQuery ? "No results found for: " + query : "No entries found matching the given filters.");
182
+ if (!filtered.length)
183
+ return ok(
184
+ hasQuery
185
+ ? "No results found for: " + query
186
+ : "No entries found matching the given filters.",
187
+ );
121
188
 
122
189
  // Decrypt encrypted entries if ctx.decrypt is available
123
190
  if (ctx.decrypt) {
@@ -132,22 +199,37 @@ export async function handler({ query, kind, category, identity_key, tags, since
132
199
  }
133
200
 
134
201
  const lines = [];
135
- if (reindexFailed) lines.push(`> **Warning:** Auto-reindex failed. Results may be stale. Run \`context-vault reindex\` to fix.\n`);
136
- if (hasQuery && isEmbedAvailable() === false) lines.push(`> **Note:** Semantic search unavailable — results ranked by keyword match only. Run \`context-vault setup\` to download the embedding model.\n`);
202
+ if (reindexFailed)
203
+ lines.push(
204
+ `> **Warning:** Auto-reindex failed. Results may be stale. Run \`context-vault reindex\` to fix.\n`,
205
+ );
206
+ if (hasQuery && isEmbedAvailable() === false)
207
+ lines.push(
208
+ `> **Note:** Semantic search unavailable — results ranked by keyword match only. Run \`context-vault setup\` to download the embedding model.\n`,
209
+ );
137
210
  const heading = hasQuery ? `Results for "${query}"` : "Filtered entries";
138
211
  lines.push(`## ${heading} (${filtered.length} matches)\n`);
139
212
  for (let i = 0; i < filtered.length; i++) {
140
213
  const r = filtered[i];
141
214
  const entryTags = r.tags ? JSON.parse(r.tags) : [];
142
215
  const tagStr = entryTags.length ? entryTags.join(", ") : "none";
143
- const relPath = r.file_path && config.vaultDir ? r.file_path.replace(config.vaultDir + "/", "") : r.file_path || "n/a";
144
- lines.push(`### [${i + 1}/${filtered.length}] ${r.title || "(untitled)"} [${r.kind}/${r.category}]`);
145
- lines.push(`${r.score.toFixed(3)} · ${tagStr} · ${relPath} · id: \`${r.id}\``);
216
+ const relPath =
217
+ r.file_path && config.vaultDir
218
+ ? r.file_path.replace(config.vaultDir + "/", "")
219
+ : r.file_path || "n/a";
220
+ lines.push(
221
+ `### [${i + 1}/${filtered.length}] ${r.title || "(untitled)"} [${r.kind}/${r.category}]`,
222
+ );
223
+ lines.push(
224
+ `${r.score.toFixed(3)} · ${tagStr} · ${relPath} · id: \`${r.id}\``,
225
+ );
146
226
  lines.push(r.body?.slice(0, 300) + (r.body?.length > 300 ? "..." : ""));
147
227
  lines.push("");
148
228
  }
149
229
  if (autoWindowed) {
150
- lines.push(`_Showing events from last ${config.eventDecayDays || 30} days. Use since/until for custom range._`);
230
+ lines.push(
231
+ `_Showing events from last ${config.eventDecayDays || 30} days. Use since/until for custom range._`,
232
+ );
151
233
  }
152
234
  return ok(lines.join("\n"));
153
235
  }
@@ -1,13 +1,13 @@
1
1
  import { z } from "zod";
2
2
  import { captureAndIndex } from "../../capture/index.js";
3
- import { indexEntry } from "../../index/index.js";
4
3
  import { ok, err, ensureVaultExists } from "../helpers.js";
4
+ import {
5
+ MAX_KIND_LENGTH,
6
+ MAX_TAG_LENGTH,
7
+ MAX_TAGS_COUNT,
8
+ } from "../../constants.js";
5
9
 
6
- // ─── Input size limits (mirrors hosted validation) ────────────────────────────
7
10
  const MAX_URL_LENGTH = 2048;
8
- const MAX_KIND_LENGTH = 64;
9
- const MAX_TAG_LENGTH = 100;
10
- const MAX_TAGS_COUNT = 20;
11
11
 
12
12
  export const name = "ingest_url";
13
13
 
@@ -25,26 +25,40 @@ export const inputSchema = {
25
25
  * @param {import('../types.js').BaseCtx & Partial<import('../types.js').HostedCtxExtensions>} ctx
26
26
  * @param {import('../types.js').ToolShared} shared
27
27
  */
28
- export async function handler({ url: targetUrl, kind, tags }, ctx, { ensureIndexed }) {
28
+ export async function handler(
29
+ { url: targetUrl, kind, tags },
30
+ ctx,
31
+ { ensureIndexed },
32
+ ) {
29
33
  const { config } = ctx;
30
34
  const userId = ctx.userId !== undefined ? ctx.userId : undefined;
31
35
 
32
36
  const vaultErr = ensureVaultExists(config);
33
37
  if (vaultErr) return vaultErr;
34
38
 
35
- if (!targetUrl?.trim()) return err("Required: url (non-empty string)", "INVALID_INPUT");
36
- if (targetUrl.length > MAX_URL_LENGTH) return err(`url must be under ${MAX_URL_LENGTH} chars`, "INVALID_INPUT");
39
+ if (!targetUrl?.trim())
40
+ return err("Required: url (non-empty string)", "INVALID_INPUT");
41
+ if (targetUrl.length > MAX_URL_LENGTH)
42
+ return err(`url must be under ${MAX_URL_LENGTH} chars`, "INVALID_INPUT");
37
43
  if (kind !== undefined && kind !== null) {
38
44
  if (typeof kind !== "string" || kind.length > MAX_KIND_LENGTH) {
39
- return err(`kind must be a string, max ${MAX_KIND_LENGTH} chars`, "INVALID_INPUT");
45
+ return err(
46
+ `kind must be a string, max ${MAX_KIND_LENGTH} chars`,
47
+ "INVALID_INPUT",
48
+ );
40
49
  }
41
50
  }
42
51
  if (tags !== undefined && tags !== null) {
43
- if (!Array.isArray(tags)) return err("tags must be an array of strings", "INVALID_INPUT");
44
- if (tags.length > MAX_TAGS_COUNT) return err(`tags: max ${MAX_TAGS_COUNT} tags allowed`, "INVALID_INPUT");
52
+ if (!Array.isArray(tags))
53
+ return err("tags must be an array of strings", "INVALID_INPUT");
54
+ if (tags.length > MAX_TAGS_COUNT)
55
+ return err(`tags: max ${MAX_TAGS_COUNT} tags allowed`, "INVALID_INPUT");
45
56
  for (const tag of tags) {
46
57
  if (typeof tag !== "string" || tag.length > MAX_TAG_LENGTH) {
47
- return err(`each tag must be a string, max ${MAX_TAG_LENGTH} chars`, "INVALID_INPUT");
58
+ return err(
59
+ `each tag must be a string, max ${MAX_TAG_LENGTH} chars`,
60
+ "INVALID_INPUT",
61
+ );
48
62
  }
49
63
  }
50
64
  }
@@ -55,15 +69,20 @@ export async function handler({ url: targetUrl, kind, tags }, ctx, { ensureIndex
55
69
  if (ctx.checkLimits) {
56
70
  const usage = ctx.checkLimits();
57
71
  if (usage.entryCount >= usage.maxEntries) {
58
- return err(`Entry limit reached (${usage.maxEntries}). Upgrade to Pro for unlimited entries.`, "LIMIT_EXCEEDED");
72
+ return err(
73
+ `Entry limit reached (${usage.maxEntries}). Upgrade to Pro for unlimited entries.`,
74
+ "LIMIT_EXCEEDED",
75
+ );
59
76
  }
60
77
  }
61
78
 
62
79
  try {
63
80
  const { ingestUrl } = await import("../../capture/ingest-url.js");
64
81
  const entryData = await ingestUrl(targetUrl, { kind, tags });
65
- const entry = await captureAndIndex(ctx, { ...entryData, userId }, indexEntry);
66
- const relPath = entry.filePath ? entry.filePath.replace(config.vaultDir + "/", "") : entry.filePath;
82
+ const entry = await captureAndIndex(ctx, { ...entryData, userId });
83
+ const relPath = entry.filePath
84
+ ? entry.filePath.replace(config.vaultDir + "/", "")
85
+ : entry.filePath;
67
86
  const parts = [
68
87
  `✓ Ingested URL → ${relPath}`,
69
88
  ` id: ${entry.id}`,
@@ -8,12 +8,30 @@ export const description =
8
8
  "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.";
9
9
 
10
10
  export const inputSchema = {
11
- kind: z.string().optional().describe("Filter by kind (e.g. 'insight', 'decision', 'pattern')"),
12
- category: z.enum(["knowledge", "entity", "event"]).optional().describe("Filter by category"),
13
- tags: z.array(z.string()).optional().describe("Filter by tags (entries must match at least one)"),
14
- since: z.string().optional().describe("ISO date, return entries created after this"),
15
- until: z.string().optional().describe("ISO date, return entries created before this"),
16
- limit: z.number().optional().describe("Max results to return (default 20, max 100)"),
11
+ kind: z
12
+ .string()
13
+ .optional()
14
+ .describe("Filter by kind (e.g. 'insight', 'decision', 'pattern')"),
15
+ category: z
16
+ .enum(["knowledge", "entity", "event"])
17
+ .optional()
18
+ .describe("Filter by category"),
19
+ tags: z
20
+ .array(z.string())
21
+ .optional()
22
+ .describe("Filter by tags (entries must match at least one)"),
23
+ since: z
24
+ .string()
25
+ .optional()
26
+ .describe("ISO date, return entries created after this"),
27
+ until: z
28
+ .string()
29
+ .optional()
30
+ .describe("ISO date, return entries created before this"),
31
+ limit: z
32
+ .number()
33
+ .optional()
34
+ .describe("Max results to return (default 20, max 100)"),
17
35
  offset: z.number().optional().describe("Skip first N results for pagination"),
18
36
  };
19
37
 
@@ -22,7 +40,11 @@ export const inputSchema = {
22
40
  * @param {import('../types.js').BaseCtx & Partial<import('../types.js').HostedCtxExtensions>} ctx
23
41
  * @param {import('../types.js').ToolShared} shared
24
42
  */
25
- export async function handler({ kind, category, tags, since, until, limit, offset }, ctx, { ensureIndexed, reindexFailed }) {
43
+ export async function handler(
44
+ { kind, category, tags, since, until, limit, offset },
45
+ ctx,
46
+ { ensureIndexed, reindexFailed },
47
+ ) {
26
48
  const { config } = ctx;
27
49
  const userId = ctx.userId !== undefined ? ctx.userId : undefined;
28
50
 
@@ -60,33 +82,52 @@ export async function handler({ kind, category, tags, since, until, limit, offse
60
82
  const fetchLimit = tags?.length ? effectiveLimit * 10 : effectiveLimit;
61
83
 
62
84
  const countParams = [...params];
63
- const total = ctx.db.prepare(`SELECT COUNT(*) as c FROM vault ${where}`).get(...countParams).c;
85
+ const total = ctx.db
86
+ .prepare(`SELECT COUNT(*) as c FROM vault ${where}`)
87
+ .get(...countParams).c;
64
88
 
65
89
  params.push(fetchLimit, effectiveOffset);
66
- 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);
90
+ const rows = ctx.db
91
+ .prepare(
92
+ `SELECT id, title, kind, category, tags, created_at, SUBSTR(body, 1, 120) as preview FROM vault ${where} ORDER BY created_at DESC LIMIT ? OFFSET ?`,
93
+ )
94
+ .all(...params);
67
95
 
68
96
  // Post-filter by tags if provided, then apply requested limit
69
97
  const filtered = tags?.length
70
- ? rows.filter((r) => {
71
- const entryTags = r.tags ? JSON.parse(r.tags) : [];
72
- return tags.some((t) => entryTags.includes(t));
73
- }).slice(0, effectiveLimit)
98
+ ? rows
99
+ .filter((r) => {
100
+ const entryTags = r.tags ? JSON.parse(r.tags) : [];
101
+ return tags.some((t) => entryTags.includes(t));
102
+ })
103
+ .slice(0, effectiveLimit)
74
104
  : rows;
75
105
 
76
- if (!filtered.length) return ok("No entries found matching the given filters.");
106
+ if (!filtered.length)
107
+ return ok("No entries found matching the given filters.");
77
108
 
78
109
  const lines = [];
79
- if (reindexFailed) lines.push(`> **Warning:** Auto-reindex failed. Results may be stale. Run \`context-vault reindex\` to fix.\n`);
110
+ if (reindexFailed)
111
+ lines.push(
112
+ `> **Warning:** Auto-reindex failed. Results may be stale. Run \`context-vault reindex\` to fix.\n`,
113
+ );
80
114
  lines.push(`## Vault Entries (${filtered.length} shown, ${total} total)\n`);
81
115
  for (const r of filtered) {
82
116
  const entryTags = r.tags ? JSON.parse(r.tags) : [];
83
117
  const tagStr = entryTags.length ? entryTags.join(", ") : "none";
84
- lines.push(`- **${r.title || "(untitled)"}** [${r.kind}/${r.category}] — ${tagStr} — ${r.created_at} — \`${r.id}\``);
85
- if (r.preview) lines.push(` ${r.preview.replace(/\n+/g, " ").trim()}${r.preview.length >= 120 ? "…" : ""}`);
118
+ lines.push(
119
+ `- **${r.title || "(untitled)"}** [${r.kind}/${r.category}] ${tagStr} ${r.created_at} \`${r.id}\``,
120
+ );
121
+ if (r.preview)
122
+ lines.push(
123
+ ` ${r.preview.replace(/\n+/g, " ").trim()}${r.preview.length >= 120 ? "…" : ""}`,
124
+ );
86
125
  }
87
126
 
88
127
  if (effectiveOffset + effectiveLimit < total) {
89
- lines.push(`\n_Page ${Math.floor(effectiveOffset / effectiveLimit) + 1}. Use offset: ${effectiveOffset + effectiveLimit} for next page._`);
128
+ lines.push(
129
+ `\n_Page ${Math.floor(effectiveOffset / effectiveLimit) + 1}. Use offset: ${effectiveOffset + effectiveLimit} for next page._`,
130
+ );
90
131
  }
91
132
 
92
133
  return ok(lines.join("\n"));