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,17 +1,12 @@
1
- import { z } from "zod";
2
- import { captureAndIndex, updateEntryFile } from "@context-vault/core/capture";
3
- import { indexEntry } from "@context-vault/core/index";
4
- import { categoryFor, defaultTierFor } from "@context-vault/core/categories";
5
- import { normalizeKind } from "@context-vault/core/files";
6
- import {
7
- ok,
8
- err,
9
- errWithHint,
10
- ensureVaultExists,
11
- ensureValidKind,
12
- } from "../helpers.js";
13
- import { maybeShowFeedbackPrompt } from "../telemetry.js";
14
- import { validateRelatedTo } from "../linking.js";
1
+ import { z } from 'zod';
2
+ import { captureAndIndex, updateEntryFile } from '@context-vault/core/capture';
3
+ import { indexEntry } from '@context-vault/core/index';
4
+ import { categoryFor, defaultTierFor } from '@context-vault/core/categories';
5
+ import { normalizeKind } from '@context-vault/core/files';
6
+ import { ok, err, errWithHint, ensureVaultExists, ensureValidKind } from '../helpers.js';
7
+ import { maybeShowFeedbackPrompt } from '../telemetry.js';
8
+ import { validateRelatedTo } from '../linking.js';
9
+ import type { LocalCtx, SharedCtx, ToolResult } from '../types.js';
15
10
  import {
16
11
  MAX_BODY_LENGTH,
17
12
  MAX_TITLE_LENGTH,
@@ -21,44 +16,41 @@ import {
21
16
  MAX_META_LENGTH,
22
17
  MAX_SOURCE_LENGTH,
23
18
  MAX_IDENTITY_KEY_LENGTH,
24
- } from "@context-vault/core/constants";
19
+ } from '@context-vault/core/constants';
25
20
 
26
21
  const DEFAULT_SIMILARITY_THRESHOLD = 0.85;
27
22
  const SKIP_THRESHOLD = 0.95;
28
23
  const UPDATE_THRESHOLD = 0.85;
29
24
 
