@context-vault/core 2.17.0 → 3.0.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.
Files changed (101) hide show
  1. package/dist/capture.d.ts +21 -0
  2. package/dist/capture.d.ts.map +1 -0
  3. package/dist/capture.js +269 -0
  4. package/dist/capture.js.map +1 -0
  5. package/dist/categories.d.ts +6 -0
  6. package/dist/categories.d.ts.map +1 -0
  7. package/dist/categories.js +50 -0
  8. package/dist/categories.js.map +1 -0
  9. package/dist/config.d.ts +4 -0
  10. package/dist/config.d.ts.map +1 -0
  11. package/dist/config.js +190 -0
  12. package/dist/config.js.map +1 -0
  13. package/dist/constants.d.ts +33 -0
  14. package/dist/constants.d.ts.map +1 -0
  15. package/dist/constants.js +23 -0
  16. package/dist/constants.js.map +1 -0
  17. package/dist/db.d.ts +13 -0
  18. package/dist/db.d.ts.map +1 -0
  19. package/dist/db.js +191 -0
  20. package/dist/db.js.map +1 -0
  21. package/dist/embed.d.ts +5 -0
  22. package/dist/embed.d.ts.map +1 -0
  23. package/dist/embed.js +78 -0
  24. package/dist/embed.js.map +1 -0
  25. package/dist/files.d.ts +13 -0
  26. package/dist/files.d.ts.map +1 -0
  27. package/dist/files.js +66 -0
  28. package/dist/files.js.map +1 -0
  29. package/dist/formatters.d.ts +8 -0
  30. package/dist/formatters.d.ts.map +1 -0
  31. package/dist/formatters.js +18 -0
  32. package/dist/formatters.js.map +1 -0
  33. package/dist/frontmatter.d.ts +12 -0
  34. package/dist/frontmatter.d.ts.map +1 -0
  35. package/dist/frontmatter.js +101 -0
  36. package/dist/frontmatter.js.map +1 -0
  37. package/dist/index.d.ts +10 -0
  38. package/dist/index.d.ts.map +1 -0
  39. package/dist/index.js +297 -0
  40. package/dist/index.js.map +1 -0
  41. package/dist/ingest-url.d.ts +20 -0
  42. package/dist/ingest-url.d.ts.map +1 -0
  43. package/dist/ingest-url.js +113 -0
  44. package/dist/ingest-url.js.map +1 -0
  45. package/dist/main.d.ts +14 -0
  46. package/dist/main.d.ts.map +1 -0
  47. package/dist/main.js +25 -0
  48. package/dist/main.js.map +1 -0
  49. package/dist/search.d.ts +18 -0
  50. package/dist/search.d.ts.map +1 -0
  51. package/dist/search.js +238 -0
  52. package/dist/search.js.map +1 -0
  53. package/dist/types.d.ts +176 -0
  54. package/dist/types.d.ts.map +1 -0
  55. package/dist/types.js +2 -0
  56. package/dist/types.js.map +1 -0
  57. package/package.json +66 -17
  58. package/src/capture.ts +308 -0
  59. package/src/categories.ts +54 -0
  60. package/src/{core/config.js → config.ts} +34 -33
  61. package/src/{constants.js → constants.ts} +6 -3
  62. package/src/db.ts +229 -0
  63. package/src/{index/embed.js → embed.ts} +10 -35
  64. package/src/files.ts +80 -0
  65. package/src/{capture/formatters.js → formatters.ts} +13 -11
  66. package/src/{core/frontmatter.js → frontmatter.ts} +27 -33
  67. package/src/index.ts +351 -0
  68. package/src/ingest-url.ts +99 -0
  69. package/src/main.ts +111 -0
  70. package/src/search.ts +285 -0
  71. package/src/types.ts +166 -0
  72. package/src/capture/file-ops.js +0 -97
  73. package/src/capture/import-pipeline.js +0 -46
  74. package/src/capture/importers.js +0 -387
  75. package/src/capture/index.js +0 -236
  76. package/src/capture/ingest-url.js +0 -252
  77. package/src/consolidation/index.js +0 -112
  78. package/src/core/categories.js +0 -72
  79. package/src/core/error-log.js +0 -54
  80. package/src/core/files.js +0 -108
  81. package/src/core/status.js +0 -350
  82. package/src/core/telemetry.js +0 -90
  83. package/src/index/db.js +0 -416
  84. package/src/index/index.js +0 -522
  85. package/src/index.js +0 -66
  86. package/src/retrieve/index.js +0 -500
  87. package/src/server/helpers.js +0 -44
  88. package/src/server/tools/clear-context.js +0 -47
  89. package/src/server/tools/context-status.js +0 -182
  90. package/src/server/tools/create-snapshot.js +0 -231
  91. package/src/server/tools/delete-context.js +0 -60
  92. package/src/server/tools/get-context.js +0 -678
  93. package/src/server/tools/ingest-project.js +0 -244
  94. package/src/server/tools/ingest-url.js +0 -88
  95. package/src/server/tools/list-buckets.js +0 -116
  96. package/src/server/tools/list-context.js +0 -163
  97. package/src/server/tools/save-context.js +0 -609
  98. package/src/server/tools/session-start.js +0 -285
  99. package/src/server/tools/submit-feedback.js +0 -55
  100. package/src/server/tools.js +0 -174
  101. package/src/sync/sync.js +0 -235
