@context-vault/core 2.17.1 → 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.
- package/dist/capture.d.ts +21 -0
- package/dist/capture.d.ts.map +1 -0
- package/dist/capture.js +269 -0
- package/dist/capture.js.map +1 -0
- package/dist/categories.d.ts +6 -0
- package/dist/categories.d.ts.map +1 -0
- package/dist/categories.js +50 -0
- package/dist/categories.js.map +1 -0
- package/dist/config.d.ts +4 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +190 -0
- package/dist/config.js.map +1 -0
- package/dist/constants.d.ts +33 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +23 -0
- package/dist/constants.js.map +1 -0
- package/dist/db.d.ts +13 -0
- package/dist/db.d.ts.map +1 -0
- package/dist/db.js +191 -0
- package/dist/db.js.map +1 -0
- package/dist/embed.d.ts +5 -0
- package/dist/embed.d.ts.map +1 -0
- package/dist/embed.js +78 -0
- package/dist/embed.js.map +1 -0
- package/dist/files.d.ts +13 -0
- package/dist/files.d.ts.map +1 -0
- package/dist/files.js +66 -0
- package/dist/files.js.map +1 -0
- package/dist/formatters.d.ts +8 -0
- package/dist/formatters.d.ts.map +1 -0
- package/dist/formatters.js +18 -0
- package/dist/formatters.js.map +1 -0
- package/dist/frontmatter.d.ts +12 -0
- package/dist/frontmatter.d.ts.map +1 -0
- package/dist/frontmatter.js +101 -0
- package/dist/frontmatter.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +297 -0
- package/dist/index.js.map +1 -0
- package/dist/ingest-url.d.ts +20 -0
- package/dist/ingest-url.d.ts.map +1 -0
- package/dist/ingest-url.js +113 -0
- package/dist/ingest-url.js.map +1 -0
- package/dist/main.d.ts +14 -0
- package/dist/main.d.ts.map +1 -0
- package/dist/main.js +25 -0
- package/dist/main.js.map +1 -0
- package/dist/search.d.ts +18 -0
- package/dist/search.d.ts.map +1 -0
- package/dist/search.js +238 -0
- package/dist/search.js.map +1 -0
- package/dist/types.d.ts +176 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +66 -16
- package/src/capture.ts +308 -0
- package/src/categories.ts +54 -0
- package/src/{core/config.js → config.ts} +34 -33
- package/src/{constants.js → constants.ts} +6 -3
- package/src/db.ts +229 -0
- package/src/{index/embed.js → embed.ts} +10 -35
- package/src/{core/files.js → files.ts} +15 -20
- package/src/{capture/formatters.js → formatters.ts} +13 -11
- package/src/{core/frontmatter.js → frontmatter.ts} +26 -33
- package/src/index.ts +351 -0
- package/src/ingest-url.ts +99 -0
- package/src/main.ts +111 -0
- package/src/{retrieve/index.js → search.ts} +62 -150
- package/src/types.ts +166 -0
- package/src/capture/file-ops.js +0 -99
- package/src/capture/import-pipeline.js +0 -46
- package/src/capture/importers.js +0 -387
- package/src/capture/index.js +0 -250
- package/src/capture/ingest-url.js +0 -252
- package/src/consolidation/index.js +0 -112
- package/src/core/categories.js +0 -73
- package/src/core/error-log.js +0 -54
- package/src/core/linking.js +0 -161
- package/src/core/migrate-dirs.js +0 -196
- package/src/core/status.js +0 -350
- package/src/core/telemetry.js +0 -90
- package/src/core/temporal.js +0 -146
- package/src/index/db.js +0 -586
- package/src/index/index.js +0 -583
- package/src/index.js +0 -71
- package/src/server/helpers.js +0 -44
- package/src/server/tools/clear-context.js +0 -47
- package/src/server/tools/context-status.js +0 -182
- package/src/server/tools/create-snapshot.js +0 -200
- package/src/server/tools/delete-context.js +0 -60
- package/src/server/tools/get-context.js +0 -765
- package/src/server/tools/ingest-project.js +0 -244
- package/src/server/tools/ingest-url.js +0 -88
- package/src/server/tools/list-buckets.js +0 -116
- package/src/server/tools/list-context.js +0 -163
- package/src/server/tools/save-context.js +0 -632
- package/src/server/tools/session-start.js +0 -285
- package/src/server/tools.js +0 -172
- 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
|
-
}
|