@owrede/vault-memory 0.8.2 → 0.9.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/README.md +55 -18
- package/dist/cli.js +283 -3
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -6,7 +6,9 @@ Reads one or more Obsidian vaults, indexes them with local embeddings via Ollama
|
|
|
6
6
|
|
|
7
7
|
## Status
|
|
8
8
|
|
|
9
|
-
**v0.
|
|
9
|
+
**v0.9.0** — Agent-Compatibility & Self-Orientation: OB1-compatible `search`/`fetch` tools so ChatGPT Custom Connectors, Claude.ai, and Deep-Research modes can use vault-memory as a connector; `vault_stats` and `recent_notes` for agent self-orientation on first connect. See `_research/vault-memory-openbrain-comparison.md` in a consuming vault for the gap analysis driving these additions.
|
|
10
|
+
|
|
11
|
+
Previous: **v0.8.3** — Phase 8 (real ONNX cross-encoder reranker) + search-quality fixes + skills consolidation.
|
|
10
12
|
|
|
11
13
|
## Architecture in one paragraph
|
|
12
14
|
|
|
@@ -96,43 +98,65 @@ brew install ollama && brew services start ollama
|
|
|
96
98
|
# 4) Embedding model (~1.1 GB)
|
|
97
99
|
ollama pull bge-m3
|
|
98
100
|
|
|
99
|
-
# 5)
|
|
100
|
-
|
|
101
|
-
gh repo clone owrede/vault-memory
|
|
102
|
-
cd vault-memory
|
|
103
|
-
npm install && npm run build && npm link # creates the global `vault-memory` binary
|
|
101
|
+
# 5) Install vault-memory from npm (public registry, no auth)
|
|
102
|
+
npm install -g @owrede/vault-memory
|
|
104
103
|
|
|
105
104
|
# 6) Register your first vault (creates config + .mcp.json + initial index)
|
|
106
105
|
vault-memory add-vault "/Users/you/Documents/Obsidian Vaults/My Vault"
|
|
107
106
|
```
|
|
108
107
|
|
|
109
|
-
The MCP-host config (`.mcp.json` in the consuming vault) calls the `vault-memory` binary, so any shell with it on `$PATH` will work.
|
|
108
|
+
The MCP-host config (`.mcp.json` in the consuming vault) calls the `vault-memory` binary, so any shell with it on `$PATH` will work. Future upgrades: `npm install -g @owrede/vault-memory@latest`.
|
|
110
109
|
|
|
111
110
|
For a guided install from inside Claude Code, see the `skills/` directory in this repo — they bundle the install, vault registration, and end-to-end smoketest behind a single command.
|
|
112
111
|
|
|
112
|
+
### Install from source (developer mode)
|
|
113
|
+
|
|
114
|
+
Only needed if you want to modify vault-memory itself. Otherwise use the npm install above.
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
cd ~/Documents/GitHub
|
|
118
|
+
gh repo clone owrede/vault-memory
|
|
119
|
+
cd vault-memory
|
|
120
|
+
npm install && npm run build && npm link # creates the global `vault-memory` binary
|
|
121
|
+
```
|
|
122
|
+
|
|
113
123
|
## Skills (Claude Code integration)
|
|
114
124
|
|
|
115
|
-
The `skills/` directory contains
|
|
125
|
+
The `skills/` directory contains two Claude Code skills you can drop into any vault's `.claude/skills/` folder. They are the user-facing way to install and operate vault-memory without remembering CLI flags.
|
|
116
126
|
|
|
117
127
|
| Skill | What it does | When to invoke |
|
|
118
128
|
|---|---|---|
|
|
119
|
-
| **`memory/`** |
|
|
120
|
-
| **`
|
|
121
|
-
| **`add-vault/`** | Wraps `vault-memory add-vault` with a confirmation flow — appends to `config.toml`, writes `.mcp.json`, builds the initial index. Atomic and idempotent. | Adding an additional vault after the system is installed. `/add-vault` |
|
|
129
|
+
| **`install-vault-memory/`** | The complete installer — 8 idempotent checkpoints from Homebrew through MCP smoketest. Defaults to autonomous mode with a `why:` line on every install prompt. Re-running on a working setup verifies state in under 5 seconds and exits. | First-time setup of a vault, or repairing a broken state. `/install-vault-memory` |
|
|
130
|
+
| **`add-vault/`** | Wraps `vault-memory add-vault` CLI with a confirmation flow — appends to `config.toml`, writes `.mcp.json`, builds the initial index. Atomic and idempotent. | Adding a *second or third* vault after vault-memory is already installed. `/add-vault` |
|
|
122
131
|
|
|
123
132
|
### Installing the skills in a vault
|
|
124
133
|
|
|
134
|
+
One-liner — works from anywhere, installs into the specified vault:
|
|
135
|
+
|
|
125
136
|
```bash
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
cp -R ~/Documents/GitHub/vault-memory/skills/{memory,setup-memory-system,add-vault} .claude/skills/
|
|
137
|
+
curl -fsSL https://raw.githubusercontent.com/owrede/vault-memory/main/scripts/install-skills.sh \
|
|
138
|
+
| bash -s -- "/path/to/your/obsidian/vault"
|
|
129
139
|
```
|
|
130
140
|
|
|
131
|
-
|
|
141
|
+
Or, from inside the vault's root directory:
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
curl -fsSL https://raw.githubusercontent.com/owrede/vault-memory/main/scripts/install-skills.sh | bash
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
The script is idempotent — re-running it fetches the latest skill versions from `main` and overwrites the local copies. Use it to update your skills whenever vault-memory ships a new release.
|
|
148
|
+
|
|
149
|
+
If you cloned the source repo, you can also copy directly:
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
cp -R ~/Documents/GitHub/vault-memory/skills/{install-vault-memory,add-vault} .claude/skills/
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
After Claude Code restarts, `/install-vault-memory` and `/add-vault` are available as slash commands in that vault.
|
|
132
156
|
|
|
133
157
|
### Autonomous mode
|
|
134
158
|
|
|
135
|
-
`VAULT_MEMORY_AUTO=1` switches `
|
|
159
|
+
`VAULT_MEMORY_AUTO=1` switches `install-vault-memory/setup.sh` to non-interactive mode: every non-destructive `confirm()` prompt auto-answers yes, with a `why:` line explaining what is being installed and why vault-memory needs it. Destructive operations (overwriting an existing multi-vault `config.toml`, rebuilding a clone with uncommitted changes) still prompt. This is the default when the skill is invoked via `/install-vault-memory`; direct invocation of `setup.sh` defaults to fully-interactive mode.
|
|
136
160
|
|
|
137
161
|
## Configuration
|
|
138
162
|
|
|
@@ -161,7 +185,7 @@ exclude_globs = [".obsidian/**", ".trash/**", "_research/**", ".claude/**"]
|
|
|
161
185
|
# secondary_embedding_model = "qwen3-embedding:0.6b"
|
|
162
186
|
```
|
|
163
187
|
|
|
164
|
-
## MCP tools (
|
|
188
|
+
## MCP tools (22)
|
|
165
189
|
|
|
166
190
|
**Discovery & Read:** `list_vaults`, `read_note`
|
|
167
191
|
**Search:** `search_semantic`, `search_text`, `search_hybrid` — all support optional `exclude_paths` (glob) and an explicit `vaults` filter; responses include a `note` field when vaults were skipped (e.g. mid-indexing)
|
|
@@ -171,13 +195,26 @@ exclude_globs = [".obsidian/**", ".trash/**", "_research/**", ".claude/**"]
|
|
|
171
195
|
**Audit:** `audit_log`, `index_runs`
|
|
172
196
|
**Model management (Phase 7c):** `list_models`, `start_shadow_index`, `switch_active_model`
|
|
173
197
|
**Maintenance (v0.7.3):** `vacuum_embeddings` — drop orphaned embedding rows whose chunk_id no longer exists
|
|
198
|
+
**Agent-Compatibility (v0.9.0):** `search`, `fetch` — OB1-compatible flat-shape adapters for ChatGPT Custom Connectors, Claude.ai, and Deep-Research modes. Backed by the hybrid (semantic+BM25+RRF) retrieval pipeline, so connector users get vault-memory's full search quality through the standardized interface.
|
|
199
|
+
**Agent self-orientation (v0.9.0):** `vault_stats`, `recent_notes` — vault overview (note count, top tags, top frontmatter keys, last index run) and recently-modified notes (mtime DESC). Use these on first connect to brief an agent on what's in the vault and what the user has been working on.
|
|
200
|
+
|
|
201
|
+
### Connector compatibility (v0.9.0)
|
|
202
|
+
|
|
203
|
+
`search`/`fetch` follow the flat-shape spec used by OB1 and adopted by ChatGPT Custom Connectors / Claude.ai / Deep-Research:
|
|
204
|
+
|
|
205
|
+
```
|
|
206
|
+
search({query, limit}) → { results: [{ id, title, url, snippet }] }
|
|
207
|
+
fetch({id}) → { id, title, text, url, metadata }
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
`id` is the opaque format `<vault>:<vault-relative-path>`. `url` is an `obsidian://open?…` URL — connectors render it as a clickable link that opens the note locally. Use the richer `search_hybrid` / `read_note` tools when working with a vault-memory-aware client (Claude Code's MCP integration); use `search` / `fetch` when integrating with a connector ecosystem that expects the standard shape.
|
|
174
211
|
|
|
175
212
|
## Development
|
|
176
213
|
|
|
177
214
|
```bash
|
|
178
215
|
npm install
|
|
179
216
|
npm run dev # MCP server on stdio with hot reload
|
|
180
|
-
npm test #
|
|
217
|
+
npm test # 318 tests across 36 files (v0.9.0)
|
|
181
218
|
npm run build
|
|
182
219
|
```
|
|
183
220
|
|
package/dist/cli.js
CHANGED
|
@@ -4652,7 +4652,13 @@ var init_watcher2 = __esm({
|
|
|
4652
4652
|
// src/server.ts
|
|
4653
4653
|
var server_exports = {};
|
|
4654
4654
|
__export(server_exports, {
|
|
4655
|
-
|
|
4655
|
+
aggregateTopFrontmatterKeys: () => aggregateTopFrontmatterKeys,
|
|
4656
|
+
aggregateTopTags: () => aggregateTopTags,
|
|
4657
|
+
decodeNoteId: () => decodeNoteId,
|
|
4658
|
+
encodeNoteId: () => encodeNoteId,
|
|
4659
|
+
obsidianUrl: () => obsidianUrl,
|
|
4660
|
+
serve: () => serve,
|
|
4661
|
+
truncateSnippet: () => truncateSnippet
|
|
4656
4662
|
});
|
|
4657
4663
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
4658
4664
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
@@ -5031,6 +5037,57 @@ async function serve() {
|
|
|
5031
5037
|
limit: { type: "integer", minimum: 1, maximum: 200, default: 20 }
|
|
5032
5038
|
}
|
|
5033
5039
|
}
|
|
5040
|
+
},
|
|
5041
|
+
{
|
|
5042
|
+
name: "search",
|
|
5043
|
+
description: "OB1-compatible search adapter. Returns a flat list of {id, title, url, snippet} for connector ecosystems (ChatGPT Custom Connectors, Claude.ai, Deep-Research). Backed by hybrid (semantic+BM25+RRF) search. For richer output use search_hybrid.",
|
|
5044
|
+
inputSchema: {
|
|
5045
|
+
type: "object",
|
|
5046
|
+
required: ["query"],
|
|
5047
|
+
properties: {
|
|
5048
|
+
query: { type: "string" },
|
|
5049
|
+
limit: { type: "integer", minimum: 1, maximum: 50, default: 10 }
|
|
5050
|
+
}
|
|
5051
|
+
}
|
|
5052
|
+
},
|
|
5053
|
+
{
|
|
5054
|
+
name: "fetch",
|
|
5055
|
+
description: "OB1-compatible fetch adapter. Resolves an opaque id (from `search`) to {id, title, text, url, metadata}. Backed by read_note.",
|
|
5056
|
+
inputSchema: {
|
|
5057
|
+
type: "object",
|
|
5058
|
+
required: ["id"],
|
|
5059
|
+
properties: {
|
|
5060
|
+
id: {
|
|
5061
|
+
type: "string",
|
|
5062
|
+
description: "Opaque id from `search` results, format: <vault>:<vault-relative-path>"
|
|
5063
|
+
}
|
|
5064
|
+
}
|
|
5065
|
+
}
|
|
5066
|
+
},
|
|
5067
|
+
{
|
|
5068
|
+
name: "vault_stats",
|
|
5069
|
+
description: "Vault overview for agent self-orientation: note/word counts, top tags, top frontmatter keys, embedding model, last index run. Omit `vault` to get all configured vaults.",
|
|
5070
|
+
inputSchema: {
|
|
5071
|
+
type: "object",
|
|
5072
|
+
properties: {
|
|
5073
|
+
vault: { type: "string", description: "Optional. Omit for all vaults." }
|
|
5074
|
+
}
|
|
5075
|
+
}
|
|
5076
|
+
},
|
|
5077
|
+
{
|
|
5078
|
+
name: "recent_notes",
|
|
5079
|
+
description: "List recently modified notes (mtime DESC). Use for agent self-orientation: 'what has the user been working on lately?'. No vector search, just SQL.",
|
|
5080
|
+
inputSchema: {
|
|
5081
|
+
type: "object",
|
|
5082
|
+
properties: {
|
|
5083
|
+
vault: { type: "string", description: "Optional. Omit for all vaults." },
|
|
5084
|
+
limit: { type: "integer", minimum: 1, maximum: 200, default: 20 },
|
|
5085
|
+
since: {
|
|
5086
|
+
type: "integer",
|
|
5087
|
+
description: "Optional unix-ms threshold. Only notes with mtime > since."
|
|
5088
|
+
}
|
|
5089
|
+
}
|
|
5090
|
+
}
|
|
5034
5091
|
}
|
|
5035
5092
|
]
|
|
5036
5093
|
}));
|
|
@@ -5211,6 +5268,39 @@ async function serve() {
|
|
|
5211
5268
|
const runs = getIndexRuns({ vault, limit: parsed.limit });
|
|
5212
5269
|
return ok({ runs, count: runs.length });
|
|
5213
5270
|
}
|
|
5271
|
+
case "search": {
|
|
5272
|
+
const parsed = SearchCompatArgs.parse(args2 ?? {});
|
|
5273
|
+
return ok(
|
|
5274
|
+
await handleSearchCompat(
|
|
5275
|
+
manager,
|
|
5276
|
+
ollama,
|
|
5277
|
+
defaultModel,
|
|
5278
|
+
activeVault,
|
|
5279
|
+
parsed.query,
|
|
5280
|
+
parsed.limit,
|
|
5281
|
+
reranker
|
|
5282
|
+
)
|
|
5283
|
+
);
|
|
5284
|
+
}
|
|
5285
|
+
case "fetch": {
|
|
5286
|
+
const parsed = FetchCompatArgs.parse(args2 ?? {});
|
|
5287
|
+
return ok(handleFetchCompat(manager, parsed.id));
|
|
5288
|
+
}
|
|
5289
|
+
case "vault_stats": {
|
|
5290
|
+
const parsed = VaultStatsArgs.parse(args2 ?? {});
|
|
5291
|
+
return ok(handleVaultStats(manager, parsed.vault));
|
|
5292
|
+
}
|
|
5293
|
+
case "recent_notes": {
|
|
5294
|
+
const parsed = RecentNotesArgs.parse(args2 ?? {});
|
|
5295
|
+
return ok(
|
|
5296
|
+
handleRecentNotes(
|
|
5297
|
+
manager,
|
|
5298
|
+
parsed.vault,
|
|
5299
|
+
parsed.limit,
|
|
5300
|
+
parsed.since
|
|
5301
|
+
)
|
|
5302
|
+
);
|
|
5303
|
+
}
|
|
5214
5304
|
default:
|
|
5215
5305
|
return errorResponse(`Unknown tool: ${name}`);
|
|
5216
5306
|
}
|
|
@@ -5409,6 +5499,181 @@ async function handleSearchHybrid(manager, ollama, defaultModel, activeVault, qu
|
|
|
5409
5499
|
}
|
|
5410
5500
|
return out;
|
|
5411
5501
|
}
|
|
5502
|
+
function encodeNoteId(vault, path5) {
|
|
5503
|
+
return `${vault}:${path5}`;
|
|
5504
|
+
}
|
|
5505
|
+
function decodeNoteId(id) {
|
|
5506
|
+
const idx = id.indexOf(":");
|
|
5507
|
+
if (idx <= 0 || idx === id.length - 1) {
|
|
5508
|
+
throw new Error(
|
|
5509
|
+
`Invalid id: ${id}. Expected format <vault>:<vault-relative-path>.`
|
|
5510
|
+
);
|
|
5511
|
+
}
|
|
5512
|
+
return { vault: id.slice(0, idx), path: id.slice(idx + 1) };
|
|
5513
|
+
}
|
|
5514
|
+
function obsidianUrl(vaultName, notePath) {
|
|
5515
|
+
return `obsidian://open?vault=${encodeURIComponent(vaultName)}&file=${encodeURIComponent(notePath)}`;
|
|
5516
|
+
}
|
|
5517
|
+
async function handleSearchCompat(manager, ollama, defaultModel, activeVault, query, limit, reranker) {
|
|
5518
|
+
const { targets, skipped } = resolveVaultTargets(manager, void 0, activeVault);
|
|
5519
|
+
if (targets.length === 0) {
|
|
5520
|
+
return {
|
|
5521
|
+
results: [],
|
|
5522
|
+
note: skipped.length > 0 ? `All eligible vaults are indexing; skipped: ${skipped.join(", ")}.` : "No vaults configured."
|
|
5523
|
+
};
|
|
5524
|
+
}
|
|
5525
|
+
const hits = await hybridSearch({
|
|
5526
|
+
query,
|
|
5527
|
+
embeddingModel: defaultModel,
|
|
5528
|
+
ollama,
|
|
5529
|
+
vaults: targets,
|
|
5530
|
+
topK: limit,
|
|
5531
|
+
rrfK: 60,
|
|
5532
|
+
includeBreakdown: false,
|
|
5533
|
+
reranker
|
|
5534
|
+
});
|
|
5535
|
+
const seen = /* @__PURE__ */ new Set();
|
|
5536
|
+
const results = [];
|
|
5537
|
+
for (const h of hits) {
|
|
5538
|
+
const noteKey = `${h.vault}:${h.notePath}`;
|
|
5539
|
+
if (seen.has(noteKey)) continue;
|
|
5540
|
+
seen.add(noteKey);
|
|
5541
|
+
results.push({
|
|
5542
|
+
id: encodeNoteId(h.vault, h.notePath),
|
|
5543
|
+
title: h.noteTitle ?? h.notePath,
|
|
5544
|
+
url: obsidianUrl(h.vault, h.notePath),
|
|
5545
|
+
snippet: truncateSnippet(h.chunkText, 280)
|
|
5546
|
+
});
|
|
5547
|
+
if (results.length >= limit) break;
|
|
5548
|
+
}
|
|
5549
|
+
const out = { results };
|
|
5550
|
+
if (skipped.length > 0) {
|
|
5551
|
+
out.note = `Skipped vault(s) currently indexing: ${skipped.join(", ")}.`;
|
|
5552
|
+
}
|
|
5553
|
+
return out;
|
|
5554
|
+
}
|
|
5555
|
+
function truncateSnippet(text, max) {
|
|
5556
|
+
const collapsed = text.replace(/\s+/g, " ").trim();
|
|
5557
|
+
if (collapsed.length <= max) return collapsed;
|
|
5558
|
+
return collapsed.slice(0, max - 1).trimEnd() + "\u2026";
|
|
5559
|
+
}
|
|
5560
|
+
function handleFetchCompat(manager, id) {
|
|
5561
|
+
const { vault: vaultName, path: path5 } = decodeNoteId(id);
|
|
5562
|
+
const vault = manager.require(vaultName);
|
|
5563
|
+
const note = vault.db.notes.getByPath(path5);
|
|
5564
|
+
if (!note) {
|
|
5565
|
+
throw new Error(`Note not found: ${vaultName}/${path5}`);
|
|
5566
|
+
}
|
|
5567
|
+
const metadata = {
|
|
5568
|
+
vault: vaultName,
|
|
5569
|
+
path: note.path,
|
|
5570
|
+
mtime: note.mtime,
|
|
5571
|
+
hash: note.hash,
|
|
5572
|
+
word_count: note.word_count
|
|
5573
|
+
};
|
|
5574
|
+
if (note.frontmatter) {
|
|
5575
|
+
try {
|
|
5576
|
+
metadata.frontmatter = JSON.parse(note.frontmatter);
|
|
5577
|
+
} catch {
|
|
5578
|
+
}
|
|
5579
|
+
}
|
|
5580
|
+
return {
|
|
5581
|
+
id,
|
|
5582
|
+
title: note.title ?? note.path,
|
|
5583
|
+
text: note.content,
|
|
5584
|
+
url: obsidianUrl(vaultName, note.path),
|
|
5585
|
+
metadata
|
|
5586
|
+
};
|
|
5587
|
+
}
|
|
5588
|
+
function handleVaultStats(manager, vaultFilter) {
|
|
5589
|
+
const targets = vaultFilter ? [manager.require(vaultFilter)] : manager.list();
|
|
5590
|
+
const stats = targets.map((v) => {
|
|
5591
|
+
const total_notes = v.db.notes.countAll();
|
|
5592
|
+
const wordRow = v.db.handle.prepare(
|
|
5593
|
+
"SELECT SUM(word_count) AS total FROM notes"
|
|
5594
|
+
).get();
|
|
5595
|
+
const lastRun = v.db.audit.listRuns(1)[0];
|
|
5596
|
+
const activeModel = v.db.models.getActive();
|
|
5597
|
+
return {
|
|
5598
|
+
vault: v.config.name,
|
|
5599
|
+
vault_path: v.config.path,
|
|
5600
|
+
total_notes,
|
|
5601
|
+
total_words: wordRow?.total ?? 0,
|
|
5602
|
+
embedding_model: activeModel?.name ?? v.config.embedding_model ?? null,
|
|
5603
|
+
indexed_at: lastRun?.finished_at ?? null,
|
|
5604
|
+
top_tags: aggregateTopTags(v.db.handle, 10),
|
|
5605
|
+
top_frontmatter_keys: aggregateTopFrontmatterKeys(v.db.handle, 10)
|
|
5606
|
+
};
|
|
5607
|
+
});
|
|
5608
|
+
if (vaultFilter) {
|
|
5609
|
+
return stats[0];
|
|
5610
|
+
}
|
|
5611
|
+
return { vaults: stats, count: stats.length };
|
|
5612
|
+
}
|
|
5613
|
+
function aggregateTopTags(db, limit) {
|
|
5614
|
+
const rows = db.prepare(
|
|
5615
|
+
`
|
|
5616
|
+
SELECT je.value AS tag, COUNT(*) AS count
|
|
5617
|
+
FROM notes
|
|
5618
|
+
JOIN json_each(json_extract(notes.frontmatter, '$.tags')) AS je
|
|
5619
|
+
WHERE notes.frontmatter IS NOT NULL
|
|
5620
|
+
AND json_type(notes.frontmatter, '$.tags') = 'array'
|
|
5621
|
+
AND typeof(je.value) = 'text'
|
|
5622
|
+
GROUP BY je.value
|
|
5623
|
+
ORDER BY count DESC, tag ASC
|
|
5624
|
+
LIMIT ?
|
|
5625
|
+
`
|
|
5626
|
+
).all(limit);
|
|
5627
|
+
return rows;
|
|
5628
|
+
}
|
|
5629
|
+
function aggregateTopFrontmatterKeys(db, limit) {
|
|
5630
|
+
const rows = db.prepare(
|
|
5631
|
+
`
|
|
5632
|
+
SELECT je.key AS key, COUNT(*) AS count
|
|
5633
|
+
FROM notes
|
|
5634
|
+
JOIN json_each(notes.frontmatter) AS je
|
|
5635
|
+
WHERE notes.frontmatter IS NOT NULL
|
|
5636
|
+
AND json_type(notes.frontmatter) = 'object'
|
|
5637
|
+
GROUP BY je.key
|
|
5638
|
+
ORDER BY count DESC, key ASC
|
|
5639
|
+
LIMIT ?
|
|
5640
|
+
`
|
|
5641
|
+
).all(limit);
|
|
5642
|
+
return rows;
|
|
5643
|
+
}
|
|
5644
|
+
function handleRecentNotes(manager, vaultFilter, limit, since) {
|
|
5645
|
+
const targets = vaultFilter ? [manager.require(vaultFilter)] : manager.list();
|
|
5646
|
+
const all = [];
|
|
5647
|
+
for (const v of targets) {
|
|
5648
|
+
const rows = since !== void 0 ? v.db.handle.prepare(
|
|
5649
|
+
"SELECT path, title, mtime, word_count, frontmatter FROM notes WHERE mtime > ? ORDER BY mtime DESC LIMIT ?"
|
|
5650
|
+
).all(since, limit) : v.db.handle.prepare(
|
|
5651
|
+
"SELECT path, title, mtime, word_count, frontmatter FROM notes ORDER BY mtime DESC LIMIT ?"
|
|
5652
|
+
).all(limit);
|
|
5653
|
+
for (const r of rows) {
|
|
5654
|
+
let tags = null;
|
|
5655
|
+
if (r.frontmatter) {
|
|
5656
|
+
try {
|
|
5657
|
+
const fm = JSON.parse(r.frontmatter);
|
|
5658
|
+
if (Array.isArray(fm.tags)) {
|
|
5659
|
+
tags = fm.tags.filter((t) => typeof t === "string");
|
|
5660
|
+
}
|
|
5661
|
+
} catch {
|
|
5662
|
+
}
|
|
5663
|
+
}
|
|
5664
|
+
all.push({
|
|
5665
|
+
vault: v.config.name,
|
|
5666
|
+
path: r.path,
|
|
5667
|
+
title: r.title,
|
|
5668
|
+
mtime: r.mtime,
|
|
5669
|
+
word_count: r.word_count,
|
|
5670
|
+
tags
|
|
5671
|
+
});
|
|
5672
|
+
}
|
|
5673
|
+
}
|
|
5674
|
+
all.sort((a, b) => b.mtime - a.mtime);
|
|
5675
|
+
return { notes: all.slice(0, limit), count: Math.min(all.length, limit) };
|
|
5676
|
+
}
|
|
5412
5677
|
function ok(data) {
|
|
5413
5678
|
return {
|
|
5414
5679
|
content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
|
|
@@ -5420,7 +5685,7 @@ function errorResponse(message) {
|
|
|
5420
5685
|
content: [{ type: "text", text: message }]
|
|
5421
5686
|
};
|
|
5422
5687
|
}
|
|
5423
|
-
var VERSION, ReadNoteArgs, SearchArgs, HybridSearchArgs, VaultPathArgs, ForwardLinksArgs, FindBrokenLinksArgs, PredicateSchema, QueryFrontmatterArgs, WriteNoteArgs, UpdateFrontmatterArgs, DeleteNoteArgs, AuditLogArgs, IndexRunsArgs, ListModelsArgs, StartShadowIndexArgs, SwitchActiveModelArgs, VacuumEmbeddingsArgs;
|
|
5688
|
+
var VERSION, ReadNoteArgs, SearchArgs, HybridSearchArgs, VaultPathArgs, ForwardLinksArgs, FindBrokenLinksArgs, PredicateSchema, QueryFrontmatterArgs, WriteNoteArgs, UpdateFrontmatterArgs, DeleteNoteArgs, AuditLogArgs, IndexRunsArgs, ListModelsArgs, StartShadowIndexArgs, SwitchActiveModelArgs, VacuumEmbeddingsArgs, SearchCompatArgs, FetchCompatArgs, VaultStatsArgs, RecentNotesArgs;
|
|
5424
5689
|
var init_server = __esm({
|
|
5425
5690
|
"src/server.ts"() {
|
|
5426
5691
|
"use strict";
|
|
@@ -5437,7 +5702,7 @@ var init_server = __esm({
|
|
|
5437
5702
|
init_audit3();
|
|
5438
5703
|
init_watcher2();
|
|
5439
5704
|
init_indexer2();
|
|
5440
|
-
VERSION = "0.
|
|
5705
|
+
VERSION = "0.9.0";
|
|
5441
5706
|
ReadNoteArgs = z3.object({
|
|
5442
5707
|
vault: z3.string(),
|
|
5443
5708
|
path: z3.string()
|
|
@@ -5528,6 +5793,21 @@ var init_server = __esm({
|
|
|
5528
5793
|
VacuumEmbeddingsArgs = z3.object({
|
|
5529
5794
|
vault: z3.string()
|
|
5530
5795
|
});
|
|
5796
|
+
SearchCompatArgs = z3.object({
|
|
5797
|
+
query: z3.string().min(1),
|
|
5798
|
+
limit: z3.number().int().positive().max(50).optional().default(10)
|
|
5799
|
+
});
|
|
5800
|
+
FetchCompatArgs = z3.object({
|
|
5801
|
+
id: z3.string().min(1)
|
|
5802
|
+
});
|
|
5803
|
+
VaultStatsArgs = z3.object({
|
|
5804
|
+
vault: z3.string().optional()
|
|
5805
|
+
});
|
|
5806
|
+
RecentNotesArgs = z3.object({
|
|
5807
|
+
vault: z3.string().optional(),
|
|
5808
|
+
limit: z3.number().int().positive().max(200).optional().default(20),
|
|
5809
|
+
since: z3.number().int().nonnegative().optional()
|
|
5810
|
+
});
|
|
5531
5811
|
}
|
|
5532
5812
|
});
|
|
5533
5813
|
|