context-vault 3.1.6 → 3.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/cli.js +1369 -1774
- package/node_modules/@context-vault/core/dist/capture.d.ts +1 -1
- package/node_modules/@context-vault/core/dist/capture.d.ts.map +1 -1
- package/node_modules/@context-vault/core/dist/capture.js +34 -47
- package/node_modules/@context-vault/core/dist/capture.js.map +1 -1
- package/node_modules/@context-vault/core/dist/categories.js +30 -30
- package/node_modules/@context-vault/core/dist/config.d.ts +1 -1
- package/node_modules/@context-vault/core/dist/config.d.ts.map +1 -1
- package/node_modules/@context-vault/core/dist/config.js +37 -43
- package/node_modules/@context-vault/core/dist/config.js.map +1 -1
- package/node_modules/@context-vault/core/dist/constants.d.ts.map +1 -1
- package/node_modules/@context-vault/core/dist/constants.js +4 -4
- package/node_modules/@context-vault/core/dist/constants.js.map +1 -1
- package/node_modules/@context-vault/core/dist/db.d.ts +2 -2
- package/node_modules/@context-vault/core/dist/db.d.ts.map +1 -1
- package/node_modules/@context-vault/core/dist/db.js +21 -20
- package/node_modules/@context-vault/core/dist/db.js.map +1 -1
- package/node_modules/@context-vault/core/dist/embed.d.ts.map +1 -1
- package/node_modules/@context-vault/core/dist/embed.js +11 -11
- package/node_modules/@context-vault/core/dist/embed.js.map +1 -1
- package/node_modules/@context-vault/core/dist/files.d.ts.map +1 -1
- package/node_modules/@context-vault/core/dist/files.js +12 -13
- package/node_modules/@context-vault/core/dist/files.js.map +1 -1
- package/node_modules/@context-vault/core/dist/formatters.js +5 -5
- package/node_modules/@context-vault/core/dist/frontmatter.d.ts.map +1 -1
- package/node_modules/@context-vault/core/dist/frontmatter.js +23 -23
- package/node_modules/@context-vault/core/dist/frontmatter.js.map +1 -1
- package/node_modules/@context-vault/core/dist/index.d.ts +1 -1
- package/node_modules/@context-vault/core/dist/index.d.ts.map +1 -1
- package/node_modules/@context-vault/core/dist/index.js +58 -46
- package/node_modules/@context-vault/core/dist/index.js.map +1 -1
- package/node_modules/@context-vault/core/dist/ingest-url.d.ts.map +1 -1
- package/node_modules/@context-vault/core/dist/ingest-url.js +30 -33
- package/node_modules/@context-vault/core/dist/ingest-url.js.map +1 -1
- package/node_modules/@context-vault/core/dist/main.d.ts +13 -13
- package/node_modules/@context-vault/core/dist/main.d.ts.map +1 -1
- package/node_modules/@context-vault/core/dist/main.js +12 -12
- package/node_modules/@context-vault/core/dist/main.js.map +1 -1
- package/node_modules/@context-vault/core/dist/search.d.ts +1 -1
- package/node_modules/@context-vault/core/dist/search.d.ts.map +1 -1
- package/node_modules/@context-vault/core/dist/search.js +20 -22
- package/node_modules/@context-vault/core/dist/search.js.map +1 -1
- package/node_modules/@context-vault/core/dist/types.d.ts +1 -1
- package/node_modules/@context-vault/core/package.json +1 -1
- package/node_modules/@context-vault/core/src/capture.ts +44 -81
- package/node_modules/@context-vault/core/src/categories.ts +30 -30
- package/node_modules/@context-vault/core/src/config.ts +45 -60
- package/node_modules/@context-vault/core/src/constants.ts +8 -10
- package/node_modules/@context-vault/core/src/db.ts +37 -56
- package/node_modules/@context-vault/core/src/embed.ts +15 -26
- package/node_modules/@context-vault/core/src/files.ts +13 -16
- package/node_modules/@context-vault/core/src/formatters.ts +5 -5
- package/node_modules/@context-vault/core/src/frontmatter.ts +26 -30
- package/node_modules/@context-vault/core/src/index.ts +94 -100
- package/node_modules/@context-vault/core/src/ingest-url.ts +56 -93
- package/node_modules/@context-vault/core/src/main.ts +13 -18
- package/node_modules/@context-vault/core/src/search.ts +34 -56
- package/node_modules/@context-vault/core/src/types.ts +1 -1
- package/package.json +2 -2
- package/scripts/postinstall.js +18 -25
- package/scripts/prepack.js +13 -19
- package/src/archive.js +211 -0
- package/src/error-log.js +7 -7
- package/src/helpers.js +11 -13
- package/src/linking.js +8 -11
- package/src/migrate-dirs.js +139 -0
- package/src/register-tools.js +46 -48
- package/src/server.js +73 -99
- package/src/status.js +35 -71
- package/src/telemetry.js +18 -22
- package/src/temporal.js +19 -30
- package/src/tools/clear-context.js +15 -18
- package/src/tools/context-status.js +37 -57
- package/src/tools/create-snapshot.js +45 -57
- package/src/tools/delete-context.js +11 -12
- package/src/tools/get-context.js +112 -160
- package/src/tools/ingest-project.js +66 -86
- package/src/tools/ingest-url.js +25 -41
- package/src/tools/list-buckets.js +19 -25
- package/src/tools/list-context.js +35 -58
- package/src/tools/save-context.js +126 -182
- package/src/tools/session-start.js +46 -62
|
@@ -1,17 +1,11 @@
|
|
|
1
|
-
import { z } from
|
|
2
|
-
import { captureAndIndex, updateEntryFile } from
|
|
3
|
-
import { indexEntry } from
|
|
4
|
-
import { categoryFor, defaultTierFor } from
|
|
5
|
-
import { normalizeKind } from
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
|
|
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';
|
|
15
9
|
import {
|
|
16
10
|
MAX_BODY_LENGTH,
|
|
17
11
|
MAX_TITLE_LENGTH,
|
|
@@ -21,7 +15,7 @@ import {
|
|
|
21
15
|
MAX_META_LENGTH,
|
|
22
16
|
MAX_SOURCE_LENGTH,
|
|
23
17
|
MAX_IDENTITY_KEY_LENGTH,
|
|
24
|
-
} from
|
|
18
|
+
} from '@context-vault/core/constants';
|
|
25
19
|
|
|
26
20
|
const DEFAULT_SIMILARITY_THRESHOLD = 0.85;
|
|
27
21
|
const SKIP_THRESHOLD = 0.95;
|
|
@@ -32,33 +26,31 @@ async function findSimilar(
|
|
|
32
26
|
embedding,
|
|
33
27
|
threshold,
|
|
34
28
|
|
|
35
|
-
{ hydrate = false } = {}
|
|
29
|
+
{ hydrate = false } = {}
|
|
36
30
|
) {
|
|
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().c;
|
|
41
33
|
if (vecCount === 0) return [];
|
|
42
34
|
|
|
43
35
|
const vecRows = 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
39
|
.all(embedding, 10);
|
|
48
40
|
|
|
49
41
|
if (!vecRows.length) return [];
|
|
50
42
|
|
|
51
43
|
const rowids = vecRows.map((vr) => vr.rowid);
|
|
52
|
-
const placeholders = rowids.map(() =>
|
|
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 ===
|
|
46
|
+
const isLocal = ctx.stmts._mode === 'local';
|
|
55
47
|
const columns = isLocal
|
|
56
48
|
? hydrate
|
|
57
|
-
?
|
|
58
|
-
:
|
|
49
|
+
? 'rowid, id, title, body, kind, tags, category, updated_at'
|
|
50
|
+
: 'rowid, id, title, category'
|
|
59
51
|
: hydrate
|
|
60
|
-
?
|
|
61
|
-
:
|
|
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);
|
|
@@ -72,7 +64,7 @@ async function findSimilar(
|
|
|
72
64
|
if (similarity < threshold) continue;
|
|
73
65
|
const row = byRowid.get(vr.rowid);
|
|
74
66
|
if (!row) continue;
|
|
75
|
-
if (row.category ===
|
|
67
|
+
if (row.category === 'entity') continue;
|
|
76
68
|
const entry = { id: row.id, title: row.title, score: similarity };
|
|
77
69
|
if (hydrate) {
|
|
78
70
|
entry.body = row.body;
|
|
@@ -89,16 +81,14 @@ async function findSimilar(
|
|
|
89
81
|
}
|
|
90
82
|
|
|
91
83
|
function formatSimilarWarning(similar) {
|
|
92
|
-
const lines = [
|
|
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}"` :
|
|
87
|
+
const title = e.title ? `"${e.title}"` : '(no title)';
|
|
96
88
|
lines.push(` - ${title} (${score}) — id: ${e.id}`);
|
|
97
89
|
}
|
|
98
|
-
lines.push(
|
|
99
|
-
|
|
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
94
|
export function buildConflictCandidates(similarEntries) {
|
|
@@ -107,31 +97,30 @@ export function buildConflictCandidates(similarEntries) {
|
|
|
107
97
|
let reasoning_context;
|
|
108
98
|
|
|
109
99
|
if (entry.score >= SKIP_THRESHOLD) {
|
|
110
|
-
suggested_action =
|
|
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 =
|
|
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 =
|
|
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
|
}
|
|
@@ -152,184 +141,145 @@ export function buildConflictCandidates(similarEntries) {
|
|
|
152
141
|
}
|
|
153
142
|
|
|
154
143
|
function formatConflictSuggestions(candidates) {
|
|
155
|
-
const lines = [
|
|
144
|
+
const lines = ['', '── Conflict Resolution Suggestions ──'];
|
|
156
145
|
for (const c of candidates) {
|
|
157
|
-
const titleDisplay = c.title ? `"${c.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(
|
|
152
|
+
return lines.join('\n');
|
|
164
153
|
}
|
|
165
154
|
|
|
166
155
|
/**
|
|
167
156
|
* Validate input fields for save_context. Returns an error response or null.
|
|
168
157
|
*/
|
|
169
|
-
function validateSaveInput({
|
|
170
|
-
kind,
|
|
171
|
-
title,
|
|
172
|
-
body,
|
|
173
|
-
tags,
|
|
174
|
-
meta,
|
|
175
|
-
source,
|
|
176
|
-
identity_key,
|
|
177
|
-
expires_at,
|
|
178
|
-
}) {
|
|
158
|
+
function validateSaveInput({ kind, title, body, tags, meta, source, identity_key, expires_at }) {
|
|
179
159
|
if (kind !== undefined && kind !== null) {
|
|
180
|
-
if (typeof kind !==
|
|
181
|
-
return err(
|
|
182
|
-
`kind must be a string, max ${MAX_KIND_LENGTH} chars`,
|
|
183
|
-
"INVALID_INPUT",
|
|
184
|
-
);
|
|
160
|
+
if (typeof kind !== 'string' || kind.length > MAX_KIND_LENGTH) {
|
|
161
|
+
return err(`kind must be a string, max ${MAX_KIND_LENGTH} chars`, 'INVALID_INPUT');
|
|
185
162
|
}
|
|
186
163
|
}
|
|
187
164
|
if (body !== undefined && body !== null) {
|
|
188
|
-
if (typeof body !==
|
|
189
|
-
return err(
|
|
190
|
-
`body must be a string, max ${MAX_BODY_LENGTH / 1024}KB`,
|
|
191
|
-
"INVALID_INPUT",
|
|
192
|
-
);
|
|
165
|
+
if (typeof body !== 'string' || body.length > MAX_BODY_LENGTH) {
|
|
166
|
+
return err(`body must be a string, max ${MAX_BODY_LENGTH / 1024}KB`, 'INVALID_INPUT');
|
|
193
167
|
}
|
|
194
168
|
}
|
|
195
169
|
if (title !== undefined && title !== null) {
|
|
196
|
-
if (typeof title !==
|
|
197
|
-
return err(
|
|
198
|
-
`title must be a string, max ${MAX_TITLE_LENGTH} chars`,
|
|
199
|
-
"INVALID_INPUT",
|
|
200
|
-
);
|
|
170
|
+
if (typeof title !== 'string' || title.length > MAX_TITLE_LENGTH) {
|
|
171
|
+
return err(`title must be a string, max ${MAX_TITLE_LENGTH} chars`, 'INVALID_INPUT');
|
|
201
172
|
}
|
|
202
173
|
}
|
|
203
174
|
if (tags !== undefined && tags !== null) {
|
|
204
|
-
if (!Array.isArray(tags))
|
|
205
|
-
return err("tags must be an array of strings", "INVALID_INPUT");
|
|
175
|
+
if (!Array.isArray(tags)) return err('tags must be an array of strings', 'INVALID_INPUT');
|
|
206
176
|
if (tags.length > MAX_TAGS_COUNT)
|
|
207
|
-
return err(`tags: max ${MAX_TAGS_COUNT} tags allowed`,
|
|
177
|
+
return err(`tags: max ${MAX_TAGS_COUNT} tags allowed`, 'INVALID_INPUT');
|
|
208
178
|
for (const tag of tags) {
|
|
209
|
-
if (typeof tag !==
|
|
210
|
-
return err(
|
|
211
|
-
`each tag must be a string, max ${MAX_TAG_LENGTH} chars`,
|
|
212
|
-
"INVALID_INPUT",
|
|
213
|
-
);
|
|
179
|
+
if (typeof tag !== 'string' || tag.length > MAX_TAG_LENGTH) {
|
|
180
|
+
return err(`each tag must be a string, max ${MAX_TAG_LENGTH} chars`, 'INVALID_INPUT');
|
|
214
181
|
}
|
|
215
182
|
}
|
|
216
183
|
}
|
|
217
184
|
if (meta !== undefined && meta !== null) {
|
|
218
185
|
const metaStr = JSON.stringify(meta);
|
|
219
186
|
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
|
-
);
|
|
187
|
+
return err(`meta must be under ${MAX_META_LENGTH / 1024}KB when serialized`, 'INVALID_INPUT');
|
|
224
188
|
}
|
|
225
189
|
}
|
|
226
190
|
if (source !== undefined && source !== null) {
|
|
227
|
-
if (typeof source !==
|
|
228
|
-
return err(
|
|
229
|
-
`source must be a string, max ${MAX_SOURCE_LENGTH} chars`,
|
|
230
|
-
"INVALID_INPUT",
|
|
231
|
-
);
|
|
191
|
+
if (typeof source !== 'string' || source.length > MAX_SOURCE_LENGTH) {
|
|
192
|
+
return err(`source must be a string, max ${MAX_SOURCE_LENGTH} chars`, 'INVALID_INPUT');
|
|
232
193
|
}
|
|
233
194
|
}
|
|
234
195
|
if (identity_key !== undefined && identity_key !== null) {
|
|
235
|
-
if (
|
|
236
|
-
typeof identity_key !== "string" ||
|
|
237
|
-
identity_key.length > MAX_IDENTITY_KEY_LENGTH
|
|
238
|
-
) {
|
|
196
|
+
if (typeof identity_key !== 'string' || identity_key.length > MAX_IDENTITY_KEY_LENGTH) {
|
|
239
197
|
return err(
|
|
240
198
|
`identity_key must be a string, max ${MAX_IDENTITY_KEY_LENGTH} chars`,
|
|
241
|
-
|
|
199
|
+
'INVALID_INPUT'
|
|
242
200
|
);
|
|
243
201
|
}
|
|
244
202
|
}
|
|
245
203
|
if (expires_at !== undefined && expires_at !== null) {
|
|
246
|
-
if (
|
|
247
|
-
|
|
248
|
-
isNaN(new Date(expires_at).getTime())
|
|
249
|
-
) {
|
|
250
|
-
return err("expires_at must be a valid ISO date string", "INVALID_INPUT");
|
|
204
|
+
if (typeof expires_at !== 'string' || isNaN(new Date(expires_at).getTime())) {
|
|
205
|
+
return err('expires_at must be a valid ISO date string', 'INVALID_INPUT');
|
|
251
206
|
}
|
|
252
207
|
}
|
|
253
208
|
return null;
|
|
254
209
|
}
|
|
255
210
|
|
|
256
|
-
export const name =
|
|
211
|
+
export const name = 'save_context';
|
|
257
212
|
|
|
258
213
|
export const description =
|
|
259
|
-
|
|
214
|
+
'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
215
|
|
|
261
216
|
export const inputSchema = {
|
|
262
217
|
id: z
|
|
263
218
|
.string()
|
|
264
219
|
.optional()
|
|
265
220
|
.describe(
|
|
266
|
-
|
|
221
|
+
'Entry ULID to update. When provided, updates the existing entry instead of creating new. Omitted fields are preserved.'
|
|
267
222
|
),
|
|
268
223
|
kind: z
|
|
269
224
|
.string()
|
|
270
225
|
.optional()
|
|
271
226
|
.describe(
|
|
272
|
-
"Entry kind — determines folder (e.g. 'insight', 'decision', 'pattern', 'reference', or any custom kind). Required for new entries."
|
|
227
|
+
"Entry kind — determines folder (e.g. 'insight', 'decision', 'pattern', 'reference', or any custom kind). Required for new entries."
|
|
273
228
|
),
|
|
274
|
-
title: z.string().optional().describe(
|
|
275
|
-
body: z
|
|
276
|
-
.string()
|
|
277
|
-
.optional()
|
|
278
|
-
.describe("Main content. Required for new entries."),
|
|
229
|
+
title: z.string().optional().describe('Entry title (optional for insights)'),
|
|
230
|
+
body: z.string().optional().describe('Main content. Required for new entries.'),
|
|
279
231
|
tags: z
|
|
280
232
|
.array(z.string())
|
|
281
233
|
.optional()
|
|
282
234
|
.describe(
|
|
283
|
-
"Tags for categorization and search. Use 'bucket:' prefix for project/domain scoping (e.g., 'bucket:autohub') to enable project-scoped retrieval."
|
|
235
|
+
"Tags for categorization and search. Use 'bucket:' prefix for project/domain scoping (e.g., 'bucket:autohub') to enable project-scoped retrieval."
|
|
284
236
|
),
|
|
285
237
|
meta: z
|
|
286
238
|
.any()
|
|
287
239
|
.optional()
|
|
288
240
|
.describe(
|
|
289
|
-
"Additional structured metadata (JSON object, e.g. { language: 'js', status: 'accepted' })"
|
|
241
|
+
"Additional structured metadata (JSON object, e.g. { language: 'js', status: 'accepted' })"
|
|
290
242
|
),
|
|
291
243
|
folder: z
|
|
292
244
|
.string()
|
|
293
245
|
.optional()
|
|
294
246
|
.describe("Subfolder within the kind directory (e.g. 'react/hooks')"),
|
|
295
|
-
source: z.string().optional().describe(
|
|
247
|
+
source: z.string().optional().describe('Where this knowledge came from'),
|
|
296
248
|
identity_key: z
|
|
297
249
|
.string()
|
|
298
250
|
.optional()
|
|
299
251
|
.describe(
|
|
300
|
-
|
|
252
|
+
'Required for entity kinds (contact, project, tool, source). The unique identifier for this entity.'
|
|
301
253
|
),
|
|
302
|
-
expires_at: z.string().optional().describe(
|
|
254
|
+
expires_at: z.string().optional().describe('ISO date for TTL expiry'),
|
|
303
255
|
supersedes: z
|
|
304
256
|
.array(z.string())
|
|
305
257
|
.optional()
|
|
306
258
|
.describe(
|
|
307
|
-
|
|
259
|
+
'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
260
|
),
|
|
309
261
|
related_to: z
|
|
310
262
|
.array(z.string())
|
|
311
263
|
.optional()
|
|
312
264
|
.describe(
|
|
313
|
-
|
|
265
|
+
'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
266
|
),
|
|
315
267
|
source_files: z
|
|
316
268
|
.array(
|
|
317
269
|
z.object({
|
|
318
|
-
path: z.string().describe(
|
|
319
|
-
hash: z
|
|
320
|
-
|
|
321
|
-
.describe("SHA-256 hash of the file contents at observation time"),
|
|
322
|
-
}),
|
|
270
|
+
path: z.string().describe('File path (absolute or relative to cwd)'),
|
|
271
|
+
hash: z.string().describe('SHA-256 hash of the file contents at observation time'),
|
|
272
|
+
})
|
|
323
273
|
)
|
|
324
274
|
.optional()
|
|
325
275
|
.describe(
|
|
326
|
-
|
|
276
|
+
'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
277
|
),
|
|
328
278
|
dry_run: z
|
|
329
279
|
.boolean()
|
|
330
280
|
.optional()
|
|
331
281
|
.describe(
|
|
332
|
-
|
|
282
|
+
'If true, check for similar entries without saving. Returns similarity results without creating a new entry. Only applies to knowledge and event categories.'
|
|
333
283
|
),
|
|
334
284
|
similarity_threshold: z
|
|
335
285
|
.number()
|
|
@@ -337,19 +287,19 @@ export const inputSchema = {
|
|
|
337
287
|
.max(1)
|
|
338
288
|
.optional()
|
|
339
289
|
.describe(
|
|
340
|
-
|
|
290
|
+
'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
291
|
),
|
|
342
292
|
tier: z
|
|
343
|
-
.enum([
|
|
293
|
+
.enum(['ephemeral', 'working', 'durable'])
|
|
344
294
|
.optional()
|
|
345
295
|
.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."
|
|
296
|
+
"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
297
|
),
|
|
348
298
|
conflict_resolution: z
|
|
349
|
-
.enum([
|
|
299
|
+
.enum(['suggest', 'off'])
|
|
350
300
|
.optional()
|
|
351
301
|
.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).'
|
|
302
|
+
'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
303
|
),
|
|
354
304
|
};
|
|
355
305
|
|
|
@@ -379,16 +329,16 @@ export async function handler(
|
|
|
379
329
|
conflict_resolution,
|
|
380
330
|
},
|
|
381
331
|
ctx,
|
|
382
|
-
{ ensureIndexed }
|
|
332
|
+
{ ensureIndexed }
|
|
383
333
|
) {
|
|
384
334
|
const { config } = ctx;
|
|
385
|
-
const suggestMode = conflict_resolution !==
|
|
335
|
+
const suggestMode = conflict_resolution !== 'off';
|
|
386
336
|
|
|
387
337
|
const vaultErr = ensureVaultExists(config);
|
|
388
338
|
if (vaultErr) return vaultErr;
|
|
389
339
|
|
|
390
340
|
const relatedToErr = validateRelatedTo(related_to);
|
|
391
|
-
if (relatedToErr) return err(relatedToErr,
|
|
341
|
+
if (relatedToErr) return err(relatedToErr, 'INVALID_INPUT');
|
|
392
342
|
|
|
393
343
|
const inputErr = validateSaveInput({
|
|
394
344
|
kind,
|
|
@@ -404,21 +354,21 @@ export async function handler(
|
|
|
404
354
|
|
|
405
355
|
// ── Update mode ──
|
|
406
356
|
if (id) {
|
|
407
|
-
await ensureIndexed();
|
|
357
|
+
await ensureIndexed({ blocking: false });
|
|
408
358
|
|
|
409
359
|
const existing = ctx.stmts.getEntryById.get(id);
|
|
410
|
-
if (!existing) return err(`Entry not found: ${id}`,
|
|
360
|
+
if (!existing) return err(`Entry not found: ${id}`, 'NOT_FOUND');
|
|
411
361
|
|
|
412
362
|
if (kind && normalizeKind(kind) !== existing.kind) {
|
|
413
363
|
return err(
|
|
414
364
|
`Cannot change kind (current: "${existing.kind}"). Delete and re-create instead.`,
|
|
415
|
-
|
|
365
|
+
'INVALID_UPDATE'
|
|
416
366
|
);
|
|
417
367
|
}
|
|
418
368
|
if (identity_key && identity_key !== existing.identity_key) {
|
|
419
369
|
return err(
|
|
420
370
|
`Cannot change identity_key (current: "${existing.identity_key}"). Delete and re-create instead.`,
|
|
421
|
-
|
|
371
|
+
'INVALID_UPDATE'
|
|
422
372
|
);
|
|
423
373
|
}
|
|
424
374
|
|
|
@@ -447,8 +397,8 @@ export async function handler(
|
|
|
447
397
|
} catch (e) {
|
|
448
398
|
return errWithHint(
|
|
449
399
|
e.message,
|
|
450
|
-
|
|
451
|
-
|
|
400
|
+
'UPDATE_FAILED',
|
|
401
|
+
'context-vault save_context update is failing. Check `cat ~/.context-mcp/error.log | tail -5` and help me debug.'
|
|
452
402
|
);
|
|
453
403
|
}
|
|
454
404
|
if (entry.related_to?.length && ctx.stmts.updateRelatedTo) {
|
|
@@ -457,43 +407,41 @@ export async function handler(
|
|
|
457
407
|
ctx.stmts.updateRelatedTo.run(null, entry.id);
|
|
458
408
|
}
|
|
459
409
|
const relPath = entry.filePath
|
|
460
|
-
? entry.filePath.replace(config.vaultDir +
|
|
410
|
+
? entry.filePath.replace(config.vaultDir + '/', '')
|
|
461
411
|
: entry.filePath;
|
|
462
412
|
const parts = [`✓ Updated ${entry.kind} → ${relPath}`, ` id: ${entry.id}`];
|
|
463
413
|
if (entry.title) parts.push(` title: ${entry.title}`);
|
|
464
414
|
const entryTags = entry.tags || [];
|
|
465
|
-
if (entryTags.length) parts.push(` tags: ${entryTags.join(
|
|
466
|
-
parts.push(
|
|
467
|
-
return ok(parts.join(
|
|
415
|
+
if (entryTags.length) parts.push(` tags: ${entryTags.join(', ')}`);
|
|
416
|
+
parts.push('', '_Search with get_context to verify changes._');
|
|
417
|
+
return ok(parts.join('\n'));
|
|
468
418
|
}
|
|
469
419
|
|
|
470
420
|
// ── Create mode ──
|
|
471
|
-
if (!kind) return err(
|
|
421
|
+
if (!kind) return err('Required: kind (for new entries)', 'INVALID_INPUT');
|
|
472
422
|
const kindErr = ensureValidKind(kind);
|
|
473
423
|
if (kindErr) return kindErr;
|
|
474
|
-
if (!body?.trim())
|
|
475
|
-
return err("Required: body (for new entries)", "INVALID_INPUT");
|
|
424
|
+
if (!body?.trim()) return err('Required: body (for new entries)', 'INVALID_INPUT');
|
|
476
425
|
|
|
477
426
|
// Normalize kind to canonical singular form (e.g. "insights" → "insight")
|
|
478
427
|
const normalizedKind = normalizeKind(kind);
|
|
479
428
|
|
|
480
|
-
if (categoryFor(normalizedKind) ===
|
|
481
|
-
return err(
|
|
482
|
-
`Entity kind "${normalizedKind}" requires identity_key`,
|
|
483
|
-
"MISSING_IDENTITY_KEY",
|
|
484
|
-
);
|
|
429
|
+
if (categoryFor(normalizedKind) === 'entity' && !identity_key) {
|
|
430
|
+
return err(`Entity kind "${normalizedKind}" requires identity_key`, 'MISSING_IDENTITY_KEY');
|
|
485
431
|
}
|
|
486
432
|
|
|
487
|
-
|
|
433
|
+
// Start reindex in background but don't wait — similarity check
|
|
434
|
+
// may miss unindexed entries, but the save won't time out
|
|
435
|
+
await ensureIndexed({ blocking: false });
|
|
488
436
|
|
|
489
437
|
// ── Similarity check (knowledge + event only) ────────────────────────────
|
|
490
438
|
const category = categoryFor(normalizedKind);
|
|
491
439
|
let similarEntries = [];
|
|
492
440
|
let queryEmbedding = null;
|
|
493
441
|
|
|
494
|
-
if (category ===
|
|
442
|
+
if (category === 'knowledge' || category === 'event') {
|
|
495
443
|
const threshold = similarity_threshold ?? DEFAULT_SIMILARITY_THRESHOLD;
|
|
496
|
-
const embeddingText = [title, body].filter(Boolean).join(
|
|
444
|
+
const embeddingText = [title, body].filter(Boolean).join(' ');
|
|
497
445
|
try {
|
|
498
446
|
queryEmbedding = await ctx.embed(embeddingText);
|
|
499
447
|
} catch {
|
|
@@ -505,43 +453,43 @@ export async function handler(
|
|
|
505
453
|
queryEmbedding,
|
|
506
454
|
threshold,
|
|
507
455
|
|
|
508
|
-
{ hydrate: suggestMode }
|
|
456
|
+
{ hydrate: suggestMode }
|
|
509
457
|
);
|
|
510
458
|
}
|
|
511
459
|
}
|
|
512
460
|
|
|
513
461
|
if (dry_run) {
|
|
514
|
-
const parts = [
|
|
462
|
+
const parts = ['(dry run — nothing saved)'];
|
|
515
463
|
if (similarEntries.length) {
|
|
516
464
|
if (suggestMode) {
|
|
517
465
|
const candidates = buildConflictCandidates(similarEntries);
|
|
518
|
-
parts.push(
|
|
466
|
+
parts.push('', '⚠ Similar entries already exist:');
|
|
519
467
|
for (const e of similarEntries) {
|
|
520
468
|
const score = e.score.toFixed(2);
|
|
521
|
-
const titleDisplay = e.title ? `"${e.title}"` :
|
|
469
|
+
const titleDisplay = e.title ? `"${e.title}"` : '(no title)';
|
|
522
470
|
parts.push(` - ${titleDisplay} (${score}) — id: ${e.id}`);
|
|
523
471
|
}
|
|
524
472
|
parts.push(formatConflictSuggestions(candidates));
|
|
525
473
|
parts.push(
|
|
526
|
-
|
|
527
|
-
|
|
474
|
+
'',
|
|
475
|
+
'Use save_context with `id: <existing>` to update one, or omit `dry_run` to save as new.'
|
|
528
476
|
);
|
|
529
477
|
} else {
|
|
530
|
-
parts.push(
|
|
478
|
+
parts.push('', '⚠ Similar entries already exist:');
|
|
531
479
|
for (const e of similarEntries) {
|
|
532
480
|
const score = e.score.toFixed(2);
|
|
533
|
-
const titleDisplay = e.title ? `"${e.title}"` :
|
|
481
|
+
const titleDisplay = e.title ? `"${e.title}"` : '(no title)';
|
|
534
482
|
parts.push(` - ${titleDisplay} (${score}) — id: ${e.id}`);
|
|
535
483
|
}
|
|
536
484
|
parts.push(
|
|
537
|
-
|
|
538
|
-
|
|
485
|
+
'',
|
|
486
|
+
'Use save_context with `id: <existing>` to update one, or omit `dry_run` to save as new.'
|
|
539
487
|
);
|
|
540
488
|
}
|
|
541
489
|
} else {
|
|
542
|
-
parts.push(
|
|
490
|
+
parts.push('', 'No similar entries found. Safe to save.');
|
|
543
491
|
}
|
|
544
|
-
return ok(parts.join(
|
|
492
|
+
return ok(parts.join('\n'));
|
|
545
493
|
}
|
|
546
494
|
|
|
547
495
|
const mergedMeta = { ...(meta || {}) };
|
|
@@ -550,7 +498,7 @@ export async function handler(
|
|
|
550
498
|
|
|
551
499
|
const effectiveTier = tier ?? defaultTierFor(normalizedKind);
|
|
552
500
|
|
|
553
|
-
const embeddingToReuse = category ===
|
|
501
|
+
const embeddingToReuse = category === 'knowledge' ? queryEmbedding : null;
|
|
554
502
|
|
|
555
503
|
let entry;
|
|
556
504
|
try {
|
|
@@ -572,13 +520,13 @@ export async function handler(
|
|
|
572
520
|
|
|
573
521
|
tier: effectiveTier,
|
|
574
522
|
},
|
|
575
|
-
embeddingToReuse
|
|
523
|
+
embeddingToReuse
|
|
576
524
|
);
|
|
577
525
|
} catch (e) {
|
|
578
526
|
return errWithHint(
|
|
579
527
|
e.message,
|
|
580
|
-
|
|
581
|
-
|
|
528
|
+
'SAVE_FAILED',
|
|
529
|
+
'context-vault save_context is failing. Check `cat ~/.context-mcp/error.log | tail -5` and help me debug.'
|
|
582
530
|
);
|
|
583
531
|
}
|
|
584
532
|
|
|
@@ -587,37 +535,33 @@ export async function handler(
|
|
|
587
535
|
}
|
|
588
536
|
|
|
589
537
|
const relPath = entry.filePath
|
|
590
|
-
? entry.filePath.replace(config.vaultDir +
|
|
538
|
+
? entry.filePath.replace(config.vaultDir + '/', '')
|
|
591
539
|
: entry.filePath;
|
|
592
540
|
const parts = [`✓ Saved ${normalizedKind} → ${relPath}`, ` id: ${entry.id}`];
|
|
593
541
|
if (title) parts.push(` title: ${title}`);
|
|
594
|
-
if (tags?.length) parts.push(` tags: ${tags.join(
|
|
542
|
+
if (tags?.length) parts.push(` tags: ${tags.join(', ')}`);
|
|
595
543
|
parts.push(` tier: ${effectiveTier}`);
|
|
596
|
-
parts.push(
|
|
597
|
-
const hasBucketTag = (tags || []).some(
|
|
598
|
-
(t) => typeof t === "string" && t.startsWith("bucket:"),
|
|
599
|
-
);
|
|
544
|
+
parts.push('', '_Use this id to update or delete later._');
|
|
545
|
+
const hasBucketTag = (tags || []).some((t) => typeof t === 'string' && t.startsWith('bucket:'));
|
|
600
546
|
if (tags && tags.length > 0 && !hasBucketTag) {
|
|
601
547
|
parts.push(
|
|
602
|
-
|
|
603
|
-
|
|
548
|
+
'',
|
|
549
|
+
'_Tip: Consider adding a `bucket:` tag (e.g., `bucket:myproject`) for project-scoped retrieval._'
|
|
604
550
|
);
|
|
605
551
|
}
|
|
606
|
-
const bucketTags = (tags || []).filter(
|
|
607
|
-
(t) => typeof t === "string" && t.startsWith("bucket:"),
|
|
608
|
-
);
|
|
552
|
+
const bucketTags = (tags || []).filter((t) => typeof t === 'string' && t.startsWith('bucket:'));
|
|
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(
|
|
581
|
+
const countRow = ctx.db.prepare('SELECT COUNT(*) as c FROM vault').get();
|
|
638
582
|
if (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(
|
|
591
|
+
return ok(parts.join('\n'));
|
|
648
592
|
}
|