context-vault 3.4.4 → 3.5.0
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 +753 -126
- 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 +26 -2
- package/dist/server.js.map +1 -1
- package/dist/status.d.ts.map +1 -1
- package/dist/status.js +29 -0
- package/dist/status.js.map +1 -1
- package/dist/tools/context-status.d.ts.map +1 -1
- package/dist/tools/context-status.js +64 -29
- package/dist/tools/context-status.js.map +1 -1
- package/dist/tools/get-context.js +23 -18
- package/dist/tools/get-context.js.map +1 -1
- package/dist/tools/list-context.d.ts +2 -1
- package/dist/tools/list-context.d.ts.map +1 -1
- package/dist/tools/list-context.js +27 -10
- 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 +95 -26
- 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 +230 -11
- 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 +13 -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 +47 -2
- package/node_modules/@context-vault/core/dist/config.js.map +1 -1
- package/node_modules/@context-vault/core/dist/constants.d.ts +13 -0
- package/node_modules/@context-vault/core/dist/constants.d.ts.map +1 -1
- package/node_modules/@context-vault/core/dist/constants.js +13 -0
- package/node_modules/@context-vault/core/dist/constants.js.map +1 -1
- package/node_modules/@context-vault/core/dist/db.d.ts +1 -1
- package/node_modules/@context-vault/core/dist/db.d.ts.map +1 -1
- package/node_modules/@context-vault/core/dist/db.js +73 -9
- 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/index.d.ts +4 -1
- package/node_modules/@context-vault/core/dist/index.d.ts.map +1 -1
- package/node_modules/@context-vault/core/dist/index.js +58 -10
- package/node_modules/@context-vault/core/dist/index.js.map +1 -1
- package/node_modules/@context-vault/core/dist/indexing.d.ts +8 -0
- package/node_modules/@context-vault/core/dist/indexing.d.ts.map +1 -0
- package/node_modules/@context-vault/core/dist/indexing.js +22 -0
- package/node_modules/@context-vault/core/dist/indexing.js.map +1 -0
- package/node_modules/@context-vault/core/dist/main.d.ts +3 -2
- package/node_modules/@context-vault/core/dist/main.d.ts.map +1 -1
- package/node_modules/@context-vault/core/dist/main.js +3 -1
- package/node_modules/@context-vault/core/dist/main.js.map +1 -1
- package/node_modules/@context-vault/core/dist/search.d.ts +2 -0
- package/node_modules/@context-vault/core/dist/search.d.ts.map +1 -1
- package/node_modules/@context-vault/core/dist/search.js +82 -6
- package/node_modules/@context-vault/core/dist/search.js.map +1 -1
- package/node_modules/@context-vault/core/dist/types.d.ts +24 -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 +11 -0
- package/node_modules/@context-vault/core/src/config.ts +40 -2
- package/node_modules/@context-vault/core/src/constants.ts +15 -0
- package/node_modules/@context-vault/core/src/db.ts +73 -9
- package/node_modules/@context-vault/core/src/frontmatter.ts +2 -0
- package/node_modules/@context-vault/core/src/index.ts +65 -11
- package/node_modules/@context-vault/core/src/indexing.ts +35 -0
- package/node_modules/@context-vault/core/src/main.ts +5 -0
- package/node_modules/@context-vault/core/src/search.ts +96 -6
- package/node_modules/@context-vault/core/src/types.ts +26 -0
- package/package.json +2 -2
- package/scripts/prepack.js +17 -0
- package/src/helpers.ts +25 -0
- package/src/server.ts +28 -2
- package/src/status.ts +35 -0
- package/src/tools/context-status.ts +65 -30
- package/src/tools/get-context.ts +24 -24
- package/src/tools/list-context.ts +25 -13
- package/src/tools/save-context.ts +106 -29
- package/src/tools/session-start.ts +257 -13
package/src/helpers.ts
CHANGED
|
@@ -58,4 +58,29 @@ export function ensureValidKind(kind: string): ToolResult | null {
|
|
|
58
58
|
return null;
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
const KIND_ICONS: Record<string, string> = {
|
|
62
|
+
insight: '◆',
|
|
63
|
+
decision: '◇',
|
|
64
|
+
pattern: '◈',
|
|
65
|
+
reference: '▸',
|
|
66
|
+
event: '○',
|
|
67
|
+
session: '◎',
|
|
68
|
+
brief: '▪',
|
|
69
|
+
bucket: '▫',
|
|
70
|
+
contact: '●',
|
|
71
|
+
project: '■',
|
|
72
|
+
tool: '▹',
|
|
73
|
+
source: '►',
|
|
74
|
+
feedback: '◉',
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
export function kindIcon(kind: string): string {
|
|
78
|
+
return KIND_ICONS[kind] || '·';
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function fmtDate(date: string | null | undefined): string {
|
|
82
|
+
if (!date) return '';
|
|
83
|
+
return date.split('T')[0];
|
|
84
|
+
}
|
|
85
|
+
|
|
61
86
|
export { pkg };
|
package/src/server.ts
CHANGED
|
@@ -7,9 +7,9 @@ import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/
|
|
|
7
7
|
import { isInitializeRequest } from '@modelcontextprotocol/sdk/types.js';
|
|
8
8
|
import { createMcpExpressApp } from '@modelcontextprotocol/sdk/server/express.js';
|
|
9
9
|
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
10
|
-
import { existsSync, mkdirSync, writeFileSync, readFileSync, unlinkSync } from 'node:fs';
|
|
10
|
+
import { existsSync, mkdirSync, writeFileSync, readFileSync, unlinkSync, readdirSync } from 'node:fs';
|
|
11
11
|
import { join, dirname } from 'node:path';
|
|
12
|
-
import { homedir } from 'node:os';
|
|
12
|
+
import { homedir, tmpdir } from 'node:os';
|
|
13
13
|
import { fileURLToPath } from 'node:url';
|
|
14
14
|
|
|
15
15
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
@@ -31,6 +31,7 @@ import {
|
|
|
31
31
|
} from '@context-vault/core/db';
|
|
32
32
|
import { registerTools } from './register-tools.js';
|
|
33
33
|
import { pruneExpired } from '@context-vault/core/index';
|
|
34
|
+
import { setSessionId } from '@context-vault/core/search';
|
|
34
35
|
|
|
35
36
|
const DAEMON_PORT = 3377;
|
|
36
37
|
const PID_PATH = join(homedir(), '.context-mcp', 'daemon.pid');
|
|
@@ -312,6 +313,29 @@ async function main(): Promise<void> {
|
|
|
312
313
|
|
|
313
314
|
config.vaultDirExists = existsSync(config.vaultDir);
|
|
314
315
|
|
|
316
|
+
// Validate vaultDir sanity
|
|
317
|
+
const osTmp = tmpdir();
|
|
318
|
+
const isTempPath = config.vaultDir.startsWith(osTmp) ||
|
|
319
|
+
config.vaultDir.startsWith('/tmp/') ||
|
|
320
|
+
config.vaultDir.startsWith('/var/folders/');
|
|
321
|
+
if (isTempPath) {
|
|
322
|
+
console.error(`[context-vault] WARNING: vaultDir points to a temp directory: ${config.vaultDir}`);
|
|
323
|
+
console.error(`[context-vault] This is likely from a test run that overwrote ~/.context-mcp/config.json`);
|
|
324
|
+
console.error(`[context-vault] Fix: run 'context-vault reconnect' or 'context-vault setup'`);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
if (config.vaultDirExists) {
|
|
328
|
+
try {
|
|
329
|
+
const entries = readdirSync(config.vaultDir);
|
|
330
|
+
const hasMdFiles = entries.some(f => f.endsWith('.md'));
|
|
331
|
+
const hasMarker = entries.includes('.context-mcp');
|
|
332
|
+
if (!hasMdFiles && hasMarker) {
|
|
333
|
+
console.error(`[context-vault] WARNING: vaultDir has no markdown files but has a marker file`);
|
|
334
|
+
console.error(`[context-vault] The vault may be misconfigured. Run 'context-vault reconnect'`);
|
|
335
|
+
}
|
|
336
|
+
} catch {}
|
|
337
|
+
}
|
|
338
|
+
|
|
315
339
|
console.error(`[context-vault] Vault: ${config.vaultDir}`);
|
|
316
340
|
console.error(`[context-vault] Database: ${config.dbPath}`);
|
|
317
341
|
console.error(`[context-vault] Dev dir: ${config.devDir}`);
|
|
@@ -336,6 +360,8 @@ async function main(): Promise<void> {
|
|
|
336
360
|
toolStats: { ok: 0, errors: 0, lastError: null },
|
|
337
361
|
};
|
|
338
362
|
|
|
363
|
+
setSessionId(randomUUID());
|
|
364
|
+
|
|
339
365
|
try {
|
|
340
366
|
const pruned = await pruneExpired(ctx);
|
|
341
367
|
if (pruned > 0) {
|
package/src/status.ts
CHANGED
|
@@ -148,6 +148,20 @@ export function gatherVaultStatus(ctx: LocalCtx, opts: Record<string, unknown> =
|
|
|
148
148
|
errors.push(`Archived count failed: ${(e as Error).message}`);
|
|
149
149
|
}
|
|
150
150
|
|
|
151
|
+
let indexingStats: { indexed: number; unindexed: number; total: number; byKind: Array<{ kind: string; indexed: number; total: number }> } | null = null;
|
|
152
|
+
try {
|
|
153
|
+
const totalRow = db.prepare('SELECT COUNT(*) as c FROM vault').get() as { c: number } | undefined;
|
|
154
|
+
const indexedRow = db.prepare('SELECT COUNT(*) as c FROM vault WHERE indexed = 1').get() as { c: number } | undefined;
|
|
155
|
+
const total = totalRow?.c ?? 0;
|
|
156
|
+
const indexed = indexedRow?.c ?? 0;
|
|
157
|
+
const byKind = db.prepare(
|
|
158
|
+
'SELECT kind, COUNT(*) as total, SUM(CASE WHEN indexed = 1 THEN 1 ELSE 0 END) as indexed FROM vault GROUP BY kind'
|
|
159
|
+
).all() as Array<{ kind: string; total: number; indexed: number }>;
|
|
160
|
+
indexingStats = { indexed, unindexed: total - indexed, total, byKind };
|
|
161
|
+
} catch (e) {
|
|
162
|
+
errors.push(`Indexing stats failed: ${(e as Error).message}`);
|
|
163
|
+
}
|
|
164
|
+
|
|
151
165
|
let staleKnowledge: unknown[] = [];
|
|
152
166
|
try {
|
|
153
167
|
const stalenessKinds = Object.entries(KIND_STALENESS_DAYS);
|
|
@@ -168,6 +182,25 @@ export function gatherVaultStatus(ctx: LocalCtx, opts: Record<string, unknown> =
|
|
|
168
182
|
errors.push(`Stale knowledge check failed: ${(e as Error).message}`);
|
|
169
183
|
}
|
|
170
184
|
|
|
185
|
+
let recallStats: { totalRecalls: number; entriesRecalled: number; avgRecallCount: number; maxRecallCount: number; topRecalled: Array<{ title: string; kind: string; recall_count: number; recall_sessions: number }> } | null = null;
|
|
186
|
+
try {
|
|
187
|
+
const totals = db.prepare(
|
|
188
|
+
`SELECT SUM(recall_count) as total_recalls, COUNT(CASE WHEN recall_count > 0 THEN 1 END) as entries_recalled, AVG(CASE WHEN recall_count > 0 THEN recall_count END) as avg_recall, MAX(recall_count) as max_recall FROM vault`
|
|
189
|
+
).get() as any;
|
|
190
|
+
const topRecalled = db.prepare(
|
|
191
|
+
`SELECT title, kind, recall_count, recall_sessions FROM vault WHERE recall_count > 0 ORDER BY recall_count DESC LIMIT 5`
|
|
192
|
+
).all() as any[];
|
|
193
|
+
recallStats = {
|
|
194
|
+
totalRecalls: totals?.total_recalls ?? 0,
|
|
195
|
+
entriesRecalled: totals?.entries_recalled ?? 0,
|
|
196
|
+
avgRecallCount: Math.round((totals?.avg_recall ?? 0) * 10) / 10,
|
|
197
|
+
maxRecallCount: totals?.max_recall ?? 0,
|
|
198
|
+
topRecalled,
|
|
199
|
+
};
|
|
200
|
+
} catch (e) {
|
|
201
|
+
errors.push(`Recall stats failed: ${(e as Error).message}`);
|
|
202
|
+
}
|
|
203
|
+
|
|
171
204
|
return {
|
|
172
205
|
fileCount,
|
|
173
206
|
subdirs,
|
|
@@ -185,6 +218,8 @@ export function gatherVaultStatus(ctx: LocalCtx, opts: Record<string, unknown> =
|
|
|
185
218
|
autoCapturedFeedbackCount,
|
|
186
219
|
archivedCount,
|
|
187
220
|
staleKnowledge,
|
|
221
|
+
indexingStats,
|
|
222
|
+
recallStats,
|
|
188
223
|
resolvedFrom: config.resolvedFrom,
|
|
189
224
|
errors,
|
|
190
225
|
};
|
|
@@ -2,7 +2,7 @@ import { existsSync, readFileSync } from 'node:fs';
|
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { gatherVaultStatus, computeGrowthWarnings } from '../status.js';
|
|
4
4
|
import { errorLogPath, errorLogCount } from '../error-log.js';
|
|
5
|
-
import { ok, err } from '../helpers.js';
|
|
5
|
+
import { ok, err, kindIcon } from '../helpers.js';
|
|
6
6
|
import type { LocalCtx, ToolResult } from '../types.js';
|
|
7
7
|
|
|
8
8
|
function relativeTime(ts: number): string {
|
|
@@ -30,53 +30,88 @@ export function handler(_args: Record<string, any>, ctx: LocalCtx): ToolResult {
|
|
|
30
30
|
const hasIssues = status.stalePaths || (status.embeddingStatus?.missing ?? 0) > 0;
|
|
31
31
|
const healthIcon = hasIssues ? '⚠' : '✓';
|
|
32
32
|
|
|
33
|
+
const schemaVersion = (ctx.db.prepare('PRAGMA user_version').get() as any)?.user_version ?? 'unknown';
|
|
34
|
+
const embedPct = status.embeddingStatus
|
|
35
|
+
? (status.embeddingStatus.total > 0 ? Math.round((status.embeddingStatus.indexed / status.embeddingStatus.total) * 100) : 100)
|
|
36
|
+
: null;
|
|
37
|
+
const embedStr = status.embeddingStatus
|
|
38
|
+
? `${status.embeddingStatus.indexed}/${status.embeddingStatus.total} (${embedPct}%)`
|
|
39
|
+
: 'n/a';
|
|
40
|
+
const modelStr = status.embedModelAvailable === false ? '⚠ unavailable' : status.embedModelAvailable === true ? '✓ loaded' : 'unknown';
|
|
41
|
+
|
|
33
42
|
const lines = [
|
|
34
|
-
`## ${healthIcon} Vault
|
|
43
|
+
`## ${healthIcon} Vault Dashboard`,
|
|
35
44
|
``,
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
45
|
+
`| | |`,
|
|
46
|
+
`|---|---|`,
|
|
47
|
+
`| **Vault** | ${config.vaultDir} (${config.vaultDirExists ? status.fileCount + ' files' : '⚠ missing'}) |`,
|
|
48
|
+
`| **Database** | ${config.dbPath} (${status.dbSize}) |`,
|
|
49
|
+
`| **Schema** | v${schemaVersion} |`,
|
|
50
|
+
`| **Embeddings** | ${embedStr} |`,
|
|
51
|
+
`| **Model** | ${modelStr} |`,
|
|
52
|
+
`| **Event decay** | ${config.eventDecayDays} days |`,
|
|
43
53
|
];
|
|
44
|
-
|
|
45
|
-
if (status.embeddingStatus) {
|
|
46
|
-
const { indexed, total, missing } = status.embeddingStatus;
|
|
47
|
-
const pct = total > 0 ? Math.round((indexed / total) * 100) : 100;
|
|
48
|
-
lines.push(`Embeddings: ${indexed}/${total} (${pct}%)`);
|
|
49
|
-
}
|
|
50
|
-
if (status.embedModelAvailable === false) {
|
|
51
|
-
lines.push(`Embed model: unavailable (semantic search disabled, FTS still works)`);
|
|
52
|
-
} else if (status.embedModelAvailable === true) {
|
|
53
|
-
lines.push(`Embed model: loaded`);
|
|
54
|
-
}
|
|
55
|
-
lines.push(`Decay: ${config.eventDecayDays} days (event recency window)`);
|
|
56
54
|
if (status.expiredCount > 0) {
|
|
57
|
-
lines.push(
|
|
58
|
-
`Expired: ${status.expiredCount} entries pending prune (run \`context-vault prune\` to remove now)`
|
|
59
|
-
);
|
|
55
|
+
lines.push(`| **Expired** | ${status.expiredCount} pending prune |`);
|
|
60
56
|
}
|
|
61
57
|
|
|
62
|
-
|
|
58
|
+
// Selective indexing stats
|
|
59
|
+
if (status.indexingStats) {
|
|
60
|
+
const ix = status.indexingStats;
|
|
61
|
+
const pct = ix.total > 0 ? Math.round((ix.indexed / ix.total) * 100) : 100;
|
|
62
|
+
lines.push(`| **Indexed entries** | ${ix.indexed}/${ix.total} (${pct}%) |`);
|
|
63
|
+
}
|
|
63
64
|
|
|
65
|
+
// Indexed kinds as compact table
|
|
66
|
+
lines.push(``, `### Entries by Kind`);
|
|
64
67
|
if (status.kindCounts.length) {
|
|
65
|
-
|
|
68
|
+
if (status.indexingStats?.byKind?.length) {
|
|
69
|
+
lines.push('| Kind | Total | Indexed |');
|
|
70
|
+
lines.push('|---|---|---|');
|
|
71
|
+
const byKindMap = new Map(status.indexingStats.byKind.map((k: any) => [k.kind, k]));
|
|
72
|
+
for (const { kind, c } of status.kindCounts) {
|
|
73
|
+
const kStats = byKindMap.get(kind) as { indexed?: number } | undefined;
|
|
74
|
+
const idxCount = kStats?.indexed ?? c;
|
|
75
|
+
lines.push(`| ${kindIcon(kind)} \`${kind}\` | ${c} | ${idxCount} |`);
|
|
76
|
+
}
|
|
77
|
+
} else {
|
|
78
|
+
lines.push('| Kind | Count |');
|
|
79
|
+
lines.push('|---|---|');
|
|
80
|
+
for (const { kind, c } of status.kindCounts) {
|
|
81
|
+
lines.push(`| ${kindIcon(kind)} \`${kind}\` | ${c} |`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
66
84
|
} else {
|
|
67
|
-
lines.push(
|
|
85
|
+
lines.push(`_(empty vault)_`);
|
|
68
86
|
}
|
|
69
87
|
|
|
70
88
|
if (status.categoryCounts.length) {
|
|
71
89
|
lines.push(``);
|
|
72
90
|
lines.push(`### Categories`);
|
|
73
|
-
for (const { category, c } of status.categoryCounts) lines.push(`-
|
|
91
|
+
for (const { category, c } of status.categoryCounts) lines.push(`- **${category}**: ${c}`);
|
|
74
92
|
}
|
|
75
93
|
|
|
76
94
|
if (status.subdirs.length) {
|
|
77
95
|
lines.push(``);
|
|
78
|
-
lines.push(`### Disk
|
|
79
|
-
for (const { name, count } of status.subdirs) lines.push(`-
|
|
96
|
+
lines.push(`### Disk`);
|
|
97
|
+
for (const { name, count } of status.subdirs) lines.push(`- \`${name}/\` ${count} files`);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (status.recallStats && status.recallStats.totalRecalls > 0) {
|
|
101
|
+
const rs = status.recallStats;
|
|
102
|
+
lines.push(``, `### Recall Frequency`);
|
|
103
|
+
lines.push(`| Metric | Value |`);
|
|
104
|
+
lines.push(`|---|---|`);
|
|
105
|
+
lines.push(`| **Total recalls** | ${rs.totalRecalls} |`);
|
|
106
|
+
lines.push(`| **Entries recalled** | ${rs.entriesRecalled} |`);
|
|
107
|
+
lines.push(`| **Avg recalls/entry** | ${rs.avgRecallCount} |`);
|
|
108
|
+
lines.push(`| **Max recalls** | ${rs.maxRecallCount} |`);
|
|
109
|
+
if (rs.topRecalled?.length) {
|
|
110
|
+
lines.push(``, `**Top recalled:**`);
|
|
111
|
+
for (const t of rs.topRecalled) {
|
|
112
|
+
lines.push(`- "${t.title || '(untitled)'}" (\`${t.kind}\`): ${t.recall_count} recalls, ${t.recall_sessions} sessions`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
80
115
|
}
|
|
81
116
|
|
|
82
117
|
if (status.stalePaths) {
|
package/src/tools/get-context.ts
CHANGED
|
@@ -8,7 +8,7 @@ import { normalizeKind } from '@context-vault/core/files';
|
|
|
8
8
|
import { parseContextParam } from '@context-vault/core/context';
|
|
9
9
|
import { resolveTemporalParams } from '../temporal.js';
|
|
10
10
|
import { collectLinkedEntries } from '../linking.js';
|
|
11
|
-
import { ok, err, errWithHint } from '../helpers.js';
|
|
11
|
+
import { ok, err, errWithHint, kindIcon, fmtDate } from '../helpers.js';
|
|
12
12
|
import { isEmbedAvailable } from '@context-vault/core/embed';
|
|
13
13
|
import type { LocalCtx, SharedCtx, ToolResult } from '../types.js';
|
|
14
14
|
|
|
@@ -526,6 +526,7 @@ export async function handler(
|
|
|
526
526
|
if (!include_superseded) {
|
|
527
527
|
clauses.push('superseded_by IS NULL');
|
|
528
528
|
}
|
|
529
|
+
clauses.push('indexed = 1');
|
|
529
530
|
const where = clauses.length ? `WHERE ${clauses.join(' AND ')}` : '';
|
|
530
531
|
params.push(fetchLimit);
|
|
531
532
|
let rows;
|
|
@@ -633,23 +634,24 @@ export async function handler(
|
|
|
633
634
|
const r = filtered[i];
|
|
634
635
|
const isSkeleton = i >= effectivePivot;
|
|
635
636
|
const entryTags = r.tags ? JSON.parse(r.tags) : [];
|
|
636
|
-
const tagStr = entryTags.length ? entryTags.join(', ') : '
|
|
637
|
-
const
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
637
|
+
const tagStr = entryTags.length ? entryTags.join(', ') : '';
|
|
638
|
+
const icon = kindIcon(r.kind);
|
|
639
|
+
const skeletonLabel = isSkeleton ? ' `skeleton`' : '';
|
|
640
|
+
const tierLabel = r.tier ? `**${r.tier}**` : '';
|
|
641
|
+
const dateStr = r.updated_at && r.updated_at !== r.created_at
|
|
642
|
+
? `${fmtDate(r.created_at)} (upd ${fmtDate(r.updated_at)})`
|
|
643
|
+
: fmtDate(r.created_at);
|
|
642
644
|
lines.push(
|
|
643
|
-
`###
|
|
644
|
-
);
|
|
645
|
-
const dateStr =
|
|
646
|
-
r.updated_at && r.updated_at !== r.created_at
|
|
647
|
-
? `${r.created_at} (updated ${r.updated_at})`
|
|
648
|
-
: r.created_at || '';
|
|
649
|
-
const tierStr = r.tier ? ` · tier: ${r.tier}` : '';
|
|
650
|
-
lines.push(
|
|
651
|
-
`${r.score.toFixed(3)} · ${tagStr} · ${relPath} · ${dateStr} · skeleton: ${isSkeleton}${tierStr} · id: \`${r.id}\``
|
|
645
|
+
`### ${icon} ${r.title || '(untitled)'}${skeletonLabel}`
|
|
652
646
|
);
|
|
647
|
+
const meta = [
|
|
648
|
+
`\`${r.score.toFixed(2)}\``,
|
|
649
|
+
`\`${r.kind}\``,
|
|
650
|
+
tierLabel,
|
|
651
|
+
tagStr,
|
|
652
|
+
dateStr,
|
|
653
|
+
].filter(Boolean).join(' · ');
|
|
654
|
+
lines.push(`${meta} \nid: \`${r.id}\``);
|
|
653
655
|
const stalenessResult = checkStaleness(r);
|
|
654
656
|
if (stalenessResult) {
|
|
655
657
|
r.stale = true;
|
|
@@ -691,15 +693,13 @@ export async function handler(
|
|
|
691
693
|
if (uniqueLinked.length > 0) {
|
|
692
694
|
lines.push(`## Linked Entries (${uniqueLinked.length} via related_to)\n`);
|
|
693
695
|
for (const r of uniqueLinked) {
|
|
694
|
-
const direction = forward.some((f: any) => f.id === r.id) ? '→
|
|
696
|
+
const direction = forward.some((f: any) => f.id === r.id) ? '→' : '←';
|
|
695
697
|
const entryTags = r.tags ? JSON.parse(r.tags) : [];
|
|
696
|
-
const tagStr = entryTags.length ? entryTags.join(', ') : '
|
|
697
|
-
const
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
lines.push(`### ${r.title || '(untitled)'} [${r.kind}/${r.category}] ${direction}`);
|
|
702
|
-
lines.push(`${tagStr} · ${relPath} · id: \`${r.id}\``);
|
|
698
|
+
const tagStr = entryTags.length ? entryTags.join(', ') : '';
|
|
699
|
+
const icon = kindIcon(r.kind);
|
|
700
|
+
lines.push(`### ${icon} ${r.title || '(untitled)'} ${direction}`);
|
|
701
|
+
const meta = [`\`${r.kind}\``, tagStr].filter(Boolean).join(' · ');
|
|
702
|
+
lines.push(`${meta} \nid: \`${r.id}\``);
|
|
703
703
|
lines.push(truncateBody(r.body, body_limit ?? 300));
|
|
704
704
|
lines.push('');
|
|
705
705
|
}
|
|
@@ -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
|
|
|
@@ -18,10 +18,11 @@ export const inputSchema = {
|
|
|
18
18
|
until: z.string().optional().describe('ISO date, return entries created before this'),
|
|
19
19
|
limit: z.number().optional().describe('Max results to return (default 20, max 100)'),
|
|
20
20
|
offset: z.number().optional().describe('Skip first N results for pagination'),
|
|
21
|
+
include_unindexed: z.boolean().optional().describe('If true, include entries that are stored but not indexed for search. Default: false.'),
|
|
21
22
|
};
|
|
22
23
|
|
|
23
24
|
export async function handler(
|
|
24
|
-
{ kind, category, tags, since, until, limit, offset }: Record<string, any>,
|
|
25
|
+
{ kind, category, tags, since, until, limit, offset, include_unindexed }: Record<string, any>,
|
|
25
26
|
ctx: LocalCtx,
|
|
26
27
|
{ ensureIndexed, reindexFailed }: SharedCtx
|
|
27
28
|
): Promise<ToolResult> {
|
|
@@ -63,6 +64,9 @@ export async function handler(
|
|
|
63
64
|
params.push(until);
|
|
64
65
|
}
|
|
65
66
|
clauses.push("(expires_at IS NULL OR expires_at > datetime('now'))");
|
|
67
|
+
if (!include_unindexed) {
|
|
68
|
+
clauses.push('indexed = 1');
|
|
69
|
+
}
|
|
66
70
|
|
|
67
71
|
const where = clauses.length ? `WHERE ${clauses.join(' AND ')}` : '';
|
|
68
72
|
const effectiveLimit = Math.min(limit || 20, 100);
|
|
@@ -81,7 +85,7 @@ export async function handler(
|
|
|
81
85
|
params.push(fetchLimit, effectiveOffset);
|
|
82
86
|
rows = ctx.db
|
|
83
87
|
.prepare(
|
|
84
|
-
`SELECT id, title, kind, category, tags, created_at, updated_at, SUBSTR(body, 1, 120) as preview FROM vault ${where} ORDER BY created_at DESC LIMIT ? OFFSET ?`
|
|
88
|
+
`SELECT id, title, kind, category, tags, created_at, updated_at, indexed, recall_count, SUBSTR(body, 1, 120) as preview FROM vault ${where} ORDER BY created_at DESC LIMIT ? OFFSET ?`
|
|
85
89
|
)
|
|
86
90
|
.all(...params) as any[];
|
|
87
91
|
} catch (e) {
|
|
@@ -124,18 +128,26 @@ export async function handler(
|
|
|
124
128
|
`> ℹ Event search limited to last ${days} days. Use \`since\` parameter for older results.\n`
|
|
125
129
|
);
|
|
126
130
|
}
|
|
131
|
+
if (include_unindexed) {
|
|
132
|
+
lines.push('| | Title | Kind | Tags | Recalls | Idx | Date | ID |');
|
|
133
|
+
lines.push('|---|---|---|---|---|---|---|---|');
|
|
134
|
+
} else {
|
|
135
|
+
lines.push('| | Title | Kind | Tags | Recalls | Date | ID |');
|
|
136
|
+
lines.push('|---|---|---|---|---|---|---|');
|
|
137
|
+
}
|
|
127
138
|
for (const r of filtered) {
|
|
128
139
|
const entryTags = r.tags ? JSON.parse(r.tags) : [];
|
|
129
|
-
const tagStr = entryTags.length ? entryTags.join(', ') : '
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
lines.push(
|
|
140
|
+
const tagStr = entryTags.length ? entryTags.join(', ') : '';
|
|
141
|
+
const date = fmtDate(r.updated_at && r.updated_at !== r.created_at ? r.updated_at : r.created_at);
|
|
142
|
+
const icon = kindIcon(r.kind);
|
|
143
|
+
const title = (r.title || '(untitled)').replace(/\|/g, '\\|');
|
|
144
|
+
const recalls = r.recall_count ?? 0;
|
|
145
|
+
if (include_unindexed) {
|
|
146
|
+
const idxStr = r.indexed ? 'Y' : 'N';
|
|
147
|
+
lines.push(`| ${icon} | **${title}** | \`${r.kind}\` | ${tagStr} | ${recalls} | ${idxStr} | ${date} | \`${r.id}\` |`);
|
|
148
|
+
} else {
|
|
149
|
+
lines.push(`| ${icon} | **${title}** | \`${r.kind}\` | ${tagStr} | ${recalls} | ${date} | \`${r.id}\` |`);
|
|
150
|
+
}
|
|
139
151
|
}
|
|
140
152
|
|
|
141
153
|
if (effectiveOffset + effectiveLimit < total) {
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
+
import { existsSync, readFileSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { resolve, basename, join } from 'node:path';
|
|
4
|
+
import { homedir } from 'node:os';
|
|
2
5
|
import { captureAndIndex, updateEntryFile } from '@context-vault/core/capture';
|
|
3
6
|
import { indexEntry } from '@context-vault/core/index';
|
|
4
7
|
import { categoryFor, defaultTierFor } from '@context-vault/core/categories';
|
|
5
|
-
import { normalizeKind } from '@context-vault/core/files';
|
|
8
|
+
import { normalizeKind, kindToPath } from '@context-vault/core/files';
|
|
6
9
|
import { parseContextParam } from '@context-vault/core/context';
|
|
7
|
-
import {
|
|
10
|
+
import { shouldIndex } from '@context-vault/core/indexing';
|
|
11
|
+
import { ok, err, errWithHint, ensureVaultExists, ensureValidKind, kindIcon } from '../helpers.js';
|
|
8
12
|
import { maybeShowFeedbackPrompt } from '../telemetry.js';
|
|
9
13
|
import { validateRelatedTo } from '../linking.js';
|
|
10
14
|
import type { LocalCtx, SharedCtx, ToolResult } from '../types.js';
|
|
@@ -23,6 +27,34 @@ const DEFAULT_SIMILARITY_THRESHOLD = 0.85;
|
|
|
23
27
|
const SKIP_THRESHOLD = 0.95;
|
|
24
28
|
const UPDATE_THRESHOLD = 0.85;
|
|
25
29
|
|
|
30
|
+
function isDualWriteEnabled(config: { dataDir: string }): boolean {
|
|
31
|
+
try {
|
|
32
|
+
const configPath = join(config.dataDir, 'config.json');
|
|
33
|
+
if (!existsSync(configPath)) return true;
|
|
34
|
+
const fc = JSON.parse(readFileSync(configPath, 'utf-8'));
|
|
35
|
+
if (fc.dualWrite && fc.dualWrite.enabled === false) return false;
|
|
36
|
+
return true;
|
|
37
|
+
} catch {
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function dualWriteLocal(entryFilePath: string, kind: string): void {
|
|
43
|
+
const cwd = process.cwd();
|
|
44
|
+
const home = homedir();
|
|
45
|
+
if (!cwd || cwd === '/' || cwd === home) return;
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
const content = readFileSync(entryFilePath, 'utf-8');
|
|
49
|
+
const localDir = resolve(cwd, '.context', kindToPath(kind));
|
|
50
|
+
mkdirSync(localDir, { recursive: true });
|
|
51
|
+
const filename = basename(entryFilePath);
|
|
52
|
+
writeFileSync(join(localDir, filename), content);
|
|
53
|
+
} catch (e) {
|
|
54
|
+
console.warn(`[context-vault] Dual-write to .context/ failed: ${(e as Error).message}`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
26
58
|
async function findSimilar(
|
|
27
59
|
ctx: LocalCtx,
|
|
28
60
|
embedding: any,
|
|
@@ -82,13 +114,13 @@ async function findSimilar(
|
|
|
82
114
|
}
|
|
83
115
|
|
|
84
116
|
function formatSimilarWarning(similar: any[]): string {
|
|
85
|
-
const lines = ['', '⚠ Similar entries
|
|
117
|
+
const lines = ['', '### ⚠ Similar entries'];
|
|
86
118
|
for (const e of similar) {
|
|
87
|
-
const score = e.score.toFixed(
|
|
88
|
-
const title = e.title ?
|
|
89
|
-
lines.push(
|
|
119
|
+
const score = (e.score * 100).toFixed(0);
|
|
120
|
+
const title = e.title ? `**${e.title}**` : '(no title)';
|
|
121
|
+
lines.push(`- ${title} \`${score}%\` · \`${e.id}\``);
|
|
90
122
|
}
|
|
91
|
-
lines.push('
|
|
123
|
+
lines.push('_Use `id` param to update instead of creating a duplicate._');
|
|
92
124
|
return lines.join('\n');
|
|
93
125
|
}
|
|
94
126
|
|
|
@@ -142,13 +174,12 @@ export function buildConflictCandidates(similarEntries: any[]): any[] {
|
|
|
142
174
|
}
|
|
143
175
|
|
|
144
176
|
function formatConflictSuggestions(candidates: any[]): string {
|
|
145
|
-
const lines = ['', '
|
|
177
|
+
const lines = ['', '### Conflict Resolution'];
|
|
146
178
|
for (const c of candidates) {
|
|
147
|
-
const titleDisplay = c.title ?
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
);
|
|
151
|
-
lines.push(` ${c.reasoning_context}`);
|
|
179
|
+
const titleDisplay = c.title ? `**${c.title}**` : '(no title)';
|
|
180
|
+
const actionIcon = c.suggested_action === 'SKIP' ? '⊘' : c.suggested_action === 'UPDATE' ? '↻' : '+';
|
|
181
|
+
lines.push(`${actionIcon} **${c.suggested_action}** ${titleDisplay} \`${(c.score * 100).toFixed(0)}%\` · \`${c.id}\``);
|
|
182
|
+
lines.push(` ${c.reasoning_context}`);
|
|
152
183
|
}
|
|
153
184
|
return lines.join('\n');
|
|
154
185
|
}
|
|
@@ -317,6 +348,12 @@ export const inputSchema = {
|
|
|
317
348
|
.describe(
|
|
318
349
|
'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.'
|
|
319
350
|
),
|
|
351
|
+
indexed: z
|
|
352
|
+
.boolean()
|
|
353
|
+
.optional()
|
|
354
|
+
.describe(
|
|
355
|
+
'Whether to index this entry for search (generate embeddings + FTS). Default: auto-determined by indexing config. Set to false to store file + metadata only, skipping embedding generation. Set to true to force indexing regardless of config rules.'
|
|
356
|
+
),
|
|
320
357
|
};
|
|
321
358
|
|
|
322
359
|
export async function handler(
|
|
@@ -339,6 +376,7 @@ export async function handler(
|
|
|
339
376
|
tier,
|
|
340
377
|
conflict_resolution,
|
|
341
378
|
encoding_context,
|
|
379
|
+
indexed,
|
|
342
380
|
}: Record<string, any>,
|
|
343
381
|
ctx: LocalCtx,
|
|
344
382
|
{ ensureIndexed }: SharedCtx
|
|
@@ -448,6 +486,10 @@ export async function handler(
|
|
|
448
486
|
} else if (entry.related_to === null && ctx.stmts.updateRelatedTo) {
|
|
449
487
|
ctx.stmts.updateRelatedTo.run(null, entry.id);
|
|
450
488
|
}
|
|
489
|
+
if (isDualWriteEnabled(config) && entry.filePath) {
|
|
490
|
+
dualWriteLocal(entry.filePath, entry.kind);
|
|
491
|
+
}
|
|
492
|
+
|
|
451
493
|
const relPath = entry.filePath
|
|
452
494
|
? entry.filePath.replace(config.vaultDir + '/', '')
|
|
453
495
|
: entry.filePath;
|
|
@@ -549,7 +591,17 @@ export async function handler(
|
|
|
549
591
|
|
|
550
592
|
const effectiveTier = tier ?? defaultTierFor(normalizedKind);
|
|
551
593
|
|
|
552
|
-
const
|
|
594
|
+
const effectiveIndexed = shouldIndex(
|
|
595
|
+
{
|
|
596
|
+
kind: normalizedKind,
|
|
597
|
+
category,
|
|
598
|
+
bodyLength: body?.length ?? 0,
|
|
599
|
+
explicitIndexed: indexed,
|
|
600
|
+
},
|
|
601
|
+
config.indexing
|
|
602
|
+
);
|
|
603
|
+
|
|
604
|
+
const embeddingToReuse = category === 'knowledge' && effectiveIndexed ? queryEmbedding : null;
|
|
553
605
|
|
|
554
606
|
let entry;
|
|
555
607
|
try {
|
|
@@ -568,8 +620,8 @@ export async function handler(
|
|
|
568
620
|
supersedes,
|
|
569
621
|
related_to,
|
|
570
622
|
source_files,
|
|
571
|
-
|
|
572
623
|
tier: effectiveTier,
|
|
624
|
+
indexed: effectiveIndexed,
|
|
573
625
|
},
|
|
574
626
|
embeddingToReuse
|
|
575
627
|
);
|
|
@@ -582,7 +634,7 @@ export async function handler(
|
|
|
582
634
|
}
|
|
583
635
|
|
|
584
636
|
// Store context embedding in vault_ctx_vec for contextual reinstatement
|
|
585
|
-
if (parsedCtx?.text && entry) {
|
|
637
|
+
if (parsedCtx?.text && entry && effectiveIndexed) {
|
|
586
638
|
try {
|
|
587
639
|
const ctxEmbedding = await ctx.embed(parsedCtx.text);
|
|
588
640
|
if (ctxEmbedding) {
|
|
@@ -599,6 +651,10 @@ export async function handler(
|
|
|
599
651
|
}
|
|
600
652
|
}
|
|
601
653
|
|
|
654
|
+
if (isDualWriteEnabled(config) && entry.filePath) {
|
|
655
|
+
dualWriteLocal(entry.filePath, normalizedKind);
|
|
656
|
+
}
|
|
657
|
+
|
|
602
658
|
if (ctx.config?.dataDir) {
|
|
603
659
|
maybeShowFeedbackPrompt(ctx.config.dataDir);
|
|
604
660
|
}
|
|
@@ -606,11 +662,25 @@ export async function handler(
|
|
|
606
662
|
const relPath = entry.filePath
|
|
607
663
|
? entry.filePath.replace(config.vaultDir + '/', '')
|
|
608
664
|
: entry.filePath;
|
|
609
|
-
const
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
665
|
+
const icon = kindIcon(normalizedKind);
|
|
666
|
+
const parts = [
|
|
667
|
+
`## ✓ Saved`,
|
|
668
|
+
`${icon} **${title || '(untitled)'}**`,
|
|
669
|
+
`\`${normalizedKind}\` · **${effectiveTier}**${tags?.length ? ` · ${tags.join(', ')}` : ''}`,
|
|
670
|
+
`\`${entry.id}\` → ${relPath}`,
|
|
671
|
+
];
|
|
672
|
+
if (!effectiveIndexed) {
|
|
673
|
+
parts.push(
|
|
674
|
+
'',
|
|
675
|
+
'_Note: this entry is stored but not indexed (no embeddings/FTS). It will not appear in search results. Use `include_unindexed: true` in list_context to browse unindexed entries._'
|
|
676
|
+
);
|
|
677
|
+
}
|
|
678
|
+
if (effectiveTier === 'ephemeral') {
|
|
679
|
+
parts.push(
|
|
680
|
+
'',
|
|
681
|
+
'_Note: ephemeral entries are excluded from default search. Use `include_ephemeral: true` in get_context to find them._'
|
|
682
|
+
);
|
|
683
|
+
}
|
|
614
684
|
const hasBucketTag = (tags || []).some(
|
|
615
685
|
(t: any) => typeof t === 'string' && t.startsWith('bucket:')
|
|
616
686
|
);
|
|
@@ -624,18 +694,25 @@ export async function handler(
|
|
|
624
694
|
(t: any) => typeof t === 'string' && t.startsWith('bucket:')
|
|
625
695
|
);
|
|
626
696
|
for (const bt of bucketTags) {
|
|
627
|
-
const bucketUserClause = '';
|
|
628
|
-
const bucketParams = false ? [bt] : [bt];
|
|
629
697
|
const exists = ctx.db
|
|
630
698
|
.prepare(
|
|
631
|
-
`SELECT 1 FROM vault WHERE kind = 'bucket' AND identity_key = ?
|
|
699
|
+
`SELECT 1 FROM vault WHERE kind = 'bucket' AND identity_key = ? LIMIT 1`
|
|
632
700
|
)
|
|
633
|
-
.get(
|
|
701
|
+
.get(bt);
|
|
634
702
|
if (!exists) {
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
703
|
+
// Auto-register the bucket silently
|
|
704
|
+
const bucketName = bt.replace(/^bucket:/, '');
|
|
705
|
+
try {
|
|
706
|
+
await captureAndIndex(ctx, {
|
|
707
|
+
kind: 'bucket',
|
|
708
|
+
title: bucketName,
|
|
709
|
+
body: `Bucket for project: ${bucketName}`,
|
|
710
|
+
tags: [bt],
|
|
711
|
+
identity_key: bt,
|
|
712
|
+
});
|
|
713
|
+
} catch {
|
|
714
|
+
// Non-fatal: bucket registration failure should not block the save
|
|
715
|
+
}
|
|
639
716
|
}
|
|
640
717
|
}
|
|
641
718
|
if (similarEntries.length) {
|