context-vault 3.1.6 → 3.1.8

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 (184) hide show
  1. package/bin/cli.js +1369 -1774
  2. package/dist/archive.d.ts +23 -0
  3. package/dist/archive.d.ts.map +1 -0
  4. package/dist/archive.js +197 -0
  5. package/dist/archive.js.map +1 -0
  6. package/dist/consolidation.d.ts +14 -0
  7. package/dist/consolidation.d.ts.map +1 -0
  8. package/dist/consolidation.js +59 -0
  9. package/dist/consolidation.js.map +1 -0
  10. package/dist/error-log.d.ts +4 -0
  11. package/dist/error-log.d.ts.map +1 -0
  12. package/dist/error-log.js +33 -0
  13. package/dist/error-log.js.map +1 -0
  14. package/dist/helpers.d.ts +10 -0
  15. package/dist/helpers.d.ts.map +1 -0
  16. package/dist/helpers.js +42 -0
  17. package/dist/helpers.js.map +1 -0
  18. package/dist/linking.d.ts +13 -0
  19. package/dist/linking.d.ts.map +1 -0
  20. package/dist/linking.js +86 -0
  21. package/dist/linking.js.map +1 -0
  22. package/dist/migrate-dirs.d.ts +16 -0
  23. package/dist/migrate-dirs.d.ts.map +1 -0
  24. package/dist/migrate-dirs.js +127 -0
  25. package/dist/migrate-dirs.js.map +1 -0
  26. package/dist/register-tools.d.ts +3 -0
  27. package/dist/register-tools.d.ts.map +1 -0
  28. package/dist/register-tools.js +161 -0
  29. package/dist/register-tools.js.map +1 -0
  30. package/dist/server.d.ts +3 -0
  31. package/dist/server.d.ts.map +1 -0
  32. package/dist/server.js +241 -0
  33. package/dist/server.js.map +1 -0
  34. package/dist/status.d.ts +18 -0
  35. package/dist/status.d.ts.map +1 -0
  36. package/dist/status.js +265 -0
  37. package/dist/status.js.map +1 -0
  38. package/dist/telemetry.d.ts +6 -0
  39. package/dist/telemetry.d.ts.map +1 -0
  40. package/dist/telemetry.js +74 -0
  41. package/dist/telemetry.js.map +1 -0
  42. package/dist/temporal.d.ts +9 -0
  43. package/dist/temporal.d.ts.map +1 -0
  44. package/dist/temporal.js +76 -0
  45. package/dist/temporal.js.map +1 -0
  46. package/dist/tools/clear-context.d.ts +11 -0
  47. package/dist/tools/clear-context.d.ts.map +1 -0
  48. package/dist/tools/clear-context.js +28 -0
  49. package/dist/tools/clear-context.js.map +1 -0
  50. package/dist/tools/context-status.d.ts +6 -0
  51. package/dist/tools/context-status.d.ts.map +1 -0
  52. package/dist/tools/context-status.js +160 -0
  53. package/dist/tools/context-status.js.map +1 -0
  54. package/dist/tools/create-snapshot.d.ts +13 -0
  55. package/dist/tools/create-snapshot.d.ts.map +1 -0
  56. package/dist/tools/create-snapshot.js +161 -0
  57. package/dist/tools/create-snapshot.js.map +1 -0
  58. package/dist/tools/delete-context.d.ts +9 -0
  59. package/dist/tools/delete-context.d.ts.map +1 -0
  60. package/dist/tools/delete-context.js +45 -0
  61. package/dist/tools/delete-context.js.map +1 -0
  62. package/dist/tools/get-context.d.ts +85 -0
  63. package/dist/tools/get-context.d.ts.map +1 -0
  64. package/dist/tools/get-context.js +576 -0
  65. package/dist/tools/get-context.js.map +1 -0
  66. package/dist/tools/ingest-project.d.ts +11 -0
  67. package/dist/tools/ingest-project.d.ts.map +1 -0
  68. package/dist/tools/ingest-project.js +226 -0
  69. package/dist/tools/ingest-project.js.map +1 -0
  70. package/dist/tools/ingest-url.d.ts +11 -0
  71. package/dist/tools/ingest-url.d.ts.map +1 -0
  72. package/dist/tools/ingest-url.js +62 -0
  73. package/dist/tools/ingest-url.js.map +1 -0
  74. package/dist/tools/list-buckets.d.ts +9 -0
  75. package/dist/tools/list-buckets.d.ts.map +1 -0
  76. package/dist/tools/list-buckets.js +76 -0
  77. package/dist/tools/list-buckets.js.map +1 -0
  78. package/dist/tools/list-context.d.ts +19 -0
  79. package/dist/tools/list-context.d.ts.map +1 -0
  80. package/dist/tools/list-context.js +110 -0
  81. package/dist/tools/list-context.js.map +1 -0
  82. package/dist/tools/save-context.d.ts +36 -0
  83. package/dist/tools/save-context.d.ts.map +1 -0
  84. package/dist/tools/save-context.js +458 -0
  85. package/dist/tools/save-context.js.map +1 -0
  86. package/dist/tools/session-start.d.ts +11 -0
  87. package/dist/tools/session-start.d.ts.map +1 -0
  88. package/dist/tools/session-start.js +224 -0
  89. package/dist/tools/session-start.js.map +1 -0
  90. package/dist/types.d.ts +37 -0
  91. package/dist/types.d.ts.map +1 -0
  92. package/dist/types.js +2 -0
  93. package/dist/types.js.map +1 -0
  94. package/node_modules/@context-vault/core/dist/capture.d.ts +1 -1
  95. package/node_modules/@context-vault/core/dist/capture.d.ts.map +1 -1
  96. package/node_modules/@context-vault/core/dist/capture.js +34 -47
  97. package/node_modules/@context-vault/core/dist/capture.js.map +1 -1
  98. package/node_modules/@context-vault/core/dist/categories.js +30 -30
  99. package/node_modules/@context-vault/core/dist/config.d.ts +1 -1
  100. package/node_modules/@context-vault/core/dist/config.d.ts.map +1 -1
  101. package/node_modules/@context-vault/core/dist/config.js +37 -43
  102. package/node_modules/@context-vault/core/dist/config.js.map +1 -1
  103. package/node_modules/@context-vault/core/dist/constants.d.ts +1 -1
  104. package/node_modules/@context-vault/core/dist/constants.d.ts.map +1 -1
  105. package/node_modules/@context-vault/core/dist/constants.js +4 -4
  106. package/node_modules/@context-vault/core/dist/constants.js.map +1 -1
  107. package/node_modules/@context-vault/core/dist/db.d.ts +2 -2
  108. package/node_modules/@context-vault/core/dist/db.d.ts.map +1 -1
  109. package/node_modules/@context-vault/core/dist/db.js +21 -20
  110. package/node_modules/@context-vault/core/dist/db.js.map +1 -1
  111. package/node_modules/@context-vault/core/dist/embed.d.ts.map +1 -1
  112. package/node_modules/@context-vault/core/dist/embed.js +11 -11
  113. package/node_modules/@context-vault/core/dist/embed.js.map +1 -1
  114. package/node_modules/@context-vault/core/dist/files.d.ts.map +1 -1
  115. package/node_modules/@context-vault/core/dist/files.js +12 -13
  116. package/node_modules/@context-vault/core/dist/files.js.map +1 -1
  117. package/node_modules/@context-vault/core/dist/formatters.js +5 -5
  118. package/node_modules/@context-vault/core/dist/frontmatter.d.ts.map +1 -1
  119. package/node_modules/@context-vault/core/dist/frontmatter.js +23 -23
  120. package/node_modules/@context-vault/core/dist/frontmatter.js.map +1 -1
  121. package/node_modules/@context-vault/core/dist/index.d.ts +1 -1
  122. package/node_modules/@context-vault/core/dist/index.d.ts.map +1 -1
  123. package/node_modules/@context-vault/core/dist/index.js +58 -46
  124. package/node_modules/@context-vault/core/dist/index.js.map +1 -1
  125. package/node_modules/@context-vault/core/dist/ingest-url.d.ts.map +1 -1
  126. package/node_modules/@context-vault/core/dist/ingest-url.js +30 -33
  127. package/node_modules/@context-vault/core/dist/ingest-url.js.map +1 -1
  128. package/node_modules/@context-vault/core/dist/main.d.ts +13 -13
  129. package/node_modules/@context-vault/core/dist/main.d.ts.map +1 -1
  130. package/node_modules/@context-vault/core/dist/main.js +12 -12
  131. package/node_modules/@context-vault/core/dist/main.js.map +1 -1
  132. package/node_modules/@context-vault/core/dist/search.d.ts +1 -1
  133. package/node_modules/@context-vault/core/dist/search.d.ts.map +1 -1
  134. package/node_modules/@context-vault/core/dist/search.js +20 -22
  135. package/node_modules/@context-vault/core/dist/search.js.map +1 -1
  136. package/node_modules/@context-vault/core/dist/types.d.ts +1 -1
  137. package/node_modules/@context-vault/core/package.json +1 -1
  138. package/node_modules/@context-vault/core/src/capture.ts +44 -81
  139. package/node_modules/@context-vault/core/src/categories.ts +30 -30
  140. package/node_modules/@context-vault/core/src/config.ts +45 -60
  141. package/node_modules/@context-vault/core/src/constants.ts +8 -10
  142. package/node_modules/@context-vault/core/src/db.ts +37 -56
  143. package/node_modules/@context-vault/core/src/embed.ts +15 -26
  144. package/node_modules/@context-vault/core/src/files.ts +13 -16
  145. package/node_modules/@context-vault/core/src/formatters.ts +5 -5
  146. package/node_modules/@context-vault/core/src/frontmatter.ts +26 -30
  147. package/node_modules/@context-vault/core/src/index.ts +94 -100
  148. package/node_modules/@context-vault/core/src/ingest-url.ts +56 -93
  149. package/node_modules/@context-vault/core/src/main.ts +13 -18
  150. package/node_modules/@context-vault/core/src/search.ts +34 -56
  151. package/node_modules/@context-vault/core/src/types.ts +1 -1
  152. package/package.json +10 -4
  153. package/scripts/postinstall.js +18 -25
  154. package/scripts/prepack.js +13 -19
  155. package/src/archive.ts +244 -0
  156. package/src/consolidation.ts +78 -0
  157. package/src/{error-log.js → error-log.ts} +10 -10
  158. package/src/helpers.ts +61 -0
  159. package/src/{linking.js → linking.ts} +22 -20
  160. package/src/migrate-dirs.ts +152 -0
  161. package/src/register-tools.ts +183 -0
  162. package/src/{server.js → server.ts} +89 -109
  163. package/src/{status.js → status.ts} +94 -108
  164. package/src/telemetry.ts +80 -0
  165. package/src/{temporal.js → temporal.ts} +29 -33
  166. package/src/tools/clear-context.ts +41 -0
  167. package/src/tools/{context-status.js → context-status.ts} +43 -66
  168. package/src/tools/{create-snapshot.js → create-snapshot.ts} +54 -65
  169. package/src/tools/delete-context.ts +53 -0
  170. package/src/tools/{get-context.js → get-context.ts} +142 -205
  171. package/src/tools/ingest-project.ts +260 -0
  172. package/src/tools/ingest-url.ts +74 -0
  173. package/src/tools/{list-buckets.js → list-buckets.ts} +27 -37
  174. package/src/tools/{list-context.js → list-context.ts} +46 -71
  175. package/src/tools/{save-context.js → save-context.ts} +148 -204
  176. package/src/tools/{session-start.js → session-start.ts} +72 -79
  177. package/src/types.ts +29 -0
  178. package/src/helpers.js +0 -57
  179. package/src/register-tools.js +0 -175
  180. package/src/telemetry.js +0 -80
  181. package/src/tools/clear-context.js +0 -47
  182. package/src/tools/delete-context.js +0 -54
  183. package/src/tools/ingest-project.js +0 -272
  184. package/src/tools/ingest-url.js +0 -87
