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.
Files changed (110) hide show
  1. package/bin/cli.js +783 -108
  2. package/node_modules/@context-vault/core/dist/capture.d.ts +21 -0
  3. package/node_modules/@context-vault/core/dist/capture.d.ts.map +1 -0
  4. package/node_modules/@context-vault/core/dist/capture.js +269 -0
  5. package/node_modules/@context-vault/core/dist/capture.js.map +1 -0
  6. package/node_modules/@context-vault/core/dist/categories.d.ts +6 -0
  7. package/node_modules/@context-vault/core/dist/categories.d.ts.map +1 -0
  8. package/node_modules/@context-vault/core/dist/categories.js +50 -0
  9. package/node_modules/@context-vault/core/dist/categories.js.map +1 -0
  10. package/node_modules/@context-vault/core/dist/config.d.ts +4 -0
  11. package/node_modules/@context-vault/core/dist/config.d.ts.map +1 -0
  12. package/node_modules/@context-vault/core/dist/config.js +190 -0
  13. package/node_modules/@context-vault/core/dist/config.js.map +1 -0
  14. package/node_modules/@context-vault/core/dist/constants.d.ts +33 -0
  15. package/node_modules/@context-vault/core/dist/constants.d.ts.map +1 -0
  16. package/node_modules/@context-vault/core/dist/constants.js +23 -0
  17. package/node_modules/@context-vault/core/dist/constants.js.map +1 -0
  18. package/node_modules/@context-vault/core/dist/db.d.ts +13 -0
  19. package/node_modules/@context-vault/core/dist/db.d.ts.map +1 -0
  20. package/node_modules/@context-vault/core/dist/db.js +191 -0
  21. package/node_modules/@context-vault/core/dist/db.js.map +1 -0
  22. package/node_modules/@context-vault/core/dist/embed.d.ts +5 -0
  23. package/node_modules/@context-vault/core/dist/embed.d.ts.map +1 -0
  24. package/node_modules/@context-vault/core/dist/embed.js +78 -0
  25. package/node_modules/@context-vault/core/dist/embed.js.map +1 -0
  26. package/node_modules/@context-vault/core/dist/files.d.ts +13 -0
  27. package/node_modules/@context-vault/core/dist/files.d.ts.map +1 -0
  28. package/node_modules/@context-vault/core/dist/files.js +66 -0
  29. package/node_modules/@context-vault/core/dist/files.js.map +1 -0
  30. package/node_modules/@context-vault/core/dist/formatters.d.ts +8 -0
  31. package/node_modules/@context-vault/core/dist/formatters.d.ts.map +1 -0
  32. package/node_modules/@context-vault/core/dist/formatters.js +18 -0
  33. package/node_modules/@context-vault/core/dist/formatters.js.map +1 -0
  34. package/node_modules/@context-vault/core/dist/frontmatter.d.ts +12 -0
  35. package/node_modules/@context-vault/core/dist/frontmatter.d.ts.map +1 -0
  36. package/node_modules/@context-vault/core/dist/frontmatter.js +101 -0
  37. package/node_modules/@context-vault/core/dist/frontmatter.js.map +1 -0
  38. package/node_modules/@context-vault/core/dist/index.d.ts +10 -0
  39. package/node_modules/@context-vault/core/dist/index.d.ts.map +1 -0
  40. package/node_modules/@context-vault/core/dist/index.js +297 -0
  41. package/node_modules/@context-vault/core/dist/index.js.map +1 -0
  42. package/node_modules/@context-vault/core/dist/ingest-url.d.ts +20 -0
  43. package/node_modules/@context-vault/core/dist/ingest-url.d.ts.map +1 -0
  44. package/node_modules/@context-vault/core/dist/ingest-url.js +113 -0
  45. package/node_modules/@context-vault/core/dist/ingest-url.js.map +1 -0
  46. package/node_modules/@context-vault/core/dist/main.d.ts +14 -0
  47. package/node_modules/@context-vault/core/dist/main.d.ts.map +1 -0
  48. package/node_modules/@context-vault/core/dist/main.js +25 -0
  49. package/node_modules/@context-vault/core/dist/main.js.map +1 -0
  50. package/node_modules/@context-vault/core/dist/search.d.ts +18 -0
  51. package/node_modules/@context-vault/core/dist/search.d.ts.map +1 -0
  52. package/node_modules/@context-vault/core/dist/search.js +238 -0
  53. package/node_modules/@context-vault/core/dist/search.js.map +1 -0
  54. package/node_modules/@context-vault/core/dist/types.d.ts +176 -0
  55. package/node_modules/@context-vault/core/dist/types.d.ts.map +1 -0
  56. package/node_modules/@context-vault/core/dist/types.js +2 -0
  57. package/node_modules/@context-vault/core/dist/types.js.map +1 -0
  58. package/node_modules/@context-vault/core/package.json +66 -16
  59. package/node_modules/@context-vault/core/src/capture.ts +308 -0
  60. package/node_modules/@context-vault/core/src/categories.ts +54 -0
  61. package/node_modules/@context-vault/core/src/{core/config.js → config.ts} +34 -33
  62. package/node_modules/@context-vault/core/src/{constants.js → constants.ts} +6 -3
  63. package/node_modules/@context-vault/core/src/db.ts +229 -0
  64. package/node_modules/@context-vault/core/src/{index/embed.js → embed.ts} +10 -35
  65. package/node_modules/@context-vault/core/src/files.ts +80 -0
  66. package/node_modules/@context-vault/core/src/{capture/formatters.js → formatters.ts} +13 -11
  67. package/node_modules/@context-vault/core/src/{core/frontmatter.js → frontmatter.ts} +27 -33
  68. package/node_modules/@context-vault/core/src/index.ts +351 -0
  69. package/node_modules/@context-vault/core/src/ingest-url.ts +99 -0
  70. package/node_modules/@context-vault/core/src/main.ts +111 -0
  71. package/node_modules/@context-vault/core/src/search.ts +285 -0
  72. package/node_modules/@context-vault/core/src/types.ts +166 -0
  73. package/package.json +12 -7
  74. package/scripts/postinstall.js +1 -1
  75. package/{node_modules/@context-vault/core/src/core → src}/error-log.js +1 -15
  76. package/{node_modules/@context-vault/core/src/server → src}/helpers.js +9 -4
  77. package/src/linking.js +100 -0
  78. package/{node_modules/@context-vault/core/src/server/tools.js → src/register-tools.js} +14 -15
  79. package/src/{server/index.js → server.js} +8 -35
  80. package/src/status.js +235 -0
  81. package/{node_modules/@context-vault/core/src/core → src}/telemetry.js +9 -19
  82. package/src/temporal.js +97 -0
  83. package/{node_modules/@context-vault/core/src/server → src}/tools/context-status.js +3 -4
  84. package/{node_modules/@context-vault/core/src/server → src}/tools/create-snapshot.js +43 -75
  85. package/{node_modules/@context-vault/core/src/server → src}/tools/delete-context.js +0 -2
  86. package/{node_modules/@context-vault/core/src/server → src}/tools/get-context.js +118 -35
  87. package/{node_modules/@context-vault/core/src/server → src}/tools/ingest-project.js +5 -6
  88. package/{node_modules/@context-vault/core/src/server → src}/tools/ingest-url.js +3 -4
  89. package/{node_modules/@context-vault/core/src/server → src}/tools/list-buckets.js +4 -5
  90. package/{node_modules/@context-vault/core/src/server → src}/tools/list-context.js +3 -6
  91. package/{node_modules/@context-vault/core/src/server → src}/tools/save-context.js +41 -21
  92. package/{node_modules/@context-vault/core/src/server → src}/tools/session-start.js +9 -16
  93. package/node_modules/@context-vault/core/src/capture/file-ops.js +0 -97
  94. package/node_modules/@context-vault/core/src/capture/import-pipeline.js +0 -46
  95. package/node_modules/@context-vault/core/src/capture/importers.js +0 -387
  96. package/node_modules/@context-vault/core/src/capture/index.js +0 -236
  97. package/node_modules/@context-vault/core/src/capture/ingest-url.js +0 -252
  98. package/node_modules/@context-vault/core/src/consolidation/index.js +0 -112
  99. package/node_modules/@context-vault/core/src/core/categories.js +0 -72
  100. package/node_modules/@context-vault/core/src/core/files.js +0 -108
  101. package/node_modules/@context-vault/core/src/core/status.js +0 -350
  102. package/node_modules/@context-vault/core/src/index/db.js +0 -416
  103. package/node_modules/@context-vault/core/src/index/index.js +0 -522
  104. package/node_modules/@context-vault/core/src/index.js +0 -66
  105. package/node_modules/@context-vault/core/src/retrieve/index.js +0 -500
  106. package/node_modules/@context-vault/core/src/server/tools/submit-feedback.js +0 -55
  107. package/node_modules/@context-vault/core/src/sync/sync.js +0 -235
  108. package/src/hooks/post-tool-call.mjs +0 -62
  109. package/src/hooks/session-end.mjs +0 -492
  110. /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 "../../retrieve/index.js";
