@owrede/vault-memory 0.9.2 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +267 -94
- package/dist/cli.js +708 -2
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -3573,6 +3573,606 @@ var init_frontmatter = __esm({
|
|
|
3573
3573
|
}
|
|
3574
3574
|
});
|
|
3575
3575
|
|
|
3576
|
+
// src/schema/folder-conventions.ts
|
|
3577
|
+
function folderOf(notePath) {
|
|
3578
|
+
const idx = notePath.lastIndexOf("/");
|
|
3579
|
+
return idx === -1 ? "" : notePath.slice(0, idx + 1);
|
|
3580
|
+
}
|
|
3581
|
+
function parentFolder(folder) {
|
|
3582
|
+
if (folder === "") return null;
|
|
3583
|
+
const trimmed = folder.endsWith("/") ? folder.slice(0, -1) : folder;
|
|
3584
|
+
const idx = trimmed.lastIndexOf("/");
|
|
3585
|
+
if (idx === -1) return "";
|
|
3586
|
+
return trimmed.slice(0, idx + 1);
|
|
3587
|
+
}
|
|
3588
|
+
function countSiblings(vault, folder, excludePath) {
|
|
3589
|
+
const handle = vault.db.handle;
|
|
3590
|
+
if (folder === "") {
|
|
3591
|
+
const row2 = handle.prepare(
|
|
3592
|
+
"SELECT COUNT(*) AS c FROM notes WHERE instr(path, '/') = 0 AND path != COALESCE(?, '')"
|
|
3593
|
+
).get(excludePath);
|
|
3594
|
+
return row2?.c ?? 0;
|
|
3595
|
+
}
|
|
3596
|
+
const row = handle.prepare(
|
|
3597
|
+
"SELECT COUNT(*) AS c FROM notes WHERE path LIKE ? || '%' AND path != COALESCE(?, '')"
|
|
3598
|
+
).get(folder, excludePath);
|
|
3599
|
+
return row?.c ?? 0;
|
|
3600
|
+
}
|
|
3601
|
+
function fetchSiblings(vault, folder, excludePath) {
|
|
3602
|
+
const handle = vault.db.handle;
|
|
3603
|
+
if (folder === "") {
|
|
3604
|
+
return handle.prepare(
|
|
3605
|
+
"SELECT path, frontmatter FROM notes WHERE instr(path, '/') = 0 AND path != COALESCE(?, '')"
|
|
3606
|
+
).all(excludePath);
|
|
3607
|
+
}
|
|
3608
|
+
return handle.prepare(
|
|
3609
|
+
"SELECT path, frontmatter FROM notes WHERE path LIKE ? || '%' AND path != COALESCE(?, '')"
|
|
3610
|
+
).all(folder, excludePath);
|
|
3611
|
+
}
|
|
3612
|
+
function resolveInferenceFolder(vault, notePath, excludePath = notePath) {
|
|
3613
|
+
const start = folderOf(notePath);
|
|
3614
|
+
let current = start;
|
|
3615
|
+
let levels = 0;
|
|
3616
|
+
while (current !== null && levels < MAX_FALLBACK_LEVELS) {
|
|
3617
|
+
const count = countSiblings(vault, current, excludePath);
|
|
3618
|
+
if (count >= MIN_SIBLINGS || current === "") {
|
|
3619
|
+
return {
|
|
3620
|
+
folder: current,
|
|
3621
|
+
fellBackFrom: current === start ? null : start,
|
|
3622
|
+
siblingCount: count
|
|
3623
|
+
};
|
|
3624
|
+
}
|
|
3625
|
+
current = parentFolder(current);
|
|
3626
|
+
levels++;
|
|
3627
|
+
}
|
|
3628
|
+
return { folder: "", fellBackFrom: start, siblingCount: 0 };
|
|
3629
|
+
}
|
|
3630
|
+
function aggregateEntries(siblings) {
|
|
3631
|
+
const total = siblings.length;
|
|
3632
|
+
if (total === 0) return [];
|
|
3633
|
+
const keyPresence = /* @__PURE__ */ new Map();
|
|
3634
|
+
const keyValues = /* @__PURE__ */ new Map();
|
|
3635
|
+
for (const row of siblings) {
|
|
3636
|
+
if (!row.frontmatter) continue;
|
|
3637
|
+
let fm;
|
|
3638
|
+
try {
|
|
3639
|
+
fm = JSON.parse(row.frontmatter);
|
|
3640
|
+
} catch {
|
|
3641
|
+
continue;
|
|
3642
|
+
}
|
|
3643
|
+
if (!fm || typeof fm !== "object" || Array.isArray(fm)) continue;
|
|
3644
|
+
const obj = fm;
|
|
3645
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
3646
|
+
keyPresence.set(key, (keyPresence.get(key) ?? 0) + 1);
|
|
3647
|
+
const valKey = stableStringify(value);
|
|
3648
|
+
if (!keyValues.has(key)) keyValues.set(key, /* @__PURE__ */ new Map());
|
|
3649
|
+
const bucket = keyValues.get(key);
|
|
3650
|
+
bucket.set(valKey, (bucket.get(valKey) ?? 0) + 1);
|
|
3651
|
+
}
|
|
3652
|
+
}
|
|
3653
|
+
const entries = [];
|
|
3654
|
+
for (const [key, presenceCount] of keyPresence) {
|
|
3655
|
+
const valueBucket = keyValues.get(key);
|
|
3656
|
+
const [domValStr, domCount] = pickDominant(valueBucket);
|
|
3657
|
+
const dominantValue = domCount / presenceCount > 0.5 ? safeParse(domValStr) : null;
|
|
3658
|
+
entries.push({
|
|
3659
|
+
key,
|
|
3660
|
+
presenceCount,
|
|
3661
|
+
siblingCount: total,
|
|
3662
|
+
prevalence: presenceCount / total,
|
|
3663
|
+
dominantValue,
|
|
3664
|
+
dominantValueRatio: domCount / presenceCount
|
|
3665
|
+
});
|
|
3666
|
+
}
|
|
3667
|
+
entries.sort((a, b) => {
|
|
3668
|
+
if (b.prevalence !== a.prevalence) return b.prevalence - a.prevalence;
|
|
3669
|
+
return a.key.localeCompare(b.key);
|
|
3670
|
+
});
|
|
3671
|
+
return entries;
|
|
3672
|
+
}
|
|
3673
|
+
function pickDominant(bucket) {
|
|
3674
|
+
let bestKey = "";
|
|
3675
|
+
let bestCount = 0;
|
|
3676
|
+
for (const [k, c] of bucket) {
|
|
3677
|
+
if (c > bestCount) {
|
|
3678
|
+
bestKey = k;
|
|
3679
|
+
bestCount = c;
|
|
3680
|
+
}
|
|
3681
|
+
}
|
|
3682
|
+
return [bestKey, bestCount];
|
|
3683
|
+
}
|
|
3684
|
+
function stableStringify(v) {
|
|
3685
|
+
if (v === void 0) return "null";
|
|
3686
|
+
return JSON.stringify(v, Object.keys(v ?? {}).sort());
|
|
3687
|
+
}
|
|
3688
|
+
function safeParse(s) {
|
|
3689
|
+
try {
|
|
3690
|
+
return JSON.parse(s);
|
|
3691
|
+
} catch {
|
|
3692
|
+
return null;
|
|
3693
|
+
}
|
|
3694
|
+
}
|
|
3695
|
+
function inferFromFolder(vault, notePath, options = {}) {
|
|
3696
|
+
const excludePath = options.excludePath ?? notePath;
|
|
3697
|
+
const { folder, fellBackFrom, siblingCount } = resolveInferenceFolder(
|
|
3698
|
+
vault,
|
|
3699
|
+
notePath,
|
|
3700
|
+
excludePath
|
|
3701
|
+
);
|
|
3702
|
+
const siblings = fetchSiblings(vault, folder, excludePath);
|
|
3703
|
+
return {
|
|
3704
|
+
resolvedFolder: folder,
|
|
3705
|
+
siblingCount,
|
|
3706
|
+
fellBackFrom,
|
|
3707
|
+
entries: aggregateEntries(siblings)
|
|
3708
|
+
};
|
|
3709
|
+
}
|
|
3710
|
+
var MIN_SIBLINGS, MAX_FALLBACK_LEVELS;
|
|
3711
|
+
var init_folder_conventions = __esm({
|
|
3712
|
+
"src/schema/folder-conventions.ts"() {
|
|
3713
|
+
"use strict";
|
|
3714
|
+
init_esm_shims();
|
|
3715
|
+
MIN_SIBLINGS = 3;
|
|
3716
|
+
MAX_FALLBACK_LEVELS = 4;
|
|
3717
|
+
}
|
|
3718
|
+
});
|
|
3719
|
+
|
|
3720
|
+
// src/schema/neighbor-inference.ts
|
|
3721
|
+
function gatherNeighbors(vault, notePath, additionalForwardTargets = []) {
|
|
3722
|
+
const seenIds = /* @__PURE__ */ new Set();
|
|
3723
|
+
const out = [];
|
|
3724
|
+
const note = vault.db.notes.getByPath(notePath);
|
|
3725
|
+
if (note) {
|
|
3726
|
+
const back = vault.db.wikilinks.getBacklinks(note.id);
|
|
3727
|
+
for (const row of back) {
|
|
3728
|
+
if (seenIds.has(row.sourceNoteId)) continue;
|
|
3729
|
+
const src = vault.db.notes.getById(row.sourceNoteId);
|
|
3730
|
+
if (!src) continue;
|
|
3731
|
+
seenIds.add(src.id);
|
|
3732
|
+
out.push({ path: src.path, frontmatter: src.frontmatter });
|
|
3733
|
+
}
|
|
3734
|
+
const forward = vault.db.wikilinks.getForwardLinks(note.id);
|
|
3735
|
+
for (const row of forward) {
|
|
3736
|
+
if (row.targetNoteId === null) continue;
|
|
3737
|
+
if (seenIds.has(row.targetNoteId)) continue;
|
|
3738
|
+
const target = vault.db.notes.getById(row.targetNoteId);
|
|
3739
|
+
if (!target) continue;
|
|
3740
|
+
seenIds.add(target.id);
|
|
3741
|
+
out.push({ path: target.path, frontmatter: target.frontmatter });
|
|
3742
|
+
}
|
|
3743
|
+
}
|
|
3744
|
+
for (const target of additionalForwardTargets) {
|
|
3745
|
+
const candidate = vault.db.notes.getByPath(`${target}.md`) ?? vault.db.notes.getByPath(target);
|
|
3746
|
+
if (!candidate) continue;
|
|
3747
|
+
if (seenIds.has(candidate.id)) continue;
|
|
3748
|
+
seenIds.add(candidate.id);
|
|
3749
|
+
out.push({ path: candidate.path, frontmatter: candidate.frontmatter });
|
|
3750
|
+
}
|
|
3751
|
+
return out;
|
|
3752
|
+
}
|
|
3753
|
+
function aggregateEntries2(neighbors) {
|
|
3754
|
+
const total = neighbors.length;
|
|
3755
|
+
if (total === 0) return [];
|
|
3756
|
+
const keyPresence = /* @__PURE__ */ new Map();
|
|
3757
|
+
const keyValues = /* @__PURE__ */ new Map();
|
|
3758
|
+
for (const row of neighbors) {
|
|
3759
|
+
if (!row.frontmatter) continue;
|
|
3760
|
+
let fm;
|
|
3761
|
+
try {
|
|
3762
|
+
fm = JSON.parse(row.frontmatter);
|
|
3763
|
+
} catch {
|
|
3764
|
+
continue;
|
|
3765
|
+
}
|
|
3766
|
+
if (!fm || typeof fm !== "object" || Array.isArray(fm)) continue;
|
|
3767
|
+
const obj = fm;
|
|
3768
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
3769
|
+
keyPresence.set(key, (keyPresence.get(key) ?? 0) + 1);
|
|
3770
|
+
const valKey = JSON.stringify(value, Object.keys(value ?? {}).sort());
|
|
3771
|
+
if (!keyValues.has(key)) keyValues.set(key, /* @__PURE__ */ new Map());
|
|
3772
|
+
const bucket = keyValues.get(key);
|
|
3773
|
+
bucket.set(valKey, (bucket.get(valKey) ?? 0) + 1);
|
|
3774
|
+
}
|
|
3775
|
+
}
|
|
3776
|
+
const entries = [];
|
|
3777
|
+
for (const [key, presenceCount] of keyPresence) {
|
|
3778
|
+
const valueBucket = keyValues.get(key);
|
|
3779
|
+
let bestKey = "";
|
|
3780
|
+
let bestCount = 0;
|
|
3781
|
+
for (const [k, c] of valueBucket) {
|
|
3782
|
+
if (c > bestCount) {
|
|
3783
|
+
bestKey = k;
|
|
3784
|
+
bestCount = c;
|
|
3785
|
+
}
|
|
3786
|
+
}
|
|
3787
|
+
const dominantValue = bestCount / presenceCount > 0.5 ? safeParse2(bestKey) : null;
|
|
3788
|
+
entries.push({
|
|
3789
|
+
key,
|
|
3790
|
+
neighborCount: presenceCount,
|
|
3791
|
+
totalNeighbors: total,
|
|
3792
|
+
prevalence: presenceCount / total,
|
|
3793
|
+
dominantValue,
|
|
3794
|
+
dominantValueRatio: bestCount / presenceCount
|
|
3795
|
+
});
|
|
3796
|
+
}
|
|
3797
|
+
entries.sort((a, b) => {
|
|
3798
|
+
if (b.prevalence !== a.prevalence) return b.prevalence - a.prevalence;
|
|
3799
|
+
return a.key.localeCompare(b.key);
|
|
3800
|
+
});
|
|
3801
|
+
return entries;
|
|
3802
|
+
}
|
|
3803
|
+
function safeParse2(s) {
|
|
3804
|
+
try {
|
|
3805
|
+
return JSON.parse(s);
|
|
3806
|
+
} catch {
|
|
3807
|
+
return null;
|
|
3808
|
+
}
|
|
3809
|
+
}
|
|
3810
|
+
function inferFromNeighbors(vault, notePath, additionalForwardTargets = []) {
|
|
3811
|
+
const neighbors = gatherNeighbors(
|
|
3812
|
+
vault,
|
|
3813
|
+
notePath,
|
|
3814
|
+
additionalForwardTargets
|
|
3815
|
+
);
|
|
3816
|
+
const note = vault.db.notes.getByPath(notePath);
|
|
3817
|
+
let forwardCount = 0;
|
|
3818
|
+
let backwardCount = 0;
|
|
3819
|
+
if (note) {
|
|
3820
|
+
forwardCount = vault.db.wikilinks.getForwardLinks(note.id).filter((r) => r.targetNoteId !== null).length;
|
|
3821
|
+
backwardCount = vault.db.wikilinks.getBacklinks(note.id).length;
|
|
3822
|
+
}
|
|
3823
|
+
return {
|
|
3824
|
+
forwardCount,
|
|
3825
|
+
backwardCount,
|
|
3826
|
+
totalNeighbors: neighbors.length,
|
|
3827
|
+
entries: aggregateEntries2(neighbors)
|
|
3828
|
+
};
|
|
3829
|
+
}
|
|
3830
|
+
var init_neighbor_inference = __esm({
|
|
3831
|
+
"src/schema/neighbor-inference.ts"() {
|
|
3832
|
+
"use strict";
|
|
3833
|
+
init_esm_shims();
|
|
3834
|
+
}
|
|
3835
|
+
});
|
|
3836
|
+
|
|
3837
|
+
// src/schema/content-heuristics.ts
|
|
3838
|
+
function inferFromContent(input) {
|
|
3839
|
+
const heuristicInput = {
|
|
3840
|
+
title: input.title,
|
|
3841
|
+
bodyHead: input.body.slice(0, 2e3),
|
|
3842
|
+
fullBody: input.body
|
|
3843
|
+
};
|
|
3844
|
+
const entries = [];
|
|
3845
|
+
const matchedRules = [];
|
|
3846
|
+
for (const rule of RULES) {
|
|
3847
|
+
const matches = rule.match(heuristicInput);
|
|
3848
|
+
if (matches.length > 0) {
|
|
3849
|
+
matchedRules.push(rule.name);
|
|
3850
|
+
for (const m of matches) {
|
|
3851
|
+
entries.push({ ...m, rule: rule.name });
|
|
3852
|
+
}
|
|
3853
|
+
}
|
|
3854
|
+
}
|
|
3855
|
+
return { entries, matchedRules };
|
|
3856
|
+
}
|
|
3857
|
+
var DEFAULT_CONFIDENCE, STRONG_CONFIDENCE, WEAK_CONFIDENCE, emailRule, meetingRule, personRule, clippingRule, factRule, dateInTitleRule, RULES;
|
|
3858
|
+
var init_content_heuristics = __esm({
|
|
3859
|
+
"src/schema/content-heuristics.ts"() {
|
|
3860
|
+
"use strict";
|
|
3861
|
+
init_esm_shims();
|
|
3862
|
+
DEFAULT_CONFIDENCE = 0.7;
|
|
3863
|
+
STRONG_CONFIDENCE = 0.85;
|
|
3864
|
+
WEAK_CONFIDENCE = 0.5;
|
|
3865
|
+
emailRule = {
|
|
3866
|
+
name: "email-title-or-header",
|
|
3867
|
+
match: ({ title, bodyHead }) => {
|
|
3868
|
+
const titleMatch = /^(E-?Mail|Email|Mail)\s+(von|from)\s+\S+/i.test(title) || /^(Re|Fwd|AW|WG):\s/i.test(title);
|
|
3869
|
+
const headerMatch = /^(From|Von):\s+\S+/im.test(bodyHead) && /^(To|An):\s+\S+/im.test(bodyHead);
|
|
3870
|
+
if (!titleMatch && !headerMatch) return [];
|
|
3871
|
+
return [
|
|
3872
|
+
{ key: "class", value: "Email", confidence: STRONG_CONFIDENCE },
|
|
3873
|
+
{ key: "type", value: "email", confidence: STRONG_CONFIDENCE }
|
|
3874
|
+
];
|
|
3875
|
+
}
|
|
3876
|
+
};
|
|
3877
|
+
meetingRule = {
|
|
3878
|
+
name: "meeting-title-keyword",
|
|
3879
|
+
match: ({ title, bodyHead }) => {
|
|
3880
|
+
const keywords = /\b(Meeting|Treffen|Call|Sondierung|Termin|Standup|Sync|Kickoff|Kick-off|Jour\s*fixe|Workshop)\b/i;
|
|
3881
|
+
const isMeeting = keywords.test(title) || /^\d{4}-\d{2}-\d{2}.*\b(Meeting|Treffen|Call|Sondierung)/i.test(title);
|
|
3882
|
+
if (!isMeeting) return [];
|
|
3883
|
+
const attendeesPresent = /^(Attendees|Teilnehmer|Participants):/im.test(bodyHead);
|
|
3884
|
+
const conf = attendeesPresent ? STRONG_CONFIDENCE : DEFAULT_CONFIDENCE;
|
|
3885
|
+
return [
|
|
3886
|
+
{ key: "class", value: "Meeting", confidence: conf },
|
|
3887
|
+
{ key: "type", value: "meeting", confidence: conf }
|
|
3888
|
+
];
|
|
3889
|
+
}
|
|
3890
|
+
};
|
|
3891
|
+
personRule = {
|
|
3892
|
+
name: "person-name-title-with-corroboration",
|
|
3893
|
+
match: ({ title, bodyHead }) => {
|
|
3894
|
+
const nameLike = /^[A-ZÄÖÜ][a-zäöüß'\-]+( [A-ZÄÖÜ][a-zäöüß'\-]+){0,3}$/.test(title.trim());
|
|
3895
|
+
if (!nameLike) return [];
|
|
3896
|
+
const corroborating = /linkedin\.com\/in\//i.test(bodyHead) || /\b[\w._-]+@[\w.-]+\.[a-z]{2,}\b/i.test(bodyHead) || /\+?\d[\d\s\-./()]{6,}/.test(bodyHead);
|
|
3897
|
+
if (!corroborating) return [];
|
|
3898
|
+
return [
|
|
3899
|
+
{ key: "class", value: "Person", confidence: STRONG_CONFIDENCE },
|
|
3900
|
+
{ key: "type", value: "person", confidence: STRONG_CONFIDENCE },
|
|
3901
|
+
{ key: "participation", value: [], confidence: WEAK_CONFIDENCE }
|
|
3902
|
+
];
|
|
3903
|
+
}
|
|
3904
|
+
};
|
|
3905
|
+
clippingRule = {
|
|
3906
|
+
name: "clipping-source-url",
|
|
3907
|
+
match: ({ bodyHead }) => {
|
|
3908
|
+
const headSnippet = bodyHead.slice(0, 500);
|
|
3909
|
+
const hasMdLink = /^\s*\[.+\]\(https?:\/\/[^\s)]+\)/m.test(headSnippet);
|
|
3910
|
+
const hasSourceField = /^source:\s*https?:\/\//im.test(headSnippet);
|
|
3911
|
+
if (!hasMdLink && !hasSourceField) return [];
|
|
3912
|
+
return [
|
|
3913
|
+
{ key: "class", value: "Clipping", confidence: DEFAULT_CONFIDENCE },
|
|
3914
|
+
{ key: "tags", value: ["clippings"], confidence: DEFAULT_CONFIDENCE }
|
|
3915
|
+
];
|
|
3916
|
+
}
|
|
3917
|
+
};
|
|
3918
|
+
factRule = {
|
|
3919
|
+
name: "short-fact",
|
|
3920
|
+
match: ({ fullBody }) => {
|
|
3921
|
+
const trimmed = fullBody.trim();
|
|
3922
|
+
if (trimmed.length === 0 || trimmed.length > 150) return [];
|
|
3923
|
+
if (/\n\s*\n/.test(trimmed)) return [];
|
|
3924
|
+
return [
|
|
3925
|
+
{ key: "class", value: "Fact", confidence: WEAK_CONFIDENCE }
|
|
3926
|
+
];
|
|
3927
|
+
}
|
|
3928
|
+
};
|
|
3929
|
+
dateInTitleRule = {
|
|
3930
|
+
name: "date-prefix-in-title",
|
|
3931
|
+
match: ({ title }) => {
|
|
3932
|
+
const m = title.match(/^(\d{4})-(\d{2})-(\d{2})/);
|
|
3933
|
+
if (!m) return [];
|
|
3934
|
+
const iso = `${m[1]}-${m[2]}-${m[3]}`;
|
|
3935
|
+
return [
|
|
3936
|
+
{ key: "created", value: iso, confidence: STRONG_CONFIDENCE }
|
|
3937
|
+
];
|
|
3938
|
+
}
|
|
3939
|
+
};
|
|
3940
|
+
RULES = [
|
|
3941
|
+
emailRule,
|
|
3942
|
+
meetingRule,
|
|
3943
|
+
personRule,
|
|
3944
|
+
clippingRule,
|
|
3945
|
+
factRule,
|
|
3946
|
+
dateInTitleRule
|
|
3947
|
+
];
|
|
3948
|
+
}
|
|
3949
|
+
});
|
|
3950
|
+
|
|
3951
|
+
// src/schema/combiner.ts
|
|
3952
|
+
function valueKey(v) {
|
|
3953
|
+
if (v === null || v === void 0) return "null";
|
|
3954
|
+
if (Array.isArray(v)) {
|
|
3955
|
+
return "[" + v.map(valueKey).join(",") + "]";
|
|
3956
|
+
}
|
|
3957
|
+
if (typeof v === "object") {
|
|
3958
|
+
const obj = v;
|
|
3959
|
+
const keys = Object.keys(obj).sort();
|
|
3960
|
+
return "{" + keys.map((k) => JSON.stringify(k) + ":" + valueKey(obj[k])).join(",") + "}";
|
|
3961
|
+
}
|
|
3962
|
+
return JSON.stringify(v);
|
|
3963
|
+
}
|
|
3964
|
+
function suggestFrontmatter(input) {
|
|
3965
|
+
const title = input.title ?? defaultTitleFromPath(input.path);
|
|
3966
|
+
const folder = inferFromFolder(input.vault, input.path, {
|
|
3967
|
+
excludePath: input.excludePath ?? input.path
|
|
3968
|
+
});
|
|
3969
|
+
const neighbor = inferFromNeighbors(
|
|
3970
|
+
input.vault,
|
|
3971
|
+
input.path,
|
|
3972
|
+
input.draftWikilinkTargets ?? []
|
|
3973
|
+
);
|
|
3974
|
+
const content = input.content !== void 0 ? inferFromContent({ title, body: input.content }) : { entries: [], matchedRules: [] };
|
|
3975
|
+
return combineSuggestions({
|
|
3976
|
+
existingFrontmatter: input.existingFrontmatter ?? null,
|
|
3977
|
+
folder,
|
|
3978
|
+
neighbor,
|
|
3979
|
+
content
|
|
3980
|
+
});
|
|
3981
|
+
}
|
|
3982
|
+
function defaultTitleFromPath(path5) {
|
|
3983
|
+
const base = path5.split("/").pop() ?? path5;
|
|
3984
|
+
return base.replace(/\.md$/i, "");
|
|
3985
|
+
}
|
|
3986
|
+
function combineSuggestions(args2) {
|
|
3987
|
+
const { existingFrontmatter, folder, neighbor, content } = args2;
|
|
3988
|
+
const candidates = /* @__PURE__ */ new Map();
|
|
3989
|
+
const push = (key, c) => {
|
|
3990
|
+
if (!candidates.has(key)) candidates.set(key, []);
|
|
3991
|
+
candidates.get(key).push(c);
|
|
3992
|
+
};
|
|
3993
|
+
for (const e of folder.entries) {
|
|
3994
|
+
if (e.prevalence < MIN_PRESENTATION_CONFIDENCE) continue;
|
|
3995
|
+
push(e.key, {
|
|
3996
|
+
source: "folder",
|
|
3997
|
+
value: e.dominantValue,
|
|
3998
|
+
confidence: e.prevalence
|
|
3999
|
+
});
|
|
4000
|
+
}
|
|
4001
|
+
for (const e of neighbor.entries) {
|
|
4002
|
+
const conf = e.prevalence * NEIGHBOR_DAMPING;
|
|
4003
|
+
if (conf < MIN_PRESENTATION_CONFIDENCE) continue;
|
|
4004
|
+
push(e.key, {
|
|
4005
|
+
source: "neighbor",
|
|
4006
|
+
value: e.dominantValue,
|
|
4007
|
+
confidence: conf
|
|
4008
|
+
});
|
|
4009
|
+
}
|
|
4010
|
+
for (const e of content.entries) {
|
|
4011
|
+
push(e.key, {
|
|
4012
|
+
source: "content",
|
|
4013
|
+
value: e.value,
|
|
4014
|
+
confidence: e.confidence,
|
|
4015
|
+
rule: e.rule
|
|
4016
|
+
});
|
|
4017
|
+
}
|
|
4018
|
+
const existing = [];
|
|
4019
|
+
const suggestions = [];
|
|
4020
|
+
const conflicts = [];
|
|
4021
|
+
const fm = existingFrontmatter ?? {};
|
|
4022
|
+
const existingKeys = new Set(Object.keys(fm));
|
|
4023
|
+
const allKeys = /* @__PURE__ */ new Set([
|
|
4024
|
+
...candidates.keys(),
|
|
4025
|
+
...existingKeys
|
|
4026
|
+
]);
|
|
4027
|
+
for (const key of allKeys) {
|
|
4028
|
+
const cands = candidates.get(key) ?? [];
|
|
4029
|
+
const existingValue = existingKeys.has(key) ? fm[key] : void 0;
|
|
4030
|
+
const hasExisting = existingValue !== void 0;
|
|
4031
|
+
const existingValueKey = hasExisting ? valueKey(existingValue) : null;
|
|
4032
|
+
const byValue = /* @__PURE__ */ new Map();
|
|
4033
|
+
for (const c of cands) {
|
|
4034
|
+
if (c.value === null) {
|
|
4035
|
+
const k = "__keyonly__";
|
|
4036
|
+
if (!byValue.has(k)) byValue.set(k, []);
|
|
4037
|
+
byValue.get(k).push(c);
|
|
4038
|
+
} else {
|
|
4039
|
+
const k = valueKey(c.value);
|
|
4040
|
+
if (!byValue.has(k)) byValue.set(k, []);
|
|
4041
|
+
byValue.get(k).push(c);
|
|
4042
|
+
}
|
|
4043
|
+
}
|
|
4044
|
+
const distinctValueCount = Array.from(byValue.keys()).filter(
|
|
4045
|
+
(k) => k !== "__keyonly__"
|
|
4046
|
+
).length;
|
|
4047
|
+
if (hasExisting) {
|
|
4048
|
+
const agreeingBucket = byValue.get(existingValueKey);
|
|
4049
|
+
if (agreeingBucket) {
|
|
4050
|
+
byValue.delete(existingValueKey);
|
|
4051
|
+
}
|
|
4052
|
+
const disagreeingValues = Array.from(byValue.entries()).filter(
|
|
4053
|
+
([k]) => k !== "__keyonly__"
|
|
4054
|
+
);
|
|
4055
|
+
if (disagreeingValues.length === 0) {
|
|
4056
|
+
existing.push({ key, value: existingValue });
|
|
4057
|
+
} else {
|
|
4058
|
+
const candidatesList = [
|
|
4059
|
+
{
|
|
4060
|
+
value: existingValue,
|
|
4061
|
+
source: "existing",
|
|
4062
|
+
confidence: 1
|
|
4063
|
+
}
|
|
4064
|
+
];
|
|
4065
|
+
for (const [, group] of disagreeingValues) {
|
|
4066
|
+
const best = pickBestCandidate(group);
|
|
4067
|
+
candidatesList.push({
|
|
4068
|
+
value: best.value,
|
|
4069
|
+
source: best.source,
|
|
4070
|
+
confidence: best.confidence,
|
|
4071
|
+
...best.rule ? { rule: best.rule } : {}
|
|
4072
|
+
});
|
|
4073
|
+
}
|
|
4074
|
+
conflicts.push({ key, candidates: candidatesList });
|
|
4075
|
+
}
|
|
4076
|
+
} else {
|
|
4077
|
+
if (distinctValueCount > 1) {
|
|
4078
|
+
const candidatesList = [];
|
|
4079
|
+
for (const [k, group] of byValue) {
|
|
4080
|
+
if (k === "__keyonly__") continue;
|
|
4081
|
+
const best = pickBestCandidate(group);
|
|
4082
|
+
candidatesList.push({
|
|
4083
|
+
value: best.value,
|
|
4084
|
+
source: best.source,
|
|
4085
|
+
confidence: best.confidence,
|
|
4086
|
+
...best.rule ? { rule: best.rule } : {}
|
|
4087
|
+
});
|
|
4088
|
+
}
|
|
4089
|
+
candidatesList.sort((a, b) => b.confidence - a.confidence);
|
|
4090
|
+
conflicts.push({ key, candidates: candidatesList });
|
|
4091
|
+
} else if (distinctValueCount === 1) {
|
|
4092
|
+
const [valueKeyStr, group] = Array.from(byValue.entries()).find(
|
|
4093
|
+
([k]) => k !== "__keyonly__"
|
|
4094
|
+
);
|
|
4095
|
+
const best = pickBestCandidate(group);
|
|
4096
|
+
const sources = uniqueSources(group);
|
|
4097
|
+
suggestions.push({
|
|
4098
|
+
key,
|
|
4099
|
+
suggestedValue: best.value,
|
|
4100
|
+
confidence: best.confidence,
|
|
4101
|
+
sources,
|
|
4102
|
+
...best.rule ? { rule: best.rule } : {}
|
|
4103
|
+
});
|
|
4104
|
+
void valueKeyStr;
|
|
4105
|
+
} else {
|
|
4106
|
+
const group = byValue.get("__keyonly__");
|
|
4107
|
+
const best = pickBestCandidate(group);
|
|
4108
|
+
suggestions.push({
|
|
4109
|
+
key,
|
|
4110
|
+
suggestedValue: null,
|
|
4111
|
+
confidence: best.confidence,
|
|
4112
|
+
sources: uniqueSources(group)
|
|
4113
|
+
});
|
|
4114
|
+
}
|
|
4115
|
+
}
|
|
4116
|
+
}
|
|
4117
|
+
suggestions.sort((a, b) => {
|
|
4118
|
+
if (b.confidence !== a.confidence) return b.confidence - a.confidence;
|
|
4119
|
+
return a.key.localeCompare(b.key);
|
|
4120
|
+
});
|
|
4121
|
+
conflicts.sort((a, b) => a.key.localeCompare(b.key));
|
|
4122
|
+
existing.sort((a, b) => a.key.localeCompare(b.key));
|
|
4123
|
+
return {
|
|
4124
|
+
existing,
|
|
4125
|
+
suggestions,
|
|
4126
|
+
conflicts,
|
|
4127
|
+
diagnostics: { folder, neighbor, content }
|
|
4128
|
+
};
|
|
4129
|
+
}
|
|
4130
|
+
function pickBestCandidate(group) {
|
|
4131
|
+
if (group.length === 0) {
|
|
4132
|
+
throw new Error("pickBestCandidate called with empty group");
|
|
4133
|
+
}
|
|
4134
|
+
let best = group[0];
|
|
4135
|
+
for (const c of group) {
|
|
4136
|
+
if (c.confidence > best.confidence) best = c;
|
|
4137
|
+
}
|
|
4138
|
+
return best;
|
|
4139
|
+
}
|
|
4140
|
+
function uniqueSources(group) {
|
|
4141
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4142
|
+
const out = [];
|
|
4143
|
+
const sorted = [...group].sort((a, b) => b.confidence - a.confidence);
|
|
4144
|
+
for (const c of sorted) {
|
|
4145
|
+
if (seen.has(c.source)) continue;
|
|
4146
|
+
seen.add(c.source);
|
|
4147
|
+
out.push(c.source);
|
|
4148
|
+
}
|
|
4149
|
+
return out;
|
|
4150
|
+
}
|
|
4151
|
+
var NEIGHBOR_DAMPING, MIN_PRESENTATION_CONFIDENCE;
|
|
4152
|
+
var init_combiner = __esm({
|
|
4153
|
+
"src/schema/combiner.ts"() {
|
|
4154
|
+
"use strict";
|
|
4155
|
+
init_esm_shims();
|
|
4156
|
+
init_folder_conventions();
|
|
4157
|
+
init_neighbor_inference();
|
|
4158
|
+
init_content_heuristics();
|
|
4159
|
+
NEIGHBOR_DAMPING = 0.6;
|
|
4160
|
+
MIN_PRESENTATION_CONFIDENCE = 0.2;
|
|
4161
|
+
}
|
|
4162
|
+
});
|
|
4163
|
+
|
|
4164
|
+
// src/schema/index.ts
|
|
4165
|
+
var init_schema2 = __esm({
|
|
4166
|
+
"src/schema/index.ts"() {
|
|
4167
|
+
"use strict";
|
|
4168
|
+
init_esm_shims();
|
|
4169
|
+
init_folder_conventions();
|
|
4170
|
+
init_neighbor_inference();
|
|
4171
|
+
init_content_heuristics();
|
|
4172
|
+
init_combiner();
|
|
4173
|
+
}
|
|
4174
|
+
});
|
|
4175
|
+
|
|
3576
4176
|
// src/indexer/single.ts
|
|
3577
4177
|
import * as path4 from "path";
|
|
3578
4178
|
async function indexNote(options) {
|
|
@@ -5137,6 +5737,33 @@ async function serve() {
|
|
|
5137
5737
|
}
|
|
5138
5738
|
}
|
|
5139
5739
|
}
|
|
5740
|
+
},
|
|
5741
|
+
{
|
|
5742
|
+
name: "suggest_frontmatter",
|
|
5743
|
+
description: "Suggest frontmatter fields for a note based on folder-conventions, wikilink-neighborhood, and title/body content-heuristics. Returns {existing, suggestions, conflicts}. Two input modes: (1) existing note via {path}; (2) draft via {content, folder_hint, title}. At least one of path/content required. Suggestions sorted by confidence DESC; conflicts list disagreements between sources.",
|
|
5744
|
+
inputSchema: {
|
|
5745
|
+
type: "object",
|
|
5746
|
+
required: ["vault"],
|
|
5747
|
+
properties: {
|
|
5748
|
+
vault: { type: "string" },
|
|
5749
|
+
path: {
|
|
5750
|
+
type: "string",
|
|
5751
|
+
description: "Vault-relative path. Required for existing-note mode; for drafts, pass content instead (folder_hint controls folder-inference)."
|
|
5752
|
+
},
|
|
5753
|
+
content: {
|
|
5754
|
+
type: "string",
|
|
5755
|
+
description: "Draft markdown body. When set, content-heuristics layer runs. If path is set AND content is omitted, the existing note's stored content is used."
|
|
5756
|
+
},
|
|
5757
|
+
title: {
|
|
5758
|
+
type: "string",
|
|
5759
|
+
description: "Title for content-heuristics. Falls back to path basename or first heading."
|
|
5760
|
+
},
|
|
5761
|
+
folder_hint: {
|
|
5762
|
+
type: "string",
|
|
5763
|
+
description: "For draft mode: the target folder (e.g. 'Personen/'). Ignored when `path` is set."
|
|
5764
|
+
}
|
|
5765
|
+
}
|
|
5766
|
+
}
|
|
5140
5767
|
}
|
|
5141
5768
|
]
|
|
5142
5769
|
}));
|
|
@@ -5350,6 +5977,10 @@ async function serve() {
|
|
|
5350
5977
|
)
|
|
5351
5978
|
);
|
|
5352
5979
|
}
|
|
5980
|
+
case "suggest_frontmatter": {
|
|
5981
|
+
const parsed = SuggestFrontmatterArgs.parse(args2 ?? {});
|
|
5982
|
+
return ok(handleSuggestFrontmatter(manager, parsed));
|
|
5983
|
+
}
|
|
5353
5984
|
default:
|
|
5354
5985
|
return errorResponse(`Unknown tool: ${name}`);
|
|
5355
5986
|
}
|
|
@@ -5723,6 +6354,71 @@ function handleRecentNotes(manager, vaultFilter, limit, since) {
|
|
|
5723
6354
|
all.sort((a, b) => b.mtime - a.mtime);
|
|
5724
6355
|
return { notes: all.slice(0, limit), count: Math.min(all.length, limit) };
|
|
5725
6356
|
}
|
|
6357
|
+
function handleSuggestFrontmatter(manager, parsed) {
|
|
6358
|
+
const vault = manager.require(parsed.vault);
|
|
6359
|
+
if (parsed.path) {
|
|
6360
|
+
const note = vault.db.notes.getByPath(parsed.path);
|
|
6361
|
+
if (!note) {
|
|
6362
|
+
throw new Error(
|
|
6363
|
+
`Note not found: ${parsed.vault}/${parsed.path}. Use draft mode ({content, folder_hint}) for unindexed notes.`
|
|
6364
|
+
);
|
|
6365
|
+
}
|
|
6366
|
+
const existingFm = note.frontmatter ? safeParseFrontmatter(note.frontmatter) : null;
|
|
6367
|
+
const result2 = suggestFrontmatter({
|
|
6368
|
+
vault,
|
|
6369
|
+
path: note.path,
|
|
6370
|
+
existingFrontmatter: existingFm,
|
|
6371
|
+
content: parsed.content ?? note.content,
|
|
6372
|
+
title: parsed.title ?? note.title ?? defaultBasename(note.path),
|
|
6373
|
+
excludePath: note.path
|
|
6374
|
+
});
|
|
6375
|
+
return {
|
|
6376
|
+
mode: "existing",
|
|
6377
|
+
path: note.path,
|
|
6378
|
+
...result2
|
|
6379
|
+
};
|
|
6380
|
+
}
|
|
6381
|
+
const folderHint = normalizeFolderHint(parsed.folder_hint);
|
|
6382
|
+
const probePath = `${folderHint}__draft__${Date.now()}.md`;
|
|
6383
|
+
const result = suggestFrontmatter({
|
|
6384
|
+
vault,
|
|
6385
|
+
path: probePath,
|
|
6386
|
+
existingFrontmatter: null,
|
|
6387
|
+
content: parsed.content,
|
|
6388
|
+
title: parsed.title ?? "Draft",
|
|
6389
|
+
// Exclude the synthetic path explicitly — though it won't match any
|
|
6390
|
+
// existing note, this future-proofs against collisions.
|
|
6391
|
+
excludePath: probePath
|
|
6392
|
+
});
|
|
6393
|
+
return {
|
|
6394
|
+
mode: "draft",
|
|
6395
|
+
folder_hint: folderHint,
|
|
6396
|
+
note: "Draft mode: no backlinks contributed. Provide `path` (and index the note first) for richer neighbor-inference.",
|
|
6397
|
+
...result
|
|
6398
|
+
};
|
|
6399
|
+
}
|
|
6400
|
+
function safeParseFrontmatter(s) {
|
|
6401
|
+
try {
|
|
6402
|
+
const parsed = JSON.parse(s);
|
|
6403
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
6404
|
+
return parsed;
|
|
6405
|
+
}
|
|
6406
|
+
return null;
|
|
6407
|
+
} catch {
|
|
6408
|
+
return null;
|
|
6409
|
+
}
|
|
6410
|
+
}
|
|
6411
|
+
function defaultBasename(path5) {
|
|
6412
|
+
const base = path5.split("/").pop() ?? path5;
|
|
6413
|
+
return base.replace(/\.md$/i, "");
|
|
6414
|
+
}
|
|
6415
|
+
function normalizeFolderHint(hint) {
|
|
6416
|
+
if (!hint) return "";
|
|
6417
|
+
let h = hint.trim();
|
|
6418
|
+
if (h.startsWith("/")) h = h.slice(1);
|
|
6419
|
+
if (h.length > 0 && !h.endsWith("/")) h = `${h}/`;
|
|
6420
|
+
return h;
|
|
6421
|
+
}
|
|
5726
6422
|
function ok(data) {
|
|
5727
6423
|
return {
|
|
5728
6424
|
content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
|
|
@@ -5734,7 +6430,7 @@ function errorResponse(message) {
|
|
|
5734
6430
|
content: [{ type: "text", text: message }]
|
|
5735
6431
|
};
|
|
5736
6432
|
}
|
|
5737
|
-
var VERSION, ReadNoteArgs, SearchArgs, HybridSearchArgs, VaultPathArgs, ForwardLinksArgs, FindBrokenLinksArgs, PredicateSchema, QueryFrontmatterArgs, WriteNoteArgs, UpdateFrontmatterArgs, DeleteNoteArgs, AuditLogArgs, IndexRunsArgs, ListModelsArgs, StartShadowIndexArgs, SwitchActiveModelArgs, VacuumEmbeddingsArgs, SearchCompatArgs, FetchCompatArgs, VaultStatsArgs, RecentNotesArgs;
|
|
6433
|
+
var VERSION, ReadNoteArgs, SearchArgs, HybridSearchArgs, VaultPathArgs, ForwardLinksArgs, FindBrokenLinksArgs, PredicateSchema, QueryFrontmatterArgs, WriteNoteArgs, UpdateFrontmatterArgs, DeleteNoteArgs, AuditLogArgs, IndexRunsArgs, ListModelsArgs, StartShadowIndexArgs, SwitchActiveModelArgs, VacuumEmbeddingsArgs, SearchCompatArgs, FetchCompatArgs, VaultStatsArgs, RecentNotesArgs, SuggestFrontmatterArgs;
|
|
5738
6434
|
var init_server = __esm({
|
|
5739
6435
|
"src/server.ts"() {
|
|
5740
6436
|
"use strict";
|
|
@@ -5747,11 +6443,12 @@ var init_server = __esm({
|
|
|
5747
6443
|
init_rerank();
|
|
5748
6444
|
init_graph2();
|
|
5749
6445
|
init_frontmatter();
|
|
6446
|
+
init_schema2();
|
|
5750
6447
|
init_write2();
|
|
5751
6448
|
init_audit3();
|
|
5752
6449
|
init_watcher2();
|
|
5753
6450
|
init_indexer2();
|
|
5754
|
-
VERSION = "0.
|
|
6451
|
+
VERSION = "1.0.0";
|
|
5755
6452
|
ReadNoteArgs = z3.object({
|
|
5756
6453
|
vault: z3.string(),
|
|
5757
6454
|
path: z3.string()
|
|
@@ -5857,6 +6554,15 @@ var init_server = __esm({
|
|
|
5857
6554
|
limit: z3.number().int().positive().max(200).optional().default(20),
|
|
5858
6555
|
since: z3.number().int().nonnegative().optional()
|
|
5859
6556
|
});
|
|
6557
|
+
SuggestFrontmatterArgs = z3.object({
|
|
6558
|
+
vault: z3.string(),
|
|
6559
|
+
path: z3.string().optional(),
|
|
6560
|
+
content: z3.string().optional(),
|
|
6561
|
+
title: z3.string().optional(),
|
|
6562
|
+
folder_hint: z3.string().optional()
|
|
6563
|
+
}).refine((v) => v.path !== void 0 || v.content !== void 0, {
|
|
6564
|
+
message: "suggest_frontmatter requires either `path` or `content`"
|
|
6565
|
+
});
|
|
5860
6566
|
}
|
|
5861
6567
|
});
|
|
5862
6568
|
|