seekstone 0.17.1 → 0.18.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 +4 -3
- package/dist/index.js +170 -58
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
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: keyword searches return in **single-digit milliseconds** (semantic search in **~
|
|
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 **~26 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
|
|
|
@@ -158,10 +158,11 @@ Every write tool (`append_note`, `patch_note`, `patch_frontmatter`, `replace_in_
|
|
|
158
158
|
| `SEEKSTONE_HISTORY_MAX_ENTRIES` | no | Cap on journal entries (default `1000`); the oldest are dropped past it. |
|
|
159
159
|
| `SEEKSTONE_AUDIT_FILE` | no | Absolute path; off unless set. Appends one JSON-line audit record per write-tool call — ok or refused — with the tool, paths, sha-256 before/after, outcome, and op metadata. Never note content. |
|
|
160
160
|
| `SEEKSTONE_AUDIT_MAX_SIZE` | no | Rotate the audit file to `<file>.1` past this size (e.g. `10mb`; default 10 MB). |
|
|
161
|
-
| `SEEKSTONE_SEMANTIC` | no | Set to `1` to enable semantic search (`search` gains `mode: "semantic"` and `"hybrid"`). Download the local model once with `npx -y seekstone fetch-model`; the running server never touches the network. |
|
|
161
|
+
| `SEEKSTONE_SEMANTIC` | no | Set to `1` to enable semantic search (`search` gains `mode: "semantic"` and `"hybrid"`). Download the local model once with `npx -y seekstone fetch-model`; the running server never touches the network. The `seekstone-semantic.mcpb` bundle sets this automatically and ships the model inside. |
|
|
162
162
|
| `SEEKSTONE_SEMANTIC_MODEL` | no | `potion-base-8M` (default, ~30 MB) or `potion-retrieval-32M` (~129 MB, more accurate, ~2× query latency). Fetch it first: `npx -y seekstone fetch-model --model potion-retrieval-32M`. |
|
|
163
163
|
| `SEEKSTONE_MODEL_PATH` | no | Directory holding the Model2Vec embedding model (default: where `fetch-model` puts the selected model). |
|
|
164
164
|
| `SEEKSTONE_CACHE_DIR` | no | Cache root for the model and per-vault embedding caches (default `~/.cache/seekstone`). |
|
|
165
|
+
| `SEEKSTONE_BUNDLED_MODEL_DIR` | no | Set by the `seekstone-semantic.mcpb` bundle's manifest — sharded model files shipped inside the extension, reassembled into the model directory at boot (disk-only, hash-verified). Not usually set by hand. |
|
|
165
166
|
|
|
166
167
|
---
|
|
167
168
|
|
|
@@ -208,7 +209,7 @@ Any MCP-over-stdio client: Claude Desktop, Claude Code, Cursor, VS Code, Windsur
|
|
|
208
209
|
Yes — tested on macOS, Linux, and Windows in CI on every commit.
|
|
209
210
|
|
|
210
211
|
**Is it safe?**
|
|
211
|
-
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.
|
|
212
|
+
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 — or shipped inside the `seekstone-semantic.mcpb` bundle and reassembled from disk at boot, verified against the same pinned hashes). 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.
|
|
212
213
|
|
|
213
214
|
---
|
|
214
215
|
|
package/dist/index.js
CHANGED
|
@@ -71,9 +71,9 @@ var init_model_manifest = __esm({
|
|
|
71
71
|
});
|
|
72
72
|
|
|
73
73
|
// src/semantic/config.ts
|
|
74
|
-
import { join as
|
|
74
|
+
import { join as join9 } from "path";
|
|
75
75
|
function defaultCacheDir(env, homedir2) {
|
|
76
|
-
return env.SEEKSTONE_CACHE_DIR ||
|
|
76
|
+
return env.SEEKSTONE_CACHE_DIR || join9(homedir2, ".cache", "seekstone");
|
|
77
77
|
}
|
|
78
78
|
function resolveModel(raw) {
|
|
79
79
|
const id = raw?.trim();
|
|
@@ -96,7 +96,7 @@ function resolveSemanticConfig(env, homedir2) {
|
|
|
96
96
|
if (env.SEEKSTONE_SEMANTIC !== "1") return void 0;
|
|
97
97
|
const modelId = resolveModelId(env.SEEKSTONE_SEMANTIC_MODEL);
|
|
98
98
|
const cacheDir = defaultCacheDir(env, homedir2);
|
|
99
|
-
const modelDir = env.SEEKSTONE_MODEL_PATH ||
|
|
99
|
+
const modelDir = env.SEEKSTONE_MODEL_PATH || join9(cacheDir, "models", modelId);
|
|
100
100
|
return { modelId, modelDir, cacheDir };
|
|
101
101
|
}
|
|
102
102
|
var DEFAULT_MODEL_ID;
|
|
@@ -114,10 +114,10 @@ __export(fetch_model_exports, {
|
|
|
114
114
|
runFetchModel: () => runFetchModel,
|
|
115
115
|
selectManifest: () => selectManifest
|
|
116
116
|
});
|
|
117
|
-
import { createHash as
|
|
118
|
-
import { existsSync } from "fs";
|
|
119
|
-
import { mkdir as
|
|
120
|
-
import { join as
|
|
117
|
+
import { createHash as createHash4 } from "crypto";
|
|
118
|
+
import { existsSync as existsSync2 } from "fs";
|
|
119
|
+
import { mkdir as mkdir10, readFile as readFile20, writeFile as writeFile5 } from "fs/promises";
|
|
120
|
+
import { join as join13 } from "path";
|
|
121
121
|
function selectManifest(argv, env) {
|
|
122
122
|
let requested;
|
|
123
123
|
for (let i = 0; i < argv.length; i++) {
|
|
@@ -146,15 +146,15 @@ async function runFetchModel(deps) {
|
|
|
146
146
|
return { output, exitCode: 1 };
|
|
147
147
|
}
|
|
148
148
|
const fetchFn = deps.fetchFn ?? fetch;
|
|
149
|
-
const destDir = deps.env.SEEKSTONE_MODEL_PATH ||
|
|
149
|
+
const destDir = deps.env.SEEKSTONE_MODEL_PATH || join13(defaultCacheDir(deps.env, deps.homedir), "models", manifest.id);
|
|
150
150
|
output.push(`fetch-model: ${manifest.id} \u2192 ${destDir}`);
|
|
151
151
|
try {
|
|
152
|
-
await
|
|
152
|
+
await mkdir10(destDir, { recursive: true });
|
|
153
153
|
let fetched = 0;
|
|
154
154
|
let skipped = 0;
|
|
155
155
|
for (const file of manifest.files) {
|
|
156
|
-
const dest =
|
|
157
|
-
if (
|
|
156
|
+
const dest = join13(destDir, file.name);
|
|
157
|
+
if (existsSync2(dest) && sha2562(await readFile20(dest)) === file.sha256) {
|
|
158
158
|
skipped++;
|
|
159
159
|
continue;
|
|
160
160
|
}
|
|
@@ -162,11 +162,11 @@ async function runFetchModel(deps) {
|
|
|
162
162
|
const res = await fetchFn(file.url);
|
|
163
163
|
if (!res.ok) throw new Error(`fetch ${file.url} failed: ${res.status}`);
|
|
164
164
|
const buf = Buffer.from(await res.arrayBuffer());
|
|
165
|
-
const got =
|
|
165
|
+
const got = sha2562(buf);
|
|
166
166
|
if (got !== file.sha256) {
|
|
167
167
|
throw new Error(`checksum mismatch for ${file.name}: expected ${file.sha256}, got ${got}`);
|
|
168
168
|
}
|
|
169
|
-
await
|
|
169
|
+
await writeFile5(dest, buf);
|
|
170
170
|
fetched++;
|
|
171
171
|
}
|
|
172
172
|
output.push(`\u2713 ${manifest.id}: ${fetched} fetched, ${skipped} already present (verified).`);
|
|
@@ -179,8 +179,8 @@ async function runFetchModel(deps) {
|
|
|
179
179
|
return { output, exitCode: 1 };
|
|
180
180
|
}
|
|
181
181
|
}
|
|
182
|
-
function
|
|
183
|
-
return
|
|
182
|
+
function sha2562(buf) {
|
|
183
|
+
return createHash4("sha256").update(buf).digest("hex");
|
|
184
184
|
}
|
|
185
185
|
var init_fetch_model = __esm({
|
|
186
186
|
"src/semantic/fetch-model.ts"() {
|
|
@@ -271,7 +271,7 @@ function createLogger(opts = {}) {
|
|
|
271
271
|
);
|
|
272
272
|
}
|
|
273
273
|
}
|
|
274
|
-
function
|
|
274
|
+
function writeFile6(record) {
|
|
275
275
|
if (!fileOk || filePath === void 0) return;
|
|
276
276
|
const line = `${JSON.stringify(record)}
|
|
277
277
|
`;
|
|
@@ -293,7 +293,7 @@ function createLogger(opts = {}) {
|
|
|
293
293
|
const f = fields ?? {};
|
|
294
294
|
stderr(`${lvl.toUpperCase().padEnd(5)} ${ts} ${msg}${fmtFields(f)}
|
|
295
295
|
`);
|
|
296
|
-
|
|
296
|
+
writeFile6({ t: ts, level: lvl, msg, ...f });
|
|
297
297
|
}
|
|
298
298
|
return {
|
|
299
299
|
level,
|
|
@@ -1010,19 +1010,27 @@ function truncate(s, max) {
|
|
|
1010
1010
|
}
|
|
1011
1011
|
|
|
1012
1012
|
// src/index/resolve.ts
|
|
1013
|
-
function
|
|
1013
|
+
function buildResolveMaps(notes2) {
|
|
1014
|
+
const byBasename = /* @__PURE__ */ new Map();
|
|
1015
|
+
const byPathNoExt = /* @__PURE__ */ new Map();
|
|
1016
|
+
for (const path2 of notes2.keys()) {
|
|
1017
|
+
const pathNoExt = path2.replace(/\.md$/i, "").toLowerCase();
|
|
1018
|
+
const base = pathNoExt.split("/").pop() ?? pathNoExt;
|
|
1019
|
+
const prevBase = byBasename.get(base);
|
|
1020
|
+
if (prevBase === void 0 || path2 < prevBase) byBasename.set(base, path2);
|
|
1021
|
+
const prevPath = byPathNoExt.get(pathNoExt);
|
|
1022
|
+
if (prevPath === void 0 || path2 < prevPath) byPathNoExt.set(pathNoExt, path2);
|
|
1023
|
+
}
|
|
1024
|
+
return { byBasename, byPathNoExt };
|
|
1025
|
+
}
|
|
1026
|
+
function resolveLink(target, notes2, maps) {
|
|
1014
1027
|
if (notes2.has(target)) return target;
|
|
1015
1028
|
const withMd = `${target}.md`;
|
|
1016
1029
|
if (notes2.has(withMd)) return withMd;
|
|
1030
|
+
const m = maps ?? buildResolveMaps(notes2);
|
|
1017
1031
|
const targetLower = target.toLowerCase();
|
|
1018
|
-
const targetBase =
|
|
1019
|
-
|
|
1020
|
-
const pathNoExt = path2.replace(/\.md$/i, "");
|
|
1021
|
-
if (pathNoExt.toLowerCase() === targetLower) return path2;
|
|
1022
|
-
const pathBase = (pathNoExt.split("/").pop() ?? pathNoExt).toLowerCase();
|
|
1023
|
-
if (pathBase === targetBase) return path2;
|
|
1024
|
-
}
|
|
1025
|
-
return void 0;
|
|
1032
|
+
const targetBase = targetLower.split("/").pop() ?? targetLower;
|
|
1033
|
+
return m.byBasename.get(targetBase) ?? m.byPathNoExt.get(targetLower);
|
|
1026
1034
|
}
|
|
1027
1035
|
|
|
1028
1036
|
// src/tools/search.ts
|
|
@@ -1475,8 +1483,11 @@ import { z as z5 } from "zod";
|
|
|
1475
1483
|
function removeNoteBacklinks(ctx2, relPath) {
|
|
1476
1484
|
const oldDoc = ctx2.notes.get(relPath);
|
|
1477
1485
|
if (oldDoc === void 0) return;
|
|
1478
|
-
|
|
1479
|
-
|
|
1486
|
+
const links = extractLinksWithLines(oldDoc.raw);
|
|
1487
|
+
if (links.length === 0) return;
|
|
1488
|
+
const maps = buildResolveMaps(ctx2.notes);
|
|
1489
|
+
for (const link of links) {
|
|
1490
|
+
const resolved = resolveLink(link.target, ctx2.notes, maps);
|
|
1480
1491
|
if (resolved === void 0) continue;
|
|
1481
1492
|
const arr = ctx2.backlinks.get(resolved);
|
|
1482
1493
|
if (arr === void 0) continue;
|
|
@@ -1486,8 +1497,11 @@ function removeNoteBacklinks(ctx2, relPath) {
|
|
|
1486
1497
|
}
|
|
1487
1498
|
function addNoteBacklinks(ctx2, relPath, raw) {
|
|
1488
1499
|
const seen = /* @__PURE__ */ new Set();
|
|
1489
|
-
|
|
1490
|
-
|
|
1500
|
+
const links = extractLinksWithLines(raw);
|
|
1501
|
+
if (links.length === 0) return;
|
|
1502
|
+
const maps = buildResolveMaps(ctx2.notes);
|
|
1503
|
+
for (const link of links) {
|
|
1504
|
+
const resolved = resolveLink(link.target, ctx2.notes, maps);
|
|
1491
1505
|
if (resolved === void 0) continue;
|
|
1492
1506
|
const dedupeKey = `${relPath}\0${resolved}`;
|
|
1493
1507
|
if (seen.has(dedupeKey)) continue;
|
|
@@ -3452,10 +3466,11 @@ async function buildIndex(vaultRoot2) {
|
|
|
3452
3466
|
}
|
|
3453
3467
|
function buildBacklinks(notes2) {
|
|
3454
3468
|
const result = /* @__PURE__ */ new Map();
|
|
3469
|
+
const resolveMaps = buildResolveMaps(notes2);
|
|
3455
3470
|
for (const [srcPath, doc] of notes2) {
|
|
3456
3471
|
const seen = /* @__PURE__ */ new Set();
|
|
3457
3472
|
for (const link of extractLinksWithLines(doc.raw)) {
|
|
3458
|
-
const resolved = resolveLink(link.target, notes2);
|
|
3473
|
+
const resolved = resolveLink(link.target, notes2, resolveMaps);
|
|
3459
3474
|
if (resolved === void 0) continue;
|
|
3460
3475
|
const dedupeKey = `${srcPath}\0${resolved}`;
|
|
3461
3476
|
if (seen.has(dedupeKey)) continue;
|
|
@@ -3779,6 +3794,82 @@ function installProcessGuards(log2) {
|
|
|
3779
3794
|
};
|
|
3780
3795
|
}
|
|
3781
3796
|
|
|
3797
|
+
// src/semantic/bundled-model.ts
|
|
3798
|
+
init_model_manifest();
|
|
3799
|
+
import { createHash as createHash2 } from "crypto";
|
|
3800
|
+
import { existsSync } from "fs";
|
|
3801
|
+
import { mkdir as mkdir8, readdir as readdir3, readFile as readFile16, rename as rename5, writeFile as writeFile3 } from "fs/promises";
|
|
3802
|
+
import { join as join8 } from "path";
|
|
3803
|
+
async function materializeBundledModel(cfg, bundledDir, deps = {}) {
|
|
3804
|
+
if (!bundledDir) return;
|
|
3805
|
+
if (!existsSync(bundledDir)) {
|
|
3806
|
+
deps.log?.warn("bundled model dir missing \u2014 skipping materialization", { dir: bundledDir });
|
|
3807
|
+
return;
|
|
3808
|
+
}
|
|
3809
|
+
let bundledId;
|
|
3810
|
+
try {
|
|
3811
|
+
const raw = await readFile16(join8(bundledDir, "manifest.json"), "utf8");
|
|
3812
|
+
bundledId = String(JSON.parse(raw).id);
|
|
3813
|
+
} catch (err) {
|
|
3814
|
+
throw new Error(
|
|
3815
|
+
`bundled model: cannot read ${join8(bundledDir, "manifest.json")} \u2014 the extension bundle is corrupt; reinstall it (${err instanceof Error ? err.message : String(err)})`
|
|
3816
|
+
);
|
|
3817
|
+
}
|
|
3818
|
+
if (bundledId !== cfg.modelId) {
|
|
3819
|
+
deps.log?.info("bundled model does not match the requested model \u2014 skipping", {
|
|
3820
|
+
bundled: bundledId,
|
|
3821
|
+
requested: cfg.modelId
|
|
3822
|
+
});
|
|
3823
|
+
return;
|
|
3824
|
+
}
|
|
3825
|
+
const manifest = (deps.manifestFor ?? findModel)(cfg.modelId);
|
|
3826
|
+
if (!manifest) return;
|
|
3827
|
+
await mkdir8(cfg.modelDir, { recursive: true });
|
|
3828
|
+
let written = 0;
|
|
3829
|
+
for (const file of manifest.files) {
|
|
3830
|
+
const dest = join8(cfg.modelDir, file.name);
|
|
3831
|
+
if (existsSync(dest) && sha256(await readFile16(dest)) === file.sha256) continue;
|
|
3832
|
+
const buf = await concatParts(bundledDir, file.name);
|
|
3833
|
+
const got = sha256(buf);
|
|
3834
|
+
if (got !== file.sha256) {
|
|
3835
|
+
throw new Error(
|
|
3836
|
+
`bundled model: checksum mismatch for ${file.name} (expected ${file.sha256}, got ${got}) \u2014 the extension bundle is corrupt; reinstall it`
|
|
3837
|
+
);
|
|
3838
|
+
}
|
|
3839
|
+
const tmp = `${dest}.tmp-${process.pid}`;
|
|
3840
|
+
await writeFile3(tmp, buf);
|
|
3841
|
+
await rename5(tmp, dest);
|
|
3842
|
+
written++;
|
|
3843
|
+
}
|
|
3844
|
+
if (written > 0) {
|
|
3845
|
+
deps.log?.info("bundled model materialized", {
|
|
3846
|
+
model: manifest.id,
|
|
3847
|
+
dir: cfg.modelDir,
|
|
3848
|
+
files: written
|
|
3849
|
+
});
|
|
3850
|
+
}
|
|
3851
|
+
}
|
|
3852
|
+
async function concatParts(bundledDir, name) {
|
|
3853
|
+
const prefix = `${name}.`;
|
|
3854
|
+
const partNum = (f) => Number(f.slice(prefix.length, -".part".length));
|
|
3855
|
+
const parts = (await readdir3(bundledDir)).filter((f) => {
|
|
3856
|
+
if (!f.startsWith(prefix) || !f.endsWith(".part")) return false;
|
|
3857
|
+
const middle = f.slice(prefix.length, -".part".length);
|
|
3858
|
+
return /^\d+$/.test(middle);
|
|
3859
|
+
}).sort((a, b) => partNum(a) - partNum(b));
|
|
3860
|
+
if (parts.length === 0) {
|
|
3861
|
+
throw new Error(
|
|
3862
|
+
`bundled model: no shards for ${name} in ${bundledDir} \u2014 the extension bundle is corrupt; reinstall it`
|
|
3863
|
+
);
|
|
3864
|
+
}
|
|
3865
|
+
const buffers = [];
|
|
3866
|
+
for (const part of parts) buffers.push(await readFile16(join8(bundledDir, part)));
|
|
3867
|
+
return Buffer.concat(buffers);
|
|
3868
|
+
}
|
|
3869
|
+
function sha256(buf) {
|
|
3870
|
+
return createHash2("sha256").update(buf).digest("hex");
|
|
3871
|
+
}
|
|
3872
|
+
|
|
3782
3873
|
// src/index.ts
|
|
3783
3874
|
init_config();
|
|
3784
3875
|
|
|
@@ -3930,8 +4021,8 @@ function candidateSetIdf(queryIds, docs) {
|
|
|
3930
4021
|
}
|
|
3931
4022
|
|
|
3932
4023
|
// ../core/src/embed/model2vec.ts
|
|
3933
|
-
import { readFile as
|
|
3934
|
-
import { basename as basename4, join as
|
|
4024
|
+
import { readFile as readFile17 } from "fs/promises";
|
|
4025
|
+
import { basename as basename4, join as join10 } from "path";
|
|
3935
4026
|
|
|
3936
4027
|
// ../core/src/embed/safetensors.ts
|
|
3937
4028
|
function readSafetensors(buf) {
|
|
@@ -4112,9 +4203,9 @@ function isCjk(cp) {
|
|
|
4112
4203
|
var TENSOR_NAME = "embeddings";
|
|
4113
4204
|
async function loadModel2Vec(modelDir) {
|
|
4114
4205
|
const [configRaw, tokenizerRaw, weights] = await Promise.all([
|
|
4115
|
-
|
|
4116
|
-
|
|
4117
|
-
|
|
4206
|
+
readFile17(join10(modelDir, "config.json"), "utf8"),
|
|
4207
|
+
readFile17(join10(modelDir, "tokenizer.json"), "utf8"),
|
|
4208
|
+
readFile17(join10(modelDir, "model.safetensors"))
|
|
4118
4209
|
]);
|
|
4119
4210
|
const config = JSON.parse(configRaw);
|
|
4120
4211
|
const tokenizer = loadWordPieceTokenizer(JSON.parse(tokenizerRaw));
|
|
@@ -4272,26 +4363,27 @@ function isTokenEmbedder(e) {
|
|
|
4272
4363
|
}
|
|
4273
4364
|
|
|
4274
4365
|
// src/semantic/cache.ts
|
|
4275
|
-
import { createHash as
|
|
4276
|
-
import { mkdir as
|
|
4277
|
-
import { join as
|
|
4278
|
-
var CACHE_VERSION =
|
|
4366
|
+
import { createHash as createHash3 } from "crypto";
|
|
4367
|
+
import { mkdir as mkdir9, readFile as readFile18, rename as rename6, writeFile as writeFile4 } from "fs/promises";
|
|
4368
|
+
import { join as join11 } from "path";
|
|
4369
|
+
var CACHE_VERSION = 3;
|
|
4279
4370
|
function cachePathsFor(cacheDir, vaultRoot2, modelId) {
|
|
4280
|
-
const vaultKey =
|
|
4281
|
-
const dir =
|
|
4282
|
-
return { dir, bin:
|
|
4371
|
+
const vaultKey = createHash3("sha256").update(vaultRoot2).digest("hex").slice(0, 16);
|
|
4372
|
+
const dir = join11(cacheDir, "embeddings", vaultKey);
|
|
4373
|
+
return { dir, bin: join11(dir, `${modelId}.bin`), manifest: join11(dir, `${modelId}.json`) };
|
|
4283
4374
|
}
|
|
4284
4375
|
async function loadCache(paths, modelId, dim) {
|
|
4285
4376
|
try {
|
|
4286
|
-
const manifest = JSON.parse(await
|
|
4377
|
+
const manifest = JSON.parse(await readFile18(paths.manifest, "utf8"));
|
|
4287
4378
|
if (manifest.version !== CACHE_VERSION || manifest.modelId !== modelId || manifest.dim !== dim) {
|
|
4288
4379
|
return void 0;
|
|
4289
4380
|
}
|
|
4290
|
-
const buf = await
|
|
4381
|
+
const buf = await readFile18(paths.bin);
|
|
4291
4382
|
const totalChunks = manifest.entries.reduce((a, e) => a + e.chunks, 0);
|
|
4292
4383
|
const vectorBytes = totalChunks * dim * 4;
|
|
4293
4384
|
const spanBytes = totalChunks * 2 * 4;
|
|
4294
4385
|
if (buf.byteLength !== vectorBytes + spanBytes) return void 0;
|
|
4386
|
+
if (binHashOf(buf) !== manifest.binHash) return void 0;
|
|
4295
4387
|
const aligned = new Uint8Array(buf.byteLength);
|
|
4296
4388
|
aligned.set(buf);
|
|
4297
4389
|
const floats = new Float32Array(aligned.buffer, 0, totalChunks * dim);
|
|
@@ -4314,17 +4406,22 @@ async function loadCache(paths, modelId, dim) {
|
|
|
4314
4406
|
}
|
|
4315
4407
|
}
|
|
4316
4408
|
async function saveCache(paths, modelId, dim, notes2, hashes) {
|
|
4317
|
-
const
|
|
4318
|
-
const kept = [];
|
|
4409
|
+
const rows = [];
|
|
4319
4410
|
let totalChunks = 0;
|
|
4320
4411
|
for (const [path2, note] of notes2) {
|
|
4321
4412
|
const contentHash2 = hashes.get(path2);
|
|
4322
4413
|
if (!contentHash2) continue;
|
|
4323
4414
|
const chunks = note.packed.length / dim;
|
|
4324
|
-
|
|
4325
|
-
kept.push(note);
|
|
4415
|
+
rows.push({ path: path2, contentHash: contentHash2, chunks, note });
|
|
4326
4416
|
totalChunks += chunks;
|
|
4327
4417
|
}
|
|
4418
|
+
rows.sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
|
|
4419
|
+
const entries = rows.map(({ path: path2, contentHash: contentHash2, chunks }) => ({
|
|
4420
|
+
path: path2,
|
|
4421
|
+
contentHash: contentHash2,
|
|
4422
|
+
chunks
|
|
4423
|
+
}));
|
|
4424
|
+
const kept = rows.map((r) => r.note);
|
|
4328
4425
|
const bin = new Uint8Array(totalChunks * dim * 4 + totalChunks * 2 * 4);
|
|
4329
4426
|
const floats = new Float32Array(bin.buffer, 0, totalChunks * dim);
|
|
4330
4427
|
const spans = new Uint32Array(bin.buffer, totalChunks * dim * 4, totalChunks * 2);
|
|
@@ -4336,15 +4433,29 @@ async function saveCache(paths, modelId, dim, notes2, hashes) {
|
|
|
4336
4433
|
floatOffset += note.packed.length;
|
|
4337
4434
|
spanOffset += note.spans.length;
|
|
4338
4435
|
}
|
|
4339
|
-
const manifest = {
|
|
4340
|
-
|
|
4436
|
+
const manifest = {
|
|
4437
|
+
version: CACHE_VERSION,
|
|
4438
|
+
modelId,
|
|
4439
|
+
dim,
|
|
4440
|
+
binHash: binHashOf(bin),
|
|
4441
|
+
entries
|
|
4442
|
+
};
|
|
4443
|
+
await mkdir9(paths.dir, { recursive: true });
|
|
4341
4444
|
await atomicWriteBytes(paths.bin, bin);
|
|
4342
4445
|
await atomicWriteBytes(paths.manifest, new TextEncoder().encode(JSON.stringify(manifest)));
|
|
4343
4446
|
}
|
|
4447
|
+
function binHashOf(bin) {
|
|
4448
|
+
return createHash3("sha256").update(bin).digest("hex");
|
|
4449
|
+
}
|
|
4450
|
+
var tempSeq = 0;
|
|
4451
|
+
function tempPathFor(absPath) {
|
|
4452
|
+
tempSeq += 1;
|
|
4453
|
+
return `${absPath}.${process.pid}.${tempSeq}.seekstone-cache-tmp`;
|
|
4454
|
+
}
|
|
4344
4455
|
async function atomicWriteBytes(absPath, bytes) {
|
|
4345
|
-
const tmpPath =
|
|
4346
|
-
await
|
|
4347
|
-
await
|
|
4456
|
+
const tmpPath = tempPathFor(absPath);
|
|
4457
|
+
await writeFile4(tmpPath, bytes);
|
|
4458
|
+
await rename6(tmpPath, absPath);
|
|
4348
4459
|
}
|
|
4349
4460
|
|
|
4350
4461
|
// src/semantic/state.ts
|
|
@@ -5268,8 +5379,8 @@ function visibleTools(policy2) {
|
|
|
5268
5379
|
|
|
5269
5380
|
// src/watcher.ts
|
|
5270
5381
|
import { realpathSync } from "fs";
|
|
5271
|
-
import { readFile as
|
|
5272
|
-
import { join as
|
|
5382
|
+
import { readFile as readFile19 } from "fs/promises";
|
|
5383
|
+
import { join as join12, relative as relative2, sep as sep3 } from "path";
|
|
5273
5384
|
import chokidar from "chokidar";
|
|
5274
5385
|
var DEFAULT_POLL_INTERVAL_MS = 1e4;
|
|
5275
5386
|
function resolvePollInterval(optValue, envValue, log2) {
|
|
@@ -5300,7 +5411,7 @@ function startWatcher(ctx2, log2, opts) {
|
|
|
5300
5411
|
const toRel = (abs) => relative2(vaultRoot2, abs).split(sep3).join("/");
|
|
5301
5412
|
async function upsert(relPath, op) {
|
|
5302
5413
|
try {
|
|
5303
|
-
const raw = await
|
|
5414
|
+
const raw = await readFile19(join12(vaultRoot2, relPath), "utf8");
|
|
5304
5415
|
removeNoteBacklinks(ctx2, relPath);
|
|
5305
5416
|
upsertDoc(ctx2, buildDoc(relPath, raw));
|
|
5306
5417
|
addNoteBacklinks(ctx2, relPath, raw);
|
|
@@ -5354,7 +5465,7 @@ function startWatcher(ctx2, log2, opts) {
|
|
|
5354
5465
|
}
|
|
5355
5466
|
|
|
5356
5467
|
// src/index.ts
|
|
5357
|
-
var VERSION = true ? "0.
|
|
5468
|
+
var VERSION = true ? "0.18.0" : "0.0.0-dev";
|
|
5358
5469
|
var intent = parseCliIntent(process.argv.slice(2));
|
|
5359
5470
|
if (intent === "version") {
|
|
5360
5471
|
process.stdout.write(`${VERSION}
|
|
@@ -5439,6 +5550,7 @@ if (auditCfg) {
|
|
|
5439
5550
|
var semanticCfg = resolveSemanticConfig(process.env, homedir());
|
|
5440
5551
|
if (semanticCfg) {
|
|
5441
5552
|
try {
|
|
5553
|
+
await materializeBundledModel(semanticCfg, process.env.SEEKSTONE_BUNDLED_MODEL_DIR, { log });
|
|
5442
5554
|
ctx.semantic = await Semantic.start(ctx, semanticCfg, { log });
|
|
5443
5555
|
log.info("semantic search enabled", {
|
|
5444
5556
|
model: ctx.semantic.embedder.id,
|