@@ -1,244 +0,0 @@
1
- import { z } from "zod";
2
- import { readFileSync, existsSync } from "node:fs";
3
- import { execSync } from "node:child_process";
4
- import { join, basename } from "node:path";
5
- import { captureAndIndex } from "../../capture/index.js";
6
- import { ok, err, ensureVaultExists } from "../helpers.js";
7
-
8
- export const name = "ingest_project";
9
-
10
- export const description =
11
- "Scan a local project directory and register it as a project entity in the vault. Extracts metadata from package.json, git history, and README. Also creates a bucket entity for project-scoped tagging.";
12
-
13
- export const inputSchema = {
14
- path: z.string().describe("Absolute path to the project directory to ingest"),
15
- tags: z
16
- .array(z.string())
17
- .optional()
18
- .describe("Additional tags to apply (bucket tags are auto-generated)"),
19
- pillar: z
20
- .string()
21
- .optional()
22
- .describe("Parent pillar/domain name — creates a bucket:pillar tag"),
23
- };
24
-
25
- function safeRead(filePath) {
26
- try {
27
- return readFileSync(filePath, "utf-8");
28
- } catch {
29
- return null;
30
- }
31
- }
32
-
33
- function safeExec(cmd, cwd) {
34
- try {
35
- return execSync(cmd, { cwd, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
36
- } catch {
37
- return null;
38
- }
39
- }
40
-
41
- function detectTechStack(projectPath, pkgJson) {
42
- const stack = [];
43
-
44
- if (existsSync(join(projectPath, "pyproject.toml")) || existsSync(join(projectPath, "setup.py"))) {
45
- stack.push("python");
46
- }
47
- if (existsSync(join(projectPath, "Cargo.toml"))) {
48
- stack.push("rust");
49
- }
50
- if (existsSync(join(projectPath, "go.mod"))) {
51
- stack.push("go");
52
- }
53
- if (pkgJson) {
54
- stack.push("javascript");
55
- const allDeps = {
56
- ...(pkgJson.dependencies || {}),
57
- ...(pkgJson.devDependencies || {}),
58
- };
59
- if (allDeps.typescript || existsSync(join(projectPath, "tsconfig.json"))) {
60
- stack.push("typescript");
61
- }
62
- if (allDeps.react || allDeps["react-dom"]) stack.push("react");
63
- if (allDeps.next || allDeps["next"]) stack.push("nextjs");
64
- if (allDeps.vue) stack.push("vue");
65
- if (allDeps.svelte) stack.push("svelte");
66
- if (allDeps.express) stack.push("express");
67
- if (allDeps.fastify) stack.push("fastify");
68
- if (allDeps.hono) stack.push("hono");
69
- if (allDeps.vite) stack.push("vite");
70
- if (allDeps.electron) stack.push("electron");
71
- if (allDeps.tauri || allDeps["@tauri-apps/api"]) stack.push("tauri");
72
- }
73
-
74
- return [...new Set(stack)];
75
- }
76
-
77
- function extractReadmeDescription(projectPath) {
78
- const raw = safeRead(join(projectPath, "README.md")) || safeRead(join(projectPath, "readme.md"));
79
- if (!raw) return null;
80
- for (const line of raw.split("\n")) {
81
- const trimmed = line.trim();
82
- if (!trimmed || trimmed.startsWith("#")) continue;
83
- return trimmed.slice(0, 200);
84
- }
85
- return null;
86
- }
87
-
88
- function buildProjectBody({ projectName, description, techStack, repoUrl, lastCommit, projectPath, hasClaudeMd }) {
89
- const lines = [];
90
- lines.push(`## ${projectName}`);
91
- if (description) lines.push("", description);
92
- lines.push("", "### Metadata");
93
- lines.push(`- **Path**: \`${projectPath}\``);
94
- if (repoUrl) lines.push(`- **Repo**: ${repoUrl}`);
95
- if (techStack.length) lines.push(`- **Stack**: ${techStack.join(", ")}`);
96
- if (lastCommit) lines.push(`- **Last commit**: ${lastCommit}`);
97
- lines.push(`- **CLAUDE.md**: ${hasClaudeMd ? "yes" : "no"}`);
98
- return lines.join("\n");
99
- }
100
-
101
- /**
102
- * @param {object} args
103
- * @param {import('../types.js').BaseCtx & Partial<import('../types.js').HostedCtxExtensions>} ctx
104
- * @param {import('../types.js').ToolShared} shared
105
- */
106
- export async function handler({ path: projectPath, tags, pillar }, ctx, { ensureIndexed }) {
107
- const { config } = ctx;
108
- const userId = ctx.userId !== undefined ? ctx.userId : undefined;
109
-
110
- const vaultErr = ensureVaultExists(config);
111
- if (vaultErr) return vaultErr;
112
-
113
- if (!projectPath?.trim()) {
114
- return err("Required: path (absolute path to project directory)", "INVALID_INPUT");
115
- }
116
- if (!existsSync(projectPath)) {
117
- return err(`Directory not found: ${projectPath}`, "INVALID_INPUT");
118
- }
119
-
120
- await ensureIndexed();
121
-
122
- // Read package.json if present
123
- let pkgJson = null;
124
- const pkgPath = join(projectPath, "package.json");
125
- if (existsSync(pkgPath)) {
126
- try {
127
- pkgJson = JSON.parse(readFileSync(pkgPath, "utf-8"));
128
- } catch {
129
- pkgJson = null;
130
- }
131
- }
132
-
133
- // Derive project name
134
- let projectName = basename(projectPath);
135
- if (pkgJson?.name) {
136
- projectName = pkgJson.name.replace(/^@[^/]+\//, "");
137
- }
138
-
139
- // Slug-safe identity_key
140
- const identityKey = projectName.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
141
-
142
- // Description: package.json > README
143
- const description =
144
- pkgJson?.description || extractReadmeDescription(projectPath) || null;
145
-
146
- // Tech stack detection
147
- const techStack = detectTechStack(projectPath, pkgJson);
148
-
149
- // Git metadata
150
- const isGitRepo = existsSync(join(projectPath, ".git"));
151
- const repoUrl = isGitRepo
152
- ? safeExec("git remote get-url origin", projectPath)
153
- : null;
154
- const lastCommit = isGitRepo
155
- ? safeExec("git log -1 --format=%ci", projectPath)
156
- : null;
157
-
158
- // CLAUDE.md presence
159
- const hasClaudeMd = existsSync(join(projectPath, "CLAUDE.md"));
160
-
161
- // Build tags
162
- const bucketTag = `bucket:${identityKey}`;
163
- const autoTags = [bucketTag];
164
- if (pillar) autoTags.push(`bucket:${pillar}`);
165
- const allTags = [...new Set([...autoTags, ...(tags || [])])];
166
-
167
- // Build body
168
- const body = buildProjectBody({
169
- projectName,
170
- description,
171
- techStack,
172
- repoUrl,
173
- lastCommit,
174
- projectPath,
175
- hasClaudeMd,
176
- });
177
-
178
- // Build meta
179
- const meta = {
180
- path: projectPath,
181
- ...(repoUrl ? { repo_url: repoUrl } : {}),
182
- ...(techStack.length ? { tech_stack: techStack } : {}),
183
- has_claude_md: hasClaudeMd,
184
- };
185
-
186
- // Save project entity
187
- const projectEntry = await captureAndIndex(ctx, {
188
- kind: "project",
189
- title: projectName,
190
- body,
191
- tags: allTags,
192
- identity_key: identityKey,
193
- meta,
194
- userId,
195
- });
196
-
197
- // Save bucket entity if it doesn't already exist
198
- const bucketUserClause = userId !== undefined ? "AND user_id = ?" : "";
199
- const bucketParams = userId !== undefined ? [bucketTag, userId] : [bucketTag];
200
- const bucketExists = ctx.db
201
- .prepare(
202
- `SELECT 1 FROM vault WHERE kind = 'bucket' AND identity_key = ? ${bucketUserClause} LIMIT 1`,
203
- )
204
- .get(...bucketParams);
205
-
206
- let bucketEntry = null;
207
- if (!bucketExists) {
208
- bucketEntry = await captureAndIndex(ctx, {
209
- kind: "bucket",
210
- title: projectName,
211
- body: `Bucket for project: ${projectName}`,
212
- tags: allTags,
213
- identity_key: bucketTag,
214
- meta: { project_path: projectPath },
215
- userId,
216
- });
217
- }
218
-
219
- const relPath = projectEntry.filePath
220
- ? projectEntry.filePath.replace(config.vaultDir + "/", "")
221
- : projectEntry.filePath;
222
-
223
- const parts = [
224
- `✓ Ingested project → ${relPath}`,
225
- ` id: ${projectEntry.id}`,
226
- ` title: ${projectEntry.title}`,
227
- ` tags: ${allTags.join(", ")}`,
228
- ...(techStack.length ? [` stack: ${techStack.join(", ")}`] : []),
229
- ...(repoUrl ? [` repo: ${repoUrl}`] : []),
230
- ];
231
-
232
- if (bucketEntry) {
233
- const bucketRelPath = bucketEntry.filePath
234
- ? bucketEntry.filePath.replace(config.vaultDir + "/", "")
235
- : bucketEntry.filePath;
236
- parts.push(``, `✓ Created bucket → ${bucketRelPath}`);
237
- parts.push(` id: ${bucketEntry.id}`);
238
- } else {
239
- parts.push(``, ` (bucket '${bucketTag}' already exists — skipped)`);
240
- }
241
-
242
- parts.push("", "_Use get_context with bucket tag to retrieve project-scoped entries._");
243
- return ok(parts.join("\n"));
244
- }
@@ -1,88 +0,0 @@
1
- import { z } from "zod";
2
- import { captureAndIndex } from "../../capture/index.js";
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";
9
-
10
- const MAX_URL_LENGTH = 2048;
11
-
12
- export const name = "ingest_url";
13
-
14
- export const description =
15
- "Fetch a URL, extract its readable content, and save it as a vault entry. Useful for saving articles, documentation, or web pages to your knowledge vault.";
16
-
17
- export const inputSchema = {
18
- url: z.string().describe("The URL to fetch and save"),
19
- kind: z.string().optional().describe("Entry kind (default: reference)"),
20
- tags: z.array(z.string()).optional().describe("Tags for the entry"),
21
- };
22
-
23
- /**
24
- * @param {object} args
25
- * @param {import('../types.js').BaseCtx & Partial<import('../types.js').HostedCtxExtensions>} ctx
26
- * @param {import('../types.js').ToolShared} shared
27
- */
28
- export async function handler(
29
- { url: targetUrl, kind, tags },
30
- ctx,
31
- { ensureIndexed },
32
- ) {
33
- const { config } = ctx;
34
- const userId = ctx.userId !== undefined ? ctx.userId : undefined;
35
-
36
- const vaultErr = ensureVaultExists(config);
37
- if (vaultErr) return vaultErr;
38
-
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");
43
- if (kind !== undefined && kind !== null) {
44
- if (typeof kind !== "string" || kind.length > MAX_KIND_LENGTH) {
45
- return err(
46
- `kind must be a string, max ${MAX_KIND_LENGTH} chars`,
47
- "INVALID_INPUT",
48
- );
49
- }
50
- }
51
- if (tags !== undefined && tags !== null) {
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");
56
- for (const tag of tags) {
57
- if (typeof tag !== "string" || tag.length > MAX_TAG_LENGTH) {
58
- return err(
59
- `each tag must be a string, max ${MAX_TAG_LENGTH} chars`,
60
- "INVALID_INPUT",
61
- );
62
- }
63
- }
64
- }
65
-
66
- await ensureIndexed();
67
-
68
- try {
69
- const { ingestUrl } = await import("../../capture/ingest-url.js");
70
- const entryData = await ingestUrl(targetUrl, { kind, tags });
71
- const entry = await captureAndIndex(ctx, { ...entryData, userId });
72
- const relPath = entry.filePath
73
- ? entry.filePath.replace(config.vaultDir + "/", "")
74
- : entry.filePath;
75
- const parts = [
76
- `✓ Ingested URL → ${relPath}`,
77
- ` id: ${entry.id}`,
78
- ` title: ${entry.title || "(untitled)"}`,
79
- ` source: ${entry.source || targetUrl}`,
80
- ];
81
- if (entry.tags?.length) parts.push(` tags: ${entry.tags.join(", ")}`);
82
- parts.push(` body: ${entry.body?.length || 0} chars`);
83
- parts.push("", "_Use this id to update or delete later._");
84
- return ok(parts.join("\n"));
85
- } catch (e) {
86
- return err(`Failed to ingest URL: ${e.message}`, "INGEST_FAILED");
87
- }
88
- }
@@ -1,116 +0,0 @@
1
- import { z } from "zod";
2
- import { ok } from "../helpers.js";
3
-
4
- export const name = "list_buckets";
5
-
6
- export const description =
7
- "List all registered bucket entities in the vault. Buckets are named scopes used to group entries via 'bucket:' prefixed tags. Returns each bucket's name, description, parent, and optional entry count.";
8
-
9
- export const inputSchema = {
10
- include_counts: z
11
- .boolean()
12
- .optional()
13
- .describe(
14
- "Include count of entries tagged with each bucket (default true). Set false to skip the count queries for faster response.",
15
- ),
16
- };
17
-
18
- /**
19
- * @param {object} args
20
- * @param {import('../types.js').BaseCtx & Partial<import('../types.js').HostedCtxExtensions>} ctx
21
- * @param {import('../types.js').ToolShared} shared
22
- */
23
- export async function handler(
24
- { include_counts = true },
25
- ctx,
26
- { ensureIndexed, reindexFailed },
27
- ) {
28
- const userId = ctx.userId !== undefined ? ctx.userId : undefined;
29
-
30
- await ensureIndexed();
31
-
32
- const userClause = userId !== undefined ? "AND user_id = ?" : "";
33
- const userParams = userId !== undefined ? [userId] : [];
34
-
35
- const buckets = ctx.db
36
- .prepare(
37
- `SELECT id, title, identity_key, body, tags, meta, created_at, updated_at
38
- FROM vault
39
- WHERE kind = 'bucket'
40
- AND (expires_at IS NULL OR expires_at > datetime('now'))
41
- AND superseded_by IS NULL
42
- ${userClause}
43
- ORDER BY title ASC`,
44
- )
45
- .all(...userParams);
46
-
47
- if (!buckets.length) {
48
- return ok(
49
- "No buckets registered.\n\nCreate one with `save_context(kind: \"bucket\", identity_key: \"bucket:myproject\", title: \"My Project\", body: \"...\")` to register a bucket.",
50
- );
51
- }
52
-
53
- const lines = [];
54
- if (reindexFailed) {
55
- lines.push(
56
- `> **Warning:** Auto-reindex failed. Results may be stale. Run \`context-vault reindex\` to fix.\n`,
57
- );
58
- }
59
- lines.push(`## Registered Buckets (${buckets.length})\n`);
60
-
61
- for (const b of buckets) {
62
- let meta = {};
63
- if (b.meta) {
64
- try {
65
- meta = typeof b.meta === "string" ? JSON.parse(b.meta) : b.meta;
66
- } catch {
67
- meta = {};
68
- }
69
- }
70
-
71
- const bucketTags = b.tags ? JSON.parse(b.tags) : [];
72
- const name = b.identity_key
73
- ? b.identity_key.replace(/^bucket:/, "")
74
- : b.title || b.id;
75
- const parent = meta.parent || null;
76
-
77
- let entryCount = null;
78
- if (include_counts && b.identity_key) {
79
- const countUserClause =
80
- userId !== undefined ? "AND user_id = ?" : "";
81
- const countParams = userId !== undefined ? [userId] : [];
82
- const row = ctx.db
83
- .prepare(
84
- `SELECT COUNT(*) as c FROM vault
85
- WHERE tags LIKE ?
86
- AND kind != 'bucket'
87
- AND (expires_at IS NULL OR expires_at > datetime('now'))
88
- AND superseded_by IS NULL
89
- ${countUserClause}`,
90
- )
91
- .get(`%"${b.identity_key}"%`, ...countParams);
92
- entryCount = row ? row.c : 0;
93
- }
94
-
95
- const titleDisplay = b.title || name;
96
- const headerParts = [`**${titleDisplay}**`];
97
- if (b.identity_key) headerParts.push(`\`${b.identity_key}\``);
98
- if (parent) headerParts.push(`parent: ${parent}`);
99
- if (entryCount !== null) headerParts.push(`${entryCount} entries`);
100
- lines.push(`- ${headerParts.join(" — ")}`);
101
-
102
- if (b.body) {
103
- const preview = b.body.replace(/\n+/g, " ").trim().slice(0, 120);
104
- lines.push(` ${preview}${b.body.length > 120 ? "…" : ""}`);
105
- }
106
- if (bucketTags.length) {
107
- lines.push(` tags: ${bucketTags.join(", ")}`);
108
- }
109
- }
110
-
111
- lines.push(
112
- "\n_Register a new bucket with `save_context(kind: \"bucket\", identity_key: \"bucket:<name>\", title: \"...\", body: \"...\")`_",
113
- );
114
-
115
- return ok(lines.join("\n"));
116
- }
@@ -1,163 +0,0 @@
1
- import { z } from "zod";
2
- import { normalizeKind } from "../../core/files.js";
3
- import { categoryFor } from "../../core/categories.js";
4
- import { ok } from "../helpers.js";
5
-
6
- export const name = "list_context";
7
-
8
- export const description =
9
- "Browse vault entries without a search query. Returns id, title, kind, category, tags, created_at, updated_at. Use get_context with a query for semantic search. Use this to browse by tags or find recent entries.";
10
-
11
- export const inputSchema = {
12
- kind: z
13
- .string()
14
- .optional()
15
- .describe("Filter by kind (e.g. 'insight', 'decision', 'pattern')"),
16
- category: z
17
- .enum(["knowledge", "entity", "event"])
18
- .optional()
19
- .describe("Filter by category"),
20
- tags: z
21
- .array(z.string())
22
- .optional()
23
- .describe("Filter by tags (entries must match at least one)"),
24
- since: z
25
- .string()
26
- .optional()
27
- .describe("ISO date, return entries created after this"),
28
- until: z
29
- .string()
30
- .optional()
31
- .describe("ISO date, return entries created before this"),
32
- limit: z
33
- .number()
34
- .optional()
35
- .describe("Max results to return (default 20, max 100)"),
36
- offset: z.number().optional().describe("Skip first N results for pagination"),
37
- };
38
-
39
- /**
40
- * @param {object} args
41
- * @param {import('../types.js').BaseCtx & Partial<import('../types.js').HostedCtxExtensions>} ctx
42
- * @param {import('../types.js').ToolShared} shared
43
- */
44
- export async function handler(
45
- { kind, category, tags, since, until, limit, offset },
46
- ctx,
47
- { ensureIndexed, reindexFailed },
48
- ) {
49
- const { config } = ctx;
50
- const userId = ctx.userId !== undefined ? ctx.userId : undefined;
51
-
52
- await ensureIndexed();
53
-
54
- const kindFilter = kind ? normalizeKind(kind) : null;
55
- const effectiveCategory =
56
- category || (kindFilter ? categoryFor(kindFilter) : null);
57
- let effectiveSince = since || null;
58
- let autoWindowed = false;
59
- if (effectiveCategory === "event" && !since && !until) {
60
- const decayMs = (config.eventDecayDays || 30) * 86400000;
61
- effectiveSince = new Date(Date.now() - decayMs).toISOString();
62
- autoWindowed = true;
63
- }
64
-
65
- const clauses = [];
66
- const params = [];
67
-
68
- if (userId !== undefined) {
69
- clauses.push("user_id = ?");
70
- params.push(userId);
71
- }
72
- if (kindFilter) {
73
- clauses.push("kind = ?");
74
- params.push(kindFilter);
75
- }
76
- if (category) {
77
- clauses.push("category = ?");
78
- params.push(category);
79
- }
80
- if (effectiveSince) {
81
- clauses.push("created_at >= ?");
82
- params.push(effectiveSince);
83
- }
84
- if (until) {
85
- clauses.push("created_at <= ?");
86
- params.push(until);
87
- }
88
- clauses.push("(expires_at IS NULL OR expires_at > datetime('now'))");
89
-
90
- const where = clauses.length ? `WHERE ${clauses.join(" AND ")}` : "";
91
- const effectiveLimit = Math.min(limit || 20, 100);
92
- const effectiveOffset = offset || 0;
93
- // When tag-filtering, over-fetch to compensate for post-filter reduction
94
- const fetchLimit = tags?.length ? effectiveLimit * 10 : effectiveLimit;
95
-
96
- const countParams = [...params];
97
- const total = ctx.db
98
- .prepare(`SELECT COUNT(*) as c FROM vault ${where}`)
99
- .get(...countParams).c;
100
-
101
- params.push(fetchLimit, effectiveOffset);
102
- const rows = ctx.db
103
- .prepare(
104
- `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 ?`,
105
- )
106
- .all(...params);
107
-
108
- // Post-filter by tags if provided, then apply requested limit
109
- const filtered = tags?.length
110
- ? rows
111
- .filter((r) => {
112
- const entryTags = r.tags ? JSON.parse(r.tags) : [];
113
- return tags.some((t) => entryTags.includes(t));
114
- })
115
- .slice(0, effectiveLimit)
116
- : rows;
117
-
118
- if (!filtered.length) {
119
- if (autoWindowed) {
120
- const days = config.eventDecayDays || 30;
121
- return ok(
122
- `No entries found matching the given filters in events (last ${days} days).\nTry with \`since: "YYYY-MM-DD"\` to search older events.`,
123
- );
124
- }
125
- return ok("No entries found matching the given filters.");
126
- }
127
-
128
- const lines = [];
129
- if (reindexFailed)
130
- lines.push(
131
- `> **Warning:** Auto-reindex failed. Results may be stale. Run \`context-vault reindex\` to fix.\n`,
132
- );
133
- lines.push(`## Vault Entries (${filtered.length} shown, ${total} total)\n`);
134
- if (autoWindowed) {
135
- const days = config.eventDecayDays || 30;
136
- lines.push(
137
- `> ℹ Event search limited to last ${days} days. Use \`since\` parameter for older results.\n`,
138
- );
139
- }
140
- for (const r of filtered) {
141
- const entryTags = r.tags ? JSON.parse(r.tags) : [];
142
- const tagStr = entryTags.length ? entryTags.join(", ") : "none";
143
- const dateStr =
144
- r.updated_at && r.updated_at !== r.created_at
145
- ? `${r.created_at} (updated ${r.updated_at})`
146
- : r.created_at;
147
- lines.push(
148
- `- **${r.title || "(untitled)"}** [${r.kind}/${r.category}] — ${tagStr} — ${dateStr} — \`${r.id}\``,
149
- );
150
- if (r.preview)
151
- lines.push(
152
- ` ${r.preview.replace(/\n+/g, " ").trim()}${r.preview.length >= 120 ? "…" : ""}`,
153
- );
154
- }
155
-
156
- if (effectiveOffset + effectiveLimit < total) {
157
- lines.push(
158
- `\n_Page ${Math.floor(effectiveOffset / effectiveLimit) + 1}. Use offset: ${effectiveOffset + effectiveLimit} for next page._`,
159
- );
160
- }
161
-
162
- return ok(lines.join("\n"));
163
- }