@owrede/vault-memory 2.0.0-rc.2 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -760,6 +760,52 @@ var init_suppress_contract_write = __esm({
760
760
  }
761
761
  });
762
762
 
763
+ // src/plugin-tools/source-tools.ts
764
+ import { z as z8 } from "zod";
765
+ async function refreshHandler(args2, deps) {
766
+ const info = await deps.refresh(args2.name);
767
+ if (info === void 0) {
768
+ return { ok: false, name: args2.name, error: `unknown source: ${args2.name}` };
769
+ }
770
+ const result = {
771
+ ok: true,
772
+ name: args2.name,
773
+ status: info.status,
774
+ tool_count: info.tools.length
775
+ };
776
+ if (info.error !== void 0) result.error = info.error;
777
+ return result;
778
+ }
779
+ async function unsetHandler(args2, deps) {
780
+ const removed = deps.remove(args2.name);
781
+ return { ok: true, name: args2.name, removed };
782
+ }
783
+ var RefreshSourceArgs, refreshSourceTool, UnsetMcpClientArgs, unsetMcpClientTool;
784
+ var init_source_tools = __esm({
785
+ "src/plugin-tools/source-tools.ts"() {
786
+ "use strict";
787
+ init_esm_shims();
788
+ RefreshSourceArgs = z8.object({
789
+ name: z8.string().min(1).describe("Peer-MCP source name to refresh (re-poll tools/list).")
790
+ });
791
+ refreshSourceTool = {
792
+ name: "refresh_source",
793
+ description: "Re-poll tools/list against a live peer-MCP source and refresh its cached tool list. Returns the updated status (connected/unavailable/unreachable) and tool_count. SOURCES-REGISTRY \xA76.3.",
794
+ inputSchema: RefreshSourceArgs,
795
+ handler: refreshHandler
796
+ };
797
+ UnsetMcpClientArgs = z8.object({
798
+ name: z8.string().min(1).describe("Peer-MCP source name to disconnect + drop from the registry.")
799
+ });
800
+ unsetMcpClientTool = {
801
+ name: "unset_mcp_client",
802
+ description: "Disconnect a live peer-MCP source and drop it from the running registry. Idempotent (removed:false when the name is unknown). Affects the running process only \u2014 pair with set_mcp_client({name, remove:true}) to also delete the persisted config entry. SOURCES-REGISTRY \xA76.2.",
803
+ inputSchema: UnsetMcpClientArgs,
804
+ handler: unsetHandler
805
+ };
806
+ }
807
+ });
808
+
763
809
  // src/plugin-tools/index.ts
764
810
  function ok(data) {
765
811
  return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
@@ -921,6 +967,54 @@ function syncPluginTools(server, registered, opts) {
921
967
  }
922
968
  }
923
969
  )
970
+ },
971
+ {
972
+ name: "refresh_source",
973
+ reg: () => server.registerTool(
974
+ refreshSourceTool.name,
975
+ {
976
+ description: refreshSourceTool.description,
977
+ inputSchema: refreshSourceTool.inputSchema.shape
978
+ },
979
+ async (args2) => {
980
+ try {
981
+ const validated = refreshSourceTool.inputSchema.parse(
982
+ args2
983
+ );
984
+ const result = await refreshSourceTool.handler(
985
+ validated,
986
+ opts.sourceRegistry
987
+ );
988
+ return ok(result);
989
+ } catch (err) {
990
+ return errorResponse(err instanceof Error ? err.message : String(err));
991
+ }
992
+ }
993
+ )
994
+ },
995
+ {
996
+ name: "unset_mcp_client",
997
+ reg: () => server.registerTool(
998
+ unsetMcpClientTool.name,
999
+ {
1000
+ description: unsetMcpClientTool.description,
1001
+ inputSchema: unsetMcpClientTool.inputSchema.shape
1002
+ },
1003
+ async (args2) => {
1004
+ try {
1005
+ const validated = unsetMcpClientTool.inputSchema.parse(
1006
+ args2
1007
+ );
1008
+ const result = await unsetMcpClientTool.handler(
1009
+ validated,
1010
+ opts.sourceRegistry
1011
+ );
1012
+ return ok(result);
1013
+ } catch (err) {
1014
+ return errorResponse(err instanceof Error ? err.message : String(err));
1015
+ }
1016
+ }
1017
+ )
924
1018
  }
925
1019
  ];
