@velvetmonkey/flywheel-memory 2.0.150 → 2.0.152
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/LICENSE +661 -0
- package/README.md +11 -5
- package/dist/index.js +619 -246
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -127,6 +127,50 @@ var init_levenshtein = __esm({
|
|
|
127
127
|
}
|
|
128
128
|
});
|
|
129
129
|
|
|
130
|
+
// src/core/shared/serverLog.ts
|
|
131
|
+
function serverLog(component, message, level = "info") {
|
|
132
|
+
const entry = {
|
|
133
|
+
ts: Date.now(),
|
|
134
|
+
component,
|
|
135
|
+
message,
|
|
136
|
+
level
|
|
137
|
+
};
|
|
138
|
+
buffer.push(entry);
|
|
139
|
+
if (buffer.length > MAX_ENTRIES) {
|
|
140
|
+
buffer.shift();
|
|
141
|
+
}
|
|
142
|
+
const now = /* @__PURE__ */ new Date();
|
|
143
|
+
const hms = `${String(now.getHours()).padStart(2, "0")}:${String(now.getMinutes()).padStart(2, "0")}:${String(now.getSeconds()).padStart(2, "0")}`;
|
|
144
|
+
const prefix = level === "error" ? "[Memory] ERROR" : level === "warn" ? "[Memory] WARN" : "[Memory]";
|
|
145
|
+
console.error(`${prefix} [${hms}] [${component}] ${message}`);
|
|
146
|
+
}
|
|
147
|
+
function getServerLog(options = {}) {
|
|
148
|
+
const { since, component, limit = 100 } = options;
|
|
149
|
+
let entries = buffer;
|
|
150
|
+
if (since) {
|
|
151
|
+
entries = entries.filter((e) => e.ts > since);
|
|
152
|
+
}
|
|
153
|
+
if (component) {
|
|
154
|
+
entries = entries.filter((e) => e.component === component);
|
|
155
|
+
}
|
|
156
|
+
if (entries.length > limit) {
|
|
157
|
+
entries = entries.slice(-limit);
|
|
158
|
+
}
|
|
159
|
+
return {
|
|
160
|
+
entries,
|
|
161
|
+
server_uptime_ms: Date.now() - serverStartTs
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
var MAX_ENTRIES, buffer, serverStartTs;
|
|
165
|
+
var init_serverLog = __esm({
|
|
166
|
+
"src/core/shared/serverLog.ts"() {
|
|
167
|
+
"use strict";
|
|
168
|
+
MAX_ENTRIES = 200;
|
|
169
|
+
buffer = [];
|
|
170
|
+
serverStartTs = Date.now();
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
|
|
130
174
|
// src/vault-scope.ts
|
|
131
175
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
132
176
|
function getActiveScopeOrNull() {
|
|
@@ -280,7 +324,27 @@ async function embedTextCached(text) {
|
|
|
280
324
|
return embedding;
|
|
281
325
|
}
|
|
282
326
|
function contentHash(content) {
|
|
283
|
-
return crypto.createHash("sha256").update(content).digest("hex").slice(0, 16);
|
|
327
|
+
return crypto.createHash("sha256").update(content + EMBEDDING_TEXT_VERSION).digest("hex").slice(0, 16);
|
|
328
|
+
}
|
|
329
|
+
function buildNoteEmbeddingText(content, filePath) {
|
|
330
|
+
const title = filePath.replace(/\.md$/, "").split("/").pop() || "";
|
|
331
|
+
const fmMatch = content.match(/^---[\s\S]*?---\n([\s\S]*)$/);
|
|
332
|
+
const body = fmMatch ? fmMatch[1] : content;
|
|
333
|
+
const frontmatter = fmMatch ? content.slice(0, content.indexOf("---", 3) + 3) : "";
|
|
334
|
+
const tags = [];
|
|
335
|
+
const arrayMatch = frontmatter.match(/^tags:\s*\[([^\]]*)\]/m);
|
|
336
|
+
if (arrayMatch) {
|
|
337
|
+
tags.push(...arrayMatch[1].split(",").map((t) => t.trim().replace(/['"]/g, "")).filter(Boolean));
|
|
338
|
+
} else {
|
|
339
|
+
const listMatch = frontmatter.match(/^tags:\s*\n((?:\s+-\s+.+\n?)+)/m);
|
|
340
|
+
if (listMatch) {
|
|
341
|
+
const items = listMatch[1].matchAll(/^\s+-\s+(.+)/gm);
|
|
342
|
+
for (const m of items) tags.push(m[1].trim().replace(/['"]/g, ""));
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
const parts = [`Note: ${title}`];
|
|
346
|
+
if (tags.length > 0) parts.push(`Tags: ${tags.slice(0, 5).join(", ")}`);
|
|
347
|
+
return parts.join(". ") + ".\n\n" + body;
|
|
284
348
|
}
|
|
285
349
|
function shouldIndexFile(filePath) {
|
|
286
350
|
const parts = filePath.split("/");
|
|
@@ -321,7 +385,7 @@ async function buildEmbeddingsIndex(vaultPath2, onProgress) {
|
|
|
321
385
|
if (onProgress) onProgress(progress);
|
|
322
386
|
continue;
|
|
323
387
|
}
|
|
324
|
-
const embedding = await embedText(content);
|
|
388
|
+
const embedding = await embedText(buildNoteEmbeddingText(content, file.path));
|
|
325
389
|
const buf = Buffer.from(embedding.buffer, embedding.byteOffset, embedding.byteLength);
|
|
326
390
|
upsert.run(file.path, buf, hash, activeModelConfig.id, Date.now());
|
|
327
391
|
} catch (err) {
|
|
@@ -351,7 +415,7 @@ async function updateEmbedding(notePath, absolutePath) {
|
|
|
351
415
|
const hash = contentHash(content);
|
|
352
416
|
const existing = db4.prepare("SELECT content_hash FROM note_embeddings WHERE path = ?").get(notePath);
|
|
353
417
|
if (existing?.content_hash === hash) return;
|
|
354
|
-
const embedding = await embedText(content);
|
|
418
|
+
const embedding = await embedText(buildNoteEmbeddingText(content, notePath));
|
|
355
419
|
const buf = Buffer.from(embedding.buffer, embedding.byteOffset, embedding.byteLength);
|
|
356
420
|
db4.prepare(`
|
|
357
421
|
INSERT OR REPLACE INTO note_embeddings (path, embedding, content_hash, model, updated_at)
|
|
@@ -652,7 +716,7 @@ function getEntityEmbeddingsCount() {
|
|
|
652
716
|
return 0;
|
|
653
717
|
}
|
|
654
718
|
}
|
|
655
|
-
var MODEL_REGISTRY, DEFAULT_MODEL, activeModelConfig, EXCLUDED_DIRS2, MAX_FILE_SIZE2, db, pipeline, initPromise, embeddingsBuilding, embeddingCache, EMBEDDING_CACHE_MAX, entityEmbeddingsMap;
|
|
719
|
+
var MODEL_REGISTRY, DEFAULT_MODEL, activeModelConfig, EXCLUDED_DIRS2, MAX_FILE_SIZE2, db, pipeline, initPromise, embeddingsBuilding, embeddingCache, EMBEDDING_CACHE_MAX, entityEmbeddingsMap, EMBEDDING_TEXT_VERSION;
|
|
656
720
|
var init_embeddings = __esm({
|
|
657
721
|
"src/core/read/embeddings.ts"() {
|
|
658
722
|
"use strict";
|
|
@@ -683,50 +747,7 @@ var init_embeddings = __esm({
|
|
|
683
747
|
embeddingCache = /* @__PURE__ */ new Map();
|
|
684
748
|
EMBEDDING_CACHE_MAX = 500;
|
|
685
749
|
entityEmbeddingsMap = /* @__PURE__ */ new Map();
|
|
686
|
-
|
|
687
|
-
});
|
|
688
|
-
|
|
689
|
-
// src/core/shared/serverLog.ts
|
|
690
|
-
function serverLog(component, message, level = "info") {
|
|
691
|
-
const entry = {
|
|
692
|
-
ts: Date.now(),
|
|
693
|
-
component,
|
|
694
|
-
message,
|
|
695
|
-
level
|
|
696
|
-
};
|
|
697
|
-
buffer.push(entry);
|
|
698
|
-
if (buffer.length > MAX_ENTRIES) {
|
|
699
|
-
buffer.shift();
|
|
700
|
-
}
|
|
701
|
-
const now = /* @__PURE__ */ new Date();
|
|
702
|
-
const hms = `${String(now.getHours()).padStart(2, "0")}:${String(now.getMinutes()).padStart(2, "0")}:${String(now.getSeconds()).padStart(2, "0")}`;
|
|
703
|
-
const prefix = level === "error" ? "[Memory] ERROR" : level === "warn" ? "[Memory] WARN" : "[Memory]";
|
|
704
|
-
console.error(`${prefix} [${hms}] [${component}] ${message}`);
|
|
705
|
-
}
|
|
706
|
-
function getServerLog(options = {}) {
|
|
707
|
-
const { since, component, limit = 100 } = options;
|
|
708
|
-
let entries = buffer;
|
|
709
|
-
if (since) {
|
|
710
|
-
entries = entries.filter((e) => e.ts > since);
|
|
711
|
-
}
|
|
712
|
-
if (component) {
|
|
713
|
-
entries = entries.filter((e) => e.component === component);
|
|
714
|
-
}
|
|
715
|
-
if (entries.length > limit) {
|
|
716
|
-
entries = entries.slice(-limit);
|
|
717
|
-
}
|
|
718
|
-
return {
|
|
719
|
-
entries,
|
|
720
|
-
server_uptime_ms: Date.now() - serverStartTs
|
|
721
|
-
};
|
|
722
|
-
}
|
|
723
|
-
var MAX_ENTRIES, buffer, serverStartTs;
|
|
724
|
-
var init_serverLog = __esm({
|
|
725
|
-
"src/core/shared/serverLog.ts"() {
|
|
726
|
-
"use strict";
|
|
727
|
-
MAX_ENTRIES = 200;
|
|
728
|
-
buffer = [];
|
|
729
|
-
serverStartTs = Date.now();
|
|
750
|
+
EMBEDDING_TEXT_VERSION = 2;
|
|
730
751
|
}
|
|
731
752
|
});
|
|
732
753
|
|
|
@@ -810,8 +831,8 @@ function getRecencyBoost(entityName, index) {
|
|
|
810
831
|
if (ageHours < 168) return 1;
|
|
811
832
|
return 0;
|
|
812
833
|
}
|
|
813
|
-
function loadRecencyFromStateDb() {
|
|
814
|
-
const stateDb2 = getStateDb();
|
|
834
|
+
function loadRecencyFromStateDb(explicitStateDb) {
|
|
835
|
+
const stateDb2 = explicitStateDb ?? getStateDb();
|
|
815
836
|
if (!stateDb2) return null;
|
|
816
837
|
try {
|
|
817
838
|
const rows = getAllRecencyFromDb(stateDb2);
|
|
@@ -833,8 +854,8 @@ function loadRecencyFromStateDb() {
|
|
|
833
854
|
return null;
|
|
834
855
|
}
|
|
835
856
|
}
|
|
836
|
-
function saveRecencyToStateDb(index) {
|
|
837
|
-
const stateDb2 = getStateDb();
|
|
857
|
+
function saveRecencyToStateDb(index, explicitStateDb) {
|
|
858
|
+
const stateDb2 = explicitStateDb ?? getStateDb();
|
|
838
859
|
if (!stateDb2) {
|
|
839
860
|
console.error("[Flywheel] saveRecencyToStateDb: No StateDb available");
|
|
840
861
|
return;
|
|
@@ -3487,6 +3508,7 @@ function isValidWikilinkText(text) {
|
|
|
3487
3508
|
if (target !== target.trim()) return false;
|
|
3488
3509
|
const trimmed = target.trim();
|
|
3489
3510
|
if (trimmed.length === 0) return false;
|
|
3511
|
+
if (/\n/.test(trimmed)) return false;
|
|
3490
3512
|
if (/[?!;]/.test(trimmed)) return false;
|
|
3491
3513
|
if (/[,.]$/.test(trimmed)) return false;
|
|
3492
3514
|
if (trimmed.includes(">")) return false;
|
|
@@ -3501,7 +3523,9 @@ function isValidWikilinkText(text) {
|
|
|
3501
3523
|
}
|
|
3502
3524
|
function sanitizeWikilinks(content) {
|
|
3503
3525
|
const removed = [];
|
|
3504
|
-
|
|
3526
|
+
let repaired = content.replace(/\[\s*\n\s*\[/g, "[[");
|
|
3527
|
+
repaired = repaired.replace(/\]\s*\n\s*\]/g, "]]");
|
|
3528
|
+
const sanitized = repaired.replace(/\[\[([^\]]+?)\]\]/g, (fullMatch, inner) => {
|
|
3505
3529
|
if (isValidWikilinkText(inner)) {
|
|
3506
3530
|
return fullMatch;
|
|
3507
3531
|
}
|
|
@@ -4416,7 +4440,7 @@ var init_wikilinks = __esm({
|
|
|
4416
4440
|
init_vault_scope();
|
|
4417
4441
|
moduleStateDb4 = null;
|
|
4418
4442
|
moduleConfig = null;
|
|
4419
|
-
ALL_IMPLICIT_PATTERNS = ["proper-nouns", "single-caps", "camel-case", "acronyms", "quoted-terms"];
|
|
4443
|
+
ALL_IMPLICIT_PATTERNS = ["proper-nouns", "single-caps", "camel-case", "acronyms", "quoted-terms", "ticket-refs"];
|
|
4420
4444
|
entityIndex = null;
|
|
4421
4445
|
indexReady = false;
|
|
4422
4446
|
indexError2 = null;
|
|
@@ -4891,6 +4915,38 @@ function isPreformattedList(content) {
|
|
|
4891
4915
|
/^\d+\.\s/.test(firstLine) || // Numbered
|
|
4892
4916
|
/^[-*+]\s*\[[ xX]\]/.test(firstLine);
|
|
4893
4917
|
}
|
|
4918
|
+
function sanitizeForList(content) {
|
|
4919
|
+
const lines = content.split("\n");
|
|
4920
|
+
const result = [];
|
|
4921
|
+
let inCodeBlock = false;
|
|
4922
|
+
for (const line of lines) {
|
|
4923
|
+
if (line.trim().startsWith("```")) {
|
|
4924
|
+
inCodeBlock = !inCodeBlock;
|
|
4925
|
+
result.push(line);
|
|
4926
|
+
continue;
|
|
4927
|
+
}
|
|
4928
|
+
if (inCodeBlock) {
|
|
4929
|
+
result.push(line);
|
|
4930
|
+
continue;
|
|
4931
|
+
}
|
|
4932
|
+
const headingMatch = line.match(/^(#{1,6})\s+(.+)$/);
|
|
4933
|
+
if (headingMatch) {
|
|
4934
|
+
result.push(`**${headingMatch[2]}**`);
|
|
4935
|
+
continue;
|
|
4936
|
+
}
|
|
4937
|
+
const starBulletMatch = line.match(/^(\s*)\*\s(.+)$/);
|
|
4938
|
+
if (starBulletMatch) {
|
|
4939
|
+
result.push(`${starBulletMatch[1]}- ${starBulletMatch[2]}`);
|
|
4940
|
+
continue;
|
|
4941
|
+
}
|
|
4942
|
+
if (/^-{3,}$/.test(line.trim()) || /^\*{3,}$/.test(line.trim()) || /^_{3,}$/.test(line.trim())) {
|
|
4943
|
+
result.push("\u2014");
|
|
4944
|
+
continue;
|
|
4945
|
+
}
|
|
4946
|
+
result.push(line);
|
|
4947
|
+
}
|
|
4948
|
+
return result.join("\n");
|
|
4949
|
+
}
|
|
4894
4950
|
function formatContent(content, format) {
|
|
4895
4951
|
const trimmed = content.trim();
|
|
4896
4952
|
if (trimmed === "") {
|
|
@@ -4914,9 +4970,10 @@ function formatContent(content, format) {
|
|
|
4914
4970
|
return trimmed;
|
|
4915
4971
|
case "bullet": {
|
|
4916
4972
|
if (isPreformattedList(trimmed)) {
|
|
4917
|
-
return trimmed;
|
|
4973
|
+
return sanitizeForList(trimmed);
|
|
4918
4974
|
}
|
|
4919
|
-
const
|
|
4975
|
+
const sanitized = sanitizeForList(trimmed);
|
|
4976
|
+
const lines = sanitized.split("\n");
|
|
4920
4977
|
return lines.map((line, i) => {
|
|
4921
4978
|
if (i === 0) return `- ${line}`;
|
|
4922
4979
|
if (line === "") return " ";
|
|
@@ -4947,13 +5004,14 @@ function formatContent(content, format) {
|
|
|
4947
5004
|
}
|
|
4948
5005
|
case "timestamp-bullet": {
|
|
4949
5006
|
if (isPreformattedList(trimmed)) {
|
|
4950
|
-
return trimmed;
|
|
5007
|
+
return sanitizeForList(trimmed);
|
|
4951
5008
|
}
|
|
5009
|
+
const sanitized = sanitizeForList(trimmed);
|
|
4952
5010
|
const now = /* @__PURE__ */ new Date();
|
|
4953
5011
|
const hours = String(now.getHours()).padStart(2, "0");
|
|
4954
5012
|
const minutes = String(now.getMinutes()).padStart(2, "0");
|
|
4955
5013
|
const prefix = `- **${hours}:${minutes}** `;
|
|
4956
|
-
const lines =
|
|
5014
|
+
const lines = sanitized.split("\n");
|
|
4957
5015
|
const indent = " ";
|
|
4958
5016
|
return lines.map((line, i) => {
|
|
4959
5017
|
if (i === 0) return `${prefix}${line}`;
|
|
@@ -6596,6 +6654,7 @@ function createEmptyNote(file) {
|
|
|
6596
6654
|
|
|
6597
6655
|
// src/core/read/graph.ts
|
|
6598
6656
|
init_levenshtein();
|
|
6657
|
+
init_serverLog();
|
|
6599
6658
|
init_embeddings();
|
|
6600
6659
|
init_vault_scope();
|
|
6601
6660
|
init_levenshtein();
|
|
@@ -6664,14 +6723,14 @@ async function buildVaultIndexInternal(vaultPath2, startTime, onProgress) {
|
|
|
6664
6723
|
return { file, note };
|
|
6665
6724
|
})
|
|
6666
6725
|
);
|
|
6667
|
-
for (
|
|
6726
|
+
for (let j = 0; j < results.length; j++) {
|
|
6727
|
+
const result = results[j];
|
|
6668
6728
|
if (result.status === "fulfilled") {
|
|
6669
6729
|
notes.set(result.value.note.path, result.value.note);
|
|
6670
6730
|
} else {
|
|
6671
|
-
const
|
|
6672
|
-
|
|
6673
|
-
|
|
6674
|
-
}
|
|
6731
|
+
const filePath = batch[j]?.path ?? "<unknown>";
|
|
6732
|
+
const reason = result.reason instanceof Error ? result.reason.message : String(result.reason);
|
|
6733
|
+
parseErrors.push({ path: filePath, reason });
|
|
6675
6734
|
}
|
|
6676
6735
|
parsedCount++;
|
|
6677
6736
|
}
|
|
@@ -6683,9 +6742,13 @@ async function buildVaultIndexInternal(vaultPath2, startTime, onProgress) {
|
|
|
6683
6742
|
}
|
|
6684
6743
|
}
|
|
6685
6744
|
if (parseErrors.length > 0) {
|
|
6686
|
-
|
|
6687
|
-
|
|
6688
|
-
|
|
6745
|
+
const msg = `Failed to parse ${parseErrors.length} file(s):`;
|
|
6746
|
+
console.error(msg);
|
|
6747
|
+
serverLog("index", msg, "error");
|
|
6748
|
+
for (const err of parseErrors) {
|
|
6749
|
+
const detail = ` - ${err.path}: ${err.reason}`;
|
|
6750
|
+
console.error(detail);
|
|
6751
|
+
serverLog("index", detail, "error");
|
|
6689
6752
|
}
|
|
6690
6753
|
}
|
|
6691
6754
|
const entities = /* @__PURE__ */ new Map();
|
|
@@ -7877,7 +7940,8 @@ import {
|
|
|
7877
7940
|
rangeOverlapsProtectedZone,
|
|
7878
7941
|
detectImplicitEntities as detectImplicitEntities2,
|
|
7879
7942
|
checkDbIntegrity,
|
|
7880
|
-
safeBackupAsync
|
|
7943
|
+
safeBackupAsync,
|
|
7944
|
+
recordEntityMention as recordEntityMention2
|
|
7881
7945
|
} from "@velvetmonkey/vault-core";
|
|
7882
7946
|
init_serverLog();
|
|
7883
7947
|
|
|
@@ -8816,6 +8880,7 @@ var PipelineRunner = class {
|
|
|
8816
8880
|
await runStep("forward_links", tracker, { files: p.events.length }, () => this.forwardLinks());
|
|
8817
8881
|
await this.wikilinkCheck();
|
|
8818
8882
|
await this.implicitFeedback();
|
|
8883
|
+
await runStep("incremental_recency", tracker, { files: p.events.length }, () => this.incrementalRecency());
|
|
8819
8884
|
await runStep("corrections", tracker, {}, () => this.corrections());
|
|
8820
8885
|
await runStep("prospect_scan", tracker, { files: p.events.length }, () => this.prospectScan());
|
|
8821
8886
|
await this.suggestionScoring();
|
|
@@ -8912,9 +8977,19 @@ var PipelineRunner = class {
|
|
|
8912
8977
|
const rows = p.sd.db.prepare("SELECT name, hub_score FROM entities").all();
|
|
8913
8978
|
for (const r of rows) this.hubBefore.set(r.name, r.hub_score);
|
|
8914
8979
|
}
|
|
8980
|
+
const entityScanAgeMs = p.ctx.lastEntityScanAt > 0 ? Date.now() - p.ctx.lastEntityScanAt : Infinity;
|
|
8981
|
+
if (entityScanAgeMs < 5 * 60 * 1e3) {
|
|
8982
|
+
tracker.start("entity_scan", {});
|
|
8983
|
+
tracker.skip("entity_scan", `cache valid (${Math.round(entityScanAgeMs / 1e3)}s old)`);
|
|
8984
|
+
this.entitiesBefore = p.sd ? getAllEntitiesFromDb(p.sd) : [];
|
|
8985
|
+
this.entitiesAfter = this.entitiesBefore;
|
|
8986
|
+
serverLog("watcher", `Entity scan: throttled (${Math.round(entityScanAgeMs / 1e3)}s old)`);
|
|
8987
|
+
return;
|
|
8988
|
+
}
|
|
8915
8989
|
this.entitiesBefore = p.sd ? getAllEntitiesFromDb(p.sd) : [];
|
|
8916
8990
|
tracker.start("entity_scan", { note_count: vaultIndex2.notes.size });
|
|
8917
8991
|
await p.updateEntitiesInStateDb(p.vp, p.sd);
|
|
8992
|
+
p.ctx.lastEntityScanAt = Date.now();
|
|
8918
8993
|
this.entitiesAfter = p.sd ? getAllEntitiesFromDb(p.sd) : [];
|
|
8919
8994
|
const entityDiff = computeEntityDiff(this.entitiesBefore, this.entitiesAfter);
|
|
8920
8995
|
const categoryChanges = [];
|
|
@@ -8951,6 +9026,11 @@ var PipelineRunner = class {
|
|
|
8951
9026
|
// ── Step 3: Hub scores ────────────────────────────────────────────
|
|
8952
9027
|
async hubScores() {
|
|
8953
9028
|
const { p } = this;
|
|
9029
|
+
const hubAgeMs = p.ctx.lastHubScoreRebuildAt > 0 ? Date.now() - p.ctx.lastHubScoreRebuildAt : Infinity;
|
|
9030
|
+
if (hubAgeMs < 5 * 60 * 1e3) {
|
|
9031
|
+
serverLog("watcher", `Hub scores: throttled (${Math.round(hubAgeMs / 1e3)}s old)`);
|
|
9032
|
+
return { skipped: true, age_ms: hubAgeMs };
|
|
9033
|
+
}
|
|
8954
9034
|
const vaultIndex2 = p.getVaultIndex();
|
|
8955
9035
|
const hubUpdated = await exportHubScores(vaultIndex2, p.sd);
|
|
8956
9036
|
const hubDiffs = [];
|
|
@@ -8961,18 +9041,19 @@ var PipelineRunner = class {
|
|
|
8961
9041
|
if (prev !== r.hub_score) hubDiffs.push({ entity: r.name, before: prev, after: r.hub_score });
|
|
8962
9042
|
}
|
|
8963
9043
|
}
|
|
9044
|
+
p.ctx.lastHubScoreRebuildAt = Date.now();
|
|
8964
9045
|
serverLog("watcher", `Hub scores: ${hubUpdated ?? 0} updated`);
|
|
8965
9046
|
return { updated: hubUpdated ?? 0, diffs: hubDiffs.slice(0, 10) };
|
|
8966
9047
|
}
|
|
8967
9048
|
// ── Step 3.5: Recency ─────────────────────────────────────────────
|
|
8968
9049
|
async recency() {
|
|
8969
9050
|
const { p } = this;
|
|
8970
|
-
const cachedRecency = loadRecencyFromStateDb();
|
|
9051
|
+
const cachedRecency = loadRecencyFromStateDb(p.sd ?? void 0);
|
|
8971
9052
|
const cacheAgeMs = cachedRecency ? Date.now() - (cachedRecency.lastUpdated ?? 0) : Infinity;
|
|
8972
9053
|
if (cacheAgeMs >= 60 * 60 * 1e3) {
|
|
8973
9054
|
const entities = this.entitiesAfter.map((e) => ({ name: e.name, path: e.path, aliases: e.aliases }));
|
|
8974
9055
|
const recencyIndex2 = await buildRecencyIndex(p.vp, entities);
|
|
8975
|
-
saveRecencyToStateDb(recencyIndex2);
|
|
9056
|
+
saveRecencyToStateDb(recencyIndex2, p.sd ?? void 0);
|
|
8976
9057
|
serverLog("watcher", `Recency: rebuilt ${recencyIndex2.lastMentioned.size} entities`);
|
|
8977
9058
|
return { rebuilt: true, entities: recencyIndex2.lastMentioned.size };
|
|
8978
9059
|
}
|
|
@@ -9082,9 +9163,16 @@ var PipelineRunner = class {
|
|
|
9082
9163
|
const { p, tracker } = this;
|
|
9083
9164
|
const vaultIndex2 = p.getVaultIndex();
|
|
9084
9165
|
if (p.sd) {
|
|
9166
|
+
const cacheAgeMs = p.ctx.lastIndexCacheSaveAt > 0 ? Date.now() - p.ctx.lastIndexCacheSaveAt : Infinity;
|
|
9167
|
+
if (cacheAgeMs < 30 * 1e3) {
|
|
9168
|
+
tracker.start("index_cache", {});
|
|
9169
|
+
tracker.skip("index_cache", `saved recently (${Math.round(cacheAgeMs / 1e3)}s ago)`);
|
|
9170
|
+
return;
|
|
9171
|
+
}
|
|
9085
9172
|
tracker.start("index_cache", { note_count: vaultIndex2.notes.size });
|
|
9086
9173
|
try {
|
|
9087
9174
|
saveVaultIndexToCache(p.sd, vaultIndex2);
|
|
9175
|
+
p.ctx.lastIndexCacheSaveAt = Date.now();
|
|
9088
9176
|
tracker.end({ saved: true });
|
|
9089
9177
|
serverLog("watcher", "Index cache saved");
|
|
9090
9178
|
} catch (err) {
|
|
@@ -9341,6 +9429,7 @@ var PipelineRunner = class {
|
|
|
9341
9429
|
);
|
|
9342
9430
|
for (const diff of this.linkDiffs) {
|
|
9343
9431
|
if (deletedFiles.has(diff.file)) continue;
|
|
9432
|
+
const newlyTracked = [];
|
|
9344
9433
|
for (const target of diff.added) {
|
|
9345
9434
|
if (checkApplication.get(target, diff.file)) continue;
|
|
9346
9435
|
const entity = this.entitiesAfter.find(
|
|
@@ -9349,8 +9438,15 @@ var PipelineRunner = class {
|
|
|
9349
9438
|
if (entity) {
|
|
9350
9439
|
recordFeedback(p.sd, entity.name, "implicit:manual_added", diff.file, true);
|
|
9351
9440
|
additionResults.push({ entity: entity.name, file: diff.file });
|
|
9441
|
+
newlyTracked.push({
|
|
9442
|
+
entity: entity.name,
|
|
9443
|
+
matchedTerm: entity.nameLower === target ? void 0 : target
|
|
9444
|
+
});
|
|
9352
9445
|
}
|
|
9353
9446
|
}
|
|
9447
|
+
if (newlyTracked.length > 0) {
|
|
9448
|
+
trackWikilinkApplications(p.sd, diff.file, newlyTracked);
|
|
9449
|
+
}
|
|
9354
9450
|
}
|
|
9355
9451
|
}
|
|
9356
9452
|
const newlySuppressed = [];
|
|
@@ -9370,6 +9466,20 @@ var PipelineRunner = class {
|
|
|
9370
9466
|
serverLog("watcher", `Suppression: ${newlySuppressed.length} entities newly suppressed: ${newlySuppressed.join(", ")}`);
|
|
9371
9467
|
}
|
|
9372
9468
|
}
|
|
9469
|
+
// ── Step 10.1: Incremental recency ──────────────────────────────
|
|
9470
|
+
async incrementalRecency() {
|
|
9471
|
+
const { p } = this;
|
|
9472
|
+
if (!p.sd) return { skipped: true };
|
|
9473
|
+
let updated = 0;
|
|
9474
|
+
const now = /* @__PURE__ */ new Date();
|
|
9475
|
+
for (const entry of this.forwardLinkResults) {
|
|
9476
|
+
for (const target of entry.resolved) {
|
|
9477
|
+
recordEntityMention2(p.sd, target, now);
|
|
9478
|
+
updated++;
|
|
9479
|
+
}
|
|
9480
|
+
}
|
|
9481
|
+
return { entities_updated: updated };
|
|
9482
|
+
}
|
|
9373
9483
|
// ── Step 10.5: Corrections ────────────────────────────────────────
|
|
9374
9484
|
async corrections() {
|
|
9375
9485
|
const { p } = this;
|
|
@@ -9635,7 +9745,7 @@ import { openStateDb, scanVaultEntities as scanVaultEntities4, getAllEntitiesFro
|
|
|
9635
9745
|
|
|
9636
9746
|
// src/core/write/memory.ts
|
|
9637
9747
|
init_wikilinkFeedback();
|
|
9638
|
-
import { recordEntityMention as
|
|
9748
|
+
import { recordEntityMention as recordEntityMention3 } from "@velvetmonkey/vault-core";
|
|
9639
9749
|
var entityCacheMap = /* @__PURE__ */ new Map();
|
|
9640
9750
|
var ENTITY_CACHE_TTL_MS = 6e4;
|
|
9641
9751
|
function getEntityList(stateDb2) {
|
|
@@ -9676,7 +9786,7 @@ function updateGraphSignals(stateDb2, memoryKey, entities) {
|
|
|
9676
9786
|
if (entities.length === 0) return;
|
|
9677
9787
|
const now = /* @__PURE__ */ new Date();
|
|
9678
9788
|
for (const entity of entities) {
|
|
9679
|
-
|
|
9789
|
+
recordEntityMention3(stateDb2, entity, now);
|
|
9680
9790
|
}
|
|
9681
9791
|
const sourcePath = `memory:${memoryKey}`;
|
|
9682
9792
|
const targets = new Set(entities.map((e) => e.toLowerCase()));
|
|
@@ -13854,6 +13964,38 @@ function enrichResultLight(result, index, stateDb2) {
|
|
|
13854
13964
|
}
|
|
13855
13965
|
return enriched;
|
|
13856
13966
|
}
|
|
13967
|
+
function enrichEntityCompact(entityName, stateDb2, index) {
|
|
13968
|
+
const enriched = {};
|
|
13969
|
+
if (stateDb2) {
|
|
13970
|
+
try {
|
|
13971
|
+
const entity = getEntityByName2(stateDb2, entityName);
|
|
13972
|
+
if (entity) {
|
|
13973
|
+
enriched.category = entity.category;
|
|
13974
|
+
enriched.hub_score = entity.hubScore;
|
|
13975
|
+
if (entity.aliases.length > 0) enriched.aliases = entity.aliases;
|
|
13976
|
+
enriched.path = entity.path;
|
|
13977
|
+
}
|
|
13978
|
+
} catch {
|
|
13979
|
+
}
|
|
13980
|
+
}
|
|
13981
|
+
if (index) {
|
|
13982
|
+
const entityPath = enriched.path ?? index.entities.get(entityName.toLowerCase());
|
|
13983
|
+
if (entityPath) {
|
|
13984
|
+
const note = index.notes.get(entityPath);
|
|
13985
|
+
const normalizedPath = entityPath.toLowerCase().replace(/\.md$/, "");
|
|
13986
|
+
const backlinks = index.backlinks.get(normalizedPath) || [];
|
|
13987
|
+
enriched.backlink_count = backlinks.length;
|
|
13988
|
+
if (note) {
|
|
13989
|
+
if (Object.keys(note.frontmatter).length > 0) enriched.frontmatter = note.frontmatter;
|
|
13990
|
+
if (note.tags.length > 0) enriched.tags = note.tags;
|
|
13991
|
+
if (note.outlinks.length > 0) {
|
|
13992
|
+
enriched.outlink_names = getOutlinkNames(note.outlinks, entityPath, index, stateDb2, COMPACT_OUTLINK_NAMES);
|
|
13993
|
+
}
|
|
13994
|
+
}
|
|
13995
|
+
}
|
|
13996
|
+
}
|
|
13997
|
+
return enriched;
|
|
13998
|
+
}
|
|
13857
13999
|
|
|
13858
14000
|
// src/core/read/multihop.ts
|
|
13859
14001
|
import { getEntityByName as getEntityByName3, searchEntities } from "@velvetmonkey/vault-core";
|
|
@@ -14156,6 +14298,151 @@ function extractDates(text) {
|
|
|
14156
14298
|
|
|
14157
14299
|
// src/tools/read/query.ts
|
|
14158
14300
|
init_stemmer();
|
|
14301
|
+
|
|
14302
|
+
// src/tools/read/structure.ts
|
|
14303
|
+
import * as fs14 from "fs";
|
|
14304
|
+
import * as path17 from "path";
|
|
14305
|
+
var HEADING_REGEX2 = /^(#{1,6})\s+(.+)$/;
|
|
14306
|
+
function extractHeadings2(content) {
|
|
14307
|
+
const lines = content.split("\n");
|
|
14308
|
+
const headings = [];
|
|
14309
|
+
let inCodeBlock = false;
|
|
14310
|
+
for (let i = 0; i < lines.length; i++) {
|
|
14311
|
+
const line = lines[i];
|
|
14312
|
+
if (line.startsWith("```")) {
|
|
14313
|
+
inCodeBlock = !inCodeBlock;
|
|
14314
|
+
continue;
|
|
14315
|
+
}
|
|
14316
|
+
if (inCodeBlock) continue;
|
|
14317
|
+
const match = line.match(HEADING_REGEX2);
|
|
14318
|
+
if (match) {
|
|
14319
|
+
headings.push({
|
|
14320
|
+
level: match[1].length,
|
|
14321
|
+
text: match[2].trim(),
|
|
14322
|
+
line: i + 1
|
|
14323
|
+
// 1-indexed
|
|
14324
|
+
});
|
|
14325
|
+
}
|
|
14326
|
+
}
|
|
14327
|
+
return headings;
|
|
14328
|
+
}
|
|
14329
|
+
function buildSections(headings, totalLines) {
|
|
14330
|
+
if (headings.length === 0) return [];
|
|
14331
|
+
const sections = [];
|
|
14332
|
+
const stack = [];
|
|
14333
|
+
for (let i = 0; i < headings.length; i++) {
|
|
14334
|
+
const heading = headings[i];
|
|
14335
|
+
const nextHeading = headings[i + 1];
|
|
14336
|
+
const lineEnd = nextHeading ? nextHeading.line - 1 : totalLines;
|
|
14337
|
+
const section = {
|
|
14338
|
+
heading,
|
|
14339
|
+
line_start: heading.line,
|
|
14340
|
+
line_end: lineEnd,
|
|
14341
|
+
subsections: []
|
|
14342
|
+
};
|
|
14343
|
+
while (stack.length > 0 && stack[stack.length - 1].heading.level >= heading.level) {
|
|
14344
|
+
stack.pop();
|
|
14345
|
+
}
|
|
14346
|
+
if (stack.length === 0) {
|
|
14347
|
+
sections.push(section);
|
|
14348
|
+
} else {
|
|
14349
|
+
stack[stack.length - 1].subsections.push(section);
|
|
14350
|
+
}
|
|
14351
|
+
stack.push(section);
|
|
14352
|
+
}
|
|
14353
|
+
return sections;
|
|
14354
|
+
}
|
|
14355
|
+
async function getNoteStructure(index, notePath, vaultPath2) {
|
|
14356
|
+
const note = index.notes.get(notePath);
|
|
14357
|
+
if (!note) return null;
|
|
14358
|
+
const absolutePath = path17.join(vaultPath2, notePath);
|
|
14359
|
+
let content;
|
|
14360
|
+
try {
|
|
14361
|
+
content = await fs14.promises.readFile(absolutePath, "utf-8");
|
|
14362
|
+
} catch {
|
|
14363
|
+
return null;
|
|
14364
|
+
}
|
|
14365
|
+
const lines = content.split("\n");
|
|
14366
|
+
const headings = extractHeadings2(content);
|
|
14367
|
+
const sections = buildSections(headings, lines.length);
|
|
14368
|
+
const contentWithoutCode = content.replace(/```[\s\S]*?```/g, "");
|
|
14369
|
+
const words = contentWithoutCode.split(/\s+/).filter((w) => w.length > 0);
|
|
14370
|
+
return {
|
|
14371
|
+
path: notePath,
|
|
14372
|
+
headings,
|
|
14373
|
+
sections,
|
|
14374
|
+
word_count: words.length,
|
|
14375
|
+
line_count: lines.length
|
|
14376
|
+
};
|
|
14377
|
+
}
|
|
14378
|
+
async function getSectionContent(index, notePath, headingText, vaultPath2, includeSubheadings = true) {
|
|
14379
|
+
const note = index.notes.get(notePath);
|
|
14380
|
+
if (!note) return null;
|
|
14381
|
+
const absolutePath = path17.join(vaultPath2, notePath);
|
|
14382
|
+
let content;
|
|
14383
|
+
try {
|
|
14384
|
+
content = await fs14.promises.readFile(absolutePath, "utf-8");
|
|
14385
|
+
} catch {
|
|
14386
|
+
return null;
|
|
14387
|
+
}
|
|
14388
|
+
const lines = content.split("\n");
|
|
14389
|
+
const headings = extractHeadings2(content);
|
|
14390
|
+
const targetHeading = headings.find(
|
|
14391
|
+
(h) => h.text.toLowerCase() === headingText.toLowerCase()
|
|
14392
|
+
);
|
|
14393
|
+
if (!targetHeading) return null;
|
|
14394
|
+
let lineEnd = lines.length;
|
|
14395
|
+
for (const h of headings) {
|
|
14396
|
+
if (h.line > targetHeading.line) {
|
|
14397
|
+
if (includeSubheadings) {
|
|
14398
|
+
if (h.level <= targetHeading.level) {
|
|
14399
|
+
lineEnd = h.line - 1;
|
|
14400
|
+
break;
|
|
14401
|
+
}
|
|
14402
|
+
} else {
|
|
14403
|
+
lineEnd = h.line - 1;
|
|
14404
|
+
break;
|
|
14405
|
+
}
|
|
14406
|
+
}
|
|
14407
|
+
}
|
|
14408
|
+
const sectionLines = lines.slice(targetHeading.line, lineEnd);
|
|
14409
|
+
const sectionContent = sectionLines.join("\n").trim();
|
|
14410
|
+
return {
|
|
14411
|
+
heading: targetHeading.text,
|
|
14412
|
+
level: targetHeading.level,
|
|
14413
|
+
content: sectionContent,
|
|
14414
|
+
line_start: targetHeading.line,
|
|
14415
|
+
line_end: lineEnd
|
|
14416
|
+
};
|
|
14417
|
+
}
|
|
14418
|
+
async function findSections(index, headingPattern, vaultPath2, folder) {
|
|
14419
|
+
const regex = new RegExp(headingPattern, "i");
|
|
14420
|
+
const results = [];
|
|
14421
|
+
for (const note of index.notes.values()) {
|
|
14422
|
+
if (folder && !note.path.startsWith(folder)) continue;
|
|
14423
|
+
const absolutePath = path17.join(vaultPath2, note.path);
|
|
14424
|
+
let content;
|
|
14425
|
+
try {
|
|
14426
|
+
content = await fs14.promises.readFile(absolutePath, "utf-8");
|
|
14427
|
+
} catch {
|
|
14428
|
+
continue;
|
|
14429
|
+
}
|
|
14430
|
+
const headings = extractHeadings2(content);
|
|
14431
|
+
for (const heading of headings) {
|
|
14432
|
+
if (regex.test(heading.text)) {
|
|
14433
|
+
results.push({
|
|
14434
|
+
path: note.path,
|
|
14435
|
+
heading: heading.text,
|
|
14436
|
+
level: heading.level,
|
|
14437
|
+
line: heading.line
|
|
14438
|
+
});
|
|
14439
|
+
}
|
|
14440
|
+
}
|
|
14441
|
+
}
|
|
14442
|
+
return results;
|
|
14443
|
+
}
|
|
14444
|
+
|
|
14445
|
+
// src/tools/read/query.ts
|
|
14159
14446
|
function applyGraphReranking(results, stateDb2) {
|
|
14160
14447
|
if (!stateDb2) return;
|
|
14161
14448
|
const cooccurrenceIndex2 = getCooccurrenceIndex();
|
|
@@ -14188,8 +14475,20 @@ function applyGraphReranking(results, stateDb2) {
|
|
|
14188
14475
|
}
|
|
14189
14476
|
function applySandwichOrdering(results) {
|
|
14190
14477
|
if (results.length < 3) return;
|
|
14191
|
-
const
|
|
14192
|
-
|
|
14478
|
+
const n = results.length;
|
|
14479
|
+
const out = new Array(n);
|
|
14480
|
+
let front = 0;
|
|
14481
|
+
let back = n - 1;
|
|
14482
|
+
for (let i = 0; i < n; i++) {
|
|
14483
|
+
if (i % 2 === 0) {
|
|
14484
|
+
out[front++] = results[i];
|
|
14485
|
+
} else {
|
|
14486
|
+
out[back--] = results[i];
|
|
14487
|
+
}
|
|
14488
|
+
}
|
|
14489
|
+
for (let i = 0; i < n; i++) {
|
|
14490
|
+
results[i] = out[i];
|
|
14491
|
+
}
|
|
14193
14492
|
}
|
|
14194
14493
|
function applyEntityBridging(results, stateDb2, maxBridgesPerResult = 3) {
|
|
14195
14494
|
if (!stateDb2 || results.length < 2) return;
|
|
@@ -14233,6 +14532,92 @@ function stripInternalFields(results) {
|
|
|
14233
14532
|
for (const key of INTERNAL) delete r[key];
|
|
14234
14533
|
}
|
|
14235
14534
|
}
|
|
14535
|
+
function scoreAndRankMemories(memories, limit) {
|
|
14536
|
+
const now = Date.now();
|
|
14537
|
+
const scored = [];
|
|
14538
|
+
for (const m of memories) {
|
|
14539
|
+
const confidenceBoost = m.confidence * 5;
|
|
14540
|
+
let typeBoost = 0;
|
|
14541
|
+
switch (m.memory_type) {
|
|
14542
|
+
case "fact":
|
|
14543
|
+
typeBoost = 3;
|
|
14544
|
+
break;
|
|
14545
|
+
case "preference":
|
|
14546
|
+
typeBoost = 2;
|
|
14547
|
+
break;
|
|
14548
|
+
case "observation": {
|
|
14549
|
+
const ageDays = (now - m.updated_at) / 864e5;
|
|
14550
|
+
const recencyFactor = Math.max(0.2, 1 - ageDays / 7);
|
|
14551
|
+
typeBoost = 1 + 4 * recencyFactor;
|
|
14552
|
+
break;
|
|
14553
|
+
}
|
|
14554
|
+
case "summary":
|
|
14555
|
+
typeBoost = 1;
|
|
14556
|
+
break;
|
|
14557
|
+
}
|
|
14558
|
+
scored.push({ memory: m, score: confidenceBoost + typeBoost });
|
|
14559
|
+
}
|
|
14560
|
+
scored.sort((a, b) => b.score - a.score);
|
|
14561
|
+
return scored.slice(0, limit).map(({ memory: m }) => {
|
|
14562
|
+
const result = {
|
|
14563
|
+
key: m.key,
|
|
14564
|
+
value: m.value,
|
|
14565
|
+
type: m.memory_type
|
|
14566
|
+
};
|
|
14567
|
+
if (m.entity) result.entity = m.entity;
|
|
14568
|
+
if (m.entities_json) {
|
|
14569
|
+
try {
|
|
14570
|
+
const entities = JSON.parse(m.entities_json);
|
|
14571
|
+
if (Array.isArray(entities) && entities.length > 0) result.entities = entities;
|
|
14572
|
+
} catch {
|
|
14573
|
+
}
|
|
14574
|
+
}
|
|
14575
|
+
return result;
|
|
14576
|
+
});
|
|
14577
|
+
}
|
|
14578
|
+
async function buildEntitySection(entityResults, query, stateDb2, index, limit) {
|
|
14579
|
+
if (!stateDb2 || entityResults.length === 0 && query.length < 20) return [];
|
|
14580
|
+
const entityMap = /* @__PURE__ */ new Map();
|
|
14581
|
+
for (const e of entityResults) {
|
|
14582
|
+
entityMap.set(e.name.toLowerCase(), e);
|
|
14583
|
+
}
|
|
14584
|
+
if (query.length >= 20 && hasEntityEmbeddingsIndex()) {
|
|
14585
|
+
try {
|
|
14586
|
+
const embedding = await embedTextCached(query);
|
|
14587
|
+
const semanticMatches = findSemanticallySimilarEntities(embedding, limit);
|
|
14588
|
+
for (const match of semanticMatches) {
|
|
14589
|
+
if (match.similarity < 0.3) continue;
|
|
14590
|
+
const key = match.entityName.toLowerCase();
|
|
14591
|
+
if (!entityMap.has(key)) {
|
|
14592
|
+
entityMap.set(key, {
|
|
14593
|
+
id: 0,
|
|
14594
|
+
name: match.entityName,
|
|
14595
|
+
nameLower: key,
|
|
14596
|
+
path: "",
|
|
14597
|
+
category: "unknown",
|
|
14598
|
+
aliases: [],
|
|
14599
|
+
hubScore: 0,
|
|
14600
|
+
rank: 0
|
|
14601
|
+
});
|
|
14602
|
+
}
|
|
14603
|
+
}
|
|
14604
|
+
} catch {
|
|
14605
|
+
}
|
|
14606
|
+
}
|
|
14607
|
+
if (entityMap.size === 0) return [];
|
|
14608
|
+
const enriched = [];
|
|
14609
|
+
let count = 0;
|
|
14610
|
+
for (const [, entity] of entityMap) {
|
|
14611
|
+
if (count >= limit) break;
|
|
14612
|
+
enriched.push({
|
|
14613
|
+
name: entity.name,
|
|
14614
|
+
...entity.description ? { description: entity.description } : {},
|
|
14615
|
+
...enrichEntityCompact(entity.name, stateDb2, index)
|
|
14616
|
+
});
|
|
14617
|
+
count++;
|
|
14618
|
+
}
|
|
14619
|
+
return enriched;
|
|
14620
|
+
}
|
|
14236
14621
|
async function enhanceSnippets(results, query, vaultPath2) {
|
|
14237
14622
|
if (!hasEmbeddingsIndex()) return;
|
|
14238
14623
|
const queryTokens = tokenize(query).map((t) => t.toLowerCase());
|
|
@@ -14256,6 +14641,29 @@ async function enhanceSnippets(results, query, vaultPath2) {
|
|
|
14256
14641
|
}
|
|
14257
14642
|
}
|
|
14258
14643
|
}
|
|
14644
|
+
async function expandToSections(results, index, vaultPath2, maxExpand = 5, maxSectionChars = 2500) {
|
|
14645
|
+
const toExpand = results.slice(0, maxExpand);
|
|
14646
|
+
for (const r of toExpand) {
|
|
14647
|
+
const sectionHeading = r.section;
|
|
14648
|
+
const notePath = r.path;
|
|
14649
|
+
if (!sectionHeading || !notePath) continue;
|
|
14650
|
+
try {
|
|
14651
|
+
const section = await getSectionContent(index, notePath, sectionHeading, vaultPath2, true);
|
|
14652
|
+
if (!section || !section.content) continue;
|
|
14653
|
+
const heading = `## ${section.heading}`;
|
|
14654
|
+
let content = section.content;
|
|
14655
|
+
if (content.length > maxSectionChars) {
|
|
14656
|
+
const truncated = content.slice(0, maxSectionChars);
|
|
14657
|
+
const lastBreak = truncated.lastIndexOf("\n\n");
|
|
14658
|
+
content = (lastBreak > 0 ? truncated.slice(0, lastBreak) : truncated) + "\n\u2026";
|
|
14659
|
+
}
|
|
14660
|
+
r.section_content = `${heading}
|
|
14661
|
+
|
|
14662
|
+
${content}`;
|
|
14663
|
+
} catch {
|
|
14664
|
+
}
|
|
14665
|
+
}
|
|
14666
|
+
}
|
|
14259
14667
|
function matchesFrontmatter(note, where) {
|
|
14260
14668
|
for (const [key, value] of Object.entries(where)) {
|
|
14261
14669
|
const noteValue = note.frontmatter[key];
|
|
@@ -14326,7 +14734,7 @@ function sortNotes(notes, sortBy, order) {
|
|
|
14326
14734
|
function registerQueryTools(server2, getIndex, getVaultPath, getStateDb3) {
|
|
14327
14735
|
server2.tool(
|
|
14328
14736
|
"search",
|
|
14329
|
-
'Search everything \u2014 notes, entities, and memories \u2014 in one call. Returns a decision surface with three sections: note results (with section provenance, dates, bridges, confidence), matching entity profiles, and relevant memories.\n\
|
|
14737
|
+
'Search everything \u2014 notes, entities, and memories \u2014 in one call. Returns a decision surface with three sections: note results (with section provenance, full section content, dates, bridges, confidence), matching entity profiles, and relevant memories.\n\nTop note results carry full metadata (frontmatter, scored backlinks/outlinks, snippets) plus section_content \u2014 the complete ## section around the match (up to 2,500 chars). Start with just a query, no filters. Narrow with filters only if needed. Between snippet, section_content, and frontmatter, most questions can be answered without follow-up reads.\n\nSearches note content (FTS5 + hybrid semantic with contextual embeddings), entity profiles (people, projects, technologies), and stored memories. Hybrid results included automatically when embeddings are built (via init_semantic).\n\nExample: search({ query: "quarterly review", limit: 5 })\nExample: search({ where: { type: "project", status: "active" } })\n\nMulti-vault: omitting `vault` searches all vaults and merges results. Pass `vault` to search a specific vault.',
|
|
14330
14738
|
{
|
|
14331
14739
|
query: z5.string().optional().describe("Search query text. Required unless using metadata filters (where, has_tag, folder, etc.)"),
|
|
14332
14740
|
// Metadata filters
|
|
@@ -14440,6 +14848,15 @@ function registerQueryTools(server2, getIndex, getVaultPath, getStateDb3) {
|
|
|
14440
14848
|
} catch {
|
|
14441
14849
|
}
|
|
14442
14850
|
}
|
|
14851
|
+
let memoryResults = [];
|
|
14852
|
+
if (stateDbEntity) {
|
|
14853
|
+
try {
|
|
14854
|
+
const rawMemories = searchMemories(stateDbEntity, { query, limit });
|
|
14855
|
+
memoryResults = scoreAndRankMemories(rawMemories, limit);
|
|
14856
|
+
} catch {
|
|
14857
|
+
}
|
|
14858
|
+
}
|
|
14859
|
+
const entitySectionPromise = buildEntitySection(entityResults, query, stateDbEntity, index, limit);
|
|
14443
14860
|
let edgeRanked = [];
|
|
14444
14861
|
if (context_note) {
|
|
14445
14862
|
const ctxStateDb = getStateDb3();
|
|
@@ -14532,12 +14949,16 @@ function registerQueryTools(server2, getIndex, getVaultPath, getStateDb3) {
|
|
|
14532
14949
|
applyEntityBridging(results2, stateDb2);
|
|
14533
14950
|
applySandwichOrdering(results2);
|
|
14534
14951
|
await enhanceSnippets(results2, query, vaultPath2);
|
|
14952
|
+
await expandToSections(results2, index, vaultPath2, detailN);
|
|
14535
14953
|
stripInternalFields(results2);
|
|
14954
|
+
const entitySection2 = await entitySectionPromise;
|
|
14536
14955
|
return { content: [{ type: "text", text: JSON.stringify({
|
|
14537
14956
|
method: "hybrid",
|
|
14538
14957
|
query,
|
|
14539
14958
|
total_results: filtered.length,
|
|
14540
|
-
results: results2
|
|
14959
|
+
results: results2,
|
|
14960
|
+
...entitySection2.length > 0 ? { entities: entitySection2 } : {},
|
|
14961
|
+
...memoryResults.length > 0 ? { memories: memoryResults } : {}
|
|
14541
14962
|
}, null, 2) }] };
|
|
14542
14963
|
} catch (err) {
|
|
14543
14964
|
console.error("[Semantic] Hybrid search failed, falling back to FTS5:", err instanceof Error ? err.message : err);
|
|
@@ -14574,12 +14995,16 @@ function registerQueryTools(server2, getIndex, getVaultPath, getStateDb3) {
|
|
|
14574
14995
|
applyEntityBridging(results2, stateDb2);
|
|
14575
14996
|
applySandwichOrdering(results2);
|
|
14576
14997
|
await enhanceSnippets(results2, query, vaultPath2);
|
|
14998
|
+
await expandToSections(results2, index, vaultPath2, detailN);
|
|
14577
14999
|
stripInternalFields(results2);
|
|
15000
|
+
const entitySection2 = await entitySectionPromise;
|
|
14578
15001
|
return { content: [{ type: "text", text: JSON.stringify({
|
|
14579
15002
|
method: "fts5",
|
|
14580
15003
|
query,
|
|
14581
15004
|
total_results: filtered.length,
|
|
14582
|
-
results: results2
|
|
15005
|
+
results: results2,
|
|
15006
|
+
...entitySection2.length > 0 ? { entities: entitySection2 } : {},
|
|
15007
|
+
...memoryResults.length > 0 ? { memories: memoryResults } : {}
|
|
14583
15008
|
}, null, 2) }] };
|
|
14584
15009
|
}
|
|
14585
15010
|
const stateDbFts = getStateDb3();
|
|
@@ -14595,12 +15020,16 @@ function registerQueryTools(server2, getIndex, getVaultPath, getStateDb3) {
|
|
|
14595
15020
|
applyEntityBridging(results, stateDbFts);
|
|
14596
15021
|
applySandwichOrdering(results);
|
|
14597
15022
|
await enhanceSnippets(results, query, vaultPath2);
|
|
15023
|
+
await expandToSections(results, index, vaultPath2, detailN);
|
|
14598
15024
|
stripInternalFields(results);
|
|
15025
|
+
const entitySection = await entitySectionPromise;
|
|
14599
15026
|
return { content: [{ type: "text", text: JSON.stringify({
|
|
14600
15027
|
method: "fts5",
|
|
14601
15028
|
query,
|
|
14602
15029
|
total_results: results.length,
|
|
14603
|
-
results
|
|
15030
|
+
results,
|
|
15031
|
+
...entitySection.length > 0 ? { entities: entitySection } : {},
|
|
15032
|
+
...memoryResults.length > 0 ? { memories: memoryResults } : {}
|
|
14604
15033
|
}, null, 2) }] };
|
|
14605
15034
|
}
|
|
14606
15035
|
return { content: [{ type: "text", text: JSON.stringify({ error: "Provide a query or metadata filters (where, has_tag, folder, etc.)" }, null, 2) }] };
|
|
@@ -14609,8 +15038,8 @@ function registerQueryTools(server2, getIndex, getVaultPath, getStateDb3) {
|
|
|
14609
15038
|
}
|
|
14610
15039
|
|
|
14611
15040
|
// src/tools/read/system.ts
|
|
14612
|
-
import * as
|
|
14613
|
-
import * as
|
|
15041
|
+
import * as fs15 from "fs";
|
|
15042
|
+
import * as path18 from "path";
|
|
14614
15043
|
import { z as z6 } from "zod";
|
|
14615
15044
|
import { scanVaultEntities as scanVaultEntities2, getEntityIndexFromDb as getEntityIndexFromDb2 } from "@velvetmonkey/vault-core";
|
|
14616
15045
|
|
|
@@ -14925,8 +15354,8 @@ function registerSystemTools(server2, getIndex, setIndex, getVaultPath, setConfi
|
|
|
14925
15354
|
continue;
|
|
14926
15355
|
}
|
|
14927
15356
|
try {
|
|
14928
|
-
const fullPath =
|
|
14929
|
-
const content = await
|
|
15357
|
+
const fullPath = path18.join(vaultPath2, note.path);
|
|
15358
|
+
const content = await fs15.promises.readFile(fullPath, "utf-8");
|
|
14930
15359
|
const lines = content.split("\n");
|
|
14931
15360
|
for (let i = 0; i < lines.length; i++) {
|
|
14932
15361
|
const line = lines[i];
|
|
@@ -15183,151 +15612,6 @@ function registerSystemTools(server2, getIndex, setIndex, getVaultPath, setConfi
|
|
|
15183
15612
|
|
|
15184
15613
|
// src/tools/read/primitives.ts
|
|
15185
15614
|
import { z as z7 } from "zod";
|
|
15186
|
-
|
|
15187
|
-
// src/tools/read/structure.ts
|
|
15188
|
-
import * as fs15 from "fs";
|
|
15189
|
-
import * as path18 from "path";
|
|
15190
|
-
var HEADING_REGEX2 = /^(#{1,6})\s+(.+)$/;
|
|
15191
|
-
function extractHeadings2(content) {
|
|
15192
|
-
const lines = content.split("\n");
|
|
15193
|
-
const headings = [];
|
|
15194
|
-
let inCodeBlock = false;
|
|
15195
|
-
for (let i = 0; i < lines.length; i++) {
|
|
15196
|
-
const line = lines[i];
|
|
15197
|
-
if (line.startsWith("```")) {
|
|
15198
|
-
inCodeBlock = !inCodeBlock;
|
|
15199
|
-
continue;
|
|
15200
|
-
}
|
|
15201
|
-
if (inCodeBlock) continue;
|
|
15202
|
-
const match = line.match(HEADING_REGEX2);
|
|
15203
|
-
if (match) {
|
|
15204
|
-
headings.push({
|
|
15205
|
-
level: match[1].length,
|
|
15206
|
-
text: match[2].trim(),
|
|
15207
|
-
line: i + 1
|
|
15208
|
-
// 1-indexed
|
|
15209
|
-
});
|
|
15210
|
-
}
|
|
15211
|
-
}
|
|
15212
|
-
return headings;
|
|
15213
|
-
}
|
|
15214
|
-
function buildSections(headings, totalLines) {
|
|
15215
|
-
if (headings.length === 0) return [];
|
|
15216
|
-
const sections = [];
|
|
15217
|
-
const stack = [];
|
|
15218
|
-
for (let i = 0; i < headings.length; i++) {
|
|
15219
|
-
const heading = headings[i];
|
|
15220
|
-
const nextHeading = headings[i + 1];
|
|
15221
|
-
const lineEnd = nextHeading ? nextHeading.line - 1 : totalLines;
|
|
15222
|
-
const section = {
|
|
15223
|
-
heading,
|
|
15224
|
-
line_start: heading.line,
|
|
15225
|
-
line_end: lineEnd,
|
|
15226
|
-
subsections: []
|
|
15227
|
-
};
|
|
15228
|
-
while (stack.length > 0 && stack[stack.length - 1].heading.level >= heading.level) {
|
|
15229
|
-
stack.pop();
|
|
15230
|
-
}
|
|
15231
|
-
if (stack.length === 0) {
|
|
15232
|
-
sections.push(section);
|
|
15233
|
-
} else {
|
|
15234
|
-
stack[stack.length - 1].subsections.push(section);
|
|
15235
|
-
}
|
|
15236
|
-
stack.push(section);
|
|
15237
|
-
}
|
|
15238
|
-
return sections;
|
|
15239
|
-
}
|
|
15240
|
-
async function getNoteStructure(index, notePath, vaultPath2) {
|
|
15241
|
-
const note = index.notes.get(notePath);
|
|
15242
|
-
if (!note) return null;
|
|
15243
|
-
const absolutePath = path18.join(vaultPath2, notePath);
|
|
15244
|
-
let content;
|
|
15245
|
-
try {
|
|
15246
|
-
content = await fs15.promises.readFile(absolutePath, "utf-8");
|
|
15247
|
-
} catch {
|
|
15248
|
-
return null;
|
|
15249
|
-
}
|
|
15250
|
-
const lines = content.split("\n");
|
|
15251
|
-
const headings = extractHeadings2(content);
|
|
15252
|
-
const sections = buildSections(headings, lines.length);
|
|
15253
|
-
const contentWithoutCode = content.replace(/```[\s\S]*?```/g, "");
|
|
15254
|
-
const words = contentWithoutCode.split(/\s+/).filter((w) => w.length > 0);
|
|
15255
|
-
return {
|
|
15256
|
-
path: notePath,
|
|
15257
|
-
headings,
|
|
15258
|
-
sections,
|
|
15259
|
-
word_count: words.length,
|
|
15260
|
-
line_count: lines.length
|
|
15261
|
-
};
|
|
15262
|
-
}
|
|
15263
|
-
async function getSectionContent(index, notePath, headingText, vaultPath2, includeSubheadings = true) {
|
|
15264
|
-
const note = index.notes.get(notePath);
|
|
15265
|
-
if (!note) return null;
|
|
15266
|
-
const absolutePath = path18.join(vaultPath2, notePath);
|
|
15267
|
-
let content;
|
|
15268
|
-
try {
|
|
15269
|
-
content = await fs15.promises.readFile(absolutePath, "utf-8");
|
|
15270
|
-
} catch {
|
|
15271
|
-
return null;
|
|
15272
|
-
}
|
|
15273
|
-
const lines = content.split("\n");
|
|
15274
|
-
const headings = extractHeadings2(content);
|
|
15275
|
-
const targetHeading = headings.find(
|
|
15276
|
-
(h) => h.text.toLowerCase() === headingText.toLowerCase()
|
|
15277
|
-
);
|
|
15278
|
-
if (!targetHeading) return null;
|
|
15279
|
-
let lineEnd = lines.length;
|
|
15280
|
-
for (const h of headings) {
|
|
15281
|
-
if (h.line > targetHeading.line) {
|
|
15282
|
-
if (includeSubheadings) {
|
|
15283
|
-
if (h.level <= targetHeading.level) {
|
|
15284
|
-
lineEnd = h.line - 1;
|
|
15285
|
-
break;
|
|
15286
|
-
}
|
|
15287
|
-
} else {
|
|
15288
|
-
lineEnd = h.line - 1;
|
|
15289
|
-
break;
|
|
15290
|
-
}
|
|
15291
|
-
}
|
|
15292
|
-
}
|
|
15293
|
-
const sectionLines = lines.slice(targetHeading.line, lineEnd);
|
|
15294
|
-
const sectionContent = sectionLines.join("\n").trim();
|
|
15295
|
-
return {
|
|
15296
|
-
heading: targetHeading.text,
|
|
15297
|
-
level: targetHeading.level,
|
|
15298
|
-
content: sectionContent,
|
|
15299
|
-
line_start: targetHeading.line,
|
|
15300
|
-
line_end: lineEnd
|
|
15301
|
-
};
|
|
15302
|
-
}
|
|
15303
|
-
async function findSections(index, headingPattern, vaultPath2, folder) {
|
|
15304
|
-
const regex = new RegExp(headingPattern, "i");
|
|
15305
|
-
const results = [];
|
|
15306
|
-
for (const note of index.notes.values()) {
|
|
15307
|
-
if (folder && !note.path.startsWith(folder)) continue;
|
|
15308
|
-
const absolutePath = path18.join(vaultPath2, note.path);
|
|
15309
|
-
let content;
|
|
15310
|
-
try {
|
|
15311
|
-
content = await fs15.promises.readFile(absolutePath, "utf-8");
|
|
15312
|
-
} catch {
|
|
15313
|
-
continue;
|
|
15314
|
-
}
|
|
15315
|
-
const headings = extractHeadings2(content);
|
|
15316
|
-
for (const heading of headings) {
|
|
15317
|
-
if (regex.test(heading.text)) {
|
|
15318
|
-
results.push({
|
|
15319
|
-
path: note.path,
|
|
15320
|
-
heading: heading.text,
|
|
15321
|
-
level: heading.level,
|
|
15322
|
-
line: heading.line
|
|
15323
|
-
});
|
|
15324
|
-
}
|
|
15325
|
-
}
|
|
15326
|
-
}
|
|
15327
|
-
return results;
|
|
15328
|
-
}
|
|
15329
|
-
|
|
15330
|
-
// src/tools/read/primitives.ts
|
|
15331
15615
|
import { getEntityByName as getEntityByName4 } from "@velvetmonkey/vault-core";
|
|
15332
15616
|
function registerPrimitiveTools(server2, getIndex, getVaultPath, getConfig2 = () => ({}), getStateDb3 = () => null) {
|
|
15333
15617
|
server2.registerTool(
|
|
@@ -17771,14 +18055,40 @@ async function executeMutation(options, operation) {
|
|
|
17771
18055
|
if (existsError) {
|
|
17772
18056
|
return { success: false, result: existsError, filesWritten: [] };
|
|
17773
18057
|
}
|
|
17774
|
-
|
|
18058
|
+
let { content, frontmatter, lineEnding, contentHash: contentHash2 } = await readVaultFile(vaultPath2, notePath);
|
|
17775
18059
|
let sectionBoundary;
|
|
17776
18060
|
if (section) {
|
|
17777
18061
|
const sectionResult = await ensureSectionExists(content, section, notePath, vaultPath2);
|
|
17778
18062
|
if ("error" in sectionResult) {
|
|
17779
|
-
|
|
18063
|
+
if (options.autoCreateSection) {
|
|
18064
|
+
const sectionName = section.replace(/^#+\s*/, "").trim();
|
|
18065
|
+
const headings = extractHeadings3(content);
|
|
18066
|
+
let level = 1;
|
|
18067
|
+
if (headings.length > 0) {
|
|
18068
|
+
const topLevel = Math.min(...headings.map((h) => h.level));
|
|
18069
|
+
const topHeadings = headings.filter((h) => h.level === topLevel);
|
|
18070
|
+
level = topHeadings.length === 1 ? topLevel + 1 : topLevel;
|
|
18071
|
+
}
|
|
18072
|
+
const hashes = "#".repeat(level);
|
|
18073
|
+
const newSection = `
|
|
18074
|
+
${hashes} ${sectionName}
|
|
18075
|
+
-
|
|
18076
|
+
`;
|
|
18077
|
+
content = content.trimEnd() + "\n" + newSection;
|
|
18078
|
+
await writeVaultFile(vaultPath2, notePath, content, frontmatter, lineEnding, contentHash2);
|
|
18079
|
+
({ content, frontmatter, lineEnding, contentHash: contentHash2 } = await readVaultFile(vaultPath2, notePath));
|
|
18080
|
+
const retryResult = await ensureSectionExists(content, section, notePath, vaultPath2);
|
|
18081
|
+
if ("error" in retryResult) {
|
|
18082
|
+
return { success: false, result: retryResult.error, filesWritten: [] };
|
|
18083
|
+
}
|
|
18084
|
+
sectionBoundary = retryResult.boundary;
|
|
18085
|
+
console.error(`[Flywheel] Auto-created section "${sectionName}" (${hashes}) in ${notePath}`);
|
|
18086
|
+
} else {
|
|
18087
|
+
return { success: false, result: sectionResult.error, filesWritten: [] };
|
|
18088
|
+
}
|
|
18089
|
+
} else {
|
|
18090
|
+
sectionBoundary = sectionResult.boundary;
|
|
17780
18091
|
}
|
|
17781
|
-
sectionBoundary = sectionResult.boundary;
|
|
17782
18092
|
}
|
|
17783
18093
|
const ctx = { content, frontmatter, lineEnding, sectionBoundary, vaultPath: vaultPath2, notePath };
|
|
17784
18094
|
const opResult = await operation(ctx);
|
|
@@ -17837,7 +18147,8 @@ async function withVaultFile(options, operation) {
|
|
|
17837
18147
|
section: options.section,
|
|
17838
18148
|
actionDescription: options.actionDescription,
|
|
17839
18149
|
scoping: options.scoping,
|
|
17840
|
-
dryRun: options.dryRun
|
|
18150
|
+
dryRun: options.dryRun,
|
|
18151
|
+
autoCreateSection: options.autoCreateSection
|
|
17841
18152
|
}, operation);
|
|
17842
18153
|
if (!outcome.success || options.dryRun) {
|
|
17843
18154
|
return formatMcpResult(outcome.result);
|
|
@@ -17989,21 +18300,44 @@ async function createNoteFromTemplate(vaultPath2, notePath, config) {
|
|
|
17989
18300
|
const templates = config.templates || {};
|
|
17990
18301
|
const filename = path25.basename(notePath, ".md").toLowerCase();
|
|
17991
18302
|
let templatePath;
|
|
18303
|
+
let periodicType;
|
|
17992
18304
|
const dailyPattern = /^\d{4}-\d{2}-\d{2}/;
|
|
17993
18305
|
const weeklyPattern = /^\d{4}-W\d{2}/;
|
|
17994
18306
|
const monthlyPattern = /^\d{4}-\d{2}$/;
|
|
17995
18307
|
const quarterlyPattern = /^\d{4}-Q[1-4]$/;
|
|
17996
18308
|
const yearlyPattern = /^\d{4}$/;
|
|
17997
|
-
if (dailyPattern.test(filename)
|
|
18309
|
+
if (dailyPattern.test(filename)) {
|
|
17998
18310
|
templatePath = templates.daily;
|
|
17999
|
-
|
|
18311
|
+
periodicType = "daily";
|
|
18312
|
+
} else if (weeklyPattern.test(filename)) {
|
|
18000
18313
|
templatePath = templates.weekly;
|
|
18001
|
-
|
|
18314
|
+
periodicType = "weekly";
|
|
18315
|
+
} else if (monthlyPattern.test(filename)) {
|
|
18002
18316
|
templatePath = templates.monthly;
|
|
18003
|
-
|
|
18317
|
+
periodicType = "monthly";
|
|
18318
|
+
} else if (quarterlyPattern.test(filename)) {
|
|
18004
18319
|
templatePath = templates.quarterly;
|
|
18005
|
-
|
|
18320
|
+
periodicType = "quarterly";
|
|
18321
|
+
} else if (yearlyPattern.test(filename)) {
|
|
18006
18322
|
templatePath = templates.yearly;
|
|
18323
|
+
periodicType = "yearly";
|
|
18324
|
+
}
|
|
18325
|
+
if (!templatePath && periodicType) {
|
|
18326
|
+
const candidates = [
|
|
18327
|
+
`templates/${periodicType}.md`,
|
|
18328
|
+
`templates/${periodicType[0].toUpperCase() + periodicType.slice(1)}.md`,
|
|
18329
|
+
`templates/${periodicType}-note.md`,
|
|
18330
|
+
`templates/${periodicType[0].toUpperCase() + periodicType.slice(1)} Note.md`
|
|
18331
|
+
];
|
|
18332
|
+
for (const candidate of candidates) {
|
|
18333
|
+
try {
|
|
18334
|
+
await fs23.access(path25.join(vaultPath2, candidate));
|
|
18335
|
+
templatePath = candidate;
|
|
18336
|
+
console.error(`[Flywheel] Template not in config but found at ${candidate} \u2014 using it`);
|
|
18337
|
+
break;
|
|
18338
|
+
} catch {
|
|
18339
|
+
}
|
|
18340
|
+
}
|
|
18007
18341
|
}
|
|
18008
18342
|
let templateContent;
|
|
18009
18343
|
if (templatePath) {
|
|
@@ -18011,6 +18345,7 @@ async function createNoteFromTemplate(vaultPath2, notePath, config) {
|
|
|
18011
18345
|
const absTemplatePath = path25.join(vaultPath2, templatePath);
|
|
18012
18346
|
templateContent = await fs23.readFile(absTemplatePath, "utf-8");
|
|
18013
18347
|
} catch {
|
|
18348
|
+
console.error(`[Flywheel] Template at ${templatePath} not readable, using minimal fallback`);
|
|
18014
18349
|
const title = path25.basename(notePath, ".md");
|
|
18015
18350
|
templateContent = `---
|
|
18016
18351
|
---
|
|
@@ -18020,6 +18355,9 @@ async function createNoteFromTemplate(vaultPath2, notePath, config) {
|
|
|
18020
18355
|
templatePath = void 0;
|
|
18021
18356
|
}
|
|
18022
18357
|
} else {
|
|
18358
|
+
if (periodicType) {
|
|
18359
|
+
console.error(`[Flywheel] No ${periodicType} template found in config or vault \u2014 using minimal fallback`);
|
|
18360
|
+
}
|
|
18023
18361
|
const title = path25.basename(notePath, ".md");
|
|
18024
18362
|
templateContent = `---
|
|
18025
18363
|
---
|
|
@@ -18104,7 +18442,8 @@ Example: vault_add_to_section({ path: "daily/2026-02-15.md", section: "Log", con
|
|
|
18104
18442
|
section,
|
|
18105
18443
|
actionDescription: "add content",
|
|
18106
18444
|
scoping: agent_id || session_id ? { agent_id, session_id } : void 0,
|
|
18107
|
-
dryRun: dry_run
|
|
18445
|
+
dryRun: dry_run,
|
|
18446
|
+
autoCreateSection: noteCreated
|
|
18108
18447
|
},
|
|
18109
18448
|
async (ctx) => {
|
|
18110
18449
|
const validationResult = runValidationPipeline(content, format, {
|
|
@@ -21256,6 +21595,7 @@ function registerWikilinkFeedbackTools(server2, getStateDb3) {
|
|
|
21256
21595
|
isError: true
|
|
21257
21596
|
};
|
|
21258
21597
|
}
|
|
21598
|
+
console.error(`[Flywheel] wikilink_feedback: stateDb path=${stateDb2.db.name}`);
|
|
21259
21599
|
try {
|
|
21260
21600
|
recordFeedback(stateDb2, entity, context || "", note_path || "", correct);
|
|
21261
21601
|
} catch (e) {
|
|
@@ -21266,6 +21606,8 @@ function registerWikilinkFeedbackTools(server2, getStateDb3) {
|
|
|
21266
21606
|
isError: true
|
|
21267
21607
|
};
|
|
21268
21608
|
}
|
|
21609
|
+
const rowCount = stateDb2.db.prepare("SELECT COUNT(*) as cnt FROM wikilink_feedback").get().cnt;
|
|
21610
|
+
console.error(`[Flywheel] wikilink_feedback: after insert, total rows=${rowCount}, db=${stateDb2.db.name}`);
|
|
21269
21611
|
if (!correct && note_path && !skip_status_update) {
|
|
21270
21612
|
stateDb2.db.prepare(
|
|
21271
21613
|
`UPDATE wikilink_applications SET status = 'removed' WHERE entity = ? AND note_path = ? COLLATE NOCASE`
|
|
@@ -21282,7 +21624,8 @@ function registerWikilinkFeedbackTools(server2, getStateDb3) {
|
|
|
21282
21624
|
correct,
|
|
21283
21625
|
suppression_updated: suppressionUpdated
|
|
21284
21626
|
},
|
|
21285
|
-
total_suppressed: getSuppressedCount(stateDb2)
|
|
21627
|
+
total_suppressed: getSuppressedCount(stateDb2),
|
|
21628
|
+
total_feedback_rows: rowCount
|
|
21286
21629
|
};
|
|
21287
21630
|
break;
|
|
21288
21631
|
}
|
|
@@ -24692,23 +25035,48 @@ function applyToolGating(targetServer, categories, getDb4, registry, getVaultPat
|
|
|
24692
25035
|
} catch {
|
|
24693
25036
|
}
|
|
24694
25037
|
}
|
|
24695
|
-
const
|
|
25038
|
+
const mergedResults = [];
|
|
25039
|
+
const mergedEntities = [];
|
|
25040
|
+
const mergedMemories = [];
|
|
24696
25041
|
const vaultsSearched = [];
|
|
24697
25042
|
let query;
|
|
24698
25043
|
for (const { vault, data } of perVault) {
|
|
24699
25044
|
vaultsSearched.push(vault);
|
|
24700
25045
|
if (data.query) query = data.query;
|
|
24701
25046
|
if (data.error || data.building) continue;
|
|
24702
|
-
const items = data.results || data.notes ||
|
|
25047
|
+
const items = data.results || data.notes || [];
|
|
24703
25048
|
for (const item of items) {
|
|
24704
|
-
|
|
25049
|
+
mergedResults.push({ vault, ...item });
|
|
25050
|
+
}
|
|
25051
|
+
if (Array.isArray(data.entities)) {
|
|
25052
|
+
for (const item of data.entities) {
|
|
25053
|
+
mergedEntities.push({ vault, ...item });
|
|
25054
|
+
}
|
|
25055
|
+
}
|
|
25056
|
+
if (Array.isArray(data.memories)) {
|
|
25057
|
+
for (const item of data.memories) {
|
|
25058
|
+
mergedMemories.push({ vault, ...item });
|
|
25059
|
+
}
|
|
24705
25060
|
}
|
|
24706
25061
|
}
|
|
24707
|
-
if (
|
|
24708
|
-
|
|
25062
|
+
if (mergedResults.some((r) => r.rrf_score != null)) {
|
|
25063
|
+
mergedResults.sort((a, b) => (b.rrf_score ?? 0) - (a.rrf_score ?? 0));
|
|
24709
25064
|
}
|
|
25065
|
+
const seenEntities = /* @__PURE__ */ new Set();
|
|
25066
|
+
const dedupedEntities = mergedEntities.filter((e) => {
|
|
25067
|
+
const key = (e.name || "").toLowerCase();
|
|
25068
|
+
if (seenEntities.has(key)) return false;
|
|
25069
|
+
seenEntities.add(key);
|
|
25070
|
+
return true;
|
|
25071
|
+
});
|
|
25072
|
+
const seenMemories = /* @__PURE__ */ new Set();
|
|
25073
|
+
const dedupedMemories = mergedMemories.filter((m) => {
|
|
25074
|
+
if (seenMemories.has(m.key)) return false;
|
|
25075
|
+
seenMemories.add(m.key);
|
|
25076
|
+
return true;
|
|
25077
|
+
});
|
|
24710
25078
|
const limit = args[0]?.limit ?? 10;
|
|
24711
|
-
const truncated =
|
|
25079
|
+
const truncated = mergedResults.slice(0, limit);
|
|
24712
25080
|
return {
|
|
24713
25081
|
content: [{
|
|
24714
25082
|
type: "text",
|
|
@@ -24716,9 +25084,11 @@ function applyToolGating(targetServer, categories, getDb4, registry, getVaultPat
|
|
|
24716
25084
|
method: "cross_vault",
|
|
24717
25085
|
query,
|
|
24718
25086
|
vaults_searched: vaultsSearched,
|
|
24719
|
-
total_results:
|
|
25087
|
+
total_results: mergedResults.length,
|
|
24720
25088
|
returned: truncated.length,
|
|
24721
|
-
results: truncated
|
|
25089
|
+
results: truncated,
|
|
25090
|
+
...dedupedEntities.length > 0 ? { entities: dedupedEntities.slice(0, limit) } : {},
|
|
25091
|
+
...dedupedMemories.length > 0 ? { memories: dedupedMemories.slice(0, limit) } : {}
|
|
24722
25092
|
}, null, 2)
|
|
24723
25093
|
}]
|
|
24724
25094
|
};
|
|
@@ -24950,7 +25320,10 @@ async function initializeVault(name, vaultPathArg) {
|
|
|
24950
25320
|
indexState: "building",
|
|
24951
25321
|
indexError: null,
|
|
24952
25322
|
lastCooccurrenceRebuildAt: 0,
|
|
24953
|
-
lastEdgeWeightRebuildAt: 0
|
|
25323
|
+
lastEdgeWeightRebuildAt: 0,
|
|
25324
|
+
lastEntityScanAt: 0,
|
|
25325
|
+
lastHubScoreRebuildAt: 0,
|
|
25326
|
+
lastIndexCacheSaveAt: 0
|
|
24954
25327
|
};
|
|
24955
25328
|
try {
|
|
24956
25329
|
ctx.stateDb = openStateDb(vaultPathArg);
|