context-vault 2.17.0 → 3.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/cli.js +783 -108
- package/node_modules/@context-vault/core/dist/capture.d.ts +21 -0
- package/node_modules/@context-vault/core/dist/capture.d.ts.map +1 -0
- package/node_modules/@context-vault/core/dist/capture.js +269 -0
- package/node_modules/@context-vault/core/dist/capture.js.map +1 -0
- package/node_modules/@context-vault/core/dist/categories.d.ts +6 -0
- package/node_modules/@context-vault/core/dist/categories.d.ts.map +1 -0
- package/node_modules/@context-vault/core/dist/categories.js +50 -0
- package/node_modules/@context-vault/core/dist/categories.js.map +1 -0
- package/node_modules/@context-vault/core/dist/config.d.ts +4 -0
- package/node_modules/@context-vault/core/dist/config.d.ts.map +1 -0
- package/node_modules/@context-vault/core/dist/config.js +190 -0
- package/node_modules/@context-vault/core/dist/config.js.map +1 -0
- package/node_modules/@context-vault/core/dist/constants.d.ts +33 -0
- package/node_modules/@context-vault/core/dist/constants.d.ts.map +1 -0
- package/node_modules/@context-vault/core/dist/constants.js +23 -0
- package/node_modules/@context-vault/core/dist/constants.js.map +1 -0
- package/node_modules/@context-vault/core/dist/db.d.ts +13 -0
- package/node_modules/@context-vault/core/dist/db.d.ts.map +1 -0
- package/node_modules/@context-vault/core/dist/db.js +191 -0
- package/node_modules/@context-vault/core/dist/db.js.map +1 -0
- package/node_modules/@context-vault/core/dist/embed.d.ts +5 -0
- package/node_modules/@context-vault/core/dist/embed.d.ts.map +1 -0
- package/node_modules/@context-vault/core/dist/embed.js +78 -0
- package/node_modules/@context-vault/core/dist/embed.js.map +1 -0
- package/node_modules/@context-vault/core/dist/files.d.ts +13 -0
- package/node_modules/@context-vault/core/dist/files.d.ts.map +1 -0
- package/node_modules/@context-vault/core/dist/files.js +66 -0
- package/node_modules/@context-vault/core/dist/files.js.map +1 -0
- package/node_modules/@context-vault/core/dist/formatters.d.ts +8 -0
- package/node_modules/@context-vault/core/dist/formatters.d.ts.map +1 -0
- package/node_modules/@context-vault/core/dist/formatters.js +18 -0
- package/node_modules/@context-vault/core/dist/formatters.js.map +1 -0
- package/node_modules/@context-vault/core/dist/frontmatter.d.ts +12 -0
- package/node_modules/@context-vault/core/dist/frontmatter.d.ts.map +1 -0
- package/node_modules/@context-vault/core/dist/frontmatter.js +101 -0
- package/node_modules/@context-vault/core/dist/frontmatter.js.map +1 -0
- package/node_modules/@context-vault/core/dist/index.d.ts +10 -0
- package/node_modules/@context-vault/core/dist/index.d.ts.map +1 -0
- package/node_modules/@context-vault/core/dist/index.js +297 -0
- package/node_modules/@context-vault/core/dist/index.js.map +1 -0
- package/node_modules/@context-vault/core/dist/ingest-url.d.ts +20 -0
- package/node_modules/@context-vault/core/dist/ingest-url.d.ts.map +1 -0
- package/node_modules/@context-vault/core/dist/ingest-url.js +113 -0
- package/node_modules/@context-vault/core/dist/ingest-url.js.map +1 -0
- package/node_modules/@context-vault/core/dist/main.d.ts +14 -0
- package/node_modules/@context-vault/core/dist/main.d.ts.map +1 -0
- package/node_modules/@context-vault/core/dist/main.js +25 -0
- package/node_modules/@context-vault/core/dist/main.js.map +1 -0
- package/node_modules/@context-vault/core/dist/search.d.ts +18 -0
- package/node_modules/@context-vault/core/dist/search.d.ts.map +1 -0
- package/node_modules/@context-vault/core/dist/search.js +238 -0
- package/node_modules/@context-vault/core/dist/search.js.map +1 -0
- package/node_modules/@context-vault/core/dist/types.d.ts +176 -0
- package/node_modules/@context-vault/core/dist/types.d.ts.map +1 -0
- package/node_modules/@context-vault/core/dist/types.js +2 -0
- package/node_modules/@context-vault/core/dist/types.js.map +1 -0
- package/node_modules/@context-vault/core/package.json +66 -16
- package/node_modules/@context-vault/core/src/capture.ts +308 -0
- package/node_modules/@context-vault/core/src/categories.ts +54 -0
- package/node_modules/@context-vault/core/src/{core/config.js → config.ts} +34 -33
- package/node_modules/@context-vault/core/src/{constants.js → constants.ts} +6 -3
- package/node_modules/@context-vault/core/src/db.ts +229 -0
- package/node_modules/@context-vault/core/src/{index/embed.js → embed.ts} +10 -35
- package/node_modules/@context-vault/core/src/files.ts +80 -0
- package/node_modules/@context-vault/core/src/{capture/formatters.js → formatters.ts} +13 -11
- package/node_modules/@context-vault/core/src/{core/frontmatter.js → frontmatter.ts} +27 -33
- package/node_modules/@context-vault/core/src/index.ts +351 -0
- package/node_modules/@context-vault/core/src/ingest-url.ts +99 -0
- package/node_modules/@context-vault/core/src/main.ts +111 -0
- package/node_modules/@context-vault/core/src/search.ts +285 -0
- package/node_modules/@context-vault/core/src/types.ts +166 -0
- package/package.json +12 -7
- package/scripts/postinstall.js +1 -1
- package/{node_modules/@context-vault/core/src/core → src}/error-log.js +1 -15
- package/{node_modules/@context-vault/core/src/server → src}/helpers.js +9 -4
- package/src/linking.js +100 -0
- package/{node_modules/@context-vault/core/src/server/tools.js → src/register-tools.js} +14 -15
- package/src/{server/index.js → server.js} +8 -35
- package/src/status.js +235 -0
- package/{node_modules/@context-vault/core/src/core → src}/telemetry.js +9 -19
- package/src/temporal.js +97 -0
- package/{node_modules/@context-vault/core/src/server → src}/tools/context-status.js +3 -4
- package/{node_modules/@context-vault/core/src/server → src}/tools/create-snapshot.js +43 -75
- package/{node_modules/@context-vault/core/src/server → src}/tools/delete-context.js +0 -2
- package/{node_modules/@context-vault/core/src/server → src}/tools/get-context.js +118 -35
- package/{node_modules/@context-vault/core/src/server → src}/tools/ingest-project.js +5 -6
- package/{node_modules/@context-vault/core/src/server → src}/tools/ingest-url.js +3 -4
- package/{node_modules/@context-vault/core/src/server → src}/tools/list-buckets.js +4 -5
- package/{node_modules/@context-vault/core/src/server → src}/tools/list-context.js +3 -6
- package/{node_modules/@context-vault/core/src/server → src}/tools/save-context.js +41 -21
- package/{node_modules/@context-vault/core/src/server → src}/tools/session-start.js +9 -16
- package/node_modules/@context-vault/core/src/capture/file-ops.js +0 -97
- package/node_modules/@context-vault/core/src/capture/import-pipeline.js +0 -46
- package/node_modules/@context-vault/core/src/capture/importers.js +0 -387
- package/node_modules/@context-vault/core/src/capture/index.js +0 -236
- package/node_modules/@context-vault/core/src/capture/ingest-url.js +0 -252
- package/node_modules/@context-vault/core/src/consolidation/index.js +0 -112
- package/node_modules/@context-vault/core/src/core/categories.js +0 -72
- package/node_modules/@context-vault/core/src/core/files.js +0 -108
- package/node_modules/@context-vault/core/src/core/status.js +0 -350
- package/node_modules/@context-vault/core/src/index/db.js +0 -416
- package/node_modules/@context-vault/core/src/index/index.js +0 -522
- package/node_modules/@context-vault/core/src/index.js +0 -66
- package/node_modules/@context-vault/core/src/retrieve/index.js +0 -500
- package/node_modules/@context-vault/core/src/server/tools/submit-feedback.js +0 -55
- package/node_modules/@context-vault/core/src/sync/sync.js +0 -235
- package/src/hooks/post-tool-call.mjs +0 -62
- package/src/hooks/session-end.mjs +0 -492
- /package/{node_modules/@context-vault/core/src/server → src}/tools/clear-context.js +0 -0
|
@@ -1,18 +1,17 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { hybridSearch } from "
|
|
3
|
-
import { captureAndIndex } from "
|
|
4
|
-
import { normalizeKind } from "
|
|
2
|
+
import { hybridSearch } from "@context-vault/core/search";
|
|
3
|
+
import { captureAndIndex } from "@context-vault/core/capture";
|
|
4
|
+
import { normalizeKind } from "@context-vault/core/files";
|
|
5
5
|
import { ok, err, ensureVaultExists } from "../helpers.js";
|
|
6
6
|
|
|
7
7
|
const NOISE_KINDS = new Set(["prompt-history", "task-notification"]);
|
|
8
|
-
const
|
|
9
|
-
const MAX_ENTRIES_FOR_SYNTHESIS = 40;
|
|
8
|
+
const MAX_ENTRIES_FOR_GATHER = 40;
|
|
10
9
|
const MAX_BODY_PER_ENTRY = 600;
|
|
11
10
|
|
|
12
11
|
export const name = "create_snapshot";
|
|
13
12
|
|
|
14
13
|
export const description =
|
|
15
|
-
"Pull all relevant vault entries matching a topic,
|
|
14
|
+
"Pull all relevant vault entries matching a topic, deduplicate, and save them as a structured context brief (kind: 'brief'). Entries are formatted as markdown — no external API or LLM call required. The calling agent can synthesize the gathered content directly. Retrieve with: get_context(kind: 'brief', identity_key: '<key>').";
|
|
16
15
|
|
|
17
16
|
export const inputSchema = {
|
|
18
17
|
topic: z.string().describe("The topic or project name to snapshot"),
|
|
@@ -38,62 +37,42 @@ export const inputSchema = {
|
|
|
38
37
|
),
|
|
39
38
|
};
|
|
40
39
|
|
|
41
|
-
function
|
|
42
|
-
const
|
|
40
|
+
function formatGatheredEntries(topic, entries) {
|
|
41
|
+
const header = [
|
|
42
|
+
`# ${topic} — Context Brief`,
|
|
43
|
+
"",
|
|
44
|
+
`*Gathered from ${entries.length} vault ${entries.length === 1 ? "entry" : "entries"}. Synthesize the content below to extract key decisions, patterns, and constraints.*`,
|
|
45
|
+
"",
|
|
46
|
+
"---",
|
|
47
|
+
"",
|
|
48
|
+
].join("\n");
|
|
49
|
+
|
|
50
|
+
const body = entries
|
|
43
51
|
.map((e, i) => {
|
|
44
52
|
const tags = e.tags ? JSON.parse(e.tags) : [];
|
|
45
53
|
const tagStr = tags.length ? tags.join(", ") : "none";
|
|
46
|
-
const
|
|
54
|
+
const updated = e.updated_at || e.created_at || "unknown";
|
|
55
|
+
const bodyText = e.body
|
|
47
56
|
? e.body.slice(0, MAX_BODY_PER_ENTRY) +
|
|
48
57
|
(e.body.length > MAX_BODY_PER_ENTRY ? "…" : "")
|
|
49
58
|
: "(no body)";
|
|
59
|
+
const title = e.title || `Entry ${i + 1}`;
|
|
50
60
|
return [
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
61
|
+
`## ${i + 1}. [${e.kind}] ${title}`,
|
|
62
|
+
"",
|
|
63
|
+
`**Tags:** ${tagStr}`,
|
|
64
|
+
`**Updated:** ${updated}`,
|
|
65
|
+
`**ID:** \`${e.id}\``,
|
|
66
|
+
"",
|
|
67
|
+
bodyText,
|
|
68
|
+
"",
|
|
69
|
+
"---",
|
|
70
|
+
"",
|
|
55
71
|
].join("\n");
|
|
56
72
|
})
|
|
57
|
-
.join("
|
|
58
|
-
|
|
59
|
-
return `You are a knowledge synthesis assistant. Given the following vault entries about "${topic}", produce a structured context brief.
|
|
60
|
-
|
|
61
|
-
Deduplicate overlapping information, resolve any contradictions (note them in Audit Notes), and organise the content into the sections below. Keep each section concise and actionable. Omit sections that have no relevant content.
|
|
62
|
-
|
|
63
|
-
Output ONLY the markdown document — no preamble, no explanation.
|
|
64
|
-
|
|
65
|
-
Required format:
|
|
66
|
-
# ${topic} — Context Brief
|
|
67
|
-
## Status
|
|
68
|
-
(current state of the topic)
|
|
69
|
-
## Key Decisions
|
|
70
|
-
(architectural or strategic decisions made)
|
|
71
|
-
## Patterns & Conventions
|
|
72
|
-
(recurring patterns, coding conventions, standards)
|
|
73
|
-
## Active Constraints
|
|
74
|
-
(known limitations, hard requirements, deadlines)
|
|
75
|
-
## Open Questions
|
|
76
|
-
(unresolved questions or areas needing investigation)
|
|
77
|
-
## Audit Notes
|
|
78
|
-
(contradictions detected, stale entries flagged with their ids)
|
|
73
|
+
.join("");
|
|
79
74
|
|
|
80
|
-
|
|
81
|
-
VAULT ENTRIES:
|
|
82
|
-
|
|
83
|
-
${entriesBlock}`;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
async function callLlm(prompt) {
|
|
87
|
-
const { Anthropic } = await import("@anthropic-ai/sdk");
|
|
88
|
-
const client = new Anthropic();
|
|
89
|
-
const message = await client.messages.create({
|
|
90
|
-
model: SYNTHESIS_MODEL,
|
|
91
|
-
max_tokens: 2048,
|
|
92
|
-
messages: [{ role: "user", content: prompt }],
|
|
93
|
-
});
|
|
94
|
-
const block = message.content.find((b) => b.type === "text");
|
|
95
|
-
if (!block) throw new Error("LLM returned no text content");
|
|
96
|
-
return block.text;
|
|
75
|
+
return header + body;
|
|
97
76
|
}
|
|
98
77
|
|
|
99
78
|
function slugifyTopic(topic) {
|
|
@@ -110,7 +89,6 @@ export async function handler(
|
|
|
110
89
|
{ ensureIndexed },
|
|
111
90
|
) {
|
|
112
91
|
const { config } = ctx;
|
|
113
|
-
const userId = ctx.userId !== undefined ? ctx.userId : undefined;
|
|
114
92
|
|
|
115
93
|
const vaultErr = ensureVaultExists(config);
|
|
116
94
|
if (vaultErr) return vaultErr;
|
|
@@ -122,7 +100,6 @@ export async function handler(
|
|
|
122
100
|
await ensureIndexed();
|
|
123
101
|
|
|
124
102
|
const normalizedKinds = kinds?.map(normalizeKind) ?? [];
|
|
125
|
-
// Expand buckets to bucket: prefixed tags and merge with explicit tags
|
|
126
103
|
const bucketTags = buckets?.length ? buckets.map((b) => `bucket:${b}`) : [];
|
|
127
104
|
const effectiveTags = [...(tags ?? []), ...bucketTags];
|
|
128
105
|
|
|
@@ -132,8 +109,8 @@ export async function handler(
|
|
|
132
109
|
for (const kindFilter of normalizedKinds) {
|
|
133
110
|
const rows = await hybridSearch(ctx, topic, {
|
|
134
111
|
kindFilter,
|
|
135
|
-
limit: Math.ceil(
|
|
136
|
-
|
|
112
|
+
limit: Math.ceil(MAX_ENTRIES_FOR_GATHER / normalizedKinds.length),
|
|
113
|
+
|
|
137
114
|
includeSuperseeded: false,
|
|
138
115
|
});
|
|
139
116
|
candidates.push(...rows);
|
|
@@ -146,8 +123,8 @@ export async function handler(
|
|
|
146
123
|
});
|
|
147
124
|
} else {
|
|
148
125
|
candidates = await hybridSearch(ctx, topic, {
|
|
149
|
-
limit:
|
|
150
|
-
|
|
126
|
+
limit: MAX_ENTRIES_FOR_GATHER,
|
|
127
|
+
|
|
151
128
|
includeSuperseeded: false,
|
|
152
129
|
});
|
|
153
130
|
}
|
|
@@ -163,25 +140,16 @@ export async function handler(
|
|
|
163
140
|
.filter((r) => NOISE_KINDS.has(r.kind))
|
|
164
141
|
.map((r) => r.id);
|
|
165
142
|
|
|
166
|
-
const
|
|
143
|
+
const gatherEntries = candidates.filter((r) => !NOISE_KINDS.has(r.kind));
|
|
167
144
|
|
|
168
|
-
if (
|
|
145
|
+
if (gatherEntries.length === 0) {
|
|
169
146
|
return err(
|
|
170
|
-
`No entries found for topic "${topic}"
|
|
147
|
+
`No entries found for topic "${topic}". Try a broader topic or different tags.`,
|
|
171
148
|
"NO_ENTRIES",
|
|
172
149
|
);
|
|
173
150
|
}
|
|
174
151
|
|
|
175
|
-
|
|
176
|
-
try {
|
|
177
|
-
const prompt = buildSynthesisPrompt(topic, synthesisEntries);
|
|
178
|
-
briefBody = await callLlm(prompt);
|
|
179
|
-
} catch (e) {
|
|
180
|
-
return err(
|
|
181
|
-
`LLM synthesis failed: ${e.message}. Ensure ANTHROPIC_API_KEY is set.`,
|
|
182
|
-
"LLM_ERROR",
|
|
183
|
-
);
|
|
184
|
-
}
|
|
152
|
+
const briefBody = formatGatheredEntries(topic, gatherEntries);
|
|
185
153
|
|
|
186
154
|
const effectiveIdentityKey =
|
|
187
155
|
identity_key ?? `snapshot-${slugifyTopic(topic)}`;
|
|
@@ -202,12 +170,12 @@ export async function handler(
|
|
|
202
170
|
source: "create_snapshot",
|
|
203
171
|
identity_key: effectiveIdentityKey,
|
|
204
172
|
supersedes,
|
|
205
|
-
|
|
173
|
+
|
|
206
174
|
meta: {
|
|
207
175
|
topic,
|
|
208
|
-
entry_count:
|
|
176
|
+
entry_count: gatherEntries.length,
|
|
209
177
|
noise_superseded: noiseIds.length,
|
|
210
|
-
synthesized_from:
|
|
178
|
+
synthesized_from: gatherEntries.map((e) => e.id),
|
|
211
179
|
},
|
|
212
180
|
});
|
|
213
181
|
|
|
@@ -215,7 +183,7 @@ export async function handler(
|
|
|
215
183
|
`✓ Snapshot created → id: ${entry.id}`,
|
|
216
184
|
` title: ${entry.title}`,
|
|
217
185
|
` identity_key: ${effectiveIdentityKey}`,
|
|
218
|
-
` synthesized from: ${
|
|
186
|
+
` synthesized from: ${gatherEntries.length} entries`,
|
|
219
187
|
noiseIds.length > 0
|
|
220
188
|
? ` noise superseded: ${noiseIds.length} entries`
|
|
221
189
|
: null,
|
|
@@ -17,7 +17,6 @@ export const inputSchema = {
|
|
|
17
17
|
* @param {import('../types.js').ToolShared} shared
|
|
18
18
|
*/
|
|
19
19
|
export async function handler({ id }, ctx, { ensureIndexed }) {
|
|
20
|
-
const userId = ctx.userId !== undefined ? ctx.userId : undefined;
|
|
21
20
|
|
|
22
21
|
if (!id?.trim())
|
|
23
22
|
return err("Required: id (non-empty string)", "INVALID_INPUT");
|
|
@@ -27,7 +26,6 @@ export async function handler({ id }, ctx, { ensureIndexed }) {
|
|
|
27
26
|
if (!entry) return err(`Entry not found: ${id}`, "NOT_FOUND");
|
|
28
27
|
|
|
29
28
|
// Ownership check: don't leak existence across users
|
|
30
|
-
if (userId !== undefined && entry.user_id !== userId) {
|
|
31
29
|
return err(`Entry not found: ${id}`, "NOT_FOUND");
|
|
32
30
|
}
|
|
33
31
|
|
|
@@ -2,11 +2,13 @@ import { z } from "zod";
|
|
|
2
2
|
import { createHash } from "node:crypto";
|
|
3
3
|
import { readFileSync, existsSync } from "node:fs";
|
|
4
4
|
import { resolve } from "node:path";
|
|
5
|
-
import { hybridSearch } from "
|
|
6
|
-
import { categoryFor } from "
|
|
7
|
-
import { normalizeKind } from "
|
|
5
|
+
import { hybridSearch } from "@context-vault/core/search";
|
|
6
|
+
import { categoryFor } from "@context-vault/core/categories";
|
|
7
|
+
import { normalizeKind } from "@context-vault/core/files";
|
|
8
|
+
import { resolveTemporalParams } from "../temporal.js";
|
|
9
|
+
import { collectLinkedEntries } from "../linking.js";
|
|
8
10
|
import { ok, err } from "../helpers.js";
|
|
9
|
-
import { isEmbedAvailable } from "
|
|
11
|
+
import { isEmbedAvailable } from "@context-vault/core/embed";
|
|
10
12
|
|
|
11
13
|
const STALE_DUPLICATE_DAYS = 7;
|
|
12
14
|
const DEFAULT_PIVOT_COUNT = 2;
|
|
@@ -129,11 +131,10 @@ export function detectConflicts(entries, _ctx) {
|
|
|
129
131
|
*
|
|
130
132
|
* @param {Array} entries - Search result rows (used to select candidate tags)
|
|
131
133
|
* @param {import('node:sqlite').DatabaseSync} db - Database handle for vault-wide counts and brief lookups
|
|
132
|
-
* @param {number|undefined} userId - Optional user_id scope
|
|
133
134
|
* @param {{ tagThreshold?: number, maxAgeDays?: number }} opts - Configurable thresholds
|
|
134
135
|
* @returns {Array<{tag: string, entry_count: number, last_snapshot_age_days: number|null}>}
|
|
135
136
|
*/
|
|
136
|
-
export function detectConsolidationHints(entries, db,
|
|
137
|
+
export function detectConsolidationHints(entries, db, opts = {}) {
|
|
137
138
|
const tagThreshold = opts.tagThreshold ?? CONSOLIDATION_TAG_THRESHOLD;
|
|
138
139
|
const maxAgeDays = opts.maxAgeDays ?? CONSOLIDATION_SNAPSHOT_MAX_AGE_DAYS;
|
|
139
140
|
|
|
@@ -152,10 +153,11 @@ export function detectConsolidationHints(entries, db, userId, opts = {}) {
|
|
|
152
153
|
for (const tag of candidateTags) {
|
|
153
154
|
let vaultCount = 0;
|
|
154
155
|
try {
|
|
155
|
-
|
|
156
|
-
|
|
156
|
+
// When userId is defined (hosted mode), scope to that user.
|
|
157
|
+
// When userId is undefined (local mode), no user scoping — column may not exist.
|
|
158
|
+
const userClause = "";
|
|
157
159
|
const countParams =
|
|
158
|
-
|
|
160
|
+
false ? [`%"${tag}"%`] : [`%"${tag}"%`];
|
|
159
161
|
const countRow = db
|
|
160
162
|
.prepare(
|
|
161
163
|
`SELECT COUNT(*) as c FROM vault WHERE kind != 'brief' AND tags LIKE ?${userClause} AND (expires_at IS NULL OR expires_at > datetime('now')) AND superseded_by IS NULL`,
|
|
@@ -170,10 +172,9 @@ export function detectConsolidationHints(entries, db, userId, opts = {}) {
|
|
|
170
172
|
|
|
171
173
|
let lastSnapshotAgeDays = null;
|
|
172
174
|
try {
|
|
173
|
-
const userClause =
|
|
174
|
-
userId !== undefined ? " AND user_id = ?" : " AND user_id IS NULL";
|
|
175
|
+
const userClause = "";
|
|
175
176
|
const params =
|
|
176
|
-
|
|
177
|
+
false ? [`%"${tag}"%`] : [`%"${tag}"%`];
|
|
177
178
|
const recentBrief = db
|
|
178
179
|
.prepare(
|
|
179
180
|
`SELECT created_at FROM vault WHERE kind = 'brief' AND tags LIKE ?${userClause} ORDER BY created_at DESC LIMIT 1`,
|
|
@@ -280,11 +281,15 @@ export const inputSchema = {
|
|
|
280
281
|
since: z
|
|
281
282
|
.string()
|
|
282
283
|
.optional()
|
|
283
|
-
.describe(
|
|
284
|
+
.describe(
|
|
285
|
+
"Return entries created after this date. Accepts ISO date strings (e.g. '2025-01-01') or natural shortcuts: 'today', 'yesterday', 'this_week', 'this_month', 'last_3_days', 'last_2_weeks', 'last_1_month'. Spaces and underscores are interchangeable.",
|
|
286
|
+
),
|
|
284
287
|
until: z
|
|
285
288
|
.string()
|
|
286
289
|
.optional()
|
|
287
|
-
.describe(
|
|
290
|
+
.describe(
|
|
291
|
+
"Return entries created before this date. Accepts ISO date strings or the same natural shortcuts as `since`. When `since` is 'yesterday' and `until` is omitted, `until` is automatically set to the end of yesterday.",
|
|
292
|
+
),
|
|
288
293
|
limit: z.number().optional().describe("Max results to return (default 10)"),
|
|
289
294
|
include_superseded: z
|
|
290
295
|
.boolean()
|
|
@@ -320,7 +325,19 @@ export const inputSchema = {
|
|
|
320
325
|
.boolean()
|
|
321
326
|
.optional()
|
|
322
327
|
.describe(
|
|
323
|
-
"If true, include event category entries in semantic search results. Default: false — events are excluded from query-based search but remain accessible via category/tag filters.",
|
|
328
|
+
"If true, include event category entries in semantic search results. Default: false — events are excluded from query-based search but remain accessible via category/tag filters. Deprecated: prefer scope parameter.",
|
|
329
|
+
),
|
|
330
|
+
scope: z
|
|
331
|
+
.enum(["hot", "events", "all"])
|
|
332
|
+
.optional()
|
|
333
|
+
.describe(
|
|
334
|
+
"Index scope: 'hot' (default) — knowledge + entity entries only; 'events' — event entries only (cold index); 'all' — entire vault including events. Overrides include_events when set.",
|
|
335
|
+
),
|
|
336
|
+
follow_links: z
|
|
337
|
+
.boolean()
|
|
338
|
+
.optional()
|
|
339
|
+
.describe(
|
|
340
|
+
"If true, follow related_to links from result entries and include linked entries (forward links) and backlinks (entries that reference the results). Enables bidirectional graph traversal.",
|
|
324
341
|
),
|
|
325
342
|
};
|
|
326
343
|
|
|
@@ -346,20 +363,45 @@ export async function handler(
|
|
|
346
363
|
pivot_count,
|
|
347
364
|
include_ephemeral,
|
|
348
365
|
include_events,
|
|
366
|
+
scope,
|
|
367
|
+
follow_links,
|
|
349
368
|
},
|
|
350
369
|
ctx,
|
|
351
370
|
{ ensureIndexed, reindexFailed },
|
|
352
371
|
) {
|
|
353
372
|
const { config } = ctx;
|
|
354
|
-
|
|
373
|
+
|
|
374
|
+
// Resolve natural-language temporal shortcuts → ISO date strings
|
|
375
|
+
const resolved = resolveTemporalParams({ since, until });
|
|
376
|
+
since = resolved.since;
|
|
377
|
+
until = resolved.until;
|
|
355
378
|
|
|
356
379
|
const hasQuery = query?.trim();
|
|
357
|
-
|
|
380
|
+
|
|
381
|
+
// Resolve effective scope — explicit scope param wins over include_events legacy flag.
|
|
382
|
+
// scope "hot" (default): knowledge + entity only — events excluded from search
|
|
383
|
+
// scope "events": force category filter to "event" (cold index query)
|
|
384
|
+
// scope "all": no category restriction — full vault
|
|
385
|
+
let effectiveScope = scope;
|
|
386
|
+
if (!effectiveScope) {
|
|
387
|
+
effectiveScope = include_events ? "all" : "hot";
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// Scope "events" forces category to "event" unless caller already set a narrower category
|
|
391
|
+
const scopedCategory =
|
|
392
|
+
!category && effectiveScope === "events" ? "event" : category;
|
|
393
|
+
const shouldExcludeEvents =
|
|
394
|
+
hasQuery && effectiveScope === "hot" && !scopedCategory;
|
|
358
395
|
// Expand buckets to bucket: prefixed tags and merge with explicit tags
|
|
359
396
|
const bucketTags = buckets?.length ? buckets.map((b) => `bucket:${b}`) : [];
|
|
360
397
|
const effectiveTags = [...(tags ?? []), ...bucketTags];
|
|
361
398
|
const hasFilters =
|
|
362
|
-
kind ||
|
|
399
|
+
kind ||
|
|
400
|
+
scopedCategory ||
|
|
401
|
+
effectiveTags.length ||
|
|
402
|
+
since ||
|
|
403
|
+
until ||
|
|
404
|
+
identity_key;
|
|
363
405
|
if (!hasQuery && !hasFilters)
|
|
364
406
|
return err(
|
|
365
407
|
"Required: query or at least one filter (kind, category, tags, since, until, identity_key)",
|
|
@@ -373,11 +415,16 @@ export async function handler(
|
|
|
373
415
|
if (identity_key) {
|
|
374
416
|
if (!kindFilter)
|
|
375
417
|
return err("identity_key requires kind to be specified", "INVALID_INPUT");
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
418
|
+
// Local mode: getByIdentityKey takes 2 params (no user_id).
|
|
419
|
+
// Hosted mode: 3 params — (kind, identity_key).
|
|
420
|
+
const match =
|
|
421
|
+
ctx.stmts._mode === "local"
|
|
422
|
+
? ctx.stmts.getByIdentityKey.get(kindFilter, identity_key)
|
|
423
|
+
: ctx.stmts.getByIdentityKey.get(
|
|
424
|
+
kindFilter,
|
|
425
|
+
identity_key,
|
|
426
|
+
null,
|
|
427
|
+
);
|
|
381
428
|
if (match) {
|
|
382
429
|
const entryTags = match.tags ? JSON.parse(match.tags) : [];
|
|
383
430
|
const tagStr = entryTags.length ? entryTags.join(", ") : "none";
|
|
@@ -398,7 +445,7 @@ export async function handler(
|
|
|
398
445
|
|
|
399
446
|
// Gap 2: Event default time-window
|
|
400
447
|
const effectiveCategory =
|
|
401
|
-
|
|
448
|
+
scopedCategory || (kindFilter ? categoryFor(kindFilter) : null);
|
|
402
449
|
let effectiveSince = since || null;
|
|
403
450
|
let effectiveUntil = until || null;
|
|
404
451
|
let autoWindowed = false;
|
|
@@ -420,13 +467,13 @@ export async function handler(
|
|
|
420
467
|
// Hybrid search mode
|
|
421
468
|
const sorted = await hybridSearch(ctx, query, {
|
|
422
469
|
kindFilter,
|
|
423
|
-
categoryFilter:
|
|
470
|
+
categoryFilter: scopedCategory || null,
|
|
424
471
|
excludeEvents: shouldExcludeEvents,
|
|
425
472
|
since: effectiveSince,
|
|
426
473
|
until: effectiveUntil,
|
|
427
474
|
limit: fetchLimit,
|
|
428
475
|
decayDays: config.eventDecayDays || 30,
|
|
429
|
-
|
|
476
|
+
|
|
430
477
|
includeSuperseeded: include_superseded ?? false,
|
|
431
478
|
});
|
|
432
479
|
|
|
@@ -443,17 +490,15 @@ export async function handler(
|
|
|
443
490
|
// Filter-only mode (no query, use SQL directly)
|
|
444
491
|
const clauses = [];
|
|
445
492
|
const params = [];
|
|
446
|
-
if (
|
|
447
|
-
clauses.push("user_id = ?");
|
|
448
|
-
params.push(userId);
|
|
493
|
+
if (false) {
|
|
449
494
|
}
|
|
450
495
|
if (kindFilter) {
|
|
451
496
|
clauses.push("kind = ?");
|
|
452
497
|
params.push(kindFilter);
|
|
453
498
|
}
|
|
454
|
-
if (
|
|
499
|
+
if (scopedCategory) {
|
|
455
500
|
clauses.push("category = ?");
|
|
456
|
-
params.push(
|
|
501
|
+
params.push(scopedCategory);
|
|
457
502
|
}
|
|
458
503
|
if (effectiveSince) {
|
|
459
504
|
clauses.push("created_at >= ?");
|
|
@@ -632,6 +677,45 @@ export async function handler(
|
|
|
632
677
|
}
|
|
633
678
|
}
|
|
634
679
|
|
|
680
|
+
// Graph traversal: follow related_to links bidirectionally
|
|
681
|
+
if (follow_links) {
|
|
682
|
+
const { forward, backward } = collectLinkedEntries(
|
|
683
|
+
ctx.db,
|
|
684
|
+
filtered,
|
|
685
|
+
|
|
686
|
+
);
|
|
687
|
+
const allLinked = [...forward, ...backward];
|
|
688
|
+
const seen = new Set();
|
|
689
|
+
const uniqueLinked = allLinked.filter((e) => {
|
|
690
|
+
if (seen.has(e.id)) return false;
|
|
691
|
+
seen.add(e.id);
|
|
692
|
+
return true;
|
|
693
|
+
});
|
|
694
|
+
|
|
695
|
+
if (uniqueLinked.length > 0) {
|
|
696
|
+
lines.push(`## Linked Entries (${uniqueLinked.length} via related_to)\n`);
|
|
697
|
+
for (const r of uniqueLinked) {
|
|
698
|
+
const direction = forward.some((f) => f.id === r.id)
|
|
699
|
+
? "→ forward"
|
|
700
|
+
: "← backlink";
|
|
701
|
+
const entryTags = r.tags ? JSON.parse(r.tags) : [];
|
|
702
|
+
const tagStr = entryTags.length ? entryTags.join(", ") : "none";
|
|
703
|
+
const relPath =
|
|
704
|
+
r.file_path && config.vaultDir
|
|
705
|
+
? r.file_path.replace(config.vaultDir + "/", "")
|
|
706
|
+
: r.file_path || "n/a";
|
|
707
|
+
lines.push(
|
|
708
|
+
`### ${r.title || "(untitled)"} [${r.kind}/${r.category}] ${direction}`,
|
|
709
|
+
);
|
|
710
|
+
lines.push(`${tagStr} · ${relPath} · id: \`${r.id}\``);
|
|
711
|
+
lines.push(r.body?.slice(0, 200) + (r.body?.length > 200 ? "..." : ""));
|
|
712
|
+
lines.push("");
|
|
713
|
+
}
|
|
714
|
+
} else {
|
|
715
|
+
lines.push(`## Linked Entries\n\nNo related entries found.\n`);
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
|
|
635
719
|
// Consolidation suggestion detection — lazy, opportunistic, vault-wide
|
|
636
720
|
const consolidationOpts = {
|
|
637
721
|
tagThreshold:
|
|
@@ -642,7 +726,7 @@ export async function handler(
|
|
|
642
726
|
const consolidationSuggestions = detectConsolidationHints(
|
|
643
727
|
filtered,
|
|
644
728
|
ctx.db,
|
|
645
|
-
|
|
729
|
+
|
|
646
730
|
consolidationOpts,
|
|
647
731
|
);
|
|
648
732
|
|
|
@@ -661,6 +745,7 @@ export async function handler(
|
|
|
661
745
|
|
|
662
746
|
const result = ok(lines.join("\n"));
|
|
663
747
|
const meta = {};
|
|
748
|
+
meta.scope = effectiveScope;
|
|
664
749
|
if (tokensBudget != null) {
|
|
665
750
|
meta.tokens_used = tokensUsed;
|
|
666
751
|
meta.tokens_budget = tokensBudget;
|
|
@@ -671,8 +756,6 @@ export async function handler(
|
|
|
671
756
|
if (consolidationSuggestions.length > 0) {
|
|
672
757
|
meta.consolidation_suggestions = consolidationSuggestions;
|
|
673
758
|
}
|
|
674
|
-
|
|
675
|
-
result._meta = meta;
|
|
676
|
-
}
|
|
759
|
+
result._meta = meta;
|
|
677
760
|
return result;
|
|
678
761
|
}
|
|
@@ -2,7 +2,7 @@ import { z } from "zod";
|
|
|
2
2
|
import { readFileSync, existsSync } from "node:fs";
|
|
3
3
|
import { execSync } from "node:child_process";
|
|
4
4
|
import { join, basename } from "node:path";
|
|
5
|
-
import { captureAndIndex } from "
|
|
5
|
+
import { captureAndIndex } from "@context-vault/core/capture";
|
|
6
6
|
import { ok, err, ensureVaultExists } from "../helpers.js";
|
|
7
7
|
|
|
8
8
|
export const name = "ingest_project";
|
|
@@ -105,7 +105,6 @@ function buildProjectBody({ projectName, description, techStack, repoUrl, lastCo
|
|
|
105
105
|
*/
|
|
106
106
|
export async function handler({ path: projectPath, tags, pillar }, ctx, { ensureIndexed }) {
|
|
107
107
|
const { config } = ctx;
|
|
108
|
-
const userId = ctx.userId !== undefined ? ctx.userId : undefined;
|
|
109
108
|
|
|
110
109
|
const vaultErr = ensureVaultExists(config);
|
|
111
110
|
if (vaultErr) return vaultErr;
|
|
@@ -191,12 +190,12 @@ export async function handler({ path: projectPath, tags, pillar }, ctx, { ensure
|
|
|
191
190
|
tags: allTags,
|
|
192
191
|
identity_key: identityKey,
|
|
193
192
|
meta,
|
|
194
|
-
|
|
193
|
+
|
|
195
194
|
});
|
|
196
195
|
|
|
197
196
|
// Save bucket entity if it doesn't already exist
|
|
198
|
-
const bucketUserClause =
|
|
199
|
-
const bucketParams =
|
|
197
|
+
const bucketUserClause = "";
|
|
198
|
+
const bucketParams = false ? [bucketTag] : [bucketTag];
|
|
200
199
|
const bucketExists = ctx.db
|
|
201
200
|
.prepare(
|
|
202
201
|
`SELECT 1 FROM vault WHERE kind = 'bucket' AND identity_key = ? ${bucketUserClause} LIMIT 1`,
|
|
@@ -212,7 +211,7 @@ export async function handler({ path: projectPath, tags, pillar }, ctx, { ensure
|
|
|
212
211
|
tags: allTags,
|
|
213
212
|
identity_key: bucketTag,
|
|
214
213
|
meta: { project_path: projectPath },
|
|
215
|
-
|
|
214
|
+
|
|
216
215
|
});
|
|
217
216
|
}
|
|
218
217
|
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { captureAndIndex } from "
|
|
2
|
+
import { captureAndIndex } from "@context-vault/core/capture";
|
|
3
3
|
import { ok, err, ensureVaultExists } from "../helpers.js";
|
|
4
4
|
import {
|
|
5
5
|
MAX_KIND_LENGTH,
|
|
6
6
|
MAX_TAG_LENGTH,
|
|
7
7
|
MAX_TAGS_COUNT,
|
|
8
|
-
} from "
|
|
8
|
+
} from "@context-vault/core/constants";
|
|
9
9
|
|
|
10
10
|
const MAX_URL_LENGTH = 2048;
|
|
11
11
|
|
|
@@ -31,7 +31,6 @@ export async function handler(
|
|
|
31
31
|
{ ensureIndexed },
|
|
32
32
|
) {
|
|
33
33
|
const { config } = ctx;
|
|
34
|
-
const userId = ctx.userId !== undefined ? ctx.userId : undefined;
|
|
35
34
|
|
|
36
35
|
const vaultErr = ensureVaultExists(config);
|
|
37
36
|
if (vaultErr) return vaultErr;
|
|
@@ -68,7 +67,7 @@ export async function handler(
|
|
|
68
67
|
try {
|
|
69
68
|
const { ingestUrl } = await import("../../capture/ingest-url.js");
|
|
70
69
|
const entryData = await ingestUrl(targetUrl, { kind, tags });
|
|
71
|
-
const entry = await captureAndIndex(ctx, { ...entryData
|
|
70
|
+
const entry = await captureAndIndex(ctx, { ...entryData });
|
|
72
71
|
const relPath = entry.filePath
|
|
73
72
|
? entry.filePath.replace(config.vaultDir + "/", "")
|
|
74
73
|
: entry.filePath;
|
|
@@ -25,12 +25,11 @@ export async function handler(
|
|
|
25
25
|
ctx,
|
|
26
26
|
{ ensureIndexed, reindexFailed },
|
|
27
27
|
) {
|
|
28
|
-
const userId = ctx.userId !== undefined ? ctx.userId : undefined;
|
|
29
28
|
|
|
30
29
|
await ensureIndexed();
|
|
31
30
|
|
|
32
|
-
const userClause =
|
|
33
|
-
const userParams =
|
|
31
|
+
const userClause = "";
|
|
32
|
+
const userParams = [];
|
|
34
33
|
|
|
35
34
|
const buckets = ctx.db
|
|
36
35
|
.prepare(
|
|
@@ -77,8 +76,8 @@ export async function handler(
|
|
|
77
76
|
let entryCount = null;
|
|
78
77
|
if (include_counts && b.identity_key) {
|
|
79
78
|
const countUserClause =
|
|
80
|
-
|
|
81
|
-
const countParams =
|
|
79
|
+
"";
|
|
80
|
+
const countParams = [];
|
|
82
81
|
const row = ctx.db
|
|
83
82
|
.prepare(
|
|
84
83
|
`SELECT COUNT(*) as c FROM vault
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { normalizeKind } from "
|
|
3
|
-
import { categoryFor } from "
|
|
2
|
+
import { normalizeKind } from "@context-vault/core/files";
|
|
3
|
+
import { categoryFor } from "@context-vault/core/categories";
|
|
4
4
|
import { ok } from "../helpers.js";
|
|
5
5
|
|
|
6
6
|
export const name = "list_context";
|
|
@@ -47,7 +47,6 @@ export async function handler(
|
|
|
47
47
|
{ ensureIndexed, reindexFailed },
|
|
48
48
|
) {
|
|
49
49
|
const { config } = ctx;
|
|
50
|
-
const userId = ctx.userId !== undefined ? ctx.userId : undefined;
|
|
51
50
|
|
|
52
51
|
await ensureIndexed();
|
|
53
52
|
|
|
@@ -65,9 +64,7 @@ export async function handler(
|
|
|
65
64
|
const clauses = [];
|
|
66
65
|
const params = [];
|
|
67
66
|
|
|
68
|
-
if (
|
|
69
|
-
clauses.push("user_id = ?");
|
|
70
|
-
params.push(userId);
|
|
67
|
+
if (false) {
|
|
71
68
|
}
|
|
72
69
|
if (kindFilter) {
|
|
73
70
|
clauses.push("kind = ?");
|