30
25
  async function findSimilar(
31
- ctx,
32
- embedding,
33
- threshold,
34
-
35
- { hydrate = false } = {},
36
- ) {
26
+ ctx: LocalCtx,
27
+ embedding: any,
28
+ threshold: number,
29
+ { hydrate = false } = {}
30
+ ): Promise<any[]> {
37
31
  try {
38
- const vecCount = ctx.db
39
- .prepare("SELECT COUNT(*) as c FROM vault_vec")
40
- .get().c;
32
+ const vecCount = (ctx.db.prepare('SELECT COUNT(*) as c FROM vault_vec').get() as any)?.c ?? 0;
41
33
  if (vecCount === 0) return [];
42
34
 
43
- const vecRows = ctx.db
35
+ const vecRows: any[] = ctx.db
44
36
  .prepare(
45
- `SELECT v.rowid, v.distance FROM vault_vec v WHERE embedding MATCH ? ORDER BY distance LIMIT ?`,
37
+ `SELECT v.rowid, v.distance FROM vault_vec v WHERE embedding MATCH ? ORDER BY distance LIMIT ?`
46
38
  )
47
- .all(embedding, 10);
39
+ .all(embedding, 10) as any[];
48
40
 
49
41
  if (!vecRows.length) return [];
50
42
 
51
- const rowids = vecRows.map((vr) => vr.rowid);
52
- const placeholders = rowids.map(() => "?").join(",");
43
+ const rowids = vecRows.map((vr: any) => vr.rowid);
44
+ const placeholders = rowids.map(() => '?').join(',');
53
45
  // Local mode has no user_id column — omit it from the SELECT list.
54
- const isLocal = ctx.stmts._mode === "local";
46
+ const isLocal = (ctx.stmts as any)._mode === 'local';
55
47
  const columns = isLocal
56
48
  ? hydrate
57
- ? "rowid, id, title, body, kind, tags, category, updated_at"
58
- : "rowid, id, title, category"
49
+ ? 'rowid, id, title, body, kind, tags, category, updated_at'
50
+ : 'rowid, id, title, category'
59
51
  : hydrate
60
- ? "rowid, id, title, body, kind, tags, category, updated_at"
61
- : "rowid, id, title, category";
52
+ ? 'rowid, id, title, body, kind, tags, category, updated_at'
53
+ : 'rowid, id, title, category';
62
54
  const hydratedRows = ctx.db
63
55
  .prepare(`SELECT ${columns} FROM vault WHERE rowid IN (${placeholders})`)
64
56
  .all(...rowids);
@@ -68,12 +60,12 @@ async function findSimilar(
68
60
 
69
61
  const results = [];
70
62
  for (const vr of vecRows) {
71
- const similarity = Math.max(0, 1 - vr.distance / 2);
63
+ const similarity = Math.max(0, 1 - (vr.distance as number) / 2);
72
64
  if (similarity < threshold) continue;
73
65
  const row = byRowid.get(vr.rowid);
74
66
  if (!row) continue;
75
- if (row.category === "entity") continue;
76
- const entry = { id: row.id, title: row.title, score: similarity };
67
+ if (row.category === 'entity') continue;
68
+ const entry: Record<string, any> = { id: row.id, title: row.title, score: similarity };
77
69
  if (hydrate) {
78
70
  entry.body = row.body;
79
71
  entry.kind = row.kind;
@@ -88,50 +80,47 @@ async function findSimilar(
88
80
  }
89
81
  }
90
82
 
91
- function formatSimilarWarning(similar) {
92
- const lines = ["", "⚠ Similar entries already exist:"];
83
+ function formatSimilarWarning(similar: any[]): string {
84
+ const lines = ['', '⚠ Similar entries already exist:'];
93
85
  for (const e of similar) {
94
86
  const score = e.score.toFixed(2);
95
- const title = e.title ? `"${e.title}"` : "(no title)";
87
+ const title = e.title ? `"${e.title}"` : '(no title)';
96
88
  lines.push(` - ${title} (${score}) — id: ${e.id}`);
97
89
  }
98
- lines.push(
99
- " Consider using `id: <existing>` in save_context to update instead.",
100
- );
101
- return lines.join("\n");
90
+ lines.push(' Consider using `id: <existing>` in save_context to update instead.');
91
+ return lines.join('\n');
102
92
  }
103
93
 
104
- export function buildConflictCandidates(similarEntries) {
105
- return similarEntries.map((entry) => {
94
+ export function buildConflictCandidates(similarEntries: any[]): any[] {
95
+ return similarEntries.map((entry: any) => {
106
96
  let suggested_action;
107
97
  let reasoning_context;
108
98
 
109
99
  if (entry.score >= SKIP_THRESHOLD) {
110
- suggested_action = "SKIP";
100
+ suggested_action = 'SKIP';
111
101
  reasoning_context =
112
102
  `Near-duplicate detected (${(entry.score * 100).toFixed(0)}% similarity)` +
113
- `${entry.title ? ` with "${entry.title}"` : ""}. ` +
103
+ `${entry.title ? ` with "${entry.title}"` : ''}. ` +
114
104
  `Content is nearly identical — saving would create a redundant entry. ` +
115
105
  `Use save_context with id: "${entry.id}" to update instead, or skip saving entirely.`;
116
106
  } else if (entry.score >= UPDATE_THRESHOLD) {
117
- suggested_action = "UPDATE";
107
+ suggested_action = 'UPDATE';
118
108
  reasoning_context =
119
109
  `High content similarity (${(entry.score * 100).toFixed(0)}%)` +
120
- `${entry.title ? ` with "${entry.title}"` : ""}. ` +
110
+ `${entry.title ? ` with "${entry.title}"` : ''}. ` +
121
111
  `Likely the same knowledge — consider updating this entry via save_context with id: "${entry.id}".`;
122
112
  } else {
123
- suggested_action = "ADD";
113
+ suggested_action = 'ADD';
124
114
  reasoning_context =
125
115
  `Moderate similarity (${(entry.score * 100).toFixed(0)}%)` +
126
- `${entry.title ? ` with "${entry.title}"` : ""}. ` +
116
+ `${entry.title ? ` with "${entry.title}"` : ''}. ` +
127
117
  `Content is related but distinct enough to coexist.`;
128
118
  }
129
119
 
130
120
  let parsedTags = [];
131
121
  if (entry.tags) {
132
122
  try {
133
- parsedTags =
134
- typeof entry.tags === "string" ? JSON.parse(entry.tags) : entry.tags;
123
+ parsedTags = typeof entry.tags === 'string' ? JSON.parse(entry.tags) : entry.tags;
135
124
  } catch {
136
125
  parsedTags = [];
137
126
  }
@@ -151,16 +140,16 @@ export function buildConflictCandidates(similarEntries) {
151
140
  });
152
141
  }
153
142
 
154
- function formatConflictSuggestions(candidates) {
155
- const lines = ["", "── Conflict Resolution Suggestions ──"];
143
+ function formatConflictSuggestions(candidates: any[]): string {
144
+ const lines = ['', '── Conflict Resolution Suggestions ──'];
156
145
  for (const c of candidates) {
157
- const titleDisplay = c.title ? `"${c.title}"` : "(no title)";
146
+ const titleDisplay = c.title ? `"${c.title}"` : '(no title)';
158
147
  lines.push(
159
- ` [${c.suggested_action}] ${titleDisplay} (${(c.score * 100).toFixed(0)}%) — id: ${c.id}`,
148
+ ` [${c.suggested_action}] ${titleDisplay} (${(c.score * 100).toFixed(0)}%) — id: ${c.id}`
160
149
  );
161
150
  lines.push(` ${c.reasoning_context}`);
162
151
  }
163
- return lines.join("\n");
152
+ return lines.join('\n');
164
153
  }
165
154
 
166
155
  /**
@@ -175,161 +164,131 @@ function validateSaveInput({
175
164
  source,
176
165
  identity_key,
177
166
  expires_at,
178
- }) {
167
+ }: Record<string, any>): ToolResult | null {
179
168
  if (kind !== undefined && kind !== null) {
180
- if (typeof kind !== "string" || kind.length > MAX_KIND_LENGTH) {
181
- return err(
182
- `kind must be a string, max ${MAX_KIND_LENGTH} chars`,
183
- "INVALID_INPUT",
184
- );
169
+ if (typeof kind !== 'string' || kind.length > MAX_KIND_LENGTH) {
170
+ return err(`kind must be a string, max ${MAX_KIND_LENGTH} chars`, 'INVALID_INPUT');
185
171
  }
186
172
  }
187
173
  if (body !== undefined && body !== null) {
188
- if (typeof body !== "string" || body.length > MAX_BODY_LENGTH) {
189
- return err(
190
- `body must be a string, max ${MAX_BODY_LENGTH / 1024}KB`,
191
- "INVALID_INPUT",
192
- );
174
+ if (typeof body !== 'string' || body.length > MAX_BODY_LENGTH) {
175
+ return err(`body must be a string, max ${MAX_BODY_LENGTH / 1024}KB`, 'INVALID_INPUT');
193
176
  }
194
177
  }
195
178
  if (title !== undefined && title !== null) {
196
- if (typeof title !== "string" || title.length > MAX_TITLE_LENGTH) {
197
- return err(
198
- `title must be a string, max ${MAX_TITLE_LENGTH} chars`,
199
- "INVALID_INPUT",
200
- );
179
+ if (typeof title !== 'string' || title.length > MAX_TITLE_LENGTH) {
180
+ return err(`title must be a string, max ${MAX_TITLE_LENGTH} chars`, 'INVALID_INPUT');
201
181
  }
202
182
  }
203
183
  if (tags !== undefined && tags !== null) {
204
- if (!Array.isArray(tags))
205
- return err("tags must be an array of strings", "INVALID_INPUT");
184
+ if (!Array.isArray(tags)) return err('tags must be an array of strings', 'INVALID_INPUT');
206
185
  if (tags.length > MAX_TAGS_COUNT)
207
- return err(`tags: max ${MAX_TAGS_COUNT} tags allowed`, "INVALID_INPUT");
186
+ return err(`tags: max ${MAX_TAGS_COUNT} tags allowed`, 'INVALID_INPUT');
208
187
  for (const tag of tags) {
209
- if (typeof tag !== "string" || tag.length > MAX_TAG_LENGTH) {
210
- return err(
211
- `each tag must be a string, max ${MAX_TAG_LENGTH} chars`,
212
- "INVALID_INPUT",
213
- );
188
+ if (typeof tag !== 'string' || tag.length > MAX_TAG_LENGTH) {
189
+ return err(`each tag must be a string, max ${MAX_TAG_LENGTH} chars`, 'INVALID_INPUT');
214
190
  }
215
191
  }
216
192
  }
217
193
  if (meta !== undefined && meta !== null) {
218
194
  const metaStr = JSON.stringify(meta);
219
195
  if (metaStr.length > MAX_META_LENGTH) {
220
- return err(
221
- `meta must be under ${MAX_META_LENGTH / 1024}KB when serialized`,
222
- "INVALID_INPUT",
223
- );
196
+ return err(`meta must be under ${MAX_META_LENGTH / 1024}KB when serialized`, 'INVALID_INPUT');
224
197
  }
225
198
  }
226
199
  if (source !== undefined && source !== null) {
227
- if (typeof source !== "string" || source.length > MAX_SOURCE_LENGTH) {
228
- return err(
229
- `source must be a string, max ${MAX_SOURCE_LENGTH} chars`,
230
- "INVALID_INPUT",
231
- );
200
+ if (typeof source !== 'string' || source.length > MAX_SOURCE_LENGTH) {
201
+ return err(`source must be a string, max ${MAX_SOURCE_LENGTH} chars`, 'INVALID_INPUT');
232
202
  }
233
203
  }
234
204
  if (identity_key !== undefined && identity_key !== null) {
235
- if (
236
- typeof identity_key !== "string" ||
237
- identity_key.length > MAX_IDENTITY_KEY_LENGTH
238
- ) {
205
+ if (typeof identity_key !== 'string' || identity_key.length > MAX_IDENTITY_KEY_LENGTH) {
239
206
  return err(
240
207
  `identity_key must be a string, max ${MAX_IDENTITY_KEY_LENGTH} chars`,
241
- "INVALID_INPUT",
208
+ 'INVALID_INPUT'
242
209
  );
243
210
  }
244
211
  }
245
212
  if (expires_at !== undefined && expires_at !== null) {
246
- if (
247
- typeof expires_at !== "string" ||
248
- isNaN(new Date(expires_at).getTime())
249
- ) {
250
- return err("expires_at must be a valid ISO date string", "INVALID_INPUT");
213
+ if (typeof expires_at !== 'string' || isNaN(new Date(expires_at).getTime())) {
214
+ return err('expires_at must be a valid ISO date string', 'INVALID_INPUT');
251
215
  }
252
216
  }
253
217
  return null;
254
218
  }
255
219
 
256
- export const name = "save_context";
220
+ export const name = 'save_context';
257
221
 
258
222
  export const description =
259
- "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.";
223
+ '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.';
260
224
 
261
225
  export const inputSchema = {
262
226
  id: z
263
227
  .string()
264
228
  .optional()
265
229
  .describe(
266
- "Entry ULID to update. When provided, updates the existing entry instead of creating new. Omitted fields are preserved.",
230
+ 'Entry ULID to update. When provided, updates the existing entry instead of creating new. Omitted fields are preserved.'
267
231
  ),
268
232
  kind: z
269
233
  .string()
270
234
  .optional()
271
235
  .describe(
272
- "Entry kind — determines folder (e.g. 'insight', 'decision', 'pattern', 'reference', or any custom kind). Required for new entries.",
236
+ "Entry kind — determines folder (e.g. 'insight', 'decision', 'pattern', 'reference', or any custom kind). Required for new entries."
273
237
  ),
274
- title: z.string().optional().describe("Entry title (optional for insights)"),
275
- body: z
276
- .string()
277
- .optional()
278
- .describe("Main content. Required for new entries."),
238
+ title: z.string().optional().describe('Entry title (optional for insights)'),
239
+ body: z.string().optional().describe('Main content. Required for new entries.'),
279
240
  tags: z
280
241
  .array(z.string())
281
242
  .optional()
282
243
  .describe(
283
- "Tags for categorization and search. Use 'bucket:' prefix for project/domain scoping (e.g., 'bucket:autohub') to enable project-scoped retrieval.",
244
+ "Tags for categorization and search. Use 'bucket:' prefix for project/domain scoping (e.g., 'bucket:autohub') to enable project-scoped retrieval."
284
245
  ),
285
246
  meta: z
286
247
  .any()
287
248
  .optional()
288
249
  .describe(
289
- "Additional structured metadata (JSON object, e.g. { language: 'js', status: 'accepted' })",
250
+ "Additional structured metadata (JSON object, e.g. { language: 'js', status: 'accepted' })"
290
251
  ),
291
252
  folder: z
292
253
  .string()
293
254
  .optional()
294
255
  .describe("Subfolder within the kind directory (e.g. 'react/hooks')"),
295
- source: z.string().optional().describe("Where this knowledge came from"),
256
+ source: z.string().optional().describe('Where this knowledge came from'),
296
257
  identity_key: z
297
258
  .string()
298
259
  .optional()
299
260
  .describe(
300
- "Required for entity kinds (contact, project, tool, source). The unique identifier for this entity.",
261
+ 'Required for entity kinds (contact, project, tool, source). The unique identifier for this entity.'
301
262
  ),
302
- expires_at: z.string().optional().describe("ISO date for TTL expiry"),
263
+ expires_at: z.string().optional().describe('ISO date for TTL expiry'),
303
264
  supersedes: z
304
265
  .array(z.string())
305
266
  .optional()
306
267
  .describe(
307
- "Array of entry IDs that this entry supersedes/replaces. Those entries will be marked with superseded_by pointing to this new entry and excluded from future search results by default.",
268
+ 'Array of entry IDs that this entry supersedes/replaces. Those entries will be marked with superseded_by pointing to this new entry and excluded from future search results by default.'
308
269
  ),
309
270
  related_to: z
310
271
  .array(z.string())
311
272
  .optional()
312
273
  .describe(
313
- "Array of entry IDs this entry is related to. Enables bidirectional graph traversal — use get_context with follow_links:true to retrieve linked entries.",
274
+ 'Array of entry IDs this entry is related to. Enables bidirectional graph traversal — use get_context with follow_links:true to retrieve linked entries.'
314
275
  ),
315
276
  source_files: z
316
277
  .array(
317
278
  z.object({
318
- path: z.string().describe("File path (absolute or relative to cwd)"),
319
- hash: z
320
- .string()
321
- .describe("SHA-256 hash of the file contents at observation time"),
322
- }),
279
+ path: z.string().describe('File path (absolute or relative to cwd)'),
280
+ hash: z.string().describe('SHA-256 hash of the file contents at observation time'),
281
+ })
323
282
  )
324
283
  .optional()
325
284
  .describe(
326
- "Source code files this entry is derived from. When these files change (hash mismatch), the entry will be flagged as stale in get_context results.",
285
+ 'Source code files this entry is derived from. When these files change (hash mismatch), the entry will be flagged as stale in get_context results.'
327
286
  ),
328
287
  dry_run: z
329
288
  .boolean()
330
289
  .optional()
331
290
  .describe(
332
- "If true, check for similar entries without saving. Returns similarity results without creating a new entry. Only applies to knowledge and event categories.",
291
+ 'If true, check for similar entries without saving. Returns similarity results without creating a new entry. Only applies to knowledge and event categories.'
333
292
  ),
334
293
  similarity_threshold: z
335
294
  .number()
@@ -337,27 +296,22 @@ export const inputSchema = {
337
296
  .max(1)
338
297
  .optional()
339
298
  .describe(
340
- "Cosine similarity threshold for duplicate detection (0–1, default 0.85). Entries above this score are flagged as similar. Only applies to knowledge and event categories.",
299
+ 'Cosine similarity threshold for duplicate detection (0–1, default 0.85). Entries above this score are flagged as similar. Only applies to knowledge and event categories.'
341
300
  ),
342
301
  tier: z
343
- .enum(["ephemeral", "working", "durable"])
302
+ .enum(['ephemeral', 'working', 'durable'])
344
303
  .optional()
345
304
  .describe(
346
- "Memory tier for lifecycle management. 'ephemeral': short-lived session data. 'working': active context (default). 'durable': long-term reference material. Defaults based on kind when not specified.",
305
+ "Memory tier for lifecycle management. 'ephemeral': short-lived session data. 'working': active context (default). 'durable': long-term reference material. Defaults based on kind when not specified."
347
306
  ),
348
307
  conflict_resolution: z
349
- .enum(["suggest", "off"])
308
+ .enum(['suggest', 'off'])
350
309
  .optional()
351
310
  .describe(
352
- 'Conflict resolution mode. "suggest" (default): when similar entries are found, return structured conflict_candidates with suggested_action (ADD/UPDATE/SKIP) and reasoning_context for the calling agent to decide. Thresholds: score > 0.95 → SKIP (near-duplicate), score > 0.85 → UPDATE (very similar), score < 0.85 → ADD (distinct enough). "off": flag similar entries only (legacy behavior).',
311
+ 'Conflict resolution mode. "suggest" (default): when similar entries are found, return structured conflict_candidates with suggested_action (ADD/UPDATE/SKIP) and reasoning_context for the calling agent to decide. Thresholds: score > 0.95 → SKIP (near-duplicate), score > 0.85 → UPDATE (very similar), score < 0.85 → ADD (distinct enough). "off": flag similar entries only (legacy behavior).'
353
312
  ),
354
313
  };
355
314
 
356
- /**
357
- * @param {object} args
358
- * @param {import('../types.js').BaseCtx & Partial<import('../types.js').HostedCtxExtensions>} ctx
359
- * @param {import('../types.js').ToolShared} shared
360
- */
361
315
  export async function handler(
362
316
  {
363
317
  id,
@@ -377,18 +331,18 @@ export async function handler(
377
331
  similarity_threshold,
378
332
  tier,
379
333
  conflict_resolution,
380
- },
381
- ctx,
382
- { ensureIndexed },
383
- ) {
334
+ }: Record<string, any>,
335
+ ctx: LocalCtx,
336
+ { ensureIndexed }: SharedCtx
337
+ ): Promise<ToolResult> {
384
338
  const { config } = ctx;
385
- const suggestMode = conflict_resolution !== "off";
339
+ const suggestMode = conflict_resolution !== 'off';
386
340
 
387
341
  const vaultErr = ensureVaultExists(config);
388
342
  if (vaultErr) return vaultErr;
389
343
 
390
344
  const relatedToErr = validateRelatedTo(related_to);
391
- if (relatedToErr) return err(relatedToErr, "INVALID_INPUT");
345
+ if (relatedToErr) return err(relatedToErr, 'INVALID_INPUT');
392
346
 
393
347
  const inputErr = validateSaveInput({
394
348
  kind,
@@ -404,32 +358,24 @@ export async function handler(
404
358
 
405
359
  // ── Update mode ──
406
360
  if (id) {
407
- await ensureIndexed();
361
+ await ensureIndexed({ blocking: false });
408
362
 
409
363
  const existing = ctx.stmts.getEntryById.get(id);
410
- if (!existing) return err(`Entry not found: ${id}`, "NOT_FOUND");
364
+ if (!existing) return err(`Entry not found: ${id}`, 'NOT_FOUND');
411
365
 
412
366
  if (kind && normalizeKind(kind) !== existing.kind) {
413
367
  return err(
414
368
  `Cannot change kind (current: "${existing.kind}"). Delete and re-create instead.`,
415
- "INVALID_UPDATE",
369
+ 'INVALID_UPDATE'
416
370
  );
417
371
  }
418
372
  if (identity_key && identity_key !== existing.identity_key) {
419
373
  return err(
420
374
  `Cannot change identity_key (current: "${existing.identity_key}"). Delete and re-create instead.`,
421
- "INVALID_UPDATE",
375
+ 'INVALID_UPDATE'
422
376
  );
423
377
  }
424
378
 
425
- // Decrypt existing entry before merge if encrypted
426
- if (ctx.decrypt && existing.body_encrypted) {
427
- const decrypted = await ctx.decrypt(existing);
428
- existing.body = decrypted.body;
429
- if (decrypted.title) existing.title = decrypted.title;
430
- if (decrypted.meta) existing.meta = JSON.stringify(decrypted.meta);
431
- }
432
-
433
379
  let entry;
434
380
  try {
435
381
  entry = updateEntryFile(ctx, existing, {
@@ -446,9 +392,9 @@ export async function handler(
446
392
  await indexEntry(ctx, entry);
447
393
  } catch (e) {
448
394
  return errWithHint(
449
- e.message,
450
- "UPDATE_FAILED",
451
- "context-vault save_context update is failing. Check `cat ~/.context-mcp/error.log | tail -5` and help me debug.",
395
+ e instanceof Error ? e.message : String(e),
396
+ 'UPDATE_FAILED',
397
+ 'context-vault save_context update is failing. Check `cat ~/.context-mcp/error.log | tail -5` and help me debug.'
452
398
  );
453
399
  }
454
400
  if (entry.related_to?.length && ctx.stmts.updateRelatedTo) {
@@ -457,43 +403,41 @@ export async function handler(
457
403
  ctx.stmts.updateRelatedTo.run(null, entry.id);
458
404
  }
459
405
  const relPath = entry.filePath
460
- ? entry.filePath.replace(config.vaultDir + "/", "")
406
+ ? entry.filePath.replace(config.vaultDir + '/', '')
461
407
  : entry.filePath;
462
408
  const parts = [`✓ Updated ${entry.kind} → ${relPath}`, ` id: ${entry.id}`];
463
409
  if (entry.title) parts.push(` title: ${entry.title}`);
464
410
  const entryTags = entry.tags || [];
465
- if (entryTags.length) parts.push(` tags: ${entryTags.join(", ")}`);
466
- parts.push("", "_Search with get_context to verify changes._");
467
- return ok(parts.join("\n"));
411
+ if (entryTags.length) parts.push(` tags: ${entryTags.join(', ')}`);
412
+ parts.push('', '_Search with get_context to verify changes._');
413
+ return ok(parts.join('\n'));
468
414
  }
469
415
 
470
416
  // ── Create mode ──
471
- if (!kind) return err("Required: kind (for new entries)", "INVALID_INPUT");
417
+ if (!kind) return err('Required: kind (for new entries)', 'INVALID_INPUT');
472
418
  const kindErr = ensureValidKind(kind);
473
419
  if (kindErr) return kindErr;
474
- if (!body?.trim())
475
- return err("Required: body (for new entries)", "INVALID_INPUT");
420
+ if (!body?.trim()) return err('Required: body (for new entries)', 'INVALID_INPUT');
476
421
 
477
422
  // Normalize kind to canonical singular form (e.g. "insights" → "insight")
478
423
  const normalizedKind = normalizeKind(kind);
479
424
 
480
- if (categoryFor(normalizedKind) === "entity" && !identity_key) {
481
- return err(
482
- `Entity kind "${normalizedKind}" requires identity_key`,
483
- "MISSING_IDENTITY_KEY",
484
- );
425
+ if (categoryFor(normalizedKind) === 'entity' && !identity_key) {
426
+ return err(`Entity kind "${normalizedKind}" requires identity_key`, 'MISSING_IDENTITY_KEY');
485
427
  }
486
428
 
487
- await ensureIndexed();
429
+ // Start reindex in background but don't wait — similarity check
430
+ // may miss unindexed entries, but the save won't time out
431
+ await ensureIndexed({ blocking: false });
488
432
 
489
433
  // ── Similarity check (knowledge + event only) ────────────────────────────
490
434
  const category = categoryFor(normalizedKind);
491
- let similarEntries = [];
492
- let queryEmbedding = null;
435
+ let similarEntries: any[] = [];
436
+ let queryEmbedding: any = null;
493
437
 
494
- if (category === "knowledge" || category === "event") {
438
+ if (category === 'knowledge' || category === 'event') {
495
439
  const threshold = similarity_threshold ?? DEFAULT_SIMILARITY_THRESHOLD;
496
- const embeddingText = [title, body].filter(Boolean).join(" ");
440
+ const embeddingText = [title, body].filter(Boolean).join(' ');
497
441
  try {
498
442
  queryEmbedding = await ctx.embed(embeddingText);
499
443
  } catch {
@@ -505,43 +449,43 @@ export async function handler(
505
449
  queryEmbedding,
506
450
  threshold,
507
451
 
508
- { hydrate: suggestMode },
452
+ { hydrate: suggestMode }
509
453
  );
510
454
  }
511
455
  }
512
456
 
513
457
  if (dry_run) {
514
- const parts = ["(dry run — nothing saved)"];
458
+ const parts = ['(dry run — nothing saved)'];
515
459
  if (similarEntries.length) {
516
460
  if (suggestMode) {
517
461
  const candidates = buildConflictCandidates(similarEntries);
518
- parts.push("", "⚠ Similar entries already exist:");
462
+ parts.push('', '⚠ Similar entries already exist:');
519
463
  for (const e of similarEntries) {
520
464
  const score = e.score.toFixed(2);
521
- const titleDisplay = e.title ? `"${e.title}"` : "(no title)";
465
+ const titleDisplay = e.title ? `"${e.title}"` : '(no title)';
522
466
  parts.push(` - ${titleDisplay} (${score}) — id: ${e.id}`);
523
467
  }
524
468
  parts.push(formatConflictSuggestions(candidates));
525
469
  parts.push(
526
- "",
527
- "Use save_context with `id: <existing>` to update one, or omit `dry_run` to save as new.",
470
+ '',
471
+ 'Use save_context with `id: <existing>` to update one, or omit `dry_run` to save as new.'
528
472
  );
529
473
  } else {
530
- parts.push("", "⚠ Similar entries already exist:");
474
+ parts.push('', '⚠ Similar entries already exist:');
531
475
  for (const e of similarEntries) {
532
476
  const score = e.score.toFixed(2);
533
- const titleDisplay = e.title ? `"${e.title}"` : "(no title)";
477
+ const titleDisplay = e.title ? `"${e.title}"` : '(no title)';
534
478
  parts.push(` - ${titleDisplay} (${score}) — id: ${e.id}`);
535
479
  }
536
480
  parts.push(
537
- "",
538
- "Use save_context with `id: <existing>` to update one, or omit `dry_run` to save as new.",
481
+ '',
482
+ 'Use save_context with `id: <existing>` to update one, or omit `dry_run` to save as new.'
539
483
  );
540
484
  }
541
485
  } else {
542
- parts.push("", "No similar entries found. Safe to save.");
486
+ parts.push('', 'No similar entries found. Safe to save.');
543
487
  }
544
- return ok(parts.join("\n"));
488
+ return ok(parts.join('\n'));
545
489
  }
546
490
 
547
491
  const mergedMeta = { ...(meta || {}) };
@@ -550,7 +494,7 @@ export async function handler(
550
494
 
551
495
  const effectiveTier = tier ?? defaultTierFor(normalizedKind);
552
496
 
553
- const embeddingToReuse = category === "knowledge" ? queryEmbedding : null;
497
+ const embeddingToReuse = category === 'knowledge' ? queryEmbedding : null;
554
498
 
555
499
  let entry;
556
500
  try {
@@ -572,13 +516,13 @@ export async function handler(
572
516
 
573
517
  tier: effectiveTier,
574
518
  },
575
- embeddingToReuse,
519
+ embeddingToReuse
576
520
  );
577
521
  } catch (e) {
578
522
  return errWithHint(
579
- e.message,
580
- "SAVE_FAILED",
581
- "context-vault save_context is failing. Check `cat ~/.context-mcp/error.log | tail -5` and help me debug.",
523
+ e instanceof Error ? e.message : String(e),
524
+ 'SAVE_FAILED',
525
+ 'context-vault save_context is failing. Check `cat ~/.context-mcp/error.log | tail -5` and help me debug.'
582
526
  );
583
527
  }
584
528
 
@@ -587,37 +531,37 @@ export async function handler(
587
531
  }
588
532
 
589
533
  const relPath = entry.filePath
590
- ? entry.filePath.replace(config.vaultDir + "/", "")
534
+ ? entry.filePath.replace(config.vaultDir + '/', '')
591
535
  : entry.filePath;
592
536
  const parts = [`✓ Saved ${normalizedKind} → ${relPath}`, ` id: ${entry.id}`];
593
537
  if (title) parts.push(` title: ${title}`);
594
- if (tags?.length) parts.push(` tags: ${tags.join(", ")}`);
538
+ if (tags?.length) parts.push(` tags: ${tags.join(', ')}`);
595
539
  parts.push(` tier: ${effectiveTier}`);
596
- parts.push("", "_Use this id to update or delete later._");
540
+ parts.push('', '_Use this id to update or delete later._');
597
541
  const hasBucketTag = (tags || []).some(
598
- (t) => typeof t === "string" && t.startsWith("bucket:"),
542
+ (t: any) => typeof t === 'string' && t.startsWith('bucket:')
599
543
  );
600
544
  if (tags && tags.length > 0 && !hasBucketTag) {
601
545
  parts.push(
602
- "",
603
- "_Tip: Consider adding a `bucket:` tag (e.g., `bucket:myproject`) for project-scoped retrieval._",
546
+ '',
547
+ '_Tip: Consider adding a `bucket:` tag (e.g., `bucket:myproject`) for project-scoped retrieval._'
604
548
  );
605
549
  }
606
550
  const bucketTags = (tags || []).filter(
607
- (t) => typeof t === "string" && t.startsWith("bucket:"),
551
+ (t: any) => typeof t === 'string' && t.startsWith('bucket:')
608
552
  );
609
553
  for (const bt of bucketTags) {
610
- const bucketUserClause = "";
554
+ const bucketUserClause = '';
611
555
  const bucketParams = false ? [bt] : [bt];
612
556
  const exists = ctx.db
613
557
  .prepare(
614
- `SELECT 1 FROM vault WHERE kind = 'bucket' AND identity_key = ? ${bucketUserClause} LIMIT 1`,
558
+ `SELECT 1 FROM vault WHERE kind = 'bucket' AND identity_key = ? ${bucketUserClause} LIMIT 1`
615
559
  )
616
560
  .get(...bucketParams);
617
561
  if (!exists) {
618
562
  parts.push(
619
563
  ``,
620
- `_Note: bucket '${bt}' is not registered. Use save_context(kind: "bucket", identity_key: "${bt}") to register it._`,
564
+ `_Note: bucket '${bt}' is not registered. Use save_context(kind: "bucket", identity_key: "${bt}") to register it._`
621
565
  );
622
566
  }
623
567
  }
@@ -634,15 +578,15 @@ export async function handler(
634
578
  const criticalLimit = config.thresholds?.totalEntries?.critical;
635
579
  if (criticalLimit != null) {
636
580
  try {
637
- const countRow = ctx.db.prepare("SELECT COUNT(*) as c FROM vault").get();
638
- if (countRow.c >= criticalLimit) {
581
+ const countRow = ctx.db.prepare('SELECT COUNT(*) as c FROM vault').get() as any;
582
+ if (countRow?.c != null && countRow.c >= criticalLimit) {
639
583
  parts.push(
640
584
  ``,
641
- `ℹ Vault has ${countRow.c.toLocaleString()} entries. Consider running \`context-vault reindex\` or reviewing old entries.`,
585
+ `ℹ Vault has ${countRow.c.toLocaleString()} entries. Consider running \`context-vault reindex\` or reviewing old entries.`
642
586
  );
643
587
  }
644
588
  } catch {}
645
589
  }
646
590
 
647
- return ok(parts.join("\n"));
591
+ return ok(parts.join('\n'));
648
592
  }