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
package/src/tools/get-context.js
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
import { z } from
|
|
2
|
-
import { createHash } from
|
|
3
|
-
import { readFileSync, existsSync } from
|
|
4
|
-
import { resolve } from
|
|
5
|
-
import { hybridSearch } from
|
|
6
|
-
import { categoryFor } from
|
|
7
|
-
import { normalizeKind } from
|
|
8
|
-
import { resolveTemporalParams } from
|
|
9
|
-
import { collectLinkedEntries } from
|
|
10
|
-
import { ok, err, errWithHint } from
|
|
11
|
-
import { isEmbedAvailable } from
|
|
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
12
|
|
|
13
13
|
const STALE_DUPLICATE_DAYS = 7;
|
|
14
14
|
const DEFAULT_PIVOT_COUNT = 2;
|
|
@@ -22,21 +22,18 @@ const BRIEF_SCORE_BOOST = 0.05;
|
|
|
22
22
|
* word boundary. Returns the truncated string with "..." appended.
|
|
23
23
|
*/
|
|
24
24
|
export function skeletonBody(body) {
|
|
25
|
-
if (!body) return
|
|
25
|
+
if (!body) return '';
|
|
26
26
|
if (body.length <= SKELETON_BODY_CHARS) return body;
|
|
27
27
|
const slice = body.slice(0, SKELETON_BODY_CHARS);
|
|
28
|
-
const sentenceEnd = Math.max(
|
|
29
|
-
slice.lastIndexOf(". "),
|
|
30
|
-
slice.lastIndexOf(".\n"),
|
|
31
|
-
);
|
|
28
|
+
const sentenceEnd = Math.max(slice.lastIndexOf('. '), slice.lastIndexOf('.\n'));
|
|
32
29
|
if (sentenceEnd > SKELETON_BODY_CHARS * 0.4) {
|
|
33
|
-
return slice.slice(0, sentenceEnd + 1) +
|
|
30
|
+
return slice.slice(0, sentenceEnd + 1) + '...';
|
|
34
31
|
}
|
|
35
|
-
const wordEnd = slice.lastIndexOf(
|
|
32
|
+
const wordEnd = slice.lastIndexOf(' ');
|
|
36
33
|
if (wordEnd > SKELETON_BODY_CHARS * 0.4) {
|
|
37
|
-
return slice.slice(0, wordEnd) +
|
|
34
|
+
return slice.slice(0, wordEnd) + '...';
|
|
38
35
|
}
|
|
39
|
-
return slice +
|
|
36
|
+
return slice + '...';
|
|
40
37
|
}
|
|
41
38
|
|
|
42
39
|
/**
|
|
@@ -65,15 +62,13 @@ export function detectConflicts(entries, _ctx) {
|
|
|
65
62
|
conflicts.push({
|
|
66
63
|
entry_a_id: entry.id,
|
|
67
64
|
entry_b_id: entry.superseded_by,
|
|
68
|
-
reason:
|
|
65
|
+
reason: 'superseded',
|
|
69
66
|
recommendation: `Discard \`${entry.id}\` — it has been explicitly superseded by \`${entry.superseded_by}\`.`,
|
|
70
67
|
});
|
|
71
68
|
}
|
|
72
69
|
}
|
|
73
70
|
|
|
74
|
-
const supersededConflictPairs = new Set(
|
|
75
|
-
conflicts.map((c) => `${c.entry_a_id}|${c.entry_b_id}`),
|
|
76
|
-
);
|
|
71
|
+
const supersededConflictPairs = new Set(conflicts.map((c) => `${c.entry_a_id}|${c.entry_b_id}`));
|
|
77
72
|
|
|
78
73
|
for (let i = 0; i < entries.length; i++) {
|
|
79
74
|
for (let j = i + 1; j < entries.length; j++) {
|
|
@@ -109,7 +104,7 @@ export function detectConflicts(entries, _ctx) {
|
|
|
109
104
|
conflicts.push({
|
|
110
105
|
entry_a_id: older.id,
|
|
111
106
|
entry_b_id: newer.id,
|
|
112
|
-
reason:
|
|
107
|
+
reason: 'stale_duplicate',
|
|
113
108
|
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
109
|
});
|
|
115
110
|
}
|
|
@@ -140,7 +135,7 @@ export function detectConsolidationHints(entries, db, opts = {}) {
|
|
|
140
135
|
|
|
141
136
|
const candidateTags = new Set();
|
|
142
137
|
for (const entry of entries) {
|
|
143
|
-
if (entry.kind ===
|
|
138
|
+
if (entry.kind === 'brief') continue;
|
|
144
139
|
const entryTags = entry.tags ? JSON.parse(entry.tags) : [];
|
|
145
140
|
for (const tag of entryTags) candidateTags.add(tag);
|
|
146
141
|
}
|
|
@@ -155,11 +150,11 @@ export function detectConsolidationHints(entries, db, opts = {}) {
|
|
|
155
150
|
try {
|
|
156
151
|
// When userId is defined (hosted mode), scope to that user.
|
|
157
152
|
// When userId is undefined (local mode), no user scoping — column may not exist.
|
|
158
|
-
const userClause =
|
|
153
|
+
const userClause = '';
|
|
159
154
|
const countParams = false ? [`%"${tag}"%`] : [`%"${tag}"%`];
|
|
160
155
|
const countRow = db
|
|
161
156
|
.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
|
|
157
|
+
`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`
|
|
163
158
|
)
|
|
164
159
|
.get(...countParams);
|
|
165
160
|
vaultCount = countRow?.c ?? 0;
|
|
@@ -171,17 +166,17 @@ export function detectConsolidationHints(entries, db, opts = {}) {
|
|
|
171
166
|
|
|
172
167
|
let lastSnapshotAgeDays = null;
|
|
173
168
|
try {
|
|
174
|
-
const userClause =
|
|
169
|
+
const userClause = '';
|
|
175
170
|
const params = false ? [`%"${tag}"%`] : [`%"${tag}"%`];
|
|
176
171
|
const recentBrief = db
|
|
177
172
|
.prepare(
|
|
178
|
-
`SELECT created_at FROM vault WHERE kind = 'brief' AND tags LIKE ?${userClause} ORDER BY created_at DESC LIMIT 1
|
|
173
|
+
`SELECT created_at FROM vault WHERE kind = 'brief' AND tags LIKE ?${userClause} ORDER BY created_at DESC LIMIT 1`
|
|
179
174
|
)
|
|
180
175
|
.get(...params);
|
|
181
176
|
|
|
182
177
|
if (recentBrief) {
|
|
183
178
|
lastSnapshotAgeDays = Math.round(
|
|
184
|
-
(Date.now() - new Date(recentBrief.created_at).getTime()) / 86400000
|
|
179
|
+
(Date.now() - new Date(recentBrief.created_at).getTime()) / 86400000
|
|
185
180
|
);
|
|
186
181
|
if (recentBrief.created_at >= cutoff) continue;
|
|
187
182
|
}
|
|
@@ -219,18 +214,16 @@ function checkStaleness(entry) {
|
|
|
219
214
|
|
|
220
215
|
for (const sf of sourceFiles) {
|
|
221
216
|
try {
|
|
222
|
-
const absPath = sf.path.startsWith(
|
|
223
|
-
? sf.path
|
|
224
|
-
: resolve(process.cwd(), sf.path);
|
|
217
|
+
const absPath = sf.path.startsWith('/') ? sf.path : resolve(process.cwd(), sf.path);
|
|
225
218
|
if (!existsSync(absPath)) {
|
|
226
|
-
return { stale: true, stale_reason:
|
|
219
|
+
return { stale: true, stale_reason: 'source file not found' };
|
|
227
220
|
}
|
|
228
221
|
const contents = readFileSync(absPath);
|
|
229
|
-
const currentHash = createHash(
|
|
222
|
+
const currentHash = createHash('sha256').update(contents).digest('hex');
|
|
230
223
|
if (currentHash !== sf.hash) {
|
|
231
224
|
return {
|
|
232
225
|
stale: true,
|
|
233
|
-
stale_reason:
|
|
226
|
+
stale_reason: 'source file modified since observation',
|
|
234
227
|
};
|
|
235
228
|
}
|
|
236
229
|
} catch {
|
|
@@ -240,107 +233,95 @@ function checkStaleness(entry) {
|
|
|
240
233
|
return null;
|
|
241
234
|
}
|
|
242
235
|
|
|
243
|
-
export const name =
|
|
236
|
+
export const name = 'get_context';
|
|
244
237
|
|
|
245
238
|
export const description =
|
|
246
|
-
|
|
239
|
+
'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
240
|
|
|
248
241
|
export const inputSchema = {
|
|
249
242
|
query: z
|
|
250
243
|
.string()
|
|
251
244
|
.optional()
|
|
252
245
|
.describe(
|
|
253
|
-
|
|
246
|
+
'Search query (natural language or keywords). Optional if filters (tags, kind, category) are provided.'
|
|
254
247
|
),
|
|
255
|
-
kind: z
|
|
256
|
-
|
|
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"),
|
|
248
|
+
kind: z.string().optional().describe("Filter by kind (e.g. 'insight', 'decision', 'pattern')"),
|
|
249
|
+
category: z.enum(['knowledge', 'entity', 'event']).optional().describe('Filter by category'),
|
|
263
250
|
identity_key: z
|
|
264
251
|
.string()
|
|
265
252
|
.optional()
|
|
266
|
-
.describe(
|
|
253
|
+
.describe('For entity lookup: exact match on identity key. Requires kind.'),
|
|
267
254
|
tags: z
|
|
268
255
|
.array(z.string())
|
|
269
256
|
.optional()
|
|
270
257
|
.describe(
|
|
271
|
-
"Filter by tags (entries must match at least one). Use 'bucket:' prefixed tags for project-scoped retrieval (e.g., ['bucket:autohub'])."
|
|
258
|
+
"Filter by tags (entries must match at least one). Use 'bucket:' prefixed tags for project-scoped retrieval (e.g., ['bucket:autohub'])."
|
|
272
259
|
),
|
|
273
260
|
buckets: z
|
|
274
261
|
.array(z.string())
|
|
275
262
|
.optional()
|
|
276
263
|
.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)."
|
|
264
|
+
"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
265
|
),
|
|
279
266
|
since: z
|
|
280
267
|
.string()
|
|
281
268
|
.optional()
|
|
282
269
|
.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."
|
|
270
|
+
"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
271
|
),
|
|
285
272
|
until: z
|
|
286
273
|
.string()
|
|
287
274
|
.optional()
|
|
288
275
|
.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."
|
|
276
|
+
"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
277
|
),
|
|
291
|
-
limit: z
|
|
292
|
-
.number()
|
|
293
|
-
.max(500)
|
|
294
|
-
.optional()
|
|
295
|
-
.describe("Max results to return (default 10)"),
|
|
278
|
+
limit: z.number().max(500).optional().describe('Max results to return (default 10)'),
|
|
296
279
|
include_superseded: z
|
|
297
280
|
.boolean()
|
|
298
281
|
.optional()
|
|
299
|
-
.describe(
|
|
300
|
-
"If true, include entries that have been superseded by newer ones. Default: false.",
|
|
301
|
-
),
|
|
282
|
+
.describe('If true, include entries that have been superseded by newer ones. Default: false.'),
|
|
302
283
|
detect_conflicts: z
|
|
303
284
|
.boolean()
|
|
304
285
|
.optional()
|
|
305
286
|
.describe(
|
|
306
|
-
|
|
287
|
+
'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
288
|
),
|
|
308
289
|
max_tokens: z
|
|
309
290
|
.number()
|
|
310
291
|
.max(100000)
|
|
311
292
|
.optional()
|
|
312
293
|
.describe(
|
|
313
|
-
|
|
294
|
+
'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
295
|
),
|
|
315
296
|
pivot_count: z
|
|
316
297
|
.number()
|
|
317
298
|
.optional()
|
|
318
299
|
.describe(
|
|
319
|
-
|
|
300
|
+
'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
301
|
),
|
|
321
302
|
include_ephemeral: z
|
|
322
303
|
.boolean()
|
|
323
304
|
.optional()
|
|
324
305
|
.describe(
|
|
325
|
-
|
|
306
|
+
'If true, include ephemeral tier entries in results. Default: false — only working and durable tiers are returned.'
|
|
326
307
|
),
|
|
327
308
|
include_events: z
|
|
328
309
|
.boolean()
|
|
329
310
|
.optional()
|
|
330
311
|
.describe(
|
|
331
|
-
|
|
312
|
+
'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
313
|
),
|
|
333
314
|
scope: z
|
|
334
|
-
.enum([
|
|
315
|
+
.enum(['hot', 'events', 'all'])
|
|
335
316
|
.optional()
|
|
336
317
|
.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."
|
|
318
|
+
"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
319
|
),
|
|
339
320
|
follow_links: z
|
|
340
321
|
.boolean()
|
|
341
322
|
.optional()
|
|
342
323
|
.describe(
|
|
343
|
-
|
|
324
|
+
'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
325
|
),
|
|
345
326
|
};
|
|
346
327
|
|
|
@@ -370,7 +351,7 @@ export async function handler(
|
|
|
370
351
|
follow_links,
|
|
371
352
|
},
|
|
372
353
|
ctx,
|
|
373
|
-
{ ensureIndexed, reindexFailed }
|
|
354
|
+
{ ensureIndexed, reindexFailed }
|
|
374
355
|
) {
|
|
375
356
|
const { config } = ctx;
|
|
376
357
|
|
|
@@ -387,28 +368,21 @@ export async function handler(
|
|
|
387
368
|
// scope "all": no category restriction — full vault
|
|
388
369
|
let effectiveScope = scope;
|
|
389
370
|
if (!effectiveScope) {
|
|
390
|
-
effectiveScope = include_events ?
|
|
371
|
+
effectiveScope = include_events ? 'all' : 'hot';
|
|
391
372
|
}
|
|
392
373
|
|
|
393
374
|
// Scope "events" forces category to "event" unless caller already set a narrower category
|
|
394
|
-
const scopedCategory =
|
|
395
|
-
|
|
396
|
-
const shouldExcludeEvents =
|
|
397
|
-
hasQuery && effectiveScope === "hot" && !scopedCategory;
|
|
375
|
+
const scopedCategory = !category && effectiveScope === 'events' ? 'event' : category;
|
|
376
|
+
const shouldExcludeEvents = hasQuery && effectiveScope === 'hot' && !scopedCategory;
|
|
398
377
|
// Expand buckets to bucket: prefixed tags and merge with explicit tags
|
|
399
378
|
const bucketTags = buckets?.length ? buckets.map((b) => `bucket:${b}`) : [];
|
|
400
379
|
const effectiveTags = [...(tags ?? []), ...bucketTags];
|
|
401
380
|
const hasFilters =
|
|
402
|
-
kind ||
|
|
403
|
-
scopedCategory ||
|
|
404
|
-
effectiveTags.length ||
|
|
405
|
-
since ||
|
|
406
|
-
until ||
|
|
407
|
-
identity_key;
|
|
381
|
+
kind || scopedCategory || effectiveTags.length || since || until || identity_key;
|
|
408
382
|
if (!hasQuery && !hasFilters)
|
|
409
383
|
return err(
|
|
410
|
-
|
|
411
|
-
|
|
384
|
+
'Required: query or at least one filter (kind, category, tags, since, until, identity_key)',
|
|
385
|
+
'INVALID_INPUT'
|
|
412
386
|
);
|
|
413
387
|
await ensureIndexed();
|
|
414
388
|
|
|
@@ -416,34 +390,32 @@ export async function handler(
|
|
|
416
390
|
|
|
417
391
|
// Gap 1: Entity exact-match by identity_key
|
|
418
392
|
if (identity_key) {
|
|
419
|
-
if (!kindFilter)
|
|
420
|
-
return err("identity_key requires kind to be specified", "INVALID_INPUT");
|
|
393
|
+
if (!kindFilter) return err('identity_key requires kind to be specified', 'INVALID_INPUT');
|
|
421
394
|
const match = ctx.stmts.getByIdentityKey.get(kindFilter, identity_key);
|
|
422
395
|
if (match) {
|
|
423
396
|
const entryTags = match.tags ? JSON.parse(match.tags) : [];
|
|
424
|
-
const tagStr = entryTags.length ? entryTags.join(
|
|
397
|
+
const tagStr = entryTags.length ? entryTags.join(', ') : 'none';
|
|
425
398
|
const relPath =
|
|
426
399
|
match.file_path && config.vaultDir
|
|
427
|
-
? match.file_path.replace(config.vaultDir +
|
|
428
|
-
: match.file_path ||
|
|
400
|
+
? match.file_path.replace(config.vaultDir + '/', '')
|
|
401
|
+
: match.file_path || 'n/a';
|
|
429
402
|
const lines = [
|
|
430
403
|
`## Entity Match (exact)\n`,
|
|
431
|
-
`### ${match.title ||
|
|
404
|
+
`### ${match.title || '(untitled)'} [${match.kind}/${match.category}]`,
|
|
432
405
|
`1.000 · ${tagStr} · ${relPath} · id: \`${match.id}\``,
|
|
433
|
-
match.body?.slice(0, 300) + (match.body?.length > 300 ?
|
|
406
|
+
match.body?.slice(0, 300) + (match.body?.length > 300 ? '...' : ''),
|
|
434
407
|
];
|
|
435
|
-
return ok(lines.join(
|
|
408
|
+
return ok(lines.join('\n'));
|
|
436
409
|
}
|
|
437
410
|
// Fall through to semantic search as fallback
|
|
438
411
|
}
|
|
439
412
|
|
|
440
413
|
// Gap 2: Event default time-window
|
|
441
|
-
const effectiveCategory =
|
|
442
|
-
scopedCategory || (kindFilter ? categoryFor(kindFilter) : null);
|
|
414
|
+
const effectiveCategory = scopedCategory || (kindFilter ? categoryFor(kindFilter) : null);
|
|
443
415
|
let effectiveSince = since || null;
|
|
444
416
|
let effectiveUntil = until || null;
|
|
445
417
|
let autoWindowed = false;
|
|
446
|
-
if (effectiveCategory ===
|
|
418
|
+
if (effectiveCategory === 'event' && !since && !until) {
|
|
447
419
|
const decayMs = (config.eventDecayDays || 30) * 86400000;
|
|
448
420
|
effectiveSince = new Date(Date.now() - decayMs).toISOString();
|
|
449
421
|
autoWindowed = true;
|
|
@@ -487,39 +459,37 @@ export async function handler(
|
|
|
487
459
|
if (false) {
|
|
488
460
|
}
|
|
489
461
|
if (kindFilter) {
|
|
490
|
-
clauses.push(
|
|
462
|
+
clauses.push('kind = ?');
|
|
491
463
|
params.push(kindFilter);
|
|
492
464
|
}
|
|
493
465
|
if (scopedCategory) {
|
|
494
|
-
clauses.push(
|
|
466
|
+
clauses.push('category = ?');
|
|
495
467
|
params.push(scopedCategory);
|
|
496
468
|
}
|
|
497
469
|
if (effectiveSince) {
|
|
498
|
-
clauses.push(
|
|
470
|
+
clauses.push('created_at >= ?');
|
|
499
471
|
params.push(effectiveSince);
|
|
500
472
|
}
|
|
501
473
|
if (effectiveUntil) {
|
|
502
|
-
clauses.push(
|
|
474
|
+
clauses.push('created_at <= ?');
|
|
503
475
|
params.push(effectiveUntil);
|
|
504
476
|
}
|
|
505
477
|
clauses.push("(expires_at IS NULL OR expires_at > datetime('now'))");
|
|
506
478
|
if (!include_superseded) {
|
|
507
|
-
clauses.push(
|
|
479
|
+
clauses.push('superseded_by IS NULL');
|
|
508
480
|
}
|
|
509
|
-
const where = clauses.length ? `WHERE ${clauses.join(
|
|
481
|
+
const where = clauses.length ? `WHERE ${clauses.join(' AND ')}` : '';
|
|
510
482
|
params.push(fetchLimit);
|
|
511
483
|
let rows;
|
|
512
484
|
try {
|
|
513
485
|
rows = ctx.db
|
|
514
|
-
.prepare(
|
|
515
|
-
`SELECT * FROM vault ${where} ORDER BY created_at DESC LIMIT ?`,
|
|
516
|
-
)
|
|
486
|
+
.prepare(`SELECT * FROM vault ${where} ORDER BY created_at DESC LIMIT ?`)
|
|
517
487
|
.all(...params);
|
|
518
488
|
} catch (e) {
|
|
519
489
|
return errWithHint(
|
|
520
490
|
e.message,
|
|
521
|
-
|
|
522
|
-
|
|
491
|
+
'DB_ERROR',
|
|
492
|
+
'context-vault get_context DB_ERROR. Check `cat ~/.context-mcp/error.log | tail -5` and help me debug.'
|
|
523
493
|
);
|
|
524
494
|
}
|
|
525
495
|
|
|
@@ -540,18 +510,18 @@ export async function handler(
|
|
|
540
510
|
// Brief score boost: briefs rank slightly higher so consolidated snapshots
|
|
541
511
|
// surface above the individual entries they summarize.
|
|
542
512
|
for (const r of filtered) {
|
|
543
|
-
if (r.kind ===
|
|
513
|
+
if (r.kind === 'brief') r.score = (r.score || 0) + BRIEF_SCORE_BOOST;
|
|
544
514
|
}
|
|
545
515
|
filtered.sort((a, b) => b.score - a.score);
|
|
546
516
|
|
|
547
517
|
// Tier filter: exclude ephemeral entries by default (NULL tier treated as working)
|
|
548
518
|
if (!include_ephemeral) {
|
|
549
|
-
filtered = filtered.filter((r) => r.tier !==
|
|
519
|
+
filtered = filtered.filter((r) => r.tier !== 'ephemeral');
|
|
550
520
|
}
|
|
551
521
|
|
|
552
522
|
// Event category filter: exclude events from semantic search by default
|
|
553
523
|
if (shouldExcludeEvents) {
|
|
554
|
-
filtered = filtered.filter((r) => r.category !==
|
|
524
|
+
filtered = filtered.filter((r) => r.category !== 'event');
|
|
555
525
|
}
|
|
556
526
|
|
|
557
527
|
if (!filtered.length) {
|
|
@@ -560,13 +530,11 @@ export async function handler(
|
|
|
560
530
|
return ok(
|
|
561
531
|
hasQuery
|
|
562
532
|
? `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
|
|
533
|
+
: `No entries found matching the given filters in events (last ${days} days).\nTry with \`since: "YYYY-MM-DD"\` to search older events.`
|
|
564
534
|
);
|
|
565
535
|
}
|
|
566
536
|
return ok(
|
|
567
|
-
hasQuery
|
|
568
|
-
? "No results found for: " + query
|
|
569
|
-
: "No entries found matching the given filters.",
|
|
537
|
+
hasQuery ? 'No results found for: ' + query : 'No entries found matching the given filters.'
|
|
570
538
|
);
|
|
571
539
|
}
|
|
572
540
|
|
|
@@ -602,8 +570,7 @@ export async function handler(
|
|
|
602
570
|
}
|
|
603
571
|
|
|
604
572
|
// Skeleton mode: determine pivot threshold
|
|
605
|
-
const effectivePivot =
|
|
606
|
-
pivot_count != null ? pivot_count : DEFAULT_PIVOT_COUNT;
|
|
573
|
+
const effectivePivot = pivot_count != null ? pivot_count : DEFAULT_PIVOT_COUNT;
|
|
607
574
|
|
|
608
575
|
// Conflict detection
|
|
609
576
|
const conflicts = detect_conflicts ? detectConflicts(filtered, ctx) : [];
|
|
@@ -611,45 +578,43 @@ export async function handler(
|
|
|
611
578
|
const lines = [];
|
|
612
579
|
if (reindexFailed)
|
|
613
580
|
lines.push(
|
|
614
|
-
`> **Warning:** Auto-reindex failed. Results may be stale. Run \`context-vault reindex\` to fix.\n
|
|
581
|
+
`> **Warning:** Auto-reindex failed. Results may be stale. Run \`context-vault reindex\` to fix.\n`
|
|
615
582
|
);
|
|
616
583
|
if (hasQuery && isEmbedAvailable() === false)
|
|
617
584
|
lines.push(
|
|
618
|
-
`> **Note:** Semantic search unavailable — results ranked by keyword match only. Run \`context-vault setup\` to download the embedding model.\n
|
|
585
|
+
`> **Note:** Semantic search unavailable — results ranked by keyword match only. Run \`context-vault setup\` to download the embedding model.\n`
|
|
619
586
|
);
|
|
620
|
-
const heading = hasQuery ? `Results for "${query}"` :
|
|
587
|
+
const heading = hasQuery ? `Results for "${query}"` : 'Filtered entries';
|
|
621
588
|
lines.push(`## ${heading} (${filtered.length} matches)\n`);
|
|
622
589
|
if (tokensBudget != null) {
|
|
623
|
-
lines.push(
|
|
624
|
-
`> Token budget: ${tokensUsed} / ${tokensBudget} tokens used.\n`,
|
|
625
|
-
);
|
|
590
|
+
lines.push(`> Token budget: ${tokensUsed} / ${tokensBudget} tokens used.\n`);
|
|
626
591
|
}
|
|
627
592
|
if (autoWindowed) {
|
|
628
593
|
const days = config.eventDecayDays || 30;
|
|
629
594
|
lines.push(
|
|
630
|
-
`> ℹ Event search limited to last ${days} days. Use \`since\` parameter for older results.\n
|
|
595
|
+
`> ℹ Event search limited to last ${days} days. Use \`since\` parameter for older results.\n`
|
|
631
596
|
);
|
|
632
597
|
}
|
|
633
598
|
for (let i = 0; i < filtered.length; i++) {
|
|
634
599
|
const r = filtered[i];
|
|
635
600
|
const isSkeleton = i >= effectivePivot;
|
|
636
601
|
const entryTags = r.tags ? JSON.parse(r.tags) : [];
|
|
637
|
-
const tagStr = entryTags.length ? entryTags.join(
|
|
602
|
+
const tagStr = entryTags.length ? entryTags.join(', ') : 'none';
|
|
638
603
|
const relPath =
|
|
639
604
|
r.file_path && config.vaultDir
|
|
640
|
-
? r.file_path.replace(config.vaultDir +
|
|
641
|
-
: r.file_path ||
|
|
642
|
-
const skeletonLabel = isSkeleton ?
|
|
605
|
+
? r.file_path.replace(config.vaultDir + '/', '')
|
|
606
|
+
: r.file_path || 'n/a';
|
|
607
|
+
const skeletonLabel = isSkeleton ? ' ⊘ skeleton' : '';
|
|
643
608
|
lines.push(
|
|
644
|
-
`### [${i + 1}/${filtered.length}] ${r.title ||
|
|
609
|
+
`### [${i + 1}/${filtered.length}] ${r.title || '(untitled)'} [${r.kind}/${r.category}]${skeletonLabel}`
|
|
645
610
|
);
|
|
646
611
|
const dateStr =
|
|
647
612
|
r.updated_at && r.updated_at !== r.created_at
|
|
648
613
|
? `${r.created_at} (updated ${r.updated_at})`
|
|
649
|
-
: r.created_at ||
|
|
650
|
-
const tierStr = r.tier ? ` · tier: ${r.tier}` :
|
|
614
|
+
: r.created_at || '';
|
|
615
|
+
const tierStr = r.tier ? ` · tier: ${r.tier}` : '';
|
|
651
616
|
lines.push(
|
|
652
|
-
`${r.score.toFixed(3)} · ${tagStr} · ${relPath} · ${dateStr} · skeleton: ${isSkeleton}${tierStr} · id: \`${r.id}
|
|
617
|
+
`${r.score.toFixed(3)} · ${tagStr} · ${relPath} · ${dateStr} · skeleton: ${isSkeleton}${tierStr} · id: \`${r.id}\``
|
|
653
618
|
);
|
|
654
619
|
const stalenessResult = checkStaleness(r);
|
|
655
620
|
if (stalenessResult) {
|
|
@@ -660,25 +625,21 @@ export async function handler(
|
|
|
660
625
|
if (isSkeleton) {
|
|
661
626
|
lines.push(skeletonBody(r.body));
|
|
662
627
|
} else {
|
|
663
|
-
lines.push(r.body?.slice(0, 300) + (r.body?.length > 300 ?
|
|
628
|
+
lines.push(r.body?.slice(0, 300) + (r.body?.length > 300 ? '...' : ''));
|
|
664
629
|
}
|
|
665
|
-
lines.push(
|
|
630
|
+
lines.push('');
|
|
666
631
|
}
|
|
667
632
|
|
|
668
633
|
if (detect_conflicts) {
|
|
669
634
|
if (conflicts.length === 0) {
|
|
670
|
-
lines.push(
|
|
671
|
-
`## Conflict Detection\n\nNo conflicts detected among results.\n`,
|
|
672
|
-
);
|
|
635
|
+
lines.push(`## Conflict Detection\n\nNo conflicts detected among results.\n`);
|
|
673
636
|
} else {
|
|
674
637
|
lines.push(`## Conflict Detection (${conflicts.length} flagged)\n`);
|
|
675
638
|
for (const c of conflicts) {
|
|
676
|
-
lines.push(
|
|
677
|
-
`- **${c.reason}**: \`${c.entry_a_id}\` vs \`${c.entry_b_id}\``,
|
|
678
|
-
);
|
|
639
|
+
lines.push(`- **${c.reason}**: \`${c.entry_a_id}\` vs \`${c.entry_b_id}\``);
|
|
679
640
|
lines.push(` Recommendation: ${c.recommendation}`);
|
|
680
641
|
}
|
|
681
|
-
lines.push(
|
|
642
|
+
lines.push('');
|
|
682
643
|
}
|
|
683
644
|
}
|
|
684
645
|
|
|
@@ -696,21 +657,17 @@ export async function handler(
|
|
|
696
657
|
if (uniqueLinked.length > 0) {
|
|
697
658
|
lines.push(`## Linked Entries (${uniqueLinked.length} via related_to)\n`);
|
|
698
659
|
for (const r of uniqueLinked) {
|
|
699
|
-
const direction = forward.some((f) => f.id === r.id)
|
|
700
|
-
? "→ forward"
|
|
701
|
-
: "← backlink";
|
|
660
|
+
const direction = forward.some((f) => f.id === r.id) ? '→ forward' : '← backlink';
|
|
702
661
|
const entryTags = r.tags ? JSON.parse(r.tags) : [];
|
|
703
|
-
const tagStr = entryTags.length ? entryTags.join(
|
|
662
|
+
const tagStr = entryTags.length ? entryTags.join(', ') : 'none';
|
|
704
663
|
const relPath =
|
|
705
664
|
r.file_path && config.vaultDir
|
|
706
|
-
? r.file_path.replace(config.vaultDir +
|
|
707
|
-
: r.file_path ||
|
|
708
|
-
lines.push(
|
|
709
|
-
`### ${r.title || "(untitled)"} [${r.kind}/${r.category}] ${direction}`,
|
|
710
|
-
);
|
|
665
|
+
? r.file_path.replace(config.vaultDir + '/', '')
|
|
666
|
+
: r.file_path || 'n/a';
|
|
667
|
+
lines.push(`### ${r.title || '(untitled)'} [${r.kind}/${r.category}] ${direction}`);
|
|
711
668
|
lines.push(`${tagStr} · ${relPath} · id: \`${r.id}\``);
|
|
712
|
-
lines.push(r.body?.slice(0, 200) + (r.body?.length > 200 ?
|
|
713
|
-
lines.push(
|
|
669
|
+
lines.push(r.body?.slice(0, 200) + (r.body?.length > 200 ? '...' : ''));
|
|
670
|
+
lines.push('');
|
|
714
671
|
}
|
|
715
672
|
} else {
|
|
716
673
|
lines.push(`## Linked Entries\n\nNo related entries found.\n`);
|
|
@@ -719,24 +676,19 @@ export async function handler(
|
|
|
719
676
|
|
|
720
677
|
// Consolidation suggestion detection — lazy, opportunistic, vault-wide
|
|
721
678
|
const consolidationOpts = {
|
|
722
|
-
tagThreshold:
|
|
723
|
-
|
|
724
|
-
maxAgeDays:
|
|
725
|
-
config.consolidation?.maxAgeDays ?? CONSOLIDATION_SNAPSHOT_MAX_AGE_DAYS,
|
|
679
|
+
tagThreshold: config.consolidation?.tagThreshold ?? CONSOLIDATION_TAG_THRESHOLD,
|
|
680
|
+
maxAgeDays: config.consolidation?.maxAgeDays ?? CONSOLIDATION_SNAPSHOT_MAX_AGE_DAYS,
|
|
726
681
|
};
|
|
727
682
|
const consolidationSuggestions = detectConsolidationHints(
|
|
728
683
|
filtered,
|
|
729
684
|
ctx.db,
|
|
730
685
|
|
|
731
|
-
consolidationOpts
|
|
686
|
+
consolidationOpts
|
|
732
687
|
);
|
|
733
688
|
|
|
734
689
|
// Auto-consolidate: fire-and-forget create_snapshot for eligible tags
|
|
735
|
-
if (
|
|
736
|
-
|
|
737
|
-
consolidationSuggestions.length > 0
|
|
738
|
-
) {
|
|
739
|
-
const { handler: snapshotHandler } = await import("./create-snapshot.js");
|
|
690
|
+
if (config.consolidation?.autoConsolidate && consolidationSuggestions.length > 0) {
|
|
691
|
+
const { handler: snapshotHandler } = await import('./create-snapshot.js');
|
|
740
692
|
for (const suggestion of consolidationSuggestions) {
|
|
741
693
|
snapshotHandler({ topic: suggestion.tag, tags: [suggestion.tag] }, ctx, {
|
|
742
694
|
ensureIndexed: async () => {},
|
|
@@ -744,7 +696,7 @@ export async function handler(
|
|
|
744
696
|
}
|
|
745
697
|
}
|
|
746
698
|
|
|
747
|
-
const result = ok(lines.join(
|
|
699
|
+
const result = ok(lines.join('\n'));
|
|
748
700
|
const meta = {};
|
|
749
701
|
meta.scope = effectiveScope;
|
|
750
702
|
if (tokensBudget != null) {
|