@openparachute/vault 0.7.3-rc.7 → 0.7.3-rc.9

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/README.md CHANGED
@@ -30,7 +30,7 @@ For remote access from Claude Desktop or mobile apps, see [Deployment](#deployme
30
30
 
31
31
  A server on port 1940 with:
32
32
 
33
- - **MCP** — 9 tools for AI agents (notes, tags, graph, vault info)
33
+ - **MCP** — tools for AI agents (notes, tags, graph, vault info, attachments)
34
34
  - **REST API** — Full CRUD for notes, tags, links, full-text search
35
35
  - **Wikilink auto-linking** — `[[wikilinks]]` in note content automatically create links in the graph
36
36
  - **Obsidian import/export** — Bidirectional interop with Obsidian vaults
@@ -252,12 +252,14 @@ parachute-vault backup --schedule daily # hourly | daily | weekly | manua
252
252
  parachute-vault backup status # schedule, last run, destinations, next run
253
253
  ```
254
254
 
255
- ## MCP tools (9)
255
+ ## MCP tools (13 core, + attachments + admin)
256
256
 
257
257
  **Notes**: `query-notes` (universal read — single by ID/path, filter, search, graph neighborhood), `create-note` (single or batch), `update-note` (single or batch — content, tags, links, metadata), `delete-note`
258
- **Tags**: `list-tags` (with optional schema detail), `update-tag` (upsert description + schema fields), `delete-tag`
258
+ **Tags**: `list-tags` (with optional schema detail), `update-tag` (upsert description + schema fields), `delete-tag`, `rename-tag`, `merge-tags`
259
259
  **Graph**: `find-path` (BFS between two notes)
260
260
  **Vault**: `vault-info` (get/update description + stats)
261
+ **Admin**: `prune-schema`, `doctor` (taxonomy/metadata integrity scan), `manage-token`
262
+ **Attachments** (2, present when the door wires an attachment-ticket provider — always true on this server): `request-attachment-upload`, `request-attachment-download` — mint a short-lived, single-use ticket a runtime's shell spends directly at `/vault/{name}/tickets/{id}`; bytes never pass through the tool call. See [`docs/HTTP_API.md`](./docs/HTTP_API.md#attachment-tickets-vault611--wave-1-bun-only).
261
263
 
262
264
  ### Vault descriptions
263
265
 
@@ -83,6 +83,47 @@ describe("computeDisplayTitle", () => {
83
83
  const result = computeDisplayTitle(emojiTitle)!;
84
84
  expect(Array.from(result).length).toBe(DISPLAY_TITLE_MAX_LEN);
85
85
  });
86
+
87
+ describe("leading frontmatter block (misuse path — direct create with raw frontmatter)", () => {
88
+ it("skips a closed frontmatter block and derives the first line of the DOCUMENT", () => {
89
+ expect(computeDisplayTitle("---\ntitle: X\n---\n# Real Title")).toBe("Real Title");
90
+ });
91
+
92
+ it("skips a closed frontmatter block with multiple fields + blank lines after", () => {
93
+ expect(
94
+ computeDisplayTitle("---\ntitle: X\ntags: [a, b]\n---\n\n\nReal Title\nbody"),
95
+ ).toBe("Real Title");
96
+ });
97
+
98
+ it("falls back to treating an UNTERMINATED opening `---` as ordinary content", () => {
99
+ // No closing fence anywhere in the content — a note whose real first
100
+ // line is literally "---" (e.g. a markdown horizontal rule opener)
101
+ // must not be mangled.
102
+ expect(computeDisplayTitle("---\nnot frontmatter, just a rule\nmore text")).toBe("---");
103
+ });
104
+
105
+ it("returns null when content is EXACTLY a closed frontmatter block (nothing after)", () => {
106
+ expect(computeDisplayTitle("---\ntitle: X\n---\n")).toBeNull();
107
+ expect(computeDisplayTitle("---\ntitle: X\n---")).toBeNull();
108
+ });
109
+
110
+ it("returns null when only blank lines follow a closed frontmatter block", () => {
111
+ expect(computeDisplayTitle("---\ntitle: X\n---\n\n \n")).toBeNull();
112
+ });
113
+
114
+ it("does not treat a closing fence found past the bounded scan window as a frontmatter close", () => {
115
+ // Opening `---` with the closing fence past FRONTMATTER_SCAN_LINES
116
+ // (100): the scan gives up and falls back to ordinary-content
117
+ // behavior, deriving from line 0.
118
+ const filler = Array.from({ length: 150 }, (_, i) => `line ${i}`).join("\n");
119
+ expect(computeDisplayTitle(`---\n${filler}\n---\nReal Title`)).toBe("---");
120
+ });
121
+
122
+ it("normal content with no leading `---` is byte-identical to prior behavior", () => {
123
+ expect(computeDisplayTitle("# Grocery List\nmilk, eggs")).toBe("Grocery List");
124
+ expect(computeDisplayTitle("Just a note\nbody text")).toBe("Just a note");
125
+ });
126
+ });
86
127
  });
87
128
 
88
129
  describe("toNoteIndex — displayTitle wiring", () => {
package/core/src/notes.ts CHANGED
@@ -2769,6 +2769,15 @@ export const NOTE_INDEX_PREVIEW_LEN = 120;
2769
2769
  /** Max code points in a computed `displayTitle` (title axis, ratified 2026-07-17). */
2770
2770
  export const DISPLAY_TITLE_MAX_LEN = 120;
2771
2771
 
2772
+ /**
2773
+ * Bounded line-count a leading frontmatter block's closing fence is
2774
+ * searched within (see `computeDisplayTitle`). Frontmatter blocks are a
2775
+ * handful of lines in practice; capping the scan keeps a pathological
2776
+ * "content starts with `---` but never closes it" note O(1)-ish rather
2777
+ * than a full-content scan on every derivation.
2778
+ */
2779
+ const FRONTMATTER_SCAN_LINES = 100;
2780
+
2772
2781
  /**
2773
2782
  * Derive a note's display title: the first non-empty line of `content`,
2774
2783
  * with a leading markdown heading marker (`#` through `######`) and its
@@ -2783,11 +2792,33 @@ export const DISPLAY_TITLE_MAX_LEN = 120;
2783
2792
  * in place of a `null` title — is deliberately NOT this function's job; it
2784
2793
  * reports the honest content-derived value (or its absence) and leaves
2785
2794
  * rendering to the caller.
2795
+ *
2796
+ * Frontmatter skip: normal ingestion strips a YAML frontmatter block into
2797
+ * `metadata` before create, so `content` never carries one — but a direct
2798
+ * MCP/REST create can paste raw frontmatter-bearing text
2799
+ * (`---\ntitle: X\n---\n# Real Title`), and the first line of that
2800
+ * DOCUMENT is not the first line of its delimiter. When `content` opens
2801
+ * with a `---` line, derivation starts after the matching CLOSING `---`
2802
+ * (searched within the first `FRONTMATTER_SCAN_LINES` lines). An
2803
+ * unterminated opening `---` falls back to the pre-existing behavior —
2804
+ * scanning from line 0 — so a note whose real first line is literally
2805
+ * `---` isn't mangled.
2786
2806
  */
2787
2807
  export function computeDisplayTitle(content: string | null | undefined): string | null {
2788
2808
  if (!content) return null;
2789
- for (const rawLine of content.split("\n")) {
2790
- const stripped = rawLine.replace(/^#{1,6}\s*/, "").trim();
2809
+ const lines = content.split("\n");
2810
+ let startIndex = 0;
2811
+ if (lines[0]?.trim() === "---") {
2812
+ const scanLimit = Math.min(lines.length, FRONTMATTER_SCAN_LINES);
2813
+ for (let i = 1; i < scanLimit; i++) {
2814
+ if (lines[i]?.trim() === "---") {
2815
+ startIndex = i + 1;
2816
+ break;
2817
+ }
2818
+ }
2819
+ }
2820
+ for (let i = startIndex; i < lines.length; i++) {
2821
+ const stripped = lines[i]!.replace(/^#{1,6}\s*/, "").trim();
2791
2822
  if (stripped === "") continue;
2792
2823
  // Iterate by Unicode code points so we don't split surrogate pairs mid-character.
2793
2824
  const codePoints = Array.from(stripped);
@@ -104,4 +104,22 @@ describe("search title-boost", () => {
104
104
  const hits = await store.searchNotes("nonexistent_term_xyz");
105
105
  expect(hits).toEqual([]);
106
106
  });
107
+
108
+ it("boosts on the post-frontmatter title line, not the `---` delimiter (shares computeDisplayTitle's frontmatter skip)", async () => {
109
+ // Direct-create misuse path: raw frontmatter-bearing content. Without
110
+ // the frontmatter skip, the derived "title" would be the literal
111
+ // string "---", which never matches any query term, so this note
112
+ // would wrongly land in the body-only tier.
113
+ await store.createNote(
114
+ "---\ntitle: irrelevant\n---\n# Budget Review\nQ3 numbers",
115
+ { path: "frontmatter-led" },
116
+ );
117
+ await store.createNote(
118
+ "Weekly Standup\nsomewhere we discuss the budget in passing",
119
+ { path: "body-only" },
120
+ );
121
+
122
+ const hits = await store.searchNotes("budget review");
123
+ expect(hits[0].path).toBe("frontmatter-led");
124
+ });
107
125
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openparachute/vault",
3
- "version": "0.7.3-rc.7",
3
+ "version": "0.7.3-rc.9",
4
4
  "description": "Agent-native knowledge graph. Notes, tags, links over MCP.",
5
5
  "module": "src/cli.ts",
6
6
  "type": "module",