context-vault 3.4.3 → 3.4.5
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/assets/agent-rules.md +50 -0
- package/assets/setup-prompt.md +58 -0
- package/assets/skills/vault-setup/skill.md +81 -0
- package/bin/cli.js +533 -11
- package/dist/helpers.d.ts +2 -0
- package/dist/helpers.d.ts.map +1 -1
- package/dist/helpers.js +23 -0
- package/dist/helpers.js.map +1 -1
- package/dist/server.js +52 -12
- package/dist/server.js.map +1 -1
- package/dist/tools/context-status.js +29 -28
- package/dist/tools/context-status.js.map +1 -1
- package/dist/tools/get-context.d.ts +2 -1
- package/dist/tools/get-context.d.ts.map +1 -1
- package/dist/tools/get-context.js +44 -20
- package/dist/tools/get-context.js.map +1 -1
- package/dist/tools/list-context.d.ts.map +1 -1
- package/dist/tools/list-context.js +8 -8
- package/dist/tools/list-context.js.map +1 -1
- package/dist/tools/save-context.d.ts +2 -1
- package/dist/tools/save-context.d.ts.map +1 -1
- package/dist/tools/save-context.js +100 -24
- package/dist/tools/save-context.js.map +1 -1
- package/dist/tools/session-start.d.ts.map +1 -1
- package/dist/tools/session-start.js +39 -5
- package/dist/tools/session-start.js.map +1 -1
- package/node_modules/@context-vault/core/dist/capture.d.ts.map +1 -1
- package/node_modules/@context-vault/core/dist/capture.js +11 -0
- package/node_modules/@context-vault/core/dist/capture.js.map +1 -1
- package/node_modules/@context-vault/core/dist/config.d.ts +8 -0
- package/node_modules/@context-vault/core/dist/config.d.ts.map +1 -1
- package/node_modules/@context-vault/core/dist/config.js +20 -1
- package/node_modules/@context-vault/core/dist/config.js.map +1 -1
- package/node_modules/@context-vault/core/dist/context.d.ts +34 -0
- package/node_modules/@context-vault/core/dist/context.d.ts.map +1 -0
- package/node_modules/@context-vault/core/dist/context.js +55 -0
- package/node_modules/@context-vault/core/dist/context.js.map +1 -0
- package/node_modules/@context-vault/core/dist/db.d.ts +3 -1
- package/node_modules/@context-vault/core/dist/db.d.ts.map +1 -1
- package/node_modules/@context-vault/core/dist/db.js +29 -2
- package/node_modules/@context-vault/core/dist/db.js.map +1 -1
- package/node_modules/@context-vault/core/dist/frontmatter.d.ts.map +1 -1
- package/node_modules/@context-vault/core/dist/frontmatter.js +2 -0
- package/node_modules/@context-vault/core/dist/frontmatter.js.map +1 -1
- package/node_modules/@context-vault/core/dist/search.d.ts +1 -0
- package/node_modules/@context-vault/core/dist/search.d.ts.map +1 -1
- package/node_modules/@context-vault/core/dist/search.js +57 -3
- package/node_modules/@context-vault/core/dist/search.js.map +1 -1
- package/node_modules/@context-vault/core/dist/types.d.ts +6 -0
- package/node_modules/@context-vault/core/dist/types.d.ts.map +1 -1
- package/node_modules/@context-vault/core/package.json +5 -1
- package/node_modules/@context-vault/core/src/capture.ts +9 -0
- package/node_modules/@context-vault/core/src/config.ts +22 -1
- package/node_modules/@context-vault/core/src/context.ts +65 -0
- package/node_modules/@context-vault/core/src/db.ts +29 -2
- package/node_modules/@context-vault/core/src/frontmatter.ts +2 -0
- package/node_modules/@context-vault/core/src/search.ts +54 -2
- package/node_modules/@context-vault/core/src/types.ts +6 -0
- package/package.json +2 -2
- package/scripts/prepack.js +17 -0
- package/src/helpers.ts +25 -0
- package/src/server.ts +57 -11
- package/src/tools/context-status.ts +30 -30
- package/src/tools/get-context.ts +48 -25
- package/src/tools/list-context.ts +8 -11
- package/src/tools/save-context.ts +101 -26
- package/src/tools/session-start.ts +36 -5
package/src/tools/get-context.ts
CHANGED
|
@@ -2,12 +2,13 @@ import { z } from 'zod';
|
|
|
2
2
|
import { createHash } from 'node:crypto';
|
|
3
3
|
import { readFileSync, existsSync } from 'node:fs';
|
|
4
4
|
import { resolve } from 'node:path';
|
|
5
|
-
import { hybridSearch } from '@context-vault/core/search';
|
|
5
|
+
import { hybridSearch, trackAccess } from '@context-vault/core/search';
|
|
6
6
|
import { categoryFor } from '@context-vault/core/categories';
|
|
7
7
|
import { normalizeKind } from '@context-vault/core/files';
|
|
8
|
+
import { parseContextParam } from '@context-vault/core/context';
|
|
8
9
|
import { resolveTemporalParams } from '../temporal.js';
|
|
9
10
|
import { collectLinkedEntries } from '../linking.js';
|
|
10
|
-
import { ok, err, errWithHint } from '../helpers.js';
|
|
11
|
+
import { ok, err, errWithHint, kindIcon, fmtDate } from '../helpers.js';
|
|
11
12
|
import { isEmbedAvailable } from '@context-vault/core/embed';
|
|
12
13
|
import type { LocalCtx, SharedCtx, ToolResult } from '../types.js';
|
|
13
14
|
|
|
@@ -346,6 +347,12 @@ export const inputSchema = {
|
|
|
346
347
|
.describe(
|
|
347
348
|
'When true with identity_key, return a clear "not found" instead of falling through to semantic search on miss. Default: false.'
|
|
348
349
|
),
|
|
350
|
+
context: z
|
|
351
|
+
.any()
|
|
352
|
+
.optional()
|
|
353
|
+
.describe(
|
|
354
|
+
'Current context for contextual reinstatement. Boosts entries that were saved in a similar context. Pass a structured object (e.g. { project: "myapp", arc: "auth-rewrite", task: "debugging token expiry" }) or a free-text string. Entries saved with matching encoding_context will rank higher.'
|
|
355
|
+
),
|
|
349
356
|
};
|
|
350
357
|
|
|
351
358
|
export async function handler(
|
|
@@ -369,6 +376,7 @@ export async function handler(
|
|
|
369
376
|
follow_links,
|
|
370
377
|
body_limit,
|
|
371
378
|
strict,
|
|
379
|
+
context,
|
|
372
380
|
}: Record<string, any>,
|
|
373
381
|
ctx: LocalCtx,
|
|
374
382
|
{ ensureIndexed, reindexFailed }: SharedCtx
|
|
@@ -413,6 +421,7 @@ export async function handler(
|
|
|
413
421
|
if (!kindFilter) return err('identity_key requires kind to be specified', 'INVALID_INPUT');
|
|
414
422
|
const match = ctx.stmts.getByIdentityKey.get(kindFilter, identity_key) as any;
|
|
415
423
|
if (match) {
|
|
424
|
+
trackAccess(ctx, [{ ...match, score: 1 }]);
|
|
416
425
|
const entryTags = match.tags ? JSON.parse(match.tags) : [];
|
|
417
426
|
const tagStr = entryTags.length ? entryTags.join(', ') : 'none';
|
|
418
427
|
const relPath =
|
|
@@ -452,6 +461,17 @@ export async function handler(
|
|
|
452
461
|
? Math.min(effectiveLimit * 10, MAX_FETCH_LIMIT)
|
|
453
462
|
: effectiveLimit;
|
|
454
463
|
|
|
464
|
+
// Generate context embedding for contextual reinstatement boosting
|
|
465
|
+
let contextEmbedding: Float32Array | null = null;
|
|
466
|
+
const parsedCtx = parseContextParam(context);
|
|
467
|
+
if (parsedCtx?.text) {
|
|
468
|
+
try {
|
|
469
|
+
contextEmbedding = await ctx.embed(parsedCtx.text);
|
|
470
|
+
} catch {
|
|
471
|
+
// Non-fatal: proceed without context boosting
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
455
475
|
let filtered: any[];
|
|
456
476
|
if (hasQuery) {
|
|
457
477
|
// Hybrid search mode
|
|
@@ -465,6 +485,7 @@ export async function handler(
|
|
|
465
485
|
decayDays: config.eventDecayDays || 30,
|
|
466
486
|
includeSuperseeded: include_superseded ?? false,
|
|
467
487
|
includeEphemeral: include_ephemeral ?? false,
|
|
488
|
+
contextEmbedding,
|
|
468
489
|
});
|
|
469
490
|
|
|
470
491
|
// Post-filter by tags if provided, then apply requested limit
|
|
@@ -532,6 +553,9 @@ export async function handler(
|
|
|
532
553
|
|
|
533
554
|
// Add score field for consistent output
|
|
534
555
|
for (const r of filtered) r.score = 0;
|
|
556
|
+
|
|
557
|
+
// Track access for filter-only results (hybrid search tracks its own)
|
|
558
|
+
trackAccess(ctx, filtered);
|
|
535
559
|
}
|
|
536
560
|
|
|
537
561
|
// Brief score boost: briefs rank slightly higher so consolidated snapshots
|
|
@@ -609,23 +633,24 @@ export async function handler(
|
|
|
609
633
|
const r = filtered[i];
|
|
610
634
|
const isSkeleton = i >= effectivePivot;
|
|
611
635
|
const entryTags = r.tags ? JSON.parse(r.tags) : [];
|
|
612
|
-
const tagStr = entryTags.length ? entryTags.join(', ') : '
|
|
613
|
-
const
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
`### [${i + 1}/${filtered.length}] ${r.title || '(untitled)'} [${r.kind}/${r.category}]${skeletonLabel}`
|
|
620
|
-
);
|
|
621
|
-
const dateStr =
|
|
622
|
-
r.updated_at && r.updated_at !== r.created_at
|
|
623
|
-
? `${r.created_at} (updated ${r.updated_at})`
|
|
624
|
-
: r.created_at || '';
|
|
625
|
-
const tierStr = r.tier ? ` · tier: ${r.tier}` : '';
|
|
636
|
+
const tagStr = entryTags.length ? entryTags.join(', ') : '';
|
|
637
|
+
const icon = kindIcon(r.kind);
|
|
638
|
+
const skeletonLabel = isSkeleton ? ' `skeleton`' : '';
|
|
639
|
+
const tierLabel = r.tier ? `**${r.tier}**` : '';
|
|
640
|
+
const dateStr = r.updated_at && r.updated_at !== r.created_at
|
|
641
|
+
? `${fmtDate(r.created_at)} (upd ${fmtDate(r.updated_at)})`
|
|
642
|
+
: fmtDate(r.created_at);
|
|
626
643
|
lines.push(
|
|
627
|
-
|
|
644
|
+
`### ${icon} ${r.title || '(untitled)'}${skeletonLabel}`
|
|
628
645
|
);
|
|
646
|
+
const meta = [
|
|
647
|
+
`\`${r.score.toFixed(2)}\``,
|
|
648
|
+
`\`${r.kind}\``,
|
|
649
|
+
tierLabel,
|
|
650
|
+
tagStr,
|
|
651
|
+
dateStr,
|
|
652
|
+
].filter(Boolean).join(' · ');
|
|
653
|
+
lines.push(`${meta} \nid: \`${r.id}\``);
|
|
629
654
|
const stalenessResult = checkStaleness(r);
|
|
630
655
|
if (stalenessResult) {
|
|
631
656
|
r.stale = true;
|
|
@@ -667,15 +692,13 @@ export async function handler(
|
|
|
667
692
|
if (uniqueLinked.length > 0) {
|
|
668
693
|
lines.push(`## Linked Entries (${uniqueLinked.length} via related_to)\n`);
|
|
669
694
|
for (const r of uniqueLinked) {
|
|
670
|
-
const direction = forward.some((f: any) => f.id === r.id) ? '→
|
|
695
|
+
const direction = forward.some((f: any) => f.id === r.id) ? '→' : '←';
|
|
671
696
|
const entryTags = r.tags ? JSON.parse(r.tags) : [];
|
|
672
|
-
const tagStr = entryTags.length ? entryTags.join(', ') : '
|
|
673
|
-
const
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
lines.push(`### ${r.title || '(untitled)'} [${r.kind}/${r.category}] ${direction}`);
|
|
678
|
-
lines.push(`${tagStr} · ${relPath} · id: \`${r.id}\``);
|
|
697
|
+
const tagStr = entryTags.length ? entryTags.join(', ') : '';
|
|
698
|
+
const icon = kindIcon(r.kind);
|
|
699
|
+
lines.push(`### ${icon} ${r.title || '(untitled)'} ${direction}`);
|
|
700
|
+
const meta = [`\`${r.kind}\``, tagStr].filter(Boolean).join(' · ');
|
|
701
|
+
lines.push(`${meta} \nid: \`${r.id}\``);
|
|
679
702
|
lines.push(truncateBody(r.body, body_limit ?? 300));
|
|
680
703
|
lines.push('');
|
|
681
704
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { normalizeKind } from '@context-vault/core/files';
|
|
3
3
|
import { categoryFor } from '@context-vault/core/categories';
|
|
4
|
-
import { ok, err, errWithHint } from '../helpers.js';
|
|
4
|
+
import { ok, err, errWithHint, kindIcon, fmtDate } from '../helpers.js';
|
|
5
5
|
import { resolveTemporalParams } from '../temporal.js';
|
|
6
6
|
import type { LocalCtx, SharedCtx, ToolResult } from '../types.js';
|
|
7
7
|
|
|
@@ -124,18 +124,15 @@ export async function handler(
|
|
|
124
124
|
`> ℹ Event search limited to last ${days} days. Use \`since\` parameter for older results.\n`
|
|
125
125
|
);
|
|
126
126
|
}
|
|
127
|
+
lines.push('| | Title | Kind | Tags | Date | ID |');
|
|
128
|
+
lines.push('|---|---|---|---|---|---|');
|
|
127
129
|
for (const r of filtered) {
|
|
128
130
|
const entryTags = r.tags ? JSON.parse(r.tags) : [];
|
|
129
|
-
const tagStr = entryTags.length ? entryTags.join(', ') : '
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
lines.push(
|
|
135
|
-
`- **${r.title || '(untitled)'}** [${r.kind}/${r.category}] — ${tagStr} — ${dateStr} — \`${r.id}\``
|
|
136
|
-
);
|
|
137
|
-
if (r.preview)
|
|
138
|
-
lines.push(` ${r.preview.replace(/\n+/g, ' ').trim()}${r.preview.length >= 120 ? '…' : ''}`);
|
|
131
|
+
const tagStr = entryTags.length ? entryTags.join(', ') : '';
|
|
132
|
+
const date = fmtDate(r.updated_at && r.updated_at !== r.created_at ? r.updated_at : r.created_at);
|
|
133
|
+
const icon = kindIcon(r.kind);
|
|
134
|
+
const title = (r.title || '(untitled)').replace(/\|/g, '\\|');
|
|
135
|
+
lines.push(`| ${icon} | **${title}** | \`${r.kind}\` | ${tagStr} | ${date} | \`${r.id}\` |`);
|
|
139
136
|
}
|
|
140
137
|
|
|
141
138
|
if (effectiveOffset + effectiveLimit < total) {
|
|
@@ -3,7 +3,8 @@ import { captureAndIndex, updateEntryFile } from '@context-vault/core/capture';
|
|
|
3
3
|
import { indexEntry } from '@context-vault/core/index';
|
|
4
4
|
import { categoryFor, defaultTierFor } from '@context-vault/core/categories';
|
|
5
5
|
import { normalizeKind } from '@context-vault/core/files';
|
|
6
|
-
import {
|
|
6
|
+
import { parseContextParam } from '@context-vault/core/context';
|
|
7
|
+
import { ok, err, errWithHint, ensureVaultExists, ensureValidKind, kindIcon } from '../helpers.js';
|
|
7
8
|
import { maybeShowFeedbackPrompt } from '../telemetry.js';
|
|
8
9
|
import { validateRelatedTo } from '../linking.js';
|
|
9
10
|
import type { LocalCtx, SharedCtx, ToolResult } from '../types.js';
|
|
@@ -81,13 +82,13 @@ async function findSimilar(
|
|
|
81
82
|
}
|
|
82
83
|
|
|
83
84
|
function formatSimilarWarning(similar: any[]): string {
|
|
84
|
-
const lines = ['', '⚠ Similar entries
|
|
85
|
+
const lines = ['', '### ⚠ Similar entries'];
|
|
85
86
|
for (const e of similar) {
|
|
86
|
-
const score = e.score.toFixed(
|
|
87
|
-
const title = e.title ?
|
|
88
|
-
lines.push(
|
|
87
|
+
const score = (e.score * 100).toFixed(0);
|
|
88
|
+
const title = e.title ? `**${e.title}**` : '(no title)';
|
|
89
|
+
lines.push(`- ${title} \`${score}%\` · \`${e.id}\``);
|
|
89
90
|
}
|
|
90
|
-
lines.push('
|
|
91
|
+
lines.push('_Use `id` param to update instead of creating a duplicate._');
|
|
91
92
|
return lines.join('\n');
|
|
92
93
|
}
|
|
93
94
|
|
|
@@ -141,13 +142,12 @@ export function buildConflictCandidates(similarEntries: any[]): any[] {
|
|
|
141
142
|
}
|
|
142
143
|
|
|
143
144
|
function formatConflictSuggestions(candidates: any[]): string {
|
|
144
|
-
const lines = ['', '
|
|
145
|
+
const lines = ['', '### Conflict Resolution'];
|
|
145
146
|
for (const c of candidates) {
|
|
146
|
-
const titleDisplay = c.title ?
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
);
|
|
150
|
-
lines.push(` ${c.reasoning_context}`);
|
|
147
|
+
const titleDisplay = c.title ? `**${c.title}**` : '(no title)';
|
|
148
|
+
const actionIcon = c.suggested_action === 'SKIP' ? '⊘' : c.suggested_action === 'UPDATE' ? '↻' : '+';
|
|
149
|
+
lines.push(`${actionIcon} **${c.suggested_action}** ${titleDisplay} \`${(c.score * 100).toFixed(0)}%\` · \`${c.id}\``);
|
|
150
|
+
lines.push(` ${c.reasoning_context}`);
|
|
151
151
|
}
|
|
152
152
|
return lines.join('\n');
|
|
153
153
|
}
|
|
@@ -310,6 +310,12 @@ export const inputSchema = {
|
|
|
310
310
|
.describe(
|
|
311
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).'
|
|
312
312
|
),
|
|
313
|
+
encoding_context: z
|
|
314
|
+
.any()
|
|
315
|
+
.optional()
|
|
316
|
+
.describe(
|
|
317
|
+
'Encoding context for contextual reinstatement. Captures the situation when this entry was created, enabling context-aware retrieval boosting. Pass a structured object (e.g. { project: "myapp", arc: "auth-rewrite", task: "implementing JWT" }) or a free-text string describing the current context.'
|
|
318
|
+
),
|
|
313
319
|
};
|
|
314
320
|
|
|
315
321
|
export async function handler(
|
|
@@ -331,6 +337,7 @@ export async function handler(
|
|
|
331
337
|
similarity_threshold,
|
|
332
338
|
tier,
|
|
333
339
|
conflict_resolution,
|
|
340
|
+
encoding_context,
|
|
334
341
|
}: Record<string, any>,
|
|
335
342
|
ctx: LocalCtx,
|
|
336
343
|
{ ensureIndexed }: SharedCtx
|
|
@@ -388,13 +395,21 @@ export async function handler(
|
|
|
388
395
|
);
|
|
389
396
|
}
|
|
390
397
|
|
|
398
|
+
// Merge encoding context into meta for update path
|
|
399
|
+
const updateParsedCtx = parseContextParam(encoding_context);
|
|
400
|
+
let updateMeta = meta;
|
|
401
|
+
if (updateParsedCtx) {
|
|
402
|
+
updateMeta = { ...(meta || {}) };
|
|
403
|
+
updateMeta.encoding_context = updateParsedCtx.structured || updateParsedCtx.text;
|
|
404
|
+
}
|
|
405
|
+
|
|
391
406
|
let entry;
|
|
392
407
|
try {
|
|
393
408
|
entry = updateEntryFile(ctx, existing, {
|
|
394
409
|
title,
|
|
395
410
|
body,
|
|
396
411
|
tags,
|
|
397
|
-
meta,
|
|
412
|
+
meta: updateMeta,
|
|
398
413
|
source,
|
|
399
414
|
expires_at,
|
|
400
415
|
supersedes,
|
|
@@ -409,6 +424,24 @@ export async function handler(
|
|
|
409
424
|
'context-vault save_context update is failing. Check `cat ~/.context-mcp/error.log | tail -5` and help me debug.'
|
|
410
425
|
);
|
|
411
426
|
}
|
|
427
|
+
|
|
428
|
+
// Store context embedding for updated entry
|
|
429
|
+
if (updateParsedCtx?.text) {
|
|
430
|
+
try {
|
|
431
|
+
const ctxEmbed = await ctx.embed(updateParsedCtx.text);
|
|
432
|
+
if (ctxEmbed) {
|
|
433
|
+
const rowidResult = ctx.stmts.getRowid.get(entry.id) as { rowid: number } | undefined;
|
|
434
|
+
if (rowidResult?.rowid) {
|
|
435
|
+
const rowid = Number(rowidResult.rowid);
|
|
436
|
+
try { ctx.deleteCtxVec(rowid); } catch {}
|
|
437
|
+
ctx.insertCtxVec(rowid, ctxEmbed);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
} catch (e) {
|
|
441
|
+
console.warn(`[context-vault] Context embedding update failed: ${(e as Error).message}`);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
412
445
|
if (entry.related_to?.length && ctx.stmts.updateRelatedTo) {
|
|
413
446
|
ctx.stmts.updateRelatedTo.run(JSON.stringify(entry.related_to), entry.id);
|
|
414
447
|
} else if (entry.related_to === null && ctx.stmts.updateRelatedTo) {
|
|
@@ -502,6 +535,15 @@ export async function handler(
|
|
|
502
535
|
|
|
503
536
|
const mergedMeta = { ...(meta || {}) };
|
|
504
537
|
if (folder) mergedMeta.folder = folder;
|
|
538
|
+
|
|
539
|
+
// Merge encoding context into meta for persistence
|
|
540
|
+
const parsedCtx = parseContextParam(encoding_context);
|
|
541
|
+
if (parsedCtx?.structured) {
|
|
542
|
+
mergedMeta.encoding_context = parsedCtx.structured;
|
|
543
|
+
} else if (parsedCtx?.text) {
|
|
544
|
+
mergedMeta.encoding_context = parsedCtx.text;
|
|
545
|
+
}
|
|
546
|
+
|
|
505
547
|
const finalMeta = Object.keys(mergedMeta).length ? mergedMeta : undefined;
|
|
506
548
|
|
|
507
549
|
const effectiveTier = tier ?? defaultTierFor(normalizedKind);
|
|
@@ -538,6 +580,24 @@ export async function handler(
|
|
|
538
580
|
);
|
|
539
581
|
}
|
|
540
582
|
|
|
583
|
+
// Store context embedding in vault_ctx_vec for contextual reinstatement
|
|
584
|
+
if (parsedCtx?.text && entry) {
|
|
585
|
+
try {
|
|
586
|
+
const ctxEmbedding = await ctx.embed(parsedCtx.text);
|
|
587
|
+
if (ctxEmbedding) {
|
|
588
|
+
const rowidResult = ctx.stmts.getRowid.get(entry.id) as { rowid: number } | undefined;
|
|
589
|
+
if (rowidResult?.rowid) {
|
|
590
|
+
const rowid = Number(rowidResult.rowid);
|
|
591
|
+
try { ctx.deleteCtxVec(rowid); } catch {}
|
|
592
|
+
ctx.insertCtxVec(rowid, ctxEmbedding);
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
} catch (e) {
|
|
596
|
+
// Non-fatal: context embedding failure should not block the save
|
|
597
|
+
console.warn(`[context-vault] Context embedding failed: ${(e as Error).message}`);
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
|
|
541
601
|
if (ctx.config?.dataDir) {
|
|
542
602
|
maybeShowFeedbackPrompt(ctx.config.dataDir);
|
|
543
603
|
}
|
|
@@ -545,11 +605,19 @@ export async function handler(
|
|
|
545
605
|
const relPath = entry.filePath
|
|
546
606
|
? entry.filePath.replace(config.vaultDir + '/', '')
|
|
547
607
|
: entry.filePath;
|
|
548
|
-
const
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
608
|
+
const icon = kindIcon(normalizedKind);
|
|
609
|
+
const parts = [
|
|
610
|
+
`## ✓ Saved`,
|
|
611
|
+
`${icon} **${title || '(untitled)'}**`,
|
|
612
|
+
`\`${normalizedKind}\` · **${effectiveTier}**${tags?.length ? ` · ${tags.join(', ')}` : ''}`,
|
|
613
|
+
`\`${entry.id}\` → ${relPath}`,
|
|
614
|
+
];
|
|
615
|
+
if (effectiveTier === 'ephemeral') {
|
|
616
|
+
parts.push(
|
|
617
|
+
'',
|
|
618
|
+
'_Note: ephemeral entries are excluded from default search. Use `include_ephemeral: true` in get_context to find them._'
|
|
619
|
+
);
|
|
620
|
+
}
|
|
553
621
|
const hasBucketTag = (tags || []).some(
|
|
554
622
|
(t: any) => typeof t === 'string' && t.startsWith('bucket:')
|
|
555
623
|
);
|
|
@@ -563,18 +631,25 @@ export async function handler(
|
|
|
563
631
|
(t: any) => typeof t === 'string' && t.startsWith('bucket:')
|
|
564
632
|
);
|
|
565
633
|
for (const bt of bucketTags) {
|
|
566
|
-
const bucketUserClause = '';
|
|
567
|
-
const bucketParams = false ? [bt] : [bt];
|
|
568
634
|
const exists = ctx.db
|
|
569
635
|
.prepare(
|
|
570
|
-
`SELECT 1 FROM vault WHERE kind = 'bucket' AND identity_key = ?
|
|
636
|
+
`SELECT 1 FROM vault WHERE kind = 'bucket' AND identity_key = ? LIMIT 1`
|
|
571
637
|
)
|
|
572
|
-
.get(
|
|
638
|
+
.get(bt);
|
|
573
639
|
if (!exists) {
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
640
|
+
// Auto-register the bucket silently
|
|
641
|
+
const bucketName = bt.replace(/^bucket:/, '');
|
|
642
|
+
try {
|
|
643
|
+
await captureAndIndex(ctx, {
|
|
644
|
+
kind: 'bucket',
|
|
645
|
+
title: bucketName,
|
|
646
|
+
body: `Bucket for project: ${bucketName}`,
|
|
647
|
+
tags: [bt],
|
|
648
|
+
identity_key: bt,
|
|
649
|
+
});
|
|
650
|
+
} catch {
|
|
651
|
+
// Non-fatal: bucket registration failure should not block the save
|
|
652
|
+
}
|
|
578
653
|
}
|
|
579
654
|
}
|
|
580
655
|
if (similarEntries.length) {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { execSync } from 'node:child_process';
|
|
3
|
-
import {
|
|
3
|
+
import { readdirSync } from 'node:fs';
|
|
4
|
+
import { ok, err, ensureVaultExists, kindIcon, fmtDate } from '../helpers.js';
|
|
4
5
|
import type { LocalCtx, SharedCtx, ToolResult } from '../types.js';
|
|
5
6
|
|
|
6
7
|
const DEFAULT_MAX_TOKENS = 4000;
|
|
@@ -67,11 +68,13 @@ function estimateTokens(text: string | null | undefined): number {
|
|
|
67
68
|
|
|
68
69
|
function formatEntry(entry: any): string {
|
|
69
70
|
const tags = entry.tags ? JSON.parse(entry.tags) : [];
|
|
70
|
-
const tagStr = tags.length ? tags.join(', ') : '
|
|
71
|
-
const date = entry.updated_at || entry.created_at
|
|
71
|
+
const tagStr = tags.length ? tags.join(', ') : '';
|
|
72
|
+
const date = fmtDate(entry.updated_at || entry.created_at);
|
|
73
|
+
const icon = kindIcon(entry.kind);
|
|
74
|
+
const meta = [`\`${entry.kind}\``, tagStr, date].filter(Boolean).join(' · ');
|
|
72
75
|
return [
|
|
73
|
-
`- **${entry.title || '(untitled)'}
|
|
74
|
-
`
|
|
76
|
+
`- ${icon} **${entry.title || '(untitled)'}**`,
|
|
77
|
+
` ${meta} · \`${entry.id}\``,
|
|
75
78
|
` ${truncateBody(entry.body).replace(/\n+/g, ' ').trim()}`,
|
|
76
79
|
].join('\n');
|
|
77
80
|
}
|
|
@@ -88,6 +91,30 @@ export async function handler(
|
|
|
88
91
|
|
|
89
92
|
await ensureIndexed();
|
|
90
93
|
|
|
94
|
+
// Sanity check: compare DB entries vs disk files
|
|
95
|
+
let indexWarning = '';
|
|
96
|
+
try {
|
|
97
|
+
const dbCount = (ctx.db.prepare('SELECT COUNT(*) as cnt FROM vault').get() as any)?.cnt ?? 0;
|
|
98
|
+
let diskCount = 0;
|
|
99
|
+
const walk = (dir: string, depth = 0) => {
|
|
100
|
+
if (depth > 3 || diskCount >= 100) return;
|
|
101
|
+
try {
|
|
102
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
103
|
+
if (diskCount >= 100) return;
|
|
104
|
+
if (entry.isDirectory() && !entry.name.startsWith('.') && entry.name !== '_archive') {
|
|
105
|
+
walk(`${dir}/${entry.name}`, depth + 1);
|
|
106
|
+
} else if (entry.name.endsWith('.md')) {
|
|
107
|
+
diskCount++;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
} catch {}
|
|
111
|
+
};
|
|
112
|
+
walk(config.vaultDir);
|
|
113
|
+
if (diskCount >= 100 && dbCount < diskCount / 10) {
|
|
114
|
+
indexWarning = `\n> **WARNING:** Vault has significantly more files on disk (~${diskCount}+) than indexed entries (${dbCount}). The search index may be out of sync. Run \`context-vault reconnect\` to fix.\n`;
|
|
115
|
+
}
|
|
116
|
+
} catch {}
|
|
117
|
+
|
|
91
118
|
const effectiveProject = project?.trim() || detectProject();
|
|
92
119
|
const tokenBudget = max_tokens || DEFAULT_MAX_TOKENS;
|
|
93
120
|
|
|
@@ -175,6 +202,10 @@ export async function handler(
|
|
|
175
202
|
return true;
|
|
176
203
|
}).length;
|
|
177
204
|
|
|
205
|
+
if (indexWarning) {
|
|
206
|
+
sections.push(indexWarning);
|
|
207
|
+
}
|
|
208
|
+
|
|
178
209
|
sections.push('---');
|
|
179
210
|
sections.push(
|
|
180
211
|
`_${tokensUsed} / ${tokenBudget} tokens used | project: ${effectiveProject || 'unscoped'}_`
|