context-vault 2.7.1 → 2.8.3
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/README.md +39 -375
- package/bin/cli.js +373 -230
- package/node_modules/@context-vault/core/package.json +7 -3
- package/node_modules/@context-vault/core/src/capture/file-ops.js +20 -2
- package/node_modules/@context-vault/core/src/capture/import-pipeline.js +11 -50
- package/node_modules/@context-vault/core/src/capture/importers.js +64 -37
- package/node_modules/@context-vault/core/src/capture/index.js +57 -15
- package/node_modules/@context-vault/core/src/capture/ingest-url.js +80 -44
- package/node_modules/@context-vault/core/src/constants.js +8 -0
- package/node_modules/@context-vault/core/src/core/config.js +65 -29
- package/node_modules/@context-vault/core/src/core/files.js +8 -15
- package/node_modules/@context-vault/core/src/core/frontmatter.js +22 -10
- package/node_modules/@context-vault/core/src/core/status.js +32 -15
- package/node_modules/@context-vault/core/src/index/db.js +47 -34
- package/node_modules/@context-vault/core/src/index/embed.js +15 -5
- package/node_modules/@context-vault/core/src/index/index.js +206 -52
- package/node_modules/@context-vault/core/src/index.js +39 -6
- package/node_modules/@context-vault/core/src/retrieve/index.js +40 -8
- package/node_modules/@context-vault/core/src/server/helpers.js +8 -6
- package/node_modules/@context-vault/core/src/server/tools/context-status.js +24 -10
- package/node_modules/@context-vault/core/src/server/tools/delete-context.js +8 -3
- package/node_modules/@context-vault/core/src/server/tools/get-context.js +117 -35
- package/node_modules/@context-vault/core/src/server/tools/ingest-url.js +34 -15
- package/node_modules/@context-vault/core/src/server/tools/list-context.js +59 -18
- package/node_modules/@context-vault/core/src/server/tools/save-context.js +164 -40
- package/node_modules/@context-vault/core/src/server/tools/submit-feedback.js +24 -18
- package/node_modules/@context-vault/core/src/server/tools.js +24 -28
- package/node_modules/@context-vault/core/src/sync/sync.js +24 -19
- package/package.json +2 -2
- package/scripts/local-server.js +334 -122
- package/scripts/postinstall.js +25 -10
- package/scripts/prepack.js +18 -15
- package/src/server/index.js +78 -29
- package/app-dist/assets/index-DjXoWapE.css +0 -1
- package/app-dist/assets/index-R4n9Qz4U.js +0 -380
- package/app-dist/index.html +0 -16
- package/node_modules/@context-vault/core/src/server/types.js +0 -78
|
@@ -19,7 +19,8 @@ export const inputSchema = {
|
|
|
19
19
|
export async function handler({ id }, ctx, { ensureIndexed }) {
|
|
20
20
|
const userId = ctx.userId !== undefined ? ctx.userId : undefined;
|
|
21
21
|
|
|
22
|
-
if (!id?.trim())
|
|
22
|
+
if (!id?.trim())
|
|
23
|
+
return err("Required: id (non-empty string)", "INVALID_INPUT");
|
|
23
24
|
await ensureIndexed();
|
|
24
25
|
|
|
25
26
|
const entry = ctx.stmts.getEntryById.get(id);
|
|
@@ -32,13 +33,17 @@ export async function handler({ id }, ctx, { ensureIndexed }) {
|
|
|
32
33
|
|
|
33
34
|
// Delete file from disk first (source of truth)
|
|
34
35
|
if (entry.file_path) {
|
|
35
|
-
try {
|
|
36
|
+
try {
|
|
37
|
+
unlinkSync(entry.file_path);
|
|
38
|
+
} catch {}
|
|
36
39
|
}
|
|
37
40
|
|
|
38
41
|
// Delete vector embedding
|
|
39
42
|
const rowidResult = ctx.stmts.getRowid.get(id);
|
|
40
43
|
if (rowidResult?.rowid) {
|
|
41
|
-
try {
|
|
44
|
+
try {
|
|
45
|
+
ctx.deleteVec(Number(rowidResult.rowid));
|
|
46
|
+
} catch {}
|
|
42
47
|
}
|
|
43
48
|
|
|
44
49
|
// Delete DB row (FTS trigger handles FTS cleanup)
|
|
@@ -11,13 +11,36 @@ export const description =
|
|
|
11
11
|
"Search your knowledge vault. Returns entries ranked by relevance using hybrid full-text + semantic search. Use this to find insights, decisions, patterns, or any saved context. Each result includes an `id` you can use with save_context or delete_context.";
|
|
12
12
|
|
|
13
13
|
export const inputSchema = {
|
|
14
|
-
query: z
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
14
|
+
query: z
|
|
15
|
+
.string()
|
|
16
|
+
.optional()
|
|
17
|
+
.describe(
|
|
18
|
+
"Search query (natural language or keywords). Optional if filters (tags, kind, category) are provided.",
|
|
19
|
+
),
|
|
20
|
+
kind: z
|
|
21
|
+
.string()
|
|
22
|
+
.optional()
|
|
23
|
+
.describe("Filter by kind (e.g. 'insight', 'decision', 'pattern')"),
|
|
24
|
+
category: z
|
|
25
|
+
.enum(["knowledge", "entity", "event"])
|
|
26
|
+
.optional()
|
|
27
|
+
.describe("Filter by category"),
|
|
28
|
+
identity_key: z
|
|
29
|
+
.string()
|
|
30
|
+
.optional()
|
|
31
|
+
.describe("For entity lookup: exact match on identity key. Requires kind."),
|
|
32
|
+
tags: z
|
|
33
|
+
.array(z.string())
|
|
34
|
+
.optional()
|
|
35
|
+
.describe("Filter by tags (entries must match at least one)"),
|
|
36
|
+
since: z
|
|
37
|
+
.string()
|
|
38
|
+
.optional()
|
|
39
|
+
.describe("ISO date, return entries created after this"),
|
|
40
|
+
until: z
|
|
41
|
+
.string()
|
|
42
|
+
.optional()
|
|
43
|
+
.describe("ISO date, return entries created before this"),
|
|
21
44
|
limit: z.number().optional().describe("Max results to return (default 10)"),
|
|
22
45
|
};
|
|
23
46
|
|
|
@@ -26,25 +49,42 @@ export const inputSchema = {
|
|
|
26
49
|
* @param {import('../types.js').BaseCtx & Partial<import('../types.js').HostedCtxExtensions>} ctx
|
|
27
50
|
* @param {import('../types.js').ToolShared} shared
|
|
28
51
|
*/
|
|
29
|
-
export async function handler(
|
|
52
|
+
export async function handler(
|
|
53
|
+
{ query, kind, category, identity_key, tags, since, until, limit },
|
|
54
|
+
ctx,
|
|
55
|
+
{ ensureIndexed, reindexFailed },
|
|
56
|
+
) {
|
|
30
57
|
const { config } = ctx;
|
|
31
58
|
const userId = ctx.userId !== undefined ? ctx.userId : undefined;
|
|
32
59
|
|
|
33
60
|
const hasQuery = query?.trim();
|
|
34
|
-
const hasFilters =
|
|
35
|
-
|
|
61
|
+
const hasFilters =
|
|
62
|
+
kind || category || tags?.length || since || until || identity_key;
|
|
63
|
+
if (!hasQuery && !hasFilters)
|
|
64
|
+
return err(
|
|
65
|
+
"Required: query or at least one filter (kind, category, tags, since, until, identity_key)",
|
|
66
|
+
"INVALID_INPUT",
|
|
67
|
+
);
|
|
36
68
|
await ensureIndexed();
|
|
37
69
|
|
|
38
70
|
const kindFilter = kind ? normalizeKind(kind) : null;
|
|
39
71
|
|
|
40
72
|
// Gap 1: Entity exact-match by identity_key
|
|
41
73
|
if (identity_key) {
|
|
42
|
-
if (!kindFilter)
|
|
43
|
-
|
|
74
|
+
if (!kindFilter)
|
|
75
|
+
return err("identity_key requires kind to be specified", "INVALID_INPUT");
|
|
76
|
+
const match = ctx.stmts.getByIdentityKey.get(
|
|
77
|
+
kindFilter,
|
|
78
|
+
identity_key,
|
|
79
|
+
userId !== undefined ? userId : null,
|
|
80
|
+
);
|
|
44
81
|
if (match) {
|
|
45
82
|
const entryTags = match.tags ? JSON.parse(match.tags) : [];
|
|
46
83
|
const tagStr = entryTags.length ? entryTags.join(", ") : "none";
|
|
47
|
-
const relPath =
|
|
84
|
+
const relPath =
|
|
85
|
+
match.file_path && config.vaultDir
|
|
86
|
+
? match.file_path.replace(config.vaultDir + "/", "")
|
|
87
|
+
: match.file_path || "n/a";
|
|
48
88
|
const lines = [
|
|
49
89
|
`## Entity Match (exact)\n`,
|
|
50
90
|
`### ${match.title || "(untitled)"} [${match.kind}/${match.category}]`,
|
|
@@ -57,7 +97,8 @@ export async function handler({ query, kind, category, identity_key, tags, since
|
|
|
57
97
|
}
|
|
58
98
|
|
|
59
99
|
// Gap 2: Event default time-window
|
|
60
|
-
const effectiveCategory =
|
|
100
|
+
const effectiveCategory =
|
|
101
|
+
category || (kindFilter ? categoryFor(kindFilter) : null);
|
|
61
102
|
let effectiveSince = since || null;
|
|
62
103
|
let effectiveUntil = until || null;
|
|
63
104
|
let autoWindowed = false;
|
|
@@ -86,38 +127,64 @@ export async function handler({ query, kind, category, identity_key, tags, since
|
|
|
86
127
|
|
|
87
128
|
// Post-filter by tags if provided, then apply requested limit
|
|
88
129
|
filtered = tags?.length
|
|
89
|
-
? sorted
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
130
|
+
? sorted
|
|
131
|
+
.filter((r) => {
|
|
132
|
+
const entryTags = r.tags ? JSON.parse(r.tags) : [];
|
|
133
|
+
return tags.some((t) => entryTags.includes(t));
|
|
134
|
+
})
|
|
135
|
+
.slice(0, effectiveLimit)
|
|
93
136
|
: sorted;
|
|
94
137
|
} else {
|
|
95
138
|
// Filter-only mode (no query, use SQL directly)
|
|
96
139
|
const clauses = [];
|
|
97
140
|
const params = [];
|
|
98
|
-
if (userId !== undefined) {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
if (
|
|
141
|
+
if (userId !== undefined) {
|
|
142
|
+
clauses.push("user_id = ?");
|
|
143
|
+
params.push(userId);
|
|
144
|
+
}
|
|
145
|
+
if (kindFilter) {
|
|
146
|
+
clauses.push("kind = ?");
|
|
147
|
+
params.push(kindFilter);
|
|
148
|
+
}
|
|
149
|
+
if (category) {
|
|
150
|
+
clauses.push("category = ?");
|
|
151
|
+
params.push(category);
|
|
152
|
+
}
|
|
153
|
+
if (effectiveSince) {
|
|
154
|
+
clauses.push("created_at >= ?");
|
|
155
|
+
params.push(effectiveSince);
|
|
156
|
+
}
|
|
157
|
+
if (effectiveUntil) {
|
|
158
|
+
clauses.push("created_at <= ?");
|
|
159
|
+
params.push(effectiveUntil);
|
|
160
|
+
}
|
|
103
161
|
clauses.push("(expires_at IS NULL OR expires_at > datetime('now'))");
|
|
104
162
|
const where = clauses.length ? `WHERE ${clauses.join(" AND ")}` : "";
|
|
105
163
|
params.push(fetchLimit);
|
|
106
|
-
const rows = ctx.db
|
|
164
|
+
const rows = ctx.db
|
|
165
|
+
.prepare(`SELECT * FROM vault ${where} ORDER BY created_at DESC LIMIT ?`)
|
|
166
|
+
.all(...params);
|
|
107
167
|
|
|
108
168
|
// Post-filter by tags if provided, then apply requested limit
|
|
109
169
|
filtered = tags?.length
|
|
110
|
-
? rows
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
170
|
+
? rows
|
|
171
|
+
.filter((r) => {
|
|
172
|
+
const entryTags = r.tags ? JSON.parse(r.tags) : [];
|
|
173
|
+
return tags.some((t) => entryTags.includes(t));
|
|
174
|
+
})
|
|
175
|
+
.slice(0, effectiveLimit)
|
|
114
176
|
: rows;
|
|
115
177
|
|
|
116
178
|
// Add score field for consistent output
|
|
117
179
|
for (const r of filtered) r.score = 0;
|
|
118
180
|
}
|
|
119
181
|
|
|
120
|
-
if (!filtered.length)
|
|
182
|
+
if (!filtered.length)
|
|
183
|
+
return ok(
|
|
184
|
+
hasQuery
|
|
185
|
+
? "No results found for: " + query
|
|
186
|
+
: "No entries found matching the given filters.",
|
|
187
|
+
);
|
|
121
188
|
|
|
122
189
|
// Decrypt encrypted entries if ctx.decrypt is available
|
|
123
190
|
if (ctx.decrypt) {
|
|
@@ -132,22 +199,37 @@ export async function handler({ query, kind, category, identity_key, tags, since
|
|
|
132
199
|
}
|
|
133
200
|
|
|
134
201
|
const lines = [];
|
|
135
|
-
if (reindexFailed)
|
|
136
|
-
|
|
202
|
+
if (reindexFailed)
|
|
203
|
+
lines.push(
|
|
204
|
+
`> **Warning:** Auto-reindex failed. Results may be stale. Run \`context-vault reindex\` to fix.\n`,
|
|
205
|
+
);
|
|
206
|
+
if (hasQuery && isEmbedAvailable() === false)
|
|
207
|
+
lines.push(
|
|
208
|
+
`> **Note:** Semantic search unavailable — results ranked by keyword match only. Run \`context-vault setup\` to download the embedding model.\n`,
|
|
209
|
+
);
|
|
137
210
|
const heading = hasQuery ? `Results for "${query}"` : "Filtered entries";
|
|
138
211
|
lines.push(`## ${heading} (${filtered.length} matches)\n`);
|
|
139
212
|
for (let i = 0; i < filtered.length; i++) {
|
|
140
213
|
const r = filtered[i];
|
|
141
214
|
const entryTags = r.tags ? JSON.parse(r.tags) : [];
|
|
142
215
|
const tagStr = entryTags.length ? entryTags.join(", ") : "none";
|
|
143
|
-
const relPath =
|
|
144
|
-
|
|
145
|
-
|
|
216
|
+
const relPath =
|
|
217
|
+
r.file_path && config.vaultDir
|
|
218
|
+
? r.file_path.replace(config.vaultDir + "/", "")
|
|
219
|
+
: r.file_path || "n/a";
|
|
220
|
+
lines.push(
|
|
221
|
+
`### [${i + 1}/${filtered.length}] ${r.title || "(untitled)"} [${r.kind}/${r.category}]`,
|
|
222
|
+
);
|
|
223
|
+
lines.push(
|
|
224
|
+
`${r.score.toFixed(3)} · ${tagStr} · ${relPath} · id: \`${r.id}\``,
|
|
225
|
+
);
|
|
146
226
|
lines.push(r.body?.slice(0, 300) + (r.body?.length > 300 ? "..." : ""));
|
|
147
227
|
lines.push("");
|
|
148
228
|
}
|
|
149
229
|
if (autoWindowed) {
|
|
150
|
-
lines.push(
|
|
230
|
+
lines.push(
|
|
231
|
+
`_Showing events from last ${config.eventDecayDays || 30} days. Use since/until for custom range._`,
|
|
232
|
+
);
|
|
151
233
|
}
|
|
152
234
|
return ok(lines.join("\n"));
|
|
153
235
|
}
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { captureAndIndex } from "../../capture/index.js";
|
|
3
|
-
import { indexEntry } from "../../index/index.js";
|
|
4
3
|
import { ok, err, ensureVaultExists } from "../helpers.js";
|
|
4
|
+
import {
|
|
5
|
+
MAX_KIND_LENGTH,
|
|
6
|
+
MAX_TAG_LENGTH,
|
|
7
|
+
MAX_TAGS_COUNT,
|
|
8
|
+
} from "../../constants.js";
|
|
5
9
|
|
|
6
|
-
// ─── Input size limits (mirrors hosted validation) ────────────────────────────
|
|
7
10
|
const MAX_URL_LENGTH = 2048;
|
|
8
|
-
const MAX_KIND_LENGTH = 64;
|
|
9
|
-
const MAX_TAG_LENGTH = 100;
|
|
10
|
-
const MAX_TAGS_COUNT = 20;
|
|
11
11
|
|
|
12
12
|
export const name = "ingest_url";
|
|
13
13
|
|
|
@@ -25,26 +25,40 @@ export const inputSchema = {
|
|
|
25
25
|
* @param {import('../types.js').BaseCtx & Partial<import('../types.js').HostedCtxExtensions>} ctx
|
|
26
26
|
* @param {import('../types.js').ToolShared} shared
|
|
27
27
|
*/
|
|
28
|
-
export async function handler(
|
|
28
|
+
export async function handler(
|
|
29
|
+
{ url: targetUrl, kind, tags },
|
|
30
|
+
ctx,
|
|
31
|
+
{ ensureIndexed },
|
|
32
|
+
) {
|
|
29
33
|
const { config } = ctx;
|
|
30
34
|
const userId = ctx.userId !== undefined ? ctx.userId : undefined;
|
|
31
35
|
|
|
32
36
|
const vaultErr = ensureVaultExists(config);
|
|
33
37
|
if (vaultErr) return vaultErr;
|
|
34
38
|
|
|
35
|
-
if (!targetUrl?.trim())
|
|
36
|
-
|
|
39
|
+
if (!targetUrl?.trim())
|
|
40
|
+
return err("Required: url (non-empty string)", "INVALID_INPUT");
|
|
41
|
+
if (targetUrl.length > MAX_URL_LENGTH)
|
|
42
|
+
return err(`url must be under ${MAX_URL_LENGTH} chars`, "INVALID_INPUT");
|
|
37
43
|
if (kind !== undefined && kind !== null) {
|
|
38
44
|
if (typeof kind !== "string" || kind.length > MAX_KIND_LENGTH) {
|
|
39
|
-
return err(
|
|
45
|
+
return err(
|
|
46
|
+
`kind must be a string, max ${MAX_KIND_LENGTH} chars`,
|
|
47
|
+
"INVALID_INPUT",
|
|
48
|
+
);
|
|
40
49
|
}
|
|
41
50
|
}
|
|
42
51
|
if (tags !== undefined && tags !== null) {
|
|
43
|
-
if (!Array.isArray(tags))
|
|
44
|
-
|
|
52
|
+
if (!Array.isArray(tags))
|
|
53
|
+
return err("tags must be an array of strings", "INVALID_INPUT");
|
|
54
|
+
if (tags.length > MAX_TAGS_COUNT)
|
|
55
|
+
return err(`tags: max ${MAX_TAGS_COUNT} tags allowed`, "INVALID_INPUT");
|
|
45
56
|
for (const tag of tags) {
|
|
46
57
|
if (typeof tag !== "string" || tag.length > MAX_TAG_LENGTH) {
|
|
47
|
-
return err(
|
|
58
|
+
return err(
|
|
59
|
+
`each tag must be a string, max ${MAX_TAG_LENGTH} chars`,
|
|
60
|
+
"INVALID_INPUT",
|
|
61
|
+
);
|
|
48
62
|
}
|
|
49
63
|
}
|
|
50
64
|
}
|
|
@@ -55,15 +69,20 @@ export async function handler({ url: targetUrl, kind, tags }, ctx, { ensureIndex
|
|
|
55
69
|
if (ctx.checkLimits) {
|
|
56
70
|
const usage = ctx.checkLimits();
|
|
57
71
|
if (usage.entryCount >= usage.maxEntries) {
|
|
58
|
-
return err(
|
|
72
|
+
return err(
|
|
73
|
+
`Entry limit reached (${usage.maxEntries}). Upgrade to Pro for unlimited entries.`,
|
|
74
|
+
"LIMIT_EXCEEDED",
|
|
75
|
+
);
|
|
59
76
|
}
|
|
60
77
|
}
|
|
61
78
|
|
|
62
79
|
try {
|
|
63
80
|
const { ingestUrl } = await import("../../capture/ingest-url.js");
|
|
64
81
|
const entryData = await ingestUrl(targetUrl, { kind, tags });
|
|
65
|
-
const entry = await captureAndIndex(ctx, { ...entryData, userId }
|
|
66
|
-
const relPath = entry.filePath
|
|
82
|
+
const entry = await captureAndIndex(ctx, { ...entryData, userId });
|
|
83
|
+
const relPath = entry.filePath
|
|
84
|
+
? entry.filePath.replace(config.vaultDir + "/", "")
|
|
85
|
+
: entry.filePath;
|
|
67
86
|
const parts = [
|
|
68
87
|
`✓ Ingested URL → ${relPath}`,
|
|
69
88
|
` id: ${entry.id}`,
|
|
@@ -8,12 +8,30 @@ export const description =
|
|
|
8
8
|
"Browse vault entries without a search query. Returns id, title, kind, category, tags, created_at. Use get_context with a query for semantic search. Use this to browse by tags or find recent entries.";
|
|
9
9
|
|
|
10
10
|
export const inputSchema = {
|
|
11
|
-
kind: z
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
11
|
+
kind: z
|
|
12
|
+
.string()
|
|
13
|
+
.optional()
|
|
14
|
+
.describe("Filter by kind (e.g. 'insight', 'decision', 'pattern')"),
|
|
15
|
+
category: z
|
|
16
|
+
.enum(["knowledge", "entity", "event"])
|
|
17
|
+
.optional()
|
|
18
|
+
.describe("Filter by category"),
|
|
19
|
+
tags: z
|
|
20
|
+
.array(z.string())
|
|
21
|
+
.optional()
|
|
22
|
+
.describe("Filter by tags (entries must match at least one)"),
|
|
23
|
+
since: z
|
|
24
|
+
.string()
|
|
25
|
+
.optional()
|
|
26
|
+
.describe("ISO date, return entries created after this"),
|
|
27
|
+
until: z
|
|
28
|
+
.string()
|
|
29
|
+
.optional()
|
|
30
|
+
.describe("ISO date, return entries created before this"),
|
|
31
|
+
limit: z
|
|
32
|
+
.number()
|
|
33
|
+
.optional()
|
|
34
|
+
.describe("Max results to return (default 20, max 100)"),
|
|
17
35
|
offset: z.number().optional().describe("Skip first N results for pagination"),
|
|
18
36
|
};
|
|
19
37
|
|
|
@@ -22,7 +40,11 @@ export const inputSchema = {
|
|
|
22
40
|
* @param {import('../types.js').BaseCtx & Partial<import('../types.js').HostedCtxExtensions>} ctx
|
|
23
41
|
* @param {import('../types.js').ToolShared} shared
|
|
24
42
|
*/
|
|
25
|
-
export async function handler(
|
|
43
|
+
export async function handler(
|
|
44
|
+
{ kind, category, tags, since, until, limit, offset },
|
|
45
|
+
ctx,
|
|
46
|
+
{ ensureIndexed, reindexFailed },
|
|
47
|
+
) {
|
|
26
48
|
const { config } = ctx;
|
|
27
49
|
const userId = ctx.userId !== undefined ? ctx.userId : undefined;
|
|
28
50
|
|
|
@@ -60,33 +82,52 @@ export async function handler({ kind, category, tags, since, until, limit, offse
|
|
|
60
82
|
const fetchLimit = tags?.length ? effectiveLimit * 10 : effectiveLimit;
|
|
61
83
|
|
|
62
84
|
const countParams = [...params];
|
|
63
|
-
const total = ctx.db
|
|
85
|
+
const total = ctx.db
|
|
86
|
+
.prepare(`SELECT COUNT(*) as c FROM vault ${where}`)
|
|
87
|
+
.get(...countParams).c;
|
|
64
88
|
|
|
65
89
|
params.push(fetchLimit, effectiveOffset);
|
|
66
|
-
const rows = ctx.db
|
|
90
|
+
const rows = ctx.db
|
|
91
|
+
.prepare(
|
|
92
|
+
`SELECT id, title, kind, category, tags, created_at, SUBSTR(body, 1, 120) as preview FROM vault ${where} ORDER BY created_at DESC LIMIT ? OFFSET ?`,
|
|
93
|
+
)
|
|
94
|
+
.all(...params);
|
|
67
95
|
|
|
68
96
|
// Post-filter by tags if provided, then apply requested limit
|
|
69
97
|
const filtered = tags?.length
|
|
70
|
-
? rows
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
98
|
+
? rows
|
|
99
|
+
.filter((r) => {
|
|
100
|
+
const entryTags = r.tags ? JSON.parse(r.tags) : [];
|
|
101
|
+
return tags.some((t) => entryTags.includes(t));
|
|
102
|
+
})
|
|
103
|
+
.slice(0, effectiveLimit)
|
|
74
104
|
: rows;
|
|
75
105
|
|
|
76
|
-
if (!filtered.length)
|
|
106
|
+
if (!filtered.length)
|
|
107
|
+
return ok("No entries found matching the given filters.");
|
|
77
108
|
|
|
78
109
|
const lines = [];
|
|
79
|
-
if (reindexFailed)
|
|
110
|
+
if (reindexFailed)
|
|
111
|
+
lines.push(
|
|
112
|
+
`> **Warning:** Auto-reindex failed. Results may be stale. Run \`context-vault reindex\` to fix.\n`,
|
|
113
|
+
);
|
|
80
114
|
lines.push(`## Vault Entries (${filtered.length} shown, ${total} total)\n`);
|
|
81
115
|
for (const r of filtered) {
|
|
82
116
|
const entryTags = r.tags ? JSON.parse(r.tags) : [];
|
|
83
117
|
const tagStr = entryTags.length ? entryTags.join(", ") : "none";
|
|
84
|
-
lines.push(
|
|
85
|
-
|
|
118
|
+
lines.push(
|
|
119
|
+
`- **${r.title || "(untitled)"}** [${r.kind}/${r.category}] — ${tagStr} — ${r.created_at} — \`${r.id}\``,
|
|
120
|
+
);
|
|
121
|
+
if (r.preview)
|
|
122
|
+
lines.push(
|
|
123
|
+
` ${r.preview.replace(/\n+/g, " ").trim()}${r.preview.length >= 120 ? "…" : ""}`,
|
|
124
|
+
);
|
|
86
125
|
}
|
|
87
126
|
|
|
88
127
|
if (effectiveOffset + effectiveLimit < total) {
|
|
89
|
-
lines.push(
|
|
128
|
+
lines.push(
|
|
129
|
+
`\n_Page ${Math.floor(effectiveOffset / effectiveLimit) + 1}. Use offset: ${effectiveOffset + effectiveLimit} for next page._`,
|
|
130
|
+
);
|
|
90
131
|
}
|
|
91
132
|
|
|
92
133
|
return ok(lines.join("\n"));
|