@@ -1,14 +1,15 @@
1
- import { z } from "zod";
2
- import { createHash } from "node:crypto";
3
- import { readFileSync, existsSync } from "node:fs";
4
- import { resolve } from "node:path";
5
- import { hybridSearch } from "@context-vault/core/search";
6
- import { categoryFor } from "@context-vault/core/categories";
7
- import { normalizeKind } from "@context-vault/core/files";
8
- import { resolveTemporalParams } from "../temporal.js";
9
- import { collectLinkedEntries } from "../linking.js";
10
- import { ok, err, errWithHint } from "../helpers.js";
11
- import { isEmbedAvailable } from "@context-vault/core/embed";
1
+ import { z } from 'zod';
2
+ import { createHash } from 'node:crypto';
3
+ import { readFileSync, existsSync } from 'node:fs';
4
+ import { resolve } from 'node:path';
5
+ import { hybridSearch } from '@context-vault/core/search';
6
+ import { categoryFor } from '@context-vault/core/categories';
7
+ import { normalizeKind } from '@context-vault/core/files';
8
+ import { resolveTemporalParams } from '../temporal.js';
9
+ import { collectLinkedEntries } from '../linking.js';
10
+ import { ok, err, errWithHint } from '../helpers.js';
11
+ import { isEmbedAvailable } from '@context-vault/core/embed';
12
+ import type { LocalCtx, SharedCtx, ToolResult } from '../types.js';
12
13
 
