@context-vault/core 2.17.0 → 2.17.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.
@@ -5,14 +5,13 @@ import { normalizeKind } from "../../core/files.js";
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) {
@@ -122,7 +101,6 @@ export async function handler(
122
101
  await ensureIndexed();
123
102
 
124
103
  const normalizedKinds = kinds?.map(normalizeKind) ?? [];
125
- // Expand buckets to bucket: prefixed tags and merge with explicit tags
126
104
  const bucketTags = buckets?.length ? buckets.map((b) => `bucket:${b}`) : [];
127
105
  const effectiveTags = [...(tags ?? []), ...bucketTags];
128
106
 
@@ -132,7 +110,7 @@ export async function handler(
132
110
  for (const kindFilter of normalizedKinds) {
133
111
  const rows = await hybridSearch(ctx, topic, {
134
112
  kindFilter,
135
- limit: Math.ceil(MAX_ENTRIES_FOR_SYNTHESIS / normalizedKinds.length),
113
+ limit: Math.ceil(MAX_ENTRIES_FOR_GATHER / normalizedKinds.length),
136
114
  userIdFilter: userId,
137
115
  includeSuperseeded: false,
138
116
  });
@@ -146,7 +124,7 @@ export async function handler(
146
124
  });
147
125
  } else {
148
126
  candidates = await hybridSearch(ctx, topic, {
149
- limit: MAX_ENTRIES_FOR_SYNTHESIS,
127
+ limit: MAX_ENTRIES_FOR_GATHER,
150
128
  userIdFilter: userId,
151
129
  includeSuperseeded: false,
152
130
  });
@@ -163,25 +141,16 @@ export async function handler(
163
141
  .filter((r) => NOISE_KINDS.has(r.kind))
164
142
  .map((r) => r.id);
165
143
 
166
- const synthesisEntries = candidates.filter((r) => !NOISE_KINDS.has(r.kind));
144
+ const gatherEntries = candidates.filter((r) => !NOISE_KINDS.has(r.kind));
167
145
 
168
- if (synthesisEntries.length === 0) {
146
+ if (gatherEntries.length === 0) {
169
147
  return err(
170
- `No entries found for topic "${topic}" to synthesize. Try a broader topic or different tags.`,
148
+ `No entries found for topic "${topic}". Try a broader topic or different tags.`,
171
149
  "NO_ENTRIES",
172
150
  );
173
151
  }
174
152
 
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
- }
153
+ const briefBody = formatGatheredEntries(topic, gatherEntries);
185
154
 
186
155
  const effectiveIdentityKey =
187
156
  identity_key ?? `snapshot-${slugifyTopic(topic)}`;
@@ -205,9 +174,9 @@ export async function handler(
205
174
  userId,
206
175
  meta: {
207
176
  topic,
208
- entry_count: synthesisEntries.length,
177
+ entry_count: gatherEntries.length,
209
178
  noise_superseded: noiseIds.length,
210
- synthesized_from: synthesisEntries.map((e) => e.id),
179
+ synthesized_from: gatherEntries.map((e) => e.id),
211
180
  },
212
181
  });
213
182
 
@@ -215,7 +184,7 @@ export async function handler(
215
184
  `✓ Snapshot created → id: ${entry.id}`,
216
185
  ` title: ${entry.title}`,
217
186
  ` identity_key: ${effectiveIdentityKey}`,
218
- ` synthesized from: ${synthesisEntries.length} entries`,
187
+ ` synthesized from: ${gatherEntries.length} entries`,
219
188
  noiseIds.length > 0
220
189
  ? ` noise superseded: ${noiseIds.length} entries`
221
190
  : null,
@@ -5,6 +5,8 @@ import { resolve } from "node:path";
5
5
  import { hybridSearch } from "../../retrieve/index.js";
6
6
  import { categoryFor } from "../../core/categories.js";
7
7
  import { normalizeKind } from "../../core/files.js";
8
+ import { resolveTemporalParams } from "../../core/temporal.js";
9
+ import { collectLinkedEntries } from "../../core/linking.js";
8
10
  import { ok, err } from "../helpers.js";
9
11
  import { isEmbedAvailable } from "../../index/embed.js";
10
12
 
@@ -152,8 +154,9 @@ export function detectConsolidationHints(entries, db, userId, opts = {}) {
152
154
  for (const tag of candidateTags) {
153
155
  let vaultCount = 0;
154
156
  try {
155
- const userClause =
156
- userId !== undefined ? " AND user_id = ?" : " AND user_id IS NULL";
157
+ // When userId is defined (hosted mode), scope to that user.
158
+ // When userId is undefined (local mode), no user scoping column may not exist.
159
+ const userClause = userId !== undefined ? " AND user_id = ?" : "";
157
160
  const countParams =
158
161
  userId !== undefined ? [`%"${tag}"%`, userId] : [`%"${tag}"%`];
159
162
  const countRow = db
@@ -170,8 +173,7 @@ export function detectConsolidationHints(entries, db, userId, opts = {}) {
170
173
 
171
174
  let lastSnapshotAgeDays = null;
172
175
  try {
173
- const userClause =
174
- userId !== undefined ? " AND user_id = ?" : " AND user_id IS NULL";
176
+ const userClause = userId !== undefined ? " AND user_id = ?" : "";
175
177
  const params =
176
178
  userId !== undefined ? [`%"${tag}"%`, userId] : [`%"${tag}"%`];
177
179
  const recentBrief = db
@@ -280,11 +282,15 @@ export const inputSchema = {
280
282
  since: z
281
283
  .string()
282
284
  .optional()
283
- .describe("ISO date, return entries created after this"),
285
+ .describe(
286
+ "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.",
287
+ ),
284
288
  until: z
285
289
  .string()
286
290
  .optional()
287
- .describe("ISO date, return entries created before this"),
291
+ .describe(
292
+ "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.",
293
+ ),
288
294
  limit: z.number().optional().describe("Max results to return (default 10)"),
289
295
  include_superseded: z
290
296
  .boolean()
@@ -320,7 +326,19 @@ export const inputSchema = {
320
326
  .boolean()
321
327
  .optional()
322
328
  .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.",
329
+ "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.",
330
+ ),
331
+ scope: z
332
+ .enum(["hot", "events", "all"])
333
+ .optional()
334
+ .describe(
335
+ "Index scope: 'hot' (default) — knowledge + entity entries only; 'events' — event entries only (cold index); 'all' — entire vault including events. Overrides include_events when set.",
336
+ ),
337
+ follow_links: z
338
+ .boolean()
339
+ .optional()
340
+ .describe(
341
+ "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
342
  ),
325
343
  };
326
344
 
@@ -346,6 +364,8 @@ export async function handler(
346
364
  pivot_count,
347
365
  include_ephemeral,
348
366
  include_events,
367
+ scope,
368
+ follow_links,
349
369
  },
350
370
  ctx,
351
371
  { ensureIndexed, reindexFailed },
@@ -353,13 +373,37 @@ export async function handler(
353
373
  const { config } = ctx;
354
374
  const userId = ctx.userId !== undefined ? ctx.userId : undefined;
355
375
 
376
+ // Resolve natural-language temporal shortcuts → ISO date strings
377
+ const resolved = resolveTemporalParams({ since, until });
378
+ since = resolved.since;
379
+ until = resolved.until;
380
+
356
381
  const hasQuery = query?.trim();
357
- const shouldExcludeEvents = hasQuery && !include_events && !category;
382
+
383
+ // Resolve effective scope — explicit scope param wins over include_events legacy flag.
384
+ // scope "hot" (default): knowledge + entity only — events excluded from search
385
+ // scope "events": force category filter to "event" (cold index query)
386
+ // scope "all": no category restriction — full vault
387
+ let effectiveScope = scope;
388
+ if (!effectiveScope) {
389
+ effectiveScope = include_events ? "all" : "hot";
390
+ }
391
+
392
+ // Scope "events" forces category to "event" unless caller already set a narrower category
393
+ const scopedCategory =
394
+ !category && effectiveScope === "events" ? "event" : category;
395
+ const shouldExcludeEvents =
396
+ hasQuery && effectiveScope === "hot" && !scopedCategory;
358
397
  // Expand buckets to bucket: prefixed tags and merge with explicit tags
359
398
  const bucketTags = buckets?.length ? buckets.map((b) => `bucket:${b}`) : [];
360
399
  const effectiveTags = [...(tags ?? []), ...bucketTags];
361
400
  const hasFilters =
362
- kind || category || effectiveTags.length || since || until || identity_key;
401
+ kind ||
402
+ scopedCategory ||
403
+ effectiveTags.length ||
404
+ since ||
405
+ until ||
406
+ identity_key;
363
407
  if (!hasQuery && !hasFilters)
364
408
  return err(
365
409
  "Required: query or at least one filter (kind, category, tags, since, until, identity_key)",
@@ -373,11 +417,16 @@ export async function handler(
373
417
  if (identity_key) {
374
418
  if (!kindFilter)
375
419
  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
- );
420
+ // Local mode: getByIdentityKey takes 2 params (no user_id).
421
+ // Hosted mode: 3 params — (kind, identity_key, userId).
422
+ const match =
423
+ ctx.stmts._mode === "local"
424
+ ? ctx.stmts.getByIdentityKey.get(kindFilter, identity_key)
425
+ : ctx.stmts.getByIdentityKey.get(
426
+ kindFilter,
427
+ identity_key,
428
+ userId !== undefined ? userId : null,
429
+ );
381
430
  if (match) {
382
431
  const entryTags = match.tags ? JSON.parse(match.tags) : [];
383
432
  const tagStr = entryTags.length ? entryTags.join(", ") : "none";
@@ -398,7 +447,7 @@ export async function handler(
398
447
 
399
448
  // Gap 2: Event default time-window
400
449
  const effectiveCategory =
401
- category || (kindFilter ? categoryFor(kindFilter) : null);
450
+ scopedCategory || (kindFilter ? categoryFor(kindFilter) : null);
402
451
  let effectiveSince = since || null;
403
452
  let effectiveUntil = until || null;
404
453
  let autoWindowed = false;
@@ -420,7 +469,7 @@ export async function handler(
420
469
  // Hybrid search mode
421
470
  const sorted = await hybridSearch(ctx, query, {
422
471
  kindFilter,
423
- categoryFilter: category || null,
472
+ categoryFilter: scopedCategory || null,
424
473
  excludeEvents: shouldExcludeEvents,
425
474
  since: effectiveSince,
426
475
  until: effectiveUntil,
@@ -451,9 +500,9 @@ export async function handler(
451
500
  clauses.push("kind = ?");
452
501
  params.push(kindFilter);
453
502
  }
454
- if (category) {
503
+ if (scopedCategory) {
455
504
  clauses.push("category = ?");
456
- params.push(category);
505
+ params.push(scopedCategory);
457
506
  }
458
507
  if (effectiveSince) {
459
508
  clauses.push("created_at >= ?");
@@ -632,6 +681,45 @@ export async function handler(
632
681
  }
633
682
  }
634
683
 
684
+ // Graph traversal: follow related_to links bidirectionally
685
+ if (follow_links) {
686
+ const { forward, backward } = collectLinkedEntries(
687
+ ctx.db,
688
+ filtered,
689
+ userId,
690
+ );
691
+ const allLinked = [...forward, ...backward];
692
+ const seen = new Set();
693
+ const uniqueLinked = allLinked.filter((e) => {
694
+ if (seen.has(e.id)) return false;
695
+ seen.add(e.id);
696
+ return true;
697
+ });
698
+
699
+ if (uniqueLinked.length > 0) {
700
+ lines.push(`## Linked Entries (${uniqueLinked.length} via related_to)\n`);
701
+ for (const r of uniqueLinked) {
702
+ const direction = forward.some((f) => f.id === r.id)
703
+ ? "→ forward"
704
+ : "← backlink";
705
+ const entryTags = r.tags ? JSON.parse(r.tags) : [];
706
+ const tagStr = entryTags.length ? entryTags.join(", ") : "none";
707
+ const relPath =
708
+ r.file_path && config.vaultDir
709
+ ? r.file_path.replace(config.vaultDir + "/", "")
710
+ : r.file_path || "n/a";
711
+ lines.push(
712
+ `### ${r.title || "(untitled)"} [${r.kind}/${r.category}] ${direction}`,
713
+ );
714
+ lines.push(`${tagStr} · ${relPath} · id: \`${r.id}\``);
715
+ lines.push(r.body?.slice(0, 200) + (r.body?.length > 200 ? "..." : ""));
716
+ lines.push("");
717
+ }
718
+ } else {
719
+ lines.push(`## Linked Entries\n\nNo related entries found.\n`);
720
+ }
721
+ }
722
+
635
723
  // Consolidation suggestion detection — lazy, opportunistic, vault-wide
636
724
  const consolidationOpts = {
637
725
  tagThreshold:
@@ -661,6 +749,7 @@ export async function handler(
661
749
 
662
750
  const result = ok(lines.join("\n"));
663
751
  const meta = {};
752
+ meta.scope = effectiveScope;
664
753
  if (tokensBudget != null) {
665
754
  meta.tokens_used = tokensUsed;
666
755
  meta.tokens_budget = tokensBudget;
@@ -671,8 +760,6 @@ export async function handler(
671
760
  if (consolidationSuggestions.length > 0) {
672
761
  meta.consolidation_suggestions = consolidationSuggestions;
673
762
  }
674
- if (Object.keys(meta).length > 0) {
675
- result._meta = meta;
676
- }
763
+ result._meta = meta;
677
764
  return result;
678
765
  }
@@ -5,6 +5,7 @@ import { categoryFor, defaultTierFor } from "../../core/categories.js";
5
5
  import { normalizeKind } from "../../core/files.js";
6
6
  import { ok, err, ensureVaultExists, ensureValidKind } from "../helpers.js";
7
7
  import { maybeShowFeedbackPrompt } from "../../core/telemetry.js";
8
+ import { validateRelatedTo } from "../../core/linking.js";
8
9
  import {
9
10
  MAX_BODY_LENGTH,
10
11
  MAX_TITLE_LENGTH,
@@ -43,9 +44,15 @@ async function findSimilar(
43
44
 
44
45
  const rowids = vecRows.map((vr) => vr.rowid);
45
46
  const placeholders = rowids.map(() => "?").join(",");
46
- const columns = hydrate
47
- ? "rowid, id, title, body, kind, tags, category, user_id, updated_at"
48
- : "rowid, id, title, category, user_id";
47
+ // Local mode has no user_id column — omit it from the SELECT list.
48
+ const isLocal = ctx.stmts._mode === "local";
49
+ const columns = isLocal
50
+ ? hydrate
51
+ ? "rowid, id, title, body, kind, tags, category, updated_at"
52
+ : "rowid, id, title, category"
53
+ : hydrate
54
+ ? "rowid, id, title, body, kind, tags, category, user_id, updated_at"
55
+ : "rowid, id, title, category, user_id";
49
56
  const hydratedRows = ctx.db
50
57
  .prepare(`SELECT ${columns} FROM vault WHERE rowid IN (${placeholders})`)
51
58
  .all(...rowids);
@@ -59,7 +66,7 @@ async function findSimilar(
59
66
  if (similarity < threshold) continue;
60
67
  const row = byRowid.get(vr.rowid);
61
68
  if (!row) continue;
62
- if (userId !== undefined && row.user_id !== userId) continue;
69
+ if (!isLocal && userId !== undefined && row.user_id !== userId) continue;
63
70
  if (row.category === "entity") continue;
64
71
  const entry = { id: row.id, title: row.title, score: similarity };
65
72
  if (hydrate) {
@@ -294,6 +301,12 @@ export const inputSchema = {
294
301
  .describe(
295
302
  "Array of entry IDs that this entry supersedes/replaces. Those entries will be marked with superseded_by pointing to this new entry and excluded from future search results by default.",
296
303
  ),
304
+ related_to: z
305
+ .array(z.string())
306
+ .optional()
307
+ .describe(
308
+ "Array of entry IDs this entry is related to. Enables bidirectional graph traversal — use get_context with follow_links:true to retrieve linked entries.",
309
+ ),
297
310
  source_files: z
298
311
  .array(
299
312
  z.object({
@@ -359,6 +372,7 @@ export async function handler(
359
372
  identity_key,
360
373
  expires_at,
361
374
  supersedes,
375
+ related_to,
362
376
  source_files,
363
377
  dry_run,
364
378
  similarity_threshold,
@@ -375,6 +389,9 @@ export async function handler(
375
389
  const vaultErr = ensureVaultExists(config);
376
390
  if (vaultErr) return vaultErr;
377
391
 
392
+ const relatedToErr = validateRelatedTo(related_to);
393
+ if (relatedToErr) return err(relatedToErr, "INVALID_INPUT");
394
+
378
395
  const inputErr = validateSaveInput({
379
396
  kind,
380
397
  title,
@@ -428,9 +445,15 @@ export async function handler(
428
445
  source,
429
446
  expires_at,
430
447
  supersedes,
448
+ related_to,
431
449
  source_files,
432
450
  });
433
451
  await indexEntry(ctx, entry);
452
+ if (entry.related_to?.length && ctx.stmts.updateRelatedTo) {
453
+ ctx.stmts.updateRelatedTo.run(JSON.stringify(entry.related_to), entry.id);
454
+ } else if (entry.related_to === null && ctx.stmts.updateRelatedTo) {
455
+ ctx.stmts.updateRelatedTo.run(null, entry.id);
456
+ }
434
457
  const relPath = entry.filePath
435
458
  ? entry.filePath.replace(config.vaultDir + "/", "")
436
459
  : entry.filePath;
@@ -531,6 +554,7 @@ export async function handler(
531
554
  identity_key,
532
555
  expires_at,
533
556
  supersedes,
557
+ related_to,
534
558
  source_files,
535
559
  userId,
536
560
  tier: effectiveTier,
@@ -562,8 +586,7 @@ export async function handler(
562
586
  );
563
587
  for (const bt of bucketTags) {
564
588
  const bucketUserClause = userId !== undefined ? "AND user_id = ?" : "";
565
- const bucketParams =
566
- userId !== undefined ? [bt, userId] : [bt];
589
+ const bucketParams = userId !== undefined ? [bt, userId] : [bt];
567
590
  const exists = ctx.db
568
591
  .prepare(
569
592
  `SELECT 1 FROM vault WHERE kind = 'bucket' AND identity_key = ? ${bucketUserClause} LIMIT 1`,
@@ -8,7 +8,6 @@ import * as getContext from "./tools/get-context.js";
8
8
  import * as saveContext from "./tools/save-context.js";
9
9
  import * as listContext from "./tools/list-context.js";
10
10
  import * as deleteContext from "./tools/delete-context.js";
11
- import * as submitFeedback from "./tools/submit-feedback.js";
12
11
  import * as ingestUrl from "./tools/ingest-url.js";
13
12
  import * as contextStatus from "./tools/context-status.js";
14
13
  import * as clearContext from "./tools/clear-context.js";
@@ -22,7 +21,6 @@ const toolModules = [
22
21
  saveContext,
23
22
  listContext,
24
23
  deleteContext,
25
- submitFeedback,
26
24
  ingestUrl,
27
25
  ingestProject,
28
26
  contextStatus,
@@ -1,55 +0,0 @@
1
- import { z } from "zod";
2
- import { captureAndIndex } from "../../capture/index.js";
3
- import { ok, ensureVaultExists } from "../helpers.js";
4
-
5
- export const name = "submit_feedback";
6
-
7
- export const description =
8
- "Report a bug, request a feature, or suggest an improvement. Feedback is stored in the vault and triaged by the development pipeline.";
9
-
10
- export const inputSchema = {
11
- type: z.enum(["bug", "feature", "improvement"]).describe("Type of feedback"),
12
- title: z.string().describe("Short summary of the feedback"),
13
- body: z.string().describe("Detailed description"),
14
- severity: z
15
- .enum(["low", "medium", "high"])
16
- .optional()
17
- .describe("Severity level (default: medium)"),
18
- };
19
-
20
- /**
21
- * @param {object} args
22
- * @param {import('../types.js').BaseCtx & Partial<import('../types.js').HostedCtxExtensions>} ctx
23
- * @param {import('../types.js').ToolShared} shared
24
- */
25
- export async function handler(
26
- { type, title, body, severity },
27
- ctx,
28
- { ensureIndexed },
29
- ) {
30
- const { config } = ctx;
31
- const userId = ctx.userId !== undefined ? ctx.userId : undefined;
32
-
33
- const vaultErr = ensureVaultExists(config);
34
- if (vaultErr) return vaultErr;
35
-
36
- await ensureIndexed();
37
-
38
- const effectiveSeverity = severity || "medium";
39
- const entry = await captureAndIndex(ctx, {
40
- kind: "feedback",
41
- title,
42
- body,
43
- tags: [type, effectiveSeverity],
44
- source: "submit_feedback",
45
- meta: { feedback_type: type, severity: effectiveSeverity, status: "new" },
46
- userId,
47
- });
48
-
49
- const relPath = entry.filePath
50
- ? entry.filePath.replace(config.vaultDir + "/", "")
51
- : entry.filePath;
52
- return ok(
53
- `Feedback submitted: ${type} [${effectiveSeverity}] → ${relPath}\n id: ${entry.id}\n title: ${title}`,
54
- );
55
- }