@finchagentic/mcp 4.1.0 → 4.4.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 (44) hide show
  1. package/README.md +19 -15
  2. package/dist/agent-loop.js +75 -72
  3. package/dist/annotations.js +23 -14
  4. package/dist/convex.js +15 -18
  5. package/dist/index.js +14 -12
  6. package/dist/llm.js +12 -1
  7. package/dist/local-memory-file.js +14 -1
  8. package/dist/local-memory.js +13 -0
  9. package/dist/output-schemas.js +71 -17
  10. package/dist/project.js +36 -0
  11. package/dist/resources.js +6 -11
  12. package/dist/server.js +35 -13
  13. package/dist/token-gate.js +2 -2
  14. package/dist/tool-filter.js +14 -5
  15. package/dist/tools/agents.js +106 -394
  16. package/dist/tools/automation.js +42 -6
  17. package/dist/tools/base-mcp.js +3 -15
  18. package/dist/tools/base.js +34 -20
  19. package/dist/tools/coder.js +1 -1
  20. package/dist/tools/deep-research.js +1 -1
  21. package/dist/tools/defi.js +14 -34
  22. package/dist/tools/equity.js +10 -2
  23. package/dist/tools/events.js +1 -1
  24. package/dist/tools/github.js +51 -1
  25. package/dist/tools/insider.js +1 -1
  26. package/dist/tools/insight.js +4 -4
  27. package/dist/tools/market.js +5 -5
  28. package/dist/tools/memory.js +52 -88
  29. package/dist/tools/miroshark.js +8 -1
  30. package/dist/tools/monitor.js +8 -8
  31. package/dist/tools/os.js +9 -4
  32. package/dist/tools/packets.js +2 -2
  33. package/dist/tools/research-chain.js +1 -1
  34. package/dist/tools/research-compare.js +1 -1
  35. package/dist/tools/research.js +2 -2
  36. package/dist/tools/rh-bridge.js +1 -1
  37. package/dist/tools/rh-mcp.js +29 -4
  38. package/dist/tools/rh-orders.js +201 -123
  39. package/dist/tools/scanner.js +33 -3
  40. package/dist/tools/stake.js +369 -0
  41. package/dist/tools/vault.js +294 -40
  42. package/dist/wallet.js +130 -19
  43. package/package.json +4 -5
  44. package/dist/tools/framework.js +0 -150
@@ -1,20 +1,24 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.VAULT_TOOLS = void 0;
4
+ exports.buildVaultList = buildVaultList;
5
+ exports.buildVaultSearch = buildVaultSearch;
4
6
  exports.handleVaultTool = handleVaultTool;
5
7
  const zod_1 = require("zod");
6
8
  const convex_js_1 = require("../convex.js");
7
9
  const memory_js_1 = require("./memory.js");
8
10
  const local_vault_js_1 = require("../local-vault.js");
9
11
  const local_memory_js_1 = require("../local-memory.js");
