seekstone 0.15.0 → 0.15.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/dist/index.js +146 -19
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/dist/chunk-RRLYVK2X.js +0 -18
- package/dist/chunk-RRLYVK2X.js.map +0 -1
- package/dist/fetch-model-7TGOJMZL.js +0 -81
- package/dist/fetch-model-7TGOJMZL.js.map +0 -1
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
Seekstone is an Obsidian MCP server that gives Claude (and any [Model Context Protocol](https://modelcontextprotocol.io) client) direct read and write access to your Obsidian vault. No Obsidian app needs to be running, no plugins are required, and nothing leaves your machine.
|
|
6
6
|
|
|
7
|
-
It reads your vault **directly from disk** instead of routing through the Obsidian Local REST API plugin, and holds a warm full-text index in-process. The practical difference: searches return in **single-digit milliseconds** with **~2 KB payloads** that stay flat as your vault grows — a broad query that costs tens of megabytes of context through a REST-proxy server (up to **95 MB** at 10k notes) costs ~2 KB through Seekstone, up to a **~47,000× reduction**. Benchmarked against 7 other Obsidian MCP servers across committed 1k/5k/10k-note vaults — [fully reproducible](https://github.com/shaqmughal/seekstone/tree/main/packages/harness), full results at [seekstone.dev/benchmarks](https://seekstone.dev/benchmarks).
|
|
7
|
+
It reads your vault **directly from disk** instead of routing through the Obsidian Local REST API plugin, and holds a warm full-text index in-process. The practical difference: keyword searches return in **single-digit milliseconds** (semantic search in **~14 ms**) with **~2 KB payloads** that stay flat as your vault grows — a broad query that costs tens of megabytes of context through a REST-proxy server (up to **95 MB** at 10k notes) costs ~2 KB through Seekstone, up to a **~47,000× reduction**. Benchmarked against 7 other Obsidian MCP servers across committed 1k/5k/10k-note vaults — [fully reproducible](https://github.com/shaqmughal/seekstone/tree/main/packages/harness), full results at [seekstone.dev/benchmarks](https://seekstone.dev/benchmarks).
|
|
8
8
|
|
|
9
9
|
(Previously also published as `obsidian-mcp-seekstone` — that alias is deprecated; existing installs keep working, but install `seekstone` going forward.)
|
|
10
10
|
|
|
@@ -131,7 +131,7 @@ Other MCP clients (Windsurf, Cline, …) take the Option 3 JSON block in their o
|
|
|
131
131
|
| `rename_heading` | Rename a heading in a note — every `[[note#heading]]` wikilink and embed across the vault is rewritten so references keep working (aliases preserved, fenced code blocks left alone). |
|
|
132
132
|
| `append_note` | Append to a note body without touching frontmatter. |
|
|
133
133
|
| `patch_frontmatter` | Set/update/delete frontmatter keys without reordering existing keys or changing quote style. |
|
|
134
|
-
| `patch_note` |
|
|
134
|
+
| `patch_note` | Append, prepend, or replace text at a heading or block reference (`createIfMissing` to add the section) — frontmatter untouched. |
|
|
135
135
|
| `replace_in_note` | Find and replace text in the note body — literal or regex, whole-word, case sensitivity, optional `limit` (replaces **all** occurrences by default), dry-run preview. |
|
|
136
136
|
| `append_periodic_note` | Append to today's periodic note, creating it from a template if it doesn't yet exist. |
|
|
137
137
|
|
|
@@ -183,7 +183,7 @@ Any MCP-over-stdio client: Claude Desktop, Claude Code, Cursor, VS Code, Windsur
|
|
|
183
183
|
Yes — tested on macOS, Linux, and Windows in CI on every commit.
|
|
184
184
|
|
|
185
185
|
**Is it safe?**
|
|
186
|
-
No network calls, no telemetry. The vault path is sandboxed — no tool reads or writes outside it. Writes are covered by the tested Write-Safety Contract above, and `SEEKSTONE_READ_ONLY=1` removes the write tools entirely.
|
|
186
|
+
No network calls while running, no telemetry (the optional semantic-search model is downloaded once by the explicit `fetch-model` subcommand, SHA-256-verified, before any serving starts). The vault path is sandboxed — no tool reads or writes outside it. Writes are covered by the tested Write-Safety Contract above, and `SEEKSTONE_READ_ONLY=1` removes the write tools entirely.
|
|
187
187
|
|
|
188
188
|
---
|
|
189
189
|
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,126 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __esm = (fn, res, err) => function __init() {
|
|
5
|
+
if (err) throw err[0];
|
|
6
|
+
try {
|
|
7
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
8
|
+
} catch (e) {
|
|
9
|
+
throw err = [e], e;
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
var __export = (target, all) => {
|
|
13
|
+
for (var name in all)
|
|
14
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
// src/semantic/config.ts
|
|
18
|
+
import { join as join7 } from "path";
|
|
19
|
+
function defaultCacheDir(env, homedir2) {
|
|
20
|
+
return env.SEEKSTONE_CACHE_DIR || join7(homedir2, ".cache", "seekstone");
|
|
21
|
+
}
|
|
22
|
+
function resolveSemanticConfig(env, homedir2) {
|
|
23
|
+
if (env.SEEKSTONE_SEMANTIC !== "1") return void 0;
|
|
24
|
+
const cacheDir = defaultCacheDir(env, homedir2);
|
|
25
|
+
const modelDir = env.SEEKSTONE_MODEL_PATH || join7(cacheDir, "models", DEFAULT_MODEL_ID);
|
|
26
|
+
return { modelDir, cacheDir };
|
|
27
|
+
}
|
|
28
|
+
var DEFAULT_MODEL_ID;
|
|
29
|
+
var init_config = __esm({
|
|
30
|
+
"src/semantic/config.ts"() {
|
|
31
|
+
"use strict";
|
|
32
|
+
DEFAULT_MODEL_ID = "potion-base-8M";
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// src/semantic/model-manifest.ts
|
|
37
|
+
var DEFAULT_MODEL;
|
|
38
|
+
var init_model_manifest = __esm({
|
|
39
|
+
"src/semantic/model-manifest.ts"() {
|
|
40
|
+
"use strict";
|
|
41
|
+
DEFAULT_MODEL = {
|
|
42
|
+
id: "potion-base-8M",
|
|
43
|
+
license: "MIT (https://huggingface.co/minishlab/potion-base-8M)",
|
|
44
|
+
files: [
|
|
45
|
+
{
|
|
46
|
+
name: "model.safetensors",
|
|
47
|
+
url: "https://huggingface.co/minishlab/potion-base-8M/resolve/main/model.safetensors",
|
|
48
|
+
sha256: "f65d0f325faadc1e121c319e2faa41170d3fa07d8c89abd48ca5358d9a223de2",
|
|
49
|
+
bytes: 30236760
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
name: "tokenizer.json",
|
|
53
|
+
url: "https://huggingface.co/minishlab/potion-base-8M/resolve/main/tokenizer.json",
|
|
54
|
+
sha256: "e67e803f624fb4d67dea1c730d06e1067e1b14d830e2c2202569e3ef0f70bb50",
|
|
55
|
+
bytes: 683666
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
name: "config.json",
|
|
59
|
+
url: "https://huggingface.co/minishlab/potion-base-8M/resolve/main/config.json",
|
|
60
|
+
sha256: "2a6ac0e9aaa356a68a5688070db78fc3a464fefe85d2f06a1905ce3718687553",
|
|
61
|
+
bytes: 202
|
|
62
|
+
}
|
|
63
|
+
]
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// src/semantic/fetch-model.ts
|
|
69
|
+
var fetch_model_exports = {};
|
|
70
|
+
__export(fetch_model_exports, {
|
|
71
|
+
runFetchModel: () => runFetchModel
|
|
72
|
+
});
|
|
73
|
+
import { createHash as createHash3 } from "crypto";
|
|
74
|
+
import { existsSync } from "fs";
|
|
75
|
+
import { mkdir as mkdir7, readFile as readFile17, writeFile as writeFile4 } from "fs/promises";
|
|
76
|
+
import { join as join11 } from "path";
|
|
77
|
+
async function runFetchModel(deps) {
|
|
78
|
+
const manifest = deps.manifest ?? DEFAULT_MODEL;
|
|
79
|
+
const fetchFn = deps.fetchFn ?? fetch;
|
|
80
|
+
const destDir = deps.env.SEEKSTONE_MODEL_PATH || join11(defaultCacheDir(deps.env, deps.homedir), "models", manifest.id);
|
|
81
|
+
const output = [];
|
|
82
|
+
output.push(`fetch-model: ${manifest.id} \u2192 ${destDir}`);
|
|
83
|
+
try {
|
|
84
|
+
await mkdir7(destDir, { recursive: true });
|
|
85
|
+
let fetched = 0;
|
|
86
|
+
let skipped = 0;
|
|
87
|
+
for (const file of manifest.files) {
|
|
88
|
+
const dest = join11(destDir, file.name);
|
|
89
|
+
if (existsSync(dest) && sha256(await readFile17(dest)) === file.sha256) {
|
|
90
|
+
skipped++;
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
output.push(`fetching ${file.name} (${file.bytes} bytes)\u2026`);
|
|
94
|
+
const res = await fetchFn(file.url);
|
|
95
|
+
if (!res.ok) throw new Error(`fetch ${file.url} failed: ${res.status}`);
|
|
96
|
+
const buf = Buffer.from(await res.arrayBuffer());
|
|
97
|
+
const got = sha256(buf);
|
|
98
|
+
if (got !== file.sha256) {
|
|
99
|
+
throw new Error(`checksum mismatch for ${file.name}: expected ${file.sha256}, got ${got}`);
|
|
100
|
+
}
|
|
101
|
+
await writeFile4(dest, buf);
|
|
102
|
+
fetched++;
|
|
103
|
+
}
|
|
104
|
+
output.push(`\u2713 ${manifest.id}: ${fetched} fetched, ${skipped} already present (verified).`);
|
|
105
|
+
output.push(
|
|
106
|
+
"Enable semantic search with SEEKSTONE_SEMANTIC=1 in your MCP server config, then restart the session."
|
|
107
|
+
);
|
|
108
|
+
return { output, exitCode: 0 };
|
|
109
|
+
} catch (err) {
|
|
110
|
+
output.push(`\u2717 ${err instanceof Error ? err.message : String(err)}`);
|
|
111
|
+
return { output, exitCode: 1 };
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
function sha256(buf) {
|
|
115
|
+
return createHash3("sha256").update(buf).digest("hex");
|
|
116
|
+
}
|
|
117
|
+
var init_fetch_model = __esm({
|
|
118
|
+
"src/semantic/fetch-model.ts"() {
|
|
119
|
+
"use strict";
|
|
120
|
+
init_config();
|
|
121
|
+
init_model_manifest();
|
|
122
|
+
}
|
|
123
|
+
});
|
|
5
124
|
|
|
6
125
|
// src/index.ts
|
|
7
126
|
import { homedir } from "os";
|
|
@@ -2137,6 +2256,7 @@ var META_KEYS = [
|
|
|
2137
2256
|
"to",
|
|
2138
2257
|
"folder",
|
|
2139
2258
|
"tag",
|
|
2259
|
+
"mode",
|
|
2140
2260
|
"limit",
|
|
2141
2261
|
"overwrite",
|
|
2142
2262
|
"pattern",
|
|
@@ -2828,7 +2948,7 @@ function createLogger(opts = {}) {
|
|
|
2828
2948
|
);
|
|
2829
2949
|
}
|
|
2830
2950
|
}
|
|
2831
|
-
function
|
|
2951
|
+
function writeFile5(record) {
|
|
2832
2952
|
if (!fileOk || filePath === void 0) return;
|
|
2833
2953
|
const line = `${JSON.stringify(record)}
|
|
2834
2954
|
`;
|
|
@@ -2850,7 +2970,7 @@ function createLogger(opts = {}) {
|
|
|
2850
2970
|
const f = fields ?? {};
|
|
2851
2971
|
stderr(`${lvl.toUpperCase().padEnd(5)} ${ts} ${msg}${fmtFields(f)}
|
|
2852
2972
|
`);
|
|
2853
|
-
|
|
2973
|
+
writeFile5({ t: ts, level: lvl, msg, ...f });
|
|
2854
2974
|
}
|
|
2855
2975
|
return {
|
|
2856
2976
|
level,
|
|
@@ -2881,6 +3001,9 @@ function installProcessGuards(log2) {
|
|
|
2881
3001
|
};
|
|
2882
3002
|
}
|
|
2883
3003
|
|
|
3004
|
+
// src/index.ts
|
|
3005
|
+
init_config();
|
|
3006
|
+
|
|
2884
3007
|
// ../core/src/embed/chunk.ts
|
|
2885
3008
|
var MIN_WINDOW_WORDS = 250;
|
|
2886
3009
|
var MAX_WINDOW_WORDS = 400;
|
|
@@ -2956,7 +3079,7 @@ function countWords(text) {
|
|
|
2956
3079
|
|
|
2957
3080
|
// ../core/src/embed/model2vec.ts
|
|
2958
3081
|
import { readFile as readFile14 } from "fs/promises";
|
|
2959
|
-
import { basename as basename4, join as
|
|
3082
|
+
import { basename as basename4, join as join8 } from "path";
|
|
2960
3083
|
|
|
2961
3084
|
// ../core/src/embed/safetensors.ts
|
|
2962
3085
|
function readSafetensors(buf) {
|
|
@@ -3137,9 +3260,9 @@ function isCjk(cp) {
|
|
|
3137
3260
|
var TENSOR_NAME = "embeddings";
|
|
3138
3261
|
async function loadModel2Vec(modelDir) {
|
|
3139
3262
|
const [configRaw, tokenizerRaw, weights] = await Promise.all([
|
|
3140
|
-
readFile14(
|
|
3141
|
-
readFile14(
|
|
3142
|
-
readFile14(
|
|
3263
|
+
readFile14(join8(modelDir, "config.json"), "utf8"),
|
|
3264
|
+
readFile14(join8(modelDir, "tokenizer.json"), "utf8"),
|
|
3265
|
+
readFile14(join8(modelDir, "model.safetensors"))
|
|
3143
3266
|
]);
|
|
3144
3267
|
const config = JSON.parse(configRaw);
|
|
3145
3268
|
const tokenizer = loadWordPieceTokenizer(JSON.parse(tokenizerRaw));
|
|
@@ -3198,12 +3321,12 @@ async function loadModel2Vec(modelDir) {
|
|
|
3198
3321
|
// src/semantic/cache.ts
|
|
3199
3322
|
import { createHash as createHash2 } from "crypto";
|
|
3200
3323
|
import { mkdir as mkdir6, readFile as readFile15, rename as rename4, writeFile as writeFile3 } from "fs/promises";
|
|
3201
|
-
import { join as
|
|
3324
|
+
import { join as join9 } from "path";
|
|
3202
3325
|
var CACHE_VERSION = 2;
|
|
3203
3326
|
function cachePathsFor(cacheDir, vaultRoot2, modelId) {
|
|
3204
3327
|
const vaultKey = createHash2("sha256").update(vaultRoot2).digest("hex").slice(0, 16);
|
|
3205
|
-
const dir =
|
|
3206
|
-
return { dir, bin:
|
|
3328
|
+
const dir = join9(cacheDir, "embeddings", vaultKey);
|
|
3329
|
+
return { dir, bin: join9(dir, `${modelId}.bin`), manifest: join9(dir, `${modelId}.json`) };
|
|
3207
3330
|
}
|
|
3208
3331
|
async function loadCache(paths, modelId, dim) {
|
|
3209
3332
|
try {
|
|
@@ -3566,7 +3689,7 @@ var ALL_TOOLS = [
|
|
|
3566
3689
|
{
|
|
3567
3690
|
name: "search",
|
|
3568
3691
|
annotations: READ_ONLY_ANNOTATIONS,
|
|
3569
|
-
description: 'Full-text search across the vault. Returns ranked excerpts (~
|
|
3692
|
+
description: 'Full-text search across the vault. Returns ranked excerpts (~120 chars, tunable) \u2014 not full notes \u2014 to minimise context usage. Supports fuzzy matching and prefix search; with SEEKSTONE_SEMANTIC=1, mode "semantic"/"hybrid" searches by meaning via a local embedding model.',
|
|
3570
3693
|
inputSchema: {
|
|
3571
3694
|
type: "object",
|
|
3572
3695
|
properties: {
|
|
@@ -3578,7 +3701,11 @@ var ALL_TOOLS = [
|
|
|
3578
3701
|
},
|
|
3579
3702
|
limit: { type: "number", description: "Max results (1\u201350, default 10)." },
|
|
3580
3703
|
folder: { type: "string", description: "Restrict to a vault-relative folder prefix." },
|
|
3581
|
-
tag: { type: "string", description: "Restrict to notes with this tag." }
|
|
3704
|
+
tag: { type: "string", description: "Restrict to notes with this tag." },
|
|
3705
|
+
excerptLength: {
|
|
3706
|
+
type: "number",
|
|
3707
|
+
description: "Max characters of match context per hit (20\u20132000, default 120)."
|
|
3708
|
+
}
|
|
3582
3709
|
},
|
|
3583
3710
|
required: ["query"]
|
|
3584
3711
|
}
|
|
@@ -4061,7 +4188,7 @@ function visibleTools(policy2) {
|
|
|
4061
4188
|
// src/watcher.ts
|
|
4062
4189
|
import { realpathSync } from "fs";
|
|
4063
4190
|
import { readFile as readFile16 } from "fs/promises";
|
|
4064
|
-
import { join as
|
|
4191
|
+
import { join as join10, relative as relative2, sep as sep3 } from "path";
|
|
4065
4192
|
import chokidar from "chokidar";
|
|
4066
4193
|
function startWatcher(ctx2, log2, opts) {
|
|
4067
4194
|
const usePolling = opts?.usePolling ?? process.env.SEEKSTONE_WATCH_POLL === "1";
|
|
@@ -4073,7 +4200,7 @@ function startWatcher(ctx2, log2, opts) {
|
|
|
4073
4200
|
const toRel = (abs) => relative2(vaultRoot2, abs).split(sep3).join("/");
|
|
4074
4201
|
async function upsert(relPath, op) {
|
|
4075
4202
|
try {
|
|
4076
|
-
const raw = await readFile16(
|
|
4203
|
+
const raw = await readFile16(join10(vaultRoot2, relPath), "utf8");
|
|
4077
4204
|
removeNoteBacklinks(ctx2, relPath);
|
|
4078
4205
|
upsertDoc(ctx2, buildDoc(relPath, raw));
|
|
4079
4206
|
addNoteBacklinks(ctx2, relPath, raw);
|
|
@@ -4127,7 +4254,7 @@ function startWatcher(ctx2, log2, opts) {
|
|
|
4127
4254
|
}
|
|
4128
4255
|
|
|
4129
4256
|
// src/index.ts
|
|
4130
|
-
var VERSION = true ? "0.15.
|
|
4257
|
+
var VERSION = true ? "0.15.1" : "0.0.0-dev";
|
|
4131
4258
|
var intent = parseCliIntent(process.argv.slice(2));
|
|
4132
4259
|
if (intent === "version") {
|
|
4133
4260
|
process.stdout.write(`${VERSION}
|
|
@@ -4156,9 +4283,9 @@ if (intent === "init") {
|
|
|
4156
4283
|
process.exit(result.exitCode);
|
|
4157
4284
|
}
|
|
4158
4285
|
if (intent === "fetch-model") {
|
|
4159
|
-
const { runFetchModel } = await
|
|
4286
|
+
const { runFetchModel: runFetchModel2 } = await Promise.resolve().then(() => (init_fetch_model(), fetch_model_exports));
|
|
4160
4287
|
const { homedir: homedir2 } = await import("os");
|
|
4161
|
-
const result = await
|
|
4288
|
+
const result = await runFetchModel2({ env: process.env, homedir: homedir2() });
|
|
4162
4289
|
process.stdout.write(`${result.output.join("\n")}
|
|
4163
4290
|
`);
|
|
4164
4291
|
process.exit(result.exitCode);
|