context-vault 3.1.6 → 3.1.8
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/dist/archive.d.ts +23 -0
- package/dist/archive.d.ts.map +1 -0
- package/dist/archive.js +197 -0
- package/dist/archive.js.map +1 -0
- package/dist/consolidation.d.ts +14 -0
- package/dist/consolidation.d.ts.map +1 -0
- package/dist/consolidation.js +59 -0
- package/dist/consolidation.js.map +1 -0
- package/dist/error-log.d.ts +4 -0
- package/dist/error-log.d.ts.map +1 -0
- package/dist/error-log.js +33 -0
- package/dist/error-log.js.map +1 -0
- package/dist/helpers.d.ts +10 -0
- package/dist/helpers.d.ts.map +1 -0
- package/dist/helpers.js +42 -0
- package/dist/helpers.js.map +1 -0
- package/dist/linking.d.ts +13 -0
- package/dist/linking.d.ts.map +1 -0
- package/dist/linking.js +86 -0
- package/dist/linking.js.map +1 -0
- package/dist/migrate-dirs.d.ts +16 -0
- package/dist/migrate-dirs.d.ts.map +1 -0
- package/dist/migrate-dirs.js +127 -0
- package/dist/migrate-dirs.js.map +1 -0
- package/dist/register-tools.d.ts +3 -0
- package/dist/register-tools.d.ts.map +1 -0
- package/dist/register-tools.js +161 -0
- package/dist/register-tools.js.map +1 -0
- package/dist/server.d.ts +3 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +241 -0
- package/dist/server.js.map +1 -0
- package/dist/status.d.ts +18 -0
- package/dist/status.d.ts.map +1 -0
- package/dist/status.js +265 -0
- package/dist/status.js.map +1 -0
- package/dist/telemetry.d.ts +6 -0
- package/dist/telemetry.d.ts.map +1 -0
- package/dist/telemetry.js +74 -0
- package/dist/telemetry.js.map +1 -0
- package/dist/temporal.d.ts +9 -0
- package/dist/temporal.d.ts.map +1 -0
- package/dist/temporal.js +76 -0
- package/dist/temporal.js.map +1 -0
- package/dist/tools/clear-context.d.ts +11 -0
- package/dist/tools/clear-context.d.ts.map +1 -0
- package/dist/tools/clear-context.js +28 -0
- package/dist/tools/clear-context.js.map +1 -0
- package/dist/tools/context-status.d.ts +6 -0
- package/dist/tools/context-status.d.ts.map +1 -0
- package/dist/tools/context-status.js +160 -0
- package/dist/tools/context-status.js.map +1 -0
- package/dist/tools/create-snapshot.d.ts +13 -0
- package/dist/tools/create-snapshot.d.ts.map +1 -0
- package/dist/tools/create-snapshot.js +161 -0
- package/dist/tools/create-snapshot.js.map +1 -0
- package/dist/tools/delete-context.d.ts +9 -0
- package/dist/tools/delete-context.d.ts.map +1 -0
- package/dist/tools/delete-context.js +45 -0
- package/dist/tools/delete-context.js.map +1 -0
- package/dist/tools/get-context.d.ts +85 -0
- package/dist/tools/get-context.d.ts.map +1 -0
- package/dist/tools/get-context.js +576 -0
- package/dist/tools/get-context.js.map +1 -0
- package/dist/tools/ingest-project.d.ts +11 -0
- package/dist/tools/ingest-project.d.ts.map +1 -0
- package/dist/tools/ingest-project.js +226 -0
- package/dist/tools/ingest-project.js.map +1 -0
- package/dist/tools/ingest-url.d.ts +11 -0
- package/dist/tools/ingest-url.d.ts.map +1 -0
- package/dist/tools/ingest-url.js +62 -0
- package/dist/tools/ingest-url.js.map +1 -0
- package/dist/tools/list-buckets.d.ts +9 -0
- package/dist/tools/list-buckets.d.ts.map +1 -0
- package/dist/tools/list-buckets.js +76 -0
- package/dist/tools/list-buckets.js.map +1 -0
- package/dist/tools/list-context.d.ts +19 -0
- package/dist/tools/list-context.d.ts.map +1 -0
- package/dist/tools/list-context.js +110 -0
- package/dist/tools/list-context.js.map +1 -0
- package/dist/tools/save-context.d.ts +36 -0
- package/dist/tools/save-context.d.ts.map +1 -0
- package/dist/tools/save-context.js +458 -0
- package/dist/tools/save-context.js.map +1 -0
- package/dist/tools/session-start.d.ts +11 -0
- package/dist/tools/session-start.d.ts.map +1 -0
- package/dist/tools/session-start.js +224 -0
- package/dist/tools/session-start.js.map +1 -0
- package/dist/types.d.ts +37 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- 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 +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 +10 -4
- package/scripts/postinstall.js +18 -25
- package/scripts/prepack.js +13 -19
- package/src/archive.ts +244 -0
- package/src/consolidation.ts +78 -0
- package/src/{error-log.js → error-log.ts} +10 -10
- package/src/helpers.ts +61 -0
- package/src/{linking.js → linking.ts} +22 -20
- package/src/migrate-dirs.ts +152 -0
- package/src/register-tools.ts +183 -0
- package/src/{server.js → server.ts} +89 -109
- package/src/{status.js → status.ts} +94 -108
- package/src/telemetry.ts +80 -0
- package/src/{temporal.js → temporal.ts} +29 -33
- package/src/tools/clear-context.ts +41 -0
- package/src/tools/{context-status.js → context-status.ts} +43 -66
- package/src/tools/{create-snapshot.js → create-snapshot.ts} +54 -65
- package/src/tools/delete-context.ts +53 -0
- package/src/tools/{get-context.js → get-context.ts} +142 -205
- package/src/tools/ingest-project.ts +260 -0
- package/src/tools/ingest-url.ts +74 -0
- package/src/tools/{list-buckets.js → list-buckets.ts} +27 -37
- package/src/tools/{list-context.js → list-context.ts} +46 -71
- package/src/tools/{save-context.js → save-context.ts} +148 -204
- package/src/tools/{session-start.js → session-start.ts} +72 -79
- package/src/types.ts +29 -0
- package/src/helpers.js +0 -57
- package/src/register-tools.js +0 -175
- package/src/telemetry.js +0 -80
- package/src/tools/clear-context.js +0 -47
- package/src/tools/delete-context.js +0 -54
- package/src/tools/ingest-project.js +0 -272
- package/src/tools/ingest-url.js +0 -87
|
@@ -1,17 +1,12 @@
|
|
|
1
|
-
import { z } from
|
|
2
|
-
import { captureAndIndex, updateEntryFile } from
|
|
3
|
-
import { indexEntry } from
|
|
4
|
-
import { categoryFor, defaultTierFor } from
|
|
5
|
-
import { normalizeKind } from
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
ensureVaultExists,
|
|
11
|
-
ensureValidKind,
|
|
12
|
-
} from "../helpers.js";
|
|
13
|
-
import { maybeShowFeedbackPrompt } from "../telemetry.js";
|
|
14
|
-
import { validateRelatedTo } from "../linking.js";
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { captureAndIndex, updateEntryFile } from '@context-vault/core/capture';
|
|
3
|
+
import { indexEntry } from '@context-vault/core/index';
|
|
4
|
+
import { categoryFor, defaultTierFor } from '@context-vault/core/categories';
|
|
5
|
+
import { normalizeKind } from '@context-vault/core/files';
|
|
6
|
+
import { ok, err, errWithHint, ensureVaultExists, ensureValidKind } from '../helpers.js';
|
|
7
|
+
import { maybeShowFeedbackPrompt } from '../telemetry.js';
|
|
8
|
+
import { validateRelatedTo } from '../linking.js';
|
|
9
|
+
import type { LocalCtx, SharedCtx, ToolResult } from '../types.js';
|
|
15
10
|
import {
|
|
16
11
|
MAX_BODY_LENGTH,
|
|
17
12
|
MAX_TITLE_LENGTH,
|
|
@@ -21,44 +16,41 @@ import {
|
|
|
21
16
|
MAX_META_LENGTH,
|
|
22
17
|
MAX_SOURCE_LENGTH,
|
|
23
18
|
MAX_IDENTITY_KEY_LENGTH,
|
|
24
|
-
} from
|
|
19
|
+
} from '@context-vault/core/constants';
|
|
25
20
|
|
|
26
21
|
const DEFAULT_SIMILARITY_THRESHOLD = 0.85;
|
|
27
22
|
const SKIP_THRESHOLD = 0.95;
|
|
28
23
|
const UPDATE_THRESHOLD = 0.85;
|
|
29
24
|
|
|
30
25
|
async function findSimilar(
|
|
31
|
-
ctx,
|
|
32
|
-
embedding,
|
|
33
|
-
threshold,
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
) {
|
|
26
|
+
ctx: LocalCtx,
|
|
27
|
+
embedding: any,
|
|
28
|
+
threshold: number,
|
|
29
|
+
{ hydrate = false } = {}
|
|
30
|
+
): Promise<any[]> {
|
|
37
31
|
try {
|
|
38
|
-
const vecCount = ctx.db
|
|
39
|
-
.prepare("SELECT COUNT(*) as c FROM vault_vec")
|
|
40
|
-
.get().c;
|
|
32
|
+
const vecCount = (ctx.db.prepare('SELECT COUNT(*) as c FROM vault_vec').get() as any)?.c ?? 0;
|
|
41
33
|
if (vecCount === 0) return [];
|
|
42
34
|
|
|
43
|
-
const vecRows = ctx.db
|
|
35
|
+
const vecRows: any[] = ctx.db
|
|
44
36
|
.prepare(
|
|
45
|
-
`SELECT v.rowid, v.distance FROM vault_vec v WHERE embedding MATCH ? ORDER BY distance LIMIT
|
|
37
|
+
`SELECT v.rowid, v.distance FROM vault_vec v WHERE embedding MATCH ? ORDER BY distance LIMIT ?`
|
|
46
38
|
)
|
|
47
|
-
.all(embedding, 10);
|
|
39
|
+
.all(embedding, 10) as any[];
|
|
48
40
|
|
|
49
41
|
if (!vecRows.length) return [];
|
|
50
42
|
|
|
51
|
-
const rowids = vecRows.map((vr) => vr.rowid);
|
|
52
|
-
const placeholders = rowids.map(() =>
|
|
43
|
+
const rowids = vecRows.map((vr: any) => vr.rowid);
|
|
44
|
+
const placeholders = rowids.map(() => '?').join(',');
|
|
53
45
|
// Local mode has no user_id column — omit it from the SELECT list.
|
|
54
|
-
const isLocal = ctx.stmts._mode ===
|
|
46
|
+
const isLocal = (ctx.stmts as any)._mode === 'local';
|
|
55
47
|
const columns = isLocal
|
|
56
48
|
? hydrate
|
|
57
|
-
?
|
|
58
|
-
:
|
|
49
|
+
? 'rowid, id, title, body, kind, tags, category, updated_at'
|
|
50
|
+
: 'rowid, id, title, category'
|
|
59
51
|
: hydrate
|
|
60
|
-
?
|
|
61
|
-
:
|
|
52
|
+
? 'rowid, id, title, body, kind, tags, category, updated_at'
|
|
53
|
+
: 'rowid, id, title, category';
|
|
62
54
|
const hydratedRows = ctx.db
|
|
63
55
|
.prepare(`SELECT ${columns} FROM vault WHERE rowid IN (${placeholders})`)
|
|
64
56
|
.all(...rowids);
|
|
@@ -68,12 +60,12 @@ async function findSimilar(
|
|
|
68
60
|
|
|
69
61
|
const results = [];
|
|
70
62
|
for (const vr of vecRows) {
|
|
71
|
-
const similarity = Math.max(0, 1 - vr.distance / 2);
|
|
63
|
+
const similarity = Math.max(0, 1 - (vr.distance as number) / 2);
|
|
72
64
|
if (similarity < threshold) continue;
|
|
73
65
|
const row = byRowid.get(vr.rowid);
|
|
74
66
|
if (!row) continue;
|
|
75
|
-
if (row.category ===
|
|
76
|
-
const entry = { id: row.id, title: row.title, score: similarity };
|
|
67
|
+
if (row.category === 'entity') continue;
|
|
68
|
+
const entry: Record<string, any> = { id: row.id, title: row.title, score: similarity };
|
|
77
69
|
if (hydrate) {
|
|
78
70
|
entry.body = row.body;
|
|
79
71
|
entry.kind = row.kind;
|
|
@@ -88,50 +80,47 @@ async function findSimilar(
|
|
|
88
80
|
}
|
|
89
81
|
}
|
|
90
82
|
|
|
91
|
-
function formatSimilarWarning(similar) {
|
|
92
|
-
const lines = [
|
|
83
|
+
function formatSimilarWarning(similar: any[]): string {
|
|
84
|
+
const lines = ['', '⚠ Similar entries already exist:'];
|
|
93
85
|
for (const e of similar) {
|
|
94
86
|
const score = e.score.toFixed(2);
|
|
95
|
-
const title = e.title ? `"${e.title}"` :
|
|
87
|
+
const title = e.title ? `"${e.title}"` : '(no title)';
|
|
96
88
|
lines.push(` - ${title} (${score}) — id: ${e.id}`);
|
|
97
89
|
}
|
|
98
|
-
lines.push(
|
|
99
|
-
|
|
100
|
-
);
|
|
101
|
-
return lines.join("\n");
|
|
90
|
+
lines.push(' Consider using `id: <existing>` in save_context to update instead.');
|
|
91
|
+
return lines.join('\n');
|
|
102
92
|
}
|
|
103
93
|
|
|
104
|
-
export function buildConflictCandidates(similarEntries) {
|
|
105
|
-
return similarEntries.map((entry) => {
|
|
94
|
+
export function buildConflictCandidates(similarEntries: any[]): any[] {
|
|
95
|
+
return similarEntries.map((entry: any) => {
|
|
106
96
|
let suggested_action;
|
|
107
97
|
let reasoning_context;
|
|
108
98
|
|
|
109
99
|
if (entry.score >= SKIP_THRESHOLD) {
|
|
110
|
-
suggested_action =
|
|
100
|
+
suggested_action = 'SKIP';
|
|
111
101
|
reasoning_context =
|
|
112
102
|
`Near-duplicate detected (${(entry.score * 100).toFixed(0)}% similarity)` +
|
|
113
|
-
`${entry.title ? ` with "${entry.title}"` :
|
|
103
|
+
`${entry.title ? ` with "${entry.title}"` : ''}. ` +
|
|
114
104
|
`Content is nearly identical — saving would create a redundant entry. ` +
|
|
115
105
|
`Use save_context with id: "${entry.id}" to update instead, or skip saving entirely.`;
|
|
116
106
|
} else if (entry.score >= UPDATE_THRESHOLD) {
|
|
117
|
-
suggested_action =
|
|
107
|
+
suggested_action = 'UPDATE';
|
|
118
108
|
reasoning_context =
|
|
119
109
|
`High content similarity (${(entry.score * 100).toFixed(0)}%)` +
|
|
120
|
-
`${entry.title ? ` with "${entry.title}"` :
|
|
110
|
+
`${entry.title ? ` with "${entry.title}"` : ''}. ` +
|
|
121
111
|
`Likely the same knowledge — consider updating this entry via save_context with id: "${entry.id}".`;
|
|
122
112
|
} else {
|
|
123
|
-
suggested_action =
|
|
113
|
+
suggested_action = 'ADD';
|
|
124
114
|
reasoning_context =
|
|
125
115
|
`Moderate similarity (${(entry.score * 100).toFixed(0)}%)` +
|
|
126
|
-
`${entry.title ? ` with "${entry.title}"` :
|
|
116
|
+
`${entry.title ? ` with "${entry.title}"` : ''}. ` +
|
|
127
117
|
`Content is related but distinct enough to coexist.`;
|
|
128
118
|
}
|
|
129
119
|
|
|
130
120
|
let parsedTags = [];
|
|
131
121
|
if (entry.tags) {
|
|
132
122
|
try {
|
|
133
|
-
parsedTags =
|
|
134
|
-
typeof entry.tags === "string" ? JSON.parse(entry.tags) : entry.tags;
|
|
123
|
+
parsedTags = typeof entry.tags === 'string' ? JSON.parse(entry.tags) : entry.tags;
|
|
135
124
|
} catch {
|
|
136
125
|
parsedTags = [];
|
|
137
126
|
}
|
|
@@ -151,16 +140,16 @@ export function buildConflictCandidates(similarEntries) {
|
|
|
151
140
|
});
|
|
152
141
|
}
|
|
153
142
|
|
|
154
|
-
function formatConflictSuggestions(candidates) {
|
|
155
|
-
const lines = [
|
|
143
|
+
function formatConflictSuggestions(candidates: any[]): string {
|
|
144
|
+
const lines = ['', '── Conflict Resolution Suggestions ──'];
|
|
156
145
|
for (const c of candidates) {
|
|
157
|
-
const titleDisplay = c.title ? `"${c.title}"` :
|
|
146
|
+
const titleDisplay = c.title ? `"${c.title}"` : '(no title)';
|
|
158
147
|
lines.push(
|
|
159
|
-
` [${c.suggested_action}] ${titleDisplay} (${(c.score * 100).toFixed(0)}%) — id: ${c.id}
|
|
148
|
+
` [${c.suggested_action}] ${titleDisplay} (${(c.score * 100).toFixed(0)}%) — id: ${c.id}`
|
|
160
149
|
);
|
|
161
150
|
lines.push(` ${c.reasoning_context}`);
|
|
162
151
|
}
|
|
163
|
-
return lines.join(
|
|
152
|
+
return lines.join('\n');
|
|
164
153
|
}
|
|
165
154
|
|
|
166
155
|
/**
|
|
@@ -175,161 +164,131 @@ function validateSaveInput({
|
|
|
175
164
|
source,
|
|
176
165
|
identity_key,
|
|
177
166
|
expires_at,
|
|
178
|
-
}) {
|
|
167
|
+
}: Record<string, any>): ToolResult | null {
|
|
179
168
|
if (kind !== undefined && kind !== null) {
|
|
180
|
-
if (typeof kind !==
|
|
181
|
-
return err(
|
|
182
|
-
`kind must be a string, max ${MAX_KIND_LENGTH} chars`,
|
|
183
|
-
"INVALID_INPUT",
|
|
184
|
-
);
|
|
169
|
+
if (typeof kind !== 'string' || kind.length > MAX_KIND_LENGTH) {
|
|
170
|
+
return err(`kind must be a string, max ${MAX_KIND_LENGTH} chars`, 'INVALID_INPUT');
|
|
185
171
|
}
|
|
186
172
|
}
|
|
187
173
|
if (body !== undefined && body !== null) {
|
|
188
|
-
if (typeof body !==
|
|
189
|
-
return err(
|
|
190
|
-
`body must be a string, max ${MAX_BODY_LENGTH / 1024}KB`,
|
|
191
|
-
"INVALID_INPUT",
|
|
192
|
-
);
|
|
174
|
+
if (typeof body !== 'string' || body.length > MAX_BODY_LENGTH) {
|
|
175
|
+
return err(`body must be a string, max ${MAX_BODY_LENGTH / 1024}KB`, 'INVALID_INPUT');
|
|
193
176
|
}
|
|
194
177
|
}
|
|
195
178
|
if (title !== undefined && title !== null) {
|
|
196
|
-
if (typeof title !==
|
|
197
|
-
return err(
|
|
198
|
-
`title must be a string, max ${MAX_TITLE_LENGTH} chars`,
|
|
199
|
-
"INVALID_INPUT",
|
|
200
|
-
);
|
|
179
|
+
if (typeof title !== 'string' || title.length > MAX_TITLE_LENGTH) {
|
|
180
|
+
return err(`title must be a string, max ${MAX_TITLE_LENGTH} chars`, 'INVALID_INPUT');
|
|
201
181
|
}
|
|
202
182
|
}
|
|
203
183
|
if (tags !== undefined && tags !== null) {
|
|
204
|
-
if (!Array.isArray(tags))
|
|
205
|
-
return err("tags must be an array of strings", "INVALID_INPUT");
|
|
184
|
+
if (!Array.isArray(tags)) return err('tags must be an array of strings', 'INVALID_INPUT');
|
|
206
185
|
if (tags.length > MAX_TAGS_COUNT)
|
|
207
|
-
return err(`tags: max ${MAX_TAGS_COUNT} tags allowed`,
|
|
186
|
+
return err(`tags: max ${MAX_TAGS_COUNT} tags allowed`, 'INVALID_INPUT');
|
|
208
187
|
for (const tag of tags) {
|
|
209
|
-
if (typeof tag !==
|
|
210
|
-
return err(
|
|
211
|
-
`each tag must be a string, max ${MAX_TAG_LENGTH} chars`,
|
|
212
|
-
"INVALID_INPUT",
|
|
213
|
-
);
|
|
188
|
+
if (typeof tag !== 'string' || tag.length > MAX_TAG_LENGTH) {
|
|
189
|
+
return err(`each tag must be a string, max ${MAX_TAG_LENGTH} chars`, 'INVALID_INPUT');
|
|
214
190
|
}
|
|
215
191
|
}
|
|
216
192
|
}
|
|
217
193
|
if (meta !== undefined && meta !== null) {
|
|
218
194
|
const metaStr = JSON.stringify(meta);
|
|
219
195
|
if (metaStr.length > MAX_META_LENGTH) {
|
|
220
|
-
return err(
|
|
221
|
-
`meta must be under ${MAX_META_LENGTH / 1024}KB when serialized`,
|
|
222
|
-
"INVALID_INPUT",
|
|
223
|
-
);
|
|
196
|
+
return err(`meta must be under ${MAX_META_LENGTH / 1024}KB when serialized`, 'INVALID_INPUT');
|
|
224
197
|
}
|
|
225
198
|
}
|
|
226
199
|
if (source !== undefined && source !== null) {
|
|
227
|
-
if (typeof source !==
|
|
228
|
-
return err(
|
|
229
|
-
`source must be a string, max ${MAX_SOURCE_LENGTH} chars`,
|
|
230
|
-
"INVALID_INPUT",
|
|
231
|
-
);
|
|
200
|
+
if (typeof source !== 'string' || source.length > MAX_SOURCE_LENGTH) {
|
|
201
|
+
return err(`source must be a string, max ${MAX_SOURCE_LENGTH} chars`, 'INVALID_INPUT');
|
|
232
202
|
}
|
|
233
203
|
}
|
|
234
204
|
if (identity_key !== undefined && identity_key !== null) {
|
|
235
|
-
if (
|
|
236
|
-
typeof identity_key !== "string" ||
|
|
237
|
-
identity_key.length > MAX_IDENTITY_KEY_LENGTH
|
|
238
|
-
) {
|
|
205
|
+
if (typeof identity_key !== 'string' || identity_key.length > MAX_IDENTITY_KEY_LENGTH) {
|
|
239
206
|
return err(
|
|
240
207
|
`identity_key must be a string, max ${MAX_IDENTITY_KEY_LENGTH} chars`,
|
|
241
|
-
|
|
208
|
+
'INVALID_INPUT'
|
|
242
209
|
);
|
|
243
210
|
}
|
|
244
211
|
}
|
|
245
212
|
if (expires_at !== undefined && expires_at !== null) {
|
|
246
|
-
if (
|
|
247
|
-
|
|
248
|
-
isNaN(new Date(expires_at).getTime())
|
|
249
|
-
) {
|
|
250
|
-
return err("expires_at must be a valid ISO date string", "INVALID_INPUT");
|
|
213
|
+
if (typeof expires_at !== 'string' || isNaN(new Date(expires_at).getTime())) {
|
|
214
|
+
return err('expires_at must be a valid ISO date string', 'INVALID_INPUT');
|
|
251
215
|
}
|
|
252
216
|
}
|
|
253
217
|
return null;
|
|
254
218
|
}
|
|
255
219
|
|
|
256
|
-
export const name =
|
|
220
|
+
export const name = 'save_context';
|
|
257
221
|
|
|
258
222
|
export const description =
|
|
259
|
-
|
|
223
|
+
'Save knowledge to your vault. Creates a .md file and indexes it for search. Use for any kind of context: insights, decisions, patterns, references, or any custom kind. To update an existing entry, pass its `id` — omitted fields are preserved.';
|
|
260
224
|
|
|
261
225
|
export const inputSchema = {
|
|
262
226
|
id: z
|
|
263
227
|
.string()
|
|
264
228
|
.optional()
|
|
265
229
|
.describe(
|
|
266
|
-
|
|
230
|
+
'Entry ULID to update. When provided, updates the existing entry instead of creating new. Omitted fields are preserved.'
|
|
267
231
|
),
|
|
268
232
|
kind: z
|
|
269
233
|
.string()
|
|
270
234
|
.optional()
|
|
271
235
|
.describe(
|
|
272
|
-
"Entry kind — determines folder (e.g. 'insight', 'decision', 'pattern', 'reference', or any custom kind). Required for new entries."
|
|
236
|
+
"Entry kind — determines folder (e.g. 'insight', 'decision', 'pattern', 'reference', or any custom kind). Required for new entries."
|
|
273
237
|
),
|
|
274
|
-
title: z.string().optional().describe(
|
|
275
|
-
body: z
|
|
276
|
-
.string()
|
|
277
|
-
.optional()
|
|
278
|
-
.describe("Main content. Required for new entries."),
|
|
238
|
+
title: z.string().optional().describe('Entry title (optional for insights)'),
|
|
239
|
+
body: z.string().optional().describe('Main content. Required for new entries.'),
|
|
279
240
|
tags: z
|
|
280
241
|
.array(z.string())
|
|
281
242
|
.optional()
|
|
282
243
|
.describe(
|
|
283
|
-
"Tags for categorization and search. Use 'bucket:' prefix for project/domain scoping (e.g., 'bucket:autohub') to enable project-scoped retrieval."
|
|
244
|
+
"Tags for categorization and search. Use 'bucket:' prefix for project/domain scoping (e.g., 'bucket:autohub') to enable project-scoped retrieval."
|
|
284
245
|
),
|
|
285
246
|
meta: z
|
|
286
247
|
.any()
|
|
287
248
|
.optional()
|
|
288
249
|
.describe(
|
|
289
|
-
"Additional structured metadata (JSON object, e.g. { language: 'js', status: 'accepted' })"
|
|
250
|
+
"Additional structured metadata (JSON object, e.g. { language: 'js', status: 'accepted' })"
|
|
290
251
|
),
|
|
291
252
|
folder: z
|
|
292
253
|
.string()
|
|
293
254
|
.optional()
|
|
294
255
|
.describe("Subfolder within the kind directory (e.g. 'react/hooks')"),
|
|
295
|
-
source: z.string().optional().describe(
|
|
256
|
+
source: z.string().optional().describe('Where this knowledge came from'),
|
|
296
257
|
identity_key: z
|
|
297
258
|
.string()
|
|
298
259
|
.optional()
|
|
299
260
|
.describe(
|
|
300
|
-
|
|
261
|
+
'Required for entity kinds (contact, project, tool, source). The unique identifier for this entity.'
|
|
301
262
|
),
|
|
302
|
-
expires_at: z.string().optional().describe(
|
|
263
|
+
expires_at: z.string().optional().describe('ISO date for TTL expiry'),
|
|
303
264
|
supersedes: z
|
|
304
265
|
.array(z.string())
|
|
305
266
|
.optional()
|
|
306
267
|
.describe(
|
|
307
|
-
|
|
268
|
+
'Array of entry IDs that this entry supersedes/replaces. Those entries will be marked with superseded_by pointing to this new entry and excluded from future search results by default.'
|
|
308
269
|
),
|
|
309
270
|
related_to: z
|
|
310
271
|
.array(z.string())
|
|
311
272
|
.optional()
|
|
312
273
|
.describe(
|
|
313
|
-
|
|
274
|
+
'Array of entry IDs this entry is related to. Enables bidirectional graph traversal — use get_context with follow_links:true to retrieve linked entries.'
|
|
314
275
|
),
|
|
315
276
|
source_files: z
|
|
316
277
|
.array(
|
|
317
278
|
z.object({
|
|
318
|
-
path: z.string().describe(
|
|
319
|
-
hash: z
|
|
320
|
-
|
|
321
|
-
.describe("SHA-256 hash of the file contents at observation time"),
|
|
322
|
-
}),
|
|
279
|
+
path: z.string().describe('File path (absolute or relative to cwd)'),
|
|
280
|
+
hash: z.string().describe('SHA-256 hash of the file contents at observation time'),
|
|
281
|
+
})
|
|
323
282
|
)
|
|
324
283
|
.optional()
|
|
325
284
|
.describe(
|
|
326
|
-
|
|
285
|
+
'Source code files this entry is derived from. When these files change (hash mismatch), the entry will be flagged as stale in get_context results.'
|
|
327
286
|
),
|
|
328
287
|
dry_run: z
|
|
329
288
|
.boolean()
|
|
330
289
|
.optional()
|
|
331
290
|
.describe(
|
|
332
|
-
|
|
291
|
+
'If true, check for similar entries without saving. Returns similarity results without creating a new entry. Only applies to knowledge and event categories.'
|
|
333
292
|
),
|
|
334
293
|
similarity_threshold: z
|
|
335
294
|
.number()
|
|
@@ -337,27 +296,22 @@ export const inputSchema = {
|
|
|
337
296
|
.max(1)
|
|
338
297
|
.optional()
|
|
339
298
|
.describe(
|
|
340
|
-
|
|
299
|
+
'Cosine similarity threshold for duplicate detection (0–1, default 0.85). Entries above this score are flagged as similar. Only applies to knowledge and event categories.'
|
|
341
300
|
),
|
|
342
301
|
tier: z
|
|
343
|
-
.enum([
|
|
302
|
+
.enum(['ephemeral', 'working', 'durable'])
|
|
344
303
|
.optional()
|
|
345
304
|
.describe(
|
|
346
|
-
"Memory tier for lifecycle management. 'ephemeral': short-lived session data. 'working': active context (default). 'durable': long-term reference material. Defaults based on kind when not specified."
|
|
305
|
+
"Memory tier for lifecycle management. 'ephemeral': short-lived session data. 'working': active context (default). 'durable': long-term reference material. Defaults based on kind when not specified."
|
|
347
306
|
),
|
|
348
307
|
conflict_resolution: z
|
|
349
|
-
.enum([
|
|
308
|
+
.enum(['suggest', 'off'])
|
|
350
309
|
.optional()
|
|
351
310
|
.describe(
|
|
352
|
-
'Conflict resolution mode. "suggest" (default): when similar entries are found, return structured conflict_candidates with suggested_action (ADD/UPDATE/SKIP) and reasoning_context for the calling agent to decide. Thresholds: score > 0.95 → SKIP (near-duplicate), score > 0.85 → UPDATE (very similar), score < 0.85 → ADD (distinct enough). "off": flag similar entries only (legacy behavior).'
|
|
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).'
|
|
353
312
|
),
|
|
354
313
|
};
|
|
355
314
|
|
|
356
|
-
/**
|
|
357
|
-
* @param {object} args
|
|
358
|
-
* @param {import('../types.js').BaseCtx & Partial<import('../types.js').HostedCtxExtensions>} ctx
|
|
359
|
-
* @param {import('../types.js').ToolShared} shared
|
|
360
|
-
*/
|
|
361
315
|
export async function handler(
|
|
362
316
|
{
|
|
363
317
|
id,
|
|
@@ -377,18 +331,18 @@ export async function handler(
|
|
|
377
331
|
similarity_threshold,
|
|
378
332
|
tier,
|
|
379
333
|
conflict_resolution,
|
|
380
|
-
},
|
|
381
|
-
ctx,
|
|
382
|
-
{ ensureIndexed }
|
|
383
|
-
) {
|
|
334
|
+
}: Record<string, any>,
|
|
335
|
+
ctx: LocalCtx,
|
|
336
|
+
{ ensureIndexed }: SharedCtx
|
|
337
|
+
): Promise<ToolResult> {
|
|
384
338
|
const { config } = ctx;
|
|
385
|
-
const suggestMode = conflict_resolution !==
|
|
339
|
+
const suggestMode = conflict_resolution !== 'off';
|
|
386
340
|
|
|
387
341
|
const vaultErr = ensureVaultExists(config);
|
|
388
342
|
if (vaultErr) return vaultErr;
|
|
389
343
|
|
|
390
344
|
const relatedToErr = validateRelatedTo(related_to);
|
|
391
|
-
if (relatedToErr) return err(relatedToErr,
|
|
345
|
+
if (relatedToErr) return err(relatedToErr, 'INVALID_INPUT');
|
|
392
346
|
|
|
393
347
|
const inputErr = validateSaveInput({
|
|
394
348
|
kind,
|
|
@@ -404,32 +358,24 @@ export async function handler(
|
|
|
404
358
|
|
|
405
359
|
// ── Update mode ──
|
|
406
360
|
if (id) {
|
|
407
|
-
await ensureIndexed();
|
|
361
|
+
await ensureIndexed({ blocking: false });
|
|
408
362
|
|
|
409
363
|
const existing = ctx.stmts.getEntryById.get(id);
|
|
410
|
-
if (!existing) return err(`Entry not found: ${id}`,
|
|
364
|
+
if (!existing) return err(`Entry not found: ${id}`, 'NOT_FOUND');
|
|
411
365
|
|
|
412
366
|
if (kind && normalizeKind(kind) !== existing.kind) {
|
|
413
367
|
return err(
|
|
414
368
|
`Cannot change kind (current: "${existing.kind}"). Delete and re-create instead.`,
|
|
415
|
-
|
|
369
|
+
'INVALID_UPDATE'
|
|
416
370
|
);
|
|
417
371
|
}
|
|
418
372
|
if (identity_key && identity_key !== existing.identity_key) {
|
|
419
373
|
return err(
|
|
420
374
|
`Cannot change identity_key (current: "${existing.identity_key}"). Delete and re-create instead.`,
|
|
421
|
-
|
|
375
|
+
'INVALID_UPDATE'
|
|
422
376
|
);
|
|
423
377
|
}
|
|
424
378
|
|
|
425
|
-
// Decrypt existing entry before merge if encrypted
|
|
426
|
-
if (ctx.decrypt && existing.body_encrypted) {
|
|
427
|
-
const decrypted = await ctx.decrypt(existing);
|
|
428
|
-
existing.body = decrypted.body;
|
|
429
|
-
if (decrypted.title) existing.title = decrypted.title;
|
|
430
|
-
if (decrypted.meta) existing.meta = JSON.stringify(decrypted.meta);
|
|
431
|
-
}
|
|
432
|
-
|
|
433
379
|
let entry;
|
|
434
380
|
try {
|
|
435
381
|
entry = updateEntryFile(ctx, existing, {
|
|
@@ -446,9 +392,9 @@ export async function handler(
|
|
|
446
392
|
await indexEntry(ctx, entry);
|
|
447
393
|
} catch (e) {
|
|
448
394
|
return errWithHint(
|
|
449
|
-
e.message,
|
|
450
|
-
|
|
451
|
-
|
|
395
|
+
e instanceof Error ? e.message : String(e),
|
|
396
|
+
'UPDATE_FAILED',
|
|
397
|
+
'context-vault save_context update is failing. Check `cat ~/.context-mcp/error.log | tail -5` and help me debug.'
|
|
452
398
|
);
|
|
453
399
|
}
|
|
454
400
|
if (entry.related_to?.length && ctx.stmts.updateRelatedTo) {
|
|
@@ -457,43 +403,41 @@ export async function handler(
|
|
|
457
403
|
ctx.stmts.updateRelatedTo.run(null, entry.id);
|
|
458
404
|
}
|
|
459
405
|
const relPath = entry.filePath
|
|
460
|
-
? entry.filePath.replace(config.vaultDir +
|
|
406
|
+
? entry.filePath.replace(config.vaultDir + '/', '')
|
|
461
407
|
: entry.filePath;
|
|
462
408
|
const parts = [`✓ Updated ${entry.kind} → ${relPath}`, ` id: ${entry.id}`];
|
|
463
409
|
if (entry.title) parts.push(` title: ${entry.title}`);
|
|
464
410
|
const entryTags = entry.tags || [];
|
|
465
|
-
if (entryTags.length) parts.push(` tags: ${entryTags.join(
|
|
466
|
-
parts.push(
|
|
467
|
-
return ok(parts.join(
|
|
411
|
+
if (entryTags.length) parts.push(` tags: ${entryTags.join(', ')}`);
|
|
412
|
+
parts.push('', '_Search with get_context to verify changes._');
|
|
413
|
+
return ok(parts.join('\n'));
|
|
468
414
|
}
|
|
469
415
|
|
|
470
416
|
// ── Create mode ──
|
|
471
|
-
if (!kind) return err(
|
|
417
|
+
if (!kind) return err('Required: kind (for new entries)', 'INVALID_INPUT');
|
|
472
418
|
const kindErr = ensureValidKind(kind);
|
|
473
419
|
if (kindErr) return kindErr;
|
|
474
|
-
if (!body?.trim())
|
|
475
|
-
return err("Required: body (for new entries)", "INVALID_INPUT");
|
|
420
|
+
if (!body?.trim()) return err('Required: body (for new entries)', 'INVALID_INPUT');
|
|
476
421
|
|
|
477
422
|
// Normalize kind to canonical singular form (e.g. "insights" → "insight")
|
|
478
423
|
const normalizedKind = normalizeKind(kind);
|
|
479
424
|
|
|
480
|
-
if (categoryFor(normalizedKind) ===
|
|
481
|
-
return err(
|
|
482
|
-
`Entity kind "${normalizedKind}" requires identity_key`,
|
|
483
|
-
"MISSING_IDENTITY_KEY",
|
|
484
|
-
);
|
|
425
|
+
if (categoryFor(normalizedKind) === 'entity' && !identity_key) {
|
|
426
|
+
return err(`Entity kind "${normalizedKind}" requires identity_key`, 'MISSING_IDENTITY_KEY');
|
|
485
427
|
}
|
|
486
428
|
|
|
487
|
-
|
|
429
|
+
// Start reindex in background but don't wait — similarity check
|
|
430
|
+
// may miss unindexed entries, but the save won't time out
|
|
431
|
+
await ensureIndexed({ blocking: false });
|
|
488
432
|
|
|
489
433
|
// ── Similarity check (knowledge + event only) ────────────────────────────
|
|
490
434
|
const category = categoryFor(normalizedKind);
|
|
491
|
-
let similarEntries = [];
|
|
492
|
-
let queryEmbedding = null;
|
|
435
|
+
let similarEntries: any[] = [];
|
|
436
|
+
let queryEmbedding: any = null;
|
|
493
437
|
|
|
494
|
-
if (category ===
|
|
438
|
+
if (category === 'knowledge' || category === 'event') {
|
|
495
439
|
const threshold = similarity_threshold ?? DEFAULT_SIMILARITY_THRESHOLD;
|
|
496
|
-
const embeddingText = [title, body].filter(Boolean).join(
|
|
440
|
+
const embeddingText = [title, body].filter(Boolean).join(' ');
|
|
497
441
|
try {
|
|
498
442
|
queryEmbedding = await ctx.embed(embeddingText);
|
|
499
443
|
} catch {
|
|
@@ -505,43 +449,43 @@ export async function handler(
|
|
|
505
449
|
queryEmbedding,
|
|
506
450
|
threshold,
|
|
507
451
|
|
|
508
|
-
{ hydrate: suggestMode }
|
|
452
|
+
{ hydrate: suggestMode }
|
|
509
453
|
);
|
|
510
454
|
}
|
|
511
455
|
}
|
|
512
456
|
|
|
513
457
|
if (dry_run) {
|
|
514
|
-
const parts = [
|
|
458
|
+
const parts = ['(dry run — nothing saved)'];
|
|
515
459
|
if (similarEntries.length) {
|
|
516
460
|
if (suggestMode) {
|
|
517
461
|
const candidates = buildConflictCandidates(similarEntries);
|
|
518
|
-
parts.push(
|
|
462
|
+
parts.push('', '⚠ Similar entries already exist:');
|
|
519
463
|
for (const e of similarEntries) {
|
|
520
464
|
const score = e.score.toFixed(2);
|
|
521
|
-
const titleDisplay = e.title ? `"${e.title}"` :
|
|
465
|
+
const titleDisplay = e.title ? `"${e.title}"` : '(no title)';
|
|
522
466
|
parts.push(` - ${titleDisplay} (${score}) — id: ${e.id}`);
|
|
523
467
|
}
|
|
524
468
|
parts.push(formatConflictSuggestions(candidates));
|
|
525
469
|
parts.push(
|
|
526
|
-
|
|
527
|
-
|
|
470
|
+
'',
|
|
471
|
+
'Use save_context with `id: <existing>` to update one, or omit `dry_run` to save as new.'
|
|
528
472
|
);
|
|
529
473
|
} else {
|
|
530
|
-
parts.push(
|
|
474
|
+
parts.push('', '⚠ Similar entries already exist:');
|
|
531
475
|
for (const e of similarEntries) {
|
|
532
476
|
const score = e.score.toFixed(2);
|
|
533
|
-
const titleDisplay = e.title ? `"${e.title}"` :
|
|
477
|
+
const titleDisplay = e.title ? `"${e.title}"` : '(no title)';
|
|
534
478
|
parts.push(` - ${titleDisplay} (${score}) — id: ${e.id}`);
|
|
535
479
|
}
|
|
536
480
|
parts.push(
|
|
537
|
-
|
|
538
|
-
|
|
481
|
+
'',
|
|
482
|
+
'Use save_context with `id: <existing>` to update one, or omit `dry_run` to save as new.'
|
|
539
483
|
);
|
|
540
484
|
}
|
|
541
485
|
} else {
|
|
542
|
-
parts.push(
|
|
486
|
+
parts.push('', 'No similar entries found. Safe to save.');
|
|
543
487
|
}
|
|
544
|
-
return ok(parts.join(
|
|
488
|
+
return ok(parts.join('\n'));
|
|
545
489
|
}
|
|
546
490
|
|
|
547
491
|
const mergedMeta = { ...(meta || {}) };
|
|
@@ -550,7 +494,7 @@ export async function handler(
|
|
|
550
494
|
|
|
551
495
|
const effectiveTier = tier ?? defaultTierFor(normalizedKind);
|
|
552
496
|
|
|
553
|
-
const embeddingToReuse = category ===
|
|
497
|
+
const embeddingToReuse = category === 'knowledge' ? queryEmbedding : null;
|
|
554
498
|
|
|
555
499
|
let entry;
|
|
556
500
|
try {
|
|
@@ -572,13 +516,13 @@ export async function handler(
|
|
|
572
516
|
|
|
573
517
|
tier: effectiveTier,
|
|
574
518
|
},
|
|
575
|
-
embeddingToReuse
|
|
519
|
+
embeddingToReuse
|
|
576
520
|
);
|
|
577
521
|
} catch (e) {
|
|
578
522
|
return errWithHint(
|
|
579
|
-
e.message,
|
|
580
|
-
|
|
581
|
-
|
|
523
|
+
e instanceof Error ? e.message : String(e),
|
|
524
|
+
'SAVE_FAILED',
|
|
525
|
+
'context-vault save_context is failing. Check `cat ~/.context-mcp/error.log | tail -5` and help me debug.'
|
|
582
526
|
);
|
|
583
527
|
}
|
|
584
528
|
|
|
@@ -587,37 +531,37 @@ export async function handler(
|
|
|
587
531
|
}
|
|
588
532
|
|
|
589
533
|
const relPath = entry.filePath
|
|
590
|
-
? entry.filePath.replace(config.vaultDir +
|
|
534
|
+
? entry.filePath.replace(config.vaultDir + '/', '')
|
|
591
535
|
: entry.filePath;
|
|
592
536
|
const parts = [`✓ Saved ${normalizedKind} → ${relPath}`, ` id: ${entry.id}`];
|
|
593
537
|
if (title) parts.push(` title: ${title}`);
|
|
594
|
-
if (tags?.length) parts.push(` tags: ${tags.join(
|
|
538
|
+
if (tags?.length) parts.push(` tags: ${tags.join(', ')}`);
|
|
595
539
|
parts.push(` tier: ${effectiveTier}`);
|
|
596
|
-
parts.push(
|
|
540
|
+
parts.push('', '_Use this id to update or delete later._');
|
|
597
541
|
const hasBucketTag = (tags || []).some(
|
|
598
|
-
(t) => typeof t ===
|
|
542
|
+
(t: any) => typeof t === 'string' && t.startsWith('bucket:')
|
|
599
543
|
);
|
|
600
544
|
if (tags && tags.length > 0 && !hasBucketTag) {
|
|
601
545
|
parts.push(
|
|
602
|
-
|
|
603
|
-
|
|
546
|
+
'',
|
|
547
|
+
'_Tip: Consider adding a `bucket:` tag (e.g., `bucket:myproject`) for project-scoped retrieval._'
|
|
604
548
|
);
|
|
605
549
|
}
|
|
606
550
|
const bucketTags = (tags || []).filter(
|
|
607
|
-
(t) => typeof t ===
|
|
551
|
+
(t: any) => typeof t === 'string' && t.startsWith('bucket:')
|
|
608
552
|
);
|
|
609
553
|
for (const bt of bucketTags) {
|
|
610
|
-
const bucketUserClause =
|
|
554
|
+
const bucketUserClause = '';
|
|
611
555
|
const bucketParams = false ? [bt] : [bt];
|
|
612
556
|
const exists = ctx.db
|
|
613
557
|
.prepare(
|
|
614
|
-
`SELECT 1 FROM vault WHERE kind = 'bucket' AND identity_key = ? ${bucketUserClause} LIMIT 1
|
|
558
|
+
`SELECT 1 FROM vault WHERE kind = 'bucket' AND identity_key = ? ${bucketUserClause} LIMIT 1`
|
|
615
559
|
)
|
|
616
560
|
.get(...bucketParams);
|
|
617
561
|
if (!exists) {
|
|
618
562
|
parts.push(
|
|
619
563
|
``,
|
|
620
|
-
`_Note: bucket '${bt}' is not registered. Use save_context(kind: "bucket", identity_key: "${bt}") to register it._
|
|
564
|
+
`_Note: bucket '${bt}' is not registered. Use save_context(kind: "bucket", identity_key: "${bt}") to register it._`
|
|
621
565
|
);
|
|
622
566
|
}
|
|
623
567
|
}
|
|
@@ -634,15 +578,15 @@ export async function handler(
|
|
|
634
578
|
const criticalLimit = config.thresholds?.totalEntries?.critical;
|
|
635
579
|
if (criticalLimit != null) {
|
|
636
580
|
try {
|
|
637
|
-
const countRow = ctx.db.prepare(
|
|
638
|
-
if (countRow.c >= criticalLimit) {
|
|
581
|
+
const countRow = ctx.db.prepare('SELECT COUNT(*) as c FROM vault').get() as any;
|
|
582
|
+
if (countRow?.c != null && countRow.c >= criticalLimit) {
|
|
639
583
|
parts.push(
|
|
640
584
|
``,
|
|
641
|
-
`ℹ Vault has ${countRow.c.toLocaleString()} entries. Consider running \`context-vault reindex\` or reviewing old entries
|
|
585
|
+
`ℹ Vault has ${countRow.c.toLocaleString()} entries. Consider running \`context-vault reindex\` or reviewing old entries.`
|
|
642
586
|
);
|
|
643
587
|
}
|
|
644
588
|
} catch {}
|
|
645
589
|
}
|
|
646
590
|
|
|
647
|
-
return ok(parts.join(
|
|
591
|
+
return ok(parts.join('\n'));
|
|
648
592
|
}
|