@owrede/vault-memory 2.1.0 → 2.2.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/CHANGELOG.md +14 -0
- package/README.md +35 -11
- package/dist/cli.js +586 -135
- package/dist/cli.js.map +1 -1
- package/package.json +2 -1
package/dist/cli.js
CHANGED
|
@@ -89,7 +89,7 @@ function extractResourceLength(handle) {
|
|
|
89
89
|
if (firstSlash === -1) return 0;
|
|
90
90
|
return afterScheme.length - (firstSlash + 1);
|
|
91
91
|
}
|
|
92
|
-
var ServerConfigSchema, VaultConfigSchema, BriefOllamaConfigSchema, BriefConfigSchema, ContractsMcpClientConfigSchema, ContractsConfigSchema, DEFAULT_CONTRACTS_CONFIG, PluginConfigSchema, DEFAULT_PLUGIN_CONFIG, MemorySinkConfigSchema, MemoryConfigSchema, AppConfigSchema, DEFAULT_CONFIG;
|
|
92
|
+
var ServerConfigSchema, ContextFitConfigSchema, VaultConfigSchema, BriefOllamaConfigSchema, BriefConfigSchema, ContractsMcpClientConfigSchema, ContractsConfigSchema, DEFAULT_CONTRACTS_CONFIG, PluginConfigSchema, DEFAULT_PLUGIN_CONFIG, MemorySinkConfigSchema, MemoryConfigSchema, AppConfigSchema, DEFAULT_CONFIG;
|
|
93
93
|
var init_loader = __esm({
|
|
94
94
|
"src/config/loader.ts"() {
|
|
95
95
|
"use strict";
|
|
@@ -102,9 +102,17 @@ var init_loader = __esm({
|
|
|
102
102
|
reranker_backend: z.enum(["onnx", "ollama"]).optional(),
|
|
103
103
|
reranker_model_dir: z.string().optional()
|
|
104
104
|
});
|
|
105
|
+
ContextFitConfigSchema = z.object({
|
|
106
|
+
command: z.string().min(1).optional(),
|
|
107
|
+
tokenizer: z.string().min(1).optional(),
|
|
108
|
+
method: z.enum(["exact", "bm25", "sid", "graph", "hierarchy", "hybrid"]).optional()
|
|
109
|
+
});
|
|
105
110
|
VaultConfigSchema = z.object({
|
|
106
111
|
name: z.string().min(1),
|
|
107
112
|
path: z.string().min(1),
|
|
113
|
+
// ADR-008: retrieval engine. Omitted ⇒ "ollama" (back-compat default).
|
|
114
|
+
backend: z.enum(["ollama", "contextfit"]).optional(),
|
|
115
|
+
contextfit: ContextFitConfigSchema.optional(),
|
|
108
116
|
embedding_model: z.string().optional(),
|
|
109
117
|
secondary_embedding_model: z.string().optional(),
|
|
110
118
|
write_enabled: z.boolean().optional(),
|
|
@@ -228,7 +236,8 @@ async function addVault(opts) {
|
|
|
228
236
|
name: proposedName,
|
|
229
237
|
path: resolvedPath,
|
|
230
238
|
writeEnabled: opts.writeEnabled ?? false,
|
|
231
|
-
excludeGlobs: opts.excludeGlobs ?? DEFAULT_EXCLUDE_GLOBS
|
|
239
|
+
excludeGlobs: opts.excludeGlobs ?? DEFAULT_EXCLUDE_GLOBS,
|
|
240
|
+
...opts.backend ? { backend: opts.backend } : {}
|
|
232
241
|
});
|
|
233
242
|
await ensureFileExists(cfgFile);
|
|
234
243
|
await appendToFile(cfgFile, block);
|
|
@@ -252,13 +261,21 @@ function renderVaultBlock(input) {
|
|
|
252
261
|
`# Added by vault-memory add-vault on ${(/* @__PURE__ */ new Date()).toISOString()}`,
|
|
253
262
|
"[[vaults]]",
|
|
254
263
|
`name = ${JSON.stringify(input.name)}`,
|
|
255
|
-
`path = ${JSON.stringify(input.path)}
|
|
264
|
+
`path = ${JSON.stringify(input.path)}`
|
|
265
|
+
];
|
|
266
|
+
if (input.backend === "contextfit") {
|
|
267
|
+
lines.push(
|
|
268
|
+
`# ADR-008: CPU-only, token-native engine (no Ollama/embeddings/GPU).`,
|
|
269
|
+
`backend = "contextfit"`
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
lines.push(
|
|
256
273
|
`write_enabled = ${input.writeEnabled}`,
|
|
257
274
|
`exclude_globs = [`,
|
|
258
275
|
...input.excludeGlobs.map((g) => ` ${JSON.stringify(g)},`),
|
|
259
276
|
`]`,
|
|
260
277
|
""
|
|
261
|
-
|
|
278
|
+
);
|
|
262
279
|
return lines.join("\n");
|
|
263
280
|
}
|
|
264
281
|
async function ensureFileExists(path7) {
|
|
@@ -4922,6 +4939,290 @@ var init_hybrid = __esm({
|
|
|
4922
4939
|
}
|
|
4923
4940
|
});
|
|
4924
4941
|
|
|
4942
|
+
// src/adapters/retrieval/contextfit/cli.ts
|
|
4943
|
+
import spawn from "cross-spawn";
|
|
4944
|
+
function runContextFit(cfg, subcommandArgs, timeoutMs) {
|
|
4945
|
+
const globalArgs = ["--kb", cfg.kbPath];
|
|
4946
|
+
if (cfg.tokenizer) globalArgs.push("--tokenizer", cfg.tokenizer);
|
|
4947
|
+
const args2 = [...globalArgs, ...subcommandArgs];
|
|
4948
|
+
return new Promise((resolve7, reject) => {
|
|
4949
|
+
let child;
|
|
4950
|
+
try {
|
|
4951
|
+
child = spawn(cfg.command, args2, { stdio: ["pipe", "pipe", "pipe"] });
|
|
4952
|
+
} catch (err) {
|
|
4953
|
+
const e = err;
|
|
4954
|
+
reject(
|
|
4955
|
+
new ContextFitError(
|
|
4956
|
+
`contextfit spawn failed: ${e.message}`,
|
|
4957
|
+
e.code === "ENOENT" ? "ENOENT" : "NONZERO_EXIT"
|
|
4958
|
+
)
|
|
4959
|
+
);
|
|
4960
|
+
return;
|
|
4961
|
+
}
|
|
4962
|
+
child.stdin?.end();
|
|
4963
|
+
let stdout = "";
|
|
4964
|
+
let stderr = "";
|
|
4965
|
+
let settled = false;
|
|
4966
|
+
const timer = setTimeout(() => {
|
|
4967
|
+
if (settled) return;
|
|
4968
|
+
settled = true;
|
|
4969
|
+
child.kill("SIGKILL");
|
|
4970
|
+
reject(new ContextFitError(`contextfit timed out after ${timeoutMs}ms`, "TIMEOUT"));
|
|
4971
|
+
}, timeoutMs);
|
|
4972
|
+
child.stdout?.on("data", (d) => {
|
|
4973
|
+
stdout += d.toString();
|
|
4974
|
+
});
|
|
4975
|
+
child.stderr?.on("data", (d) => {
|
|
4976
|
+
stderr += d.toString();
|
|
4977
|
+
});
|
|
4978
|
+
child.on("error", (err) => {
|
|
4979
|
+
if (settled) return;
|
|
4980
|
+
settled = true;
|
|
4981
|
+
clearTimeout(timer);
|
|
4982
|
+
if (err.code === "ENOENT") {
|
|
4983
|
+
reject(
|
|
4984
|
+
new ContextFitError(
|
|
4985
|
+
`contextfit not found (tried '${cfg.command}'). Install it with \`pipx install contextfit\` (or pip), or set the command path.`,
|
|
4986
|
+
"ENOENT"
|
|
4987
|
+
)
|
|
4988
|
+
);
|
|
4989
|
+
} else {
|
|
4990
|
+
reject(new ContextFitError(`contextfit spawn failed: ${err.message}`));
|
|
4991
|
+
}
|
|
4992
|
+
});
|
|
4993
|
+
child.on("close", (codeNum) => {
|
|
4994
|
+
if (settled) return;
|
|
4995
|
+
settled = true;
|
|
4996
|
+
clearTimeout(timer);
|
|
4997
|
+
if (codeNum === 0) {
|
|
4998
|
+
resolve7({ stdout, stderr });
|
|
4999
|
+
} else {
|
|
5000
|
+
reject(
|
|
5001
|
+
new ContextFitError(
|
|
5002
|
+
`contextfit exited ${codeNum}: ${stderr.trim() || stdout.trim() || "(no output)"}`,
|
|
5003
|
+
"NONZERO_EXIT"
|
|
5004
|
+
)
|
|
5005
|
+
);
|
|
5006
|
+
}
|
|
5007
|
+
});
|
|
5008
|
+
});
|
|
5009
|
+
}
|
|
5010
|
+
async function runContextFitWithRetry(cfg, subcommandArgs, timeoutMs) {
|
|
5011
|
+
try {
|
|
5012
|
+
return await runContextFit(cfg, subcommandArgs, timeoutMs);
|
|
5013
|
+
} catch (err) {
|
|
5014
|
+
const isEbadf = err instanceof ContextFitError && /EBADF/.test(err.message);
|
|
5015
|
+
if (!isEbadf) throw err;
|
|
5016
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
5017
|
+
return runContextFit(cfg, subcommandArgs, timeoutMs);
|
|
5018
|
+
}
|
|
5019
|
+
}
|
|
5020
|
+
async function contextFitIngest(cfg, source, opts = {}) {
|
|
5021
|
+
const args2 = ["ingest", source, "--rebuild-index-after-ingest"];
|
|
5022
|
+
if (opts.chunkSize !== void 0) args2.push("--chunk-size", String(opts.chunkSize));
|
|
5023
|
+
if (opts.overlap !== void 0) args2.push("--overlap", String(opts.overlap));
|
|
5024
|
+
const { stdout } = await runContextFitWithRetry(cfg, args2, cfg.timeoutMs ?? 6e5);
|
|
5025
|
+
return stdout;
|
|
5026
|
+
}
|
|
5027
|
+
async function contextFitQuery(cfg, query, opts = {}) {
|
|
5028
|
+
const args2 = ["query", query, "--json"];
|
|
5029
|
+
if (opts.topK !== void 0) args2.push("--top-k", String(opts.topK));
|
|
5030
|
+
if (opts.method !== void 0) args2.push("--method", opts.method);
|
|
5031
|
+
const { stdout } = await runContextFitWithRetry(cfg, args2, cfg.timeoutMs ?? 3e4);
|
|
5032
|
+
return parseQueryOutput(stdout);
|
|
5033
|
+
}
|
|
5034
|
+
function parseQueryOutput(stdout) {
|
|
5035
|
+
const start = stdout.indexOf("{");
|
|
5036
|
+
if (start === -1) {
|
|
5037
|
+
throw new ContextFitError(
|
|
5038
|
+
`contextfit query produced no JSON: ${stdout.slice(0, 200)}`,
|
|
5039
|
+
"BAD_JSON"
|
|
5040
|
+
);
|
|
5041
|
+
}
|
|
5042
|
+
let parsed;
|
|
5043
|
+
try {
|
|
5044
|
+
parsed = JSON.parse(stdout.slice(start));
|
|
5045
|
+
} catch (err) {
|
|
5046
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
5047
|
+
throw new ContextFitError(`contextfit query JSON parse failed: ${msg}`, "BAD_JSON");
|
|
5048
|
+
}
|
|
5049
|
+
const obj = parsed;
|
|
5050
|
+
if (!Array.isArray(obj.chunks)) {
|
|
5051
|
+
throw new ContextFitError(
|
|
5052
|
+
`contextfit query JSON missing 'chunks' array (got keys: ${Object.keys(obj ?? {}).join(", ")})`,
|
|
5053
|
+
"BAD_JSON"
|
|
5054
|
+
);
|
|
5055
|
+
}
|
|
5056
|
+
return {
|
|
5057
|
+
query: typeof obj.query === "string" ? obj.query : "",
|
|
5058
|
+
method: typeof obj.method === "string" ? obj.method : "hybrid",
|
|
5059
|
+
retrieved_chunks: typeof obj.retrieved_chunks === "number" ? obj.retrieved_chunks : obj.chunks.length,
|
|
5060
|
+
chunks: obj.chunks
|
|
5061
|
+
};
|
|
5062
|
+
}
|
|
5063
|
+
async function contextFitProbe(cfg) {
|
|
5064
|
+
try {
|
|
5065
|
+
await new Promise((resolve7, reject) => {
|
|
5066
|
+
const child = spawn(cfg.command, ["--help"], { stdio: ["pipe", "pipe", "pipe"] });
|
|
5067
|
+
child.stdin?.end();
|
|
5068
|
+
child.on("error", reject);
|
|
5069
|
+
child.on(
|
|
5070
|
+
"close",
|
|
5071
|
+
(c) => c === 0 ? resolve7() : reject(new Error(`exit ${c}`))
|
|
5072
|
+
);
|
|
5073
|
+
});
|
|
5074
|
+
return true;
|
|
5075
|
+
} catch {
|
|
5076
|
+
return false;
|
|
5077
|
+
}
|
|
5078
|
+
}
|
|
5079
|
+
var ContextFitError;
|
|
5080
|
+
var init_cli = __esm({
|
|
5081
|
+
"src/adapters/retrieval/contextfit/cli.ts"() {
|
|
5082
|
+
"use strict";
|
|
5083
|
+
init_esm_shims();
|
|
5084
|
+
ContextFitError = class extends Error {
|
|
5085
|
+
constructor(message, code = "NONZERO_EXIT") {
|
|
5086
|
+
super(message);
|
|
5087
|
+
this.code = code;
|
|
5088
|
+
}
|
|
5089
|
+
code;
|
|
5090
|
+
name = "ContextFitError";
|
|
5091
|
+
};
|
|
5092
|
+
}
|
|
5093
|
+
});
|
|
5094
|
+
|
|
5095
|
+
// src/adapters/retrieval/contextfit/index.ts
|
|
5096
|
+
var contextfit_exports = {};
|
|
5097
|
+
__export(contextfit_exports, {
|
|
5098
|
+
cliConfigForVault: () => cliConfigForVault,
|
|
5099
|
+
contextFitKbDir: () => contextFitKbDir,
|
|
5100
|
+
indexVaultWithContextFit: () => indexVaultWithContextFit,
|
|
5101
|
+
searchVaultWithContextFit: () => searchVaultWithContextFit,
|
|
5102
|
+
sourceToNotePath: () => sourceToNotePath
|
|
5103
|
+
});
|
|
5104
|
+
import { homedir as homedir4 } from "os";
|
|
5105
|
+
import { rm } from "fs/promises";
|
|
5106
|
+
import { join as join4, relative, isAbsolute } from "path";
|
|
5107
|
+
function contextFitKbDir(vaultName) {
|
|
5108
|
+
return join4(homedir4(), ".vault-memory", "contextfit", vaultName);
|
|
5109
|
+
}
|
|
5110
|
+
function cliConfigForVault(vault) {
|
|
5111
|
+
const cfg = {
|
|
5112
|
+
command: vault.contextfit?.command ?? DEFAULT_COMMAND,
|
|
5113
|
+
kbPath: contextFitKbDir(vault.name)
|
|
5114
|
+
};
|
|
5115
|
+
if (vault.contextfit?.tokenizer) cfg.tokenizer = vault.contextfit.tokenizer;
|
|
5116
|
+
return cfg;
|
|
5117
|
+
}
|
|
5118
|
+
async function indexVaultWithContextFit(vault, opts = {}) {
|
|
5119
|
+
const log = opts.onProgress ?? (() => {
|
|
5120
|
+
});
|
|
5121
|
+
const cfg = cliConfigForVault(vault);
|
|
5122
|
+
const start = Date.now();
|
|
5123
|
+
log(`ContextFit: ingesting ${vault.path} \u2192 ${cfg.kbPath}`);
|
|
5124
|
+
const available = await contextFitProbe({ command: cfg.command });
|
|
5125
|
+
if (!available) {
|
|
5126
|
+
return {
|
|
5127
|
+
status: "failed",
|
|
5128
|
+
stats: "",
|
|
5129
|
+
durationMs: Date.now() - start,
|
|
5130
|
+
error: `ContextFit CLI not runnable (tried '${cfg.command}'). Install with \`pipx install contextfit\` or set [[vaults]].contextfit.command.`
|
|
5131
|
+
};
|
|
5132
|
+
}
|
|
5133
|
+
try {
|
|
5134
|
+
await rm(cfg.kbPath, { recursive: true, force: true });
|
|
5135
|
+
const stats = await contextFitIngest(cfg, vault.path);
|
|
5136
|
+
log(stats.trim().split("\n").slice(-3).join(" \xB7 "));
|
|
5137
|
+
return { status: "completed", stats, durationMs: Date.now() - start };
|
|
5138
|
+
} catch (err) {
|
|
5139
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
5140
|
+
return { status: "failed", stats: "", durationMs: Date.now() - start, error: message };
|
|
5141
|
+
}
|
|
5142
|
+
}
|
|
5143
|
+
function sourceToNotePath(source, vaultPath) {
|
|
5144
|
+
if (!source) return null;
|
|
5145
|
+
const rel = isAbsolute(source) ? relative(vaultPath, source) : source;
|
|
5146
|
+
if (rel.startsWith("..")) return null;
|
|
5147
|
+
return rel.split(/[\\/]/).join("/");
|
|
5148
|
+
}
|
|
5149
|
+
function chunkToHit(chunk, vault) {
|
|
5150
|
+
const notePath = sourceToNotePath(chunk.metadata?.source, vault.path);
|
|
5151
|
+
if (notePath === null) return null;
|
|
5152
|
+
const base = notePath.split("/").pop() ?? notePath;
|
|
5153
|
+
const noteTitle = base.replace(/\.md$/i, "");
|
|
5154
|
+
const hit = {
|
|
5155
|
+
vault: vault.name,
|
|
5156
|
+
notePath,
|
|
5157
|
+
noteTitle,
|
|
5158
|
+
chunkText: chunk.preview ?? "",
|
|
5159
|
+
chunkIdx: chunk.chunk_id,
|
|
5160
|
+
headingPath: null,
|
|
5161
|
+
score: chunk.score,
|
|
5162
|
+
scoreBreakdown: { contextfit: chunk.score }
|
|
5163
|
+
};
|
|
5164
|
+
return hit;
|
|
5165
|
+
}
|
|
5166
|
+
async function searchVaultWithContextFit(vault, query, opts = {}) {
|
|
5167
|
+
const cfg = cliConfigForVault(vault);
|
|
5168
|
+
const method = vault.contextfit?.method ?? "hybrid";
|
|
5169
|
+
const result = await contextFitQuery(cfg, query, {
|
|
5170
|
+
topK: opts.topK ?? 10,
|
|
5171
|
+
method
|
|
5172
|
+
});
|
|
5173
|
+
const hits = [];
|
|
5174
|
+
for (const chunk of result.chunks) {
|
|
5175
|
+
const hit = chunkToHit(chunk, vault);
|
|
5176
|
+
if (hit) hits.push(hit);
|
|
5177
|
+
}
|
|
5178
|
+
return hits;
|
|
5179
|
+
}
|
|
5180
|
+
var DEFAULT_COMMAND;
|
|
5181
|
+
var init_contextfit = __esm({
|
|
5182
|
+
"src/adapters/retrieval/contextfit/index.ts"() {
|
|
5183
|
+
"use strict";
|
|
5184
|
+
init_esm_shims();
|
|
5185
|
+
init_cli();
|
|
5186
|
+
DEFAULT_COMMAND = "contextfit";
|
|
5187
|
+
}
|
|
5188
|
+
});
|
|
5189
|
+
|
|
5190
|
+
// src/search/dispatch.ts
|
|
5191
|
+
function isContextFit(vault) {
|
|
5192
|
+
return vault.config.backend === "contextfit";
|
|
5193
|
+
}
|
|
5194
|
+
async function searchVaults(opts) {
|
|
5195
|
+
const topK = opts.topK ?? 10;
|
|
5196
|
+
const cfVaults = opts.vaults.filter(isContextFit);
|
|
5197
|
+
const ollamaVaults = opts.vaults.filter((v) => !isContextFit(v));
|
|
5198
|
+
if (cfVaults.length === 0) {
|
|
5199
|
+
return hybridSearch(opts);
|
|
5200
|
+
}
|
|
5201
|
+
const { searchVaultWithContextFit: searchVaultWithContextFit2 } = await Promise.resolve().then(() => (init_contextfit(), contextfit_exports));
|
|
5202
|
+
const cfPromise = Promise.all(
|
|
5203
|
+
cfVaults.map(
|
|
5204
|
+
(v) => searchVaultWithContextFit2(v.config, opts.query, { topK }).catch((err) => {
|
|
5205
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
5206
|
+
console.error(`[search:${v.config.name}] ContextFit query failed: ${msg}`);
|
|
5207
|
+
return [];
|
|
5208
|
+
})
|
|
5209
|
+
)
|
|
5210
|
+
);
|
|
5211
|
+
const ollamaPromise = ollamaVaults.length > 0 ? hybridSearch({ ...opts, vaults: ollamaVaults }) : Promise.resolve([]);
|
|
5212
|
+
const [cfResultsNested, ollamaResults] = await Promise.all([cfPromise, ollamaPromise]);
|
|
5213
|
+
const cfResults = cfResultsNested.flat();
|
|
5214
|
+
const merged = [...ollamaResults, ...cfResults];
|
|
5215
|
+
merged.sort((a, b) => b.score - a.score);
|
|
5216
|
+
return merged.slice(0, topK);
|
|
5217
|
+
}
|
|
5218
|
+
var init_dispatch = __esm({
|
|
5219
|
+
"src/search/dispatch.ts"() {
|
|
5220
|
+
"use strict";
|
|
5221
|
+
init_esm_shims();
|
|
5222
|
+
init_hybrid();
|
|
5223
|
+
}
|
|
5224
|
+
});
|
|
5225
|
+
|
|
4925
5226
|
// src/search/glob.ts
|
|
4926
5227
|
function compile(pattern) {
|
|
4927
5228
|
const cached = cache.get(pattern);
|
|
@@ -4969,6 +5270,7 @@ var init_search = __esm({
|
|
|
4969
5270
|
"use strict";
|
|
4970
5271
|
init_esm_shims();
|
|
4971
5272
|
init_hybrid();
|
|
5273
|
+
init_dispatch();
|
|
4972
5274
|
init_glob();
|
|
4973
5275
|
}
|
|
4974
5276
|
});
|
|
@@ -5014,7 +5316,7 @@ var init_reranker = __esm({
|
|
|
5014
5316
|
// src/rerank/onnx-reranker.ts
|
|
5015
5317
|
import { readFile as readFile3 } from "fs/promises";
|
|
5016
5318
|
import { existsSync } from "fs";
|
|
5017
|
-
import { join as
|
|
5319
|
+
import { join as join5 } from "path";
|
|
5018
5320
|
function sigmoid(x) {
|
|
5019
5321
|
return 1 / (1 + Math.exp(-x));
|
|
5020
5322
|
}
|
|
@@ -5092,8 +5394,8 @@ var init_onnx_reranker = __esm({
|
|
|
5092
5394
|
if (this.loaded) return this.loaded;
|
|
5093
5395
|
if (this.loading) return this.loading;
|
|
5094
5396
|
this.loading = (async () => {
|
|
5095
|
-
const modelPath =
|
|
5096
|
-
const tokenizerPath =
|
|
5397
|
+
const modelPath = join5(this.modelDir, "model_quantized.onnx");
|
|
5398
|
+
const tokenizerPath = join5(this.modelDir, "tokenizer.json");
|
|
5097
5399
|
if (!existsSync(modelPath)) {
|
|
5098
5400
|
throw new Error(
|
|
5099
5401
|
`OnnxReranker: model file not found at ${modelPath}. Run: curl -L https://huggingface.co/onnx-community/bge-reranker-v2-m3-ONNX/resolve/main/onnx/model_quantized.onnx -o ${modelPath}`
|
|
@@ -6419,53 +6721,62 @@ async function indexVault(vault, options) {
|
|
|
6419
6721
|
const mode = options.mode ?? "incremental";
|
|
6420
6722
|
const log = options.onProgress ?? (() => {
|
|
6421
6723
|
});
|
|
6422
|
-
|
|
6423
|
-
const
|
|
6424
|
-
|
|
6425
|
-
|
|
6426
|
-
}
|
|
6427
|
-
const modelExists = await options.ollama.modelExists(options.embeddingModel);
|
|
6428
|
-
if (!modelExists) {
|
|
6429
|
-
throw new Error(
|
|
6430
|
-
`Embedding model "${options.embeddingModel}" not found in Ollama. Available: ${health.models?.join(", ") ?? "(none)"}. Run: ollama pull ${options.embeddingModel}`
|
|
6431
|
-
);
|
|
6432
|
-
}
|
|
6433
|
-
const probe = await options.ollama.embed({
|
|
6434
|
-
model: options.embeddingModel,
|
|
6435
|
-
texts: ["probe"]
|
|
6436
|
-
});
|
|
6437
|
-
const dim = probe.dim;
|
|
6438
|
-
const modelRow = vault.db.models.upsert({
|
|
6439
|
-
name: options.embeddingModel,
|
|
6440
|
-
provider: "ollama",
|
|
6441
|
-
dim
|
|
6442
|
-
});
|
|
6724
|
+
const embedMode = options.embeddings ?? "ollama";
|
|
6725
|
+
const ollama = options.ollama;
|
|
6726
|
+
let dim = 0;
|
|
6727
|
+
let modelRow = null;
|
|
6443
6728
|
let secondaryModelRow = null;
|
|
6444
|
-
if (
|
|
6445
|
-
|
|
6446
|
-
|
|
6447
|
-
|
|
6448
|
-
|
|
6729
|
+
if (embedMode === "ollama") {
|
|
6730
|
+
if (!ollama) {
|
|
6731
|
+
throw new Error("indexVault: embeddings='ollama' requires an OllamaClient (options.ollama).");
|
|
6732
|
+
}
|
|
6733
|
+
log(`Probing Ollama model: ${options.embeddingModel}`);
|
|
6734
|
+
const health = await ollama.healthCheck();
|
|
6735
|
+
if (!health.ok) {
|
|
6736
|
+
throw new Error(`Ollama unreachable: ${health.error ?? "unknown error"}`);
|
|
6737
|
+
}
|
|
6738
|
+
const modelExists = await ollama.modelExists(options.embeddingModel);
|
|
6739
|
+
if (!modelExists) {
|
|
6449
6740
|
throw new Error(
|
|
6450
|
-
`
|
|
6741
|
+
`Embedding model "${options.embeddingModel}" not found in Ollama. Available: ${health.models?.join(", ") ?? "(none)"}. Run: ollama pull ${options.embeddingModel}`
|
|
6451
6742
|
);
|
|
6452
6743
|
}
|
|
6453
|
-
const
|
|
6454
|
-
model:
|
|
6744
|
+
const probe = await ollama.embed({
|
|
6745
|
+
model: options.embeddingModel,
|
|
6455
6746
|
texts: ["probe"]
|
|
6456
6747
|
});
|
|
6457
|
-
|
|
6458
|
-
|
|
6748
|
+
dim = probe.dim;
|
|
6749
|
+
modelRow = vault.db.models.upsert({
|
|
6750
|
+
name: options.embeddingModel,
|
|
6459
6751
|
provider: "ollama",
|
|
6460
|
-
dim
|
|
6461
|
-
active: false
|
|
6752
|
+
dim
|
|
6462
6753
|
});
|
|
6463
|
-
|
|
6754
|
+
if (options.secondaryEmbeddingModel) {
|
|
6755
|
+
const secName = options.secondaryEmbeddingModel;
|
|
6756
|
+
log(`Probing secondary (shadow) model: ${secName}`);
|
|
6757
|
+
const secExists = await ollama.modelExists(secName);
|
|
6758
|
+
if (!secExists) {
|
|
6759
|
+
throw new Error(
|
|
6760
|
+
`Secondary embedding model "${secName}" not found in Ollama. Run: ollama pull ${secName}`
|
|
6761
|
+
);
|
|
6762
|
+
}
|
|
6763
|
+
const secProbe = await ollama.embed({
|
|
6764
|
+
model: secName,
|
|
6765
|
+
texts: ["probe"]
|
|
6766
|
+
});
|
|
6767
|
+
const row = vault.db.models.upsert({
|
|
6768
|
+
name: secName,
|
|
6769
|
+
provider: "ollama",
|
|
6770
|
+
dim: secProbe.dim,
|
|
6771
|
+
active: false
|
|
6772
|
+
});
|
|
6773
|
+
secondaryModelRow = { id: row.id, dim: row.dim };
|
|
6774
|
+
}
|
|
6464
6775
|
}
|
|
6465
6776
|
vault.db.audit.startRun({
|
|
6466
6777
|
runId,
|
|
6467
6778
|
vaultName: vault.config.name,
|
|
6468
|
-
modelId: modelRow
|
|
6779
|
+
modelId: modelRow?.id ?? null,
|
|
6469
6780
|
trigger: mode === "full" ? "manual-full" : "manual-incremental"
|
|
6470
6781
|
});
|
|
6471
6782
|
let notesIndexed = 0;
|
|
@@ -6556,36 +6867,38 @@ async function indexVault(vault, options) {
|
|
|
6556
6867
|
`[indexer:${vault.config.name}] section build failed for ${parsed.relativePath}: ${message} \u2014 skipping sections for this note`
|
|
6557
6868
|
);
|
|
6558
6869
|
}
|
|
6559
|
-
|
|
6560
|
-
|
|
6561
|
-
|
|
6562
|
-
});
|
|
6563
|
-
if (embedResult.dim !== dim) {
|
|
6564
|
-
throw new Error(`Embedding dimension mismatch: expected ${dim}, got ${embedResult.dim}`);
|
|
6565
|
-
}
|
|
6566
|
-
const embeddingInputs = chunkIds.map((chunkId, i) => ({
|
|
6567
|
-
chunkId,
|
|
6568
|
-
modelId: modelRow.id,
|
|
6569
|
-
vector: embedResult.vectors[i]
|
|
6570
|
-
}));
|
|
6571
|
-
vault.db.embeddings.insertBatch(embeddingInputs);
|
|
6572
|
-
if (secondaryModelRow) {
|
|
6573
|
-
const secEmbed = await options.ollama.embed({
|
|
6574
|
-
model: options.secondaryEmbeddingModel,
|
|
6870
|
+
if (embedMode === "ollama") {
|
|
6871
|
+
const embedResult = await ollama.embed({
|
|
6872
|
+
model: options.embeddingModel,
|
|
6575
6873
|
texts: chunks.map((c) => c.text)
|
|
6576
6874
|
});
|
|
6577
|
-
if (
|
|
6578
|
-
throw new Error(
|
|
6579
|
-
|
|
6875
|
+
if (embedResult.dim !== dim) {
|
|
6876
|
+
throw new Error(`Embedding dimension mismatch: expected ${dim}, got ${embedResult.dim}`);
|
|
6877
|
+
}
|
|
6878
|
+
const embeddingInputs = chunkIds.map((chunkId, i) => ({
|
|
6879
|
+
chunkId,
|
|
6880
|
+
modelId: modelRow.id,
|
|
6881
|
+
vector: embedResult.vectors[i]
|
|
6882
|
+
}));
|
|
6883
|
+
vault.db.embeddings.insertBatch(embeddingInputs);
|
|
6884
|
+
if (secondaryModelRow) {
|
|
6885
|
+
const secEmbed = await ollama.embed({
|
|
6886
|
+
model: options.secondaryEmbeddingModel,
|
|
6887
|
+
texts: chunks.map((c) => c.text)
|
|
6888
|
+
});
|
|
6889
|
+
if (secEmbed.dim !== secondaryModelRow.dim) {
|
|
6890
|
+
throw new Error(
|
|
6891
|
+
`Secondary embedding dimension mismatch: expected ${secondaryModelRow.dim}, got ${secEmbed.dim}`
|
|
6892
|
+
);
|
|
6893
|
+
}
|
|
6894
|
+
vault.db.embeddings.insertBatch(
|
|
6895
|
+
chunkIds.map((chunkId, i) => ({
|
|
6896
|
+
chunkId,
|
|
6897
|
+
modelId: secondaryModelRow.id,
|
|
6898
|
+
vector: secEmbed.vectors[i]
|
|
6899
|
+
}))
|
|
6580
6900
|
);
|
|
6581
6901
|
}
|
|
6582
|
-
vault.db.embeddings.insertBatch(
|
|
6583
|
-
chunkIds.map((chunkId, i) => ({
|
|
6584
|
-
chunkId,
|
|
6585
|
-
modelId: secondaryModelRow.id,
|
|
6586
|
-
vector: secEmbed.vectors[i]
|
|
6587
|
-
}))
|
|
6588
|
-
);
|
|
6589
6902
|
}
|
|
6590
6903
|
insertWikilinks(vault, noteId, parsed.wikilinks, firstPassResolver);
|
|
6591
6904
|
writeAllEdges(vault, noteId, parsed, firstPassResolver);
|
|
@@ -6850,16 +7163,24 @@ async function indexNote(options) {
|
|
|
6850
7163
|
isNew: false
|
|
6851
7164
|
};
|
|
6852
7165
|
}
|
|
6853
|
-
const
|
|
6854
|
-
|
|
6855
|
-
|
|
6856
|
-
|
|
6857
|
-
|
|
6858
|
-
|
|
6859
|
-
|
|
6860
|
-
|
|
6861
|
-
|
|
6862
|
-
|
|
7166
|
+
const embedMode = options.embeddings ?? "ollama";
|
|
7167
|
+
let activeModel = null;
|
|
7168
|
+
if (embedMode === "ollama") {
|
|
7169
|
+
if (!ollama) {
|
|
7170
|
+
throw new Error("single-indexer: embeddings='ollama' requires an OllamaClient.");
|
|
7171
|
+
}
|
|
7172
|
+
const am = vault.db.models.getActive();
|
|
7173
|
+
if (!am) {
|
|
7174
|
+
throw new Error(
|
|
7175
|
+
`single-indexer: no active embedding model in DB. Run a full index first to register "${embeddingModel}".`
|
|
7176
|
+
);
|
|
7177
|
+
}
|
|
7178
|
+
if (am.name !== embeddingModel) {
|
|
7179
|
+
throw new Error(
|
|
7180
|
+
`single-indexer: active model "${am.name}" does not match requested "${embeddingModel}". Run a full re-index to switch models.`
|
|
7181
|
+
);
|
|
7182
|
+
}
|
|
7183
|
+
activeModel = am;
|
|
6863
7184
|
}
|
|
6864
7185
|
const upsert = vault.db.notes.upsertByPath({
|
|
6865
7186
|
path: parsed.relativePath,
|
|
@@ -6872,6 +7193,7 @@ async function indexNote(options) {
|
|
|
6872
7193
|
wordCount: parsed.wordCount
|
|
6873
7194
|
});
|
|
6874
7195
|
vault.db.aliases.setForNote(upsert.id, extractAliases(parsed.frontmatter));
|
|
7196
|
+
vault.db.sections.deleteByNote(upsert.id);
|
|
6875
7197
|
vault.db.chunks.deleteByNote(upsert.id);
|
|
6876
7198
|
vault.db.wikilinks.deleteByNote(upsert.id);
|
|
6877
7199
|
vault.db.edges.deleteByNote(upsert.id);
|
|
@@ -6901,41 +7223,52 @@ async function indexNote(options) {
|
|
|
6901
7223
|
chunkIdFragment: computeChunkIdFragment(c.text)
|
|
6902
7224
|
}))
|
|
6903
7225
|
);
|
|
6904
|
-
|
|
6905
|
-
|
|
6906
|
-
|
|
6907
|
-
|
|
6908
|
-
|
|
6909
|
-
|
|
6910
|
-
|
|
7226
|
+
try {
|
|
7227
|
+
buildSectionsForNote(vault, upsert.id, parsed.content, chunkIds);
|
|
7228
|
+
} catch (err) {
|
|
7229
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
7230
|
+
process.stderr.write(
|
|
7231
|
+
`[single-indexer:${vault.config.name}] section build failed for ${parsed.relativePath}: ${message}
|
|
7232
|
+
`
|
|
6911
7233
|
);
|
|
6912
7234
|
}
|
|
6913
|
-
|
|
6914
|
-
|
|
6915
|
-
|
|
6916
|
-
|
|
6917
|
-
|
|
6918
|
-
|
|
6919
|
-
|
|
6920
|
-
|
|
6921
|
-
|
|
6922
|
-
|
|
6923
|
-
|
|
6924
|
-
|
|
6925
|
-
|
|
6926
|
-
|
|
6927
|
-
|
|
6928
|
-
|
|
6929
|
-
|
|
7235
|
+
if (embedMode === "ollama") {
|
|
7236
|
+
const embedResult = await ollama.embed({
|
|
7237
|
+
model: embeddingModel,
|
|
7238
|
+
texts: chunks.map((c) => c.text)
|
|
7239
|
+
});
|
|
7240
|
+
if (embedResult.dim !== activeModel.dim) {
|
|
7241
|
+
throw new Error(
|
|
7242
|
+
`single-indexer: embedding dim ${embedResult.dim} does not match registered dim ${activeModel.dim} for model "${embeddingModel}".`
|
|
7243
|
+
);
|
|
7244
|
+
}
|
|
7245
|
+
vault.db.embeddings.insertBatch(
|
|
7246
|
+
chunkIds.map((chunkId, i) => ({
|
|
7247
|
+
chunkId,
|
|
7248
|
+
modelId: activeModel.id,
|
|
7249
|
+
vector: embedResult.vectors[i]
|
|
7250
|
+
}))
|
|
7251
|
+
);
|
|
7252
|
+
if (secondaryName) {
|
|
7253
|
+
const secondaryModel = vault.db.models.getByName(secondaryName);
|
|
7254
|
+
if (secondaryModel && secondaryModel.id !== activeModel.id) {
|
|
7255
|
+
const secEmbed = await ollama.embed({
|
|
7256
|
+
model: secondaryName,
|
|
7257
|
+
texts: chunks.map((c) => c.text)
|
|
7258
|
+
});
|
|
7259
|
+
if (secEmbed.dim !== secondaryModel.dim) {
|
|
7260
|
+
throw new Error(
|
|
7261
|
+
`single-indexer: shadow embedding dim ${secEmbed.dim} does not match registered dim ${secondaryModel.dim} for "${secondaryName}".`
|
|
7262
|
+
);
|
|
7263
|
+
}
|
|
7264
|
+
vault.db.embeddings.insertBatch(
|
|
7265
|
+
chunkIds.map((chunkId, i) => ({
|
|
7266
|
+
chunkId,
|
|
7267
|
+
modelId: secondaryModel.id,
|
|
7268
|
+
vector: secEmbed.vectors[i]
|
|
7269
|
+
}))
|
|
6930
7270
|
);
|
|
6931
7271
|
}
|
|
6932
|
-
vault.db.embeddings.insertBatch(
|
|
6933
|
-
chunkIds.map((chunkId, i) => ({
|
|
6934
|
-
chunkId,
|
|
6935
|
-
modelId: secondaryModel.id,
|
|
6936
|
-
vector: secEmbed.vectors[i]
|
|
6937
|
-
}))
|
|
6938
|
-
);
|
|
6939
7272
|
}
|
|
6940
7273
|
}
|
|
6941
7274
|
insertWikilinks2(vault, upsert.id, parsed.wikilinks);
|
|
@@ -7027,6 +7360,7 @@ async function catchupVault(options) {
|
|
|
7027
7360
|
});
|
|
7028
7361
|
let reindexed = 0;
|
|
7029
7362
|
const knownPaths = /* @__PURE__ */ new Set();
|
|
7363
|
+
const isContextFit2 = vault.config.backend === "contextfit";
|
|
7030
7364
|
for (const file of files) {
|
|
7031
7365
|
const parsed = await parseNote(file, vault.config.path).catch(() => null);
|
|
7032
7366
|
if (!parsed) continue;
|
|
@@ -7039,7 +7373,7 @@ async function catchupVault(options) {
|
|
|
7039
7373
|
vault,
|
|
7040
7374
|
absolutePath: file,
|
|
7041
7375
|
embeddingModel: options.embeddingModel,
|
|
7042
|
-
ollama: options.ollama
|
|
7376
|
+
...isContextFit2 ? { embeddings: "none" } : { ollama: options.ollama }
|
|
7043
7377
|
});
|
|
7044
7378
|
if (result.status === "indexed") {
|
|
7045
7379
|
reindexed++;
|
|
@@ -7056,6 +7390,13 @@ async function catchupVault(options) {
|
|
|
7056
7390
|
}
|
|
7057
7391
|
}
|
|
7058
7392
|
}
|
|
7393
|
+
if (isContextFit2 && (reindexed > 0 || removed > 0)) {
|
|
7394
|
+
const { indexVaultWithContextFit: indexVaultWithContextFit2 } = await Promise.resolve().then(() => (init_contextfit(), contextfit_exports));
|
|
7395
|
+
const r = await indexVaultWithContextFit2(vault.config, { onProgress: log });
|
|
7396
|
+
log(
|
|
7397
|
+
r.status === "completed" ? `catch-up: ContextFit KB rebuilt (${r.durationMs}ms)` : `catch-up: ContextFit KB rebuild failed: ${r.error}`
|
|
7398
|
+
);
|
|
7399
|
+
}
|
|
7059
7400
|
return {
|
|
7060
7401
|
scanned: files.length,
|
|
7061
7402
|
reindexed,
|
|
@@ -7063,9 +7404,9 @@ async function catchupVault(options) {
|
|
|
7063
7404
|
durationMs: Date.now() - started
|
|
7064
7405
|
};
|
|
7065
7406
|
}
|
|
7066
|
-
function joinAbs(root,
|
|
7067
|
-
if (root.endsWith("/")) return `${root}${
|
|
7068
|
-
return `${root}/${
|
|
7407
|
+
function joinAbs(root, relative6) {
|
|
7408
|
+
if (root.endsWith("/")) return `${root}${relative6}`;
|
|
7409
|
+
return `${root}/${relative6}`;
|
|
7069
7410
|
}
|
|
7070
7411
|
var init_catchup = __esm({
|
|
7071
7412
|
"src/indexer/catchup.ts"() {
|
|
@@ -7317,10 +7658,10 @@ var init_indexer2 = __esm({
|
|
|
7317
7658
|
|
|
7318
7659
|
// src/adapters/delivery/obsidian-fs/fs.ts
|
|
7319
7660
|
import { promises as fs5 } from "fs";
|
|
7320
|
-
import { dirname, isAbsolute, resolve as resolve6, sep as sep5 } from "path";
|
|
7661
|
+
import { dirname, isAbsolute as isAbsolute2, resolve as resolve6, sep as sep5 } from "path";
|
|
7321
7662
|
import { randomBytes } from "crypto";
|
|
7322
7663
|
async function atomicWriteFile(absPath, content) {
|
|
7323
|
-
if (!
|
|
7664
|
+
if (!isAbsolute2(absPath)) {
|
|
7324
7665
|
throw new Error(`atomicWriteFile requires an absolute path: ${absPath}`);
|
|
7325
7666
|
}
|
|
7326
7667
|
const parent = dirname(absPath);
|
|
@@ -7342,7 +7683,7 @@ async function safeJoinInsideVault(vaultRoot, relativePath) {
|
|
|
7342
7683
|
if (typeof relativePath !== "string" || relativePath.length === 0) {
|
|
7343
7684
|
throw new OutsideVaultError(relativePath, vaultRoot);
|
|
7344
7685
|
}
|
|
7345
|
-
if (
|
|
7686
|
+
if (isAbsolute2(relativePath)) {
|
|
7346
7687
|
throw new OutsideVaultError(relativePath, vaultRoot);
|
|
7347
7688
|
}
|
|
7348
7689
|
const root = resolve6(vaultRoot);
|
|
@@ -9902,14 +10243,14 @@ var init_get = __esm({
|
|
|
9902
10243
|
|
|
9903
10244
|
// src/brief/lock.ts
|
|
9904
10245
|
import { open, readFile as readFile5, unlink, mkdir as mkdir2 } from "fs/promises";
|
|
9905
|
-
import { homedir as
|
|
9906
|
-
import { join as
|
|
10246
|
+
import { homedir as homedir5 } from "os";
|
|
10247
|
+
import { join as join7 } from "path";
|
|
9907
10248
|
function lockDir(rootOverride) {
|
|
9908
|
-
if (rootOverride !== void 0) return
|
|
9909
|
-
return
|
|
10249
|
+
if (rootOverride !== void 0) return join7(rootOverride, "locks");
|
|
10250
|
+
return join7(homedir5(), ".vault-memory", "locks");
|
|
9910
10251
|
}
|
|
9911
10252
|
function lockPath(vaultName, rootOverride) {
|
|
9912
|
-
return
|
|
10253
|
+
return join7(lockDir(rootOverride), `${vaultName}.lock`);
|
|
9913
10254
|
}
|
|
9914
10255
|
function isProcessAlive(pid) {
|
|
9915
10256
|
try {
|
|
@@ -10746,6 +11087,9 @@ var init_watcher = __esm({
|
|
|
10746
11087
|
queue;
|
|
10747
11088
|
opts;
|
|
10748
11089
|
started = false;
|
|
11090
|
+
/** ADR-008: debounce timer for ContextFit KB re-ingest (coalesces bursts). */
|
|
11091
|
+
cfReingestTimer = null;
|
|
11092
|
+
cfReingestInFlight = false;
|
|
10749
11093
|
constructor(options) {
|
|
10750
11094
|
this.opts = {
|
|
10751
11095
|
vault: options.vault,
|
|
@@ -10793,12 +11137,51 @@ var init_watcher = __esm({
|
|
|
10793
11137
|
if (!this.started) return;
|
|
10794
11138
|
this.started = false;
|
|
10795
11139
|
this.queue.shutdown();
|
|
11140
|
+
if (this.cfReingestTimer) {
|
|
11141
|
+
clearTimeout(this.cfReingestTimer);
|
|
11142
|
+
this.cfReingestTimer = null;
|
|
11143
|
+
}
|
|
10796
11144
|
if (this.fsWatcher) {
|
|
10797
11145
|
await this.fsWatcher.close();
|
|
10798
11146
|
this.fsWatcher = null;
|
|
10799
11147
|
}
|
|
10800
11148
|
}
|
|
10801
11149
|
// ─── internal ──────────────────────────────────────────────────────────
|
|
11150
|
+
/**
|
|
11151
|
+
* ADR-008: schedule a debounced full ContextFit KB re-ingest. Per-note
|
|
11152
|
+
* changes update the SQLite layer immediately (via indexNote); the ContextFit
|
|
11153
|
+
* search KB is rebuilt in one coalesced pass ~1.5s after the last change so a
|
|
11154
|
+
* burst of edits triggers a single re-ingest. CPU-only and fast.
|
|
11155
|
+
*/
|
|
11156
|
+
scheduleContextFitReingest() {
|
|
11157
|
+
if (this.cfReingestTimer) clearTimeout(this.cfReingestTimer);
|
|
11158
|
+
this.cfReingestTimer = setTimeout(() => {
|
|
11159
|
+
this.cfReingestTimer = null;
|
|
11160
|
+
void this.runContextFitReingest();
|
|
11161
|
+
}, 1500);
|
|
11162
|
+
}
|
|
11163
|
+
async runContextFitReingest() {
|
|
11164
|
+
if (this.cfReingestInFlight) {
|
|
11165
|
+
this.scheduleContextFitReingest();
|
|
11166
|
+
return;
|
|
11167
|
+
}
|
|
11168
|
+
this.cfReingestInFlight = true;
|
|
11169
|
+
try {
|
|
11170
|
+
const { indexVaultWithContextFit: indexVaultWithContextFit2 } = await Promise.resolve().then(() => (init_contextfit(), contextfit_exports));
|
|
11171
|
+
const r = await indexVaultWithContextFit2(this.opts.vault.config, {});
|
|
11172
|
+
if (r.status === "completed") {
|
|
11173
|
+
this.opts.log(`ContextFit KB refreshed (${r.durationMs}ms)`);
|
|
11174
|
+
} else {
|
|
11175
|
+
this.opts.log(`ContextFit KB refresh failed: ${r.error}`);
|
|
11176
|
+
}
|
|
11177
|
+
} catch (err) {
|
|
11178
|
+
this.opts.log(
|
|
11179
|
+
`ContextFit KB refresh error: ${err instanceof Error ? err.message : String(err)}`
|
|
11180
|
+
);
|
|
11181
|
+
} finally {
|
|
11182
|
+
this.cfReingestInFlight = false;
|
|
11183
|
+
}
|
|
11184
|
+
}
|
|
10802
11185
|
onFsEvent(absolutePath, kind) {
|
|
10803
11186
|
if (!absolutePath.endsWith(".md")) return;
|
|
10804
11187
|
const relativePath = this.toRelative(absolutePath);
|
|
@@ -10817,10 +11200,12 @@ var init_watcher = __esm({
|
|
|
10817
11200
|
}
|
|
10818
11201
|
async handleFlush(event) {
|
|
10819
11202
|
const relativePath = this.toRelative(event.path);
|
|
11203
|
+
const isContextFit2 = this.opts.vault.config.backend === "contextfit";
|
|
10820
11204
|
if (event.kind === "delete") {
|
|
10821
11205
|
const result2 = removeNote(this.opts.vault, event.path);
|
|
10822
11206
|
if (result2.removed) {
|
|
10823
11207
|
this.opts.log(`removed ${relativePath}`);
|
|
11208
|
+
if (isContextFit2) this.scheduleContextFitReingest();
|
|
10824
11209
|
} else {
|
|
10825
11210
|
this.opts.log(`delete event for unknown ${relativePath} (skip)`);
|
|
10826
11211
|
}
|
|
@@ -10831,13 +11216,16 @@ var init_watcher = __esm({
|
|
|
10831
11216
|
absolutePath: event.path,
|
|
10832
11217
|
embeddingModel: this.opts.embeddingModel,
|
|
10833
11218
|
secondaryEmbeddingModel: this.opts.secondaryEmbeddingModel,
|
|
10834
|
-
|
|
11219
|
+
// ADR-008: ContextFit vaults build the SQLite layer without embeddings;
|
|
11220
|
+
// their search KB is refreshed by the debounced re-ingest below.
|
|
11221
|
+
...isContextFit2 ? { embeddings: "none" } : { ollama: this.opts.ollama }
|
|
10835
11222
|
});
|
|
10836
11223
|
switch (result.status) {
|
|
10837
11224
|
case "indexed":
|
|
10838
11225
|
this.opts.log(
|
|
10839
11226
|
`indexed ${relativePath} (${result.isNew ? "new" : "updated"}, ${result.chunksCreated} chunks)`
|
|
10840
11227
|
);
|
|
11228
|
+
if (isContextFit2) this.scheduleContextFitReingest();
|
|
10841
11229
|
break;
|
|
10842
11230
|
case "unchanged":
|
|
10843
11231
|
break;
|
|
@@ -14521,6 +14909,13 @@ async function handleWriteNote(registry, vault, parsed) {
|
|
|
14521
14909
|
if (res.observedValue !== void 0) out.observedValue = res.observedValue;
|
|
14522
14910
|
return out;
|
|
14523
14911
|
}
|
|
14912
|
+
if (vault.config.backend === "contextfit") {
|
|
14913
|
+
try {
|
|
14914
|
+
const { indexVaultWithContextFit: indexVaultWithContextFit2 } = await Promise.resolve().then(() => (init_contextfit(), contextfit_exports));
|
|
14915
|
+
await indexVaultWithContextFit2(vault.config, {});
|
|
14916
|
+
} catch {
|
|
14917
|
+
}
|
|
14918
|
+
}
|
|
14524
14919
|
const noteRow = vault.db.notes.getByPath(parsed.path);
|
|
14525
14920
|
return {
|
|
14526
14921
|
ok: true,
|
|
@@ -14734,7 +15129,12 @@ function handleSearchText(manager, activeVault, query, vaultFilter, topK, exclud
|
|
|
14734
15129
|
const fanK = hasExclude ? topK * 3 : topK;
|
|
14735
15130
|
const sanitized = FtsQueries.sanitize(query);
|
|
14736
15131
|
const allHits = [];
|
|
15132
|
+
const skippedContextFit = [];
|
|
14737
15133
|
for (const vault of targets) {
|
|
15134
|
+
if (vault.config.backend === "contextfit") {
|
|
15135
|
+
skippedContextFit.push(vault.config.name);
|
|
15136
|
+
continue;
|
|
15137
|
+
}
|
|
14738
15138
|
const ftsHits = vault.db.fts.search(sanitized, fanK, true);
|
|
14739
15139
|
for (const hit of ftsHits) {
|
|
14740
15140
|
const chunk = vault.db.chunks.getById(hit.chunkId);
|
|
@@ -14759,9 +15159,14 @@ function handleSearchText(manager, activeVault, query, vaultFilter, topK, exclud
|
|
|
14759
15159
|
hits: allHits.slice(0, topK),
|
|
14760
15160
|
count: allHits.length
|
|
14761
15161
|
};
|
|
14762
|
-
|
|
14763
|
-
|
|
15162
|
+
const notes = [];
|
|
15163
|
+
if (skipped.length > 0) notes.push(`Skipped vault(s) currently indexing: ${skipped.join(", ")}.`);
|
|
15164
|
+
if (skippedContextFit.length > 0) {
|
|
15165
|
+
notes.push(
|
|
15166
|
+
`search_text is not supported for ContextFit vault(s): ${skippedContextFit.join(", ")} \u2014 use search_hybrid or search_semantic instead.`
|
|
15167
|
+
);
|
|
14764
15168
|
}
|
|
15169
|
+
if (notes.length > 0) out.note = notes.join(" ");
|
|
14765
15170
|
return out;
|
|
14766
15171
|
}
|
|
14767
15172
|
async function handleSearchHybrid(manager, ollama, defaultModel, activeVault, query, vaultFilter, topK, rrfK, excludePaths, reranker, recencyWeight = 0, authorityWeight = 0, halfLifeDays = 30, includeSuperseded = false, displayUrlFor2, expandOpts, expandDeps) {
|
|
@@ -14774,7 +15179,7 @@ async function handleSearchHybrid(manager, ollama, defaultModel, activeVault, qu
|
|
|
14774
15179
|
}
|
|
14775
15180
|
const hasExclude = excludePaths !== void 0 && excludePaths.length > 0;
|
|
14776
15181
|
const innerTopK = hasExclude ? topK * 3 : topK;
|
|
14777
|
-
const hits = await
|
|
15182
|
+
const hits = await searchVaults({
|
|
14778
15183
|
query,
|
|
14779
15184
|
embeddingModel: defaultModel,
|
|
14780
15185
|
ollama,
|
|
@@ -14812,7 +15217,7 @@ async function handleSearchCompat(manager, registry, ollama, defaultModel, activ
|
|
|
14812
15217
|
note: skipped.length > 0 ? `All eligible vaults are indexing; skipped: ${skipped.join(", ")}.` : "No vaults configured."
|
|
14813
15218
|
};
|
|
14814
15219
|
}
|
|
14815
|
-
const hits = await
|
|
15220
|
+
const hits = await searchVaults({
|
|
14816
15221
|
query,
|
|
14817
15222
|
embeddingModel: defaultModel,
|
|
14818
15223
|
ollama,
|
|
@@ -15729,7 +16134,7 @@ __export(server_exports, {
|
|
|
15729
16134
|
});
|
|
15730
16135
|
import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
15731
16136
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
15732
|
-
import { homedir as
|
|
16137
|
+
import { homedir as homedir6 } from "os";
|
|
15733
16138
|
import { join as joinPath } from "path";
|
|
15734
16139
|
async function discoverMemorySinks(configured, vaults) {
|
|
15735
16140
|
if (configured.length > 0) {
|
|
@@ -15796,19 +16201,20 @@ async function serve(options = {}) {
|
|
|
15796
16201
|
const activeVault = process.env.VAULT_MEMORY_ACTIVE_VAULT?.trim() || void 0;
|
|
15797
16202
|
const rerankerBackend = config.server.reranker_backend ?? (config.server.reranker_model ? "onnx" : void 0);
|
|
15798
16203
|
const reranker = config.server.reranker_model ? rerankerBackend === "ollama" ? new OllamaReranker({ ollama, model: config.server.reranker_model }) : new OnnxReranker({
|
|
15799
|
-
modelDir: config.server.reranker_model_dir ?? joinPath(
|
|
16204
|
+
modelDir: config.server.reranker_model_dir ?? joinPath(homedir6(), ".vault-memory", "models", "bge-reranker-v2-m3")
|
|
15800
16205
|
}) : void 0;
|
|
15801
16206
|
const watchers = /* @__PURE__ */ new Map();
|
|
15802
16207
|
const briefDaemons = /* @__PURE__ */ new Map();
|
|
15803
16208
|
const startCatchupAndWatchers = async () => {
|
|
15804
16209
|
for (const vault of manager.list()) {
|
|
15805
|
-
|
|
16210
|
+
const isContextFit2 = vault.config.backend === "contextfit";
|
|
16211
|
+
if (!isContextFit2 && !vault.config.embedding_model && !vault.db.models.getActive()) continue;
|
|
15806
16212
|
const modelName = vault.config.embedding_model ?? defaultModel;
|
|
15807
16213
|
try {
|
|
15808
16214
|
const result = await catchupVault({
|
|
15809
16215
|
vault,
|
|
15810
16216
|
embeddingModel: modelName,
|
|
15811
|
-
ollama,
|
|
16217
|
+
...isContextFit2 ? {} : { ollama },
|
|
15812
16218
|
log: (m) => process.stderr.write(`[catchup:${vault.config.name}] ${m}
|
|
15813
16219
|
`)
|
|
15814
16220
|
});
|
|
@@ -16891,6 +17297,36 @@ async function runIndex(rest) {
|
|
|
16891
17297
|
});
|
|
16892
17298
|
const targets = vaultName ? [manager.require(vaultName)] : manager.list();
|
|
16893
17299
|
for (const vault of targets) {
|
|
17300
|
+
if (vault.config.backend === "contextfit") {
|
|
17301
|
+
const { indexVaultWithContextFit: indexVaultWithContextFit2 } = await Promise.resolve().then(() => (init_contextfit(), contextfit_exports));
|
|
17302
|
+
console.error(
|
|
17303
|
+
`
|
|
17304
|
+
\u2192 Indexing "${vault.config.name}" with ContextFit (CPU-only, no embeddings)`
|
|
17305
|
+
);
|
|
17306
|
+
const sqlite = await indexVault2(vault, {
|
|
17307
|
+
mode,
|
|
17308
|
+
embeddingModel: "contextfit",
|
|
17309
|
+
embeddings: "none",
|
|
17310
|
+
onProgress: (msg) => console.error(` ${msg}`)
|
|
17311
|
+
});
|
|
17312
|
+
if (sqlite.status !== "completed") {
|
|
17313
|
+
console.error(`\u2717 ${vault.config.name}: SQLite layer failed \u2014 ${sqlite.error}`);
|
|
17314
|
+
process.exitCode = 1;
|
|
17315
|
+
continue;
|
|
17316
|
+
}
|
|
17317
|
+
const cfResult = await indexVaultWithContextFit2(vault.config, {
|
|
17318
|
+
onProgress: (msg) => console.error(` ${msg}`)
|
|
17319
|
+
});
|
|
17320
|
+
if (cfResult.status === "completed") {
|
|
17321
|
+
console.error(
|
|
17322
|
+
`\u2713 ${vault.config.name}: ${sqlite.notesIndexed} notes (SQLite) + ContextFit KB \xB7 ${sqlite.durationMs + cfResult.durationMs}ms`
|
|
17323
|
+
);
|
|
17324
|
+
} else {
|
|
17325
|
+
console.error(`\u2717 ${vault.config.name}: ContextFit KB failed \u2014 ${cfResult.error}`);
|
|
17326
|
+
process.exitCode = 1;
|
|
17327
|
+
}
|
|
17328
|
+
continue;
|
|
17329
|
+
}
|
|
16894
17330
|
const model = vault.config.embedding_model ?? config.server.default_embedding_model ?? "qwen3-embedding";
|
|
16895
17331
|
console.error(`
|
|
16896
17332
|
\u2192 Indexing "${vault.config.name}" (${mode}) with ${model}`);
|
|
@@ -16918,6 +17354,8 @@ async function runAddVault(rest) {
|
|
|
16918
17354
|
let name;
|
|
16919
17355
|
let writeEnabled = false;
|
|
16920
17356
|
let skipIndex = false;
|
|
17357
|
+
let backend;
|
|
17358
|
+
const USAGE = "Usage: vault-memory add-vault <path> [--name <name>] [--write] [--backend ollama|contextfit] [--no-index]";
|
|
16921
17359
|
for (let i = 0; i < rest.length; i++) {
|
|
16922
17360
|
const arg = rest[i];
|
|
16923
17361
|
if (arg === "--name") {
|
|
@@ -16925,24 +17363,37 @@ async function runAddVault(rest) {
|
|
|
16925
17363
|
i++;
|
|
16926
17364
|
} else if (arg === "--write" || arg === "--write-enabled") {
|
|
16927
17365
|
writeEnabled = true;
|
|
17366
|
+
} else if (arg === "--backend") {
|
|
17367
|
+
const v = rest[i + 1];
|
|
17368
|
+
i++;
|
|
17369
|
+
if (v !== "ollama" && v !== "contextfit") {
|
|
17370
|
+
console.error(`--backend must be "ollama" or "contextfit" (got: ${v ?? "<missing>"})`);
|
|
17371
|
+
process.exit(2);
|
|
17372
|
+
}
|
|
17373
|
+
backend = v;
|
|
16928
17374
|
} else if (arg === "--no-index") {
|
|
16929
17375
|
skipIndex = true;
|
|
16930
17376
|
} else if (arg === "--help" || arg === "-h") {
|
|
16931
|
-
console.error(
|
|
17377
|
+
console.error(`${USAGE}
|
|
16932
17378
|
|
|
16933
17379
|
Registers a vault in ~/.vault-memory/config.toml, writes a .mcp.json
|
|
16934
|
-
into the vault root, and runs an initial index. Idempotent
|
|
17380
|
+
into the vault root, and runs an initial index. Idempotent.
|
|
17381
|
+
|
|
17382
|
+
--backend contextfit Use the CPU-only, token-native ContextFit engine
|
|
17383
|
+
(no Ollama / embeddings / GPU). Requires the
|
|
17384
|
+
\`contextfit\` CLI (pipx install contextfit). Ideal for
|
|
17385
|
+
resource-limited / non-GPU hosts (e.g. a Synology NAS).`);
|
|
16935
17386
|
return;
|
|
16936
17387
|
} else if (arg && !arg.startsWith("--") && path7 === null) {
|
|
16937
17388
|
path7 = arg;
|
|
16938
17389
|
}
|
|
16939
17390
|
}
|
|
16940
17391
|
if (path7 === null) {
|
|
16941
|
-
console.error(
|
|
17392
|
+
console.error(USAGE);
|
|
16942
17393
|
process.exit(2);
|
|
16943
17394
|
}
|
|
16944
|
-
console.error(`\u2192 Registering vault: ${path7}`);
|
|
16945
|
-
const result = await addVault2({ path: path7, name, writeEnabled });
|
|
17395
|
+
console.error(`\u2192 Registering vault: ${path7}${backend ? ` (backend: ${backend})` : ""}`);
|
|
17396
|
+
const result = await addVault2({ path: path7, name, writeEnabled, ...backend ? { backend } : {} });
|
|
16946
17397
|
for (const step of result.steps) {
|
|
16947
17398
|
switch (step.kind) {
|
|
16948
17399
|
case "config-added":
|