seekstone 0.16.0 → 0.17.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 +29 -5
- package/dist/index.js +1508 -397
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -14,79 +14,147 @@ var __export = (target, all) => {
|
|
|
14
14
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
15
15
|
};
|
|
16
16
|
|
|
17
|
+
// src/semantic/model-manifest.ts
|
|
18
|
+
function hfFiles(repo, hashes) {
|
|
19
|
+
const base = `https://huggingface.co/minishlab/${repo}/resolve/main`;
|
|
20
|
+
return [
|
|
21
|
+
{
|
|
22
|
+
name: "model.safetensors",
|
|
23
|
+
url: `${base}/model.safetensors`,
|
|
24
|
+
sha256: hashes.safetensors[0],
|
|
25
|
+
bytes: hashes.safetensors[1]
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
name: "tokenizer.json",
|
|
29
|
+
url: `${base}/tokenizer.json`,
|
|
30
|
+
sha256: hashes.tokenizer[0],
|
|
31
|
+
bytes: hashes.tokenizer[1]
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
name: "config.json",
|
|
35
|
+
url: `${base}/config.json`,
|
|
36
|
+
sha256: hashes.config[0],
|
|
37
|
+
bytes: hashes.config[1]
|
|
38
|
+
}
|
|
39
|
+
];
|
|
40
|
+
}
|
|
41
|
+
function findModel(id) {
|
|
42
|
+
return MODELS.find((m) => m.id === id);
|
|
43
|
+
}
|
|
44
|
+
var DEFAULT_MODEL, RETRIEVAL_32M_MODEL, MODELS, MODEL_IDS;
|
|
45
|
+
var init_model_manifest = __esm({
|
|
46
|
+
"src/semantic/model-manifest.ts"() {
|
|
47
|
+
"use strict";
|
|
48
|
+
DEFAULT_MODEL = {
|
|
49
|
+
id: "potion-base-8M",
|
|
50
|
+
license: "MIT (https://huggingface.co/minishlab/potion-base-8M)",
|
|
51
|
+
dim: 256,
|
|
52
|
+
files: hfFiles("potion-base-8M", {
|
|
53
|
+
safetensors: ["f65d0f325faadc1e121c319e2faa41170d3fa07d8c89abd48ca5358d9a223de2", 30236760],
|
|
54
|
+
tokenizer: ["e67e803f624fb4d67dea1c730d06e1067e1b14d830e2c2202569e3ef0f70bb50", 683666],
|
|
55
|
+
config: ["2a6ac0e9aaa356a68a5688070db78fc3a464fefe85d2f06a1905ce3718687553", 202]
|
|
56
|
+
})
|
|
57
|
+
};
|
|
58
|
+
RETRIEVAL_32M_MODEL = {
|
|
59
|
+
id: "potion-retrieval-32M",
|
|
60
|
+
license: "MIT (https://huggingface.co/minishlab/potion-retrieval-32M)",
|
|
61
|
+
dim: 512,
|
|
62
|
+
files: hfFiles("potion-retrieval-32M", {
|
|
63
|
+
safetensors: ["07609e5bd33aad37900b3fd62f4ec96f6daec88ca4d46b9d8b928bfababf6ea0", 129210456],
|
|
64
|
+
tokenizer: ["7d75cbc54318138807c401b0f0c9721117c628b39de8e8e0edb6cb17e0ee7d18", 1493150],
|
|
65
|
+
config: ["63c00d90824c832c04ec1d02b6a983fb90489bf049f29fbff15ba481b8a432ee", 202]
|
|
66
|
+
})
|
|
67
|
+
};
|
|
68
|
+
MODELS = [DEFAULT_MODEL, RETRIEVAL_32M_MODEL];
|
|
69
|
+
MODEL_IDS = MODELS.map((m) => m.id);
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
|
|
17
73
|
// src/semantic/config.ts
|
|
18
|
-
import { join as
|
|
74
|
+
import { join as join8 } from "path";
|
|
19
75
|
function defaultCacheDir(env, homedir2) {
|
|
20
|
-
return env.SEEKSTONE_CACHE_DIR ||
|
|
76
|
+
return env.SEEKSTONE_CACHE_DIR || join8(homedir2, ".cache", "seekstone");
|
|
77
|
+
}
|
|
78
|
+
function resolveModel(raw) {
|
|
79
|
+
const id = raw?.trim();
|
|
80
|
+
if (!id) return DEFAULT_MODEL;
|
|
81
|
+
const manifest = findModel(id);
|
|
82
|
+
if (!manifest) {
|
|
83
|
+
throw new Error(
|
|
84
|
+
`SEEKSTONE_SEMANTIC_MODEL: unknown model "${id}" \u2014 expected one of ${MODEL_IDS.join(", ")}`
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
return manifest;
|
|
88
|
+
}
|
|
89
|
+
function resolveModelId(raw) {
|
|
90
|
+
return resolveModel(raw).id;
|
|
91
|
+
}
|
|
92
|
+
function fetchCommandFor(modelId) {
|
|
93
|
+
return modelId === DEFAULT_MODEL_ID ? "npx -y seekstone fetch-model" : `npx -y seekstone fetch-model --model ${modelId}`;
|
|
21
94
|
}
|
|
22
95
|
function resolveSemanticConfig(env, homedir2) {
|
|
23
96
|
if (env.SEEKSTONE_SEMANTIC !== "1") return void 0;
|
|
97
|
+
const modelId = resolveModelId(env.SEEKSTONE_SEMANTIC_MODEL);
|
|
24
98
|
const cacheDir = defaultCacheDir(env, homedir2);
|
|
25
|
-
const modelDir = env.SEEKSTONE_MODEL_PATH ||
|
|
26
|
-
return { modelDir, cacheDir };
|
|
99
|
+
const modelDir = env.SEEKSTONE_MODEL_PATH || join8(cacheDir, "models", modelId);
|
|
100
|
+
return { modelId, modelDir, cacheDir };
|
|
27
101
|
}
|
|
28
102
|
var DEFAULT_MODEL_ID;
|
|
29
103
|
var init_config = __esm({
|
|
30
104
|
"src/semantic/config.ts"() {
|
|
31
105
|
"use strict";
|
|
32
|
-
|
|
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
|
-
};
|
|
106
|
+
init_model_manifest();
|
|
107
|
+
DEFAULT_MODEL_ID = DEFAULT_MODEL.id;
|
|
65
108
|
}
|
|
66
109
|
});
|
|
67
110
|
|
|
68
111
|
// src/semantic/fetch-model.ts
|
|
69
112
|
var fetch_model_exports = {};
|
|
70
113
|
__export(fetch_model_exports, {
|
|
71
|
-
runFetchModel: () => runFetchModel
|
|
114
|
+
runFetchModel: () => runFetchModel,
|
|
115
|
+
selectManifest: () => selectManifest
|
|
72
116
|
});
|
|
73
117
|
import { createHash as createHash3 } from "crypto";
|
|
74
118
|
import { existsSync } from "fs";
|
|
75
|
-
import { mkdir as
|
|
76
|
-
import { join as
|
|
119
|
+
import { mkdir as mkdir9, readFile as readFile19, writeFile as writeFile4 } from "fs/promises";
|
|
120
|
+
import { join as join12 } from "path";
|
|
121
|
+
function selectManifest(argv, env) {
|
|
122
|
+
let requested;
|
|
123
|
+
for (let i = 0; i < argv.length; i++) {
|
|
124
|
+
const arg = argv[i];
|
|
125
|
+
if (arg === "--model") {
|
|
126
|
+
requested = argv[i + 1];
|
|
127
|
+
if (!requested || requested.startsWith("--")) {
|
|
128
|
+
throw new Error(`--model needs a value: one of ${MODEL_IDS.join(", ")}`);
|
|
129
|
+
}
|
|
130
|
+
i++;
|
|
131
|
+
} else if (arg?.startsWith("--model=")) {
|
|
132
|
+
requested = arg.slice("--model=".length);
|
|
133
|
+
} else {
|
|
134
|
+
throw new Error(`fetch-model: unknown argument "${arg}" (usage: fetch-model [--model <id>])`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return resolveModel(requested ?? env.SEEKSTONE_SEMANTIC_MODEL);
|
|
138
|
+
}
|
|
77
139
|
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
140
|
const output = [];
|
|
141
|
+
let manifest;
|
|
142
|
+
try {
|
|
143
|
+
manifest = deps.manifest ?? selectManifest(deps.argv ?? [], deps.env);
|
|
144
|
+
} catch (err) {
|
|
145
|
+
output.push(`\u2717 ${err instanceof Error ? err.message : String(err)}`);
|
|
146
|
+
return { output, exitCode: 1 };
|
|
147
|
+
}
|
|
148
|
+
const fetchFn = deps.fetchFn ?? fetch;
|
|
149
|
+
const destDir = deps.env.SEEKSTONE_MODEL_PATH || join12(defaultCacheDir(deps.env, deps.homedir), "models", manifest.id);
|
|
82
150
|
output.push(`fetch-model: ${manifest.id} \u2192 ${destDir}`);
|
|
83
151
|
try {
|
|
84
|
-
await
|
|
152
|
+
await mkdir9(destDir, { recursive: true });
|
|
85
153
|
let fetched = 0;
|
|
86
154
|
let skipped = 0;
|
|
87
155
|
for (const file of manifest.files) {
|
|
88
|
-
const dest =
|
|
89
|
-
if (existsSync(dest) && sha256(await
|
|
156
|
+
const dest = join12(destDir, file.name);
|
|
157
|
+
if (existsSync(dest) && sha256(await readFile19(dest)) === file.sha256) {
|
|
90
158
|
skipped++;
|
|
91
159
|
continue;
|
|
92
160
|
}
|
|
@@ -103,7 +171,7 @@ async function runFetchModel(deps) {
|
|
|
103
171
|
}
|
|
104
172
|
output.push(`\u2713 ${manifest.id}: ${fetched} fetched, ${skipped} already present (verified).`);
|
|
105
173
|
output.push(
|
|
106
|
-
"Enable semantic search with SEEKSTONE_SEMANTIC=1 in your MCP server config, then restart the session."
|
|
174
|
+
manifest.id === DEFAULT_MODEL_ID ? "Enable semantic search with SEEKSTONE_SEMANTIC=1 in your MCP server config, then restart the session." : `Enable it with SEEKSTONE_SEMANTIC=1 and SEEKSTONE_SEMANTIC_MODEL=${manifest.id} in your MCP server config, then restart the session.`
|
|
107
175
|
);
|
|
108
176
|
return { output, exitCode: 0 };
|
|
109
177
|
} catch (err) {
|
|
@@ -132,6 +200,198 @@ import {
|
|
|
132
200
|
ListToolsRequestSchema
|
|
133
201
|
} from "@modelcontextprotocol/sdk/types.js";
|
|
134
202
|
|
|
203
|
+
// src/audit.ts
|
|
204
|
+
import {
|
|
205
|
+
appendFileSync as appendFileSync2,
|
|
206
|
+
closeSync,
|
|
207
|
+
fsyncSync,
|
|
208
|
+
openSync,
|
|
209
|
+
renameSync as renameSync2,
|
|
210
|
+
statSync as statSync2,
|
|
211
|
+
writeSync
|
|
212
|
+
} from "fs";
|
|
213
|
+
|
|
214
|
+
// src/log.ts
|
|
215
|
+
import { appendFileSync, renameSync, statSync } from "fs";
|
|
216
|
+
var ORDER = ["error", "warn", "info", "debug"];
|
|
217
|
+
var DEFAULT_MAX_SIZE = 5 * 1024 * 1024;
|
|
218
|
+
function parseLevel(raw, onBad) {
|
|
219
|
+
if (raw === void 0 || raw === "") return "info";
|
|
220
|
+
const v = raw.trim().toLowerCase();
|
|
221
|
+
for (const lvl of ORDER) {
|
|
222
|
+
if (lvl === v) return lvl;
|
|
223
|
+
}
|
|
224
|
+
onBad(`invalid SEEKSTONE_LOG_LEVEL=${JSON.stringify(raw)}; using "info"`);
|
|
225
|
+
return "info";
|
|
226
|
+
}
|
|
227
|
+
function parseSize(raw, fallback = DEFAULT_MAX_SIZE) {
|
|
228
|
+
if (raw === void 0) return fallback;
|
|
229
|
+
const m = /^(\d+(?:\.\d+)?)(b|kb|mb|gb)?$/i.exec(raw.trim());
|
|
230
|
+
if (m === null) return fallback;
|
|
231
|
+
const num = Number(m[1]);
|
|
232
|
+
const unit = (m[2] ?? "b").toLowerCase();
|
|
233
|
+
const mult = unit === "gb" ? 1e9 : unit === "mb" ? 1e6 : unit === "kb" ? 1e3 : 1;
|
|
234
|
+
return Math.max(1, Math.floor(num * mult));
|
|
235
|
+
}
|
|
236
|
+
function fmtFields(fields) {
|
|
237
|
+
const parts = [];
|
|
238
|
+
for (const [k, v] of Object.entries(fields)) {
|
|
239
|
+
if (v === void 0) continue;
|
|
240
|
+
parts.push(`${k}=${typeof v === "string" ? v : JSON.stringify(v)}`);
|
|
241
|
+
}
|
|
242
|
+
return parts.length > 0 ? ` ${parts.join(" ")}` : "";
|
|
243
|
+
}
|
|
244
|
+
function createLogger(opts = {}) {
|
|
245
|
+
const env = opts.env ?? process.env;
|
|
246
|
+
const stderr = opts.stderr ?? ((line) => void process.stderr.write(line));
|
|
247
|
+
const now = opts.now ?? (() => /* @__PURE__ */ new Date());
|
|
248
|
+
const level = parseLevel(env.SEEKSTONE_LOG_LEVEL, (m) => {
|
|
249
|
+
stderr(`WARN ${now().toISOString()} ${m}
|
|
250
|
+
`);
|
|
251
|
+
});
|
|
252
|
+
const threshold = ORDER.indexOf(level);
|
|
253
|
+
const filePath = env.SEEKSTONE_LOG_FILE;
|
|
254
|
+
const maxSize = parseSize(env.SEEKSTONE_LOG_MAX_SIZE);
|
|
255
|
+
let fileOk = false;
|
|
256
|
+
let bytes = 0;
|
|
257
|
+
if (filePath !== void 0 && filePath !== "") {
|
|
258
|
+
try {
|
|
259
|
+
try {
|
|
260
|
+
bytes = statSync(filePath).size;
|
|
261
|
+
} catch {
|
|
262
|
+
bytes = 0;
|
|
263
|
+
}
|
|
264
|
+
appendFileSync(filePath, "");
|
|
265
|
+
fileOk = true;
|
|
266
|
+
} catch (err) {
|
|
267
|
+
const m = err instanceof Error ? err.message : String(err);
|
|
268
|
+
stderr(
|
|
269
|
+
`WARN ${now().toISOString()} cannot write log file ${filePath}: ${m}; file logging disabled
|
|
270
|
+
`
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
function writeFile5(record) {
|
|
275
|
+
if (!fileOk || filePath === void 0) return;
|
|
276
|
+
const line = `${JSON.stringify(record)}
|
|
277
|
+
`;
|
|
278
|
+
const len = Buffer.byteLength(line);
|
|
279
|
+
try {
|
|
280
|
+
if (bytes > 0 && bytes + len > maxSize) {
|
|
281
|
+
renameSync(filePath, `${filePath}.1`);
|
|
282
|
+
bytes = 0;
|
|
283
|
+
}
|
|
284
|
+
appendFileSync(filePath, line);
|
|
285
|
+
bytes += len;
|
|
286
|
+
} catch {
|
|
287
|
+
fileOk = false;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
function emit(lvl, msg, fields) {
|
|
291
|
+
if (ORDER.indexOf(lvl) > threshold) return;
|
|
292
|
+
const ts = now().toISOString();
|
|
293
|
+
const f = fields ?? {};
|
|
294
|
+
stderr(`${lvl.toUpperCase().padEnd(5)} ${ts} ${msg}${fmtFields(f)}
|
|
295
|
+
`);
|
|
296
|
+
writeFile5({ t: ts, level: lvl, msg, ...f });
|
|
297
|
+
}
|
|
298
|
+
return {
|
|
299
|
+
level,
|
|
300
|
+
error: (m, f) => {
|
|
301
|
+
emit("error", m, f);
|
|
302
|
+
},
|
|
303
|
+
warn: (m, f) => {
|
|
304
|
+
emit("warn", m, f);
|
|
305
|
+
},
|
|
306
|
+
info: (m, f) => {
|
|
307
|
+
emit("info", m, f);
|
|
308
|
+
},
|
|
309
|
+
debug: (m, f) => {
|
|
310
|
+
emit("debug", m, f);
|
|
311
|
+
}
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// src/audit.ts
|
|
316
|
+
var DEFAULT_MAX_SIZE2 = 10 * 1e6;
|
|
317
|
+
function resolveAuditConfig(env) {
|
|
318
|
+
const path2 = (env.SEEKSTONE_AUDIT_FILE ?? "").trim();
|
|
319
|
+
if (path2 === "") return void 0;
|
|
320
|
+
return { path: path2, maxBytes: parseSize(env.SEEKSTONE_AUDIT_MAX_SIZE, DEFAULT_MAX_SIZE2) };
|
|
321
|
+
}
|
|
322
|
+
var AuditLog = class _AuditLog {
|
|
323
|
+
path;
|
|
324
|
+
maxBytes;
|
|
325
|
+
bytes;
|
|
326
|
+
constructor(path2, maxBytes, bytes) {
|
|
327
|
+
this.path = path2;
|
|
328
|
+
this.maxBytes = maxBytes;
|
|
329
|
+
this.bytes = bytes;
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* Open (create if missing) the audit file, verifying it is writable now so
|
|
333
|
+
* an unwritable path fails at boot rather than on the first write.
|
|
334
|
+
*/
|
|
335
|
+
static open(config) {
|
|
336
|
+
let bytes = 0;
|
|
337
|
+
try {
|
|
338
|
+
bytes = statSync2(config.path).size;
|
|
339
|
+
} catch {
|
|
340
|
+
bytes = 0;
|
|
341
|
+
}
|
|
342
|
+
appendFileSync2(config.path, "");
|
|
343
|
+
return new _AuditLog(config.path, config.maxBytes, bytes);
|
|
344
|
+
}
|
|
345
|
+
/** Append one record durably. Throws if the append or fsync fails. */
|
|
346
|
+
record(rec) {
|
|
347
|
+
const line = `${JSON.stringify(rec)}
|
|
348
|
+
`;
|
|
349
|
+
const len = Buffer.byteLength(line);
|
|
350
|
+
if (this.bytes > 0 && this.bytes + len > this.maxBytes) {
|
|
351
|
+
renameSync2(this.path, `${this.path}.1`);
|
|
352
|
+
this.bytes = 0;
|
|
353
|
+
}
|
|
354
|
+
const fd = openSync(this.path, "a");
|
|
355
|
+
try {
|
|
356
|
+
writeSync(fd, line);
|
|
357
|
+
fsyncSync(fd);
|
|
358
|
+
} finally {
|
|
359
|
+
closeSync(fd);
|
|
360
|
+
}
|
|
361
|
+
this.bytes += len;
|
|
362
|
+
}
|
|
363
|
+
};
|
|
364
|
+
function classifyError(message) {
|
|
365
|
+
if (message.startsWith("{")) {
|
|
366
|
+
try {
|
|
367
|
+
const parsed = JSON.parse(message);
|
|
368
|
+
if (typeof parsed.error === "string") {
|
|
369
|
+
if (parsed.error === "hash_conflict")
|
|
370
|
+
return { outcome: "hash_conflict", error: parsed.error };
|
|
371
|
+
if (parsed.error === "undo_conflict")
|
|
372
|
+
return { outcome: "undo_conflict", error: parsed.error };
|
|
373
|
+
return { outcome: "error", error: parsed.error };
|
|
374
|
+
}
|
|
375
|
+
} catch {
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
if (message.startsWith("Server is read-only")) {
|
|
379
|
+
return { outcome: "policy_denied", policy: "read_only", error: "read_only" };
|
|
380
|
+
}
|
|
381
|
+
if (message.startsWith("Write not permitted")) {
|
|
382
|
+
return { outcome: "policy_denied", policy: "write_paths", error: "write_paths" };
|
|
383
|
+
}
|
|
384
|
+
if (/ENOENT|not found/i.test(message)) return { outcome: "error", error: "not_found" };
|
|
385
|
+
if (/already exists/i.test(message)) return { outcome: "error", error: "exists" };
|
|
386
|
+
if (/^Invalid regex/.test(message)) return { outcome: "error", error: "invalid_regex" };
|
|
387
|
+
if (/^Write-safety violation/.test(message))
|
|
388
|
+
return { outcome: "error", error: "write_safety_violation" };
|
|
389
|
+
if (/^\[\s*\{/.test(message) || /invalid_type|Required|Expected|expected/.test(message)) {
|
|
390
|
+
return { outcome: "error", error: "invalid_input" };
|
|
391
|
+
}
|
|
392
|
+
return { outcome: "error", error: "error" };
|
|
393
|
+
}
|
|
394
|
+
|
|
135
395
|
// src/cli-args.ts
|
|
136
396
|
function parseCliIntent(argv) {
|
|
137
397
|
if (argv[0] === "init") {
|
|
@@ -160,6 +420,7 @@ Usage:
|
|
|
160
420
|
seekstone Start the MCP server (reads from stdin/stdout)
|
|
161
421
|
seekstone init Validate a vault and print/patch the client MCP config
|
|
162
422
|
seekstone fetch-model Download the local embedding model for semantic search
|
|
423
|
+
(--model potion-base-8M | potion-retrieval-32M)
|
|
163
424
|
seekstone --version Print the version and exit
|
|
164
425
|
seekstone --help Print this help and exit
|
|
165
426
|
|
|
@@ -175,6 +436,7 @@ Optional environment:
|
|
|
175
436
|
SEEKSTONE_WATCH_POLL Set to 1 to poll for changes (network drives, WSL)
|
|
176
437
|
SEEKSTONE_WATCH_POLL_INTERVAL Poll interval in ms (default 10000)
|
|
177
438
|
SEEKSTONE_SEMANTIC Set to 1 to enable semantic search (needs fetch-model first)
|
|
439
|
+
SEEKSTONE_SEMANTIC_MODEL potion-base-8M (default) | potion-retrieval-32M (bigger, more accurate)
|
|
178
440
|
SEEKSTONE_MODEL_PATH Override the embedding-model directory
|
|
179
441
|
SEEKSTONE_CACHE_DIR Override the cache root (default ~/.cache/seekstone)
|
|
180
442
|
|
|
@@ -204,7 +466,7 @@ Docs: https://github.com/shaqmughal/seekstone`;
|
|
|
204
466
|
}
|
|
205
467
|
|
|
206
468
|
// src/tools/append_note.ts
|
|
207
|
-
import { readFile } from "fs/promises";
|
|
469
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
208
470
|
|
|
209
471
|
// ../core/src/frontmatter.ts
|
|
210
472
|
import { parse as parseYaml } from "yaml";
|
|
@@ -300,6 +562,271 @@ function assertHashMatch(raw, prevHash, path2) {
|
|
|
300
562
|
}
|
|
301
563
|
}
|
|
302
564
|
|
|
565
|
+
// src/journal.ts
|
|
566
|
+
import { timingSafeEqual } from "crypto";
|
|
567
|
+
import { mkdir, open, readdir, readFile, rename as rename2, rm, stat } from "fs/promises";
|
|
568
|
+
import { join } from "path";
|
|
569
|
+
var DEFAULT_MAX_BYTES = 50 * 1e6;
|
|
570
|
+
var DEFAULT_MAX_ENTRIES = 1e3;
|
|
571
|
+
var HISTORY_DIR = ".seekstone/history";
|
|
572
|
+
function resolveJournalConfig(env) {
|
|
573
|
+
const flag = (env.SEEKSTONE_HISTORY ?? "").trim().toLowerCase();
|
|
574
|
+
if (flag === "0" || flag === "false" || flag === "off") return void 0;
|
|
575
|
+
const maxEntriesRaw = Number.parseInt(env.SEEKSTONE_HISTORY_MAX_ENTRIES ?? "", 10);
|
|
576
|
+
return {
|
|
577
|
+
maxBytes: parseSize(env.SEEKSTONE_HISTORY_MAX_SIZE, DEFAULT_MAX_BYTES),
|
|
578
|
+
maxEntries: Number.isFinite(maxEntriesRaw) && maxEntriesRaw > 0 ? maxEntriesRaw : DEFAULT_MAX_ENTRIES
|
|
579
|
+
};
|
|
580
|
+
}
|
|
581
|
+
async function writeDurable(dest, content) {
|
|
582
|
+
const tmp = `${dest}.seekstone-tmp`;
|
|
583
|
+
const fh = await open(tmp, "w");
|
|
584
|
+
try {
|
|
585
|
+
await fh.writeFile(content, "utf8");
|
|
586
|
+
await fh.sync();
|
|
587
|
+
} finally {
|
|
588
|
+
await fh.close();
|
|
589
|
+
}
|
|
590
|
+
await rename2(tmp, dest);
|
|
591
|
+
}
|
|
592
|
+
async function appendDurable(dest, line) {
|
|
593
|
+
const fh = await open(dest, "a");
|
|
594
|
+
try {
|
|
595
|
+
await fh.writeFile(line, "utf8");
|
|
596
|
+
await fh.sync();
|
|
597
|
+
} finally {
|
|
598
|
+
await fh.close();
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
var Journal = class _Journal {
|
|
602
|
+
config;
|
|
603
|
+
dir;
|
|
604
|
+
blobDir;
|
|
605
|
+
manifestPath;
|
|
606
|
+
log;
|
|
607
|
+
entries;
|
|
608
|
+
nextSeq;
|
|
609
|
+
/** Blob hash → byte size, for every blob currently on disk. */
|
|
610
|
+
blobSizes;
|
|
611
|
+
totalBytes;
|
|
612
|
+
/** Serialises commits so seq assignment and eviction never interleave. */
|
|
613
|
+
chain = Promise.resolve();
|
|
614
|
+
constructor(vaultRoot2, config, entries, blobSizes, log2) {
|
|
615
|
+
this.config = config;
|
|
616
|
+
this.dir = join(vaultRoot2, HISTORY_DIR);
|
|
617
|
+
this.blobDir = join(this.dir, "blobs");
|
|
618
|
+
this.manifestPath = join(this.dir, "manifest.jsonl");
|
|
619
|
+
this.entries = entries;
|
|
620
|
+
this.nextSeq = (entries.at(-1)?.seq ?? 0) + 1;
|
|
621
|
+
this.blobSizes = blobSizes;
|
|
622
|
+
this.totalBytes = 0;
|
|
623
|
+
for (const n of blobSizes.values()) this.totalBytes += n;
|
|
624
|
+
this.log = log2;
|
|
625
|
+
}
|
|
626
|
+
/** Open (or create) the journal under `vaultRoot`, loading the manifest into memory. */
|
|
627
|
+
static async open(vaultRoot2, config, opts = {}) {
|
|
628
|
+
const dir = join(vaultRoot2, HISTORY_DIR);
|
|
629
|
+
const blobDir = join(dir, "blobs");
|
|
630
|
+
await mkdir(blobDir, { recursive: true });
|
|
631
|
+
const entries = [];
|
|
632
|
+
let manifest = "";
|
|
633
|
+
try {
|
|
634
|
+
manifest = await readFile(join(dir, "manifest.jsonl"), "utf8");
|
|
635
|
+
} catch (err) {
|
|
636
|
+
if (err.code !== "ENOENT") throw err;
|
|
637
|
+
}
|
|
638
|
+
let lastSeq = 0;
|
|
639
|
+
for (const line of manifest.split("\n")) {
|
|
640
|
+
if (line.trim() === "") continue;
|
|
641
|
+
let parsed;
|
|
642
|
+
try {
|
|
643
|
+
parsed = JSON.parse(line);
|
|
644
|
+
} catch {
|
|
645
|
+
opts.log?.warn("journal: skipping malformed manifest line");
|
|
646
|
+
continue;
|
|
647
|
+
}
|
|
648
|
+
if (!isEntry(parsed) || parsed.seq <= lastSeq) {
|
|
649
|
+
opts.log?.warn("journal: skipping invalid manifest entry");
|
|
650
|
+
continue;
|
|
651
|
+
}
|
|
652
|
+
entries.push(parsed);
|
|
653
|
+
lastSeq = parsed.seq;
|
|
654
|
+
}
|
|
655
|
+
const blobSizes = /* @__PURE__ */ new Map();
|
|
656
|
+
for (const name of await readdir(blobDir)) {
|
|
657
|
+
if (!/^[0-9a-f]{64}$/.test(name)) continue;
|
|
658
|
+
try {
|
|
659
|
+
blobSizes.set(name, (await stat(join(blobDir, name))).size);
|
|
660
|
+
} catch {
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
return new _Journal(vaultRoot2, config, entries, blobSizes, opts.log);
|
|
664
|
+
}
|
|
665
|
+
begin(tool, undoOf) {
|
|
666
|
+
const files = [];
|
|
667
|
+
let committed = false;
|
|
668
|
+
return {
|
|
669
|
+
add: async (path2, pre, post) => {
|
|
670
|
+
if (committed) throw new Error("journal: add after commit");
|
|
671
|
+
const preHash = pre === null ? null : await this.storeBlob(pre);
|
|
672
|
+
files.push({ path: path2, preHash, postHash: post === null ? null : contentHash(post) });
|
|
673
|
+
},
|
|
674
|
+
commit: async () => {
|
|
675
|
+
if (committed) throw new Error("journal: double commit");
|
|
676
|
+
committed = true;
|
|
677
|
+
return this.enqueue(() => this.commit(tool, files, undoOf));
|
|
678
|
+
}
|
|
679
|
+
};
|
|
680
|
+
}
|
|
681
|
+
/** Newest-first rows for `list_writes`. */
|
|
682
|
+
list(opts = {}) {
|
|
683
|
+
const limit = opts.limit ?? 20;
|
|
684
|
+
const filtered = opts.path === void 0 ? this.entries : this.entries.filter((e) => e.files.some((f) => f.path === opts.path));
|
|
685
|
+
const writes = [];
|
|
686
|
+
for (let i = filtered.length - 1; i >= 0 && writes.length < limit; i--) {
|
|
687
|
+
const e = filtered[i];
|
|
688
|
+
const row = {
|
|
689
|
+
seq: e.seq,
|
|
690
|
+
ts: e.ts,
|
|
691
|
+
tool: e.tool,
|
|
692
|
+
paths: e.files.map((f) => f.path),
|
|
693
|
+
undoable: this.isUndoable(e)
|
|
694
|
+
};
|
|
695
|
+
if (e.undoOf !== void 0) row.undoOf = e.undoOf;
|
|
696
|
+
writes.push(row);
|
|
697
|
+
}
|
|
698
|
+
return { writes, total: filtered.length };
|
|
699
|
+
}
|
|
700
|
+
/** Seq of the newest entry (0 when empty). */
|
|
701
|
+
get lastSeq() {
|
|
702
|
+
return this.nextSeq - 1;
|
|
703
|
+
}
|
|
704
|
+
/** Entries committed after `seq`, oldest first. */
|
|
705
|
+
since(seq) {
|
|
706
|
+
return this.entries.filter((e) => e.seq > seq);
|
|
707
|
+
}
|
|
708
|
+
get(seq) {
|
|
709
|
+
return this.entries.find((e) => e.seq === seq);
|
|
710
|
+
}
|
|
711
|
+
/**
|
|
712
|
+
* The most recent write that is still in effect and restorable — the
|
|
713
|
+
* default target of `undo_write`. Undo entries and writes they reverted are
|
|
714
|
+
* skipped (so repeated default undos walk backwards instead of ping-ponging
|
|
715
|
+
* undo/redo); an undo that was itself undone re-exposes its target. Redo is
|
|
716
|
+
* always explicit: `undo_write({ seq: <the undo's seq> })`.
|
|
717
|
+
*/
|
|
718
|
+
latestUndoable() {
|
|
719
|
+
const cancelled = /* @__PURE__ */ new Set();
|
|
720
|
+
for (let i = this.entries.length - 1; i >= 0; i--) {
|
|
721
|
+
const e = this.entries[i];
|
|
722
|
+
if (cancelled.has(e.seq)) continue;
|
|
723
|
+
if (e.undoOf !== void 0) {
|
|
724
|
+
cancelled.add(e.undoOf);
|
|
725
|
+
continue;
|
|
726
|
+
}
|
|
727
|
+
if (this.isUndoable(e)) return e;
|
|
728
|
+
}
|
|
729
|
+
return void 0;
|
|
730
|
+
}
|
|
731
|
+
isUndoable(entry) {
|
|
732
|
+
return entry.files.every((f) => f.preHash === null || this.blobSizes.has(f.preHash));
|
|
733
|
+
}
|
|
734
|
+
/** Read a stored pre-image by hash. Throws if evicted. */
|
|
735
|
+
async preimage(hash) {
|
|
736
|
+
const raw = await readFile(join(this.blobDir, hash), "utf8");
|
|
737
|
+
const actual = Buffer.from(contentHash(raw), "hex");
|
|
738
|
+
const expected = Buffer.from(hash, "hex");
|
|
739
|
+
if (actual.length !== expected.length || !timingSafeEqual(actual, expected)) {
|
|
740
|
+
throw new Error(`journal: blob ${hash} is corrupt (hash mismatch)`);
|
|
741
|
+
}
|
|
742
|
+
return raw;
|
|
743
|
+
}
|
|
744
|
+
/** Bytes of pre-images currently stored (for diagnostics/tests). */
|
|
745
|
+
get storedBytes() {
|
|
746
|
+
return this.totalBytes;
|
|
747
|
+
}
|
|
748
|
+
enqueue(fn) {
|
|
749
|
+
const next = this.chain.then(fn, fn);
|
|
750
|
+
this.chain = next.catch(() => void 0);
|
|
751
|
+
return next;
|
|
752
|
+
}
|
|
753
|
+
async storeBlob(content) {
|
|
754
|
+
const hash = contentHash(content);
|
|
755
|
+
if (this.blobSizes.has(hash)) return hash;
|
|
756
|
+
await writeDurable(join(this.blobDir, hash), content);
|
|
757
|
+
const size = Buffer.byteLength(content, "utf8");
|
|
758
|
+
this.blobSizes.set(hash, size);
|
|
759
|
+
this.totalBytes += size;
|
|
760
|
+
return hash;
|
|
761
|
+
}
|
|
762
|
+
async commit(tool, files, undoOf) {
|
|
763
|
+
const entry = { seq: this.nextSeq, ts: (/* @__PURE__ */ new Date()).toISOString(), tool, files };
|
|
764
|
+
if (undoOf !== void 0) entry.undoOf = undoOf;
|
|
765
|
+
await appendDurable(this.manifestPath, `${JSON.stringify(entry)}
|
|
766
|
+
`);
|
|
767
|
+
this.nextSeq++;
|
|
768
|
+
this.entries.push(entry);
|
|
769
|
+
await this.evict();
|
|
770
|
+
return entry;
|
|
771
|
+
}
|
|
772
|
+
/**
|
|
773
|
+
* Enforce the caps. Blobs are reference-counted across entries, so a
|
|
774
|
+
* pre-image shared by an old and a new entry survives the old one's
|
|
775
|
+
* eviction. Over the entry cap, the oldest rows are dropped and the
|
|
776
|
+
* manifest rewritten atomically; over the byte cap, the oldest entries'
|
|
777
|
+
* blobs are removed but the rows stay (listed as undoable: false).
|
|
778
|
+
*/
|
|
779
|
+
async evict() {
|
|
780
|
+
const refs = /* @__PURE__ */ new Map();
|
|
781
|
+
for (const e of this.entries) {
|
|
782
|
+
for (const f of e.files) {
|
|
783
|
+
if (f.preHash !== null) refs.set(f.preHash, (refs.get(f.preHash) ?? 0) + 1);
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
const dropBlobs = async (e) => {
|
|
787
|
+
for (const f of e.files) {
|
|
788
|
+
if (f.preHash === null) continue;
|
|
789
|
+
const n = (refs.get(f.preHash) ?? 1) - 1;
|
|
790
|
+
refs.set(f.preHash, n);
|
|
791
|
+
if (n > 0 || !this.blobSizes.has(f.preHash)) continue;
|
|
792
|
+
await rm(join(this.blobDir, f.preHash), { force: true });
|
|
793
|
+
this.totalBytes -= this.blobSizes.get(f.preHash) ?? 0;
|
|
794
|
+
this.blobSizes.delete(f.preHash);
|
|
795
|
+
}
|
|
796
|
+
};
|
|
797
|
+
let rewrite = false;
|
|
798
|
+
while (this.entries.length > this.config.maxEntries) {
|
|
799
|
+
const oldest = this.entries.shift();
|
|
800
|
+
await dropBlobs(oldest);
|
|
801
|
+
rewrite = true;
|
|
802
|
+
}
|
|
803
|
+
for (let i = 0; this.totalBytes > this.config.maxBytes && i < this.entries.length - 1; i++) {
|
|
804
|
+
await dropBlobs(this.entries[i]);
|
|
805
|
+
}
|
|
806
|
+
if (rewrite) {
|
|
807
|
+
await writeDurable(
|
|
808
|
+
this.manifestPath,
|
|
809
|
+
this.entries.map((e) => `${JSON.stringify(e)}
|
|
810
|
+
`).join("")
|
|
811
|
+
);
|
|
812
|
+
this.log?.debug("journal: manifest compacted", { entries: this.entries.length });
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
};
|
|
816
|
+
function isEntry(v) {
|
|
817
|
+
if (typeof v !== "object" || v === null) return false;
|
|
818
|
+
const e = v;
|
|
819
|
+
return typeof e.seq === "number" && typeof e.ts === "string" && typeof e.tool === "string" && Array.isArray(e.files) && e.files.every(
|
|
820
|
+
(f) => typeof f === "object" && f !== null && typeof f.path === "string" && (f.preHash === null || typeof f.preHash === "string") && (f.postHash === null || typeof f.postHash === "string")
|
|
821
|
+
);
|
|
822
|
+
}
|
|
823
|
+
async function journalWrite(ctx2, tool, path2, pre, post) {
|
|
824
|
+
if (!ctx2.journal) return;
|
|
825
|
+
const txn = ctx2.journal.begin(tool);
|
|
826
|
+
await txn.add(path2, pre, post);
|
|
827
|
+
await txn.commit();
|
|
828
|
+
}
|
|
829
|
+
|
|
303
830
|
// src/policy.ts
|
|
304
831
|
import picomatch from "picomatch";
|
|
305
832
|
function parseWritePolicy(env) {
|
|
@@ -352,7 +879,7 @@ var AppendNoteInput = z.object({
|
|
|
352
879
|
async function appendNote(ctx2, input) {
|
|
353
880
|
const absPath = resolveVaultPath(ctx2.vaultRoot, input.path);
|
|
354
881
|
assertWritable(ctx2.policy, input.path);
|
|
355
|
-
const original = await
|
|
882
|
+
const original = await readFile2(absPath, "utf8");
|
|
356
883
|
if (input.prevHash !== void 0) assertHashMatch(original, input.prevHash, input.path);
|
|
357
884
|
const fm = parseFrontmatter(original);
|
|
358
885
|
const body = fm.body.endsWith("\n") ? fm.body : `${fm.body}
|
|
@@ -361,6 +888,7 @@ async function appendNote(ctx2, input) {
|
|
|
361
888
|
const newBody = `${body}${separator}${input.content}`;
|
|
362
889
|
const header = original.slice(0, fm.bodyStart);
|
|
363
890
|
const newContent = `${header}${newBody}`;
|
|
891
|
+
await journalWrite(ctx2, "append_note", input.path, original, newContent);
|
|
364
892
|
await atomicWrite(absPath, newContent);
|
|
365
893
|
const cached = ctx2.notes.get(input.path);
|
|
366
894
|
if (cached) {
|
|
@@ -611,7 +1139,10 @@ function lexicalSearch(ctx2, input) {
|
|
|
611
1139
|
}
|
|
612
1140
|
function semanticSearch(ctx2, semantic, input) {
|
|
613
1141
|
const queryVec = semantic.embedQuery(input.query);
|
|
614
|
-
const candidates = semantic.
|
|
1142
|
+
const candidates = semantic.rerank(
|
|
1143
|
+
input.query,
|
|
1144
|
+
semantic.store.topNotes(queryVec, SEMANTIC_CANDIDATES)
|
|
1145
|
+
);
|
|
615
1146
|
const terms = searchTerms(input.query);
|
|
616
1147
|
const hits = [];
|
|
617
1148
|
for (const c of candidates) {
|
|
@@ -839,7 +1370,7 @@ function contextPack(ctx2, input) {
|
|
|
839
1370
|
}
|
|
840
1371
|
|
|
841
1372
|
// src/tools/create_note.ts
|
|
842
|
-
import { access, mkdir, readFile as
|
|
1373
|
+
import { access, mkdir as mkdir2, readFile as readFile3 } from "fs/promises";
|
|
843
1374
|
import { dirname } from "path";
|
|
844
1375
|
import { stringify as stringifyYaml } from "yaml";
|
|
845
1376
|
import { z as z4 } from "zod";
|
|
@@ -888,6 +1419,7 @@ async function createNote(ctx2, rawInput) {
|
|
|
888
1419
|
const input = CreateNoteInput.parse(rawInput);
|
|
889
1420
|
const absPath = resolveVaultPath(ctx2.vaultRoot, input.path);
|
|
890
1421
|
assertWritable(ctx2.policy, input.path);
|
|
1422
|
+
let existing = null;
|
|
891
1423
|
if (!input.overwrite) {
|
|
892
1424
|
try {
|
|
893
1425
|
await access(absPath);
|
|
@@ -895,22 +1427,26 @@ async function createNote(ctx2, rawInput) {
|
|
|
895
1427
|
} catch (err) {
|
|
896
1428
|
if (err.code !== "ENOENT") throw err;
|
|
897
1429
|
}
|
|
898
|
-
} else
|
|
899
|
-
let existing;
|
|
1430
|
+
} else {
|
|
900
1431
|
try {
|
|
901
|
-
existing = await
|
|
902
|
-
} catch {
|
|
903
|
-
throw
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
1432
|
+
existing = await readFile3(absPath, "utf8");
|
|
1433
|
+
} catch (err) {
|
|
1434
|
+
if (err.code !== "ENOENT") throw err;
|
|
1435
|
+
if (input.prevHash !== void 0) {
|
|
1436
|
+
throw new Error(
|
|
1437
|
+
JSON.stringify({
|
|
1438
|
+
error: "hash_conflict",
|
|
1439
|
+
path: input.path,
|
|
1440
|
+
expected: input.prevHash,
|
|
1441
|
+
actual: null,
|
|
1442
|
+
hint: "The note no longer exists on disk. Re-check before overwriting."
|
|
1443
|
+
})
|
|
1444
|
+
);
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
if (existing !== null && input.prevHash !== void 0) {
|
|
1448
|
+
assertHashMatch(existing, input.prevHash, input.path);
|
|
912
1449
|
}
|
|
913
|
-
assertHashMatch(existing, input.prevHash, input.path);
|
|
914
1450
|
}
|
|
915
1451
|
let raw = "";
|
|
916
1452
|
if (input.frontmatter && Object.keys(input.frontmatter).length > 0) {
|
|
@@ -919,7 +1455,8 @@ ${stringifyYaml(input.frontmatter, { lineWidth: 0 })}---
|
|
|
919
1455
|
`;
|
|
920
1456
|
}
|
|
921
1457
|
raw += input.content ?? "";
|
|
922
|
-
await
|
|
1458
|
+
await mkdir2(dirname(absPath), { recursive: true });
|
|
1459
|
+
await journalWrite(ctx2, "create_note", input.path, existing, raw);
|
|
923
1460
|
await atomicWrite(absPath, raw);
|
|
924
1461
|
upsertDoc(ctx2, buildDoc(input.path, raw));
|
|
925
1462
|
return {
|
|
@@ -930,8 +1467,8 @@ ${stringifyYaml(input.frontmatter, { lineWidth: 0 })}---
|
|
|
930
1467
|
}
|
|
931
1468
|
|
|
932
1469
|
// src/tools/delete_note.ts
|
|
933
|
-
import { access as access2, mkdir as
|
|
934
|
-
import { basename as basename2, extname as extname2, join } from "path";
|
|
1470
|
+
import { access as access2, mkdir as mkdir3, readFile as readFile4, rename as rename3, rm as rm2 } from "fs/promises";
|
|
1471
|
+
import { basename as basename2, extname as extname2, join as join2 } from "path";
|
|
935
1472
|
import { z as z5 } from "zod";
|
|
936
1473
|
|
|
937
1474
|
// src/index/backlinks.ts
|
|
@@ -982,7 +1519,7 @@ async function freeTrashPath(vaultRoot2, noteBase) {
|
|
|
982
1519
|
let candidate = `.trash/${noteBase}`;
|
|
983
1520
|
for (; ; ) {
|
|
984
1521
|
try {
|
|
985
|
-
await access2(
|
|
1522
|
+
await access2(join2(vaultRoot2, candidate));
|
|
986
1523
|
} catch {
|
|
987
1524
|
return candidate;
|
|
988
1525
|
}
|
|
@@ -993,15 +1530,16 @@ async function deleteNote(ctx2, rawInput) {
|
|
|
993
1530
|
const input = DeleteNoteInput.parse(rawInput);
|
|
994
1531
|
const absPath = resolveVaultPath(ctx2.vaultRoot, input.path);
|
|
995
1532
|
assertWritable(ctx2.policy, input.path);
|
|
996
|
-
const raw = await
|
|
1533
|
+
const raw = await readFile4(absPath, "utf8");
|
|
997
1534
|
if (input.prevHash !== void 0) assertHashMatch(raw, input.prevHash, input.path);
|
|
1535
|
+
await journalWrite(ctx2, "delete_note", input.path, raw, null);
|
|
998
1536
|
let trashedTo;
|
|
999
1537
|
if (input.permanent) {
|
|
1000
|
-
await
|
|
1538
|
+
await rm2(absPath);
|
|
1001
1539
|
} else {
|
|
1002
|
-
await
|
|
1540
|
+
await mkdir3(join2(ctx2.vaultRoot, ".trash"), { recursive: true });
|
|
1003
1541
|
trashedTo = await freeTrashPath(ctx2.vaultRoot, basename2(input.path));
|
|
1004
|
-
await
|
|
1542
|
+
await rename3(absPath, join2(ctx2.vaultRoot, trashedTo));
|
|
1005
1543
|
}
|
|
1006
1544
|
removeNoteBacklinks(ctx2, input.path);
|
|
1007
1545
|
if (ctx2.notes.has(input.path)) {
|
|
@@ -1139,10 +1677,24 @@ function listTags(ctx2, input) {
|
|
|
1139
1677
|
return { tags, total: tags.length };
|
|
1140
1678
|
}
|
|
1141
1679
|
|
|
1142
|
-
// src/tools/
|
|
1143
|
-
import { access as access3, mkdir as mkdir3, readFile as readFile4, rename as rename3 } from "fs/promises";
|
|
1144
|
-
import { dirname as dirname2, join as join2 } from "path";
|
|
1680
|
+
// src/tools/list_writes.ts
|
|
1145
1681
|
import { z as z10 } from "zod";
|
|
1682
|
+
var ListWritesInput = z10.object({
|
|
1683
|
+
limit: z10.number().int().min(1).max(200).default(20).describe("Max entries to return, newest first (1\u2013200, default 20)."),
|
|
1684
|
+
path: z10.string().optional().describe("Only entries that touched this vault-relative path.")
|
|
1685
|
+
});
|
|
1686
|
+
function listWrites(ctx2, rawInput) {
|
|
1687
|
+
const input = ListWritesInput.parse(rawInput);
|
|
1688
|
+
if (!ctx2.journal) return { writes: [], total: 0, disabled: true };
|
|
1689
|
+
const opts = { limit: input.limit };
|
|
1690
|
+
if (input.path !== void 0) opts.path = input.path;
|
|
1691
|
+
return ctx2.journal.list(opts);
|
|
1692
|
+
}
|
|
1693
|
+
|
|
1694
|
+
// src/tools/move_note.ts
|
|
1695
|
+
import { mkdir as mkdir4, readFile as readFile5, rename as rename4 } from "fs/promises";
|
|
1696
|
+
import { dirname as dirname2, join as join3 } from "path";
|
|
1697
|
+
import { z as z11 } from "zod";
|
|
1146
1698
|
|
|
1147
1699
|
// src/tools/rewrite_links.ts
|
|
1148
1700
|
import { posix } from "path";
|
|
@@ -1237,14 +1789,14 @@ function hasMarkdownLinkTo(raw, sourcePath, targetPath) {
|
|
|
1237
1789
|
}
|
|
1238
1790
|
|
|
1239
1791
|
// src/tools/move_note.ts
|
|
1240
|
-
var MoveNoteInput =
|
|
1241
|
-
from:
|
|
1242
|
-
to:
|
|
1243
|
-
overwrite:
|
|
1244
|
-
rewriteLinks:
|
|
1792
|
+
var MoveNoteInput = z11.object({
|
|
1793
|
+
from: z11.string().describe("Vault-relative path of the note to move."),
|
|
1794
|
+
to: z11.string().describe("Vault-relative destination path."),
|
|
1795
|
+
overwrite: z11.boolean().default(false).describe("Overwrite the destination if it exists. Defaults to false."),
|
|
1796
|
+
rewriteLinks: z11.boolean().default(true).describe(
|
|
1245
1797
|
"Rewrite wikilinks and markdown links in other notes that point at the moved note, so nothing breaks. Defaults to true; pass false to move the file only."
|
|
1246
1798
|
),
|
|
1247
|
-
prevHash:
|
|
1799
|
+
prevHash: z11.string().optional().describe(
|
|
1248
1800
|
"Optional compare-and-swap guard on the source note: the contentHash from a prior read. Fails with hash_conflict if the note changed since."
|
|
1249
1801
|
)
|
|
1250
1802
|
});
|
|
@@ -1256,20 +1808,19 @@ async function moveNote(ctx2, rawInput) {
|
|
|
1256
1808
|
assertWritable(ctx2.policy, input.to);
|
|
1257
1809
|
let raw;
|
|
1258
1810
|
try {
|
|
1259
|
-
raw = await
|
|
1811
|
+
raw = await readFile5(absFrom, "utf8");
|
|
1260
1812
|
} catch {
|
|
1261
1813
|
throw new Error(`Note not found: ${input.from}`);
|
|
1262
1814
|
}
|
|
1263
1815
|
if (input.prevHash !== void 0) assertHashMatch(raw, input.prevHash, input.from);
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
}
|
|
1816
|
+
let existingAtTo = null;
|
|
1817
|
+
try {
|
|
1818
|
+
existingAtTo = await readFile5(absTo, "utf8");
|
|
1819
|
+
} catch (err) {
|
|
1820
|
+
if (err.code !== "ENOENT") throw err;
|
|
1821
|
+
}
|
|
1822
|
+
if (existingAtTo !== null && !input.overwrite) {
|
|
1823
|
+
throw new Error(`Destination already exists: ${input.to}. Pass overwrite: true to replace it.`);
|
|
1273
1824
|
}
|
|
1274
1825
|
const preNotes = new Map(ctx2.notes);
|
|
1275
1826
|
const candidates = /* @__PURE__ */ new Set();
|
|
@@ -1281,20 +1832,13 @@ async function moveNote(ctx2, rawInput) {
|
|
|
1281
1832
|
}
|
|
1282
1833
|
candidates.delete(input.from);
|
|
1283
1834
|
}
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
ctx2.index.discard(input.from);
|
|
1289
|
-
ctx2.notes.delete(input.from);
|
|
1290
|
-
}
|
|
1291
|
-
upsertDoc(ctx2, buildDoc(input.to, raw));
|
|
1292
|
-
addNoteBacklinks(ctx2, input.to, raw);
|
|
1293
|
-
let notesRewritten = 0;
|
|
1294
|
-
let linksRewritten = 0;
|
|
1835
|
+
const postNotes = new Map(preNotes);
|
|
1836
|
+
postNotes.delete(input.from);
|
|
1837
|
+
postNotes.set(input.to, buildDoc(input.to, raw));
|
|
1838
|
+
const rewrites = [];
|
|
1295
1839
|
const skipped = [];
|
|
1296
1840
|
for (const path2 of [...candidates].sort()) {
|
|
1297
|
-
const doc =
|
|
1841
|
+
const doc = preNotes.get(path2);
|
|
1298
1842
|
if (doc === void 0) continue;
|
|
1299
1843
|
if (!isWritable(ctx2.policy, path2)) {
|
|
1300
1844
|
skipped.push(path2);
|
|
@@ -1306,11 +1850,34 @@ async function moveNote(ctx2, rawInput) {
|
|
|
1306
1850
|
input.from,
|
|
1307
1851
|
input.to,
|
|
1308
1852
|
preNotes,
|
|
1309
|
-
|
|
1853
|
+
postNotes
|
|
1310
1854
|
);
|
|
1855
|
+
rewrites.push({ path: path2, pre: doc.raw, content, count });
|
|
1856
|
+
}
|
|
1857
|
+
if (ctx2.journal) {
|
|
1858
|
+
const txn = ctx2.journal.begin("move_note");
|
|
1859
|
+
await txn.add(input.from, raw, null);
|
|
1860
|
+
await txn.add(input.to, existingAtTo, raw);
|
|
1861
|
+
for (const r of rewrites) if (r.count > 0) await txn.add(r.path, r.pre, r.content);
|
|
1862
|
+
await txn.commit();
|
|
1863
|
+
}
|
|
1864
|
+
await mkdir4(dirname2(absTo), { recursive: true });
|
|
1865
|
+
await rename4(absFrom, absTo);
|
|
1866
|
+
removeNoteBacklinks(ctx2, input.from);
|
|
1867
|
+
if (ctx2.notes.has(input.from)) {
|
|
1868
|
+
ctx2.index.discard(input.from);
|
|
1869
|
+
ctx2.notes.delete(input.from);
|
|
1870
|
+
}
|
|
1871
|
+
upsertDoc(ctx2, buildDoc(input.to, raw));
|
|
1872
|
+
addNoteBacklinks(ctx2, input.to, raw);
|
|
1873
|
+
let notesRewritten = 0;
|
|
1874
|
+
let linksRewritten = 0;
|
|
1875
|
+
for (const { path: path2, content, count } of rewrites) {
|
|
1876
|
+
const doc = ctx2.notes.get(path2);
|
|
1877
|
+
if (doc === void 0) continue;
|
|
1311
1878
|
removeNoteBacklinks(ctx2, path2);
|
|
1312
1879
|
if (count > 0) {
|
|
1313
|
-
await atomicWrite(
|
|
1880
|
+
await atomicWrite(join3(ctx2.vaultRoot, path2), content);
|
|
1314
1881
|
upsertDoc(ctx2, buildDoc(path2, content));
|
|
1315
1882
|
notesRewritten++;
|
|
1316
1883
|
linksRewritten += count;
|
|
@@ -1332,7 +1899,7 @@ async function moveNote(ctx2, rawInput) {
|
|
|
1332
1899
|
}
|
|
1333
1900
|
|
|
1334
1901
|
// src/tools/outline_note.ts
|
|
1335
|
-
import { readFile as
|
|
1902
|
+
import { readFile as readFile6 } from "fs/promises";
|
|
1336
1903
|
|
|
1337
1904
|
// ../core/src/outline.ts
|
|
1338
1905
|
var HEADING_RE = /^(#{1,6})\s+(.+)/;
|
|
@@ -1382,15 +1949,15 @@ function buildOutline(raw, options = {}) {
|
|
|
1382
1949
|
}
|
|
1383
1950
|
|
|
1384
1951
|
// src/tools/outline_note.ts
|
|
1385
|
-
import { z as
|
|
1386
|
-
var OutlineNoteInput =
|
|
1387
|
-
path:
|
|
1388
|
-
includeBlocks:
|
|
1389
|
-
includeSizes:
|
|
1952
|
+
import { z as z12 } from "zod";
|
|
1953
|
+
var OutlineNoteInput = z12.object({
|
|
1954
|
+
path: z12.string().describe("Vault-relative path to the note."),
|
|
1955
|
+
includeBlocks: z12.boolean().default(true).describe("Include block-reference anchors (^id). Default true."),
|
|
1956
|
+
includeSizes: z12.boolean().default(false).describe("Include per-section character length in each heading entry. Default false.")
|
|
1390
1957
|
});
|
|
1391
1958
|
async function outlineNote(ctx2, input) {
|
|
1392
1959
|
const absPath = resolveVaultPath(ctx2.vaultRoot, input.path);
|
|
1393
|
-
const raw = await
|
|
1960
|
+
const raw = await readFile6(absPath, "utf8");
|
|
1394
1961
|
return buildOutline(raw, {
|
|
1395
1962
|
includeBlocks: input.includeBlocks,
|
|
1396
1963
|
includeSizes: input.includeSizes
|
|
@@ -1398,22 +1965,22 @@ async function outlineNote(ctx2, input) {
|
|
|
1398
1965
|
}
|
|
1399
1966
|
|
|
1400
1967
|
// src/tools/patch_frontmatter.ts
|
|
1401
|
-
import { readFile as
|
|
1968
|
+
import { readFile as readFile7 } from "fs/promises";
|
|
1402
1969
|
import { parseDocument as parseDocument2 } from "yaml";
|
|
1403
|
-
import { z as
|
|
1404
|
-
var PatchFrontmatterInput =
|
|
1405
|
-
path:
|
|
1406
|
-
patch:
|
|
1970
|
+
import { z as z13 } from "zod";
|
|
1971
|
+
var PatchFrontmatterInput = z13.object({
|
|
1972
|
+
path: z13.string().describe("Vault-relative path to the note."),
|
|
1973
|
+
patch: z13.record(z13.string(), z13.unknown()).describe(
|
|
1407
1974
|
"Key-value pairs to set in the frontmatter. Existing keys not in the patch are unchanged. Pass null as a value to remove a key."
|
|
1408
1975
|
),
|
|
1409
|
-
prevHash:
|
|
1976
|
+
prevHash: z13.string().optional().describe(
|
|
1410
1977
|
"Optional compare-and-swap guard: the contentHash from a prior read. Fails with hash_conflict if the note changed since."
|
|
1411
1978
|
)
|
|
1412
1979
|
});
|
|
1413
1980
|
async function patchFrontmatter(ctx2, input) {
|
|
1414
1981
|
const absPath = resolveVaultPath(ctx2.vaultRoot, input.path);
|
|
1415
1982
|
assertWritable(ctx2.policy, input.path);
|
|
1416
|
-
const original = await
|
|
1983
|
+
const original = await readFile7(absPath, "utf8");
|
|
1417
1984
|
if (input.prevHash !== void 0) assertHashMatch(original, input.prevHash, input.path);
|
|
1418
1985
|
const fm = parseFrontmatter(original);
|
|
1419
1986
|
const keysChanged = [];
|
|
@@ -1456,6 +2023,7 @@ ${original}`;
|
|
|
1456
2023
|
const tail = opensWithCRLF ? "---\r\n" : "---\n";
|
|
1457
2024
|
newContent = `${head}${doc.toString({ lineWidth: 0 })}${tail}${fm.body}`;
|
|
1458
2025
|
}
|
|
2026
|
+
await journalWrite(ctx2, "patch_frontmatter", input.path, original, newContent);
|
|
1459
2027
|
await atomicWrite(absPath, newContent);
|
|
1460
2028
|
const cached = ctx2.notes.get(input.path);
|
|
1461
2029
|
if (cached) {
|
|
@@ -1473,22 +2041,22 @@ ${original}`;
|
|
|
1473
2041
|
}
|
|
1474
2042
|
|
|
1475
2043
|
// src/tools/patch_note.ts
|
|
1476
|
-
import { readFile as
|
|
1477
|
-
import { z as
|
|
1478
|
-
var PatchNoteInput =
|
|
1479
|
-
path:
|
|
1480
|
-
target:
|
|
1481
|
-
|
|
1482
|
-
|
|
2044
|
+
import { readFile as readFile8 } from "fs/promises";
|
|
2045
|
+
import { z as z14 } from "zod";
|
|
2046
|
+
var PatchNoteInput = z14.object({
|
|
2047
|
+
path: z14.string().describe("Vault-relative path to the note."),
|
|
2048
|
+
target: z14.union([
|
|
2049
|
+
z14.object({ heading: z14.string().describe("Heading text (without # markers).") }),
|
|
2050
|
+
z14.object({ block: z14.string().describe("Block reference ID (without the ^ prefix).") })
|
|
1483
2051
|
]).describe("Target section. Provide exactly one of heading or block."),
|
|
1484
|
-
operation:
|
|
2052
|
+
operation: z14.enum(["append", "prepend", "replace"]).describe(
|
|
1485
2053
|
"append: insert after section content. prepend: insert after heading line. replace: swap section content."
|
|
1486
2054
|
),
|
|
1487
|
-
content:
|
|
1488
|
-
createIfMissing:
|
|
2055
|
+
content: z14.string().min(1).describe("Content to insert or replace with."),
|
|
2056
|
+
createIfMissing: z14.boolean().default(false).describe(
|
|
1489
2057
|
"If the heading target is not found, append a new heading (level 2) + content. Only valid for heading targets."
|
|
1490
2058
|
),
|
|
1491
|
-
prevHash:
|
|
2059
|
+
prevHash: z14.string().optional().describe(
|
|
1492
2060
|
"Optional compare-and-swap guard: the contentHash from a prior read. Fails with hash_conflict if the note changed since."
|
|
1493
2061
|
)
|
|
1494
2062
|
});
|
|
@@ -1504,7 +2072,7 @@ function lineStart(raw, offset) {
|
|
|
1504
2072
|
async function patchNote(ctx2, input) {
|
|
1505
2073
|
const absPath = resolveVaultPath(ctx2.vaultRoot, input.path);
|
|
1506
2074
|
assertWritable(ctx2.policy, input.path);
|
|
1507
|
-
const raw = await
|
|
2075
|
+
const raw = await readFile8(absPath, "utf8");
|
|
1508
2076
|
if (input.prevHash !== void 0) assertHashMatch(raw, input.prevHash, input.path);
|
|
1509
2077
|
const fm = parseFrontmatter(raw);
|
|
1510
2078
|
const originalFmRegion = raw.slice(0, fm.bodyStart);
|
|
@@ -1522,6 +2090,7 @@ async function patchNote(ctx2, input) {
|
|
|
1522
2090
|
const newContent2 = `${raw}${sep4}## ${targetText}
|
|
1523
2091
|
|
|
1524
2092
|
${input.content}${contentNl}`;
|
|
2093
|
+
await journalWrite(ctx2, "patch_note", input.path, raw, newContent2);
|
|
1525
2094
|
await atomicWrite(absPath, newContent2);
|
|
1526
2095
|
await verifyFrontmatter(absPath, originalFmRegion);
|
|
1527
2096
|
updateCache(ctx2, input.path, newContent2, fm.bodyStart);
|
|
@@ -1592,6 +2161,7 @@ ${input.content}${contentNl}`;
|
|
|
1592
2161
|
default:
|
|
1593
2162
|
throw new Error(`Unknown operation: ${String(input.operation)}`);
|
|
1594
2163
|
}
|
|
2164
|
+
await journalWrite(ctx2, "patch_note", input.path, raw, newContent);
|
|
1595
2165
|
await atomicWrite(absPath, newContent);
|
|
1596
2166
|
await verifyFrontmatter(absPath, originalFmRegion);
|
|
1597
2167
|
updateCache(ctx2, input.path, newContent, fm.bodyStart);
|
|
@@ -1604,7 +2174,7 @@ ${input.content}${contentNl}`;
|
|
|
1604
2174
|
};
|
|
1605
2175
|
}
|
|
1606
2176
|
async function verifyFrontmatter(absPath, originalFmRegion) {
|
|
1607
|
-
const written = await
|
|
2177
|
+
const written = await readFile8(absPath, "utf8");
|
|
1608
2178
|
if (written.slice(0, originalFmRegion.length) !== originalFmRegion) {
|
|
1609
2179
|
throw new Error("Write-safety violation: frontmatter region changed unexpectedly");
|
|
1610
2180
|
}
|
|
@@ -1619,10 +2189,10 @@ function updateCache(ctx2, path2, newContent, bodyStart) {
|
|
|
1619
2189
|
}
|
|
1620
2190
|
|
|
1621
2191
|
// src/tools/periodic_note.ts
|
|
1622
|
-
import { access as
|
|
1623
|
-
import { dirname as dirname3, join as
|
|
1624
|
-
import { z as
|
|
1625
|
-
var PERIOD =
|
|
2192
|
+
import { access as access3, mkdir as mkdir5, readFile as readFile9 } from "fs/promises";
|
|
2193
|
+
import { dirname as dirname3, join as join4 } from "path";
|
|
2194
|
+
import { z as z15 } from "zod";
|
|
2195
|
+
var PERIOD = z15.enum(["daily", "weekly", "monthly", "quarterly", "yearly"]);
|
|
1626
2196
|
var DEFAULTS = {
|
|
1627
2197
|
daily: { folder: "", format: "YYYY-MM-DD", template: "" },
|
|
1628
2198
|
weekly: { folder: "", format: "gggg-[W]ww", template: "" },
|
|
@@ -1632,7 +2202,7 @@ var DEFAULTS = {
|
|
|
1632
2202
|
};
|
|
1633
2203
|
async function readJson(absPath) {
|
|
1634
2204
|
try {
|
|
1635
|
-
return JSON.parse(await
|
|
2205
|
+
return JSON.parse(await readFile9(absPath, "utf8"));
|
|
1636
2206
|
} catch {
|
|
1637
2207
|
return null;
|
|
1638
2208
|
}
|
|
@@ -1640,7 +2210,7 @@ async function readJson(absPath) {
|
|
|
1640
2210
|
async function resolveConfig(vaultRoot2, period) {
|
|
1641
2211
|
const defaults = DEFAULTS[period];
|
|
1642
2212
|
if (period === "daily") {
|
|
1643
|
-
const cfg2 = await readJson(
|
|
2213
|
+
const cfg2 = await readJson(join4(vaultRoot2, ".obsidian", "daily-notes.json"));
|
|
1644
2214
|
return {
|
|
1645
2215
|
folder: cfg2?.folder ?? defaults.folder,
|
|
1646
2216
|
format: cfg2?.format ?? defaults.format,
|
|
@@ -1648,7 +2218,7 @@ async function resolveConfig(vaultRoot2, period) {
|
|
|
1648
2218
|
};
|
|
1649
2219
|
}
|
|
1650
2220
|
const cfg = await readJson(
|
|
1651
|
-
|
|
2221
|
+
join4(vaultRoot2, ".obsidian", "plugins", "periodic-notes", "data.json")
|
|
1652
2222
|
);
|
|
1653
2223
|
const entry = cfg?.[period];
|
|
1654
2224
|
return {
|
|
@@ -1687,12 +2257,12 @@ function resolveNotePath(cfg, date) {
|
|
|
1687
2257
|
const filename = `${formatMomentDate(date, cfg.format)}.md`;
|
|
1688
2258
|
return cfg.folder ? `${cfg.folder}/${filename}` : filename;
|
|
1689
2259
|
}
|
|
1690
|
-
var GetPeriodicNoteInput =
|
|
2260
|
+
var GetPeriodicNoteInput = z15.object({
|
|
1691
2261
|
period: PERIOD.default("daily").describe(
|
|
1692
2262
|
"Period type: daily (default), weekly, monthly, quarterly, or yearly."
|
|
1693
2263
|
),
|
|
1694
|
-
date:
|
|
1695
|
-
createIfMissing:
|
|
2264
|
+
date: z15.string().optional().describe("ISO date string (YYYY-MM-DD). Defaults to today when omitted."),
|
|
2265
|
+
createIfMissing: z15.boolean().default(false).describe(
|
|
1696
2266
|
"Create the note from the configured template if it does not exist. Default false \u2014 reports missing without creating."
|
|
1697
2267
|
)
|
|
1698
2268
|
});
|
|
@@ -1704,7 +2274,7 @@ async function getPeriodicNote(ctx2, rawInput) {
|
|
|
1704
2274
|
const abs = resolveVaultPath(ctx2.vaultRoot, path2);
|
|
1705
2275
|
let existed = false;
|
|
1706
2276
|
try {
|
|
1707
|
-
await
|
|
2277
|
+
await access3(abs);
|
|
1708
2278
|
existed = true;
|
|
1709
2279
|
} catch {
|
|
1710
2280
|
}
|
|
@@ -1718,21 +2288,22 @@ async function getPeriodicNote(ctx2, rawInput) {
|
|
|
1718
2288
|
ctx2.vaultRoot,
|
|
1719
2289
|
cfg.template.endsWith(".md") ? cfg.template : `${cfg.template}.md`
|
|
1720
2290
|
);
|
|
1721
|
-
body = await
|
|
2291
|
+
body = await readFile9(templateAbs, "utf8");
|
|
1722
2292
|
} catch {
|
|
1723
2293
|
}
|
|
1724
2294
|
}
|
|
1725
|
-
await
|
|
2295
|
+
await mkdir5(dirname3(abs), { recursive: true });
|
|
2296
|
+
await journalWrite(ctx2, "get_periodic_note", path2, null, body);
|
|
1726
2297
|
await atomicWrite(abs, body);
|
|
1727
2298
|
upsertDoc(ctx2, buildDoc(path2, body));
|
|
1728
2299
|
return { path: path2, existed: false, created: true, contentHash: contentHash(body) };
|
|
1729
2300
|
}
|
|
1730
|
-
var AppendPeriodicNoteInput =
|
|
2301
|
+
var AppendPeriodicNoteInput = z15.object({
|
|
1731
2302
|
period: PERIOD.default("daily").describe("Period type. Default: daily."),
|
|
1732
|
-
date:
|
|
1733
|
-
content:
|
|
1734
|
-
createIfMissing:
|
|
1735
|
-
prevHash:
|
|
2303
|
+
date: z15.string().optional().describe("ISO date string (YYYY-MM-DD). Defaults to today."),
|
|
2304
|
+
content: z15.string().min(1).describe("Text to append to the note body. Separated from existing content by a blank line."),
|
|
2305
|
+
createIfMissing: z15.boolean().default(true).describe("Create the note if it does not exist before appending. Default true."),
|
|
2306
|
+
prevHash: z15.string().optional().describe(
|
|
1736
2307
|
"Optional compare-and-swap guard: the contentHash from a prior read of the resolved periodic note. Fails with hash_conflict if it changed since."
|
|
1737
2308
|
)
|
|
1738
2309
|
});
|
|
@@ -1745,7 +2316,7 @@ async function appendPeriodicNote(ctx2, rawInput) {
|
|
|
1745
2316
|
assertWritable(ctx2.policy, path2);
|
|
1746
2317
|
let original = "";
|
|
1747
2318
|
try {
|
|
1748
|
-
original = await
|
|
2319
|
+
original = await readFile9(abs, "utf8");
|
|
1749
2320
|
if (input.prevHash !== void 0) assertHashMatch(original, input.prevHash, path2);
|
|
1750
2321
|
} catch (err) {
|
|
1751
2322
|
if (err.code !== "ENOENT") throw err;
|
|
@@ -1761,7 +2332,8 @@ async function appendPeriodicNote(ctx2, rawInput) {
|
|
|
1761
2332
|
);
|
|
1762
2333
|
}
|
|
1763
2334
|
if (!input.createIfMissing) throw new Error(`Periodic note not found: ${path2}`);
|
|
1764
|
-
await
|
|
2335
|
+
await mkdir5(dirname3(abs), { recursive: true });
|
|
2336
|
+
await journalWrite(ctx2, "append_periodic_note", path2, null, input.content);
|
|
1765
2337
|
await atomicWrite(abs, input.content);
|
|
1766
2338
|
upsertDoc(ctx2, buildDoc(path2, input.content));
|
|
1767
2339
|
return {
|
|
@@ -1777,6 +2349,7 @@ async function appendPeriodicNote(ctx2, rawInput) {
|
|
|
1777
2349
|
const newBody = `${body}${separator}${input.content}`;
|
|
1778
2350
|
const header = original.slice(0, fm.bodyStart);
|
|
1779
2351
|
const newRaw = `${header}${newBody}`;
|
|
2352
|
+
await journalWrite(ctx2, "append_periodic_note", path2, original, newRaw);
|
|
1780
2353
|
await atomicWrite(abs, newRaw);
|
|
1781
2354
|
const cached = ctx2.notes.get(path2);
|
|
1782
2355
|
if (cached) {
|
|
@@ -1791,36 +2364,36 @@ async function appendPeriodicNote(ctx2, rawInput) {
|
|
|
1791
2364
|
}
|
|
1792
2365
|
|
|
1793
2366
|
// src/tools/query_notes.ts
|
|
1794
|
-
import { z as
|
|
2367
|
+
import { z as z16 } from "zod";
|
|
1795
2368
|
var OPS_WITHOUT_VALUE = /* @__PURE__ */ new Set(["exists", "missing"]);
|
|
1796
|
-
var Predicate =
|
|
1797
|
-
key:
|
|
1798
|
-
op:
|
|
2369
|
+
var Predicate = z16.object({
|
|
2370
|
+
key: z16.string().min(1).describe("Frontmatter key to test (top-level)."),
|
|
2371
|
+
op: z16.enum(["eq", "ne", "contains", "exists", "missing", "gt", "gte", "lt", "lte"]).describe(
|
|
1799
2372
|
"eq/ne compare scalars; contains matches array membership or substring; exists/missing test key presence; gt/gte/lt/lte compare numbers or strings (ISO dates sort correctly)."
|
|
1800
2373
|
),
|
|
1801
|
-
value:
|
|
2374
|
+
value: z16.union([z16.string(), z16.number(), z16.boolean()]).optional().describe("Comparison value. Required for every op except exists/missing.")
|
|
1802
2375
|
}).refine((p) => OPS_WITHOUT_VALUE.has(p.op) || p.value !== void 0, {
|
|
1803
2376
|
message: "value is required for this op"
|
|
1804
2377
|
});
|
|
1805
|
-
var ISO_DATE =
|
|
1806
|
-
var QueryNotesInput =
|
|
1807
|
-
where:
|
|
1808
|
-
folder:
|
|
1809
|
-
tag:
|
|
2378
|
+
var ISO_DATE = z16.string().refine((s) => !Number.isNaN(Date.parse(s)), { message: "not a parseable date" });
|
|
2379
|
+
var QueryNotesInput = z16.object({
|
|
2380
|
+
where: z16.array(Predicate).max(20).default([]).describe("Frontmatter predicates. All must match (AND)."),
|
|
2381
|
+
folder: z16.string().optional().describe("Restrict to notes under this vault-relative folder prefix."),
|
|
2382
|
+
tag: z16.string().optional().describe("Restrict to notes with this tag (# prefix optional)."),
|
|
1810
2383
|
modifiedAfter: ISO_DATE.optional().describe(
|
|
1811
2384
|
"Only notes modified at or after this ISO 8601 date/time."
|
|
1812
2385
|
),
|
|
1813
2386
|
modifiedBefore: ISO_DATE.optional().describe(
|
|
1814
2387
|
"Only notes modified before this ISO 8601 date/time."
|
|
1815
2388
|
),
|
|
1816
|
-
minSizeBytes:
|
|
1817
|
-
maxSizeBytes:
|
|
1818
|
-
select:
|
|
2389
|
+
minSizeBytes: z16.number().int().min(0).optional().describe("Only notes at least this many bytes."),
|
|
2390
|
+
maxSizeBytes: z16.number().int().min(0).optional().describe("Only notes at most this many bytes."),
|
|
2391
|
+
select: z16.array(z16.string()).max(20).default([]).describe(
|
|
1819
2392
|
'Extra fields to return per hit: frontmatter keys, or the specials "mtime", "size", "tags". Default returns only path + title, keeping the payload minimal.'
|
|
1820
2393
|
),
|
|
1821
|
-
sort:
|
|
1822
|
-
order:
|
|
1823
|
-
limit:
|
|
2394
|
+
sort: z16.enum(["path", "title", "mtime", "size"]).default("path").describe("Sort field."),
|
|
2395
|
+
order: z16.enum(["asc", "desc"]).default("asc").describe("Sort order."),
|
|
2396
|
+
limit: z16.number().int().min(1).max(500).default(100).describe("Maximum number of results.")
|
|
1824
2397
|
});
|
|
1825
2398
|
function scalarEq(a, b) {
|
|
1826
2399
|
if (a === b) return true;
|
|
@@ -1918,17 +2491,17 @@ function queryNotes(ctx2, input) {
|
|
|
1918
2491
|
}
|
|
1919
2492
|
|
|
1920
2493
|
// src/tools/read_note.ts
|
|
1921
|
-
import { readFile as
|
|
1922
|
-
import { z as
|
|
1923
|
-
var ReadNoteInput =
|
|
1924
|
-
path:
|
|
1925
|
-
section:
|
|
1926
|
-
block:
|
|
1927
|
-
lines:
|
|
1928
|
-
from:
|
|
1929
|
-
to:
|
|
2494
|
+
import { readFile as readFile10 } from "fs/promises";
|
|
2495
|
+
import { z as z17 } from "zod";
|
|
2496
|
+
var ReadNoteInput = z17.object({
|
|
2497
|
+
path: z17.string().describe('Vault-relative path to the note, e.g. "Daily Notes/2026-05-29.md".'),
|
|
2498
|
+
section: z17.string().optional().describe("Return only this heading section (heading text, # prefix optional)."),
|
|
2499
|
+
block: z17.string().optional().describe("Return only the block anchored by this id (^ prefix optional)."),
|
|
2500
|
+
lines: z17.object({
|
|
2501
|
+
from: z17.number().int().min(1).describe("First line (1-indexed, inclusive)."),
|
|
2502
|
+
to: z17.number().int().min(1).optional().describe("Last line (1-indexed, inclusive). Defaults to end of file.")
|
|
1930
2503
|
}).optional().describe("Return only this line range (1-indexed, inclusive)."),
|
|
1931
|
-
includeFrontmatter:
|
|
2504
|
+
includeFrontmatter: z17.boolean().optional().describe(
|
|
1932
2505
|
"Include frontmatter in span results. Default false for section/block spans. No effect on lines or whole-note reads."
|
|
1933
2506
|
)
|
|
1934
2507
|
}).refine((v) => [v.section, v.block, v.lines].filter((x) => x !== void 0).length <= 1, {
|
|
@@ -1936,7 +2509,7 @@ var ReadNoteInput = z16.object({
|
|
|
1936
2509
|
});
|
|
1937
2510
|
async function readNote(ctx2, input) {
|
|
1938
2511
|
const absPath = resolveVaultPath(ctx2.vaultRoot, input.path);
|
|
1939
|
-
const raw = await
|
|
2512
|
+
const raw = await readFile10(absPath, "utf8");
|
|
1940
2513
|
const noteBytes = Buffer.byteLength(raw, "utf8");
|
|
1941
2514
|
const hash = contentHash(raw);
|
|
1942
2515
|
if (input.section === void 0 && input.block === void 0 && input.lines === void 0) {
|
|
@@ -2024,18 +2597,18 @@ async function readNote(ctx2, input) {
|
|
|
2024
2597
|
}
|
|
2025
2598
|
|
|
2026
2599
|
// src/tools/rename_heading.ts
|
|
2027
|
-
import { readFile as
|
|
2028
|
-
import { join as
|
|
2029
|
-
import { z as
|
|
2030
|
-
var RenameHeadingInput =
|
|
2031
|
-
path:
|
|
2032
|
-
oldHeading:
|
|
2600
|
+
import { readFile as readFile11 } from "fs/promises";
|
|
2601
|
+
import { join as join5 } from "path";
|
|
2602
|
+
import { z as z18 } from "zod";
|
|
2603
|
+
var RenameHeadingInput = z18.object({
|
|
2604
|
+
path: z18.string().describe("Vault-relative path of the note containing the heading."),
|
|
2605
|
+
oldHeading: z18.string().min(1).describe(
|
|
2033
2606
|
"Current heading text, without # markers. Matched case-insensitively; if the note has duplicate headings the first one wins, mirroring Obsidian link resolution."
|
|
2034
2607
|
),
|
|
2035
|
-
newHeading:
|
|
2608
|
+
newHeading: z18.string().min(1).refine((s) => !/[\n[\]|#]/.test(s), {
|
|
2036
2609
|
message: 'newHeading must not contain newlines, "[", "]", "|", or "#" \u2014 those break [[note#heading]] link syntax.'
|
|
2037
2610
|
}).describe("New heading text, without # markers."),
|
|
2038
|
-
prevHash:
|
|
2611
|
+
prevHash: z18.string().optional().describe(
|
|
2039
2612
|
"Optional compare-and-swap guard: the contentHash from a prior read of this note. Fails with hash_conflict if the note changed since."
|
|
2040
2613
|
)
|
|
2041
2614
|
});
|
|
@@ -2065,7 +2638,7 @@ async function renameHeading(ctx2, rawInput) {
|
|
|
2065
2638
|
assertWritable(ctx2.policy, input.path);
|
|
2066
2639
|
let raw;
|
|
2067
2640
|
try {
|
|
2068
|
-
raw = await
|
|
2641
|
+
raw = await readFile11(absPath, "utf8");
|
|
2069
2642
|
} catch {
|
|
2070
2643
|
throw new Error(`Note not found: ${input.path}`);
|
|
2071
2644
|
}
|
|
@@ -2099,21 +2672,11 @@ async function renameHeading(ctx2, rawInput) {
|
|
|
2099
2672
|
(target) => target === "" || resolveLink(target, ctx2.notes) === input.path
|
|
2100
2673
|
);
|
|
2101
2674
|
newRaw = newRaw.slice(0, fm.bodyStart) + selfRewrite.content;
|
|
2102
|
-
const originalFmRegion = raw.slice(0, fm.bodyStart);
|
|
2103
|
-
await atomicWrite(absPath, newRaw);
|
|
2104
|
-
const written = await readFile10(absPath, "utf8");
|
|
2105
|
-
if (written.slice(0, originalFmRegion.length) !== originalFmRegion) {
|
|
2106
|
-
throw new Error("Write-safety violation: frontmatter region changed unexpectedly");
|
|
2107
|
-
}
|
|
2108
|
-
removeNoteBacklinks(ctx2, input.path);
|
|
2109
|
-
upsertDoc(ctx2, buildDoc(input.path, newRaw));
|
|
2110
|
-
addNoteBacklinks(ctx2, input.path, newRaw);
|
|
2111
|
-
let notesRewritten = selfRewrite.count > 0 ? 1 : 0;
|
|
2112
|
-
let linksRewritten = selfRewrite.count;
|
|
2113
|
-
const skipped = [];
|
|
2114
2675
|
const candidates = /* @__PURE__ */ new Set();
|
|
2115
2676
|
for (const ref of ctx2.backlinks.get(input.path) ?? []) candidates.add(ref.path);
|
|
2116
2677
|
candidates.delete(input.path);
|
|
2678
|
+
const rewrites = [];
|
|
2679
|
+
const skipped = [];
|
|
2117
2680
|
for (const path2 of [...candidates].sort()) {
|
|
2118
2681
|
const doc = ctx2.notes.get(path2);
|
|
2119
2682
|
if (doc === void 0) continue;
|
|
@@ -2128,7 +2691,27 @@ async function renameHeading(ctx2, rawInput) {
|
|
|
2128
2691
|
skipped.push(path2);
|
|
2129
2692
|
continue;
|
|
2130
2693
|
}
|
|
2131
|
-
|
|
2694
|
+
rewrites.push({ path: path2, pre: doc.raw, content, count });
|
|
2695
|
+
}
|
|
2696
|
+
if (ctx2.journal) {
|
|
2697
|
+
const txn = ctx2.journal.begin("rename_heading");
|
|
2698
|
+
await txn.add(input.path, raw, newRaw);
|
|
2699
|
+
for (const r of rewrites) await txn.add(r.path, r.pre, r.content);
|
|
2700
|
+
await txn.commit();
|
|
2701
|
+
}
|
|
2702
|
+
const originalFmRegion = raw.slice(0, fm.bodyStart);
|
|
2703
|
+
await atomicWrite(absPath, newRaw);
|
|
2704
|
+
const written = await readFile11(absPath, "utf8");
|
|
2705
|
+
if (written.slice(0, originalFmRegion.length) !== originalFmRegion) {
|
|
2706
|
+
throw new Error("Write-safety violation: frontmatter region changed unexpectedly");
|
|
2707
|
+
}
|
|
2708
|
+
removeNoteBacklinks(ctx2, input.path);
|
|
2709
|
+
upsertDoc(ctx2, buildDoc(input.path, newRaw));
|
|
2710
|
+
addNoteBacklinks(ctx2, input.path, newRaw);
|
|
2711
|
+
let notesRewritten = selfRewrite.count > 0 ? 1 : 0;
|
|
2712
|
+
let linksRewritten = selfRewrite.count;
|
|
2713
|
+
for (const { path: path2, content, count } of rewrites) {
|
|
2714
|
+
await atomicWrite(join5(ctx2.vaultRoot, path2), content);
|
|
2132
2715
|
upsertDoc(ctx2, buildDoc(path2, content));
|
|
2133
2716
|
notesRewritten++;
|
|
2134
2717
|
linksRewritten += count;
|
|
@@ -2147,18 +2730,18 @@ async function renameHeading(ctx2, rawInput) {
|
|
|
2147
2730
|
}
|
|
2148
2731
|
|
|
2149
2732
|
// src/tools/replace_in_note.ts
|
|
2150
|
-
import { readFile as
|
|
2151
|
-
import { z as
|
|
2152
|
-
var ReplaceInNoteInput =
|
|
2153
|
-
path:
|
|
2154
|
-
find:
|
|
2155
|
-
replace:
|
|
2156
|
-
regex:
|
|
2157
|
-
caseSensitive:
|
|
2158
|
-
wholeWord:
|
|
2159
|
-
limit:
|
|
2160
|
-
dryRun:
|
|
2161
|
-
prevHash:
|
|
2733
|
+
import { readFile as readFile12 } from "fs/promises";
|
|
2734
|
+
import { z as z19 } from "zod";
|
|
2735
|
+
var ReplaceInNoteInput = z19.object({
|
|
2736
|
+
path: z19.string().describe("Vault-relative path to the note."),
|
|
2737
|
+
find: z19.string().min(1).max(1e3).describe("Text or pattern to find (max 1000 chars; bounds regex-mode pattern size)."),
|
|
2738
|
+
replace: z19.string().describe("Replacement text. Supports $1, $2, \u2026 backreferences in regex mode."),
|
|
2739
|
+
regex: z19.boolean().default(false).describe("Treat find as a regular expression."),
|
|
2740
|
+
caseSensitive: z19.boolean().default(false).describe("Case-sensitive matching."),
|
|
2741
|
+
wholeWord: z19.boolean().default(false).describe("Match whole words only (\\b word-boundary)."),
|
|
2742
|
+
limit: z19.number().int().min(1).optional().describe("Maximum number of replacements. Omit to replace all."),
|
|
2743
|
+
dryRun: z19.boolean().default(false).describe("If true, report matches without writing anything."),
|
|
2744
|
+
prevHash: z19.string().optional().describe(
|
|
2162
2745
|
"Optional compare-and-swap guard: the contentHash from a prior read. Fails with hash_conflict if the note changed since."
|
|
2163
2746
|
)
|
|
2164
2747
|
});
|
|
@@ -2184,7 +2767,7 @@ async function replaceInNote(ctx2, input) {
|
|
|
2184
2767
|
} catch (err) {
|
|
2185
2768
|
throw new Error(`Invalid regex: ${err instanceof Error ? err.message : String(err)}`);
|
|
2186
2769
|
}
|
|
2187
|
-
const raw = await
|
|
2770
|
+
const raw = await readFile12(absPath, "utf8");
|
|
2188
2771
|
if (input.prevHash !== void 0) assertHashMatch(raw, input.prevHash, input.path);
|
|
2189
2772
|
const fm = parseFrontmatter(raw);
|
|
2190
2773
|
const originalFmRegion = raw.slice(0, fm.bodyStart);
|
|
@@ -2218,8 +2801,9 @@ async function replaceInNote(ctx2, input) {
|
|
|
2218
2801
|
}
|
|
2219
2802
|
newBody += body.slice(lastIndex);
|
|
2220
2803
|
const newContent = `${originalFmRegion}${newBody}`;
|
|
2804
|
+
await journalWrite(ctx2, "replace_in_note", input.path, raw, newContent);
|
|
2221
2805
|
await atomicWrite(absPath, newContent);
|
|
2222
|
-
const written = await
|
|
2806
|
+
const written = await readFile12(absPath, "utf8");
|
|
2223
2807
|
if (written.slice(0, originalFmRegion.length) !== originalFmRegion) {
|
|
2224
2808
|
throw new Error("Write-safety violation: frontmatter region changed unexpectedly");
|
|
2225
2809
|
}
|
|
@@ -2239,6 +2823,123 @@ async function replaceInNote(ctx2, input) {
|
|
|
2239
2823
|
};
|
|
2240
2824
|
}
|
|
2241
2825
|
|
|
2826
|
+
// src/tools/undo_write.ts
|
|
2827
|
+
import { mkdir as mkdir6, readFile as readFile13, rm as rm3 } from "fs/promises";
|
|
2828
|
+
import { dirname as dirname4 } from "path";
|
|
2829
|
+
import { z as z20 } from "zod";
|
|
2830
|
+
var UndoWriteInput = z20.object({
|
|
2831
|
+
seq: z20.number().int().min(1).optional().describe(
|
|
2832
|
+
"Journal entry to revert (from list_writes). Defaults to the most recent undoable write."
|
|
2833
|
+
),
|
|
2834
|
+
force: z20.boolean().default(false).describe(
|
|
2835
|
+
"Restore even if a file changed after the journaled write. The clobbered state is journaled first, so nothing is lost. Defaults to false \u2014 conflicts are refused."
|
|
2836
|
+
)
|
|
2837
|
+
});
|
|
2838
|
+
async function undoWrite(ctx2, rawInput) {
|
|
2839
|
+
const input = UndoWriteInput.parse(rawInput);
|
|
2840
|
+
const journal = ctx2.journal;
|
|
2841
|
+
if (!journal) {
|
|
2842
|
+
throw new Error("Write journal is disabled (SEEKSTONE_HISTORY=0); nothing to undo.");
|
|
2843
|
+
}
|
|
2844
|
+
let entry;
|
|
2845
|
+
if (input.seq === void 0) {
|
|
2846
|
+
entry = journal.latestUndoable();
|
|
2847
|
+
if (!entry) throw new Error("No undoable writes in the journal.");
|
|
2848
|
+
} else {
|
|
2849
|
+
entry = journal.get(input.seq);
|
|
2850
|
+
if (!entry) throw new Error(`Journal entry ${input.seq} not found.`);
|
|
2851
|
+
if (!journal.isUndoable(entry)) {
|
|
2852
|
+
throw new Error(
|
|
2853
|
+
JSON.stringify({
|
|
2854
|
+
error: "entry_evicted",
|
|
2855
|
+
seq: entry.seq,
|
|
2856
|
+
hint: "Its pre-image was evicted by the retention cap (SEEKSTONE_HISTORY_MAX_SIZE); it can no longer be undone."
|
|
2857
|
+
})
|
|
2858
|
+
);
|
|
2859
|
+
}
|
|
2860
|
+
}
|
|
2861
|
+
for (const f of entry.files) assertWritable(ctx2.policy, f.path);
|
|
2862
|
+
const current = /* @__PURE__ */ new Map();
|
|
2863
|
+
const conflicts = [];
|
|
2864
|
+
for (const f of entry.files) {
|
|
2865
|
+
const abs = resolveVaultPath(ctx2.vaultRoot, f.path);
|
|
2866
|
+
let cur = null;
|
|
2867
|
+
try {
|
|
2868
|
+
cur = await readFile13(abs, "utf8");
|
|
2869
|
+
} catch (err) {
|
|
2870
|
+
if (err.code !== "ENOENT") throw err;
|
|
2871
|
+
}
|
|
2872
|
+
current.set(f.path, cur);
|
|
2873
|
+
const actualHash = cur === null ? null : contentHash(cur);
|
|
2874
|
+
if (actualHash !== f.postHash) {
|
|
2875
|
+
conflicts.push({
|
|
2876
|
+
path: f.path,
|
|
2877
|
+
expectedHash: f.postHash,
|
|
2878
|
+
actualHash,
|
|
2879
|
+
expectedBytes: f.postHash === null ? null : await postBytes(journal, f.postHash),
|
|
2880
|
+
actualBytes: cur === null ? null : Buffer.byteLength(cur, "utf8")
|
|
2881
|
+
});
|
|
2882
|
+
}
|
|
2883
|
+
}
|
|
2884
|
+
if (conflicts.length > 0 && !input.force) {
|
|
2885
|
+
throw new Error(
|
|
2886
|
+
JSON.stringify({
|
|
2887
|
+
error: "undo_conflict",
|
|
2888
|
+
seq: entry.seq,
|
|
2889
|
+
conflicts,
|
|
2890
|
+
hint: "A file changed after this write (by you or another tool). Re-check it, then pass force: true to restore anyway \u2014 the current state is journaled first."
|
|
2891
|
+
})
|
|
2892
|
+
);
|
|
2893
|
+
}
|
|
2894
|
+
const preimages = /* @__PURE__ */ new Map();
|
|
2895
|
+
for (const f of entry.files) {
|
|
2896
|
+
preimages.set(f.path, f.preHash === null ? null : await journal.preimage(f.preHash));
|
|
2897
|
+
}
|
|
2898
|
+
const txn = journal.begin("undo_write", entry.seq);
|
|
2899
|
+
for (const f of entry.files) {
|
|
2900
|
+
await txn.add(f.path, current.get(f.path) ?? null, preimages.get(f.path) ?? null);
|
|
2901
|
+
}
|
|
2902
|
+
const undoEntry = await txn.commit();
|
|
2903
|
+
const restored = [];
|
|
2904
|
+
for (const f of entry.files) {
|
|
2905
|
+
const abs = resolveVaultPath(ctx2.vaultRoot, f.path);
|
|
2906
|
+
const pre = preimages.get(f.path) ?? null;
|
|
2907
|
+
if (pre === null) {
|
|
2908
|
+
await rm3(abs, { force: true });
|
|
2909
|
+
removeNoteBacklinks(ctx2, f.path);
|
|
2910
|
+
if (ctx2.notes.has(f.path)) {
|
|
2911
|
+
ctx2.index.discard(f.path);
|
|
2912
|
+
ctx2.notes.delete(f.path);
|
|
2913
|
+
}
|
|
2914
|
+
restored.push({ path: f.path, restoredHash: null });
|
|
2915
|
+
} else {
|
|
2916
|
+
await mkdir6(dirname4(abs), { recursive: true });
|
|
2917
|
+
await atomicWrite(abs, pre);
|
|
2918
|
+
if (f.path.endsWith(".md")) {
|
|
2919
|
+
removeNoteBacklinks(ctx2, f.path);
|
|
2920
|
+
upsertDoc(ctx2, buildDoc(f.path, pre));
|
|
2921
|
+
addNoteBacklinks(ctx2, f.path, pre);
|
|
2922
|
+
}
|
|
2923
|
+
restored.push({ path: f.path, restoredHash: contentHash(pre) });
|
|
2924
|
+
}
|
|
2925
|
+
}
|
|
2926
|
+
const result = {
|
|
2927
|
+
seq: entry.seq,
|
|
2928
|
+
undoSeq: undoEntry.seq,
|
|
2929
|
+
tool: entry.tool,
|
|
2930
|
+
restored
|
|
2931
|
+
};
|
|
2932
|
+
if (conflicts.length > 0) result.overrode = conflicts;
|
|
2933
|
+
return result;
|
|
2934
|
+
}
|
|
2935
|
+
async function postBytes(journal, hash) {
|
|
2936
|
+
try {
|
|
2937
|
+
return Buffer.byteLength(await journal.preimage(hash), "utf8");
|
|
2938
|
+
} catch {
|
|
2939
|
+
return null;
|
|
2940
|
+
}
|
|
2941
|
+
}
|
|
2942
|
+
|
|
2242
2943
|
// src/dispatch.ts
|
|
2243
2944
|
var WRITE_TOOLS = /* @__PURE__ */ new Set([
|
|
2244
2945
|
"create_note",
|
|
@@ -2249,7 +2950,8 @@ var WRITE_TOOLS = /* @__PURE__ */ new Set([
|
|
|
2249
2950
|
"patch_frontmatter",
|
|
2250
2951
|
"patch_note",
|
|
2251
2952
|
"replace_in_note",
|
|
2252
|
-
"append_periodic_note"
|
|
2953
|
+
"append_periodic_note",
|
|
2954
|
+
"undo_write"
|
|
2253
2955
|
]);
|
|
2254
2956
|
var META_KEYS = [
|
|
2255
2957
|
"path",
|
|
@@ -2273,7 +2975,9 @@ var META_KEYS = [
|
|
|
2273
2975
|
"date",
|
|
2274
2976
|
"permanent",
|
|
2275
2977
|
"prevHash",
|
|
2276
|
-
"budgetBytes"
|
|
2978
|
+
"budgetBytes",
|
|
2979
|
+
"seq",
|
|
2980
|
+
"force"
|
|
2277
2981
|
];
|
|
2278
2982
|
function safeMeta(args) {
|
|
2279
2983
|
if (typeof args !== "object" || args === null) return {};
|
|
@@ -2297,7 +3001,7 @@ async function dispatch(ctx2, name, args, log2) {
|
|
|
2297
3001
|
log2.debug("tool start", { tool: name, args });
|
|
2298
3002
|
if (ctx2.policy.readOnly && WRITE_TOOLS.has(name)) {
|
|
2299
3003
|
log2.info("tool rejected (read-only)", { tool: name, ...meta });
|
|
2300
|
-
|
|
3004
|
+
const rejected = {
|
|
2301
3005
|
content: [
|
|
2302
3006
|
{
|
|
2303
3007
|
type: "text",
|
|
@@ -2306,17 +3010,39 @@ async function dispatch(ctx2, name, args, log2) {
|
|
|
2306
3010
|
],
|
|
2307
3011
|
isError: true
|
|
2308
3012
|
};
|
|
3013
|
+
return audit(ctx2, log2, rejected, {
|
|
3014
|
+
v: 1,
|
|
3015
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3016
|
+
tool: name,
|
|
3017
|
+
outcome: "policy_denied",
|
|
3018
|
+
policy: "read_only",
|
|
3019
|
+
error: "read_only",
|
|
3020
|
+
durationMs: 0,
|
|
3021
|
+
...pathMeta(meta)
|
|
3022
|
+
});
|
|
2309
3023
|
}
|
|
2310
3024
|
if (ctx2.policy.readOnly && name === "get_periodic_note") {
|
|
2311
3025
|
if (typeof args === "object" && args !== null) {
|
|
2312
3026
|
args = { ...args, createIfMissing: false };
|
|
2313
3027
|
}
|
|
2314
3028
|
}
|
|
3029
|
+
const isWrite = WRITE_TOOLS.has(name) || name === "get_periodic_note";
|
|
3030
|
+
const seqBefore = ctx2.journal?.lastSeq ?? 0;
|
|
3031
|
+
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
2315
3032
|
try {
|
|
2316
|
-
const result = await run(ctx2, name, args);
|
|
3033
|
+
const { audit: detail, ...result } = await run(ctx2, name, args);
|
|
2317
3034
|
const durationMs = Math.round((performance.now() - start) * 100) / 100;
|
|
2318
3035
|
log2.info("tool ok", { tool: name, durationMs, resultBytes: resultBytes(result), ...meta });
|
|
2319
|
-
return result;
|
|
3036
|
+
if (!isWrite || detail === void 0) return result;
|
|
3037
|
+
return audit(ctx2, log2, result, {
|
|
3038
|
+
v: 1,
|
|
3039
|
+
ts,
|
|
3040
|
+
tool: name,
|
|
3041
|
+
outcome: "ok",
|
|
3042
|
+
durationMs,
|
|
3043
|
+
...journalFiles(ctx2, seqBefore, detail),
|
|
3044
|
+
...detail
|
|
3045
|
+
});
|
|
2320
3046
|
} catch (err) {
|
|
2321
3047
|
const durationMs = Math.round((performance.now() - start) * 100) / 100;
|
|
2322
3048
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -2327,7 +3053,66 @@ async function dispatch(ctx2, name, args, log2) {
|
|
|
2327
3053
|
stack: err instanceof Error ? err.stack : void 0,
|
|
2328
3054
|
...meta
|
|
2329
3055
|
});
|
|
2330
|
-
|
|
3056
|
+
const failed = {
|
|
3057
|
+
content: [{ type: "text", text: `Error: ${message}` }],
|
|
3058
|
+
isError: true
|
|
3059
|
+
};
|
|
3060
|
+
if (!isWrite) return failed;
|
|
3061
|
+
return audit(ctx2, log2, failed, {
|
|
3062
|
+
v: 1,
|
|
3063
|
+
ts,
|
|
3064
|
+
tool: name,
|
|
3065
|
+
...classifyError(message),
|
|
3066
|
+
durationMs,
|
|
3067
|
+
...pathMeta(meta)
|
|
3068
|
+
});
|
|
3069
|
+
}
|
|
3070
|
+
}
|
|
3071
|
+
function pathMeta(meta) {
|
|
3072
|
+
const out = {};
|
|
3073
|
+
for (const k of ["path", "from", "to", "seq"])
|
|
3074
|
+
if (meta[k] !== void 0) out[k] = meta[k];
|
|
3075
|
+
return out;
|
|
3076
|
+
}
|
|
3077
|
+
function journalFiles(ctx2, seqBefore, detail) {
|
|
3078
|
+
const entry = ctx2.journal?.since(seqBefore).at(-1);
|
|
3079
|
+
if (entry !== void 0) {
|
|
3080
|
+
return {
|
|
3081
|
+
seq: entry.seq,
|
|
3082
|
+
files: entry.files.map((f) => ({
|
|
3083
|
+
path: f.path,
|
|
3084
|
+
hashBefore: f.preHash,
|
|
3085
|
+
hashAfter: f.postHash
|
|
3086
|
+
}))
|
|
3087
|
+
};
|
|
3088
|
+
}
|
|
3089
|
+
if (typeof detail.path === "string" && typeof detail.contentHash === "string") {
|
|
3090
|
+
return { files: [{ path: detail.path, hashAfter: detail.contentHash }] };
|
|
3091
|
+
}
|
|
3092
|
+
return {};
|
|
3093
|
+
}
|
|
3094
|
+
function audit(ctx2, log2, result, rec) {
|
|
3095
|
+
if (!ctx2.audit) return result;
|
|
3096
|
+
try {
|
|
3097
|
+
ctx2.audit.record(rec);
|
|
3098
|
+
return result;
|
|
3099
|
+
} catch (err) {
|
|
3100
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
3101
|
+
log2.error("audit write failed", { tool: rec.tool, error: message });
|
|
3102
|
+
return {
|
|
3103
|
+
content: [
|
|
3104
|
+
{
|
|
3105
|
+
type: "text",
|
|
3106
|
+
text: `Error: ${JSON.stringify({
|
|
3107
|
+
error: "audit_failed",
|
|
3108
|
+
tool: rec.tool,
|
|
3109
|
+
outcome: rec.outcome,
|
|
3110
|
+
hint: `The audit record could not be written (${message}). The vault operation itself ${rec.outcome === "ok" ? "DID complete" : "did not run"}; fix SEEKSTONE_AUDIT_FILE before continuing.`
|
|
3111
|
+
})}`
|
|
3112
|
+
}
|
|
3113
|
+
],
|
|
3114
|
+
isError: true
|
|
3115
|
+
};
|
|
2331
3116
|
}
|
|
2332
3117
|
}
|
|
2333
3118
|
async function run(ctx2, name, args) {
|
|
@@ -2371,14 +3156,26 @@ async function run(ctx2, name, args) {
|
|
|
2371
3156
|
type: "text",
|
|
2372
3157
|
text: `Created ${result.path} (${result.bytesWritten} bytes). contentHash: ${result.contentHash}`
|
|
2373
3158
|
}
|
|
2374
|
-
]
|
|
3159
|
+
],
|
|
3160
|
+
audit: {
|
|
3161
|
+
path: result.path,
|
|
3162
|
+
contentHash: result.contentHash,
|
|
3163
|
+
bytesWritten: result.bytesWritten,
|
|
3164
|
+
overwrite: input.overwrite
|
|
3165
|
+
}
|
|
2375
3166
|
};
|
|
2376
3167
|
}
|
|
2377
3168
|
case "delete_note": {
|
|
2378
3169
|
const input = DeleteNoteInput.parse(args);
|
|
2379
3170
|
const result = await deleteNote(ctx2, input);
|
|
2380
3171
|
const text = result.trashedTo ? `Moved ${result.path} to ${result.trashedTo} (recoverable \u2014 restore by moving it back).` : `Permanently deleted ${result.path}.`;
|
|
2381
|
-
|
|
3172
|
+
const audit2 = {
|
|
3173
|
+
path: result.path,
|
|
3174
|
+
contentHash: result.contentHash,
|
|
3175
|
+
permanent: result.permanent
|
|
3176
|
+
};
|
|
3177
|
+
if (result.trashedTo !== void 0) audit2.trashedTo = result.trashedTo;
|
|
3178
|
+
return { content: [{ type: "text", text }], audit: audit2 };
|
|
2382
3179
|
}
|
|
2383
3180
|
case "move_note": {
|
|
2384
3181
|
const input = MoveNoteInput.parse(args);
|
|
@@ -2387,7 +3184,17 @@ async function run(ctx2, name, args) {
|
|
|
2387
3184
|
if (result.skipped?.length) {
|
|
2388
3185
|
text += ` Skipped (outside SEEKSTONE_WRITE_PATHS): ${result.skipped.join(", ")}.`;
|
|
2389
3186
|
}
|
|
2390
|
-
return {
|
|
3187
|
+
return {
|
|
3188
|
+
content: [{ type: "text", text }],
|
|
3189
|
+
audit: {
|
|
3190
|
+
from: result.from,
|
|
3191
|
+
to: result.to,
|
|
3192
|
+
contentHash: result.contentHash,
|
|
3193
|
+
notesRewritten: result.notesRewritten,
|
|
3194
|
+
linksRewritten: result.linksRewritten,
|
|
3195
|
+
...result.skipped?.length ? { skipped: result.skipped } : {}
|
|
3196
|
+
}
|
|
3197
|
+
};
|
|
2391
3198
|
}
|
|
2392
3199
|
case "rename_heading": {
|
|
2393
3200
|
const input = RenameHeadingInput.parse(args);
|
|
@@ -2396,7 +3203,17 @@ async function run(ctx2, name, args) {
|
|
|
2396
3203
|
if (result.skipped?.length) {
|
|
2397
3204
|
text += ` Skipped (outside SEEKSTONE_WRITE_PATHS): ${result.skipped.join(", ")}.`;
|
|
2398
3205
|
}
|
|
2399
|
-
return {
|
|
3206
|
+
return {
|
|
3207
|
+
content: [{ type: "text", text }],
|
|
3208
|
+
audit: {
|
|
3209
|
+
path: result.path,
|
|
3210
|
+
contentHash: result.contentHash,
|
|
3211
|
+
line: result.line,
|
|
3212
|
+
notesRewritten: result.notesRewritten,
|
|
3213
|
+
linksRewritten: result.linksRewritten,
|
|
3214
|
+
...result.skipped?.length ? { skipped: result.skipped } : {}
|
|
3215
|
+
}
|
|
3216
|
+
};
|
|
2400
3217
|
}
|
|
2401
3218
|
case "append_note": {
|
|
2402
3219
|
const input = AppendNoteInput.parse(args);
|
|
@@ -2407,13 +3224,28 @@ async function run(ctx2, name, args) {
|
|
|
2407
3224
|
type: "text",
|
|
2408
3225
|
text: `Appended ${result.bytesWritten} bytes to ${result.path}. contentHash: ${result.contentHash}`
|
|
2409
3226
|
}
|
|
2410
|
-
]
|
|
3227
|
+
],
|
|
3228
|
+
audit: {
|
|
3229
|
+
path: result.path,
|
|
3230
|
+
contentHash: result.contentHash,
|
|
3231
|
+
bytesWritten: result.bytesWritten
|
|
3232
|
+
}
|
|
2411
3233
|
};
|
|
2412
3234
|
}
|
|
2413
3235
|
case "patch_frontmatter": {
|
|
2414
3236
|
const input = PatchFrontmatterInput.parse(args);
|
|
2415
3237
|
const result = await patchFrontmatter(ctx2, input);
|
|
2416
|
-
return {
|
|
3238
|
+
return {
|
|
3239
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
3240
|
+
// Key NAMES only — values are note content and never leave the tool.
|
|
3241
|
+
audit: {
|
|
3242
|
+
path: result.path,
|
|
3243
|
+
contentHash: result.contentHash,
|
|
3244
|
+
keysChanged: result.keysChanged,
|
|
3245
|
+
keysAdded: result.keysAdded,
|
|
3246
|
+
keysRemoved: result.keysRemoved
|
|
3247
|
+
}
|
|
3248
|
+
};
|
|
2417
3249
|
}
|
|
2418
3250
|
case "outline_note": {
|
|
2419
3251
|
const input = OutlineNoteInput.parse(args);
|
|
@@ -2423,7 +3255,16 @@ async function run(ctx2, name, args) {
|
|
|
2423
3255
|
case "patch_note": {
|
|
2424
3256
|
const input = PatchNoteInput.parse(args);
|
|
2425
3257
|
const result = await patchNote(ctx2, input);
|
|
2426
|
-
return {
|
|
3258
|
+
return {
|
|
3259
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
3260
|
+
audit: {
|
|
3261
|
+
path: result.path,
|
|
3262
|
+
contentHash: result.contentHash,
|
|
3263
|
+
bytesWritten: result.bytesWritten,
|
|
3264
|
+
operation: input.operation,
|
|
3265
|
+
targetLine: result.targetResolvedAt.line
|
|
3266
|
+
}
|
|
3267
|
+
};
|
|
2427
3268
|
}
|
|
2428
3269
|
case "get_backlinks": {
|
|
2429
3270
|
const input = GetBacklinksInput.parse(args);
|
|
@@ -2438,12 +3279,26 @@ async function run(ctx2, name, args) {
|
|
|
2438
3279
|
case "replace_in_note": {
|
|
2439
3280
|
const input = ReplaceInNoteInput.parse(args);
|
|
2440
3281
|
const result = await replaceInNote(ctx2, input);
|
|
2441
|
-
return {
|
|
3282
|
+
return {
|
|
3283
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
3284
|
+
audit: {
|
|
3285
|
+
path: result.path,
|
|
3286
|
+
contentHash: result.contentHash,
|
|
3287
|
+
replacements: result.replacements,
|
|
3288
|
+
dryRun: input.dryRun
|
|
3289
|
+
}
|
|
3290
|
+
};
|
|
2442
3291
|
}
|
|
2443
3292
|
case "get_periodic_note": {
|
|
2444
3293
|
const input = GetPeriodicNoteInput.parse(args);
|
|
2445
3294
|
const result = await getPeriodicNote(ctx2, input);
|
|
2446
|
-
|
|
3295
|
+
const out = {
|
|
3296
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
3297
|
+
};
|
|
3298
|
+
if (result.created) {
|
|
3299
|
+
out.audit = { path: result.path, contentHash: result.contentHash, created: true };
|
|
3300
|
+
}
|
|
3301
|
+
return out;
|
|
2447
3302
|
}
|
|
2448
3303
|
case "append_periodic_note": {
|
|
2449
3304
|
const input = AppendPeriodicNoteInput.parse(args);
|
|
@@ -2454,7 +3309,29 @@ async function run(ctx2, name, args) {
|
|
|
2454
3309
|
type: "text",
|
|
2455
3310
|
text: `Appended ${result.bytesWritten} bytes to ${result.path}. contentHash: ${result.contentHash}`
|
|
2456
3311
|
}
|
|
2457
|
-
]
|
|
3312
|
+
],
|
|
3313
|
+
audit: {
|
|
3314
|
+
path: result.path,
|
|
3315
|
+
contentHash: result.contentHash,
|
|
3316
|
+
bytesWritten: result.bytesWritten
|
|
3317
|
+
}
|
|
3318
|
+
};
|
|
3319
|
+
}
|
|
3320
|
+
case "list_writes": {
|
|
3321
|
+
const input = ListWritesInput.parse(args);
|
|
3322
|
+
const result = listWrites(ctx2, input);
|
|
3323
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
3324
|
+
}
|
|
3325
|
+
case "undo_write": {
|
|
3326
|
+
const input = UndoWriteInput.parse(args);
|
|
3327
|
+
const result = await undoWrite(ctx2, input);
|
|
3328
|
+
return {
|
|
3329
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
3330
|
+
audit: {
|
|
3331
|
+
undoOf: result.seq,
|
|
3332
|
+
restored: result.restored.length,
|
|
3333
|
+
...result.overrode ? { forced: true, overrode: result.overrode.length } : {}
|
|
3334
|
+
}
|
|
2458
3335
|
};
|
|
2459
3336
|
}
|
|
2460
3337
|
default:
|
|
@@ -2463,12 +3340,12 @@ async function run(ctx2, name, args) {
|
|
|
2463
3340
|
}
|
|
2464
3341
|
|
|
2465
3342
|
// src/index/build.ts
|
|
2466
|
-
import { readFile as
|
|
3343
|
+
import { readFile as readFile14 } from "fs/promises";
|
|
2467
3344
|
import { basename as basename3, extname as extname4 } from "path";
|
|
2468
3345
|
|
|
2469
3346
|
// ../core/src/walk.ts
|
|
2470
|
-
import { stat } from "fs/promises";
|
|
2471
|
-
import { extname as extname3, join as
|
|
3347
|
+
import { stat as stat2 } from "fs/promises";
|
|
3348
|
+
import { extname as extname3, join as join6, relative, sep as sep2 } from "path";
|
|
2472
3349
|
import fg from "fast-glob";
|
|
2473
3350
|
var IMAGE_EXTS = /* @__PURE__ */ new Set([".png", ".jpg", ".jpeg", ".gif", ".webp", ".svg", ".bmp", ".heic"]);
|
|
2474
3351
|
var VIDEO_EXTS = /* @__PURE__ */ new Set([".mp4", ".mov", ".webm", ".mkv", ".avi"]);
|
|
@@ -2476,6 +3353,7 @@ var AUDIO_EXTS = /* @__PURE__ */ new Set([".mp3", ".m4a", ".wav", ".flac", ".ogg
|
|
|
2476
3353
|
var IGNORE_GLOBS = [
|
|
2477
3354
|
"**/.obsidian/**",
|
|
2478
3355
|
"**/.trash/**",
|
|
3356
|
+
"**/.seekstone/**",
|
|
2479
3357
|
"**/.git/**",
|
|
2480
3358
|
"**/.DS_Store",
|
|
2481
3359
|
"**/node_modules/**"
|
|
@@ -2507,7 +3385,7 @@ async function walkVault(vaultRoot2) {
|
|
|
2507
3385
|
const entries = [];
|
|
2508
3386
|
for (const m of matches2) {
|
|
2509
3387
|
const absPath = typeof m === "string" ? m : m.path;
|
|
2510
|
-
const stats = typeof m === "string" ? await
|
|
3388
|
+
const stats = typeof m === "string" ? await stat2(absPath) : m.stats;
|
|
2511
3389
|
if (!stats) continue;
|
|
2512
3390
|
const relPath = relative(vaultRoot2, absPath).split(sep2).join("/");
|
|
2513
3391
|
const topDir = relPath.split("/")[0] ?? "";
|
|
@@ -2545,7 +3423,7 @@ async function buildIndex(vaultRoot2) {
|
|
|
2545
3423
|
await Promise.all(
|
|
2546
3424
|
noteEntries.map(async (entry) => {
|
|
2547
3425
|
try {
|
|
2548
|
-
const raw = await
|
|
3426
|
+
const raw = await readFile14(entry.absPath, "utf8");
|
|
2549
3427
|
const fm = parseFrontmatter(raw);
|
|
2550
3428
|
const inlineTags = extractInlineTags(fm.body);
|
|
2551
3429
|
const fmTags = frontmatterTags(fm.data);
|
|
@@ -2598,8 +3476,8 @@ function buildBacklinks(notes2) {
|
|
|
2598
3476
|
|
|
2599
3477
|
// src/init.ts
|
|
2600
3478
|
import { execFileSync } from "child_process";
|
|
2601
|
-
import { access as
|
|
2602
|
-
import path, { dirname as
|
|
3479
|
+
import { access as access4, copyFile, mkdir as mkdir7, readdir as readdir2, readFile as readFile15, stat as stat3, writeFile as writeFile2 } from "fs/promises";
|
|
3480
|
+
import path, { dirname as dirname5, join as join7 } from "path";
|
|
2603
3481
|
function parseInitArgs(argv) {
|
|
2604
3482
|
const opts = { write: false, client: "desktop" };
|
|
2605
3483
|
for (let i = 0; i < argv.length; i++) {
|
|
@@ -2642,7 +3520,7 @@ function cursorConfigPath(platform, env) {
|
|
|
2642
3520
|
return j(env.home ?? "", ".cursor", "mcp.json");
|
|
2643
3521
|
}
|
|
2644
3522
|
function vscodeMcpConfigPath(cwd) {
|
|
2645
|
-
return
|
|
3523
|
+
return join7(cwd, ".vscode", "mcp.json");
|
|
2646
3524
|
}
|
|
2647
3525
|
function obsidianRegistryPath(platform, env) {
|
|
2648
3526
|
if (platform === "win32") {
|
|
@@ -2698,13 +3576,13 @@ function mergeSeekstoneConfigVsCode(existing, vaultPath) {
|
|
|
2698
3576
|
}
|
|
2699
3577
|
async function validateVault(vaultPath) {
|
|
2700
3578
|
try {
|
|
2701
|
-
const st = await
|
|
3579
|
+
const st = await stat3(vaultPath);
|
|
2702
3580
|
if (!st.isDirectory()) return { ok: false, error: `Not a directory: ${vaultPath}` };
|
|
2703
3581
|
} catch {
|
|
2704
3582
|
return { ok: false, error: `Path does not exist: ${vaultPath}` };
|
|
2705
3583
|
}
|
|
2706
3584
|
try {
|
|
2707
|
-
await
|
|
3585
|
+
await access4(join7(vaultPath, ".obsidian"));
|
|
2708
3586
|
} catch {
|
|
2709
3587
|
return {
|
|
2710
3588
|
ok: false,
|
|
@@ -2718,13 +3596,13 @@ async function countMarkdown(root) {
|
|
|
2718
3596
|
async function walk(dir) {
|
|
2719
3597
|
let entries;
|
|
2720
3598
|
try {
|
|
2721
|
-
entries = await
|
|
3599
|
+
entries = await readdir2(dir, { withFileTypes: true });
|
|
2722
3600
|
} catch {
|
|
2723
3601
|
return;
|
|
2724
3602
|
}
|
|
2725
3603
|
for (const e of entries) {
|
|
2726
3604
|
if (e.name.startsWith(".")) continue;
|
|
2727
|
-
const full =
|
|
3605
|
+
const full = join7(dir, e.name);
|
|
2728
3606
|
if (e.isDirectory()) await walk(full);
|
|
2729
3607
|
else if (e.name.endsWith(".md")) count++;
|
|
2730
3608
|
}
|
|
@@ -2742,7 +3620,7 @@ async function runInit(opts, deps) {
|
|
|
2742
3620
|
]
|
|
2743
3621
|
};
|
|
2744
3622
|
}
|
|
2745
|
-
const rf = deps.readFile ??
|
|
3623
|
+
const rf = deps.readFile ?? readFile15;
|
|
2746
3624
|
let vaultPath = opts.vault ?? deps.env.SEEKSTONE_VAULT;
|
|
2747
3625
|
if (!vaultPath) {
|
|
2748
3626
|
const registryPath = obsidianRegistryPath(deps.platform, {
|
|
@@ -2873,7 +3751,7 @@ async function runInit(opts, deps) {
|
|
|
2873
3751
|
}
|
|
2874
3752
|
const merged = opts.client === "vscode" ? mergeSeekstoneConfigVsCode(existing, vaultPath) : mergeSeekstoneConfig(existing, vaultPath);
|
|
2875
3753
|
let backupPath;
|
|
2876
|
-
await
|
|
3754
|
+
await mkdir7(dirname5(configPath), { recursive: true });
|
|
2877
3755
|
if (existingRaw !== null) {
|
|
2878
3756
|
backupPath = `${configPath}.backup-${deps.timestamp}`;
|
|
2879
3757
|
await copyFile(configPath, backupPath);
|
|
@@ -2889,107 +3767,6 @@ async function runInit(opts, deps) {
|
|
|
2889
3767
|
return { ok: true, exitCode: 0, output: out, wrotePath: configPath, backupPath };
|
|
2890
3768
|
}
|
|
2891
3769
|
|
|
2892
|
-
// src/log.ts
|
|
2893
|
-
import { appendFileSync, renameSync, statSync } from "fs";
|
|
2894
|
-
var ORDER = ["error", "warn", "info", "debug"];
|
|
2895
|
-
var DEFAULT_MAX_SIZE = 5 * 1024 * 1024;
|
|
2896
|
-
function parseLevel(raw, onBad) {
|
|
2897
|
-
if (raw === void 0 || raw === "") return "info";
|
|
2898
|
-
const v = raw.trim().toLowerCase();
|
|
2899
|
-
for (const lvl of ORDER) {
|
|
2900
|
-
if (lvl === v) return lvl;
|
|
2901
|
-
}
|
|
2902
|
-
onBad(`invalid SEEKSTONE_LOG_LEVEL=${JSON.stringify(raw)}; using "info"`);
|
|
2903
|
-
return "info";
|
|
2904
|
-
}
|
|
2905
|
-
function parseSize(raw) {
|
|
2906
|
-
if (raw === void 0) return DEFAULT_MAX_SIZE;
|
|
2907
|
-
const m = /^(\d+(?:\.\d+)?)(b|kb|mb|gb)?$/i.exec(raw.trim());
|
|
2908
|
-
if (m === null) return DEFAULT_MAX_SIZE;
|
|
2909
|
-
const num = Number(m[1]);
|
|
2910
|
-
const unit = (m[2] ?? "b").toLowerCase();
|
|
2911
|
-
const mult = unit === "gb" ? 1e9 : unit === "mb" ? 1e6 : unit === "kb" ? 1e3 : 1;
|
|
2912
|
-
return Math.max(1, Math.floor(num * mult));
|
|
2913
|
-
}
|
|
2914
|
-
function fmtFields(fields) {
|
|
2915
|
-
const parts = [];
|
|
2916
|
-
for (const [k, v] of Object.entries(fields)) {
|
|
2917
|
-
if (v === void 0) continue;
|
|
2918
|
-
parts.push(`${k}=${typeof v === "string" ? v : JSON.stringify(v)}`);
|
|
2919
|
-
}
|
|
2920
|
-
return parts.length > 0 ? ` ${parts.join(" ")}` : "";
|
|
2921
|
-
}
|
|
2922
|
-
function createLogger(opts = {}) {
|
|
2923
|
-
const env = opts.env ?? process.env;
|
|
2924
|
-
const stderr = opts.stderr ?? ((line) => void process.stderr.write(line));
|
|
2925
|
-
const now = opts.now ?? (() => /* @__PURE__ */ new Date());
|
|
2926
|
-
const level = parseLevel(env.SEEKSTONE_LOG_LEVEL, (m) => {
|
|
2927
|
-
stderr(`WARN ${now().toISOString()} ${m}
|
|
2928
|
-
`);
|
|
2929
|
-
});
|
|
2930
|
-
const threshold = ORDER.indexOf(level);
|
|
2931
|
-
const filePath = env.SEEKSTONE_LOG_FILE;
|
|
2932
|
-
const maxSize = parseSize(env.SEEKSTONE_LOG_MAX_SIZE);
|
|
2933
|
-
let fileOk = false;
|
|
2934
|
-
let bytes = 0;
|
|
2935
|
-
if (filePath !== void 0 && filePath !== "") {
|
|
2936
|
-
try {
|
|
2937
|
-
try {
|
|
2938
|
-
bytes = statSync(filePath).size;
|
|
2939
|
-
} catch {
|
|
2940
|
-
bytes = 0;
|
|
2941
|
-
}
|
|
2942
|
-
appendFileSync(filePath, "");
|
|
2943
|
-
fileOk = true;
|
|
2944
|
-
} catch (err) {
|
|
2945
|
-
const m = err instanceof Error ? err.message : String(err);
|
|
2946
|
-
stderr(
|
|
2947
|
-
`WARN ${now().toISOString()} cannot write log file ${filePath}: ${m}; file logging disabled
|
|
2948
|
-
`
|
|
2949
|
-
);
|
|
2950
|
-
}
|
|
2951
|
-
}
|
|
2952
|
-
function writeFile5(record) {
|
|
2953
|
-
if (!fileOk || filePath === void 0) return;
|
|
2954
|
-
const line = `${JSON.stringify(record)}
|
|
2955
|
-
`;
|
|
2956
|
-
const len = Buffer.byteLength(line);
|
|
2957
|
-
try {
|
|
2958
|
-
if (bytes > 0 && bytes + len > maxSize) {
|
|
2959
|
-
renameSync(filePath, `${filePath}.1`);
|
|
2960
|
-
bytes = 0;
|
|
2961
|
-
}
|
|
2962
|
-
appendFileSync(filePath, line);
|
|
2963
|
-
bytes += len;
|
|
2964
|
-
} catch {
|
|
2965
|
-
fileOk = false;
|
|
2966
|
-
}
|
|
2967
|
-
}
|
|
2968
|
-
function emit(lvl, msg, fields) {
|
|
2969
|
-
if (ORDER.indexOf(lvl) > threshold) return;
|
|
2970
|
-
const ts = now().toISOString();
|
|
2971
|
-
const f = fields ?? {};
|
|
2972
|
-
stderr(`${lvl.toUpperCase().padEnd(5)} ${ts} ${msg}${fmtFields(f)}
|
|
2973
|
-
`);
|
|
2974
|
-
writeFile5({ t: ts, level: lvl, msg, ...f });
|
|
2975
|
-
}
|
|
2976
|
-
return {
|
|
2977
|
-
level,
|
|
2978
|
-
error: (m, f) => {
|
|
2979
|
-
emit("error", m, f);
|
|
2980
|
-
},
|
|
2981
|
-
warn: (m, f) => {
|
|
2982
|
-
emit("warn", m, f);
|
|
2983
|
-
},
|
|
2984
|
-
info: (m, f) => {
|
|
2985
|
-
emit("info", m, f);
|
|
2986
|
-
},
|
|
2987
|
-
debug: (m, f) => {
|
|
2988
|
-
emit("debug", m, f);
|
|
2989
|
-
}
|
|
2990
|
-
};
|
|
2991
|
-
}
|
|
2992
|
-
|
|
2993
3770
|
// src/process-guards.ts
|
|
2994
3771
|
function installProcessGuards(log2) {
|
|
2995
3772
|
const onUnhandledRejection = (reason) => {
|
|
@@ -3078,9 +3855,83 @@ function countWords(text) {
|
|
|
3078
3855
|
return m ? m.length : 0;
|
|
3079
3856
|
}
|
|
3080
3857
|
|
|
3858
|
+
// ../core/src/embed/maxsim.ts
|
|
3859
|
+
function maxsimScoreTokens(query, docs, tokenVector, opts = {}) {
|
|
3860
|
+
const n = query.ids.length;
|
|
3861
|
+
const weights = opts.weights;
|
|
3862
|
+
if (weights !== void 0 && weights.length !== n) {
|
|
3863
|
+
throw new Error(`maxsim: ${weights.length} weights for ${n} query tokens`);
|
|
3864
|
+
}
|
|
3865
|
+
const mean = (opts.aggregate ?? "sum") === "mean";
|
|
3866
|
+
const out = new Array(docs.length).fill(0);
|
|
3867
|
+
if (n === 0) return out;
|
|
3868
|
+
const { dim } = query;
|
|
3869
|
+
const q = query.vectors;
|
|
3870
|
+
const simCache = /* @__PURE__ */ new Map();
|
|
3871
|
+
const best = new Float64Array(n);
|
|
3872
|
+
for (let d = 0; d < docs.length; d++) {
|
|
3873
|
+
const ids = docs[d];
|
|
3874
|
+
const m = ids.length;
|
|
3875
|
+
if (m === 0) continue;
|
|
3876
|
+
best.fill(Number.NEGATIVE_INFINITY);
|
|
3877
|
+
for (let t = 0; t < m; t++) {
|
|
3878
|
+
const id = ids[t];
|
|
3879
|
+
let sims = simCache.get(id);
|
|
3880
|
+
if (sims === void 0) {
|
|
3881
|
+
sims = new Float64Array(n);
|
|
3882
|
+
const vec = tokenVector(id);
|
|
3883
|
+
if (vec.length !== dim) {
|
|
3884
|
+
throw new Error(`maxsim: token vector dim ${vec.length} does not match query dim ${dim}`);
|
|
3885
|
+
}
|
|
3886
|
+
for (let i = 0; i < n; i++) {
|
|
3887
|
+
const qBase = i * dim;
|
|
3888
|
+
let dot = 0;
|
|
3889
|
+
for (let j = 0; j < dim; j++) {
|
|
3890
|
+
dot += q[qBase + j] * vec[j];
|
|
3891
|
+
}
|
|
3892
|
+
sims[i] = dot;
|
|
3893
|
+
}
|
|
3894
|
+
simCache.set(id, sims);
|
|
3895
|
+
}
|
|
3896
|
+
for (let i = 0; i < n; i++) {
|
|
3897
|
+
const s = sims[i];
|
|
3898
|
+
if (s > best[i]) best[i] = s;
|
|
3899
|
+
}
|
|
3900
|
+
}
|
|
3901
|
+
let sum = 0;
|
|
3902
|
+
let weightSum = 0;
|
|
3903
|
+
for (let i = 0; i < n; i++) {
|
|
3904
|
+
const w = weights === void 0 ? 1 : weights[i];
|
|
3905
|
+
sum += w * best[i];
|
|
3906
|
+
weightSum += w;
|
|
3907
|
+
}
|
|
3908
|
+
out[d] = mean ? weightSum > 0 ? sum / weightSum : 0 : sum;
|
|
3909
|
+
}
|
|
3910
|
+
return out;
|
|
3911
|
+
}
|
|
3912
|
+
function candidateSetIdf(queryIds, docs) {
|
|
3913
|
+
const n = docs.length;
|
|
3914
|
+
const docSets = [];
|
|
3915
|
+
for (const doc of docs) {
|
|
3916
|
+
const set = /* @__PURE__ */ new Set();
|
|
3917
|
+
for (let t = 0; t < doc.length; t++) set.add(doc[t]);
|
|
3918
|
+
docSets.push(set);
|
|
3919
|
+
}
|
|
3920
|
+
const out = new Array(queryIds.length);
|
|
3921
|
+
for (let i = 0; i < queryIds.length; i++) {
|
|
3922
|
+
const id = queryIds[i];
|
|
3923
|
+
let df = 0;
|
|
3924
|
+
for (const set of docSets) {
|
|
3925
|
+
if (set.has(id)) df++;
|
|
3926
|
+
}
|
|
3927
|
+
out[i] = Math.log(1 + (n - df + 0.5) / (df + 0.5));
|
|
3928
|
+
}
|
|
3929
|
+
return out;
|
|
3930
|
+
}
|
|
3931
|
+
|
|
3081
3932
|
// ../core/src/embed/model2vec.ts
|
|
3082
|
-
import { readFile as
|
|
3083
|
-
import { basename as basename4, join as
|
|
3933
|
+
import { readFile as readFile16 } from "fs/promises";
|
|
3934
|
+
import { basename as basename4, join as join9 } from "path";
|
|
3084
3935
|
|
|
3085
3936
|
// ../core/src/embed/safetensors.ts
|
|
3086
3937
|
function readSafetensors(buf) {
|
|
@@ -3157,7 +4008,7 @@ function loadWordPieceTokenizer(tokenizerJson) {
|
|
|
3157
4008
|
const specialIds = new Set(
|
|
3158
4009
|
(tj.added_tokens ?? []).filter((t) => t.special).map((t) => t.id)
|
|
3159
4010
|
);
|
|
3160
|
-
function
|
|
4011
|
+
function normalize2(text) {
|
|
3161
4012
|
let out = cleanText ? doCleanText(text) : text;
|
|
3162
4013
|
if (handleCjk) out = padCjkChars(out);
|
|
3163
4014
|
if (stripAccents) out = out.normalize("NFD").replace(new RegExp("\\p{Mn}", "gu"), "");
|
|
@@ -3193,7 +4044,7 @@ function loadWordPieceTokenizer(tokenizerJson) {
|
|
|
3193
4044
|
specialIds,
|
|
3194
4045
|
encode(text) {
|
|
3195
4046
|
const ids = [];
|
|
3196
|
-
for (const word of preTokenize(
|
|
4047
|
+
for (const word of preTokenize(normalize2(text))) {
|
|
3197
4048
|
for (const id of wordPiece(word)) ids.push(id);
|
|
3198
4049
|
}
|
|
3199
4050
|
return ids;
|
|
@@ -3261,9 +4112,9 @@ function isCjk(cp) {
|
|
|
3261
4112
|
var TENSOR_NAME = "embeddings";
|
|
3262
4113
|
async function loadModel2Vec(modelDir) {
|
|
3263
4114
|
const [configRaw, tokenizerRaw, weights] = await Promise.all([
|
|
3264
|
-
|
|
3265
|
-
|
|
3266
|
-
|
|
4115
|
+
readFile16(join9(modelDir, "config.json"), "utf8"),
|
|
4116
|
+
readFile16(join9(modelDir, "tokenizer.json"), "utf8"),
|
|
4117
|
+
readFile16(join9(modelDir, "model.safetensors"))
|
|
3267
4118
|
]);
|
|
3268
4119
|
const config = JSON.parse(configRaw);
|
|
3269
4120
|
const tokenizer = loadWordPieceTokenizer(JSON.parse(tokenizerRaw));
|
|
@@ -3289,20 +4140,27 @@ async function loadModel2Vec(modelDir) {
|
|
|
3289
4140
|
);
|
|
3290
4141
|
}
|
|
3291
4142
|
const table = tensor.data;
|
|
4143
|
+
function pooledIds(text) {
|
|
4144
|
+
const ids = [];
|
|
4145
|
+
for (const id of tokenizer.encode(text)) {
|
|
4146
|
+
if (tokenizer.specialIds.has(id) && id !== tokenizer.unkId) continue;
|
|
4147
|
+
ids.push(id);
|
|
4148
|
+
}
|
|
4149
|
+
return ids;
|
|
4150
|
+
}
|
|
3292
4151
|
return {
|
|
3293
4152
|
id: basename4(modelDir),
|
|
3294
4153
|
dim,
|
|
3295
4154
|
embed(text) {
|
|
3296
4155
|
const out = new Float32Array(dim);
|
|
3297
|
-
|
|
3298
|
-
for (const id of
|
|
3299
|
-
if (tokenizer.specialIds.has(id) && id !== tokenizer.unkId) continue;
|
|
4156
|
+
const ids = pooledIds(text);
|
|
4157
|
+
for (const id of ids) {
|
|
3300
4158
|
const base = id * dim;
|
|
3301
4159
|
for (let j = 0; j < dim; j++) {
|
|
3302
4160
|
out[j] = out[j] + table[base + j];
|
|
3303
4161
|
}
|
|
3304
|
-
pooled++;
|
|
3305
4162
|
}
|
|
4163
|
+
const pooled = ids.length;
|
|
3306
4164
|
if (pooled === 0) return out;
|
|
3307
4165
|
let sumSquares = 0;
|
|
3308
4166
|
for (let j = 0; j < dim; j++) {
|
|
@@ -3315,27 +4173,121 @@ async function loadModel2Vec(modelDir) {
|
|
|
3315
4173
|
for (let j = 0; j < dim; j++) out[j] = out[j] / norm;
|
|
3316
4174
|
}
|
|
3317
4175
|
return out;
|
|
4176
|
+
},
|
|
4177
|
+
tokenEmbed(text) {
|
|
4178
|
+
const ids = pooledIds(text);
|
|
4179
|
+
const vectors = new Float32Array(ids.length * dim);
|
|
4180
|
+
for (let t = 0; t < ids.length; t++) {
|
|
4181
|
+
writeNormalizedRow(ids[t], vectors, t * dim);
|
|
4182
|
+
}
|
|
4183
|
+
return { ids, dim, vectors };
|
|
4184
|
+
},
|
|
4185
|
+
tokenIds: pooledIds,
|
|
4186
|
+
tokenVector(id) {
|
|
4187
|
+
if (!Number.isInteger(id) || id < 0 || id >= tokenizer.vocabSize) {
|
|
4188
|
+
throw new Error(`model2vec: token id ${id} outside vocab [0, ${tokenizer.vocabSize})`);
|
|
4189
|
+
}
|
|
4190
|
+
const out = new Float32Array(dim);
|
|
4191
|
+
writeNormalizedRow(id, out, 0);
|
|
4192
|
+
return out;
|
|
3318
4193
|
}
|
|
3319
4194
|
};
|
|
4195
|
+
function writeNormalizedRow(id, out, offset) {
|
|
4196
|
+
const base = id * dim;
|
|
4197
|
+
let sumSquares = 0;
|
|
4198
|
+
for (let j = 0; j < dim; j++) {
|
|
4199
|
+
const x = table[base + j];
|
|
4200
|
+
out[offset + j] = x;
|
|
4201
|
+
sumSquares += x * x;
|
|
4202
|
+
}
|
|
4203
|
+
const norm = Math.sqrt(sumSquares);
|
|
4204
|
+
if (norm > 0) {
|
|
4205
|
+
for (let j = 0; j < dim; j++) {
|
|
4206
|
+
out[offset + j] = out[offset + j] / norm;
|
|
4207
|
+
}
|
|
4208
|
+
}
|
|
4209
|
+
}
|
|
4210
|
+
}
|
|
4211
|
+
|
|
4212
|
+
// ../core/src/embed/pooling.ts
|
|
4213
|
+
var PoolAccumulator = class {
|
|
4214
|
+
top = Number.NEGATIVE_INFINITY;
|
|
4215
|
+
second = Number.NEGATIVE_INFINITY;
|
|
4216
|
+
bestChunk = 0;
|
|
4217
|
+
count = 0;
|
|
4218
|
+
/** Σ w_i and Σ s_i·w_i with w_i = exp((s_i − 1) / τ); shift keeps exp() ≤ 1. */
|
|
4219
|
+
sumW = 0;
|
|
4220
|
+
sumSW = 0;
|
|
4221
|
+
invTemp;
|
|
4222
|
+
constructor(pooling) {
|
|
4223
|
+
this.invTemp = typeof pooling === "object" && pooling.kind === "softmax" ? 1 / pooling.temperature : 0;
|
|
4224
|
+
}
|
|
4225
|
+
add(score, chunkIndex) {
|
|
4226
|
+
this.count++;
|
|
4227
|
+
if (score > this.top) {
|
|
4228
|
+
this.second = this.top;
|
|
4229
|
+
this.top = score;
|
|
4230
|
+
this.bestChunk = chunkIndex;
|
|
4231
|
+
} else if (score > this.second) {
|
|
4232
|
+
this.second = score;
|
|
4233
|
+
}
|
|
4234
|
+
if (this.invTemp > 0) {
|
|
4235
|
+
const w = Math.exp((score - 1) * this.invTemp);
|
|
4236
|
+
this.sumW += w;
|
|
4237
|
+
this.sumSW += score * w;
|
|
4238
|
+
}
|
|
4239
|
+
}
|
|
4240
|
+
pool(pooling) {
|
|
4241
|
+
if (this.count === 0) return Number.NEGATIVE_INFINITY;
|
|
4242
|
+
if (pooling === "max") return this.top;
|
|
4243
|
+
if (pooling === "top2mean") {
|
|
4244
|
+
return this.second === Number.NEGATIVE_INFINITY ? this.top : (this.top + this.second) / 2;
|
|
4245
|
+
}
|
|
4246
|
+
if (pooling.kind === "logdiscount") return this.top - pooling.lambda * Math.log(this.count);
|
|
4247
|
+
return this.sumW > 0 ? this.sumSW / this.sumW : this.top;
|
|
4248
|
+
}
|
|
4249
|
+
};
|
|
4250
|
+
function assertValidPooling(p) {
|
|
4251
|
+
const raw = p;
|
|
4252
|
+
if (typeof p === "string") {
|
|
4253
|
+
if (p !== "max" && p !== "top2mean") throw new Error(`pooling: unknown kind "${String(raw)}"`);
|
|
4254
|
+
return;
|
|
4255
|
+
}
|
|
4256
|
+
if (p.kind === "logdiscount") {
|
|
4257
|
+
if (!(p.lambda >= 0))
|
|
4258
|
+
throw new Error(`pooling: logdiscount lambda must be \u2265 0 (got ${p.lambda})`);
|
|
4259
|
+
} else if (p.kind === "softmax") {
|
|
4260
|
+
if (!(p.temperature > 0)) {
|
|
4261
|
+
throw new Error(`pooling: softmax temperature must be > 0 (got ${p.temperature})`);
|
|
4262
|
+
}
|
|
4263
|
+
} else {
|
|
4264
|
+
const kind = typeof raw === "object" && raw !== null ? raw.kind : raw;
|
|
4265
|
+
throw new Error(`pooling: unknown kind "${String(kind)}"`);
|
|
4266
|
+
}
|
|
4267
|
+
}
|
|
4268
|
+
|
|
4269
|
+
// ../core/src/embed/types.ts
|
|
4270
|
+
function isTokenEmbedder(e) {
|
|
4271
|
+
return typeof e.tokenEmbed === "function";
|
|
3320
4272
|
}
|
|
3321
4273
|
|
|
3322
4274
|
// src/semantic/cache.ts
|
|
3323
4275
|
import { createHash as createHash2 } from "crypto";
|
|
3324
|
-
import { mkdir as
|
|
3325
|
-
import { join as
|
|
4276
|
+
import { mkdir as mkdir8, readFile as readFile17, rename as rename5, writeFile as writeFile3 } from "fs/promises";
|
|
4277
|
+
import { join as join10 } from "path";
|
|
3326
4278
|
var CACHE_VERSION = 2;
|
|
3327
4279
|
function cachePathsFor(cacheDir, vaultRoot2, modelId) {
|
|
3328
4280
|
const vaultKey = createHash2("sha256").update(vaultRoot2).digest("hex").slice(0, 16);
|
|
3329
|
-
const dir =
|
|
3330
|
-
return { dir, bin:
|
|
4281
|
+
const dir = join10(cacheDir, "embeddings", vaultKey);
|
|
4282
|
+
return { dir, bin: join10(dir, `${modelId}.bin`), manifest: join10(dir, `${modelId}.json`) };
|
|
3331
4283
|
}
|
|
3332
4284
|
async function loadCache(paths, modelId, dim) {
|
|
3333
4285
|
try {
|
|
3334
|
-
const manifest = JSON.parse(await
|
|
4286
|
+
const manifest = JSON.parse(await readFile17(paths.manifest, "utf8"));
|
|
3335
4287
|
if (manifest.version !== CACHE_VERSION || manifest.modelId !== modelId || manifest.dim !== dim) {
|
|
3336
4288
|
return void 0;
|
|
3337
4289
|
}
|
|
3338
|
-
const buf = await
|
|
4290
|
+
const buf = await readFile17(paths.bin);
|
|
3339
4291
|
const totalChunks = manifest.entries.reduce((a, e) => a + e.chunks, 0);
|
|
3340
4292
|
const vectorBytes = totalChunks * dim * 4;
|
|
3341
4293
|
const spanBytes = totalChunks * 2 * 4;
|
|
@@ -3385,26 +4337,64 @@ async function saveCache(paths, modelId, dim, notes2, hashes) {
|
|
|
3385
4337
|
spanOffset += note.spans.length;
|
|
3386
4338
|
}
|
|
3387
4339
|
const manifest = { version: CACHE_VERSION, modelId, dim, entries };
|
|
3388
|
-
await
|
|
4340
|
+
await mkdir8(paths.dir, { recursive: true });
|
|
3389
4341
|
await atomicWriteBytes(paths.bin, bin);
|
|
3390
4342
|
await atomicWriteBytes(paths.manifest, new TextEncoder().encode(JSON.stringify(manifest)));
|
|
3391
4343
|
}
|
|
3392
4344
|
async function atomicWriteBytes(absPath, bytes) {
|
|
3393
4345
|
const tmpPath = `${absPath}.seekstone-cache-tmp`;
|
|
3394
4346
|
await writeFile3(tmpPath, bytes);
|
|
3395
|
-
await
|
|
4347
|
+
await rename5(tmpPath, absPath);
|
|
4348
|
+
}
|
|
4349
|
+
|
|
4350
|
+
// src/semantic/state.ts
|
|
4351
|
+
init_config();
|
|
4352
|
+
|
|
4353
|
+
// src/semantic/rerank.ts
|
|
4354
|
+
var MAXSIM_BETA = 0.5;
|
|
4355
|
+
var EMPTY = [];
|
|
4356
|
+
function maxsimRerankHits(embedder, query, hits, tokenIdsFor, beta = MAXSIM_BETA) {
|
|
4357
|
+
if (hits.length < 2 || beta <= 0) return [...hits];
|
|
4358
|
+
const qTok = embedder.tokenEmbed(query);
|
|
4359
|
+
if (qTok.ids.length === 0) return [...hits];
|
|
4360
|
+
const docs = hits.map((h) => tokenIdsFor(h) ?? EMPTY);
|
|
4361
|
+
const weights = candidateSetIdf(qTok.ids, docs);
|
|
4362
|
+
const maxsim = maxsimScoreTokens(qTok, docs, (id) => embedder.tokenVector(id), {
|
|
4363
|
+
aggregate: "mean",
|
|
4364
|
+
weights
|
|
4365
|
+
});
|
|
4366
|
+
const stage1 = normalize(hits.map((h) => h.score));
|
|
4367
|
+
const rerank = normalize(maxsim);
|
|
4368
|
+
return hits.map((h, i) => ({
|
|
4369
|
+
...h,
|
|
4370
|
+
score: (1 - beta) * stage1[i] + beta * rerank[i]
|
|
4371
|
+
})).sort((a, b) => b.score - a.score || (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
|
|
4372
|
+
}
|
|
4373
|
+
function normalize(scores) {
|
|
4374
|
+
let min = Number.POSITIVE_INFINITY;
|
|
4375
|
+
let max = Number.NEGATIVE_INFINITY;
|
|
4376
|
+
for (const s of scores) {
|
|
4377
|
+
if (s < min) min = s;
|
|
4378
|
+
if (s > max) max = s;
|
|
4379
|
+
}
|
|
4380
|
+
const span = max - min;
|
|
4381
|
+
return scores.map((s) => span > 0 ? (s - min) / span : 1);
|
|
3396
4382
|
}
|
|
3397
4383
|
|
|
3398
4384
|
// src/semantic/store.ts
|
|
4385
|
+
var DEFAULT_POOLING = "max";
|
|
3399
4386
|
var SemanticStore = class {
|
|
3400
4387
|
dim;
|
|
4388
|
+
pooling;
|
|
3401
4389
|
notes = /* @__PURE__ */ new Map();
|
|
3402
4390
|
chunks = 0;
|
|
3403
|
-
constructor(dim) {
|
|
4391
|
+
constructor(dim, pooling = DEFAULT_POOLING) {
|
|
3404
4392
|
if (!Number.isInteger(dim) || dim <= 0) {
|
|
3405
4393
|
throw new Error(`semantic store: invalid dim ${dim}`);
|
|
3406
4394
|
}
|
|
4395
|
+
assertValidPooling(pooling);
|
|
3407
4396
|
this.dim = dim;
|
|
4397
|
+
this.pooling = pooling;
|
|
3408
4398
|
}
|
|
3409
4399
|
/** Total chunk vectors across all notes. */
|
|
3410
4400
|
get chunkCount() {
|
|
@@ -3444,38 +4434,55 @@ var SemanticStore = class {
|
|
|
3444
4434
|
entries() {
|
|
3445
4435
|
return this.notes.entries();
|
|
3446
4436
|
}
|
|
3447
|
-
/**
|
|
4437
|
+
/**
|
|
4438
|
+
* Top `k` notes by pooled chunk cosine, ties broken path-ascending. The
|
|
4439
|
+
* returned chunk/span is always the note's single best chunk, whatever
|
|
4440
|
+
* pooling ranked the note.
|
|
4441
|
+
*/
|
|
3448
4442
|
topNotes(query, k) {
|
|
3449
4443
|
if (query.length !== this.dim) {
|
|
3450
4444
|
throw new Error(`semantic store: query dim ${query.length} does not match ${this.dim}`);
|
|
3451
4445
|
}
|
|
3452
|
-
const dim = this.dim;
|
|
3453
4446
|
const hits = [];
|
|
3454
|
-
for (const [path2,
|
|
3455
|
-
|
|
3456
|
-
let bestChunk = 0;
|
|
3457
|
-
const n = packed.length / dim;
|
|
3458
|
-
for (let c = 0; c < n; c++) {
|
|
3459
|
-
const base = c * dim;
|
|
3460
|
-
let score = 0;
|
|
3461
|
-
for (let j = 0; j < dim; j++) {
|
|
3462
|
-
score += packed[base + j] * query[j];
|
|
3463
|
-
}
|
|
3464
|
-
if (score > best) {
|
|
3465
|
-
best = score;
|
|
3466
|
-
bestChunk = c;
|
|
3467
|
-
}
|
|
3468
|
-
}
|
|
3469
|
-
hits.push({
|
|
3470
|
-
path: path2,
|
|
3471
|
-
score: best,
|
|
3472
|
-
chunkIndex: bestChunk,
|
|
3473
|
-
start: spans[bestChunk * 2],
|
|
3474
|
-
end: spans[bestChunk * 2 + 1]
|
|
3475
|
-
});
|
|
4447
|
+
for (const [path2, vectors] of this.notes) {
|
|
4448
|
+
hits.push(this.scoreVectors(path2, vectors, query));
|
|
3476
4449
|
}
|
|
3477
4450
|
return hits.sort((a, b) => b.score - a.score || (a.path < b.path ? -1 : a.path > b.path ? 1 : 0)).slice(0, Math.max(0, k));
|
|
3478
4451
|
}
|
|
4452
|
+
/**
|
|
4453
|
+
* Score a single note's cached chunk vectors against the query (SHA-315
|
|
4454
|
+
* graph expansion gates candidates on this). Same pooling and best-chunk
|
|
4455
|
+
* span as topNotes; undefined when the note has no vectors.
|
|
4456
|
+
*/
|
|
4457
|
+
scoreNote(path2, query) {
|
|
4458
|
+
if (query.length !== this.dim) {
|
|
4459
|
+
throw new Error(`semantic store: query dim ${query.length} does not match ${this.dim}`);
|
|
4460
|
+
}
|
|
4461
|
+
const vectors = this.notes.get(path2);
|
|
4462
|
+
return vectors === void 0 ? void 0 : this.scoreVectors(path2, vectors, query);
|
|
4463
|
+
}
|
|
4464
|
+
scoreVectors(path2, { packed, spans }, query) {
|
|
4465
|
+
const dim = this.dim;
|
|
4466
|
+
const pooling = this.pooling;
|
|
4467
|
+
const acc = new PoolAccumulator(pooling);
|
|
4468
|
+
const n = packed.length / dim;
|
|
4469
|
+
for (let c = 0; c < n; c++) {
|
|
4470
|
+
const base = c * dim;
|
|
4471
|
+
let score = 0;
|
|
4472
|
+
for (let j = 0; j < dim; j++) {
|
|
4473
|
+
score += packed[base + j] * query[j];
|
|
4474
|
+
}
|
|
4475
|
+
acc.add(score, c);
|
|
4476
|
+
}
|
|
4477
|
+
const bestChunk = acc.bestChunk;
|
|
4478
|
+
return {
|
|
4479
|
+
path: path2,
|
|
4480
|
+
score: acc.pool(pooling),
|
|
4481
|
+
chunkIndex: bestChunk,
|
|
4482
|
+
start: spans[bestChunk * 2],
|
|
4483
|
+
end: spans[bestChunk * 2 + 1]
|
|
4484
|
+
};
|
|
4485
|
+
}
|
|
3479
4486
|
};
|
|
3480
4487
|
|
|
3481
4488
|
// src/semantic/state.ts
|
|
@@ -3490,11 +4497,21 @@ var Semantic = class _Semantic {
|
|
|
3490
4497
|
saveDebounceMs;
|
|
3491
4498
|
yieldEvery;
|
|
3492
4499
|
hashes = /* @__PURE__ */ new Map();
|
|
4500
|
+
/**
|
|
4501
|
+
* Lazy per-note chunk token ids for the MaxSim rerank (SHA-314): filled on
|
|
4502
|
+
* first rerank touch, dropped whenever the note changes or is removed.
|
|
4503
|
+
* Only queried candidates are ever tokenized, so the memo stays a small
|
|
4504
|
+
* fraction of the vault; it lives in memory only — the cache file format
|
|
4505
|
+
* is untouched.
|
|
4506
|
+
*/
|
|
4507
|
+
tokenIdCache = /* @__PURE__ */ new Map();
|
|
4508
|
+
tokenEmbedder;
|
|
3493
4509
|
pending = /* @__PURE__ */ new Map();
|
|
3494
4510
|
saveTimer;
|
|
3495
4511
|
stopped = false;
|
|
3496
4512
|
constructor(embedder, ctx2, cfg, deps) {
|
|
3497
4513
|
this.embedder = embedder;
|
|
4514
|
+
this.tokenEmbedder = isTokenEmbedder(embedder) ? embedder : void 0;
|
|
3498
4515
|
this.store = new SemanticStore(embedder.dim);
|
|
3499
4516
|
this.ctx = ctx2;
|
|
3500
4517
|
this.paths = cachePathsFor(cfg.cacheDir, ctx2.vaultRoot, embedder.id);
|
|
@@ -3516,7 +4533,7 @@ var Semantic = class _Semantic {
|
|
|
3516
4533
|
} catch (err) {
|
|
3517
4534
|
const reason = err instanceof Error ? err.message : String(err);
|
|
3518
4535
|
throw new Error(
|
|
3519
|
-
`semantic search: could not load the embedding model from ${cfg.modelDir} \u2014 run \`
|
|
4536
|
+
`semantic search: could not load the embedding model ${cfg.modelId} from ${cfg.modelDir} \u2014 run \`${fetchCommandFor(cfg.modelId)}\` to download it, or point SEEKSTONE_MODEL_PATH at a Model2Vec model directory (${reason})`
|
|
3520
4537
|
);
|
|
3521
4538
|
}
|
|
3522
4539
|
const semantic = new _Semantic(embedder, ctx2, cfg, deps);
|
|
@@ -3591,9 +4608,35 @@ var Semantic = class _Semantic {
|
|
|
3591
4608
|
embedQuery(query) {
|
|
3592
4609
|
return this.embedder.embed(query);
|
|
3593
4610
|
}
|
|
4611
|
+
/**
|
|
4612
|
+
* MaxSim-rerank stage-1 candidates (SHA-314). Pass-through when the
|
|
4613
|
+
* embedder exposes no token vectors (alternative runtimes) — semantic
|
|
4614
|
+
* search then ships stage-1 order unchanged.
|
|
4615
|
+
*/
|
|
4616
|
+
rerank(query, hits) {
|
|
4617
|
+
const embedder = this.tokenEmbedder;
|
|
4618
|
+
if (!embedder) return [...hits];
|
|
4619
|
+
return maxsimRerankHits(embedder, query, hits, (hit) => {
|
|
4620
|
+
const ids = this.chunkTokenIds(hit.path);
|
|
4621
|
+
return ids?.[hit.chunkIndex];
|
|
4622
|
+
});
|
|
4623
|
+
}
|
|
4624
|
+
chunkTokenIds(path2) {
|
|
4625
|
+
const cached = this.tokenIdCache.get(path2);
|
|
4626
|
+
if (cached) return cached;
|
|
4627
|
+
const embedder = this.tokenEmbedder;
|
|
4628
|
+
const note = this.ctx.notes.get(path2);
|
|
4629
|
+
if (!embedder || !note) return void 0;
|
|
4630
|
+
const ids = chunkNote(note.title, note.body).map(
|
|
4631
|
+
(chunk) => Uint32Array.from(embedder.tokenIds(chunk.text))
|
|
4632
|
+
);
|
|
4633
|
+
this.tokenIdCache.set(path2, ids);
|
|
4634
|
+
return ids;
|
|
4635
|
+
}
|
|
3594
4636
|
/** Watcher hook: a note was added or changed (ctx.notes is already fresh). */
|
|
3595
4637
|
noteChanged(path2) {
|
|
3596
4638
|
if (this.stopped) return;
|
|
4639
|
+
this.tokenIdCache.delete(path2);
|
|
3597
4640
|
const existing = this.pending.get(path2);
|
|
3598
4641
|
if (existing) clearTimeout(existing);
|
|
3599
4642
|
const timer = setTimeout(() => {
|
|
@@ -3610,6 +4653,7 @@ var Semantic = class _Semantic {
|
|
|
3610
4653
|
clearTimeout(existing);
|
|
3611
4654
|
this.pending.delete(path2);
|
|
3612
4655
|
}
|
|
4656
|
+
this.tokenIdCache.delete(path2);
|
|
3613
4657
|
if (this.hashes.has(path2) || this.store.getNote(path2)) {
|
|
3614
4658
|
this.store.removeNote(path2);
|
|
3615
4659
|
this.hashes.delete(path2);
|
|
@@ -4179,6 +5223,42 @@ var ALL_TOOLS = [
|
|
|
4179
5223
|
},
|
|
4180
5224
|
required: ["content"]
|
|
4181
5225
|
}
|
|
5226
|
+
},
|
|
5227
|
+
{
|
|
5228
|
+
name: "list_writes",
|
|
5229
|
+
annotations: READ_ONLY_ANNOTATIONS,
|
|
5230
|
+
description: "List recent writes from the journal \u2014 every write tool records the pre-image of each file it touches, so any of them can be reverted with undo_write. Returns compact metadata rows (seq, timestamp, tool, paths, undoable), never note content.",
|
|
5231
|
+
inputSchema: {
|
|
5232
|
+
type: "object",
|
|
5233
|
+
properties: {
|
|
5234
|
+
limit: {
|
|
5235
|
+
type: "number",
|
|
5236
|
+
description: "Max entries to return, newest first (1\u2013200, default 20)."
|
|
5237
|
+
},
|
|
5238
|
+
path: {
|
|
5239
|
+
type: "string",
|
|
5240
|
+
description: "Only entries that touched this vault-relative path."
|
|
5241
|
+
}
|
|
5242
|
+
}
|
|
5243
|
+
}
|
|
5244
|
+
},
|
|
5245
|
+
{
|
|
5246
|
+
name: "undo_write",
|
|
5247
|
+
annotations: DESTRUCTIVE_WRITE_ANNOTATIONS,
|
|
5248
|
+
description: "Revert a journaled write: restores every file it touched to its byte-identical pre-write state (a multi-file move or heading rename is restored whole; a delete is restored even if it was permanent). Defaults to the most recent undoable write. Refuses with undo_conflict if a file changed since that write, unless force: true. The undo is itself journaled \u2014 undo it to redo.",
|
|
5249
|
+
inputSchema: {
|
|
5250
|
+
type: "object",
|
|
5251
|
+
properties: {
|
|
5252
|
+
seq: {
|
|
5253
|
+
type: "number",
|
|
5254
|
+
description: "Journal entry to revert (from list_writes). Defaults to the most recent undoable write."
|
|
5255
|
+
},
|
|
5256
|
+
force: {
|
|
5257
|
+
type: "boolean",
|
|
5258
|
+
description: "Restore even if a file changed after the journaled write; the clobbered state is journaled first so nothing is lost. Defaults to false."
|
|
5259
|
+
}
|
|
5260
|
+
}
|
|
5261
|
+
}
|
|
4182
5262
|
}
|
|
4183
5263
|
];
|
|
4184
5264
|
function visibleTools(policy2) {
|
|
@@ -4188,8 +5268,8 @@ function visibleTools(policy2) {
|
|
|
4188
5268
|
|
|
4189
5269
|
// src/watcher.ts
|
|
4190
5270
|
import { realpathSync } from "fs";
|
|
4191
|
-
import { readFile as
|
|
4192
|
-
import { join as
|
|
5271
|
+
import { readFile as readFile18 } from "fs/promises";
|
|
5272
|
+
import { join as join11, relative as relative2, sep as sep3 } from "path";
|
|
4193
5273
|
import chokidar from "chokidar";
|
|
4194
5274
|
var DEFAULT_POLL_INTERVAL_MS = 1e4;
|
|
4195
5275
|
function resolvePollInterval(optValue, envValue, log2) {
|
|
@@ -4220,7 +5300,7 @@ function startWatcher(ctx2, log2, opts) {
|
|
|
4220
5300
|
const toRel = (abs) => relative2(vaultRoot2, abs).split(sep3).join("/");
|
|
4221
5301
|
async function upsert(relPath, op) {
|
|
4222
5302
|
try {
|
|
4223
|
-
const raw = await
|
|
5303
|
+
const raw = await readFile18(join11(vaultRoot2, relPath), "utf8");
|
|
4224
5304
|
removeNoteBacklinks(ctx2, relPath);
|
|
4225
5305
|
upsertDoc(ctx2, buildDoc(relPath, raw));
|
|
4226
5306
|
addNoteBacklinks(ctx2, relPath, raw);
|
|
@@ -4274,7 +5354,7 @@ function startWatcher(ctx2, log2, opts) {
|
|
|
4274
5354
|
}
|
|
4275
5355
|
|
|
4276
5356
|
// src/index.ts
|
|
4277
|
-
var VERSION = true ? "0.
|
|
5357
|
+
var VERSION = true ? "0.17.1" : "0.0.0-dev";
|
|
4278
5358
|
var intent = parseCliIntent(process.argv.slice(2));
|
|
4279
5359
|
if (intent === "version") {
|
|
4280
5360
|
process.stdout.write(`${VERSION}
|
|
@@ -4305,7 +5385,11 @@ if (intent === "init") {
|
|
|
4305
5385
|
if (intent === "fetch-model") {
|
|
4306
5386
|
const { runFetchModel: runFetchModel2 } = await Promise.resolve().then(() => (init_fetch_model(), fetch_model_exports));
|
|
4307
5387
|
const { homedir: homedir2 } = await import("os");
|
|
4308
|
-
const result = await runFetchModel2({
|
|
5388
|
+
const result = await runFetchModel2({
|
|
5389
|
+
env: process.env,
|
|
5390
|
+
homedir: homedir2(),
|
|
5391
|
+
argv: process.argv.slice(3)
|
|
5392
|
+
});
|
|
4309
5393
|
process.stdout.write(`${result.output.join("\n")}
|
|
4310
5394
|
`);
|
|
4311
5395
|
process.exit(result.exitCode);
|
|
@@ -4325,6 +5409,33 @@ log.info("building index", { vault: vaultRoot });
|
|
|
4325
5409
|
var { index, notes, backlinks, buildMs } = await buildIndex(vaultRoot);
|
|
4326
5410
|
log.info("index ready", { notes: notes.size, buildMs });
|
|
4327
5411
|
var ctx = { vaultRoot, index, notes, backlinks, policy };
|
|
5412
|
+
var journalCfg = resolveJournalConfig(process.env);
|
|
5413
|
+
if (journalCfg && !policy.readOnly) {
|
|
5414
|
+
try {
|
|
5415
|
+
ctx.journal = await Journal.open(vaultRoot, journalCfg, { log });
|
|
5416
|
+
log.info("write journal ready", {
|
|
5417
|
+
entries: ctx.journal.list({ limit: 1 }).total,
|
|
5418
|
+
maxBytes: journalCfg.maxBytes
|
|
5419
|
+
});
|
|
5420
|
+
} catch (err) {
|
|
5421
|
+
log.error(`write journal failed to open: ${err instanceof Error ? err.message : String(err)}`);
|
|
5422
|
+
process.exit(1);
|
|
5423
|
+
}
|
|
5424
|
+
} else if (!journalCfg) {
|
|
5425
|
+
log.info("write journal disabled", { env: "SEEKSTONE_HISTORY" });
|
|
5426
|
+
}
|
|
5427
|
+
var auditCfg = resolveAuditConfig(process.env);
|
|
5428
|
+
if (auditCfg) {
|
|
5429
|
+
try {
|
|
5430
|
+
ctx.audit = AuditLog.open(auditCfg);
|
|
5431
|
+
log.info("audit log ready", { file: auditCfg.path, maxBytes: auditCfg.maxBytes });
|
|
5432
|
+
} catch (err) {
|
|
5433
|
+
log.error(
|
|
5434
|
+
`audit log ${auditCfg.path} is not writable: ${err instanceof Error ? err.message : String(err)}`
|
|
5435
|
+
);
|
|
5436
|
+
process.exit(1);
|
|
5437
|
+
}
|
|
5438
|
+
}
|
|
4328
5439
|
var semanticCfg = resolveSemanticConfig(process.env, homedir());
|
|
4329
5440
|
if (semanticCfg) {
|
|
4330
5441
|
try {
|