926
1020
  for (const { name, reg } of adds) {
@@ -941,12 +1035,14 @@ var init_plugin_tools = __esm({
941
1035
  init_get_runtime_stats();
942
1036
  init_trigger_reindex();
943
1037
  init_suppress_contract_write();
1038
+ init_source_tools();
944
1039
  init_set_runtime_config();
945
1040
  init_resolve_secret();
946
1041
  init_set_mcp_client();
947
1042
  init_get_runtime_stats();
948
1043
  init_trigger_reindex();
949
1044
  init_suppress_contract_write();
1045
+ init_source_tools();
950
1046
  init_runtime_config();
951
1047
  PLUGIN_TOOL_NAMES = [
952
1048
  "set_runtime_config",
@@ -954,7 +1050,10 @@ var init_plugin_tools = __esm({
954
1050
  "set_mcp_client",
955
1051
  "get_runtime_stats",
956
1052
  "trigger_reindex",
957
- "suppress_contract_write"
1053
+ "suppress_contract_write",
1054
+ // SOURCES-REGISTRY.md §6 (Stage 2) — live-registry source management.
1055
+ "refresh_source",
1056
+ "unset_mcp_client"
958
1057
  ];
959
1058
  }
960
1059
  });
@@ -2957,7 +3056,7 @@ var init_sections = __esm({
2957
3056
  constructor(db) {
2958
3057
  this.db = db;
2959
3058
  this._insert = db.prepare(`
2960
- INSERT INTO sections
3059
+ INSERT OR IGNORE INTO sections
2961
3060
  (note_id, anchor, heading_path, heading_text, level,
2962
3061
  parent_id, ord, chunk_id_first, chunk_id_last, created_at)
2963
3062
  VALUES
@@ -3027,6 +3126,33 @@ var init_sections = __esm({
3027
3126
  tx(rows);
3028
3127
  return ids;
3029
3128
  }
3129
+ /**
3130
+ * Insert one section, collision-safe. Returns the id of the row that
3131
+ * now owns (note_id, anchor): the freshly inserted row, or — when a
3132
+ * same-anchor sibling already won the unique slot — that surviving
3133
+ * row's id (so callers can resolve parent_id linkage). Mirrors the
3134
+ * backfill behavior in src/sections/backfill.ts. The live indexer
3135
+ * uses this instead of `insertMany` so duplicate-anchor sibling
3136
+ * headings can't abort the whole index run
3137
+ * (see ISSUE-indexer-duplicate-anchor.md).
3138
+ */
3139
+ insertOneResolving(r) {
3140
+ const info = this._insert.run({
3141
+ note_id: r.note_id,
3142
+ anchor: r.anchor,
3143
+ heading_path: r.heading_path,
3144
+ heading_text: r.heading_text,
3145
+ level: r.level,
3146
+ parent_id: r.parent_id,
3147
+ ord: r.ord,
3148
+ chunk_id_first: r.chunk_id_first,
3149
+ chunk_id_last: r.chunk_id_last,
3150
+ created_at: Date.now()
3151
+ });
3152
+ if (info.changes > 0) return Number(info.lastInsertRowid);
3153
+ const existing = this._getByAnchor.get(r.note_id, r.anchor);
3154
+ return existing ? Number(existing.id) : null;
3155
+ }
3030
3156
  deleteByNote(noteId) {
3031
3157
  return this._deleteByNote.run(noteId).changes;
3032
3158
  }
@@ -3525,7 +3651,7 @@ var init_retry = __esm({
3525
3651
  });
3526
3652
 
3527
3653
  // src/ollama/client.ts
3528
- import { z as z8 } from "zod";
3654
+ import { z as z9 } from "zod";
3529
3655
  function isRetryable(err) {
3530
3656
  if (err instanceof OllamaHttpError) {
3531
3657
  return err.status >= 500 && err.status < 600;
@@ -3548,26 +3674,26 @@ var init_client = __esm({
3548
3674
  DEFAULT_BATCH_SIZE = 10;
3549
3675
  DEFAULT_TIMEOUT_MS = 3e4;
3550
3676
  DEFAULT_RETRIES = 3;
3551
- EmbedResponseSchema = z8.object({
3552
- embeddings: z8.array(z8.array(z8.number())),
3553
- model: z8.string().optional()
3677
+ EmbedResponseSchema = z9.object({
3678
+ embeddings: z9.array(z9.array(z9.number())),
3679
+ model: z9.string().optional()
3554
3680
  });
3555
- TagsResponseSchema = z8.object({
3556
- models: z8.array(
3557
- z8.object({
3558
- name: z8.string()
3681
+ TagsResponseSchema = z9.object({
3682
+ models: z9.array(
3683
+ z9.object({
3684
+ name: z9.string()
3559
3685
  })
3560
3686
  )
3561
3687
  });
3562
- ChatResponseSchema = z8.object({
3563
- model: z8.string(),
3564
- message: z8.object({
3565
- role: z8.literal("assistant"),
3566
- content: z8.string()
3688
+ ChatResponseSchema = z9.object({
3689
+ model: z9.string(),
3690
+ message: z9.object({
3691
+ role: z9.literal("assistant"),
3692
+ content: z9.string()
3567
3693
  }),
3568
- done: z8.boolean().optional(),
3569
- total_duration: z8.number().optional(),
3570
- eval_count: z8.number().optional()
3694
+ done: z9.boolean().optional(),
3695
+ total_duration: z9.number().optional(),
3696
+ eval_count: z9.number().optional()
3571
3697
  });
3572
3698
  OllamaHttpError = class extends Error {
3573
3699
  status;
@@ -5434,7 +5560,16 @@ var init_obsidian_fs = __esm({
5434
5560
  if (since !== void 0 && mtime < since) continue;
5435
5561
  const body = await fs4.readFile(abs, "utf-8");
5436
5562
  const hash = computeBodyHash(body);
5437
- yield { id: this.pathToDocId(rel), mtime, hash };
5563
+ let id;
5564
+ try {
5565
+ id = this.pathToDocId(rel);
5566
+ } catch (err) {
5567
+ console.error(
5568
+ `[obsidian-fs:${this.vault.name}] skipping un-addressable file ${JSON.stringify(rel)}: ${err instanceof Error ? err.message : String(err)}`
5569
+ );
5570
+ continue;
5571
+ }
5572
+ yield { id, mtime, hash };
5438
5573
  yielded++;
5439
5574
  }
5440
5575
  }
@@ -6244,7 +6379,14 @@ async function indexVault(vault, options) {
6244
6379
  chunkIdFragment: computeChunkIdFragment(c.text)
6245
6380
  }));
6246
6381
  const chunkIds = vault.db.chunks.insertBatch(noteId, chunkInputs);
6247
- buildSectionsForNote(vault, noteId, parsed.content, chunkIds);
6382
+ try {
6383
+ buildSectionsForNote(vault, noteId, parsed.content, chunkIds);
6384
+ } catch (err) {
6385
+ const message = err instanceof Error ? err.message : String(err);
6386
+ console.error(
6387
+ `[indexer:${vault.config.name}] section build failed for ${parsed.relativePath}: ${message} \u2014 skipping sections for this note`
6388
+ );
6389
+ }
6248
6390
  const embedResult = await options.ollama.embed({
6249
6391
  model: options.embeddingModel,
6250
6392
  texts: chunks.map((c) => c.text)
@@ -6409,15 +6551,15 @@ function buildSectionsForNote(vault, noteId, content, insertedChunkIds) {
6409
6551
  chunk_id_first: pair.first,
6410
6552
  chunk_id_last: pair.last
6411
6553
  };
6412
- const ids = vault.db.sections.insertMany([row]);
6413
- insertedIds.push(ids[0]);
6554
+ insertedIds.push(vault.db.sections.insertOneResolving(row));
6414
6555
  }
6415
6556
  return insertedIds.length;
6416
6557
  }
6417
6558
  function mapChunksToSections(chunks, sectionRanges) {
6418
- const out = sectionRanges.map(
6419
- () => ({ first: null, last: null })
6420
- );
6559
+ const out = sectionRanges.map(() => ({
6560
+ first: null,
6561
+ last: null
6562
+ }));
6421
6563
  for (const chunk of chunks) {
6422
6564
  const offset = chunk.start_offset;
6423
6565
  let chosenIdx = null;
@@ -7395,7 +7537,7 @@ var init_validator = __esm({
7395
7537
  });
7396
7538
 
7397
7539
  // src/memory/contract/default-v1.ts
7398
- import { z as z9 } from "zod";
7540
+ import { z as z10 } from "zod";
7399
7541
  var requiredKeys, baseShape, DEFAULT_MEMORY_V1;
7400
7542
  var init_default_v1 = __esm({
7401
7543
  "src/memory/contract/default-v1.ts"() {
@@ -7410,15 +7552,15 @@ var init_default_v1 = __esm({
7410
7552
  "superseded_by",
7411
7553
  "type"
7412
7554
  ];
7413
- baseShape = z9.object({
7414
- source: z9.enum(["agent", "user", "imported"]),
7415
- confidence: z9.enum(["direct", "inferred", "uncertain"]),
7416
- evidence: z9.array(z9.string()),
7417
- status: z9.enum(["active", "superseded", "archived"]).default("active"),
7418
- observed_at: z9.string().datetime({ offset: true }),
7419
- superseded_by: z9.string().nullable().default(null),
7420
- type: z9.string().min(1),
7421
- superseded_reason: z9.string().optional()
7555
+ baseShape = z10.object({
7556
+ source: z10.enum(["agent", "user", "imported"]),
7557
+ confidence: z10.enum(["direct", "inferred", "uncertain"]),
7558
+ evidence: z10.array(z10.string()),
7559
+ status: z10.enum(["active", "superseded", "archived"]).default("active"),
7560
+ observed_at: z10.string().datetime({ offset: true }),
7561
+ superseded_by: z10.string().nullable().default(null),
7562
+ type: z10.string().min(1),
7563
+ superseded_reason: z10.string().optional()
7422
7564
  }).passthrough().superRefine((data, ctx) => {
7423
7565
  if (data.status === "superseded") {
7424
7566
  if (data.superseded_by === null || data.superseded_by === void 0) {
@@ -7451,7 +7593,7 @@ var init_default_v1 = __esm({
7451
7593
  });
7452
7594
 
7453
7595
  // src/memory/contract/default-brief-v1.ts
7454
- import { z as z10 } from "zod";
7596
+ import { z as z11 } from "zod";
7455
7597
  var requiredKeys2, baseShape2, DEFAULT_BRIEF_V1;
7456
7598
  var init_default_brief_v1 = __esm({
7457
7599
  "src/memory/contract/default-brief-v1.ts"() {
@@ -7473,29 +7615,29 @@ var init_default_brief_v1 = __esm({
7473
7615
  "compiled_at",
7474
7616
  "source_hashes"
7475
7617
  ];
7476
- baseShape2 = z10.object({
7618
+ baseShape2 = z11.object({
7477
7619
  // ── Base shape inherited from default-v1 ────────────────────────
7478
- source: z10.enum(["agent", "user", "imported"]),
7479
- confidence: z10.enum(["direct", "inferred", "uncertain"]),
7480
- evidence: z10.array(z10.string()),
7620
+ source: z11.enum(["agent", "user", "imported"]),
7621
+ confidence: z11.enum(["direct", "inferred", "uncertain"]),
7622
+ evidence: z11.array(z11.string()),
7481
7623
  // ── Status enum WIDENED for briefs: + "stale" ──────────────────
7482
- status: z10.enum(["active", "stale", "superseded", "archived"]).default("active"),
7483
- observed_at: z10.string().datetime({ offset: true }),
7484
- superseded_by: z10.string().nullable().default(null),
7485
- type: z10.string().min(1),
7486
- superseded_reason: z10.string().optional(),
7624
+ status: z11.enum(["active", "stale", "superseded", "archived"]).default("active"),
7625
+ observed_at: z11.string().datetime({ offset: true }),
7626
+ superseded_by: z11.string().nullable().default(null),
7627
+ type: z11.string().min(1),
7628
+ superseded_reason: z11.string().optional(),
7487
7629
  // ── Brief-specific properties (D-11 brief shape) ───────────────
7488
- target: z10.string().min(1),
7630
+ target: z11.string().min(1),
7489
7631
  /**
7490
7632
  * Brief purpose — free text but bounded at 500 chars so
7491
7633
  * `list_briefs` stays scannable. Lower bound `min(1)` matches
7492
7634
  * BRF-03 "no empty purpose".
7493
7635
  */
7494
- purpose: z10.string().min(1).max(500),
7636
+ purpose: z11.string().min(1).max(500),
7495
7637
  /** DocId list of all sources the brief was compiled from. */
7496
- compiled_from: z10.array(z10.string()).min(1),
7638
+ compiled_from: z11.array(z11.string()).min(1),
7497
7639
  /** ISO-8601 datetime with offset (mirrors observed_at). */
7498
- compiled_at: z10.string().datetime({ offset: true }),
7640
+ compiled_at: z11.string().datetime({ offset: true }),
7499
7641
  /**
7500
7642
  * Record<ChunkId, BriefSourceHash> — staleness contract. The map
7501
7643
  * key is the public ChunkId (`<DocId>#chunk-<7-hex>`); the value
@@ -7503,12 +7645,12 @@ var init_default_brief_v1 = __esm({
7503
7645
  * the cross-field invariant below only REQUIRES it on stale; the
7504
7646
  * validator still rejects `status: "stale"` writes that omit it.
7505
7647
  */
7506
- source_hashes: z10.record(z10.string(), z10.string()).optional(),
7648
+ source_hashes: z11.record(z11.string(), z11.string()).optional(),
7507
7649
  /**
7508
7650
  * Daemon-computed list of source DocIds whose hashes have
7509
7651
  * diverged. Populated when `status` flips to `"stale"`.
7510
7652
  */
7511
- changed_sources: z10.array(z10.string()).optional()
7653
+ changed_sources: z11.array(z11.string()).optional()
7512
7654
  }).passthrough().superRefine((data, ctx) => {
7513
7655
  if (data.status === "superseded") {
7514
7656
  if (data.superseded_by === null || data.superseded_by === void 0) {
@@ -7578,36 +7720,36 @@ var init_contract_yaml_read = __esm({
7578
7720
  });
7579
7721
 
7580
7722
  // src/memory/contract/schema.ts
7581
- import { z as z11 } from "zod";
7723
+ import { z as z12 } from "zod";
7582
7724
  var PropertyRuleSchema, CrossFieldRuleSchema, MemoryContractYamlSchema;
7583
7725
  var init_schema2 = __esm({
7584
7726
  "src/memory/contract/schema.ts"() {
7585
7727
  "use strict";
7586
7728
  init_esm_shims();
7587
- PropertyRuleSchema = z11.object({
7588
- type: z11.enum(["string", "datetime", "array", "doc_id", "number", "boolean", "reference", "date"]),
7589
- allowed: z11.array(z11.string()).optional(),
7590
- default: z11.unknown().optional(),
7591
- items: z11.object({ type: z11.string() }).optional(),
7592
- min_length: z11.number().optional(),
7729
+ PropertyRuleSchema = z12.object({
7730
+ type: z12.enum(["string", "datetime", "array", "doc_id", "number", "boolean", "reference", "date"]),
7731
+ allowed: z12.array(z12.string()).optional(),
7732
+ default: z12.unknown().optional(),
7733
+ items: z12.object({ type: z12.string() }).optional(),
7734
+ min_length: z12.number().optional(),
7593
7735
  /** When true, the property accepts `null` as a sentinel value (in
7594
7736
  * addition to whatever `type` says). Used for required-but-null-by-
7595
7737
  * default properties like `superseded_by` on active observations. */
7596
- nullable: z11.boolean().optional()
7738
+ nullable: z12.boolean().optional()
7597
7739
  });
7598
- CrossFieldRuleSchema = z11.object({
7599
- when: z11.string(),
7600
- require: z11.string()
7740
+ CrossFieldRuleSchema = z12.object({
7741
+ when: z12.string(),
7742
+ require: z12.string()
7601
7743
  });
7602
- MemoryContractYamlSchema = z11.object({
7603
- name: z11.string().min(1),
7604
- version: z11.string().default("1.0"),
7605
- required_properties: z11.record(z11.string(), PropertyRuleSchema),
7606
- optional_properties: z11.record(z11.string(), PropertyRuleSchema).default({}),
7607
- cross_field_rules: z11.array(CrossFieldRuleSchema).default([]),
7608
- naming: z11.object({
7609
- strategy: z11.enum(["caller-provided", "date-slug", "adapter-assigned"]),
7610
- pattern: z11.string().optional()
7744
+ MemoryContractYamlSchema = z12.object({
7745
+ name: z12.string().min(1),
7746
+ version: z12.string().default("1.0"),
7747
+ required_properties: z12.record(z12.string(), PropertyRuleSchema),
7748
+ optional_properties: z12.record(z12.string(), PropertyRuleSchema).default({}),
7749
+ cross_field_rules: z12.array(CrossFieldRuleSchema).default([]),
7750
+ naming: z12.object({
7751
+ strategy: z12.enum(["caller-provided", "date-slug", "adapter-assigned"]),
7752
+ pattern: z12.string().optional()
7611
7753
  })
7612
7754
  });
7613
7755
  }
@@ -7615,7 +7757,7 @@ var init_schema2 = __esm({
7615
7757
 
7616
7758
  // src/memory/contract/loader.ts
7617
7759
  import { parse as parseYaml } from "yaml";
7618
- import { z as z12 } from "zod";
7760
+ import { z as z13 } from "zod";
7619
7761
  function __cacheContract(name, contract) {
7620
7762
  contractCache.set(name, contract);
7621
7763
  }
@@ -9213,7 +9355,7 @@ var init_memory_stats = __esm({
9213
9355
  });
9214
9356
 
9215
9357
  // src/memory/resources/index.ts
9216
- var RESOURCE_URI_LIST_SINKS, RESOURCE_URI_MEMORY_STATS, RESOURCE_URI_LIST_BRIEFS, RESOURCE_URI_LIST_CONTRACTS, RESOURCE_URI_LIST_CONTRACT_VERBS, RESOURCE_URI_VAULTS, RESOURCE_URI_MODELS, RESOURCE_URI_RECENT, RESOURCE_URI_STATS, RESOURCE_URI_BACKLINKS;
9358
+ var RESOURCE_URI_LIST_SINKS, RESOURCE_URI_MEMORY_STATS, RESOURCE_URI_LIST_BRIEFS, RESOURCE_URI_LIST_CONTRACTS, RESOURCE_URI_LIST_CONTRACT_VERBS, RESOURCE_URI_SOURCES, RESOURCE_URI_VAULTS, RESOURCE_URI_MODELS, RESOURCE_URI_RECENT, RESOURCE_URI_STATS, RESOURCE_URI_BACKLINKS;
9217
9359
  var init_resources = __esm({
9218
9360
  "src/memory/resources/index.ts"() {
9219
9361
  "use strict";
@@ -9225,6 +9367,7 @@ var init_resources = __esm({
9225
9367
  RESOURCE_URI_LIST_BRIEFS = "vault-memory://briefs";
9226
9368
  RESOURCE_URI_LIST_CONTRACTS = "vault-memory://contracts";
9227
9369
  RESOURCE_URI_LIST_CONTRACT_VERBS = "vault-memory://contract-verbs";
9370
+ RESOURCE_URI_SOURCES = "vault-memory://sources";
9228
9371
  RESOURCE_URI_VAULTS = "vault-memory://vaults";
9229
9372
  RESOURCE_URI_MODELS = "vault-memory://models";
9230
9373
  RESOURCE_URI_RECENT = "vault-memory://recent";
@@ -9286,6 +9429,25 @@ var init_resource_registry = __esm({
9286
9429
  description: "List baseline assembly verbs + custom (mcp://) verbs in use, with invocation_count + last_seen aggregated from contract_audit (D-A2b). Baseline verbs are constant per ADR-006 \xA7Decision 3.",
9287
9430
  mimeType: "application/json"
9288
9431
  },
9432
+ // ─── SOURCES-REGISTRY.md §5 (Stage 2) — peer-MCP source discovery ───────
9433
+ {
9434
+ name: "sources",
9435
+ uriTemplate: "vault-memory://sources",
9436
+ description: "List peer MCP servers vault-memory connects to, with per-source status (connected/unavailable/unreachable), tool_count, and last_refreshed. vault-memory itself is not included. SOURCES-REGISTRY \xA75.1.",
9437
+ mimeType: "application/json"
9438
+ },
9439
+ {
9440
+ name: "source-tools",
9441
+ uriTemplate: "vault-memory://sources/{name}/tools",
9442
+ description: "List the cached tools/list for one peer MCP source. Empty when the source is not connected. SOURCES-REGISTRY \xA75.2.",
9443
+ mimeType: "application/json"
9444
+ },
9445
+ {
9446
+ name: "source-tool",
9447
+ uriTemplate: "vault-memory://sources/{name}/tools/{tool}",
9448
+ description: "Read a single tool's schema from one peer MCP source, inlined from the cached tools/list. SOURCES-REGISTRY \xA75.3.",
9449
+ mimeType: "application/json"
9450
+ },
9289
9451
  // ─── Phase 8 (Plan 08-05 / REL-08) — promoted from v1 tools ─────────────
9290
9452
  {
9291
9453
  name: "vaults",
@@ -11797,11 +11959,11 @@ var init_obsidian_fs3 = __esm({
11797
11959
  });
11798
11960
 
11799
11961
  // src/tool-registry.ts
11800
- import { z as z13 } from "zod";
11962
+ import { z as z14 } from "zod";
11801
11963
  function buildToolSchema(name) {
11802
11964
  const builder = SCHEMA_BUILDERS[name];
11803
11965
  if (builder) return builder();
11804
- return z13.object(TOOL_SCHEMAS[name]);
11966
+ return z14.object(TOOL_SCHEMAS[name]);
11805
11967
  }
11806
11968
  var TOOLS, DOC_ID_PATTERN2, PredicateSchema, TOOL_SCHEMAS, SCHEMA_BUILDERS;
11807
11969
  var init_tool_registry = __esm({
@@ -12674,243 +12836,243 @@ var init_tool_registry = __esm({
12674
12836
  }
12675
12837
  ];
12676
12838
  DOC_ID_PATTERN2 = /^[a-z][a-z0-9-]*:\/\/[^/]+\/.+$/;
12677
- PredicateSchema = z13.union([
12678
- z13.string(),
12679
- z13.number(),
12680
- z13.boolean(),
12681
- z13.null(),
12682
- z13.object({ $in: z13.array(z13.union([z13.string(), z13.number(), z13.boolean(), z13.null()])) }),
12683
- z13.object({ $exists: z13.boolean() }),
12684
- z13.object({ $contains: z13.union([z13.string(), z13.number(), z13.boolean(), z13.null()]) })
12839
+ PredicateSchema = z14.union([
12840
+ z14.string(),
12841
+ z14.number(),
12842
+ z14.boolean(),
12843
+ z14.null(),
12844
+ z14.object({ $in: z14.array(z14.union([z14.string(), z14.number(), z14.boolean(), z14.null()])) }),
12845
+ z14.object({ $exists: z14.boolean() }),
12846
+ z14.object({ $contains: z14.union([z14.string(), z14.number(), z14.boolean(), z14.null()]) })
12685
12847
  ]);
12686
12848
  TOOL_SCHEMAS = {
12687
12849
  list_vaults: {},
12688
12850
  read_note: {
12689
- vault: z13.string(),
12690
- path: z13.string()
12851
+ vault: z14.string(),
12852
+ path: z14.string()
12691
12853
  },
12692
12854
  search_semantic: {
12693
- query: z13.string().min(1),
12694
- vaults: z13.array(z13.string()).optional(),
12695
- top_k: z13.number().int().positive().max(100).optional().default(10),
12696
- exclude_paths: z13.array(z13.string()).optional()
12855
+ query: z14.string().min(1),
12856
+ vaults: z14.array(z14.string()).optional(),
12857
+ top_k: z14.number().int().positive().max(100).optional().default(10),
12858
+ exclude_paths: z14.array(z14.string()).optional()
12697
12859
  },
12698
12860
  search_text: {
12699
- query: z13.string().min(1),
12700
- vaults: z13.array(z13.string()).optional(),
12701
- top_k: z13.number().int().positive().max(100).optional().default(10),
12702
- exclude_paths: z13.array(z13.string()).optional()
12861
+ query: z14.string().min(1),
12862
+ vaults: z14.array(z14.string()).optional(),
12863
+ top_k: z14.number().int().positive().max(100).optional().default(10),
12864
+ exclude_paths: z14.array(z14.string()).optional()
12703
12865
  },
12704
12866
  search_hybrid: {
12705
- query: z13.string().min(1),
12706
- vaults: z13.array(z13.string()).optional(),
12707
- top_k: z13.number().int().positive().max(100).optional().default(10),
12708
- rrf_k: z13.number().int().positive().max(1e3).optional().default(60),
12709
- exclude_paths: z13.array(z13.string()).optional(),
12710
- rerank: z13.boolean().optional().default(false),
12867
+ query: z14.string().min(1),
12868
+ vaults: z14.array(z14.string()).optional(),
12869
+ top_k: z14.number().int().positive().max(100).optional().default(10),
12870
+ rrf_k: z14.number().int().positive().max(1e3).optional().default(60),
12871
+ exclude_paths: z14.array(z14.string()).optional(),
12872
+ rerank: z14.boolean().optional().default(false),
12711
12873
  // Phase 3 / 03-05 additive params — D-07, D-08, ASM-07, ASM-08.
12712
12874
  // All `.optional()` with defaults that vanish when unset, so v1
12713
12875
  // callers see no behavior change.
12714
- recency_weight: z13.number().optional().default(0),
12715
- authority_weight: z13.number().optional().default(0),
12716
- half_life_days: z13.number().positive().optional().default(30),
12717
- include_superseded: z13.boolean().optional().default(false),
12876
+ recency_weight: z14.number().optional().default(0),
12877
+ authority_weight: z14.number().optional().default(0),
12878
+ half_life_days: z14.number().positive().optional().default(30),
12879
+ include_superseded: z14.boolean().optional().default(false),
12718
12880
  // ── Phase 4 / 04-04 / GRA-03 (D-15): additive auto-expansion ──
12719
12881
  // Nested under a single optional `expand` object per D-15. When
12720
12882
  // omitted, hybridSearch behavior is byte-identical to v1 (the
12721
12883
  // guard `if (opts.expand && opts.expandDeps && ...)` at the end of
12722
12884
  // `src/search/hybrid.ts` short-circuits entirely). The literal-
12723
12885
  // union for `hops` enforces the D-05 hop cap at the boundary.
12724
- expand: z13.object({
12725
- hops: z13.union([z13.literal(1), z13.literal(2)]),
12726
- direction: z13.enum(["forward", "backward", "both"]).optional(),
12727
- edge_types: z13.array(z13.enum(["wikilink", "mention", "frontmatter-ref", "hyperlink"])).optional()
12886
+ expand: z14.object({
12887
+ hops: z14.union([z14.literal(1), z14.literal(2)]),
12888
+ direction: z14.enum(["forward", "backward", "both"]).optional(),
12889
+ edge_types: z14.array(z14.enum(["wikilink", "mention", "frontmatter-ref", "hyperlink"])).optional()
12728
12890
  }).optional()
12729
12891
  },
12730
12892
  list_backlinks: {
12731
- vault: z13.string(),
12732
- path: z13.string()
12893
+ vault: z14.string(),
12894
+ path: z14.string()
12733
12895
  },
12734
12896
  list_forward_links: {
12735
- vault: z13.string(),
12736
- path: z13.string(),
12737
- include_broken: z13.boolean().optional().default(true)
12897
+ vault: z14.string(),
12898
+ path: z14.string(),
12899
+ include_broken: z14.boolean().optional().default(true)
12738
12900
  },
12739
12901
  find_broken_links: {
12740
- vault: z13.string()
12902
+ vault: z14.string()
12741
12903
  },
12742
12904
  query_frontmatter: {
12743
- vault: z13.string(),
12744
- where: z13.record(z13.string(), PredicateSchema),
12745
- limit: z13.number().int().positive().max(1e3).optional().default(100)
12905
+ vault: z14.string(),
12906
+ where: z14.record(z14.string(), PredicateSchema),
12907
+ limit: z14.number().int().positive().max(1e3).optional().default(100)
12746
12908
  },
12747
12909
  write_note: {
12748
- vault: z13.string(),
12749
- path: z13.string(),
12750
- content: z13.string(),
12751
- frontmatter: z13.record(z13.string(), z13.unknown()).nullable().optional(),
12752
- expected_hash: z13.string().optional(),
12753
- client_id: z13.string().optional()
12910
+ vault: z14.string(),
12911
+ path: z14.string(),
12912
+ content: z14.string(),
12913
+ frontmatter: z14.record(z14.string(), z14.unknown()).nullable().optional(),
12914
+ expected_hash: z14.string().optional(),
12915
+ client_id: z14.string().optional()
12754
12916
  },
12755
12917
  update_frontmatter: {
12756
- vault: z13.string(),
12757
- path: z13.string(),
12758
- merge: z13.record(z13.string(), z13.unknown()),
12759
- expected_hash: z13.string().optional(),
12760
- client_id: z13.string().optional()
12918
+ vault: z14.string(),
12919
+ path: z14.string(),
12920
+ merge: z14.record(z14.string(), z14.unknown()),
12921
+ expected_hash: z14.string().optional(),
12922
+ client_id: z14.string().optional()
12761
12923
  },
12762
12924
  delete_note: {
12763
- vault: z13.string(),
12764
- path: z13.string(),
12765
- expected_hash: z13.string(),
12766
- client_id: z13.string().optional()
12925
+ vault: z14.string(),
12926
+ path: z14.string(),
12927
+ expected_hash: z14.string(),
12928
+ client_id: z14.string().optional()
12767
12929
  },
12768
12930
  audit_log: {
12769
- vault: z13.string(),
12770
- note_path: z13.string().optional(),
12771
- op: z13.enum(["create", "update", "delete"]).optional(),
12772
- since: z13.number().int().nonnegative().optional(),
12773
- limit: z13.number().int().positive().max(1e3).optional().default(50),
12931
+ vault: z14.string(),
12932
+ note_path: z14.string().optional(),
12933
+ op: z14.enum(["create", "update", "delete"]).optional(),
12934
+ since: z14.number().int().nonnegative().optional(),
12935
+ limit: z14.number().int().positive().max(1e3).optional().default(50),
12774
12936
  // Plan 02-06 (MEM-08): additive optional filter. The MCP tool's
12775
12937
  // `description` string is INTENTIONALLY unchanged — Phase 1 byte-identity
12776
12938
  // is preserved. New capability is documented in docs/tools/audit_log.md.
12777
- is_memory_sink_write: z13.boolean().optional()
12939
+ is_memory_sink_write: z14.boolean().optional()
12778
12940
  },
12779
12941
  list_models: {
12780
- vault: z13.string()
12942
+ vault: z14.string()
12781
12943
  },
12782
12944
  start_shadow_index: {
12783
- vault: z13.string(),
12784
- model: z13.string().min(1),
12785
- batch_size: z13.number().int().positive().max(256).optional()
12945
+ vault: z14.string(),
12946
+ model: z14.string().min(1),
12947
+ batch_size: z14.number().int().positive().max(256).optional()
12786
12948
  },
12787
12949
  switch_active_model: {
12788
- vault: z13.string(),
12789
- model_name: z13.string().min(1)
12950
+ vault: z14.string(),
12951
+ model_name: z14.string().min(1)
12790
12952
  },
12791
12953
  vacuum_embeddings: {
12792
- vault: z13.string()
12954
+ vault: z14.string()
12793
12955
  },
12794
12956
  index_runs: {
12795
- vault: z13.string(),
12796
- limit: z13.number().int().positive().max(200).optional().default(20)
12957
+ vault: z14.string(),
12958
+ limit: z14.number().int().positive().max(200).optional().default(20)
12797
12959
  },
12798
12960
  search: {
12799
- query: z13.string().min(1),
12800
- limit: z13.number().int().positive().max(50).optional().default(10)
12961
+ query: z14.string().min(1),
12962
+ limit: z14.number().int().positive().max(50).optional().default(10)
12801
12963
  },
12802
12964
  fetch: {
12803
- id: z13.string().min(1)
12965
+ id: z14.string().min(1)
12804
12966
  },
12805
12967
  vault_stats: {
12806
- vault: z13.string().optional()
12968
+ vault: z14.string().optional()
12807
12969
  },
12808
12970
  recent_notes: {
12809
- vault: z13.string().optional(),
12810
- limit: z13.number().int().positive().max(200).optional().default(20),
12811
- since: z13.number().int().nonnegative().optional()
12971
+ vault: z14.string().optional(),
12972
+ limit: z14.number().int().positive().max(200).optional().default(20),
12973
+ since: z14.number().int().nonnegative().optional()
12812
12974
  },
12813
12975
  suggest_frontmatter: {
12814
- vault: z13.string(),
12815
- path: z13.string().optional(),
12816
- content: z13.string().optional(),
12817
- title: z13.string().optional(),
12818
- folder_hint: z13.string().optional()
12976
+ vault: z14.string(),
12977
+ path: z14.string().optional(),
12978
+ content: z14.string().optional(),
12979
+ title: z14.string().optional(),
12980
+ folder_hint: z14.string().optional()
12819
12981
  },
12820
12982
  // ── Phase 2 memory tools (Plan 02-04) ───────────────────────────────────
12821
12983
  record_observation: {
12822
- vault: z13.string().min(1).describe("Vault name (registered in [vaults] config block)"),
12823
- claim: z13.string().min(1).describe("Short natural-language statement of the observation (becomes title + body)"),
12824
- evidence: z13.array(z13.string()).describe("DocIds or quoted source spans supporting the claim; empty array allowed"),
12825
- confidence: z13.enum(["direct", "inferred", "uncertain"]).describe("How the agent arrived at this claim"),
12826
- type: z13.string().min(1).describe(
12984
+ vault: z14.string().min(1).describe("Vault name (registered in [vaults] config block)"),
12985
+ claim: z14.string().min(1).describe("Short natural-language statement of the observation (becomes title + body)"),
12986
+ evidence: z14.array(z14.string()).describe("DocIds or quoted source spans supporting the claim; empty array allowed"),
12987
+ confidence: z14.enum(["direct", "inferred", "uncertain"]).describe("How the agent arrived at this claim"),
12988
+ type: z14.string().min(1).describe(
12827
12989
  "Observation type per the sink contract (e.g. 'observation', 'hypothesis', 'decision')"
12828
12990
  ),
12829
- sink: z13.string().min(1).optional().describe(
12991
+ sink: z14.string().min(1).optional().describe(
12830
12992
  "Memory sink name OR full obsidian-fs://\u2026 handle. Defaults to the vault's default sink."
12831
12993
  ),
12832
- properties: z13.record(z13.string(), z13.unknown()).optional().describe(
12994
+ properties: z14.record(z14.string(), z14.unknown()).optional().describe(
12833
12995
  "Escape-hatch: contract-allowed extra properties; merged AFTER sugar args (caller wins)"
12834
12996
  )
12835
12997
  },
12836
12998
  supersede: {
12837
- doc_id: z13.string().regex(DOC_ID_PATTERN2).describe("DocId of the document being superseded"),
12838
- replacement_doc_id: z13.string().regex(DOC_ID_PATTERN2).describe("DocId of the replacement document"),
12839
- reason: z13.string().min(1).describe("Why the old document is being retired; written to superseded_reason")
12999
+ doc_id: z14.string().regex(DOC_ID_PATTERN2).describe("DocId of the document being superseded"),
13000
+ replacement_doc_id: z14.string().regex(DOC_ID_PATTERN2).describe("DocId of the replacement document"),
13001
+ reason: z14.string().min(1).describe("Why the old document is being retired; written to superseded_reason")
12840
13002
  },
12841
13003
  // ── Phase 5 brief tools (Plan 05-02 / BRF-03, BRF-04) ───────────────────
12842
13004
  compile_brief: {
12843
- vault: z13.string().min(1).describe("Vault name (registered in [vaults] config block)"),
12844
- target: z13.string().min(1).describe("Stable cross-version handle for the brief (e.g. 'atlas-q3')"),
12845
- source_doc_ids: z13.array(z13.string().regex(DOC_ID_PATTERN2)).min(1).max(50).describe("DocIds the brief is compiled from; deduped, capped at 50 (D-03)"),
12846
- purpose: z13.string().min(1).max(500).describe("Free-form purpose; bounded so list_briefs stays scannable"),
12847
- max_tokens: z13.number().int().positive().optional().default(2e3).describe("Hint for the LLM ladder; default 2000"),
12848
- prepared_text: z13.string().min(1).optional().describe("D-10 tier 3 fallback when no LLM is reachable \u2014 verbatim body to stitch in"),
12849
- sink: z13.string().min(1).optional().describe("Override the default `_memory/_briefs` sink")
13005
+ vault: z14.string().min(1).describe("Vault name (registered in [vaults] config block)"),
13006
+ target: z14.string().min(1).describe("Stable cross-version handle for the brief (e.g. 'atlas-q3')"),
13007
+ source_doc_ids: z14.array(z14.string().regex(DOC_ID_PATTERN2)).min(1).max(50).describe("DocIds the brief is compiled from; deduped, capped at 50 (D-03)"),
13008
+ purpose: z14.string().min(1).max(500).describe("Free-form purpose; bounded so list_briefs stays scannable"),
13009
+ max_tokens: z14.number().int().positive().optional().default(2e3).describe("Hint for the LLM ladder; default 2000"),
13010
+ prepared_text: z14.string().min(1).optional().describe("D-10 tier 3 fallback when no LLM is reachable \u2014 verbatim body to stitch in"),
13011
+ sink: z14.string().min(1).optional().describe("Override the default `_memory/_briefs` sink")
12850
13012
  },
12851
13013
  get_brief: {
12852
- vault: z13.string().min(1).describe("Vault name (registered in [vaults] config block)"),
12853
- target: z13.string().min(1).describe("Stable cross-version handle for the brief"),
12854
- max_age_days: z13.number().int().nonnegative().optional().describe("Reject briefs older than this many days unless allow_stale=true"),
12855
- allow_stale: z13.boolean().optional().default(false).describe(
13014
+ vault: z14.string().min(1).describe("Vault name (registered in [vaults] config block)"),
13015
+ target: z14.string().min(1).describe("Stable cross-version handle for the brief"),
13016
+ max_age_days: z14.number().int().nonnegative().optional().describe("Reject briefs older than this many days unless allow_stale=true"),
13017
+ allow_stale: z14.boolean().optional().default(false).describe(
12856
13018
  "When true, return briefs flagged stale or too_old with annotation rather than null"
12857
13019
  )
12858
13020
  },
12859
13021
  // ── Phase 3 assembly tools (Plan 03-02 / ASM-02) ────────────────────────
12860
13022
  get_outline: {
12861
- doc_id: z13.string().regex(DOC_ID_PATTERN2).describe("Opaque DocId (obsidian-fs://<vault>/<path>) of the document"),
12862
- vaults: z13.array(z13.string().min(1)).optional().describe("Optional vault filter; usually omitted (the DocId names a vault)")
13023
+ doc_id: z14.string().regex(DOC_ID_PATTERN2).describe("Opaque DocId (obsidian-fs://<vault>/<path>) of the document"),
13024
+ vaults: z14.array(z14.string().min(1)).optional().describe("Optional vault filter; usually omitted (the DocId names a vault)")
12863
13025
  },
12864
13026
  // ── Phase 3 assembly tools (Plan 03-03) ─────────────────────────────────
12865
13027
  search_sections: {
12866
- query: z13.string().min(1),
12867
- limit: z13.number().int().positive().max(50).optional().default(10),
12868
- vaults: z13.array(z13.string().min(1)).optional(),
13028
+ query: z14.string().min(1),
13029
+ limit: z14.number().int().positive().max(50).optional().default(10),
13030
+ vaults: z14.array(z14.string().min(1)).optional(),
12869
13031
  // Forward-compat with slice 03-05's authority/staleness rescore.
12870
13032
  // Accepted today; ignored by the controller until 03-05 wires the
12871
13033
  // forwarding inside hybridSearch. See 03-03-DEVIATIONS.md.
12872
- recency_weight: z13.number().min(0).optional().default(0),
12873
- authority_weight: z13.number().min(0).optional().default(0),
12874
- include_superseded: z13.boolean().optional().default(false)
13034
+ recency_weight: z14.number().min(0).optional().default(0),
13035
+ authority_weight: z14.number().min(0).optional().default(0),
13036
+ include_superseded: z14.boolean().optional().default(false)
12875
13037
  },
12876
13038
  // ── Phase 2 memory tools (Plan 02-05) ───────────────────────────────────
12877
13039
  recall: {
12878
- query: z13.string().min(1).describe("Natural-language query; routes through hybrid (semantic + BM25) search"),
12879
- min_confidence: z13.enum(["direct", "inferred", "uncertain"]).optional().describe(
13040
+ query: z14.string().min(1).describe("Natural-language query; routes through hybrid (semantic + BM25) search"),
13041
+ min_confidence: z14.enum(["direct", "inferred", "uncertain"]).optional().describe(
12880
13042
  "Exclude docs whose confidence ordinal is lower than this (direct=3, inferred=2, uncertain=1)"
12881
13043
  ),
12882
- types: z13.array(z13.string().min(1)).optional().describe("Restrict to docs whose `type` property is in this set"),
12883
- max_age_days: z13.number().int().positive().optional().describe("Exclude docs whose `observed_at` is older than this many days"),
12884
- sink: z13.string().min(1).optional().describe(
13044
+ types: z14.array(z14.string().min(1)).optional().describe("Restrict to docs whose `type` property is in this set"),
13045
+ max_age_days: z14.number().int().positive().optional().describe("Exclude docs whose `observed_at` is older than this many days"),
13046
+ sink: z14.string().min(1).optional().describe(
12885
13047
  "Memory sink name OR full obsidian-fs://\u2026 handle. Defaults to all configured sinks."
12886
13048
  ),
12887
- limit: z13.number().int().positive().max(200).optional().describe("Maximum results AFTER filter+sort; default 20"),
12888
- vaults: z13.array(z13.string().min(1)).optional().describe("Restrict to these vault names; defaults to all configured")
13049
+ limit: z14.number().int().positive().max(200).optional().describe("Maximum results AFTER filter+sort; default 20"),
13050
+ vaults: z14.array(z14.string().min(1)).optional().describe("Restrict to these vault names; defaults to all configured")
12889
13051
  },
12890
13052
  // ── Phase 3 assembly tools (Plan 03-04 / ASM-01) ────────────────────────
12891
13053
  get_document_bundle: {
12892
- doc_id: z13.string().regex(DOC_ID_PATTERN2).describe("Opaque DocId (obsidian-fs://<vault>/<path>) of the anchor document"),
13054
+ doc_id: z14.string().regex(DOC_ID_PATTERN2).describe("Opaque DocId (obsidian-fs://<vault>/<path>) of the anchor document"),
12893
13055
  // v2.0.0 accepts only depth:1. The literal pin guarantees Zod
12894
13056
  // rejects any other value at the boundary so the controller does
12895
13057
  // not need to clamp. Phase 4 may widen additively (z.union of
12896
13058
  // literals, or `z.number().int().min(1).max(2)`).
12897
- depth: z13.literal(1).optional().default(1).describe("Link-walk depth. v2.0.0: only 1 (one-hop). Phase 4 may widen."),
12898
- vaults: z13.array(z13.string().min(1)).optional().describe("Optional vault filter; usually omitted (the DocId names a vault)")
13059
+ depth: z14.literal(1).optional().default(1).describe("Link-walk depth. v2.0.0: only 1 (one-hop). Phase 4 may widen."),
13060
+ vaults: z14.array(z14.string().min(1)).optional().describe("Optional vault filter; usually omitted (the DocId names a vault)")
12899
13061
  },
12900
13062
  // ── Phase 4 graph tools (Plan 04-03 / GRA-01) ───────────────────────────
12901
13063
  expand: {
12902
- seed_doc_ids: z13.array(z13.string().regex(DOC_ID_PATTERN2)).min(1).describe(
13064
+ seed_doc_ids: z14.array(z14.string().regex(DOC_ID_PATTERN2)).min(1).describe(
12903
13065
  "1+ opaque DocIds (e.g. obsidian-fs://<vault>/<path>) \u2014 seeds of the BFS."
12904
13066
  ),
12905
13067
  // Hops hard-capped at 2 (D-05) via Zod literal union — `hops: 3`
12906
13068
  // is rejected at the boundary; the controller does not clamp.
12907
- hops: z13.union([z13.literal(1), z13.literal(2)]).describe("Hop cap (1 or 2). v2.0.0 hard-caps at 2."),
12908
- direction: z13.enum(["forward", "backward", "both"]).optional().default("both").describe("Edge traversal direction; default 'both'."),
12909
- edge_types: z13.array(z13.enum(["wikilink", "mention", "frontmatter-ref", "hyperlink"])).optional().describe("Optional filter on edge types; default = all four types."),
12910
- filter_properties: z13.record(z13.string(), z13.unknown()).optional().describe(
13069
+ hops: z14.union([z14.literal(1), z14.literal(2)]).describe("Hop cap (1 or 2). v2.0.0 hard-caps at 2."),
13070
+ direction: z14.enum(["forward", "backward", "both"]).optional().default("both").describe("Edge traversal direction; default 'both'."),
13071
+ edge_types: z14.array(z14.enum(["wikilink", "mention", "frontmatter-ref", "hyperlink"])).optional().describe("Optional filter on edge types; default = all four types."),
13072
+ filter_properties: z14.record(z14.string(), z14.unknown()).optional().describe(
12911
13073
  "Strict-equality predicate on document properties (e.g. {type: 'Project'})."
12912
13074
  ),
12913
- include_superseded: z13.boolean().optional().default(false).describe(
13075
+ include_superseded: z14.boolean().optional().default(false).describe(
12914
13076
  "When false (default), docs whose properties.status === 'superseded' are dropped."
12915
13077
  )
12916
13078
  },
@@ -12924,50 +13086,50 @@ var init_tool_registry = __esm({
12924
13086
  // works. The runtime path goes through `buildToolSchema("cluster")`
12925
13087
  // which calls the SCHEMA_BUILDERS entry.
12926
13088
  cluster: {
12927
- query: z13.string().min(1).optional(),
12928
- seed_doc_ids: z13.array(z13.string().regex(DOC_ID_PATTERN2)).min(1).optional(),
13089
+ query: z14.string().min(1).optional(),
13090
+ seed_doc_ids: z14.array(z14.string().regex(DOC_ID_PATTERN2)).min(1).optional(),
12929
13091
  // CR-02: `vault` scopes the `query` path on multi-vault setups so
12930
13092
  // search_hybrid is not silently restricted to whichever vault
12931
13093
  // sorts first in VaultManager insertion order. Optional at the
12932
13094
  // schema layer; the runtime cluster() entry enforces the
12933
13095
  // multi-vault-without-vault error.
12934
- vault: z13.string().min(1).optional(),
12935
- method: z13.literal("edge-community"),
12936
- query_top_k: z13.number().int().positive().max(200).optional().default(50),
12937
- force: z13.boolean().optional().default(false)
13096
+ vault: z14.string().min(1).optional(),
13097
+ method: z14.literal("edge-community"),
13098
+ query_top_k: z14.number().int().positive().max(200).optional().default(50),
13099
+ force: z14.boolean().optional().default(false)
12938
13100
  },
12939
13101
  // ── Phase 3 assembly tools (Plan 03-06) ─────────────────────────────────
12940
13102
  assemble_dossier: {
12941
- type: z13.string().min(1).describe(
13103
+ type: z14.string().min(1).describe(
12942
13104
  "Exact-match value for properties.type on the anchor document (D-03 \u2014 no fuzzy match)"
12943
13105
  ),
12944
- key: z13.string().min(1).describe(
13106
+ key: z14.string().min(1).describe(
12945
13107
  "Candidate key \u2014 matches the document's title OR any entry in properties.aliases (D-04)"
12946
13108
  ),
12947
- vaults: z13.array(z13.string().min(1)).optional().describe("Restrict to these vault names; defaults to all configured")
13109
+ vaults: z14.array(z14.string().min(1)).optional().describe("Restrict to these vault names; defaults to all configured")
12948
13110
  },
12949
13111
  // ── Phase 6 task-contract DSL (Plan 06-02 / D-A1 escape valve) ─────────
12950
13112
  register_contracts_as_tools: {
12951
- vault: z13.string().min(1).optional().describe("Vault name; omit to apply to all vaults")
13113
+ vault: z14.string().min(1).optional().describe("Vault name; omit to apply to all vaults")
12952
13114
  },
12953
13115
  // ── Phase 6 task-contract DSL (Plan 06-03 / CON-05, Q-DESCRIBE) ────────
12954
13116
  describe_contract: {
12955
- name: z13.string().min(1).describe("Registered contract name (see register_contracts_as_tools)"),
12956
- vault: z13.string().min(1).optional().describe("Vault name; omit on single-vault setups")
13117
+ name: z14.string().min(1).describe("Registered contract name (see register_contracts_as_tools)"),
13118
+ vault: z14.string().min(1).optional().describe("Vault name; omit on single-vault setups")
12957
13119
  },
12958
13120
  // ── Phase 6 task-contract DSL (Plan 06-03 / CON-06) ────────────────────
12959
13121
  instantiate_contract: {
12960
- name: z13.string().min(1).describe("Registered contract name"),
12961
- inputs: z13.record(z13.string(), z13.unknown()).optional().default({}).describe("Contract inputs; validated against the contract's inputZodSchema"),
12962
- source_overrides: z13.record(z13.string(), z13.string()).optional().describe("Override declared source handles by handle name"),
12963
- sink_overrides: z13.record(z13.string(), z13.string()).optional().describe(
13122
+ name: z14.string().min(1).describe("Registered contract name"),
13123
+ inputs: z14.record(z14.string(), z14.unknown()).optional().default({}).describe("Contract inputs; validated against the contract's inputZodSchema"),
13124
+ source_overrides: z14.record(z14.string(), z14.string()).optional().describe("Override declared source handles by handle name"),
13125
+ sink_overrides: z14.record(z14.string(), z14.string()).optional().describe(
12964
13126
  "Override declared sink handles by handle name. Targets MUST resolve through MemorySinkRegistry (D-A4c)."
12965
13127
  ),
12966
- vault: z13.string().min(1).optional().describe("Vault name; omit on single-vault setups")
13128
+ vault: z14.string().min(1).optional().describe("Vault name; omit on single-vault setups")
12967
13129
  }
12968
13130
  };
12969
13131
  SCHEMA_BUILDERS = {
12970
- suggest_frontmatter: () => z13.object(TOOL_SCHEMAS.suggest_frontmatter).refine((v) => v.path !== void 0 || v.content !== void 0, {
13132
+ suggest_frontmatter: () => z14.object(TOOL_SCHEMAS.suggest_frontmatter).refine((v) => v.path !== void 0 || v.content !== void 0, {
12971
13133
  message: "suggest_frontmatter requires either `path` or `content`"
12972
13134
  }),
12973
13135
  // Plan 04-05 / D-15a — EXACTLY ONE of `query` or `seed_doc_ids` must
@@ -12976,7 +13138,7 @@ var init_tool_registry = __esm({
12976
13138
  // so this Zod refinement is the early-rejection gate at the MCP
12977
13139
  // boundary (cluster's internal validator handles the same case for
12978
13140
  // direct callers that bypass Zod).
12979
- cluster: () => z13.object(TOOL_SCHEMAS.cluster).refine(
13141
+ cluster: () => z14.object(TOOL_SCHEMAS.cluster).refine(
12980
13142
  (v) => v.query !== void 0 && v.seed_doc_ids === void 0 || v.query === void 0 && v.seed_doc_ids !== void 0,
12981
13143
  {
12982
13144
  message: "cluster requires EXACTLY ONE of `query` or `seed_doc_ids` (D-15a mutual exclusion)"
@@ -13071,7 +13233,7 @@ var init_json_schema_ref = __esm({
13071
13233
  });
13072
13234
 
13073
13235
  // src/contracts/input-schema.ts
13074
- import { z as z14 } from "zod";
13236
+ import { z as z15 } from "zod";
13075
13237
  function buildInputSchema(yamlInputs, required = []) {
13076
13238
  const resolvedProperties = resolveRefs(yamlInputs);
13077
13239
  const jsonSchema = {
@@ -13080,7 +13242,7 @@ function buildInputSchema(yamlInputs, required = []) {
13080
13242
  required,
13081
13243
  additionalProperties: false
13082
13244
  };
13083
- const zodSchema = z14.fromJSONSchema(
13245
+ const zodSchema = z15.fromJSONSchema(
13084
13246
  jsonSchema
13085
13247
  );
13086
13248
  return { zodSchema, jsonSchema };
@@ -13166,7 +13328,7 @@ var init_audit4 = __esm({
13166
13328
  });
13167
13329
 
13168
13330
  // src/contracts/schema.ts
13169
- import { z as z15 } from "zod";
13331
+ import { z as z16 } from "zod";
13170
13332
  var BASELINE_VERBS, MCP_VERB_RE, VerbSchema, StepSchema, HandleDeclSchema, WriteBackSchema, ContractFileSchema;
13171
13333
  var init_schema4 = __esm({
13172
13334
  "src/contracts/schema.ts"() {
@@ -13186,40 +13348,40 @@ var init_schema4 = __esm({
13186
13348
  "read_note"
13187
13349
  ];
13188
13350
  MCP_VERB_RE = /^mcp:\/\/[a-z][a-z0-9_-]*\/[a-z][a-z0-9_-]*$/;
13189
- VerbSchema = z15.union([
13190
- z15.enum([...BASELINE_VERBS, "literal"]),
13191
- z15.string().regex(MCP_VERB_RE)
13351
+ VerbSchema = z16.union([
13352
+ z16.enum([...BASELINE_VERBS, "literal"]),
13353
+ z16.string().regex(MCP_VERB_RE)
13192
13354
  ]);
13193
- StepSchema = z15.object({
13194
- as: z15.string().min(1).regex(/^[a-z_][a-z0-9_]*$/, "alias must be snake_case").describe("D-A2c \u2014 unique snake_case alias for this step's output"),
13355
+ StepSchema = z16.object({
13356
+ as: z16.string().min(1).regex(/^[a-z_][a-z0-9_]*$/, "alias must be snake_case").describe("D-A2c \u2014 unique snake_case alias for this step's output"),
13195
13357
  verb: VerbSchema.describe(
13196
13358
  "Closed enum + literal + mcp:// extension (D-A2a / C-1)"
13197
13359
  ),
13198
- args: z15.record(z15.string(), z15.unknown()).optional(),
13199
- value: z15.unknown().optional()
13360
+ args: z16.record(z16.string(), z16.unknown()).optional(),
13361
+ value: z16.unknown().optional()
13200
13362
  }).describe("One step in an assembly: array");
13201
- HandleDeclSchema = z15.object({
13202
- handle: z15.string().min(1),
13203
- required: z15.boolean().default(true)
13363
+ HandleDeclSchema = z16.object({
13364
+ handle: z16.string().min(1),
13365
+ required: z16.boolean().default(true)
13204
13366
  }).describe("Source or sink handle declaration (D-A4a)");
13205
- WriteBackSchema = z15.object({
13206
- sink: z15.string().min(1).describe("Template expression OR literal sink handle"),
13207
- document_kind: z15.enum(["brief", "observation", "custom"]),
13208
- properties: z15.record(z15.string(), z15.unknown()).default({}),
13209
- body_from: z15.string().min(1).describe("Template expression that resolves to the body string")
13367
+ WriteBackSchema = z16.object({
13368
+ sink: z16.string().min(1).describe("Template expression OR literal sink handle"),
13369
+ document_kind: z16.enum(["brief", "observation", "custom"]),
13370
+ properties: z16.record(z16.string(), z16.unknown()).default({}),
13371
+ body_from: z16.string().min(1).describe("Template expression that resolves to the body string")
13210
13372
  }).describe(
13211
13373
  "DeliveryAdapter.write chokepoint \u2014 only ground-truth DocId source (C-3)"
13212
13374
  );
13213
- ContractFileSchema = z15.object({
13214
- version: z15.literal(1).describe("v2.0.0 supports version 1 only; v2.x may extend additively"),
13215
- name: z15.string().min(1).regex(/^[a-z][a-z0-9-]*$/, "name must be kebab-case").describe("Contract name \u2014 used by instantiate_contract and slugify"),
13216
- description: z15.string().default(""),
13217
- inputs: z15.record(z15.string(), z15.unknown()).default({}),
13218
- required: z15.array(z15.string()).default([]),
13219
- sources: z15.record(z15.string(), HandleDeclSchema).default({}),
13220
- sinks: z15.record(z15.string(), HandleDeclSchema).default({}),
13221
- assembly: z15.array(StepSchema).min(1, "assembly must contain at least one step"),
13222
- output_shape: z15.unknown().optional(),
13375
+ ContractFileSchema = z16.object({
13376
+ version: z16.literal(1).describe("v2.0.0 supports version 1 only; v2.x may extend additively"),
13377
+ name: z16.string().min(1).regex(/^[a-z][a-z0-9-]*$/, "name must be kebab-case").describe("Contract name \u2014 used by instantiate_contract and slugify"),
13378
+ description: z16.string().default(""),
13379
+ inputs: z16.record(z16.string(), z16.unknown()).default({}),
13380
+ required: z16.array(z16.string()).default([]),
13381
+ sources: z16.record(z16.string(), HandleDeclSchema).default({}),
13382
+ sinks: z16.record(z16.string(), HandleDeclSchema).default({}),
13383
+ assembly: z16.array(StepSchema).min(1, "assembly must contain at least one step"),
13384
+ output_shape: z16.unknown().optional(),
13223
13385
  write_back: WriteBackSchema.optional()
13224
13386
  }).superRefine((data, ctx) => {
13225
13387
  const aliases = /* @__PURE__ */ new Set();
@@ -13571,6 +13733,9 @@ var init_templates = __esm({
13571
13733
  // src/contracts/mcp-clients.ts
13572
13734
  import { Client } from "@modelcontextprotocol/sdk/client/index.js";
13573
13735
  import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
13736
+ function nowSeconds() {
13737
+ return Math.floor(Date.now() / 1e3);
13738
+ }
13574
13739
  function wrapAvailable(client, transport) {
13575
13740
  return {
13576
13741
  available: true,
@@ -13592,6 +13757,27 @@ function wrapAvailable(client, transport) {
13592
13757
  }
13593
13758
  return res;
13594
13759
  },
13760
+ async listTools() {
13761
+ if (typeof client.listTools !== "function") return [];
13762
+ const res = await client.listTools();
13763
+ const tools = res.tools;
13764
+ if (!Array.isArray(tools)) return [];
13765
+ const out = [];
13766
+ for (const t of tools) {
13767
+ if (!t || typeof t !== "object") continue;
13768
+ const name = t.name;
13769
+ if (typeof name !== "string") continue;
13770
+ const tool = { name };
13771
+ const description = t.description;
13772
+ if (typeof description === "string") tool.description = description;
13773
+ const inputSchema = t.inputSchema;
13774
+ if (inputSchema && typeof inputSchema === "object") {
13775
+ tool.inputSchema = inputSchema;
13776
+ }
13777
+ out.push(tool);
13778
+ }
13779
+ return out;
13780
+ },
13595
13781
  [Symbol.dispose]() {
13596
13782
  transport.close();
13597
13783
  }
@@ -13603,6 +13789,9 @@ function wrapUnavailable() {
13603
13789
  async callTool() {
13604
13790
  throw new Error("peer-MCP client unavailable");
13605
13791
  },
13792
+ async listTools() {
13793
+ throw new Error("peer-MCP client unavailable");
13794
+ },
13606
13795
  [Symbol.dispose]() {
13607
13796
  }
13608
13797
  };
@@ -13613,49 +13802,155 @@ var init_mcp_clients = __esm({
13613
13802
  "use strict";
13614
13803
  init_esm_shims();
13615
13804
  PeerMcpRegistry = class {
13616
- clients = /* @__PURE__ */ new Map();
13805
+ entries = /* @__PURE__ */ new Map();
13617
13806
  clientFactory;
13618
13807
  constructor(clientFactory) {
13619
13808
  this.clientFactory = clientFactory;
13620
13809
  }
13621
13810
  get size() {
13622
- return this.clients.size;
13811
+ return this.entries.size;
13623
13812
  }
13624
13813
  /**
13625
13814
  * Boot every `[contracts.mcp_clients.<name>]` entry. Failures are
13626
13815
  * non-fatal: the name is recorded as unavailable and a WARN line is
13627
13816
  * written to stderr. Returns when all attempts have settled.
13817
+ *
13818
+ * On a successful connect we prime the tools cache via tools/list. A
13819
+ * tools/list failure does NOT mark the source unavailable — the
13820
+ * connection is live and callTool may still work — but the status
13821
+ * becomes "unreachable" so the UI can prompt a retry.
13628
13822
  */
13629
13823
  async start(configs) {
13630
13824
  for (const [name, cfg] of Object.entries(configs)) {
13825
+ await this.connectAndStore(name, cfg);
13826
+ }
13827
+ }
13828
+ get(name) {
13829
+ return this.entries.get(name)?.client;
13830
+ }
13831
+ /** All registered source names, in insertion order. */
13832
+ names() {
13833
+ return Array.from(this.entries.keys());
13834
+ }
13835
+ /** Cached metadata projection for one source; undefined if unknown. */
13836
+ getInfo(name) {
13837
+ const e = this.entries.get(name);
13838
+ if (e === void 0) return void 0;
13839
+ return {
13840
+ status: e.status,
13841
+ tools: e.tools,
13842
+ lastRefreshed: e.lastRefreshed,
13843
+ ...e.error !== void 0 ? { error: e.error } : {}
13844
+ };
13845
+ }
13846
+ /**
13847
+ * Register a new source at runtime: spawn + connect, then prime the
13848
+ * tools cache. Replaces any existing entry of the same name (the old
13849
+ * client is disposed first). Returns the resulting info projection.
13850
+ */
13851
+ async add(name, cfg) {
13852
+ const existing = this.entries.get(name);
13853
+ if (existing !== void 0) {
13631
13854
  try {
13632
- const { client, transport } = this.clientFactory ? await this.clientFactory(cfg) : await this.defaultConnect(cfg);
13633
- this.clients.set(name, wrapAvailable(client, transport));
13634
- } catch (err) {
13635
- const msg = err instanceof Error ? err.message : String(err);
13636
- process.stderr.write(
13637
- `[contracts] peer-MCP client '${name}' failed to start: ${msg}
13638
- `
13639
- );
13640
- this.clients.set(name, wrapUnavailable());
13855
+ existing.client[Symbol.dispose]();
13856
+ } catch {
13641
13857
  }
13642
13858
  }
13859
+ await this.connectAndStore(name, cfg);
13860
+ return this.getInfo(name);
13643
13861
  }
13644
- get(name) {
13645
- return this.clients.get(name);
13862
+ /**
13863
+ * Dispose a source and drop it from the registry. Idempotent —
13864
+ * removing an unknown name is a no-op that returns false.
13865
+ */
13866
+ remove(name) {
13867
+ const e = this.entries.get(name);
13868
+ if (e === void 0) return false;
13869
+ try {
13870
+ e.client[Symbol.dispose]();
13871
+ } catch (err) {
13872
+ const msg = err instanceof Error ? err.message : String(err);
13873
+ process.stderr.write(`[contracts] peer-MCP dispose error: ${msg}
13874
+ `);
13875
+ }
13876
+ this.entries.delete(name);
13877
+ return true;
13878
+ }
13879
+ /**
13880
+ * Re-issue tools/list against the live client and refresh the cache.
13881
+ * Returns the updated info, or undefined if the name is unknown.
13882
+ *
13883
+ * If the client is currently unavailable this only updates the error;
13884
+ * re-spawning a failed source requires `add(name, cfg)` with the
13885
+ * config (the registry does not retain configs).
13886
+ */
13887
+ async refresh(name) {
13888
+ const e = this.entries.get(name);
13889
+ if (e === void 0) return void 0;
13890
+ if (!e.client.available) {
13891
+ e.status = "unavailable";
13892
+ return this.getInfo(name);
13893
+ }
13894
+ await this.primeTools(e);
13895
+ return this.getInfo(name);
13646
13896
  }
13647
13897
  /** Dispose every client and clear the internal map. Idempotent. */
13648
13898
  async shutdown() {
13649
- for (const c of this.clients.values()) {
13899
+ for (const e of this.entries.values()) {
13650
13900
  try {
13651
- c[Symbol.dispose]();
13901
+ e.client[Symbol.dispose]();
13652
13902
  } catch (err) {
13653
13903
  const msg = err instanceof Error ? err.message : String(err);
13654
13904
  process.stderr.write(`[contracts] peer-MCP dispose error: ${msg}
13655
13905
  `);
13656
13906
  }
13657
13907
  }
13658
- this.clients.clear();
13908
+ this.entries.clear();
13909
+ }
13910
+ // ─── internals ─────────────────────────────────────────────────────────
13911
+ /** Connect (factory or default), store the entry, prime tools cache. */
13912
+ async connectAndStore(name, cfg) {
13913
+ try {
13914
+ const { client, transport } = this.clientFactory ? await this.clientFactory(cfg) : await this.defaultConnect(cfg);
13915
+ const entry = {
13916
+ client: wrapAvailable(client, transport),
13917
+ status: "connected",
13918
+ tools: [],
13919
+ lastRefreshed: null
13920
+ };
13921
+ this.entries.set(name, entry);
13922
+ await this.primeTools(entry);
13923
+ } catch (err) {
13924
+ const msg = err instanceof Error ? err.message : String(err);
13925
+ process.stderr.write(
13926
+ `[contracts] peer-MCP client '${name}' failed to start: ${msg}
13927
+ `
13928
+ );
13929
+ this.entries.set(name, {
13930
+ client: wrapUnavailable(),
13931
+ status: "unavailable",
13932
+ tools: [],
13933
+ lastRefreshed: null,
13934
+ error: msg
13935
+ });
13936
+ }
13937
+ }
13938
+ /**
13939
+ * Call tools/list and update the entry's cache + status. A failure
13940
+ * keeps the connection (status → "unreachable") rather than tearing it
13941
+ * down — the source is reachable for callTool even if discovery failed.
13942
+ */
13943
+ async primeTools(entry) {
13944
+ try {
13945
+ const tools = await entry.client.listTools();
13946
+ entry.tools = tools;
13947
+ entry.lastRefreshed = nowSeconds();
13948
+ entry.status = "connected";
13949
+ delete entry.error;
13950
+ } catch (err) {
13951
+ entry.status = "unreachable";
13952
+ entry.error = err instanceof Error ? err.message : String(err);
13953
+ }
13659
13954
  }
13660
13955
  async defaultConnect(cfg) {
13661
13956
  const transport = new StdioClientTransport({
@@ -13760,7 +14055,7 @@ var init_verbs = __esm({
13760
14055
  });
13761
14056
 
13762
14057
  // src/contracts/instantiate.ts
13763
- import { z as z16 } from "zod";
14058
+ import { z as z17 } from "zod";
13764
14059
  async function instantiateContract(deps, args2) {
13765
14060
  const parsed = deps.registry.get(args2.name);
13766
14061
  if (!parsed) return { ok: false, reason: "unknown_contract", name: args2.name };
@@ -13908,7 +14203,7 @@ async function instantiateContract(deps, args2) {
13908
14203
  };
13909
14204
  if (parsed.output_shape) {
13910
14205
  try {
13911
- const outputSchema = z16.fromJSONSchema(
14206
+ const outputSchema = z17.fromJSONSchema(
13912
14207
  parsed.output_shape
13913
14208
  );
13914
14209
  const check = outputSchema.safeParse(bundle);
@@ -13996,6 +14291,12 @@ var init_instantiate = __esm({
13996
14291
  });
13997
14292
 
13998
14293
  // src/contracts/describe.ts
14294
+ function glossFor(verb) {
14295
+ if (VERB_GLOSS[verb]) return VERB_GLOSS[verb];
14296
+ if (verb === "literal") return "Use a fixed inline value";
14297
+ if (verb.startsWith("mcp://")) return `Call an external tool (${verb})`;
14298
+ return verb;
14299
+ }
13999
14300
  function describeContract(deps, args2) {
14000
14301
  const parsed = deps.registry.get(args2.name);
14001
14302
  if (!parsed) return { ok: false, reason: "unknown_contract", name: args2.name };
@@ -14045,7 +14346,9 @@ function renderSummary(parsed) {
14045
14346
  lines.push("## Assembly");
14046
14347
  parsed.assembly.forEach((step, i) => {
14047
14348
  const argsRender = step.args ? `(${Object.keys(step.args).join(", ")})` : "()";
14048
- lines.push(`${i + 1}. **${step.as}** \u2190 \`${step.verb}${argsRender}\``);
14349
+ lines.push(
14350
+ `${i + 1}. **${step.as}** \u2014 ${glossFor(step.verb)} _(\`${step.verb}${argsRender}\`)_`
14351
+ );
14049
14352
  });
14050
14353
  lines.push("");
14051
14354
  }
@@ -14069,10 +14372,24 @@ function renderSummary(parsed) {
14069
14372
  }
14070
14373
  return lines.join("\n").trim() + "\n";
14071
14374
  }
14375
+ var VERB_GLOSS;
14072
14376
  var init_describe = __esm({
14073
14377
  "src/contracts/describe.ts"() {
14074
14378
  "use strict";
14075
14379
  init_esm_shims();
14380
+ VERB_GLOSS = {
14381
+ read_note: "Read a note's content",
14382
+ search_hybrid: "Search the vault (semantic + keyword)",
14383
+ search_sections: "Search for matching sections within notes",
14384
+ query_frontmatter: "Find notes by their properties (frontmatter)",
14385
+ expand: "Gather notes linked to the starting note (follow the graph)",
14386
+ cluster: "Group the gathered notes into related communities",
14387
+ recall: "Recall earlier agent observations from memory",
14388
+ compile_brief: "Compile the gathered notes into a brief",
14389
+ get_brief: "Fetch an already-compiled brief",
14390
+ list_backlinks: "List notes that link back to this one",
14391
+ get_outline: "Read a note's heading outline"
14392
+ };
14076
14393
  }
14077
14394
  });
14078
14395
 
@@ -14143,6 +14460,59 @@ var init_resources3 = __esm({
14143
14460
  }
14144
14461
  });
14145
14462
 
14463
+ // src/contracts/sources-resources.ts
14464
+ function readListSources(reg, configMeta) {
14465
+ const sources = [];
14466
+ for (const name of reg.names()) {
14467
+ const info = reg.getInfo(name);
14468
+ if (info === void 0) continue;
14469
+ const meta = configMeta[name];
14470
+ const entry = {
14471
+ name,
14472
+ transport: "stdio",
14473
+ command: meta?.command ?? "",
14474
+ args: meta?.args ?? [],
14475
+ status: info.status,
14476
+ tool_count: info.tools.length,
14477
+ last_refreshed: info.lastRefreshed
14478
+ };
14479
+ if (info.error !== void 0) entry.error = info.error;
14480
+ sources.push(entry);
14481
+ }
14482
+ return { sources };
14483
+ }
14484
+ function readSourceTools(reg, name) {
14485
+ const info = reg.getInfo(name);
14486
+ if (info === void 0) {
14487
+ return { error: `unknown source: ${name}` };
14488
+ }
14489
+ const out = {
14490
+ name,
14491
+ status: info.status,
14492
+ last_refreshed: info.lastRefreshed,
14493
+ tools: info.tools
14494
+ };
14495
+ if (info.error !== void 0) out.error = info.error;
14496
+ return out;
14497
+ }
14498
+ function readSourceTool(reg, name, toolName) {
14499
+ const info = reg.getInfo(name);
14500
+ if (info === void 0) {
14501
+ return { found: false, error: `unknown source: ${name}` };
14502
+ }
14503
+ const tool = info.tools.find((t) => t.name === toolName);
14504
+ if (tool === void 0) {
14505
+ return { found: false, error: `unknown tool: ${name}/${toolName}` };
14506
+ }
14507
+ return { found: true, name, tool };
14508
+ }
14509
+ var init_sources_resources = __esm({
14510
+ "src/contracts/sources-resources.ts"() {
14511
+ "use strict";
14512
+ init_esm_shims();
14513
+ }
14514
+ });
14515
+
14146
14516
  // src/contracts/index.ts
14147
14517
  var init_contracts = __esm({
14148
14518
  "src/contracts/index.ts"() {
@@ -14165,6 +14535,7 @@ var init_contracts = __esm({
14165
14535
  init_instantiate();
14166
14536
  init_describe();
14167
14537
  init_resources3();
14538
+ init_sources_resources();
14168
14539
  }
14169
14540
  });
14170
14541
 
@@ -14359,6 +14730,20 @@ async function serve(options = {}) {
14359
14730
  process.on("SIGTERM", () => {
14360
14731
  void shutdown().finally(() => process.exit(0));
14361
14732
  });
14733
+ let stdinClosing = false;
14734
+ const onStdinClose = (reason) => {
14735
+ if (stdinClosing) return;
14736
+ stdinClosing = true;
14737
+ process.stderr.write(
14738
+ `[vault-memory] stdin ${reason} \u2014 parent process gone; shutting down.
14739
+ `
14740
+ );
14741
+ setTimeout(() => {
14742
+ void shutdown().finally(() => process.exit(0));
14743
+ }, 500);
14744
+ };
14745
+ process.stdin.on("end", () => onStdinClose("end"));
14746
+ process.stdin.on("close", () => onStdinClose("close"));
14362
14747
  const server = new McpServer(
14363
14748
  { name: "vault-memory", version: VERSION },
14364
14749
  // Plan 02-06 (MEM-09): advertise `resources` capability so MCP clients
@@ -15404,6 +15789,86 @@ async function serve(options = {}) {
15404
15789
  };
15405
15790
  }
15406
15791
  );
15792
+ const sourceConfigMeta = () => {
15793
+ const out = {};
15794
+ for (const [name, cfg] of Object.entries(config.contracts.mcp_clients)) {
15795
+ out[name] = { command: cfg.command, args: cfg.args ?? [] };
15796
+ }
15797
+ return out;
15798
+ };
15799
+ server.registerResource(
15800
+ "sources",
15801
+ RESOURCE_URI_SOURCES,
15802
+ {
15803
+ title: "Peer MCP sources",
15804
+ description: "List peer MCP servers vault-memory connects to, with per-source status (connected/unavailable/unreachable), tool_count, and last_refreshed. vault-memory itself is not included. SOURCES-REGISTRY \xA75.1.",
15805
+ mimeType: "application/json"
15806
+ },
15807
+ async (uri) => ({
15808
+ contents: [
15809
+ {
15810
+ uri: uri.href,
15811
+ mimeType: "application/json",
15812
+ text: JSON.stringify(
15813
+ readListSources(peerMcpRegistry, sourceConfigMeta()),
15814
+ null,
15815
+ 2
15816
+ )
15817
+ }
15818
+ ]
15819
+ })
15820
+ );
15821
+ server.registerResource(
15822
+ "source-tools",
15823
+ new ResourceTemplate(`${RESOURCE_URI_SOURCES}/{name}/tools`, {
15824
+ list: void 0
15825
+ }),
15826
+ {
15827
+ title: "Peer MCP source tools",
15828
+ description: "List the cached tools/list for one peer MCP source. Empty when the source is not connected. SOURCES-REGISTRY \xA75.2.",
15829
+ mimeType: "application/json"
15830
+ },
15831
+ async (uri, variables) => {
15832
+ const name = String(variables.name ?? "");
15833
+ return {
15834
+ contents: [
15835
+ {
15836
+ uri: uri.href,
15837
+ mimeType: "application/json",
15838
+ text: JSON.stringify(readSourceTools(peerMcpRegistry, name), null, 2)
15839
+ }
15840
+ ]
15841
+ };
15842
+ }
15843
+ );
15844
+ server.registerResource(
15845
+ "source-tool",
15846
+ new ResourceTemplate(`${RESOURCE_URI_SOURCES}/{name}/tools/{tool}`, {
15847
+ list: void 0
15848
+ }),
15849
+ {
15850
+ title: "Peer MCP source tool",
15851
+ description: "Read a single tool's schema from one peer MCP source, inlined from the cached tools/list (no extra peer call). SOURCES-REGISTRY \xA75.3.",
15852
+ mimeType: "application/json"
15853
+ },
15854
+ async (uri, variables) => {
15855
+ const name = String(variables.name ?? "");
15856
+ const tool = String(variables.tool ?? "");
15857
+ return {
15858
+ contents: [
15859
+ {
15860
+ uri: uri.href,
15861
+ mimeType: "application/json",
15862
+ text: JSON.stringify(
15863
+ readSourceTool(peerMcpRegistry, name, tool),
15864
+ null,
15865
+ 2
15866
+ )
15867
+ }
15868
+ ]
15869
+ };
15870
+ }
15871
+ );
15407
15872
  const rel08Vaults = RESOURCES.find((r) => r.name === "vaults");
15408
15873
  const rel08Models = RESOURCES.find((r) => r.name === "models");
15409
15874
  const rel08Recent = RESOURCES.find((r) => r.name === "recent");
@@ -15716,6 +16181,9 @@ async function serve(options = {}) {
15716
16181
  // sees, so the plugin's `suppress_contract_write` call and the
15717
16182
  // change-feed handler observe the same entries.
15718
16183
  suppression,
16184
+ // SOURCES-REGISTRY.md §6 (Stage 2) — live registry for refresh_source
16185
+ // + unset_mcp_client. The singleton booted above.
16186
+ sourceRegistry: peerMcpRegistry,
15719
16187
  notifier: (notification) => {
15720
16188
  server.server.notification(notification);
15721
16189
  }