context-vault 3.4.4 → 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 +24 -2
- 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.map +1 -1
- package/dist/tools/get-context.js +22 -18
- 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.map +1 -1
- package/dist/tools/save-context.js +37 -22
- 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/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/package.json +1 -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/frontmatter.ts +2 -0
- package/package.json +2 -2
- package/scripts/prepack.js +17 -0
- package/src/helpers.ts +25 -0
- package/src/server.ts +25 -2
- package/src/tools/context-status.ts +30 -30
- package/src/tools/get-context.ts +23 -24
- package/src/tools/list-context.ts +8 -11
- package/src/tools/save-context.ts +39 -25
- package/src/tools/session-start.ts +36 -5
|
@@ -1,9 +1,30 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from 'node:fs';
|
|
2
2
|
import { join, resolve } from 'node:path';
|
|
3
|
-
import { homedir } from 'node:os';
|
|
3
|
+
import { homedir, tmpdir } from 'node:os';
|
|
4
4
|
import { DEFAULT_GROWTH_THRESHOLDS, DEFAULT_LIFECYCLE } from './constants.js';
|
|
5
5
|
import type { VaultConfig } from './types.js';
|
|
6
6
|
|
|
7
|
+
/**
|
|
8
|
+
* Guard against writes to the real config file during test runs.
|
|
9
|
+
* Set CONTEXT_VAULT_TEST=1 in test helpers to activate.
|
|
10
|
+
*
|
|
11
|
+
* Allows writes if the target path is under a temp directory (tests with
|
|
12
|
+
* HOME overridden to a temp dir). Blocks writes to non-temp paths.
|
|
13
|
+
*/
|
|
14
|
+
export function assertNotTestMode(targetPath: string): void {
|
|
15
|
+
if (process.env.CONTEXT_VAULT_TEST !== '1') return;
|
|
16
|
+
const resolved = resolve(targetPath);
|
|
17
|
+
const tmp = tmpdir();
|
|
18
|
+
// Allow writes to temp directories (tests with HOME isolation)
|
|
19
|
+
if (resolved.startsWith(tmp) || resolved.startsWith('/tmp/') || resolved.startsWith('/var/folders/')) {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
throw new Error(
|
|
23
|
+
`[context-vault] Refusing to write to real config in test mode (${targetPath}). ` +
|
|
24
|
+
'Set HOME or CONTEXT_VAULT_DATA_DIR to a temp directory.'
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
7
28
|
export function parseArgs(argv: string[]): Record<string, string | number> {
|
|
8
29
|
const args: Record<string, string | number> = {};
|
|
9
30
|
for (let i = 2; i < argv.length; i++) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "context-vault",
|
|
3
|
-
"version": "3.4.
|
|
3
|
+
"version": "3.4.5",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Persistent memory for AI agents — saves and searches knowledge across sessions",
|
|
6
6
|
"bin": {
|
|
@@ -63,7 +63,7 @@
|
|
|
63
63
|
"@context-vault/core"
|
|
64
64
|
],
|
|
65
65
|
"dependencies": {
|
|
66
|
-
"@context-vault/core": "^3.4.
|
|
66
|
+
"@context-vault/core": "^3.4.5",
|
|
67
67
|
"@modelcontextprotocol/sdk": "^1.26.0",
|
|
68
68
|
"adm-zip": "^0.5.16",
|
|
69
69
|
"sqlite-vec": "^0.1.0"
|
package/scripts/prepack.js
CHANGED
|
@@ -35,3 +35,20 @@ delete corePkg.dependencies;
|
|
|
35
35
|
writeFileSync(corePkgPath, JSON.stringify(corePkg, null, 2) + '\n');
|
|
36
36
|
|
|
37
37
|
console.log('[prepack] Bundled @context-vault/core into node_modules');
|
|
38
|
+
|
|
39
|
+
// Copy monorepo-level assets (agent-rules.md, setup-prompt.md) into local assets/
|
|
40
|
+
// so they ship with the npm package.
|
|
41
|
+
const MONOREPO_ASSETS = join(LOCAL_ROOT, '..', '..', 'assets');
|
|
42
|
+
const LOCAL_ASSETS = join(LOCAL_ROOT, 'assets');
|
|
43
|
+
const ASSET_FILES = ['agent-rules.md', 'setup-prompt.md'];
|
|
44
|
+
|
|
45
|
+
for (const file of ASSET_FILES) {
|
|
46
|
+
const src = join(MONOREPO_ASSETS, file);
|
|
47
|
+
const dest = join(LOCAL_ASSETS, file);
|
|
48
|
+
try {
|
|
49
|
+
cpSync(src, dest);
|
|
50
|
+
console.log(`[prepack] Copied ${file} to assets/`);
|
|
51
|
+
} catch (e) {
|
|
52
|
+
console.warn(`[prepack] Warning: could not copy ${file}: ${e.message}`);
|
|
53
|
+
}
|
|
54
|
+
}
|
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));
|
|
@@ -312,6 +312,29 @@ async function main(): Promise<void> {
|
|
|
312
312
|
|
|
313
313
|
config.vaultDirExists = existsSync(config.vaultDir);
|
|
314
314
|
|
|
315
|
+
// Validate vaultDir sanity
|
|
316
|
+
const osTmp = tmpdir();
|
|
317
|
+
const isTempPath = config.vaultDir.startsWith(osTmp) ||
|
|
318
|
+
config.vaultDir.startsWith('/tmp/') ||
|
|
319
|
+
config.vaultDir.startsWith('/var/folders/');
|
|
320
|
+
if (isTempPath) {
|
|
321
|
+
console.error(`[context-vault] WARNING: vaultDir points to a temp directory: ${config.vaultDir}`);
|
|
322
|
+
console.error(`[context-vault] This is likely from a test run that overwrote ~/.context-mcp/config.json`);
|
|
323
|
+
console.error(`[context-vault] Fix: run 'context-vault reconnect' or 'context-vault setup'`);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
if (config.vaultDirExists) {
|
|
327
|
+
try {
|
|
328
|
+
const entries = readdirSync(config.vaultDir);
|
|
329
|
+
const hasMdFiles = entries.some(f => f.endsWith('.md'));
|
|
330
|
+
const hasMarker = entries.includes('.context-mcp');
|
|
331
|
+
if (!hasMdFiles && hasMarker) {
|
|
332
|
+
console.error(`[context-vault] WARNING: vaultDir has no markdown files but has a marker file`);
|
|
333
|
+
console.error(`[context-vault] The vault may be misconfigured. Run 'context-vault reconnect'`);
|
|
334
|
+
}
|
|
335
|
+
} catch {}
|
|
336
|
+
}
|
|
337
|
+
|
|
315
338
|
console.error(`[context-vault] Vault: ${config.vaultDir}`);
|
|
316
339
|
console.error(`[context-vault] Database: ${config.dbPath}`);
|
|
317
340
|
console.error(`[context-vault] Dev dir: ${config.devDir}`);
|
|
@@ -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,53 @@ 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
|
|
|
58
|
+
// Indexed kinds as compact table
|
|
62
59
|
lines.push(``, `### Indexed`);
|
|
63
|
-
|
|
64
60
|
if (status.kindCounts.length) {
|
|
65
|
-
|
|
61
|
+
lines.push('| Kind | Count |');
|
|
62
|
+
lines.push('|---|---|');
|
|
63
|
+
for (const { kind, c } of status.kindCounts) {
|
|
64
|
+
lines.push(`| ${kindIcon(kind)} \`${kind}\` | ${c} |`);
|
|
65
|
+
}
|
|
66
66
|
} else {
|
|
67
|
-
lines.push(
|
|
67
|
+
lines.push(`_(empty vault)_`);
|
|
68
68
|
}
|
|
69
69
|
|
|
70
70
|
if (status.categoryCounts.length) {
|
|
71
71
|
lines.push(``);
|
|
72
72
|
lines.push(`### Categories`);
|
|
73
|
-
for (const { category, c } of status.categoryCounts) lines.push(`-
|
|
73
|
+
for (const { category, c } of status.categoryCounts) lines.push(`- **${category}**: ${c}`);
|
|
74
74
|
}
|
|
75
75
|
|
|
76
76
|
if (status.subdirs.length) {
|
|
77
77
|
lines.push(``);
|
|
78
|
-
lines.push(`### Disk
|
|
79
|
-
for (const { name, count } of status.subdirs) lines.push(`-
|
|
78
|
+
lines.push(`### Disk`);
|
|
79
|
+
for (const { name, count } of status.subdirs) lines.push(`- \`${name}/\` ${count} files`);
|
|
80
80
|
}
|
|
81
81
|
|
|
82
82
|
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
|
|
|
@@ -633,23 +633,24 @@ export async function handler(
|
|
|
633
633
|
const r = filtered[i];
|
|
634
634
|
const isSkeleton = i >= effectivePivot;
|
|
635
635
|
const entryTags = r.tags ? JSON.parse(r.tags) : [];
|
|
636
|
-
const tagStr = entryTags.length ? entryTags.join(', ') : '
|
|
637
|
-
const
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
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);
|
|
642
643
|
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}\``
|
|
644
|
+
`### ${icon} ${r.title || '(untitled)'}${skeletonLabel}`
|
|
652
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}\``);
|
|
653
654
|
const stalenessResult = checkStaleness(r);
|
|
654
655
|
if (stalenessResult) {
|
|
655
656
|
r.stale = true;
|
|
@@ -691,15 +692,13 @@ export async function handler(
|
|
|
691
692
|
if (uniqueLinked.length > 0) {
|
|
692
693
|
lines.push(`## Linked Entries (${uniqueLinked.length} via related_to)\n`);
|
|
693
694
|
for (const r of uniqueLinked) {
|
|
694
|
-
const direction = forward.some((f: any) => f.id === r.id) ? '→
|
|
695
|
+
const direction = forward.some((f: any) => f.id === r.id) ? '→' : '←';
|
|
695
696
|
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}\``);
|
|
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}\``);
|
|
703
702
|
lines.push(truncateBody(r.body, body_limit ?? 300));
|
|
704
703
|
lines.push('');
|
|
705
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) {
|
|
@@ -4,7 +4,7 @@ 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
6
|
import { parseContextParam } from '@context-vault/core/context';
|
|
7
|
-
import { ok, err, errWithHint, ensureVaultExists, ensureValidKind } from '../helpers.js';
|
|
7
|
+
import { ok, err, errWithHint, ensureVaultExists, ensureValidKind, kindIcon } from '../helpers.js';
|
|
8
8
|
import { maybeShowFeedbackPrompt } from '../telemetry.js';
|
|
9
9
|
import { validateRelatedTo } from '../linking.js';
|
|
10
10
|
import type { LocalCtx, SharedCtx, ToolResult } from '../types.js';
|
|
@@ -82,13 +82,13 @@ async function findSimilar(
|
|
|
82
82
|
}
|
|
83
83
|
|
|
84
84
|
function formatSimilarWarning(similar: any[]): string {
|
|
85
|
-
const lines = ['', '⚠ Similar entries
|
|
85
|
+
const lines = ['', '### ⚠ Similar entries'];
|
|
86
86
|
for (const e of similar) {
|
|
87
|
-
const score = e.score.toFixed(
|
|
88
|
-
const title = e.title ?
|
|
89
|
-
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}\``);
|
|
90
90
|
}
|
|
91
|
-
lines.push('
|
|
91
|
+
lines.push('_Use `id` param to update instead of creating a duplicate._');
|
|
92
92
|
return lines.join('\n');
|
|
93
93
|
}
|
|
94
94
|
|
|
@@ -142,13 +142,12 @@ export function buildConflictCandidates(similarEntries: any[]): any[] {
|
|
|
142
142
|
}
|
|
143
143
|
|
|
144
144
|
function formatConflictSuggestions(candidates: any[]): string {
|
|
145
|
-
const lines = ['', '
|
|
145
|
+
const lines = ['', '### Conflict Resolution'];
|
|
146
146
|
for (const c of candidates) {
|
|
147
|
-
const titleDisplay = c.title ?
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
);
|
|
151
|
-
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}`);
|
|
152
151
|
}
|
|
153
152
|
return lines.join('\n');
|
|
154
153
|
}
|
|
@@ -606,11 +605,19 @@ export async function handler(
|
|
|
606
605
|
const relPath = entry.filePath
|
|
607
606
|
? entry.filePath.replace(config.vaultDir + '/', '')
|
|
608
607
|
: entry.filePath;
|
|
609
|
-
const
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
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
|
+
}
|
|
614
621
|
const hasBucketTag = (tags || []).some(
|
|
615
622
|
(t: any) => typeof t === 'string' && t.startsWith('bucket:')
|
|
616
623
|
);
|
|
@@ -624,18 +631,25 @@ export async function handler(
|
|
|
624
631
|
(t: any) => typeof t === 'string' && t.startsWith('bucket:')
|
|
625
632
|
);
|
|
626
633
|
for (const bt of bucketTags) {
|
|
627
|
-
const bucketUserClause = '';
|
|
628
|
-
const bucketParams = false ? [bt] : [bt];
|
|
629
634
|
const exists = ctx.db
|
|
630
635
|
.prepare(
|
|
631
|
-
`SELECT 1 FROM vault WHERE kind = 'bucket' AND identity_key = ?
|
|
636
|
+
`SELECT 1 FROM vault WHERE kind = 'bucket' AND identity_key = ? LIMIT 1`
|
|
632
637
|
)
|
|
633
|
-
.get(
|
|
638
|
+
.get(bt);
|
|
634
639
|
if (!exists) {
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
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
|
+
}
|
|
639
653
|
}
|
|
640
654
|
}
|
|
641
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'}_`
|