10
- const VAULT_TYPES = ["research", "execution", "workflow", "prompt", "file", "memory", "credential"];
12
+ const project_js_1 = require("../project.js");
13
+ const VAULT_TYPES = ["research", "execution", "workflow", "prompt", "file", "memory", "code", "credential"];
11
14
  exports.VAULT_TOOLS = [
12
15
  {
13
16
  name: "vault_save",
14
17
  description: "Save or update a versioned artifact in Finch Vault. Same key = update (git-style: prior version snapshotted, patched to v+1). " +
15
- "Types: research | execution | workflow | prompt | file | memory. " +
18
+ "Types: research | execution | workflow | prompt | file | memory | code. " +
16
19
  "Entries up to 10MB - content over 600KB auto-offloads to blob storage. " +
17
- "For quick unstructured notes, use memory_add instead.",
20
+ "For quick unstructured notes, use memory_add instead. For coding sessions specifically, " +
21
+ "prefer code_session_save - same versioning, but a structured template and auto-linking built in.",
18
22
  inputSchema: {
19
23
  type: "object",
20
24
  properties: {
@@ -27,10 +31,37 @@ exports.VAULT_TOOLS = [
27
31
  tags: { type: "array", items: { type: "string" }, description: "Tags for filtering and search" },
28
32
  commitMsg: { type: "string", description: "Commit message for this version, e.g. 'initial research', 'refined with on-chain data'" },
29
33
  metadata: { type: "string", description: "Optional JSON string for extra structured fields" },
34
+ workspaceProject: {
35
+ type: "string",
36
+ description: "Optional: file this entry into a named Finch workspace project (the same Projects a user " +
37
+ "organizes their Agents/vault into on the Agents page). Matched case-insensitively by name; " +
38
+ "created automatically if it doesn't exist yet. Not the same thing as a `key` path segment - " +
39
+ "this tags the entry in Finch's own project system. Hosted vault only (no effect in local-vault mode).",
40
+ },
30
41
  },
31
42
  required: ["type", "content"],
32
43
  },
33
44
  },
45
+ {
46
+ name: "code_session_save",
47
+ description: "Persist a coding/debugging session as a versioned Markdown snapshot in Finch Vault, keyed by " +
48
+ "project (`code/<project>`) - so the next session (yours, or another agent's) has real context " +
49
+ "instead of starting cold. Same project = new version, full history kept (git-style, like vault_save). " +
50
+ "Auto-links to related past code and research entries. " +
51
+ "Call this at the end of a substantive coding task - not for every single file read or trivial edit.",
52
+ inputSchema: {
53
+ type: "object",
54
+ properties: {
55
+ project: { type: "string", description: "Project or repo slug, e.g. 'finch-webapp', 'mcp-server'. Becomes the vault key: code/<project>." },
56
+ summary: { type: "string", description: "What was done this session - the task, the approach, the outcome." },
57
+ filesChanged: { type: "array", items: { type: "string" }, description: "Files touched, e.g. ['app/convex/vault.ts', 'app/src/App.tsx']" },
58
+ decisions: { type: "string", description: "Notable decisions or tradeoffs made and why - the part a future session can't re-derive from a diff alone." },
59
+ nextSteps: { type: "string", description: "What's left, or what to pick up next session." },
60
+ tags: { type: "array", items: { type: "string" }, description: "Extra tags for search, e.g. ['bugfix', 'refactor']" },
61
+ },
62
+ required: ["project", "summary"],
63
+ },
64
+ },
34
65
  {
35
66
  name: "vault_read",
36
67
  description: "Read a Finch Vault entry by its key. Returns full content, version, tags, and any linked entries.",
@@ -230,6 +261,14 @@ exports.VAULT_TOOLS = [
230
261
  required: ["key"],
231
262
  },
232
263
  },
264
+ {
265
+ name: "list_projects",
266
+ description: "List your Finch workspace projects - the same Projects used to organize Agents and vault content on the " +
267
+ "webapp Agents page. Read-only. Check this before passing `workspaceProject` to vault_save/agent_spawn if " +
268
+ "you want to reuse an existing project rather than relying on the automatic case-insensitive name match. " +
269
+ "No effect / nothing to list in local-vault mode.",
270
+ inputSchema: { type: "object", properties: {}, required: [] },
271
+ },
233
272
  ];
234
273
  // ─── Zod schemas ─────────────────────────────────────────────────────────────
235
274
  const SaveSchema = zod_1.z.object({
@@ -242,6 +281,15 @@ const SaveSchema = zod_1.z.object({
242
281
  tags: zod_1.z.array(zod_1.z.string()).optional(),
243
282
  commitMsg: zod_1.z.string().optional(),
244
283
  metadata: zod_1.z.string().optional(),
284
+ workspaceProject: zod_1.z.string().optional(),
285
+ });
286
+ const CodeSessionSchema = zod_1.z.object({
287
+ project: zod_1.z.string().min(1).max(80),
288
+ summary: zod_1.z.string().min(1),
289
+ filesChanged: zod_1.z.array(zod_1.z.string()).max(100).optional(),
290
+ decisions: zod_1.z.string().optional(),
291
+ nextSteps: zod_1.z.string().optional(),
292
+ tags: zod_1.z.array(zod_1.z.string()).optional(),
245
293
  });
246
294
  const ReadSchema = zod_1.z.object({ key: zod_1.z.string().min(1) });
247
295
  const ListSchema = zod_1.z.object({
@@ -280,6 +328,35 @@ function formatBytes(n) {
280
328
  function formatDate(ts) {
281
329
  return new Date(ts).toUTCString();
282
330
  }
331
+ // ── Structured output builders (schemas in output-schemas.ts) ───────────────
332
+ function buildVaultList(entries, type) {
333
+ return {
334
+ type: type ?? null,
335
+ count: entries.length,
336
+ entries: entries.map((e) => ({
337
+ key: e.key,
338
+ title: e.title ?? null,
339
+ type: e.type ?? null,
340
+ version: e.version ?? null,
341
+ size: e.size ?? null,
342
+ updatedAt: e.updatedAt ?? null,
343
+ isPinned: !!e.isPinned,
344
+ })),
345
+ };
346
+ }
347
+ function buildVaultSearch(query, results) {
348
+ return {
349
+ query,
350
+ count: results.length,
351
+ results: results.map((r) => ({
352
+ key: r.key,
353
+ title: r.title ?? null,
354
+ type: r.type ?? null,
355
+ score: r.score ?? null,
356
+ preview: r.preview ?? null,
357
+ })),
358
+ };
359
+ }
283
360
  // ─── Handler ─────────────────────────────────────────────────────────────────
284
361
  async function handleVaultTool(name, args) {
285
362
  // When the user has opted into a fully-local, user-owned vault
@@ -291,11 +368,37 @@ async function handleVaultTool(name, args) {
291
368
  case "vault_save": {
292
369
  const parsed = SaveSchema.safeParse(args);
293
370
  if (!parsed.success)
294
- return { content: [{ type: "text", text: `Invalid input: ${parsed.error.issues[0].message}` }], isError: true };
371
+ return { content: [{ type: "text", text: `${parsed.error.issues[0].message}` }], isError: true };
372
+ if (parsed.data.type === "credential") {
373
+ // vault_save writes plaintext to disk/DB - "credential" is only a
374
+ // valid FILTER value for vault_list/search/export (which correctly
375
+ // exclude it), never a valid type to actually SAVE through here.
376
+ // vault_store_credential is the only path that encrypts at rest.
377
+ return {
378
+ content: [{
379
+ type: "text",
380
+ text: "Use `vault_store_credential` to save a secret - it encrypts at rest (AES-256-GCM). " +
381
+ "`vault_save` writes plaintext, so `type: \"credential\"` is refused here.",
382
+ }],
383
+ isError: true,
384
+ };
385
+ }
295
386
  // Auto-generate title from content if not provided
296
387
  const firstLine = parsed.data.content.split("\n")[0].replace(/^#+\s*/, "").slice(0, 80);
297
388
  const autoTitle = parsed.data.title ?? (firstLine || `${parsed.data.type} - ${new Date().toISOString().slice(0, 10)}`);
298
- const savePayload = { ...parsed.data, title: autoTitle };
389
+ const { workspaceProject, ...rest } = parsed.data;
390
+ const savePayload = { ...rest, title: autoTitle };
391
+ // Resolve the project name -> id server-side (auto-creates on first use).
392
+ // Local vault has no project concept at all - workspaceProject is
393
+ // silently a no-op there rather than a confusing network error.
394
+ let resolvedProjectName = null;
395
+ if (workspaceProject && !localVault) {
396
+ const resolved = await (0, project_js_1.resolveProjectId)(workspaceProject);
397
+ if (resolved) {
398
+ savePayload.projectId = resolved.projectId;
399
+ resolvedProjectName = resolved.name;
400
+ }
401
+ }
299
402
  const data = localVault
300
403
  ? (0, local_vault_js_1.localVaultSave)(localVault, savePayload)
301
404
  : await (0, convex_js_1.callConvex)("/vault/save", "POST", savePayload, "vault_save");
@@ -335,6 +438,7 @@ async function handleVaultTool(name, args) {
335
438
  `Key: \`${key}\``,
336
439
  `Version: v${version}`,
337
440
  changed && version > 1 ? `Previous version auto-snapshotted.` : "",
441
+ resolvedProjectName ? `📁 Project: ${resolvedProjectName}` : "",
338
442
  mirrorToMemory ? `🧠 Synced to searchable memory` : (localVault ? `💾 Stored locally at ~/.finch/vault` : ""),
339
443
  ...linkSummary,
340
444
  ``,
@@ -342,10 +446,99 @@ async function handleVaultTool(name, args) {
342
446
  ].filter(Boolean);
343
447
  return { content: [{ type: "text", text: lines.join("\n") }] };
344
448
  }
449
+ case "code_session_save": {
450
+ const parsed = CodeSessionSchema.safeParse(args);
451
+ if (!parsed.success)
452
+ return { content: [{ type: "text", text: `${parsed.error.issues[0].message}` }], isError: true };
453
+ const { project, summary, filesChanged, decisions, nextSteps, tags } = parsed.data;
454
+ const projectSlug = project.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 60) || "session";
455
+ const key = `code/${projectSlug}`;
456
+ const content = [
457
+ `# Code session: ${project}`,
458
+ ``,
459
+ `_${new Date().toISOString()}_`,
460
+ ``,
461
+ `## Summary`,
462
+ summary,
463
+ filesChanged?.length ? `\n## Files changed\n${filesChanged.map((f) => `- \`${f}\``).join("\n")}` : "",
464
+ decisions ? `\n## Decisions\n${decisions}` : "",
465
+ nextSteps ? `\n## Next steps\n${nextSteps}` : "",
466
+ ].filter(Boolean).join("\n");
467
+ const savePayload = {
468
+ type: "code",
469
+ key,
470
+ title: `Code: ${project}`,
471
+ content,
472
+ contentType: "markdown",
473
+ agentId: "code-session",
474
+ tags: ["code-session", ...(tags ?? [])],
475
+ commitMsg: summary.slice(0, 80),
476
+ };
477
+ const data = localVault
478
+ ? (0, local_vault_js_1.localVaultSave)(localVault, savePayload)
479
+ : await (0, convex_js_1.callConvex)("/vault/save", "POST", savePayload, "vault_save");
480
+ if (data.error)
481
+ return { content: [{ type: "text", text: `Error: ${data.error}` }], isError: true };
482
+ const { version, changed } = data;
483
+ // Same mirror-to-memory rule as vault_save: skip only when local vault
484
+ // is on WITHOUT local memory, so query terms never phone home for a
485
+ // fully offline setup.
486
+ const mirrorToMemory = !localVault || !!(0, local_memory_js_1.getLocalMemoryConfig)();
487
+ if (mirrorToMemory) {
488
+ (0, memory_js_1.syncToSupermemory)(content, {
489
+ vaultKey: key, title: savePayload.title, type: "code",
490
+ tags: savePayload.tags, version, source: "code_session_save",
491
+ });
492
+ }
493
+ // Auto-link to related past code/research entries - same idea as
494
+ // deep_research's auto-linking, so a project's session history and any
495
+ // research that informed it stay connected instead of sitting as
496
+ // disconnected entries. Purely additive - never blocks the save.
497
+ const linked = [];
498
+ try {
499
+ const searchQuery = `${project} ${summary}`.slice(0, 200);
500
+ let hits = [];
501
+ if (localVault) {
502
+ const searchResult = (0, local_vault_js_1.localVaultSearch)(localVault, searchQuery, { limit: 6 });
503
+ hits = (searchResult.results ?? [])
504
+ .filter((r) => !!r.key && r.key !== key)
505
+ .map((r) => ({ key: r.key, title: r.title ?? "(untitled)" }));
506
+ }
507
+ else {
508
+ const searchResult = (await (0, convex_js_1.callConvex)("/vault/search", "POST", { q: searchQuery, n: 8 }, "vault_search"));
509
+ hits = (searchResult?.results ?? [])
510
+ .map((r) => {
511
+ const hitKey = r.metadata?.vaultKey ?? r.metadata?.key ?? null;
512
+ return hitKey ? { key: hitKey, title: r.metadata?.title ?? "(untitled)" } : null;
513
+ })
514
+ .filter((h) => !!h && h.key !== key);
515
+ }
516
+ for (const hit of hits.slice(0, 3)) {
517
+ try {
518
+ if (localVault)
519
+ (0, local_vault_js_1.localVaultLink)(localVault, key, hit.key, "related");
520
+ else
521
+ await (0, convex_js_1.callConvex)("/vault/link", "POST", { fromKey: key, toKey: hit.key, relation: "related" }, "vault_link");
522
+ linked.push(hit.key);
523
+ }
524
+ catch { /* skip individual link failures */ }
525
+ }
526
+ }
527
+ catch { /* auto-link is purely additive */ }
528
+ const lines = [
529
+ `📦 **Code session ${changed ? (version === 1 ? "saved" : "updated") : "unchanged"}** - \`${key}\` (v${version})`,
530
+ filesChanged?.length ? `Files: ${filesChanged.length}` : "",
531
+ linked.length ? `🔗 Linked to ${linked.length} related entr${linked.length === 1 ? "y" : "ies"}: ${linked.map((k) => `\`${k}\``).join(", ")}` : "",
532
+ mirrorToMemory ? `🧠 Synced to searchable memory` : "",
533
+ ``,
534
+ `Next session: \`vault_read key="${key}"\` for the latest state, or \`vault_history key="${key}"\` for the full timeline.`,
535
+ ].filter(Boolean);
536
+ return { content: [{ type: "text", text: lines.join("\n") }] };
537
+ }
345
538
  case "vault_read": {
346
539
  const parsed = ReadSchema.safeParse(args);
347
540
  if (!parsed.success)
348
- return { content: [{ type: "text", text: `Invalid input: ${parsed.error.issues[0].message}` }], isError: true };
541
+ return { content: [{ type: "text", text: `${parsed.error.issues[0].message}` }], isError: true };
349
542
  const vaultReadKey = parsed.data.key;
350
543
  let data;
351
544
  try {
@@ -383,27 +576,38 @@ async function handleVaultTool(name, args) {
383
576
  }
384
577
  return { content: [{ type: "text", text: `vault_read error: ${data.error}` }], isError: true };
385
578
  }
386
- // Large entries are offloaded to Convex File Storage. The doc holds a
387
- // preview only; pull the real content from /vault/blob.
388
- let fullContent = data.content ?? "";
389
- if (data.contentFileId) {
390
- try {
391
- fullContent = await (0, convex_js_1.callConvexRaw)(`/vault/blob?id=${encodeURIComponent(data.contentFileId)}`, "vault_read");
392
- }
393
- catch (err) {
394
- fullContent = (data.content ?? "") + `\n\n_(could not load full blob: ${err.message})_`;
395
- }
396
- }
579
+ // NOTE: there is no blob-storage tier on the backend (see
580
+ // app/convex/vault.ts MAX_CONTENT_BYTES comment) - oversized content is
581
+ // rejected at save time, not offloaded to file storage, so `contentFileId`
582
+ // never comes back on a vault entry. A `/vault/blob` fallback used to live
583
+ // here but the route was never registered in http.ts either, so it was
584
+ // dead in both directions - removed rather than fixed against a storage
585
+ // tier that doesn't exist. Re-add only alongside building that tier.
586
+ const fullContent = data.content ?? "";
397
587
  const sizeLabel = data.originalSize ? formatBytes(data.originalSize) : formatBytes(data.size);
398
588
  const backlinksBlock = Array.isArray(data.backlinks) && data.backlinks.length > 0
399
589
  ? `\n🔙 Linked from (${data.backlinks.length}):\n${data.backlinks.map((b) => ` ← \`${b.key}\`${b.title ? ` - ${b.title}` : ""}`).join("\n")}`
400
590
  : "";
591
+ // A raw projectId means nothing to a reader - resolve it to a name.
592
+ // One extra call only when the entry is actually tagged; skipped
593
+ // entirely for the common case (unassigned) and in local-vault mode.
594
+ let projectLabel = "";
595
+ if (data.projectId && !localVault) {
596
+ try {
597
+ const projData = await (0, convex_js_1.callConvex)("/projects/list", "GET", undefined, "list_projects");
598
+ const match = projData?.projects?.find((p) => p.id === data.projectId);
599
+ if (match)
600
+ projectLabel = `📁 Project: ${match.name}`;
601
+ }
602
+ catch { /* best-effort - never block the read over this */ }
603
+ }
401
604
  const lines = [
402
605
  `📂 **${data.title}**`,
403
606
  `Key: \`${data.key}\` · Type: ${data.type} · v${data.version} · ${sizeLabel}${data.contentFileId ? " · blob" : ""}`,
404
607
  data.tags?.length ? `Tags: ${data.tags.join(", ")}` : "",
405
608
  data.isPinned ? "📌 Pinned" : "",
406
609
  data.agentId ? `Agent: ${data.agentId}` : "",
610
+ projectLabel,
407
611
  `Updated: ${formatDate(data.updatedAt)}`,
408
612
  data.linkedKeys?.length ? `\nLinks out:\n${data.linkedKeys.map((l) => ` → ${l}`).join("\n")}` : "",
409
613
  backlinksBlock,
@@ -417,7 +621,7 @@ async function handleVaultTool(name, args) {
417
621
  case "vault_list": {
418
622
  const parsed = ListSchema.safeParse(args ?? {});
419
623
  if (!parsed.success)
420
- return { content: [{ type: "text", text: `Invalid input: ${parsed.error.issues[0].message}` }], isError: true };
624
+ return { content: [{ type: "text", text: `${parsed.error.issues[0].message}` }], isError: true };
421
625
  const params = new URLSearchParams();
422
626
  if (parsed.data.type)
423
627
  params.set("type", parsed.data.type);
@@ -433,23 +637,35 @@ async function handleVaultTool(name, args) {
433
637
  if (data.error)
434
638
  return { content: [{ type: "text", text: `Error: ${data.error}` }], isError: true };
435
639
  const entries = data.entries ?? [];
436
- if (!entries.length)
437
- return { content: [{ type: "text", text: `No vault entries found${parsed.data.type ? ` of type '${parsed.data.type}'` : ""}.` }] };
640
+ if (!entries.length) {
641
+ return {
642
+ content: [{ type: "text", text: `No vault entries found${parsed.data.type ? ` of type '${parsed.data.type}'` : ""}.` }],
643
+ structuredContent: buildVaultList([], parsed.data.type),
644
+ };
645
+ }
438
646
  const header = `📚 **Finch Vault** (${entries.length} entries)`;
439
647
  const rows = entries.map((e) => `${e.isPinned ? "📌 " : ""}[\`${e.key}\`] ${e.title} - v${e.version} · ${e.type} · ${formatBytes(e.size)} · ${formatDate(e.updatedAt)}`);
440
- return { content: [{ type: "text", text: [header, "", ...rows].join("\n") }] };
648
+ return {
649
+ content: [{ type: "text", text: [header, "", ...rows].join("\n") }],
650
+ structuredContent: buildVaultList(entries, parsed.data.type),
651
+ };
441
652
  }
442
653
  case "vault_search": {
443
654
  const parsed = SearchSchema.safeParse(args);
444
655
  if (!parsed.success)
445
- return { content: [{ type: "text", text: `Invalid input: ${parsed.error.issues[0].message}` }], isError: true };
656
+ return { content: [{ type: "text", text: `${parsed.error.issues[0].message}` }], isError: true };
446
657
  // Full-text search, proxied through Convex (searchSupermemory is a
447
658
  // legacy name - it calls Finch's own /memory/search endpoint, not a
448
659
  // third-party semantic service; there is no embedding step here). Large vault
449
660
  // entries are indexed as multiple chunks tagged with isVaultChunk +
450
661
  // vaultKey - group chunks back to their parent entry so the result
451
662
  // list shows one row per entry, not one row per chunk.
452
- {
663
+ //
664
+ // Skipped entirely when vaultBackend is local - a local vault's whole
665
+ // point is "no network," so the query string must never leave the
666
+ // machine, not even to check for results before falling back to the
667
+ // (always-local) full-text branch below.
668
+ if (!localVault) {
453
669
  const limit = parsed.data.limit ?? 20;
454
670
  // Over-fetch so that after chunk dedup we still have ~limit rows.
455
671
  const smResults = await (0, memory_js_1.searchSupermemory)(parsed.data.query, Math.min(50, limit * 3));
@@ -489,7 +705,7 @@ async function handleVaultTool(name, args) {
489
705
  const grouped = Array.from(groups.values())
490
706
  .sort((a, b) => b.bestScore - a.bestScore)
491
707
  .slice(0, limit);
492
- const header = `🔍 **Vault Search** [Semantic]: "${parsed.data.query}" - ${grouped.length} entry/entries`;
708
+ const header = `🔍 **Vault Search**: "${parsed.data.query}" - ${grouped.length} entry/entries`;
493
709
  const rows = grouped.map((g, i) => {
494
710
  const score = g.bestScore ? ` ${(g.bestScore * 100).toFixed(0)}%` : "";
495
711
  const chunkBadge = g.isVaultChunk && g.chunkHits > 1
@@ -500,7 +716,10 @@ async function handleVaultTool(name, args) {
500
716
  ` ${g.bestPreview}${g.bestPreview.length >= 200 ? "…" : ""}`,
501
717
  ].join("\n");
502
718
  });
503
- return { content: [{ type: "text", text: [header, "", ...rows].join("\n") }] };
719
+ return {
720
+ content: [{ type: "text", text: [header, "", ...rows].join("\n") }],
721
+ structuredContent: buildVaultSearch(parsed.data.query, grouped.map((g) => ({ key: g.key, title: g.title, type: g.type, score: g.bestScore, preview: g.bestPreview }))),
722
+ };
504
723
  }
505
724
  }
506
725
  }
@@ -516,19 +735,26 @@ async function handleVaultTool(name, args) {
516
735
  if (data.error)
517
736
  return { content: [{ type: "text", text: `Error: ${data.error}` }], isError: true };
518
737
  const results = data.results ?? [];
519
- if (!results.length)
520
- return { content: [{ type: "text", text: `No vault entries found for: "${parsed.data.query}"` }] };
738
+ if (!results.length) {
739
+ return {
740
+ content: [{ type: "text", text: `No vault entries found for: "${parsed.data.query}"` }],
741
+ structuredContent: buildVaultSearch(parsed.data.query, []),
742
+ };
743
+ }
521
744
  const header = `🔍 **Vault Search**: "${parsed.data.query}" - ${results.length} result(s)`;
522
745
  const rows = results.map((r, i) => [
523
746
  `${i + 1}. [\`${r.key}\`] **${r.title}** (${r.type} · v${r.version})`,
524
747
  ` ${r.preview}`,
525
748
  ].join("\n"));
526
- return { content: [{ type: "text", text: [header, "", ...rows].join("\n") }] };
749
+ return {
750
+ content: [{ type: "text", text: [header, "", ...rows].join("\n") }],
751
+ structuredContent: buildVaultSearch(parsed.data.query, results),
752
+ };
527
753
  }
528
754
  case "vault_history": {
529
755
  const parsed = HistorySchema.safeParse(args);
530
756
  if (!parsed.success)
531
- return { content: [{ type: "text", text: `Invalid input: ${parsed.error.issues[0].message}` }], isError: true };
757
+ return { content: [{ type: "text", text: `${parsed.error.issues[0].message}` }], isError: true };
532
758
  const histKey = parsed.data.key;
533
759
  let data;
534
760
  try {
@@ -568,7 +794,7 @@ async function handleVaultTool(name, args) {
568
794
  case "vault_diff": {
569
795
  const parsed = DiffSchema.safeParse(args);
570
796
  if (!parsed.success)
571
- return { content: [{ type: "text", text: `Invalid input: ${parsed.error.issues[0].message}` }], isError: true };
797
+ return { content: [{ type: "text", text: `${parsed.error.issues[0].message}` }], isError: true };
572
798
  const { key, fromVersion, toVersion } = parsed.data;
573
799
  let data;
574
800
  try {
@@ -612,7 +838,7 @@ async function handleVaultTool(name, args) {
612
838
  case "vault_export": {
613
839
  const parsed = ExportSchema.safeParse(args ?? {});
614
840
  if (!parsed.success)
615
- return { content: [{ type: "text", text: `Invalid input: ${parsed.error.issues[0].message}` }], isError: true };
841
+ return { content: [{ type: "text", text: `${parsed.error.issues[0].message}` }], isError: true };
616
842
  const params = parsed.data.type ? `?type=${parsed.data.type}` : "";
617
843
  const data = localVault
618
844
  ? (0, local_vault_js_1.localVaultExport)(localVault, parsed.data.type)
@@ -631,7 +857,7 @@ async function handleVaultTool(name, args) {
631
857
  case "vault_store_credential": {
632
858
  const parsed = StoreCredentialSchema.safeParse(args);
633
859
  if (!parsed.success)
634
- return { content: [{ type: "text", text: `Invalid input: ${parsed.error.issues[0].message}` }], isError: true };
860
+ return { content: [{ type: "text", text: `${parsed.error.issues[0].message}` }], isError: true };
635
861
  const data = localVault
636
862
  ? (0, local_vault_js_1.localVaultStoreCredential)(localVault, parsed.data.name, parsed.data.value, parsed.data.description)
637
863
  : await (0, convex_js_1.callConvex)("/vault/credential/store", "POST", parsed.data, "vault_store_credential");
@@ -642,7 +868,7 @@ async function handleVaultTool(name, args) {
642
868
  case "vault_get_credential": {
643
869
  const parsed = GetCredentialSchema.safeParse(args);
644
870
  if (!parsed.success)
645
- return { content: [{ type: "text", text: `Invalid input: ${parsed.error.issues[0].message}` }], isError: true };
871
+ return { content: [{ type: "text", text: `${parsed.error.issues[0].message}` }], isError: true };
646
872
  const params = new URLSearchParams({ name: parsed.data.name });
647
873
  let data;
648
874
  try {
@@ -665,7 +891,7 @@ async function handleVaultTool(name, args) {
665
891
  case "vault_pin": {
666
892
  const parsed = PinSchema.safeParse(args);
667
893
  if (!parsed.success)
668
- return { content: [{ type: "text", text: `Invalid input: ${parsed.error.issues[0].message}` }], isError: true };
894
+ return { content: [{ type: "text", text: `${parsed.error.issues[0].message}` }], isError: true };
669
895
  const { key, pinned = true } = parsed.data;
670
896
  const data = localVault
671
897
  ? (0, local_vault_js_1.localVaultPin)(localVault, key, pinned)
@@ -677,7 +903,7 @@ async function handleVaultTool(name, args) {
677
903
  case "vault_unpublish": {
678
904
  const parsed = UnpublishSchema.safeParse(args);
679
905
  if (!parsed.success)
680
- return { content: [{ type: "text", text: `Invalid input: ${parsed.error.issues[0].message}` }], isError: true };
906
+ return { content: [{ type: "text", text: `${parsed.error.issues[0].message}` }], isError: true };
681
907
  // Publishing is a hosted/marketplace concept - a local vault is private
682
908
  // by construction, so there is nothing to retract.
683
909
  if (localVault) {
@@ -698,7 +924,7 @@ async function handleVaultTool(name, args) {
698
924
  case "vault_delete": {
699
925
  const parsed = DeleteSchema.safeParse(args);
700
926
  if (!parsed.success)
701
- return { content: [{ type: "text", text: `Invalid input: ${parsed.error.issues[0].message}` }], isError: true };
927
+ return { content: [{ type: "text", text: `${parsed.error.issues[0].message}` }], isError: true };
702
928
  if (args?.confirm !== true) {
703
929
  return {
704
930
  content: [{
@@ -714,12 +940,26 @@ async function handleVaultTool(name, args) {
714
940
  : await (0, convex_js_1.callConvex)("/vault/delete", "POST", { key: parsed.data.key }, "vault_delete");
715
941
  if (data.error)
716
942
  return { content: [{ type: "text", text: `Error: ${data.error}` }], isError: true };
717
- return { content: [{ type: "text", text: `🗑️ Deleted: \`${parsed.data.key}\` (${data.versionsRemoved ?? 0} versions removed)` }] };
943
+ // vault_save mirrors non-credential entries into memory for search - a
944
+ // "PERMANENT... cannot be undone" delete that leaves that mirror intact
945
+ // is not actually permanent. Only relevant for the local memory-file
946
+ // backend (the hosted Convex path cleans its own memories table inside
947
+ // the /vault/delete mutation itself, same request, no separate call).
948
+ let memoriesRemoved = 0;
949
+ const localMem = (0, local_memory_js_1.getLocalMemoryConfig)();
950
+ if (localMem) {
951
+ memoriesRemoved = (0, local_memory_js_1.localMemoryDeleteByVaultKey)(localMem, parsed.data.key);
952
+ }
953
+ else if (typeof data.memoriesRemoved === "number") {
954
+ memoriesRemoved = data.memoriesRemoved;
955
+ }
956
+ const memoryNote = memoriesRemoved > 0 ? ` + ${memoriesRemoved} memory mirror${memoriesRemoved === 1 ? "" : "s"} removed` : "";
957
+ return { content: [{ type: "text", text: `🗑️ Deleted: \`${parsed.data.key}\` (${data.versionsRemoved ?? 0} versions removed${memoryNote})` }] };
718
958
  }
719
959
  case "vault_tag": {
720
960
  const parsed = TagSchema.safeParse(args);
721
961
  if (!parsed.success)
722
- return { content: [{ type: "text", text: `Invalid input: ${parsed.error.issues[0].message}` }], isError: true };
962
+ return { content: [{ type: "text", text: `${parsed.error.issues[0].message}` }], isError: true };
723
963
  const { key, tags, replace = false } = parsed.data;
724
964
  const data = localVault
725
965
  ? (0, local_vault_js_1.localVaultTag)(localVault, key, tags, replace)
@@ -731,7 +971,7 @@ async function handleVaultTool(name, args) {
731
971
  case "vault_link": {
732
972
  const parsed = LinkSchema.safeParse(args);
733
973
  if (!parsed.success)
734
- return { content: [{ type: "text", text: `Invalid input: ${parsed.error.issues[0].message}` }], isError: true };
974
+ return { content: [{ type: "text", text: `${parsed.error.issues[0].message}` }], isError: true };
735
975
  const { fromKey, toKey, relation } = parsed.data;
736
976
  const data = localVault
737
977
  ? (0, local_vault_js_1.localVaultLink)(localVault, fromKey, toKey, relation)
@@ -744,7 +984,7 @@ async function handleVaultTool(name, args) {
744
984
  case "vault_related": {
745
985
  const parsed = RelatedSchema.safeParse(args);
746
986
  if (!parsed.success)
747
- return { content: [{ type: "text", text: `Invalid input: ${parsed.error.issues[0].message}` }], isError: true };
987
+ return { content: [{ type: "text", text: `${parsed.error.issues[0].message}` }], isError: true };
748
988
  const { key, relation } = parsed.data;
749
989
  const params = new URLSearchParams({ key });
750
990
  if (relation)
@@ -760,6 +1000,20 @@ async function handleVaultTool(name, args) {
760
1000
  const lines = items.map(r => `- **${r.title}** (\`${r.key}\`) [${r.type}] - ${r.direction} \`${r.relation}\``);
761
1001
  return { content: [{ type: "text", text: `## Related entries for \`${key}\` (${items.length})\n\n${lines.join("\n")}` }] };
762
1002
  }
1003
+ case "list_projects": {
1004
+ if (localVault) {
1005
+ return { content: [{ type: "text", text: "Local vault mode has no project concept - `workspaceProject` has no effect, and there's nothing to list here." }] };
1006
+ }
1007
+ const data = await (0, convex_js_1.callConvex)("/projects/list", "GET", undefined, "list_projects");
1008
+ if (data.error)
1009
+ return { content: [{ type: "text", text: `Error: ${data.error}` }], isError: true };
1010
+ const projects = data.projects ?? [];
1011
+ if (!projects.length) {
1012
+ return { content: [{ type: "text", text: "No projects yet. Pass `workspaceProject: \"name\"` to `vault_save` or `agent_spawn` and one is created automatically." }] };
1013
+ }
1014
+ const lines = projects.map((p) => `- **${p.name}** (\`${p.slug}\`)${p.description ? ` - ${p.description}` : ""}`);
1015
+ return { content: [{ type: "text", text: `📁 **Your projects** (${projects.length})\n\n${lines.join("\n")}` }] };
1016
+ }
763
1017
  default:
764
1018
  return null;
765
1019
  }