seekstone 0.15.2 → 0.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +30 -5
- package/dist/index.js +1530 -399
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
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
|
|
|
@@ -173,7 +434,9 @@ Optional environment:
|
|
|
173
434
|
SEEKSTONE_LOG_LEVEL error | warn | info (default) | debug
|
|
174
435
|
SEEKSTONE_LOG_FILE Absolute path; append JSON-line logs here
|
|
175
436
|
SEEKSTONE_WATCH_POLL Set to 1 to poll for changes (network drives, WSL)
|
|
437
|
+
SEEKSTONE_WATCH_POLL_INTERVAL Poll interval in ms (default 10000)
|
|
176
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)
|
|
177
440
|
SEEKSTONE_MODEL_PATH Override the embedding-model directory
|
|
178
441
|
SEEKSTONE_CACHE_DIR Override the cache root (default ~/.cache/seekstone)
|
|
179
442
|
|
|
@@ -203,7 +466,7 @@ Docs: https://github.com/shaqmughal/seekstone`;
|
|
|
203
466
|
}
|
|
204
467
|
|
|
205
468
|
// src/tools/append_note.ts
|
|
206
|
-
import { readFile } from "fs/promises";
|
|
469
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
207
470
|
|
|
208
471
|
// ../core/src/frontmatter.ts
|
|
209
472
|
import { parse as parseYaml } from "yaml";
|
|
@@ -299,6 +562,271 @@ function assertHashMatch(raw, prevHash, path2) {
|
|
|
299
562
|
}
|
|
300
563
|
}
|
|
301
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
|
+
|
|
302
830
|
// src/policy.ts
|
|
303
831
|
import picomatch from "picomatch";
|
|
304
832
|
function parseWritePolicy(env) {
|
|
@@ -351,7 +879,7 @@ var AppendNoteInput = z.object({
|
|
|
351
879
|
async function appendNote(ctx2, input) {
|
|
352
880
|
const absPath = resolveVaultPath(ctx2.vaultRoot, input.path);
|
|
353
881
|
assertWritable(ctx2.policy, input.path);
|
|
354
|
-
const original = await
|
|
882
|
+
const original = await readFile2(absPath, "utf8");
|
|
355
883
|
if (input.prevHash !== void 0) assertHashMatch(original, input.prevHash, input.path);
|
|
356
884
|
const fm = parseFrontmatter(original);
|
|
357
885
|
const body = fm.body.endsWith("\n") ? fm.body : `${fm.body}
|
|
@@ -360,6 +888,7 @@ async function appendNote(ctx2, input) {
|
|
|
360
888
|
const newBody = `${body}${separator}${input.content}`;
|
|
361
889
|
const header = original.slice(0, fm.bodyStart);
|
|
362
890
|
const newContent = `${header}${newBody}`;
|
|
891
|
+
await journalWrite(ctx2, "append_note", input.path, original, newContent);
|
|
363
892
|
await atomicWrite(absPath, newContent);
|
|
364
893
|
const cached = ctx2.notes.get(input.path);
|
|
365
894
|
if (cached) {
|
|
@@ -610,7 +1139,10 @@ function lexicalSearch(ctx2, input) {
|
|
|
610
1139
|
}
|
|
611
1140
|
function semanticSearch(ctx2, semantic, input) {
|
|
612
1141
|
const queryVec = semantic.embedQuery(input.query);
|
|
613
|
-
const candidates = semantic.
|
|
1142
|
+
const candidates = semantic.rerank(
|
|
1143
|
+
input.query,
|
|
1144
|
+
semantic.store.topNotes(queryVec, SEMANTIC_CANDIDATES)
|
|
1145
|
+
);
|
|
614
1146
|
const terms = searchTerms(input.query);
|
|
615
1147
|
const hits = [];
|
|
616
1148
|
for (const c of candidates) {
|
|
@@ -838,7 +1370,7 @@ function contextPack(ctx2, input) {
|
|
|
838
1370
|
}
|
|
839
1371
|
|
|
840
1372
|
// src/tools/create_note.ts
|
|
841
|
-
import { access, mkdir, readFile as
|
|
1373
|
+
import { access, mkdir as mkdir2, readFile as readFile3 } from "fs/promises";
|
|
842
1374
|
import { dirname } from "path";
|
|
843
1375
|
import { stringify as stringifyYaml } from "yaml";
|
|
844
1376
|
import { z as z4 } from "zod";
|
|
@@ -887,6 +1419,7 @@ async function createNote(ctx2, rawInput) {
|
|
|
887
1419
|
const input = CreateNoteInput.parse(rawInput);
|
|
888
1420
|
const absPath = resolveVaultPath(ctx2.vaultRoot, input.path);
|
|
889
1421
|
assertWritable(ctx2.policy, input.path);
|
|
1422
|
+
let existing = null;
|
|
890
1423
|
if (!input.overwrite) {
|
|
891
1424
|
try {
|
|
892
1425
|
await access(absPath);
|
|
@@ -894,22 +1427,26 @@ async function createNote(ctx2, rawInput) {
|
|
|
894
1427
|
} catch (err) {
|
|
895
1428
|
if (err.code !== "ENOENT") throw err;
|
|
896
1429
|
}
|
|
897
|
-
} else
|
|
898
|
-
let existing;
|
|
1430
|
+
} else {
|
|
899
1431
|
try {
|
|
900
|
-
existing = await
|
|
901
|
-
} catch {
|
|
902
|
-
throw
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
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);
|
|
911
1449
|
}
|
|
912
|
-
assertHashMatch(existing, input.prevHash, input.path);
|
|
913
1450
|
}
|
|
914
1451
|
let raw = "";
|
|
915
1452
|
if (input.frontmatter && Object.keys(input.frontmatter).length > 0) {
|
|
@@ -918,7 +1455,8 @@ ${stringifyYaml(input.frontmatter, { lineWidth: 0 })}---
|
|
|
918
1455
|
`;
|
|
919
1456
|
}
|
|
920
1457
|
raw += input.content ?? "";
|
|
921
|
-
await
|
|
1458
|
+
await mkdir2(dirname(absPath), { recursive: true });
|
|
1459
|
+
await journalWrite(ctx2, "create_note", input.path, existing, raw);
|
|
922
1460
|
await atomicWrite(absPath, raw);
|
|
923
1461
|
upsertDoc(ctx2, buildDoc(input.path, raw));
|
|
924
1462
|
return {
|
|
@@ -929,8 +1467,8 @@ ${stringifyYaml(input.frontmatter, { lineWidth: 0 })}---
|
|
|
929
1467
|
}
|
|
930
1468
|
|
|
931
1469
|
// src/tools/delete_note.ts
|
|
932
|
-
import { access as access2, mkdir as
|
|
933
|
-
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";
|
|
934
1472
|
import { z as z5 } from "zod";
|
|
935
1473
|
|
|
936
1474
|
// src/index/backlinks.ts
|
|
@@ -981,7 +1519,7 @@ async function freeTrashPath(vaultRoot2, noteBase) {
|
|
|
981
1519
|
let candidate = `.trash/${noteBase}`;
|
|
982
1520
|
for (; ; ) {
|
|
983
1521
|
try {
|
|
984
|
-
await access2(
|
|
1522
|
+
await access2(join2(vaultRoot2, candidate));
|
|
985
1523
|
} catch {
|
|
986
1524
|
return candidate;
|
|
987
1525
|
}
|
|
@@ -992,15 +1530,16 @@ async function deleteNote(ctx2, rawInput) {
|
|
|
992
1530
|
const input = DeleteNoteInput.parse(rawInput);
|
|
993
1531
|
const absPath = resolveVaultPath(ctx2.vaultRoot, input.path);
|
|
994
1532
|
assertWritable(ctx2.policy, input.path);
|
|
995
|
-
const raw = await
|
|
1533
|
+
const raw = await readFile4(absPath, "utf8");
|
|
996
1534
|
if (input.prevHash !== void 0) assertHashMatch(raw, input.prevHash, input.path);
|
|
1535
|
+
await journalWrite(ctx2, "delete_note", input.path, raw, null);
|
|
997
1536
|
let trashedTo;
|
|
998
1537
|
if (input.permanent) {
|
|
999
|
-
await
|
|
1538
|
+
await rm2(absPath);
|
|
1000
1539
|
} else {
|
|
1001
|
-
await
|
|
1540
|
+
await mkdir3(join2(ctx2.vaultRoot, ".trash"), { recursive: true });
|
|
1002
1541
|
trashedTo = await freeTrashPath(ctx2.vaultRoot, basename2(input.path));
|
|
1003
|
-
await
|
|
1542
|
+
await rename3(absPath, join2(ctx2.vaultRoot, trashedTo));
|
|
1004
1543
|
}
|
|
1005
1544
|
removeNoteBacklinks(ctx2, input.path);
|
|
1006
1545
|
if (ctx2.notes.has(input.path)) {
|
|
@@ -1138,10 +1677,24 @@ function listTags(ctx2, input) {
|
|
|
1138
1677
|
return { tags, total: tags.length };
|
|
1139
1678
|
}
|
|
1140
1679
|
|
|
1141
|
-
// src/tools/
|
|
1142
|
-
import { access as access3, mkdir as mkdir3, readFile as readFile4, rename as rename3 } from "fs/promises";
|
|
1143
|
-
import { dirname as dirname2, join as join2 } from "path";
|
|
1680
|
+
// src/tools/list_writes.ts
|
|
1144
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";
|
|
1145
1698
|
|
|
1146
1699
|
// src/tools/rewrite_links.ts
|
|
1147
1700
|
import { posix } from "path";
|
|
@@ -1236,14 +1789,14 @@ function hasMarkdownLinkTo(raw, sourcePath, targetPath) {
|
|
|
1236
1789
|
}
|
|
1237
1790
|
|
|
1238
1791
|
// src/tools/move_note.ts
|
|
1239
|
-
var MoveNoteInput =
|
|
1240
|
-
from:
|
|
1241
|
-
to:
|
|
1242
|
-
overwrite:
|
|
1243
|
-
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(
|
|
1244
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."
|
|
1245
1798
|
),
|
|
1246
|
-
prevHash:
|
|
1799
|
+
prevHash: z11.string().optional().describe(
|
|
1247
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."
|
|
1248
1801
|
)
|
|
1249
1802
|
});
|
|
@@ -1255,20 +1808,19 @@ async function moveNote(ctx2, rawInput) {
|
|
|
1255
1808
|
assertWritable(ctx2.policy, input.to);
|
|
1256
1809
|
let raw;
|
|
1257
1810
|
try {
|
|
1258
|
-
raw = await
|
|
1811
|
+
raw = await readFile5(absFrom, "utf8");
|
|
1259
1812
|
} catch {
|
|
1260
1813
|
throw new Error(`Note not found: ${input.from}`);
|
|
1261
1814
|
}
|
|
1262
1815
|
if (input.prevHash !== void 0) assertHashMatch(raw, input.prevHash, input.from);
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
}
|
|
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.`);
|
|
1272
1824
|
}
|
|
1273
1825
|
const preNotes = new Map(ctx2.notes);
|
|
1274
1826
|
const candidates = /* @__PURE__ */ new Set();
|
|
@@ -1280,20 +1832,13 @@ async function moveNote(ctx2, rawInput) {
|
|
|
1280
1832
|
}
|
|
1281
1833
|
candidates.delete(input.from);
|
|
1282
1834
|
}
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
ctx2.index.discard(input.from);
|
|
1288
|
-
ctx2.notes.delete(input.from);
|
|
1289
|
-
}
|
|
1290
|
-
upsertDoc(ctx2, buildDoc(input.to, raw));
|
|
1291
|
-
addNoteBacklinks(ctx2, input.to, raw);
|
|
1292
|
-
let notesRewritten = 0;
|
|
1293
|
-
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 = [];
|
|
1294
1839
|
const skipped = [];
|
|
1295
1840
|
for (const path2 of [...candidates].sort()) {
|
|
1296
|
-
const doc =
|
|
1841
|
+
const doc = preNotes.get(path2);
|
|
1297
1842
|
if (doc === void 0) continue;
|
|
1298
1843
|
if (!isWritable(ctx2.policy, path2)) {
|
|
1299
1844
|
skipped.push(path2);
|
|
@@ -1305,11 +1850,34 @@ async function moveNote(ctx2, rawInput) {
|
|
|
1305
1850
|
input.from,
|
|
1306
1851
|
input.to,
|
|
1307
1852
|
preNotes,
|
|
1308
|
-
|
|
1853
|
+
postNotes
|
|
1309
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;
|
|
1310
1878
|
removeNoteBacklinks(ctx2, path2);
|
|
1311
1879
|
if (count > 0) {
|
|
1312
|
-
await atomicWrite(
|
|
1880
|
+
await atomicWrite(join3(ctx2.vaultRoot, path2), content);
|
|
1313
1881
|
upsertDoc(ctx2, buildDoc(path2, content));
|
|
1314
1882
|
notesRewritten++;
|
|
1315
1883
|
linksRewritten += count;
|
|
@@ -1331,7 +1899,7 @@ async function moveNote(ctx2, rawInput) {
|
|
|
1331
1899
|
}
|
|
1332
1900
|
|
|
1333
1901
|
// src/tools/outline_note.ts
|
|
1334
|
-
import { readFile as
|
|
1902
|
+
import { readFile as readFile6 } from "fs/promises";
|
|
1335
1903
|
|
|
1336
1904
|
// ../core/src/outline.ts
|
|
1337
1905
|
var HEADING_RE = /^(#{1,6})\s+(.+)/;
|
|
@@ -1381,15 +1949,15 @@ function buildOutline(raw, options = {}) {
|
|
|
1381
1949
|
}
|
|
1382
1950
|
|
|
1383
1951
|
// src/tools/outline_note.ts
|
|
1384
|
-
import { z as
|
|
1385
|
-
var OutlineNoteInput =
|
|
1386
|
-
path:
|
|
1387
|
-
includeBlocks:
|
|
1388
|
-
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.")
|
|
1389
1957
|
});
|
|
1390
1958
|
async function outlineNote(ctx2, input) {
|
|
1391
1959
|
const absPath = resolveVaultPath(ctx2.vaultRoot, input.path);
|
|
1392
|
-
const raw = await
|
|
1960
|
+
const raw = await readFile6(absPath, "utf8");
|
|
1393
1961
|
return buildOutline(raw, {
|
|
1394
1962
|
includeBlocks: input.includeBlocks,
|
|
1395
1963
|
includeSizes: input.includeSizes
|
|
@@ -1397,22 +1965,22 @@ async function outlineNote(ctx2, input) {
|
|
|
1397
1965
|
}
|
|
1398
1966
|
|
|
1399
1967
|
// src/tools/patch_frontmatter.ts
|
|
1400
|
-
import { readFile as
|
|
1968
|
+
import { readFile as readFile7 } from "fs/promises";
|
|
1401
1969
|
import { parseDocument as parseDocument2 } from "yaml";
|
|
1402
|
-
import { z as
|
|
1403
|
-
var PatchFrontmatterInput =
|
|
1404
|
-
path:
|
|
1405
|
-
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(
|
|
1406
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."
|
|
1407
1975
|
),
|
|
1408
|
-
prevHash:
|
|
1976
|
+
prevHash: z13.string().optional().describe(
|
|
1409
1977
|
"Optional compare-and-swap guard: the contentHash from a prior read. Fails with hash_conflict if the note changed since."
|
|
1410
1978
|
)
|
|
1411
1979
|
});
|
|
1412
1980
|
async function patchFrontmatter(ctx2, input) {
|
|
1413
1981
|
const absPath = resolveVaultPath(ctx2.vaultRoot, input.path);
|
|
1414
1982
|
assertWritable(ctx2.policy, input.path);
|
|
1415
|
-
const original = await
|
|
1983
|
+
const original = await readFile7(absPath, "utf8");
|
|
1416
1984
|
if (input.prevHash !== void 0) assertHashMatch(original, input.prevHash, input.path);
|
|
1417
1985
|
const fm = parseFrontmatter(original);
|
|
1418
1986
|
const keysChanged = [];
|
|
@@ -1455,6 +2023,7 @@ ${original}`;
|
|
|
1455
2023
|
const tail = opensWithCRLF ? "---\r\n" : "---\n";
|
|
1456
2024
|
newContent = `${head}${doc.toString({ lineWidth: 0 })}${tail}${fm.body}`;
|
|
1457
2025
|
}
|
|
2026
|
+
await journalWrite(ctx2, "patch_frontmatter", input.path, original, newContent);
|
|
1458
2027
|
await atomicWrite(absPath, newContent);
|
|
1459
2028
|
const cached = ctx2.notes.get(input.path);
|
|
1460
2029
|
if (cached) {
|
|
@@ -1472,22 +2041,22 @@ ${original}`;
|
|
|
1472
2041
|
}
|
|
1473
2042
|
|
|
1474
2043
|
// src/tools/patch_note.ts
|
|
1475
|
-
import { readFile as
|
|
1476
|
-
import { z as
|
|
1477
|
-
var PatchNoteInput =
|
|
1478
|
-
path:
|
|
1479
|
-
target:
|
|
1480
|
-
|
|
1481
|
-
|
|
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).") })
|
|
1482
2051
|
]).describe("Target section. Provide exactly one of heading or block."),
|
|
1483
|
-
operation:
|
|
2052
|
+
operation: z14.enum(["append", "prepend", "replace"]).describe(
|
|
1484
2053
|
"append: insert after section content. prepend: insert after heading line. replace: swap section content."
|
|
1485
2054
|
),
|
|
1486
|
-
content:
|
|
1487
|
-
createIfMissing:
|
|
2055
|
+
content: z14.string().min(1).describe("Content to insert or replace with."),
|
|
2056
|
+
createIfMissing: z14.boolean().default(false).describe(
|
|
1488
2057
|
"If the heading target is not found, append a new heading (level 2) + content. Only valid for heading targets."
|
|
1489
2058
|
),
|
|
1490
|
-
prevHash:
|
|
2059
|
+
prevHash: z14.string().optional().describe(
|
|
1491
2060
|
"Optional compare-and-swap guard: the contentHash from a prior read. Fails with hash_conflict if the note changed since."
|
|
1492
2061
|
)
|
|
1493
2062
|
});
|
|
@@ -1503,7 +2072,7 @@ function lineStart(raw, offset) {
|
|
|
1503
2072
|
async function patchNote(ctx2, input) {
|
|
1504
2073
|
const absPath = resolveVaultPath(ctx2.vaultRoot, input.path);
|
|
1505
2074
|
assertWritable(ctx2.policy, input.path);
|
|
1506
|
-
const raw = await
|
|
2075
|
+
const raw = await readFile8(absPath, "utf8");
|
|
1507
2076
|
if (input.prevHash !== void 0) assertHashMatch(raw, input.prevHash, input.path);
|
|
1508
2077
|
const fm = parseFrontmatter(raw);
|
|
1509
2078
|
const originalFmRegion = raw.slice(0, fm.bodyStart);
|
|
@@ -1521,6 +2090,7 @@ async function patchNote(ctx2, input) {
|
|
|
1521
2090
|
const newContent2 = `${raw}${sep4}## ${targetText}
|
|
1522
2091
|
|
|
1523
2092
|
${input.content}${contentNl}`;
|
|
2093
|
+
await journalWrite(ctx2, "patch_note", input.path, raw, newContent2);
|
|
1524
2094
|
await atomicWrite(absPath, newContent2);
|
|
1525
2095
|
await verifyFrontmatter(absPath, originalFmRegion);
|
|
1526
2096
|
updateCache(ctx2, input.path, newContent2, fm.bodyStart);
|
|
@@ -1591,6 +2161,7 @@ ${input.content}${contentNl}`;
|
|
|
1591
2161
|
default:
|
|
1592
2162
|
throw new Error(`Unknown operation: ${String(input.operation)}`);
|
|
1593
2163
|
}
|
|
2164
|
+
await journalWrite(ctx2, "patch_note", input.path, raw, newContent);
|
|
1594
2165
|
await atomicWrite(absPath, newContent);
|
|
1595
2166
|
await verifyFrontmatter(absPath, originalFmRegion);
|
|
1596
2167
|
updateCache(ctx2, input.path, newContent, fm.bodyStart);
|
|
@@ -1603,7 +2174,7 @@ ${input.content}${contentNl}`;
|
|
|
1603
2174
|
};
|
|
1604
2175
|
}
|
|
1605
2176
|
async function verifyFrontmatter(absPath, originalFmRegion) {
|
|
1606
|
-
const written = await
|
|
2177
|
+
const written = await readFile8(absPath, "utf8");
|
|
1607
2178
|
if (written.slice(0, originalFmRegion.length) !== originalFmRegion) {
|
|
1608
2179
|
throw new Error("Write-safety violation: frontmatter region changed unexpectedly");
|
|
1609
2180
|
}
|
|
@@ -1618,10 +2189,10 @@ function updateCache(ctx2, path2, newContent, bodyStart) {
|
|
|
1618
2189
|
}
|
|
1619
2190
|
|
|
1620
2191
|
// src/tools/periodic_note.ts
|
|
1621
|
-
import { access as
|
|
1622
|
-
import { dirname as dirname3, join as
|
|
1623
|
-
import { z as
|
|
1624
|
-
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"]);
|
|
1625
2196
|
var DEFAULTS = {
|
|
1626
2197
|
daily: { folder: "", format: "YYYY-MM-DD", template: "" },
|
|
1627
2198
|
weekly: { folder: "", format: "gggg-[W]ww", template: "" },
|
|
@@ -1631,7 +2202,7 @@ var DEFAULTS = {
|
|
|
1631
2202
|
};
|
|
1632
2203
|
async function readJson(absPath) {
|
|
1633
2204
|
try {
|
|
1634
|
-
return JSON.parse(await
|
|
2205
|
+
return JSON.parse(await readFile9(absPath, "utf8"));
|
|
1635
2206
|
} catch {
|
|
1636
2207
|
return null;
|
|
1637
2208
|
}
|
|
@@ -1639,7 +2210,7 @@ async function readJson(absPath) {
|
|
|
1639
2210
|
async function resolveConfig(vaultRoot2, period) {
|
|
1640
2211
|
const defaults = DEFAULTS[period];
|
|
1641
2212
|
if (period === "daily") {
|
|
1642
|
-
const cfg2 = await readJson(
|
|
2213
|
+
const cfg2 = await readJson(join4(vaultRoot2, ".obsidian", "daily-notes.json"));
|
|
1643
2214
|
return {
|
|
1644
2215
|
folder: cfg2?.folder ?? defaults.folder,
|
|
1645
2216
|
format: cfg2?.format ?? defaults.format,
|
|
@@ -1647,7 +2218,7 @@ async function resolveConfig(vaultRoot2, period) {
|
|
|
1647
2218
|
};
|
|
1648
2219
|
}
|
|
1649
2220
|
const cfg = await readJson(
|
|
1650
|
-
|
|
2221
|
+
join4(vaultRoot2, ".obsidian", "plugins", "periodic-notes", "data.json")
|
|
1651
2222
|
);
|
|
1652
2223
|
const entry = cfg?.[period];
|
|
1653
2224
|
return {
|
|
@@ -1686,12 +2257,12 @@ function resolveNotePath(cfg, date) {
|
|
|
1686
2257
|
const filename = `${formatMomentDate(date, cfg.format)}.md`;
|
|
1687
2258
|
return cfg.folder ? `${cfg.folder}/${filename}` : filename;
|
|
1688
2259
|
}
|
|
1689
|
-
var GetPeriodicNoteInput =
|
|
2260
|
+
var GetPeriodicNoteInput = z15.object({
|
|
1690
2261
|
period: PERIOD.default("daily").describe(
|
|
1691
2262
|
"Period type: daily (default), weekly, monthly, quarterly, or yearly."
|
|
1692
2263
|
),
|
|
1693
|
-
date:
|
|
1694
|
-
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(
|
|
1695
2266
|
"Create the note from the configured template if it does not exist. Default false \u2014 reports missing without creating."
|
|
1696
2267
|
)
|
|
1697
2268
|
});
|
|
@@ -1703,7 +2274,7 @@ async function getPeriodicNote(ctx2, rawInput) {
|
|
|
1703
2274
|
const abs = resolveVaultPath(ctx2.vaultRoot, path2);
|
|
1704
2275
|
let existed = false;
|
|
1705
2276
|
try {
|
|
1706
|
-
await
|
|
2277
|
+
await access3(abs);
|
|
1707
2278
|
existed = true;
|
|
1708
2279
|
} catch {
|
|
1709
2280
|
}
|
|
@@ -1717,21 +2288,22 @@ async function getPeriodicNote(ctx2, rawInput) {
|
|
|
1717
2288
|
ctx2.vaultRoot,
|
|
1718
2289
|
cfg.template.endsWith(".md") ? cfg.template : `${cfg.template}.md`
|
|
1719
2290
|
);
|
|
1720
|
-
body = await
|
|
2291
|
+
body = await readFile9(templateAbs, "utf8");
|
|
1721
2292
|
} catch {
|
|
1722
2293
|
}
|
|
1723
2294
|
}
|
|
1724
|
-
await
|
|
2295
|
+
await mkdir5(dirname3(abs), { recursive: true });
|
|
2296
|
+
await journalWrite(ctx2, "get_periodic_note", path2, null, body);
|
|
1725
2297
|
await atomicWrite(abs, body);
|
|
1726
2298
|
upsertDoc(ctx2, buildDoc(path2, body));
|
|
1727
2299
|
return { path: path2, existed: false, created: true, contentHash: contentHash(body) };
|
|
1728
2300
|
}
|
|
1729
|
-
var AppendPeriodicNoteInput =
|
|
2301
|
+
var AppendPeriodicNoteInput = z15.object({
|
|
1730
2302
|
period: PERIOD.default("daily").describe("Period type. Default: daily."),
|
|
1731
|
-
date:
|
|
1732
|
-
content:
|
|
1733
|
-
createIfMissing:
|
|
1734
|
-
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(
|
|
1735
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."
|
|
1736
2308
|
)
|
|
1737
2309
|
});
|
|
@@ -1744,7 +2316,7 @@ async function appendPeriodicNote(ctx2, rawInput) {
|
|
|
1744
2316
|
assertWritable(ctx2.policy, path2);
|
|
1745
2317
|
let original = "";
|
|
1746
2318
|
try {
|
|
1747
|
-
original = await
|
|
2319
|
+
original = await readFile9(abs, "utf8");
|
|
1748
2320
|
if (input.prevHash !== void 0) assertHashMatch(original, input.prevHash, path2);
|
|
1749
2321
|
} catch (err) {
|
|
1750
2322
|
if (err.code !== "ENOENT") throw err;
|
|
@@ -1760,7 +2332,8 @@ async function appendPeriodicNote(ctx2, rawInput) {
|
|
|
1760
2332
|
);
|
|
1761
2333
|
}
|
|
1762
2334
|
if (!input.createIfMissing) throw new Error(`Periodic note not found: ${path2}`);
|
|
1763
|
-
await
|
|
2335
|
+
await mkdir5(dirname3(abs), { recursive: true });
|
|
2336
|
+
await journalWrite(ctx2, "append_periodic_note", path2, null, input.content);
|
|
1764
2337
|
await atomicWrite(abs, input.content);
|
|
1765
2338
|
upsertDoc(ctx2, buildDoc(path2, input.content));
|
|
1766
2339
|
return {
|
|
@@ -1776,6 +2349,7 @@ async function appendPeriodicNote(ctx2, rawInput) {
|
|
|
1776
2349
|
const newBody = `${body}${separator}${input.content}`;
|
|
1777
2350
|
const header = original.slice(0, fm.bodyStart);
|
|
1778
2351
|
const newRaw = `${header}${newBody}`;
|
|
2352
|
+
await journalWrite(ctx2, "append_periodic_note", path2, original, newRaw);
|
|
1779
2353
|
await atomicWrite(abs, newRaw);
|
|
1780
2354
|
const cached = ctx2.notes.get(path2);
|
|
1781
2355
|
if (cached) {
|
|
@@ -1790,36 +2364,36 @@ async function appendPeriodicNote(ctx2, rawInput) {
|
|
|
1790
2364
|
}
|
|
1791
2365
|
|
|
1792
2366
|
// src/tools/query_notes.ts
|
|
1793
|
-
import { z as
|
|
2367
|
+
import { z as z16 } from "zod";
|
|
1794
2368
|
var OPS_WITHOUT_VALUE = /* @__PURE__ */ new Set(["exists", "missing"]);
|
|
1795
|
-
var Predicate =
|
|
1796
|
-
key:
|
|
1797
|
-
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(
|
|
1798
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)."
|
|
1799
2373
|
),
|
|
1800
|
-
value:
|
|
2374
|
+
value: z16.union([z16.string(), z16.number(), z16.boolean()]).optional().describe("Comparison value. Required for every op except exists/missing.")
|
|
1801
2375
|
}).refine((p) => OPS_WITHOUT_VALUE.has(p.op) || p.value !== void 0, {
|
|
1802
2376
|
message: "value is required for this op"
|
|
1803
2377
|
});
|
|
1804
|
-
var ISO_DATE =
|
|
1805
|
-
var QueryNotesInput =
|
|
1806
|
-
where:
|
|
1807
|
-
folder:
|
|
1808
|
-
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)."),
|
|
1809
2383
|
modifiedAfter: ISO_DATE.optional().describe(
|
|
1810
2384
|
"Only notes modified at or after this ISO 8601 date/time."
|
|
1811
2385
|
),
|
|
1812
2386
|
modifiedBefore: ISO_DATE.optional().describe(
|
|
1813
2387
|
"Only notes modified before this ISO 8601 date/time."
|
|
1814
2388
|
),
|
|
1815
|
-
minSizeBytes:
|
|
1816
|
-
maxSizeBytes:
|
|
1817
|
-
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(
|
|
1818
2392
|
'Extra fields to return per hit: frontmatter keys, or the specials "mtime", "size", "tags". Default returns only path + title, keeping the payload minimal.'
|
|
1819
2393
|
),
|
|
1820
|
-
sort:
|
|
1821
|
-
order:
|
|
1822
|
-
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.")
|
|
1823
2397
|
});
|
|
1824
2398
|
function scalarEq(a, b) {
|
|
1825
2399
|
if (a === b) return true;
|
|
@@ -1917,17 +2491,17 @@ function queryNotes(ctx2, input) {
|
|
|
1917
2491
|
}
|
|
1918
2492
|
|
|
1919
2493
|
// src/tools/read_note.ts
|
|
1920
|
-
import { readFile as
|
|
1921
|
-
import { z as
|
|
1922
|
-
var ReadNoteInput =
|
|
1923
|
-
path:
|
|
1924
|
-
section:
|
|
1925
|
-
block:
|
|
1926
|
-
lines:
|
|
1927
|
-
from:
|
|
1928
|
-
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.")
|
|
1929
2503
|
}).optional().describe("Return only this line range (1-indexed, inclusive)."),
|
|
1930
|
-
includeFrontmatter:
|
|
2504
|
+
includeFrontmatter: z17.boolean().optional().describe(
|
|
1931
2505
|
"Include frontmatter in span results. Default false for section/block spans. No effect on lines or whole-note reads."
|
|
1932
2506
|
)
|
|
1933
2507
|
}).refine((v) => [v.section, v.block, v.lines].filter((x) => x !== void 0).length <= 1, {
|
|
@@ -1935,7 +2509,7 @@ var ReadNoteInput = z16.object({
|
|
|
1935
2509
|
});
|
|
1936
2510
|
async function readNote(ctx2, input) {
|
|
1937
2511
|
const absPath = resolveVaultPath(ctx2.vaultRoot, input.path);
|
|
1938
|
-
const raw = await
|
|
2512
|
+
const raw = await readFile10(absPath, "utf8");
|
|
1939
2513
|
const noteBytes = Buffer.byteLength(raw, "utf8");
|
|
1940
2514
|
const hash = contentHash(raw);
|
|
1941
2515
|
if (input.section === void 0 && input.block === void 0 && input.lines === void 0) {
|
|
@@ -2023,18 +2597,18 @@ async function readNote(ctx2, input) {
|
|
|
2023
2597
|
}
|
|
2024
2598
|
|
|
2025
2599
|
// src/tools/rename_heading.ts
|
|
2026
|
-
import { readFile as
|
|
2027
|
-
import { join as
|
|
2028
|
-
import { z as
|
|
2029
|
-
var RenameHeadingInput =
|
|
2030
|
-
path:
|
|
2031
|
-
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(
|
|
2032
2606
|
"Current heading text, without # markers. Matched case-insensitively; if the note has duplicate headings the first one wins, mirroring Obsidian link resolution."
|
|
2033
2607
|
),
|
|
2034
|
-
newHeading:
|
|
2608
|
+
newHeading: z18.string().min(1).refine((s) => !/[\n[\]|#]/.test(s), {
|
|
2035
2609
|
message: 'newHeading must not contain newlines, "[", "]", "|", or "#" \u2014 those break [[note#heading]] link syntax.'
|
|
2036
2610
|
}).describe("New heading text, without # markers."),
|
|
2037
|
-
prevHash:
|
|
2611
|
+
prevHash: z18.string().optional().describe(
|
|
2038
2612
|
"Optional compare-and-swap guard: the contentHash from a prior read of this note. Fails with hash_conflict if the note changed since."
|
|
2039
2613
|
)
|
|
2040
2614
|
});
|
|
@@ -2064,7 +2638,7 @@ async function renameHeading(ctx2, rawInput) {
|
|
|
2064
2638
|
assertWritable(ctx2.policy, input.path);
|
|
2065
2639
|
let raw;
|
|
2066
2640
|
try {
|
|
2067
|
-
raw = await
|
|
2641
|
+
raw = await readFile11(absPath, "utf8");
|
|
2068
2642
|
} catch {
|
|
2069
2643
|
throw new Error(`Note not found: ${input.path}`);
|
|
2070
2644
|
}
|
|
@@ -2098,21 +2672,11 @@ async function renameHeading(ctx2, rawInput) {
|
|
|
2098
2672
|
(target) => target === "" || resolveLink(target, ctx2.notes) === input.path
|
|
2099
2673
|
);
|
|
2100
2674
|
newRaw = newRaw.slice(0, fm.bodyStart) + selfRewrite.content;
|
|
2101
|
-
const originalFmRegion = raw.slice(0, fm.bodyStart);
|
|
2102
|
-
await atomicWrite(absPath, newRaw);
|
|
2103
|
-
const written = await readFile10(absPath, "utf8");
|
|
2104
|
-
if (written.slice(0, originalFmRegion.length) !== originalFmRegion) {
|
|
2105
|
-
throw new Error("Write-safety violation: frontmatter region changed unexpectedly");
|
|
2106
|
-
}
|
|
2107
|
-
removeNoteBacklinks(ctx2, input.path);
|
|
2108
|
-
upsertDoc(ctx2, buildDoc(input.path, newRaw));
|
|
2109
|
-
addNoteBacklinks(ctx2, input.path, newRaw);
|
|
2110
|
-
let notesRewritten = selfRewrite.count > 0 ? 1 : 0;
|
|
2111
|
-
let linksRewritten = selfRewrite.count;
|
|
2112
|
-
const skipped = [];
|
|
2113
2675
|
const candidates = /* @__PURE__ */ new Set();
|
|
2114
2676
|
for (const ref of ctx2.backlinks.get(input.path) ?? []) candidates.add(ref.path);
|
|
2115
2677
|
candidates.delete(input.path);
|
|
2678
|
+
const rewrites = [];
|
|
2679
|
+
const skipped = [];
|
|
2116
2680
|
for (const path2 of [...candidates].sort()) {
|
|
2117
2681
|
const doc = ctx2.notes.get(path2);
|
|
2118
2682
|
if (doc === void 0) continue;
|
|
@@ -2127,7 +2691,27 @@ async function renameHeading(ctx2, rawInput) {
|
|
|
2127
2691
|
skipped.push(path2);
|
|
2128
2692
|
continue;
|
|
2129
2693
|
}
|
|
2130
|
-
|
|
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);
|
|
2131
2715
|
upsertDoc(ctx2, buildDoc(path2, content));
|
|
2132
2716
|
notesRewritten++;
|
|
2133
2717
|
linksRewritten += count;
|
|
@@ -2146,18 +2730,18 @@ async function renameHeading(ctx2, rawInput) {
|
|
|
2146
2730
|
}
|
|
2147
2731
|
|
|
2148
2732
|
// src/tools/replace_in_note.ts
|
|
2149
|
-
import { readFile as
|
|
2150
|
-
import { z as
|
|
2151
|
-
var ReplaceInNoteInput =
|
|
2152
|
-
path:
|
|
2153
|
-
find:
|
|
2154
|
-
replace:
|
|
2155
|
-
regex:
|
|
2156
|
-
caseSensitive:
|
|
2157
|
-
wholeWord:
|
|
2158
|
-
limit:
|
|
2159
|
-
dryRun:
|
|
2160
|
-
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(
|
|
2161
2745
|
"Optional compare-and-swap guard: the contentHash from a prior read. Fails with hash_conflict if the note changed since."
|
|
2162
2746
|
)
|
|
2163
2747
|
});
|
|
@@ -2183,7 +2767,7 @@ async function replaceInNote(ctx2, input) {
|
|
|
2183
2767
|
} catch (err) {
|
|
2184
2768
|
throw new Error(`Invalid regex: ${err instanceof Error ? err.message : String(err)}`);
|
|
2185
2769
|
}
|
|
2186
|
-
const raw = await
|
|
2770
|
+
const raw = await readFile12(absPath, "utf8");
|
|
2187
2771
|
if (input.prevHash !== void 0) assertHashMatch(raw, input.prevHash, input.path);
|
|
2188
2772
|
const fm = parseFrontmatter(raw);
|
|
2189
2773
|
const originalFmRegion = raw.slice(0, fm.bodyStart);
|
|
@@ -2217,8 +2801,9 @@ async function replaceInNote(ctx2, input) {
|
|
|
2217
2801
|
}
|
|
2218
2802
|
newBody += body.slice(lastIndex);
|
|
2219
2803
|
const newContent = `${originalFmRegion}${newBody}`;
|
|
2804
|
+
await journalWrite(ctx2, "replace_in_note", input.path, raw, newContent);
|
|
2220
2805
|
await atomicWrite(absPath, newContent);
|
|
2221
|
-
const written = await
|
|
2806
|
+
const written = await readFile12(absPath, "utf8");
|
|
2222
2807
|
if (written.slice(0, originalFmRegion.length) !== originalFmRegion) {
|
|
2223
2808
|
throw new Error("Write-safety violation: frontmatter region changed unexpectedly");
|
|
2224
2809
|
}
|
|
@@ -2238,6 +2823,123 @@ async function replaceInNote(ctx2, input) {
|
|
|
2238
2823
|
};
|
|
2239
2824
|
}
|
|
2240
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
|
+
|
|
2241
2943
|
// src/dispatch.ts
|
|
2242
2944
|
var WRITE_TOOLS = /* @__PURE__ */ new Set([
|
|
2243
2945
|
"create_note",
|
|
@@ -2248,7 +2950,8 @@ var WRITE_TOOLS = /* @__PURE__ */ new Set([
|
|
|
2248
2950
|
"patch_frontmatter",
|
|
2249
2951
|
"patch_note",
|
|
2250
2952
|
"replace_in_note",
|
|
2251
|
-
"append_periodic_note"
|
|
2953
|
+
"append_periodic_note",
|
|
2954
|
+
"undo_write"
|
|
2252
2955
|
]);
|
|
2253
2956
|
var META_KEYS = [
|
|
2254
2957
|
"path",
|
|
@@ -2272,7 +2975,9 @@ var META_KEYS = [
|
|
|
2272
2975
|
"date",
|
|
2273
2976
|
"permanent",
|
|
2274
2977
|
"prevHash",
|
|
2275
|
-
"budgetBytes"
|
|
2978
|
+
"budgetBytes",
|
|
2979
|
+
"seq",
|
|
2980
|
+
"force"
|
|
2276
2981
|
];
|
|
2277
2982
|
function safeMeta(args) {
|
|
2278
2983
|
if (typeof args !== "object" || args === null) return {};
|
|
@@ -2296,7 +3001,7 @@ async function dispatch(ctx2, name, args, log2) {
|
|
|
2296
3001
|
log2.debug("tool start", { tool: name, args });
|
|
2297
3002
|
if (ctx2.policy.readOnly && WRITE_TOOLS.has(name)) {
|
|
2298
3003
|
log2.info("tool rejected (read-only)", { tool: name, ...meta });
|
|
2299
|
-
|
|
3004
|
+
const rejected = {
|
|
2300
3005
|
content: [
|
|
2301
3006
|
{
|
|
2302
3007
|
type: "text",
|
|
@@ -2305,17 +3010,39 @@ async function dispatch(ctx2, name, args, log2) {
|
|
|
2305
3010
|
],
|
|
2306
3011
|
isError: true
|
|
2307
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
|
+
});
|
|
2308
3023
|
}
|
|
2309
3024
|
if (ctx2.policy.readOnly && name === "get_periodic_note") {
|
|
2310
3025
|
if (typeof args === "object" && args !== null) {
|
|
2311
3026
|
args = { ...args, createIfMissing: false };
|
|
2312
3027
|
}
|
|
2313
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();
|
|
2314
3032
|
try {
|
|
2315
|
-
const result = await run(ctx2, name, args);
|
|
3033
|
+
const { audit: detail, ...result } = await run(ctx2, name, args);
|
|
2316
3034
|
const durationMs = Math.round((performance.now() - start) * 100) / 100;
|
|
2317
3035
|
log2.info("tool ok", { tool: name, durationMs, resultBytes: resultBytes(result), ...meta });
|
|
2318
|
-
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
|
+
});
|
|
2319
3046
|
} catch (err) {
|
|
2320
3047
|
const durationMs = Math.round((performance.now() - start) * 100) / 100;
|
|
2321
3048
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -2326,7 +3053,66 @@ async function dispatch(ctx2, name, args, log2) {
|
|
|
2326
3053
|
stack: err instanceof Error ? err.stack : void 0,
|
|
2327
3054
|
...meta
|
|
2328
3055
|
});
|
|
2329
|
-
|
|
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
|
+
};
|
|
2330
3116
|
}
|
|
2331
3117
|
}
|
|
2332
3118
|
async function run(ctx2, name, args) {
|
|
@@ -2370,14 +3156,26 @@ async function run(ctx2, name, args) {
|
|
|
2370
3156
|
type: "text",
|
|
2371
3157
|
text: `Created ${result.path} (${result.bytesWritten} bytes). contentHash: ${result.contentHash}`
|
|
2372
3158
|
}
|
|
2373
|
-
]
|
|
3159
|
+
],
|
|
3160
|
+
audit: {
|
|
3161
|
+
path: result.path,
|
|
3162
|
+
contentHash: result.contentHash,
|
|
3163
|
+
bytesWritten: result.bytesWritten,
|
|
3164
|
+
overwrite: input.overwrite
|
|
3165
|
+
}
|
|
2374
3166
|
};
|
|
2375
3167
|
}
|
|
2376
3168
|
case "delete_note": {
|
|
2377
3169
|
const input = DeleteNoteInput.parse(args);
|
|
2378
3170
|
const result = await deleteNote(ctx2, input);
|
|
2379
3171
|
const text = result.trashedTo ? `Moved ${result.path} to ${result.trashedTo} (recoverable \u2014 restore by moving it back).` : `Permanently deleted ${result.path}.`;
|
|
2380
|
-
|
|
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 };
|
|
2381
3179
|
}
|
|
2382
3180
|
case "move_note": {
|
|
2383
3181
|
const input = MoveNoteInput.parse(args);
|
|
@@ -2386,7 +3184,17 @@ async function run(ctx2, name, args) {
|
|
|
2386
3184
|
if (result.skipped?.length) {
|
|
2387
3185
|
text += ` Skipped (outside SEEKSTONE_WRITE_PATHS): ${result.skipped.join(", ")}.`;
|
|
2388
3186
|
}
|
|
2389
|
-
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
|
+
};
|
|
2390
3198
|
}
|
|
2391
3199
|
case "rename_heading": {
|
|
2392
3200
|
const input = RenameHeadingInput.parse(args);
|
|
@@ -2395,7 +3203,17 @@ async function run(ctx2, name, args) {
|
|
|
2395
3203
|
if (result.skipped?.length) {
|
|
2396
3204
|
text += ` Skipped (outside SEEKSTONE_WRITE_PATHS): ${result.skipped.join(", ")}.`;
|
|
2397
3205
|
}
|
|
2398
|
-
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
|
+
};
|
|
2399
3217
|
}
|
|
2400
3218
|
case "append_note": {
|
|
2401
3219
|
const input = AppendNoteInput.parse(args);
|
|
@@ -2406,13 +3224,28 @@ async function run(ctx2, name, args) {
|
|
|
2406
3224
|
type: "text",
|
|
2407
3225
|
text: `Appended ${result.bytesWritten} bytes to ${result.path}. contentHash: ${result.contentHash}`
|
|
2408
3226
|
}
|
|
2409
|
-
]
|
|
3227
|
+
],
|
|
3228
|
+
audit: {
|
|
3229
|
+
path: result.path,
|
|
3230
|
+
contentHash: result.contentHash,
|
|
3231
|
+
bytesWritten: result.bytesWritten
|
|
3232
|
+
}
|
|
2410
3233
|
};
|
|
2411
3234
|
}
|
|
2412
3235
|
case "patch_frontmatter": {
|
|
2413
3236
|
const input = PatchFrontmatterInput.parse(args);
|
|
2414
3237
|
const result = await patchFrontmatter(ctx2, input);
|
|
2415
|
-
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
|
+
};
|
|
2416
3249
|
}
|
|
2417
3250
|
case "outline_note": {
|
|
2418
3251
|
const input = OutlineNoteInput.parse(args);
|
|
@@ -2422,7 +3255,16 @@ async function run(ctx2, name, args) {
|
|
|
2422
3255
|
case "patch_note": {
|
|
2423
3256
|
const input = PatchNoteInput.parse(args);
|
|
2424
3257
|
const result = await patchNote(ctx2, input);
|
|
2425
|
-
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
|
+
};
|
|
2426
3268
|
}
|
|
2427
3269
|
case "get_backlinks": {
|
|
2428
3270
|
const input = GetBacklinksInput.parse(args);
|
|
@@ -2437,12 +3279,26 @@ async function run(ctx2, name, args) {
|
|
|
2437
3279
|
case "replace_in_note": {
|
|
2438
3280
|
const input = ReplaceInNoteInput.parse(args);
|
|
2439
3281
|
const result = await replaceInNote(ctx2, input);
|
|
2440
|
-
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
|
+
};
|
|
2441
3291
|
}
|
|
2442
3292
|
case "get_periodic_note": {
|
|
2443
3293
|
const input = GetPeriodicNoteInput.parse(args);
|
|
2444
3294
|
const result = await getPeriodicNote(ctx2, input);
|
|
2445
|
-
|
|
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;
|
|
2446
3302
|
}
|
|
2447
3303
|
case "append_periodic_note": {
|
|
2448
3304
|
const input = AppendPeriodicNoteInput.parse(args);
|
|
@@ -2453,7 +3309,29 @@ async function run(ctx2, name, args) {
|
|
|
2453
3309
|
type: "text",
|
|
2454
3310
|
text: `Appended ${result.bytesWritten} bytes to ${result.path}. contentHash: ${result.contentHash}`
|
|
2455
3311
|
}
|
|
2456
|
-
]
|
|
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
|
+
}
|
|
2457
3335
|
};
|
|
2458
3336
|
}
|
|
2459
3337
|
default:
|
|
@@ -2462,12 +3340,12 @@ async function run(ctx2, name, args) {
|
|
|
2462
3340
|
}
|
|
2463
3341
|
|
|
2464
3342
|
// src/index/build.ts
|
|
2465
|
-
import { readFile as
|
|
3343
|
+
import { readFile as readFile14 } from "fs/promises";
|
|
2466
3344
|
import { basename as basename3, extname as extname4 } from "path";
|
|
2467
3345
|
|
|
2468
3346
|
// ../core/src/walk.ts
|
|
2469
|
-
import { stat } from "fs/promises";
|
|
2470
|
-
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";
|
|
2471
3349
|
import fg from "fast-glob";
|
|
2472
3350
|
var IMAGE_EXTS = /* @__PURE__ */ new Set([".png", ".jpg", ".jpeg", ".gif", ".webp", ".svg", ".bmp", ".heic"]);
|
|
2473
3351
|
var VIDEO_EXTS = /* @__PURE__ */ new Set([".mp4", ".mov", ".webm", ".mkv", ".avi"]);
|
|
@@ -2475,6 +3353,7 @@ var AUDIO_EXTS = /* @__PURE__ */ new Set([".mp3", ".m4a", ".wav", ".flac", ".ogg
|
|
|
2475
3353
|
var IGNORE_GLOBS = [
|
|
2476
3354
|
"**/.obsidian/**",
|
|
2477
3355
|
"**/.trash/**",
|
|
3356
|
+
"**/.seekstone/**",
|
|
2478
3357
|
"**/.git/**",
|
|
2479
3358
|
"**/.DS_Store",
|
|
2480
3359
|
"**/node_modules/**"
|
|
@@ -2506,7 +3385,7 @@ async function walkVault(vaultRoot2) {
|
|
|
2506
3385
|
const entries = [];
|
|
2507
3386
|
for (const m of matches2) {
|
|
2508
3387
|
const absPath = typeof m === "string" ? m : m.path;
|
|
2509
|
-
const stats = typeof m === "string" ? await
|
|
3388
|
+
const stats = typeof m === "string" ? await stat2(absPath) : m.stats;
|
|
2510
3389
|
if (!stats) continue;
|
|
2511
3390
|
const relPath = relative(vaultRoot2, absPath).split(sep2).join("/");
|
|
2512
3391
|
const topDir = relPath.split("/")[0] ?? "";
|
|
@@ -2544,7 +3423,7 @@ async function buildIndex(vaultRoot2) {
|
|
|
2544
3423
|
await Promise.all(
|
|
2545
3424
|
noteEntries.map(async (entry) => {
|
|
2546
3425
|
try {
|
|
2547
|
-
const raw = await
|
|
3426
|
+
const raw = await readFile14(entry.absPath, "utf8");
|
|
2548
3427
|
const fm = parseFrontmatter(raw);
|
|
2549
3428
|
const inlineTags = extractInlineTags(fm.body);
|
|
2550
3429
|
const fmTags = frontmatterTags(fm.data);
|
|
@@ -2597,8 +3476,8 @@ function buildBacklinks(notes2) {
|
|
|
2597
3476
|
|
|
2598
3477
|
// src/init.ts
|
|
2599
3478
|
import { execFileSync } from "child_process";
|
|
2600
|
-
import { access as
|
|
2601
|
-
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";
|
|
2602
3481
|
function parseInitArgs(argv) {
|
|
2603
3482
|
const opts = { write: false, client: "desktop" };
|
|
2604
3483
|
for (let i = 0; i < argv.length; i++) {
|
|
@@ -2641,7 +3520,7 @@ function cursorConfigPath(platform, env) {
|
|
|
2641
3520
|
return j(env.home ?? "", ".cursor", "mcp.json");
|
|
2642
3521
|
}
|
|
2643
3522
|
function vscodeMcpConfigPath(cwd) {
|
|
2644
|
-
return
|
|
3523
|
+
return join7(cwd, ".vscode", "mcp.json");
|
|
2645
3524
|
}
|
|
2646
3525
|
function obsidianRegistryPath(platform, env) {
|
|
2647
3526
|
if (platform === "win32") {
|
|
@@ -2697,13 +3576,13 @@ function mergeSeekstoneConfigVsCode(existing, vaultPath) {
|
|
|
2697
3576
|
}
|
|
2698
3577
|
async function validateVault(vaultPath) {
|
|
2699
3578
|
try {
|
|
2700
|
-
const st = await
|
|
3579
|
+
const st = await stat3(vaultPath);
|
|
2701
3580
|
if (!st.isDirectory()) return { ok: false, error: `Not a directory: ${vaultPath}` };
|
|
2702
3581
|
} catch {
|
|
2703
3582
|
return { ok: false, error: `Path does not exist: ${vaultPath}` };
|
|
2704
3583
|
}
|
|
2705
3584
|
try {
|
|
2706
|
-
await
|
|
3585
|
+
await access4(join7(vaultPath, ".obsidian"));
|
|
2707
3586
|
} catch {
|
|
2708
3587
|
return {
|
|
2709
3588
|
ok: false,
|
|
@@ -2717,13 +3596,13 @@ async function countMarkdown(root) {
|
|
|
2717
3596
|
async function walk(dir) {
|
|
2718
3597
|
let entries;
|
|
2719
3598
|
try {
|
|
2720
|
-
entries = await
|
|
3599
|
+
entries = await readdir2(dir, { withFileTypes: true });
|
|
2721
3600
|
} catch {
|
|
2722
3601
|
return;
|
|
2723
3602
|
}
|
|
2724
3603
|
for (const e of entries) {
|
|
2725
3604
|
if (e.name.startsWith(".")) continue;
|
|
2726
|
-
const full =
|
|
3605
|
+
const full = join7(dir, e.name);
|
|
2727
3606
|
if (e.isDirectory()) await walk(full);
|
|
2728
3607
|
else if (e.name.endsWith(".md")) count++;
|
|
2729
3608
|
}
|
|
@@ -2741,7 +3620,7 @@ async function runInit(opts, deps) {
|
|
|
2741
3620
|
]
|
|
2742
3621
|
};
|
|
2743
3622
|
}
|
|
2744
|
-
const rf = deps.readFile ??
|
|
3623
|
+
const rf = deps.readFile ?? readFile15;
|
|
2745
3624
|
let vaultPath = opts.vault ?? deps.env.SEEKSTONE_VAULT;
|
|
2746
3625
|
if (!vaultPath) {
|
|
2747
3626
|
const registryPath = obsidianRegistryPath(deps.platform, {
|
|
@@ -2872,7 +3751,7 @@ async function runInit(opts, deps) {
|
|
|
2872
3751
|
}
|
|
2873
3752
|
const merged = opts.client === "vscode" ? mergeSeekstoneConfigVsCode(existing, vaultPath) : mergeSeekstoneConfig(existing, vaultPath);
|
|
2874
3753
|
let backupPath;
|
|
2875
|
-
await
|
|
3754
|
+
await mkdir7(dirname5(configPath), { recursive: true });
|
|
2876
3755
|
if (existingRaw !== null) {
|
|
2877
3756
|
backupPath = `${configPath}.backup-${deps.timestamp}`;
|
|
2878
3757
|
await copyFile(configPath, backupPath);
|
|
@@ -2888,107 +3767,6 @@ async function runInit(opts, deps) {
|
|
|
2888
3767
|
return { ok: true, exitCode: 0, output: out, wrotePath: configPath, backupPath };
|
|
2889
3768
|
}
|
|
2890
3769
|
|
|
2891
|
-
// src/log.ts
|
|
2892
|
-
import { appendFileSync, renameSync, statSync } from "fs";
|
|
2893
|
-
var ORDER = ["error", "warn", "info", "debug"];
|
|
2894
|
-
var DEFAULT_MAX_SIZE = 5 * 1024 * 1024;
|
|
2895
|
-
function parseLevel(raw, onBad) {
|
|
2896
|
-
if (raw === void 0 || raw === "") return "info";
|
|
2897
|
-
const v = raw.trim().toLowerCase();
|
|
2898
|
-
for (const lvl of ORDER) {
|
|
2899
|
-
if (lvl === v) return lvl;
|
|
2900
|
-
}
|
|
2901
|
-
onBad(`invalid SEEKSTONE_LOG_LEVEL=${JSON.stringify(raw)}; using "info"`);
|
|
2902
|
-
return "info";
|
|
2903
|
-
}
|
|
2904
|
-
function parseSize(raw) {
|
|
2905
|
-
if (raw === void 0) return DEFAULT_MAX_SIZE;
|
|
2906
|
-
const m = /^(\d+(?:\.\d+)?)(b|kb|mb|gb)?$/i.exec(raw.trim());
|
|
2907
|
-
if (m === null) return DEFAULT_MAX_SIZE;
|
|
2908
|
-
const num = Number(m[1]);
|
|
2909
|
-
const unit = (m[2] ?? "b").toLowerCase();
|
|
2910
|
-
const mult = unit === "gb" ? 1e9 : unit === "mb" ? 1e6 : unit === "kb" ? 1e3 : 1;
|
|
2911
|
-
return Math.max(1, Math.floor(num * mult));
|
|
2912
|
-
}
|
|
2913
|
-
function fmtFields(fields) {
|
|
2914
|
-
const parts = [];
|
|
2915
|
-
for (const [k, v] of Object.entries(fields)) {
|
|
2916
|
-
if (v === void 0) continue;
|
|
2917
|
-
parts.push(`${k}=${typeof v === "string" ? v : JSON.stringify(v)}`);
|
|
2918
|
-
}
|
|
2919
|
-
return parts.length > 0 ? ` ${parts.join(" ")}` : "";
|
|
2920
|
-
}
|
|
2921
|
-
function createLogger(opts = {}) {
|
|
2922
|
-
const env = opts.env ?? process.env;
|
|
2923
|
-
const stderr = opts.stderr ?? ((line) => void process.stderr.write(line));
|
|
2924
|
-
const now = opts.now ?? (() => /* @__PURE__ */ new Date());
|
|
2925
|
-
const level = parseLevel(env.SEEKSTONE_LOG_LEVEL, (m) => {
|
|
2926
|
-
stderr(`WARN ${now().toISOString()} ${m}
|
|
2927
|
-
`);
|
|
2928
|
-
});
|
|
2929
|
-
const threshold = ORDER.indexOf(level);
|
|
2930
|
-
const filePath = env.SEEKSTONE_LOG_FILE;
|
|
2931
|
-
const maxSize = parseSize(env.SEEKSTONE_LOG_MAX_SIZE);
|
|
2932
|
-
let fileOk = false;
|
|
2933
|
-
let bytes = 0;
|
|
2934
|
-
if (filePath !== void 0 && filePath !== "") {
|
|
2935
|
-
try {
|
|
2936
|
-
try {
|
|
2937
|
-
bytes = statSync(filePath).size;
|
|
2938
|
-
} catch {
|
|
2939
|
-
bytes = 0;
|
|
2940
|
-
}
|
|
2941
|
-
appendFileSync(filePath, "");
|
|
2942
|
-
fileOk = true;
|
|
2943
|
-
} catch (err) {
|
|
2944
|
-
const m = err instanceof Error ? err.message : String(err);
|
|
2945
|
-
stderr(
|
|
2946
|
-
`WARN ${now().toISOString()} cannot write log file ${filePath}: ${m}; file logging disabled
|
|
2947
|
-
`
|
|
2948
|
-
);
|
|
2949
|
-
}
|
|
2950
|
-
}
|
|
2951
|
-
function writeFile5(record) {
|
|
2952
|
-
if (!fileOk || filePath === void 0) return;
|
|
2953
|
-
const line = `${JSON.stringify(record)}
|
|
2954
|
-
`;
|
|
2955
|
-
const len = Buffer.byteLength(line);
|
|
2956
|
-
try {
|
|
2957
|
-
if (bytes > 0 && bytes + len > maxSize) {
|
|
2958
|
-
renameSync(filePath, `${filePath}.1`);
|
|
2959
|
-
bytes = 0;
|
|
2960
|
-
}
|
|
2961
|
-
appendFileSync(filePath, line);
|
|
2962
|
-
bytes += len;
|
|
2963
|
-
} catch {
|
|
2964
|
-
fileOk = false;
|
|
2965
|
-
}
|
|
2966
|
-
}
|
|
2967
|
-
function emit(lvl, msg, fields) {
|
|
2968
|
-
if (ORDER.indexOf(lvl) > threshold) return;
|
|
2969
|
-
const ts = now().toISOString();
|
|
2970
|
-
const f = fields ?? {};
|
|
2971
|
-
stderr(`${lvl.toUpperCase().padEnd(5)} ${ts} ${msg}${fmtFields(f)}
|
|
2972
|
-
`);
|
|
2973
|
-
writeFile5({ t: ts, level: lvl, msg, ...f });
|
|
2974
|
-
}
|
|
2975
|
-
return {
|
|
2976
|
-
level,
|
|
2977
|
-
error: (m, f) => {
|
|
2978
|
-
emit("error", m, f);
|
|
2979
|
-
},
|
|
2980
|
-
warn: (m, f) => {
|
|
2981
|
-
emit("warn", m, f);
|
|
2982
|
-
},
|
|
2983
|
-
info: (m, f) => {
|
|
2984
|
-
emit("info", m, f);
|
|
2985
|
-
},
|
|
2986
|
-
debug: (m, f) => {
|
|
2987
|
-
emit("debug", m, f);
|
|
2988
|
-
}
|
|
2989
|
-
};
|
|
2990
|
-
}
|
|
2991
|
-
|
|
2992
3770
|
// src/process-guards.ts
|
|
2993
3771
|
function installProcessGuards(log2) {
|
|
2994
3772
|
const onUnhandledRejection = (reason) => {
|
|
@@ -3077,9 +3855,83 @@ function countWords(text) {
|
|
|
3077
3855
|
return m ? m.length : 0;
|
|
3078
3856
|
}
|
|
3079
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
|
+
|
|
3080
3932
|
// ../core/src/embed/model2vec.ts
|
|
3081
|
-
import { readFile as
|
|
3082
|
-
import { basename as basename4, join as
|
|
3933
|
+
import { readFile as readFile16 } from "fs/promises";
|
|
3934
|
+
import { basename as basename4, join as join9 } from "path";
|
|
3083
3935
|
|
|
3084
3936
|
// ../core/src/embed/safetensors.ts
|
|
3085
3937
|
function readSafetensors(buf) {
|
|
@@ -3156,7 +4008,7 @@ function loadWordPieceTokenizer(tokenizerJson) {
|
|
|
3156
4008
|
const specialIds = new Set(
|
|
3157
4009
|
(tj.added_tokens ?? []).filter((t) => t.special).map((t) => t.id)
|
|
3158
4010
|
);
|
|
3159
|
-
function
|
|
4011
|
+
function normalize2(text) {
|
|
3160
4012
|
let out = cleanText ? doCleanText(text) : text;
|
|
3161
4013
|
if (handleCjk) out = padCjkChars(out);
|
|
3162
4014
|
if (stripAccents) out = out.normalize("NFD").replace(new RegExp("\\p{Mn}", "gu"), "");
|
|
@@ -3192,7 +4044,7 @@ function loadWordPieceTokenizer(tokenizerJson) {
|
|
|
3192
4044
|
specialIds,
|
|
3193
4045
|
encode(text) {
|
|
3194
4046
|
const ids = [];
|
|
3195
|
-
for (const word of preTokenize(
|
|
4047
|
+
for (const word of preTokenize(normalize2(text))) {
|
|
3196
4048
|
for (const id of wordPiece(word)) ids.push(id);
|
|
3197
4049
|
}
|
|
3198
4050
|
return ids;
|
|
@@ -3260,9 +4112,9 @@ function isCjk(cp) {
|
|
|
3260
4112
|
var TENSOR_NAME = "embeddings";
|
|
3261
4113
|
async function loadModel2Vec(modelDir) {
|
|
3262
4114
|
const [configRaw, tokenizerRaw, weights] = await Promise.all([
|
|
3263
|
-
|
|
3264
|
-
|
|
3265
|
-
|
|
4115
|
+
readFile16(join9(modelDir, "config.json"), "utf8"),
|
|
4116
|
+
readFile16(join9(modelDir, "tokenizer.json"), "utf8"),
|
|
4117
|
+
readFile16(join9(modelDir, "model.safetensors"))
|
|
3266
4118
|
]);
|
|
3267
4119
|
const config = JSON.parse(configRaw);
|
|
3268
4120
|
const tokenizer = loadWordPieceTokenizer(JSON.parse(tokenizerRaw));
|
|
@@ -3288,20 +4140,27 @@ async function loadModel2Vec(modelDir) {
|
|
|
3288
4140
|
);
|
|
3289
4141
|
}
|
|
3290
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
|
+
}
|
|
3291
4151
|
return {
|
|
3292
4152
|
id: basename4(modelDir),
|
|
3293
4153
|
dim,
|
|
3294
4154
|
embed(text) {
|
|
3295
4155
|
const out = new Float32Array(dim);
|
|
3296
|
-
|
|
3297
|
-
for (const id of
|
|
3298
|
-
if (tokenizer.specialIds.has(id) && id !== tokenizer.unkId) continue;
|
|
4156
|
+
const ids = pooledIds(text);
|
|
4157
|
+
for (const id of ids) {
|
|
3299
4158
|
const base = id * dim;
|
|
3300
4159
|
for (let j = 0; j < dim; j++) {
|
|
3301
4160
|
out[j] = out[j] + table[base + j];
|
|
3302
4161
|
}
|
|
3303
|
-
pooled++;
|
|
3304
4162
|
}
|
|
4163
|
+
const pooled = ids.length;
|
|
3305
4164
|
if (pooled === 0) return out;
|
|
3306
4165
|
let sumSquares = 0;
|
|
3307
4166
|
for (let j = 0; j < dim; j++) {
|
|
@@ -3314,27 +4173,121 @@ async function loadModel2Vec(modelDir) {
|
|
|
3314
4173
|
for (let j = 0; j < dim; j++) out[j] = out[j] / norm;
|
|
3315
4174
|
}
|
|
3316
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;
|
|
3317
4193
|
}
|
|
3318
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";
|
|
3319
4272
|
}
|
|
3320
4273
|
|
|
3321
4274
|
// src/semantic/cache.ts
|
|
3322
4275
|
import { createHash as createHash2 } from "crypto";
|
|
3323
|
-
import { mkdir as
|
|
3324
|
-
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";
|
|
3325
4278
|
var CACHE_VERSION = 2;
|
|
3326
4279
|
function cachePathsFor(cacheDir, vaultRoot2, modelId) {
|
|
3327
4280
|
const vaultKey = createHash2("sha256").update(vaultRoot2).digest("hex").slice(0, 16);
|
|
3328
|
-
const dir =
|
|
3329
|
-
return { dir, bin:
|
|
4281
|
+
const dir = join10(cacheDir, "embeddings", vaultKey);
|
|
4282
|
+
return { dir, bin: join10(dir, `${modelId}.bin`), manifest: join10(dir, `${modelId}.json`) };
|
|
3330
4283
|
}
|
|
3331
4284
|
async function loadCache(paths, modelId, dim) {
|
|
3332
4285
|
try {
|
|
3333
|
-
const manifest = JSON.parse(await
|
|
4286
|
+
const manifest = JSON.parse(await readFile17(paths.manifest, "utf8"));
|
|
3334
4287
|
if (manifest.version !== CACHE_VERSION || manifest.modelId !== modelId || manifest.dim !== dim) {
|
|
3335
4288
|
return void 0;
|
|
3336
4289
|
}
|
|
3337
|
-
const buf = await
|
|
4290
|
+
const buf = await readFile17(paths.bin);
|
|
3338
4291
|
const totalChunks = manifest.entries.reduce((a, e) => a + e.chunks, 0);
|
|
3339
4292
|
const vectorBytes = totalChunks * dim * 4;
|
|
3340
4293
|
const spanBytes = totalChunks * 2 * 4;
|
|
@@ -3384,26 +4337,64 @@ async function saveCache(paths, modelId, dim, notes2, hashes) {
|
|
|
3384
4337
|
spanOffset += note.spans.length;
|
|
3385
4338
|
}
|
|
3386
4339
|
const manifest = { version: CACHE_VERSION, modelId, dim, entries };
|
|
3387
|
-
await
|
|
4340
|
+
await mkdir8(paths.dir, { recursive: true });
|
|
3388
4341
|
await atomicWriteBytes(paths.bin, bin);
|
|
3389
4342
|
await atomicWriteBytes(paths.manifest, new TextEncoder().encode(JSON.stringify(manifest)));
|
|
3390
4343
|
}
|
|
3391
4344
|
async function atomicWriteBytes(absPath, bytes) {
|
|
3392
4345
|
const tmpPath = `${absPath}.seekstone-cache-tmp`;
|
|
3393
4346
|
await writeFile3(tmpPath, bytes);
|
|
3394
|
-
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);
|
|
3395
4382
|
}
|
|
3396
4383
|
|
|
3397
4384
|
// src/semantic/store.ts
|
|
4385
|
+
var DEFAULT_POOLING = "max";
|
|
3398
4386
|
var SemanticStore = class {
|
|
3399
4387
|
dim;
|
|
4388
|
+
pooling;
|
|
3400
4389
|
notes = /* @__PURE__ */ new Map();
|
|
3401
4390
|
chunks = 0;
|
|
3402
|
-
constructor(dim) {
|
|
4391
|
+
constructor(dim, pooling = DEFAULT_POOLING) {
|
|
3403
4392
|
if (!Number.isInteger(dim) || dim <= 0) {
|
|
3404
4393
|
throw new Error(`semantic store: invalid dim ${dim}`);
|
|
3405
4394
|
}
|
|
4395
|
+
assertValidPooling(pooling);
|
|
3406
4396
|
this.dim = dim;
|
|
4397
|
+
this.pooling = pooling;
|
|
3407
4398
|
}
|
|
3408
4399
|
/** Total chunk vectors across all notes. */
|
|
3409
4400
|
get chunkCount() {
|
|
@@ -3443,38 +4434,55 @@ var SemanticStore = class {
|
|
|
3443
4434
|
entries() {
|
|
3444
4435
|
return this.notes.entries();
|
|
3445
4436
|
}
|
|
3446
|
-
/**
|
|
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
|
+
*/
|
|
3447
4442
|
topNotes(query, k) {
|
|
3448
4443
|
if (query.length !== this.dim) {
|
|
3449
4444
|
throw new Error(`semantic store: query dim ${query.length} does not match ${this.dim}`);
|
|
3450
4445
|
}
|
|
3451
|
-
const dim = this.dim;
|
|
3452
4446
|
const hits = [];
|
|
3453
|
-
for (const [path2,
|
|
3454
|
-
|
|
3455
|
-
let bestChunk = 0;
|
|
3456
|
-
const n = packed.length / dim;
|
|
3457
|
-
for (let c = 0; c < n; c++) {
|
|
3458
|
-
const base = c * dim;
|
|
3459
|
-
let score = 0;
|
|
3460
|
-
for (let j = 0; j < dim; j++) {
|
|
3461
|
-
score += packed[base + j] * query[j];
|
|
3462
|
-
}
|
|
3463
|
-
if (score > best) {
|
|
3464
|
-
best = score;
|
|
3465
|
-
bestChunk = c;
|
|
3466
|
-
}
|
|
3467
|
-
}
|
|
3468
|
-
hits.push({
|
|
3469
|
-
path: path2,
|
|
3470
|
-
score: best,
|
|
3471
|
-
chunkIndex: bestChunk,
|
|
3472
|
-
start: spans[bestChunk * 2],
|
|
3473
|
-
end: spans[bestChunk * 2 + 1]
|
|
3474
|
-
});
|
|
4447
|
+
for (const [path2, vectors] of this.notes) {
|
|
4448
|
+
hits.push(this.scoreVectors(path2, vectors, query));
|
|
3475
4449
|
}
|
|
3476
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));
|
|
3477
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
|
+
}
|
|
3478
4486
|
};
|
|
3479
4487
|
|
|
3480
4488
|
// src/semantic/state.ts
|
|
@@ -3489,11 +4497,21 @@ var Semantic = class _Semantic {
|
|
|
3489
4497
|
saveDebounceMs;
|
|
3490
4498
|
yieldEvery;
|
|
3491
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;
|
|
3492
4509
|
pending = /* @__PURE__ */ new Map();
|
|
3493
4510
|
saveTimer;
|
|
3494
4511
|
stopped = false;
|
|
3495
4512
|
constructor(embedder, ctx2, cfg, deps) {
|
|
3496
4513
|
this.embedder = embedder;
|
|
4514
|
+
this.tokenEmbedder = isTokenEmbedder(embedder) ? embedder : void 0;
|
|
3497
4515
|
this.store = new SemanticStore(embedder.dim);
|
|
3498
4516
|
this.ctx = ctx2;
|
|
3499
4517
|
this.paths = cachePathsFor(cfg.cacheDir, ctx2.vaultRoot, embedder.id);
|
|
@@ -3515,7 +4533,7 @@ var Semantic = class _Semantic {
|
|
|
3515
4533
|
} catch (err) {
|
|
3516
4534
|
const reason = err instanceof Error ? err.message : String(err);
|
|
3517
4535
|
throw new Error(
|
|
3518
|
-
`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})`
|
|
3519
4537
|
);
|
|
3520
4538
|
}
|
|
3521
4539
|
const semantic = new _Semantic(embedder, ctx2, cfg, deps);
|
|
@@ -3590,9 +4608,35 @@ var Semantic = class _Semantic {
|
|
|
3590
4608
|
embedQuery(query) {
|
|
3591
4609
|
return this.embedder.embed(query);
|
|
3592
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
|
+
}
|
|
3593
4636
|
/** Watcher hook: a note was added or changed (ctx.notes is already fresh). */
|
|
3594
4637
|
noteChanged(path2) {
|
|
3595
4638
|
if (this.stopped) return;
|
|
4639
|
+
this.tokenIdCache.delete(path2);
|
|
3596
4640
|
const existing = this.pending.get(path2);
|
|
3597
4641
|
if (existing) clearTimeout(existing);
|
|
3598
4642
|
const timer = setTimeout(() => {
|
|
@@ -3609,6 +4653,7 @@ var Semantic = class _Semantic {
|
|
|
3609
4653
|
clearTimeout(existing);
|
|
3610
4654
|
this.pending.delete(path2);
|
|
3611
4655
|
}
|
|
4656
|
+
this.tokenIdCache.delete(path2);
|
|
3612
4657
|
if (this.hashes.has(path2) || this.store.getNote(path2)) {
|
|
3613
4658
|
this.store.removeNote(path2);
|
|
3614
4659
|
this.hashes.delete(path2);
|
|
@@ -4178,6 +5223,42 @@ var ALL_TOOLS = [
|
|
|
4178
5223
|
},
|
|
4179
5224
|
required: ["content"]
|
|
4180
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
|
+
}
|
|
4181
5262
|
}
|
|
4182
5263
|
];
|
|
4183
5264
|
function visibleTools(policy2) {
|
|
@@ -4187,11 +5268,30 @@ function visibleTools(policy2) {
|
|
|
4187
5268
|
|
|
4188
5269
|
// src/watcher.ts
|
|
4189
5270
|
import { realpathSync } from "fs";
|
|
4190
|
-
import { readFile as
|
|
4191
|
-
import { join as
|
|
5271
|
+
import { readFile as readFile18 } from "fs/promises";
|
|
5272
|
+
import { join as join11, relative as relative2, sep as sep3 } from "path";
|
|
4192
5273
|
import chokidar from "chokidar";
|
|
5274
|
+
var DEFAULT_POLL_INTERVAL_MS = 1e4;
|
|
5275
|
+
function resolvePollInterval(optValue, envValue, log2) {
|
|
5276
|
+
if (optValue !== void 0) return optValue;
|
|
5277
|
+
if (envValue === void 0 || envValue === "") return DEFAULT_POLL_INTERVAL_MS;
|
|
5278
|
+
const parsed = Number(envValue);
|
|
5279
|
+
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
5280
|
+
log2?.warn("invalid SEEKSTONE_WATCH_POLL_INTERVAL, using default", {
|
|
5281
|
+
value: envValue,
|
|
5282
|
+
defaultMs: DEFAULT_POLL_INTERVAL_MS
|
|
5283
|
+
});
|
|
5284
|
+
return DEFAULT_POLL_INTERVAL_MS;
|
|
5285
|
+
}
|
|
5286
|
+
return parsed;
|
|
5287
|
+
}
|
|
4193
5288
|
function startWatcher(ctx2, log2, opts) {
|
|
4194
5289
|
const usePolling = opts?.usePolling ?? process.env.SEEKSTONE_WATCH_POLL === "1";
|
|
5290
|
+
const pollInterval = resolvePollInterval(
|
|
5291
|
+
opts?.pollInterval,
|
|
5292
|
+
process.env.SEEKSTONE_WATCH_POLL_INTERVAL,
|
|
5293
|
+
log2
|
|
5294
|
+
);
|
|
4195
5295
|
let vaultRoot2 = ctx2.vaultRoot;
|
|
4196
5296
|
try {
|
|
4197
5297
|
vaultRoot2 = realpathSync(ctx2.vaultRoot);
|
|
@@ -4200,7 +5300,7 @@ function startWatcher(ctx2, log2, opts) {
|
|
|
4200
5300
|
const toRel = (abs) => relative2(vaultRoot2, abs).split(sep3).join("/");
|
|
4201
5301
|
async function upsert(relPath, op) {
|
|
4202
5302
|
try {
|
|
4203
|
-
const raw = await
|
|
5303
|
+
const raw = await readFile18(join11(vaultRoot2, relPath), "utf8");
|
|
4204
5304
|
removeNoteBacklinks(ctx2, relPath);
|
|
4205
5305
|
upsertDoc(ctx2, buildDoc(relPath, raw));
|
|
4206
5306
|
addNoteBacklinks(ctx2, relPath, raw);
|
|
@@ -4227,8 +5327,8 @@ function startWatcher(ctx2, log2, opts) {
|
|
|
4227
5327
|
// The startup index build already covers existing files.
|
|
4228
5328
|
ignoreInitial: true,
|
|
4229
5329
|
usePolling,
|
|
4230
|
-
interval:
|
|
4231
|
-
binaryInterval:
|
|
5330
|
+
interval: pollInterval,
|
|
5331
|
+
binaryInterval: pollInterval * 2,
|
|
4232
5332
|
// Skip dot-dirs (.obsidian, .git, .trash, …) regardless of where the vault
|
|
4233
5333
|
// lives — matched on the path relative to the vault root.
|
|
4234
5334
|
ignored: (p) => {
|
|
@@ -4254,7 +5354,7 @@ function startWatcher(ctx2, log2, opts) {
|
|
|
4254
5354
|
}
|
|
4255
5355
|
|
|
4256
5356
|
// src/index.ts
|
|
4257
|
-
var VERSION = true ? "0.
|
|
5357
|
+
var VERSION = true ? "0.17.0" : "0.0.0-dev";
|
|
4258
5358
|
var intent = parseCliIntent(process.argv.slice(2));
|
|
4259
5359
|
if (intent === "version") {
|
|
4260
5360
|
process.stdout.write(`${VERSION}
|
|
@@ -4285,7 +5385,11 @@ if (intent === "init") {
|
|
|
4285
5385
|
if (intent === "fetch-model") {
|
|
4286
5386
|
const { runFetchModel: runFetchModel2 } = await Promise.resolve().then(() => (init_fetch_model(), fetch_model_exports));
|
|
4287
5387
|
const { homedir: homedir2 } = await import("os");
|
|
4288
|
-
const result = await runFetchModel2({
|
|
5388
|
+
const result = await runFetchModel2({
|
|
5389
|
+
env: process.env,
|
|
5390
|
+
homedir: homedir2(),
|
|
5391
|
+
argv: process.argv.slice(3)
|
|
5392
|
+
});
|
|
4289
5393
|
process.stdout.write(`${result.output.join("\n")}
|
|
4290
5394
|
`);
|
|
4291
5395
|
process.exit(result.exitCode);
|
|
@@ -4305,6 +5409,33 @@ log.info("building index", { vault: vaultRoot });
|
|
|
4305
5409
|
var { index, notes, backlinks, buildMs } = await buildIndex(vaultRoot);
|
|
4306
5410
|
log.info("index ready", { notes: notes.size, buildMs });
|
|
4307
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
|
+
}
|
|
4308
5439
|
var semanticCfg = resolveSemanticConfig(process.env, homedir());
|
|
4309
5440
|
if (semanticCfg) {
|
|
4310
5441
|
try {
|