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 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` | Insert text immediately after a heading without touching frontmatter. |
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
- import {
3
- resolveSemanticConfig
4
- } from "./chunk-RRLYVK2X.js";
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 writeFile4(record) {
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
- writeFile4({ t: ts, level: lvl, msg, ...f });
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 join7 } from "path";
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(join7(modelDir, "config.json"), "utf8"),
3141
- readFile14(join7(modelDir, "tokenizer.json"), "utf8"),
3142
- readFile14(join7(modelDir, "model.safetensors"))
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 join8 } from "path";
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 = join8(cacheDir, "embeddings", vaultKey);
3206
- return { dir, bin: join8(dir, `${modelId}.bin`), manifest: join8(dir, `${modelId}.json`) };
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 (~200 chars) \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.',
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 join9, relative as relative2, sep as sep3 } from "path";
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(join9(vaultRoot2, relPath), "utf8");
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.0" : "0.0.0-dev";
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 import("./fetch-model-7TGOJMZL.js");
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 runFetchModel({ env: process.env, homedir: homedir2() });
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);