3
- import { captureAndIndex } from "../../capture/index.js";
4
- import { normalizeKind } from "../../core/files.js";
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 SYNTHESIS_MODEL = "claude-haiku-4-5-20251001";
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, run an LLM synthesis pass to deduplicate and structure them into a context brief, then save and return the brief's ULID. The brief is saved as kind: 'brief' with a deterministic identity_key for retrieval.";
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 buildSynthesisPrompt(topic, entries) {
42
- const entriesBlock = entries
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 body = e.body
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
- `### Entry ${i + 1} [${e.kind}] id: ${e.id}`,
52
- `tags: ${tagStr}`,
53
- `updated: ${e.updated_at || e.created_at || "unknown"}`,
54
- body,
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("\n\n");
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(MAX_ENTRIES_FOR_SYNTHESIS / normalizedKinds.length),
136
- userIdFilter: userId,
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: MAX_ENTRIES_FOR_SYNTHESIS,
150
- userIdFilter: userId,
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 synthesisEntries = candidates.filter((r) => !NOISE_KINDS.has(r.kind));
143
+ const gatherEntries = candidates.filter((r) => !NOISE_KINDS.has(r.kind));
167
144
 
168
- if (synthesisEntries.length === 0) {
145
+ if (gatherEntries.length === 0) {
169
146
  return err(
170
- `No entries found for topic "${topic}" to synthesize. Try a broader topic or different tags.`,
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
- let briefBody;
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
- userId,
173
+
206
174
  meta: {
207
175
  topic,
208
- entry_count: synthesisEntries.length,
176
+ entry_count: gatherEntries.length,
209
177
  noise_superseded: noiseIds.length,
210
- synthesized_from: synthesisEntries.map((e) => e.id),
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: ${synthesisEntries.length} entries`,
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 "../../retrieve/index.js";
6
- import { categoryFor } from "../../core/categories.js";
7
- import { normalizeKind } from "../../core/files.js";
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 "../../index/embed.js";
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, userId, opts = {}) {
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
- const userClause =
156
- userId !== undefined ? " AND user_id = ?" : " AND user_id IS NULL";
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
- userId !== undefined ? [`%"${tag}"%`, userId] : [`%"${tag}"%`];
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
- userId !== undefined ? [`%"${tag}"%`, userId] : [`%"${tag}"%`];
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("ISO date, return entries created after this"),
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("ISO date, return entries created before this"),
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
- const userId = ctx.userId !== undefined ? ctx.userId : undefined;
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
- const shouldExcludeEvents = hasQuery && !include_events && !category;
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 || category || effectiveTags.length || since || until || identity_key;
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
- const match = ctx.stmts.getByIdentityKey.get(
377
- kindFilter,
378
- identity_key,
379
- userId !== undefined ? userId : null,
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
- category || (kindFilter ? categoryFor(kindFilter) : null);
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: category || null,
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
- userIdFilter: userId,
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 (userId !== undefined) {
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 (category) {
499
+ if (scopedCategory) {
455
500
  clauses.push("category = ?");
456
- params.push(category);
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
- userId,
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
- if (Object.keys(meta).length > 0) {
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 "../../capture/index.js";
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
- userId,
193
+
195
194
  });
196
195
 
197
196
  // 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];
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
- userId,
214
+
216
215
  });
217
216
  }
218
217
 
@@ -1,11 +1,11 @@
1
1
  import { z } from "zod";
2
- import { captureAndIndex } from "../../capture/index.js";
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 "../../constants.js";
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, userId });
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 = userId !== undefined ? "AND user_id = ?" : "";
33
- const userParams = userId !== undefined ? [userId] : [];
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
- userId !== undefined ? "AND user_id = ?" : "";
81
- const countParams = userId !== undefined ? [userId] : [];
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 "../../core/files.js";
3
- import { categoryFor } from "../../core/categories.js";
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 (userId !== undefined) {
69
- clauses.push("user_id = ?");
70
- params.push(userId);
67
+ if (false) {
71
68
  }
72
69
  if (kindFilter) {
73
70
  clauses.push("kind = ?");