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