13
14
  const STALE_DUPLICATE_DAYS = 7;
14
15
  const DEFAULT_PIVOT_COUNT = 2;
@@ -21,22 +22,19 @@ const BRIEF_SCORE_BOOST = 0.05;
21
22
  * Truncate a body string to ~SKELETON_BODY_CHARS, breaking at sentence or
22
23
  * word boundary. Returns the truncated string with "..." appended.
23
24
  */
24
- export function skeletonBody(body) {
25
- if (!body) return "";
25
+ export function skeletonBody(body: string | null | undefined): string {
26
+ if (!body) return '';
26
27
  if (body.length <= SKELETON_BODY_CHARS) return body;
27
28
  const slice = body.slice(0, SKELETON_BODY_CHARS);
28
- const sentenceEnd = Math.max(
29
- slice.lastIndexOf(". "),
30
- slice.lastIndexOf(".\n"),
31
- );
29
+ const sentenceEnd = Math.max(slice.lastIndexOf('. '), slice.lastIndexOf('.\n'));
32
30
  if (sentenceEnd > SKELETON_BODY_CHARS * 0.4) {
33
- return slice.slice(0, sentenceEnd + 1) + "...";
31
+ return slice.slice(0, sentenceEnd + 1) + '...';
34
32
  }
35
- const wordEnd = slice.lastIndexOf(" ");
33
+ const wordEnd = slice.lastIndexOf(' ');
36
34
  if (wordEnd > SKELETON_BODY_CHARS * 0.4) {
37
- return slice.slice(0, wordEnd) + "...";
35
+ return slice.slice(0, wordEnd) + '...';
38
36
  }
39
- return slice + "...";
37
+ return slice + '...';
40
38
  }
41
39
 
42
40
  /**
@@ -53,10 +51,13 @@ export function skeletonBody(body) {
53
51
  * rows already fetched from the DB.
54
52
  *
55
53
  * @param {Array} entries - Result rows (as returned by hybridSearch / filter-only mode)
56
- * @param {import('../types.js').BaseCtx} _ctx - Unused for now; reserved for future DB look-ups
54
+ * @param {import('@context-vault/core/types').BaseCtx} _ctx
57
55
  * @returns {Array<{entry_a_id: string, entry_b_id: string, reason: string, recommendation: string}>}
58
56
  */
59
- export function detectConflicts(entries, _ctx) {
57
+ export function detectConflicts(
58
+ entries: any[],
59
+ _ctx: any
60
+ ): Array<{ entry_a_id: string; entry_b_id: string; reason: string; recommendation: string }> {
60
61
  const conflicts = [];
61
62
  const idSet = new Set(entries.map((e) => e.id));
62
63
 
@@ -65,15 +66,13 @@ export function detectConflicts(entries, _ctx) {
65
66
  conflicts.push({
66
67
  entry_a_id: entry.id,
67
68
  entry_b_id: entry.superseded_by,
68
- reason: "superseded",
69
+ reason: 'superseded',
69
70
  recommendation: `Discard \`${entry.id}\` — it has been explicitly superseded by \`${entry.superseded_by}\`.`,
70
71
  });
71
72
  }
72
73
  }
73
74
 
74
- const supersededConflictPairs = new Set(
75
- conflicts.map((c) => `${c.entry_a_id}|${c.entry_b_id}`),
76
- );
75
+ const supersededConflictPairs = new Set(conflicts.map((c) => `${c.entry_a_id}|${c.entry_b_id}`));
77
76
 
78
77
  for (let i = 0; i < entries.length; i++) {
79
78
  for (let j = i + 1; j < entries.length; j++) {
@@ -95,21 +94,21 @@ export function detectConflicts(entries, _ctx) {
95
94
  if (!tagsA.length || !tagsB.length) continue;
96
95
 
97
96
  const tagsSetA = new Set(tagsA);
98
- const sharedTag = tagsB.some((t) => tagsSetA.has(t));
97
+ const sharedTag = tagsB.some((t: any) => tagsSetA.has(t));
99
98
  if (!sharedTag) continue;
100
99
 
101
100
  const dateA = new Date(a.updated_at || a.created_at);
102
101
  const dateB = new Date(b.updated_at || b.created_at);
103
102
  if (isNaN(dateA.getTime()) || isNaN(dateB.getTime())) continue;
104
103
 
105
- const diffDays = Math.abs(dateA - dateB) / 86400000;
104
+ const diffDays = Math.abs(dateA.getTime() - dateB.getTime()) / 86400000;
106
105
  if (diffDays <= STALE_DUPLICATE_DAYS) continue;
107
106
 
108
107
  const [older, newer] = dateA < dateB ? [a, b] : [b, a];
109
108
  conflicts.push({
110
109
  entry_a_id: older.id,
111
110
  entry_b_id: newer.id,
112
- reason: "stale_duplicate",
111
+ reason: 'stale_duplicate',
113
112
  recommendation: `Verify \`${older.id}\` is still accurate — it shares kind "${older.kind}" and tags with \`${newer.id}\` but was last updated ${Math.round(diffDays)} days earlier.`,
114
113
  });
115
114
  }
@@ -134,13 +133,17 @@ export function detectConflicts(entries, _ctx) {
134
133
  * @param {{ tagThreshold?: number, maxAgeDays?: number }} opts - Configurable thresholds
135
134
  * @returns {Array<{tag: string, entry_count: number, last_snapshot_age_days: number|null}>}
136
135
  */
137
- export function detectConsolidationHints(entries, db, opts = {}) {
136
+ export function detectConsolidationHints(
137
+ entries: any[],
138
+ db: any,
139
+ opts: { tagThreshold?: number; maxAgeDays?: number } = {}
140
+ ): Array<{ tag: string; entry_count: number; last_snapshot_age_days: number | null }> {
138
141
  const tagThreshold = opts.tagThreshold ?? CONSOLIDATION_TAG_THRESHOLD;
139
142
  const maxAgeDays = opts.maxAgeDays ?? CONSOLIDATION_SNAPSHOT_MAX_AGE_DAYS;
140
143
 
141
- const candidateTags = new Set();
144
+ const candidateTags = new Set<string>();
142
145
  for (const entry of entries) {
143
- if (entry.kind === "brief") continue;
146
+ if (entry.kind === 'brief') continue;
144
147
  const entryTags = entry.tags ? JSON.parse(entry.tags) : [];
145
148
  for (const tag of entryTags) candidateTags.add(tag);
146
149
  }
@@ -153,15 +156,11 @@ export function detectConsolidationHints(entries, db, opts = {}) {
153
156
  for (const tag of candidateTags) {
154
157
  let vaultCount = 0;
155
158
  try {
156
- // When userId is defined (hosted mode), scope to that user.
157
- // When userId is undefined (local mode), no user scoping — column may not exist.
158
- const userClause = "";
159
- const countParams = false ? [`%"${tag}"%`] : [`%"${tag}"%`];
160
159
  const countRow = db
161
160
  .prepare(
162
- `SELECT COUNT(*) as c FROM vault WHERE kind != 'brief' AND tags LIKE ?${userClause} AND (expires_at IS NULL OR expires_at > datetime('now')) AND superseded_by IS NULL`,
161
+ `SELECT COUNT(*) as c FROM vault WHERE kind != 'brief' AND tags LIKE ? AND (expires_at IS NULL OR expires_at > datetime('now')) AND superseded_by IS NULL`
163
162
  )
164
- .get(...countParams);
163
+ .get(`%"${tag}"%`);
165
164
  vaultCount = countRow?.c ?? 0;
166
165
  } catch {
167
166
  continue;
@@ -171,17 +170,15 @@ export function detectConsolidationHints(entries, db, opts = {}) {
171
170
 
172
171
  let lastSnapshotAgeDays = null;
173
172
  try {
174
- const userClause = "";
175
- const params = false ? [`%"${tag}"%`] : [`%"${tag}"%`];
176
173
  const recentBrief = db
177
174
  .prepare(
178
- `SELECT created_at FROM vault WHERE kind = 'brief' AND tags LIKE ?${userClause} ORDER BY created_at DESC LIMIT 1`,
175
+ `SELECT created_at FROM vault WHERE kind = 'brief' AND tags LIKE ? ORDER BY created_at DESC LIMIT 1`
179
176
  )
180
- .get(...params);
177
+ .get(`%"${tag}"%`);
181
178
 
182
179
  if (recentBrief) {
183
180
  lastSnapshotAgeDays = Math.round(
184
- (Date.now() - new Date(recentBrief.created_at).getTime()) / 86400000,
181
+ (Date.now() - new Date(recentBrief.created_at).getTime()) / 86400000
185
182
  );
186
183
  if (recentBrief.created_at >= cutoff) continue;
187
184
  }
@@ -207,7 +204,7 @@ export function detectConsolidationHints(entries, db, opts = {}) {
207
204
  * @param {object} entry - DB row with source_files JSON column
208
205
  * @returns {{ stale: boolean, stale_reason: string } | null}
209
206
  */
210
- function checkStaleness(entry) {
207
+ function checkStaleness(entry: any): { stale: boolean; stale_reason: string } | null {
211
208
  if (!entry.source_files) return null;
212
209
  let sourceFiles;
213
210
  try {
@@ -219,18 +216,16 @@ function checkStaleness(entry) {
219
216
 
220
217
  for (const sf of sourceFiles) {
221
218
  try {
222
- const absPath = sf.path.startsWith("/")
223
- ? sf.path
224
- : resolve(process.cwd(), sf.path);
219
+ const absPath = sf.path.startsWith('/') ? sf.path : resolve(process.cwd(), sf.path);
225
220
  if (!existsSync(absPath)) {
226
- return { stale: true, stale_reason: "source file not found" };
221
+ return { stale: true, stale_reason: 'source file not found' };
227
222
  }
228
223
  const contents = readFileSync(absPath);
229
- const currentHash = createHash("sha256").update(contents).digest("hex");
224
+ const currentHash = createHash('sha256').update(contents).digest('hex');
230
225
  if (currentHash !== sf.hash) {
231
226
  return {
232
227
  stale: true,
233
- stale_reason: "source file modified since observation",
228
+ stale_reason: 'source file modified since observation',
234
229
  };
235
230
  }
236
231
  } catch {
@@ -240,115 +235,98 @@ function checkStaleness(entry) {
240
235
  return null;
241
236
  }
242
237
 
243
- export const name = "get_context";
238
+ export const name = 'get_context';
244
239
 
245
240
  export const description =
246
- "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.";
241
+ '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.';
247
242
 
248
243
  export const inputSchema = {
249
244
  query: z
250
245
  .string()
251
246
  .optional()
252
247
  .describe(
253
- "Search query (natural language or keywords). Optional if filters (tags, kind, category) are provided.",
248
+ 'Search query (natural language or keywords). Optional if filters (tags, kind, category) are provided.'
254
249
  ),
255
- kind: z
256
- .string()
257
- .optional()
258
- .describe("Filter by kind (e.g. 'insight', 'decision', 'pattern')"),
259
- category: z
260
- .enum(["knowledge", "entity", "event"])
261
- .optional()
262
- .describe("Filter by category"),
250
+ kind: z.string().optional().describe("Filter by kind (e.g. 'insight', 'decision', 'pattern')"),
251
+ category: z.enum(['knowledge', 'entity', 'event']).optional().describe('Filter by category'),
263
252
  identity_key: z
264
253
  .string()
265
254
  .optional()
266
- .describe("For entity lookup: exact match on identity key. Requires kind."),
255
+ .describe('For entity lookup: exact match on identity key. Requires kind.'),
267
256
  tags: z
268
257
  .array(z.string())
269
258
  .optional()
270
259
  .describe(
271
- "Filter by tags (entries must match at least one). Use 'bucket:' prefixed tags for project-scoped retrieval (e.g., ['bucket:autohub']).",
260
+ "Filter by tags (entries must match at least one). Use 'bucket:' prefixed tags for project-scoped retrieval (e.g., ['bucket:autohub'])."
272
261
  ),
273
262
  buckets: z
274
263
  .array(z.string())
275
264
  .optional()
276
265
  .describe(
277
- "Filter by project-scoped buckets. Each name expands to a 'bucket:<name>' tag. Composes with 'tags' via OR (entries matching any tag or any bucket are included).",
266
+ "Filter by project-scoped buckets. Each name expands to a 'bucket:<name>' tag. Composes with 'tags' via OR (entries matching any tag or any bucket are included)."
278
267
  ),
279
268
  since: z
280
269
  .string()
281
270
  .optional()
282
271
  .describe(
283
- "Return entries created after this date. Accepts ISO date strings (e.g. '2025-01-01') or natural shortcuts: 'today', 'yesterday', 'this_week', 'this_month', 'last_3_days', 'last_2_weeks', 'last_1_month'. Spaces and underscores are interchangeable.",
272
+ "Return entries created after this date. Accepts ISO date strings (e.g. '2025-01-01') or natural shortcuts: 'today', 'yesterday', 'this_week', 'this_month', 'last_3_days', 'last_2_weeks', 'last_1_month'. Spaces and underscores are interchangeable."
284
273
  ),
285
274
  until: z
286
275
  .string()
287
276
  .optional()
288
277
  .describe(
289
- "Return entries created before this date. Accepts ISO date strings or the same natural shortcuts as `since`. When `since` is 'yesterday' and `until` is omitted, `until` is automatically set to the end of yesterday.",
278
+ "Return entries created before this date. Accepts ISO date strings or the same natural shortcuts as `since`. When `since` is 'yesterday' and `until` is omitted, `until` is automatically set to the end of yesterday."
290
279
  ),
291
- limit: z
292
- .number()
293
- .max(500)
294
- .optional()
295
- .describe("Max results to return (default 10)"),
280
+ limit: z.number().max(500).optional().describe('Max results to return (default 10)'),
296
281
  include_superseded: z
297
282
  .boolean()
298
283
  .optional()
299
- .describe(
300
- "If true, include entries that have been superseded by newer ones. Default: false.",
301
- ),
284
+ .describe('If true, include entries that have been superseded by newer ones. Default: false.'),
302
285
  detect_conflicts: z
303
286
  .boolean()
304
287
  .optional()
305
288
  .describe(
306
- "If true, compare results for contradicting entries and append a conflicts array. Flags superseded entries still in results and stale duplicates (same kind+tags, updated_at >7 days apart). No LLM calls — pure DB logic.",
289
+ 'If true, compare results for contradicting entries and append a conflicts array. Flags superseded entries still in results and stale duplicates (same kind+tags, updated_at >7 days apart). No LLM calls — pure DB logic.'
307
290
  ),
308
291
  max_tokens: z
309
292
  .number()
310
293
  .max(100000)
311
294
  .optional()
312
295
  .describe(
313
- "Limit output to entries that fit within this token budget (rough estimate: 1 token ≈ 4 chars). Entries are packed greedily by relevance rank. At least 1 result is always returned. Response metadata includes tokens_used and tokens_budget.",
296
+ 'Limit output to entries that fit within this token budget (rough estimate: 1 token ≈ 4 chars). Entries are packed greedily by relevance rank. At least 1 result is always returned. Response metadata includes tokens_used and tokens_budget.'
314
297
  ),
315
298
  pivot_count: z
316
299
  .number()
317
300
  .optional()
318
301
  .describe(
319
- "Skeleton mode: top pivot_count entries by relevance are returned with full body. Remaining entries are returned as skeletons (title + tags + first ~100 chars of body). Default: 2. Set to 0 to skeleton all results, or a high number to disable.",
302
+ 'Skeleton mode: top pivot_count entries by relevance are returned with full body. Remaining entries are returned as skeletons (title + tags + first ~100 chars of body). Default: 2. Set to 0 to skeleton all results, or a high number to disable.'
320
303
  ),
321
304
  include_ephemeral: z
322
305
  .boolean()
323
306
  .optional()
324
307
  .describe(
325
- "If true, include ephemeral tier entries in results. Default: false — only working and durable tiers are returned.",
308
+ 'If true, include ephemeral tier entries in results. Default: false — only working and durable tiers are returned.'
326
309
  ),
327
310
  include_events: z
328
311
  .boolean()
329
312
  .optional()
330
313
  .describe(
331
- "If true, include event category entries in semantic search results. Default: false — events are excluded from query-based search but remain accessible via category/tag filters. Deprecated: prefer scope parameter.",
314
+ 'If true, include event category entries in semantic search results. Default: false — events are excluded from query-based search but remain accessible via category/tag filters. Deprecated: prefer scope parameter.'
332
315
  ),
333
316
  scope: z
334
- .enum(["hot", "events", "all"])
317
+ .enum(['hot', 'events', 'all'])
335
318
  .optional()
336
319
  .describe(
337
- "Index scope: 'hot' (default) — knowledge + entity entries only; 'events' — event entries only (cold index); 'all' — entire vault including events. Overrides include_events when set.",
320
+ "Index scope: 'hot' (default) — knowledge + entity entries only; 'events' — event entries only (cold index); 'all' — entire vault including events. Overrides include_events when set."
338
321
  ),
339
322
  follow_links: z
340
323
  .boolean()
341
324
  .optional()
342
325
  .describe(
343
- "If true, follow related_to links from result entries and include linked entries (forward links) and backlinks (entries that reference the results). Enables bidirectional graph traversal.",
326
+ 'If true, follow related_to links from result entries and include linked entries (forward links) and backlinks (entries that reference the results). Enables bidirectional graph traversal.'
344
327
  ),
345
328
  };
346
329
 
347
- /**
348
- * @param {object} args
349
- * @param {import('../types.js').BaseCtx & Partial<import('../types.js').HostedCtxExtensions>} ctx
350
- * @param {import('../types.js').ToolShared} shared
351
- */
352
330
  export async function handler(
353
331
  {
354
332
  query,
@@ -368,10 +346,10 @@ export async function handler(
368
346
  include_events,
369
347
  scope,
370
348
  follow_links,
371
- },
372
- ctx,
373
- { ensureIndexed, reindexFailed },
374
- ) {
349
+ }: Record<string, any>,
350
+ ctx: LocalCtx,
351
+ { ensureIndexed, reindexFailed }: SharedCtx
352
+ ): Promise<ToolResult> {
375
353
  const { config } = ctx;
376
354
 
377
355
  // Resolve natural-language temporal shortcuts → ISO date strings
@@ -387,28 +365,21 @@ export async function handler(
387
365
  // scope "all": no category restriction — full vault
388
366
  let effectiveScope = scope;
389
367
  if (!effectiveScope) {
390
- effectiveScope = include_events ? "all" : "hot";
368
+ effectiveScope = include_events ? 'all' : 'hot';
391
369
  }
392
370
 
393
371
  // Scope "events" forces category to "event" unless caller already set a narrower category
394
- const scopedCategory =
395
- !category && effectiveScope === "events" ? "event" : category;
396
- const shouldExcludeEvents =
397
- hasQuery && effectiveScope === "hot" && !scopedCategory;
372
+ const scopedCategory = !category && effectiveScope === 'events' ? 'event' : category;
373
+ const shouldExcludeEvents = hasQuery && effectiveScope === 'hot' && !scopedCategory;
398
374
  // Expand buckets to bucket: prefixed tags and merge with explicit tags
399
- const bucketTags = buckets?.length ? buckets.map((b) => `bucket:${b}`) : [];
375
+ const bucketTags = buckets?.length ? buckets.map((b: string) => `bucket:${b}`) : [];
400
376
  const effectiveTags = [...(tags ?? []), ...bucketTags];
401
377
  const hasFilters =
402
- kind ||
403
- scopedCategory ||
404
- effectiveTags.length ||
405
- since ||
406
- until ||
407
- identity_key;
378
+ kind || scopedCategory || effectiveTags.length || since || until || identity_key;
408
379
  if (!hasQuery && !hasFilters)
409
380
  return err(
410
- "Required: query or at least one filter (kind, category, tags, since, until, identity_key)",
411
- "INVALID_INPUT",
381
+ 'Required: query or at least one filter (kind, category, tags, since, until, identity_key)',
382
+ 'INVALID_INPUT'
412
383
  );
413
384
  await ensureIndexed();
414
385
 
@@ -416,34 +387,32 @@ export async function handler(
416
387
 
417
388
  // Gap 1: Entity exact-match by identity_key
418
389
  if (identity_key) {
419
- if (!kindFilter)
420
- return err("identity_key requires kind to be specified", "INVALID_INPUT");
421
- const match = ctx.stmts.getByIdentityKey.get(kindFilter, identity_key);
390
+ if (!kindFilter) return err('identity_key requires kind to be specified', 'INVALID_INPUT');
391
+ const match = ctx.stmts.getByIdentityKey.get(kindFilter, identity_key) as any;
422
392
  if (match) {
423
393
  const entryTags = match.tags ? JSON.parse(match.tags) : [];
424
- const tagStr = entryTags.length ? entryTags.join(", ") : "none";
394
+ const tagStr = entryTags.length ? entryTags.join(', ') : 'none';
425
395
  const relPath =
426
396
  match.file_path && config.vaultDir
427
- ? match.file_path.replace(config.vaultDir + "/", "")
428
- : match.file_path || "n/a";
397
+ ? match.file_path.replace(config.vaultDir + '/', '')
398
+ : match.file_path || 'n/a';
429
399
  const lines = [
430
400
  `## Entity Match (exact)\n`,
431
- `### ${match.title || "(untitled)"} [${match.kind}/${match.category}]`,
401
+ `### ${match.title || '(untitled)'} [${match.kind}/${match.category}]`,
432
402
  `1.000 · ${tagStr} · ${relPath} · id: \`${match.id}\``,
433
- match.body?.slice(0, 300) + (match.body?.length > 300 ? "..." : ""),
403
+ match.body?.slice(0, 300) + (match.body?.length > 300 ? '...' : ''),
434
404
  ];
435
- return ok(lines.join("\n"));
405
+ return ok(lines.join('\n'));
436
406
  }
437
407
  // Fall through to semantic search as fallback
438
408
  }
439
409
 
440
410
  // Gap 2: Event default time-window
441
- const effectiveCategory =
442
- scopedCategory || (kindFilter ? categoryFor(kindFilter) : null);
411
+ const effectiveCategory = scopedCategory || (kindFilter ? categoryFor(kindFilter) : null);
443
412
  let effectiveSince = since || null;
444
413
  let effectiveUntil = until || null;
445
414
  let autoWindowed = false;
446
- if (effectiveCategory === "event" && !since && !until) {
415
+ if (effectiveCategory === 'event' && !since && !until) {
447
416
  const decayMs = (config.eventDecayDays || 30) * 86400000;
448
417
  effectiveSince = new Date(Date.now() - decayMs).toISOString();
449
418
  autoWindowed = true;
@@ -456,7 +425,7 @@ export async function handler(
456
425
  ? Math.min(effectiveLimit * 10, MAX_FETCH_LIMIT)
457
426
  : effectiveLimit;
458
427
 
459
- let filtered;
428
+ let filtered: any[];
460
429
  if (hasQuery) {
461
430
  // Hybrid search mode
462
431
  const sorted = await hybridSearch(ctx, query, {
@@ -487,48 +456,46 @@ export async function handler(
487
456
  if (false) {
488
457
  }
489
458
  if (kindFilter) {
490
- clauses.push("kind = ?");
459
+ clauses.push('kind = ?');
491
460
  params.push(kindFilter);
492
461
  }
493
462
  if (scopedCategory) {
494
- clauses.push("category = ?");
463
+ clauses.push('category = ?');
495
464
  params.push(scopedCategory);
496
465
  }
497
466
  if (effectiveSince) {
498
- clauses.push("created_at >= ?");
467
+ clauses.push('created_at >= ?');
499
468
  params.push(effectiveSince);
500
469
  }
501
470
  if (effectiveUntil) {
502
- clauses.push("created_at <= ?");
471
+ clauses.push('created_at <= ?');
503
472
  params.push(effectiveUntil);
504
473
  }
505
474
  clauses.push("(expires_at IS NULL OR expires_at > datetime('now'))");
506
475
  if (!include_superseded) {
507
- clauses.push("superseded_by IS NULL");
476
+ clauses.push('superseded_by IS NULL');
508
477
  }
509
- const where = clauses.length ? `WHERE ${clauses.join(" AND ")}` : "";
478
+ const where = clauses.length ? `WHERE ${clauses.join(' AND ')}` : '';
510
479
  params.push(fetchLimit);
511
480
  let rows;
512
481
  try {
513
482
  rows = ctx.db
514
- .prepare(
515
- `SELECT * FROM vault ${where} ORDER BY created_at DESC LIMIT ?`,
516
- )
483
+ .prepare(`SELECT * FROM vault ${where} ORDER BY created_at DESC LIMIT ?`)
517
484
  .all(...params);
518
485
  } catch (e) {
519
486
  return errWithHint(
520
- e.message,
521
- "DB_ERROR",
522
- "context-vault get_context DB_ERROR. Check `cat ~/.context-mcp/error.log | tail -5` and help me debug.",
487
+ e instanceof Error ? e.message : String(e),
488
+ 'DB_ERROR',
489
+ 'context-vault get_context DB_ERROR. Check `cat ~/.context-mcp/error.log | tail -5` and help me debug.'
523
490
  );
524
491
  }
525
492
 
526
493
  // Post-filter by tags if provided, then apply requested limit
527
494
  filtered = effectiveTags.length
528
495
  ? rows
529
- .filter((r) => {
496
+ .filter((r: any) => {
530
497
  const entryTags = r.tags ? JSON.parse(r.tags) : [];
531
- return effectiveTags.some((t) => entryTags.includes(t));
498
+ return effectiveTags.some((t: string) => entryTags.includes(t));
532
499
  })
533
500
  .slice(0, effectiveLimit)
534
501
  : rows;
@@ -540,18 +507,18 @@ export async function handler(
540
507
  // Brief score boost: briefs rank slightly higher so consolidated snapshots
541
508
  // surface above the individual entries they summarize.
542
509
  for (const r of filtered) {
543
- if (r.kind === "brief") r.score = (r.score || 0) + BRIEF_SCORE_BOOST;
510
+ if (r.kind === 'brief') r.score = (r.score || 0) + BRIEF_SCORE_BOOST;
544
511
  }
545
- filtered.sort((a, b) => b.score - a.score);
512
+ filtered.sort((a: any, b: any) => b.score - a.score);
546
513
 
547
514
  // Tier filter: exclude ephemeral entries by default (NULL tier treated as working)
548
515
  if (!include_ephemeral) {
549
- filtered = filtered.filter((r) => r.tier !== "ephemeral");
516
+ filtered = filtered.filter((r: any) => r.tier !== 'ephemeral');
550
517
  }
551
518
 
552
519
  // Event category filter: exclude events from semantic search by default
553
520
  if (shouldExcludeEvents) {
554
- filtered = filtered.filter((r) => r.category !== "event");
521
+ filtered = filtered.filter((r: any) => r.category !== 'event');
555
522
  }
556
523
 
557
524
  if (!filtered.length) {
@@ -560,28 +527,14 @@ export async function handler(
560
527
  return ok(
561
528
  hasQuery
562
529
  ? `No results found for "${query}" in events (last ${days} days).\nTry with \`since: "YYYY-MM-DD"\` to search older events.`
563
- : `No entries found matching the given filters in events (last ${days} days).\nTry with \`since: "YYYY-MM-DD"\` to search older events.`,
530
+ : `No entries found matching the given filters in events (last ${days} days).\nTry with \`since: "YYYY-MM-DD"\` to search older events.`
564
531
  );
565
532
  }
566
533
  return ok(
567
- hasQuery
568
- ? "No results found for: " + query
569
- : "No entries found matching the given filters.",
534
+ hasQuery ? 'No results found for: ' + query : 'No entries found matching the given filters.'
570
535
  );
571
536
  }
572
537
 
573
- // Decrypt encrypted entries if ctx.decrypt is available
574
- if (ctx.decrypt) {
575
- for (const r of filtered) {
576
- if (r.body_encrypted) {
577
- const decrypted = await ctx.decrypt(r);
578
- r.body = decrypted.body;
579
- if (decrypted.title) r.title = decrypted.title;
580
- if (decrypted.meta) r.meta = JSON.stringify(decrypted.meta);
581
- }
582
- }
583
- }
584
-
585
538
  // Token-budgeted packing
586
539
  let tokensBudget = null;
587
540
  let tokensUsed = null;
@@ -602,8 +555,7 @@ export async function handler(
602
555
  }
603
556
 
604
557
  // Skeleton mode: determine pivot threshold
605
- const effectivePivot =
606
- pivot_count != null ? pivot_count : DEFAULT_PIVOT_COUNT;
558
+ const effectivePivot = pivot_count != null ? pivot_count : DEFAULT_PIVOT_COUNT;
607
559
 
608
560
  // Conflict detection
609
561
  const conflicts = detect_conflicts ? detectConflicts(filtered, ctx) : [];
@@ -611,45 +563,43 @@ export async function handler(
611
563
  const lines = [];
612
564
  if (reindexFailed)
613
565
  lines.push(
614
- `> **Warning:** Auto-reindex failed. Results may be stale. Run \`context-vault reindex\` to fix.\n`,
566
+ `> **Warning:** Auto-reindex failed. Results may be stale. Run \`context-vault reindex\` to fix.\n`
615
567
  );
616
568
  if (hasQuery && isEmbedAvailable() === false)
617
569
  lines.push(
618
- `> **Note:** Semantic search unavailable — results ranked by keyword match only. Run \`context-vault setup\` to download the embedding model.\n`,
570
+ `> **Note:** Semantic search unavailable — results ranked by keyword match only. Run \`context-vault setup\` to download the embedding model.\n`
619
571
  );
620
- const heading = hasQuery ? `Results for "${query}"` : "Filtered entries";
572
+ const heading = hasQuery ? `Results for "${query}"` : 'Filtered entries';
621
573
  lines.push(`## ${heading} (${filtered.length} matches)\n`);
622
574
  if (tokensBudget != null) {
623
- lines.push(
624
- `> Token budget: ${tokensUsed} / ${tokensBudget} tokens used.\n`,
625
- );
575
+ lines.push(`> Token budget: ${tokensUsed} / ${tokensBudget} tokens used.\n`);
626
576
  }
627
577
  if (autoWindowed) {
628
578
  const days = config.eventDecayDays || 30;
629
579
  lines.push(
630
- `> ℹ Event search limited to last ${days} days. Use \`since\` parameter for older results.\n`,
580
+ `> ℹ Event search limited to last ${days} days. Use \`since\` parameter for older results.\n`
631
581
  );
632
582
  }
633
583
  for (let i = 0; i < filtered.length; i++) {
634
584
  const r = filtered[i];
635
585
  const isSkeleton = i >= effectivePivot;
636
586
  const entryTags = r.tags ? JSON.parse(r.tags) : [];
637
- const tagStr = entryTags.length ? entryTags.join(", ") : "none";
587
+ const tagStr = entryTags.length ? entryTags.join(', ') : 'none';
638
588
  const relPath =
639
589
  r.file_path && config.vaultDir
640
- ? r.file_path.replace(config.vaultDir + "/", "")
641
- : r.file_path || "n/a";
642
- const skeletonLabel = isSkeleton ? " ⊘ skeleton" : "";
590
+ ? r.file_path.replace(config.vaultDir + '/', '')
591
+ : r.file_path || 'n/a';
592
+ const skeletonLabel = isSkeleton ? ' ⊘ skeleton' : '';
643
593
  lines.push(
644
- `### [${i + 1}/${filtered.length}] ${r.title || "(untitled)"} [${r.kind}/${r.category}]${skeletonLabel}`,
594
+ `### [${i + 1}/${filtered.length}] ${r.title || '(untitled)'} [${r.kind}/${r.category}]${skeletonLabel}`
645
595
  );
646
596
  const dateStr =
647
597
  r.updated_at && r.updated_at !== r.created_at
648
598
  ? `${r.created_at} (updated ${r.updated_at})`
649
- : r.created_at || "";
650
- const tierStr = r.tier ? ` · tier: ${r.tier}` : "";
599
+ : r.created_at || '';
600
+ const tierStr = r.tier ? ` · tier: ${r.tier}` : '';
651
601
  lines.push(
652
- `${r.score.toFixed(3)} · ${tagStr} · ${relPath} · ${dateStr} · skeleton: ${isSkeleton}${tierStr} · id: \`${r.id}\``,
602
+ `${r.score.toFixed(3)} · ${tagStr} · ${relPath} · ${dateStr} · skeleton: ${isSkeleton}${tierStr} · id: \`${r.id}\``
653
603
  );
654
604
  const stalenessResult = checkStaleness(r);
655
605
  if (stalenessResult) {
@@ -660,34 +610,30 @@ export async function handler(
660
610
  if (isSkeleton) {
661
611
  lines.push(skeletonBody(r.body));
662
612
  } else {
663
- lines.push(r.body?.slice(0, 300) + (r.body?.length > 300 ? "..." : ""));
613
+ lines.push(r.body?.slice(0, 300) + (r.body?.length > 300 ? '...' : ''));
664
614
  }
665
- lines.push("");
615
+ lines.push('');
666
616
  }
667
617
 
668
618
  if (detect_conflicts) {
669
619
  if (conflicts.length === 0) {
670
- lines.push(
671
- `## Conflict Detection\n\nNo conflicts detected among results.\n`,
672
- );
620
+ lines.push(`## Conflict Detection\n\nNo conflicts detected among results.\n`);
673
621
  } else {
674
622
  lines.push(`## Conflict Detection (${conflicts.length} flagged)\n`);
675
623
  for (const c of conflicts) {
676
- lines.push(
677
- `- **${c.reason}**: \`${c.entry_a_id}\` vs \`${c.entry_b_id}\``,
678
- );
624
+ lines.push(`- **${c.reason}**: \`${c.entry_a_id}\` vs \`${c.entry_b_id}\``);
679
625
  lines.push(` Recommendation: ${c.recommendation}`);
680
626
  }
681
- lines.push("");
627
+ lines.push('');
682
628
  }
683
629
  }
684
630
 
685
631
  // Graph traversal: follow related_to links bidirectionally
686
632
  if (follow_links) {
687
- const { forward, backward } = collectLinkedEntries(ctx.db, filtered);
688
- const allLinked = [...forward, ...backward];
633
+ const { forward, backward } = collectLinkedEntries(ctx.db, filtered as any);
634
+ const allLinked: any[] = [...(forward as any[]), ...(backward as any[])];
689
635
  const seen = new Set();
690
- const uniqueLinked = allLinked.filter((e) => {
636
+ const uniqueLinked = allLinked.filter((e: any) => {
691
637
  if (seen.has(e.id)) return false;
692
638
  seen.add(e.id);
693
639
  return true;
@@ -696,21 +642,17 @@ export async function handler(
696
642
  if (uniqueLinked.length > 0) {
697
643
  lines.push(`## Linked Entries (${uniqueLinked.length} via related_to)\n`);
698
644
  for (const r of uniqueLinked) {
699
- const direction = forward.some((f) => f.id === r.id)
700
- ? "→ forward"
701
- : "← backlink";
645
+ const direction = forward.some((f: any) => f.id === r.id) ? '→ forward' : '← backlink';
702
646
  const entryTags = r.tags ? JSON.parse(r.tags) : [];
703
- const tagStr = entryTags.length ? entryTags.join(", ") : "none";
647
+ const tagStr = entryTags.length ? entryTags.join(', ') : 'none';
704
648
  const relPath =
705
649
  r.file_path && config.vaultDir
706
- ? r.file_path.replace(config.vaultDir + "/", "")
707
- : r.file_path || "n/a";
708
- lines.push(
709
- `### ${r.title || "(untitled)"} [${r.kind}/${r.category}] ${direction}`,
710
- );
650
+ ? r.file_path.replace(config.vaultDir + '/', '')
651
+ : r.file_path || 'n/a';
652
+ lines.push(`### ${r.title || '(untitled)'} [${r.kind}/${r.category}] ${direction}`);
711
653
  lines.push(`${tagStr} · ${relPath} · id: \`${r.id}\``);
712
- lines.push(r.body?.slice(0, 200) + (r.body?.length > 200 ? "..." : ""));
713
- lines.push("");
654
+ lines.push(r.body?.slice(0, 200) + (r.body?.length > 200 ? '...' : ''));
655
+ lines.push('');
714
656
  }
715
657
  } else {
716
658
  lines.push(`## Linked Entries\n\nNo related entries found.\n`);
@@ -719,24 +661,19 @@ export async function handler(
719
661
 
720
662
  // Consolidation suggestion detection — lazy, opportunistic, vault-wide
721
663
  const consolidationOpts = {
722
- tagThreshold:
723
- config.consolidation?.tagThreshold ?? CONSOLIDATION_TAG_THRESHOLD,
724
- maxAgeDays:
725
- config.consolidation?.maxAgeDays ?? CONSOLIDATION_SNAPSHOT_MAX_AGE_DAYS,
664
+ tagThreshold: config.consolidation?.tagThreshold ?? CONSOLIDATION_TAG_THRESHOLD,
665
+ maxAgeDays: config.consolidation?.maxAgeDays ?? CONSOLIDATION_SNAPSHOT_MAX_AGE_DAYS,
726
666
  };
727
667
  const consolidationSuggestions = detectConsolidationHints(
728
668
  filtered,
729
669
  ctx.db,
730
670
 
731
- consolidationOpts,
671
+ consolidationOpts
732
672
  );
733
673
 
734
674
  // Auto-consolidate: fire-and-forget create_snapshot for eligible tags
735
- if (
736
- config.consolidation?.autoConsolidate &&
737
- consolidationSuggestions.length > 0
738
- ) {
739
- const { handler: snapshotHandler } = await import("./create-snapshot.js");
675
+ if (config.consolidation?.autoConsolidate && consolidationSuggestions.length > 0) {
676
+ const { handler: snapshotHandler } = await import('./create-snapshot.js');
740
677
  for (const suggestion of consolidationSuggestions) {
741
678
  snapshotHandler({ topic: suggestion.tag, tags: [suggestion.tag] }, ctx, {
742
679
  ensureIndexed: async () => {},
@@ -744,8 +681,8 @@ export async function handler(
744
681
  }
745
682
  }
746
683
 
747
- const result = ok(lines.join("\n"));
748
- const meta = {};
684
+ const result: ToolResult = ok(lines.join('\n'));
685
+ const meta: Record<string, unknown> = {};
749
686
  meta.scope = effectiveScope;
750
687
  if (tokensBudget != null) {
751
688
  meta.tokens_used = tokensUsed;