smoking-mirror 1.1.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +344 -204
- package/dist/index.js +1180 -7
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -212,8 +212,8 @@ var PROGRESS_INTERVAL = 100;
|
|
|
212
212
|
function normalizeTarget(target) {
|
|
213
213
|
return target.toLowerCase().replace(/\.md$/, "");
|
|
214
214
|
}
|
|
215
|
-
function normalizeNotePath(
|
|
216
|
-
return
|
|
215
|
+
function normalizeNotePath(path6) {
|
|
216
|
+
return path6.toLowerCase().replace(/\.md$/, "");
|
|
217
217
|
}
|
|
218
218
|
async function buildVaultIndex(vaultPath2, options = {}) {
|
|
219
219
|
const { timeoutMs = DEFAULT_TIMEOUT_MS, onProgress } = options;
|
|
@@ -717,14 +717,14 @@ function registerWikilinkTools(server2, getIndex, getVaultPath) {
|
|
|
717
717
|
};
|
|
718
718
|
function findSimilarEntity(target, entities) {
|
|
719
719
|
const targetLower = target.toLowerCase();
|
|
720
|
-
for (const [name,
|
|
720
|
+
for (const [name, path6] of entities) {
|
|
721
721
|
if (name.startsWith(targetLower) || targetLower.startsWith(name)) {
|
|
722
|
-
return
|
|
722
|
+
return path6;
|
|
723
723
|
}
|
|
724
724
|
}
|
|
725
|
-
for (const [name,
|
|
725
|
+
for (const [name, path6] of entities) {
|
|
726
726
|
if (name.includes(targetLower) || targetLower.includes(name)) {
|
|
727
|
-
return
|
|
727
|
+
return path6;
|
|
728
728
|
}
|
|
729
729
|
}
|
|
730
730
|
return void 0;
|
|
@@ -1584,6 +1584,1174 @@ function registerSystemTools(server2, getIndex, setIndex, getVaultPath) {
|
|
|
1584
1584
|
);
|
|
1585
1585
|
}
|
|
1586
1586
|
|
|
1587
|
+
// src/tools/primitives.ts
|
|
1588
|
+
import { z as z6 } from "zod";
|
|
1589
|
+
|
|
1590
|
+
// src/tools/temporal.ts
|
|
1591
|
+
function getNotesModifiedOn(index, date) {
|
|
1592
|
+
const targetDate = new Date(date);
|
|
1593
|
+
const targetDay = targetDate.toISOString().split("T")[0];
|
|
1594
|
+
const results = [];
|
|
1595
|
+
for (const note of index.notes.values()) {
|
|
1596
|
+
const noteDay = note.modified.toISOString().split("T")[0];
|
|
1597
|
+
if (noteDay === targetDay) {
|
|
1598
|
+
results.push({
|
|
1599
|
+
path: note.path,
|
|
1600
|
+
title: note.title,
|
|
1601
|
+
created: note.created,
|
|
1602
|
+
modified: note.modified
|
|
1603
|
+
});
|
|
1604
|
+
}
|
|
1605
|
+
}
|
|
1606
|
+
return results.sort((a, b) => b.modified.getTime() - a.modified.getTime());
|
|
1607
|
+
}
|
|
1608
|
+
function getNotesInRange(index, startDate, endDate) {
|
|
1609
|
+
const start = new Date(startDate);
|
|
1610
|
+
start.setHours(0, 0, 0, 0);
|
|
1611
|
+
const end = new Date(endDate);
|
|
1612
|
+
end.setHours(23, 59, 59, 999);
|
|
1613
|
+
const results = [];
|
|
1614
|
+
for (const note of index.notes.values()) {
|
|
1615
|
+
if (note.modified >= start && note.modified <= end) {
|
|
1616
|
+
results.push({
|
|
1617
|
+
path: note.path,
|
|
1618
|
+
title: note.title,
|
|
1619
|
+
created: note.created,
|
|
1620
|
+
modified: note.modified
|
|
1621
|
+
});
|
|
1622
|
+
}
|
|
1623
|
+
}
|
|
1624
|
+
return results.sort((a, b) => b.modified.getTime() - a.modified.getTime());
|
|
1625
|
+
}
|
|
1626
|
+
function getStaleNotes(index, days, minBacklinks = 0) {
|
|
1627
|
+
const cutoff = /* @__PURE__ */ new Date();
|
|
1628
|
+
cutoff.setDate(cutoff.getDate() - days);
|
|
1629
|
+
const results = [];
|
|
1630
|
+
for (const note of index.notes.values()) {
|
|
1631
|
+
if (note.modified < cutoff) {
|
|
1632
|
+
const backlinkCount = getBacklinksForNote(index, note.path).length;
|
|
1633
|
+
if (backlinkCount >= minBacklinks) {
|
|
1634
|
+
const daysSince = Math.floor(
|
|
1635
|
+
(Date.now() - note.modified.getTime()) / (1e3 * 60 * 60 * 24)
|
|
1636
|
+
);
|
|
1637
|
+
results.push({
|
|
1638
|
+
path: note.path,
|
|
1639
|
+
title: note.title,
|
|
1640
|
+
backlink_count: backlinkCount,
|
|
1641
|
+
days_since_modified: daysSince,
|
|
1642
|
+
modified: note.modified
|
|
1643
|
+
});
|
|
1644
|
+
}
|
|
1645
|
+
}
|
|
1646
|
+
}
|
|
1647
|
+
return results.sort((a, b) => {
|
|
1648
|
+
if (b.backlink_count !== a.backlink_count) {
|
|
1649
|
+
return b.backlink_count - a.backlink_count;
|
|
1650
|
+
}
|
|
1651
|
+
return b.days_since_modified - a.days_since_modified;
|
|
1652
|
+
});
|
|
1653
|
+
}
|
|
1654
|
+
function getContemporaneousNotes(index, path6, hours = 24) {
|
|
1655
|
+
const targetNote = index.notes.get(path6);
|
|
1656
|
+
if (!targetNote) {
|
|
1657
|
+
return [];
|
|
1658
|
+
}
|
|
1659
|
+
const targetTime = targetNote.modified.getTime();
|
|
1660
|
+
const windowMs = hours * 60 * 60 * 1e3;
|
|
1661
|
+
const results = [];
|
|
1662
|
+
for (const note of index.notes.values()) {
|
|
1663
|
+
if (note.path === path6) continue;
|
|
1664
|
+
const timeDiff = Math.abs(note.modified.getTime() - targetTime);
|
|
1665
|
+
if (timeDiff <= windowMs) {
|
|
1666
|
+
results.push({
|
|
1667
|
+
path: note.path,
|
|
1668
|
+
title: note.title,
|
|
1669
|
+
modified: note.modified,
|
|
1670
|
+
time_diff_hours: Math.round(timeDiff / (1e3 * 60 * 60) * 10) / 10
|
|
1671
|
+
});
|
|
1672
|
+
}
|
|
1673
|
+
}
|
|
1674
|
+
return results.sort((a, b) => a.time_diff_hours - b.time_diff_hours);
|
|
1675
|
+
}
|
|
1676
|
+
function getActivitySummary(index, days) {
|
|
1677
|
+
const cutoff = /* @__PURE__ */ new Date();
|
|
1678
|
+
cutoff.setDate(cutoff.getDate() - days);
|
|
1679
|
+
cutoff.setHours(0, 0, 0, 0);
|
|
1680
|
+
const dailyCounts = {};
|
|
1681
|
+
let notesModified = 0;
|
|
1682
|
+
let notesCreated = 0;
|
|
1683
|
+
for (const note of index.notes.values()) {
|
|
1684
|
+
if (note.modified >= cutoff) {
|
|
1685
|
+
notesModified++;
|
|
1686
|
+
const day = note.modified.toISOString().split("T")[0];
|
|
1687
|
+
dailyCounts[day] = (dailyCounts[day] || 0) + 1;
|
|
1688
|
+
}
|
|
1689
|
+
if (note.created && note.created >= cutoff) {
|
|
1690
|
+
notesCreated++;
|
|
1691
|
+
}
|
|
1692
|
+
}
|
|
1693
|
+
let mostActiveDay = null;
|
|
1694
|
+
let maxCount = 0;
|
|
1695
|
+
for (const [day, count] of Object.entries(dailyCounts)) {
|
|
1696
|
+
if (count > maxCount) {
|
|
1697
|
+
maxCount = count;
|
|
1698
|
+
mostActiveDay = day;
|
|
1699
|
+
}
|
|
1700
|
+
}
|
|
1701
|
+
return {
|
|
1702
|
+
period_days: days,
|
|
1703
|
+
notes_modified: notesModified,
|
|
1704
|
+
notes_created: notesCreated,
|
|
1705
|
+
most_active_day: mostActiveDay,
|
|
1706
|
+
daily_counts: dailyCounts
|
|
1707
|
+
};
|
|
1708
|
+
}
|
|
1709
|
+
|
|
1710
|
+
// src/tools/structure.ts
|
|
1711
|
+
import * as fs5 from "fs";
|
|
1712
|
+
import * as path4 from "path";
|
|
1713
|
+
var HEADING_REGEX = /^(#{1,6})\s+(.+)$/;
|
|
1714
|
+
function extractHeadings(content) {
|
|
1715
|
+
const lines = content.split("\n");
|
|
1716
|
+
const headings = [];
|
|
1717
|
+
let inCodeBlock = false;
|
|
1718
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1719
|
+
const line = lines[i];
|
|
1720
|
+
if (line.startsWith("```")) {
|
|
1721
|
+
inCodeBlock = !inCodeBlock;
|
|
1722
|
+
continue;
|
|
1723
|
+
}
|
|
1724
|
+
if (inCodeBlock) continue;
|
|
1725
|
+
const match = line.match(HEADING_REGEX);
|
|
1726
|
+
if (match) {
|
|
1727
|
+
headings.push({
|
|
1728
|
+
level: match[1].length,
|
|
1729
|
+
text: match[2].trim(),
|
|
1730
|
+
line: i + 1
|
|
1731
|
+
// 1-indexed
|
|
1732
|
+
});
|
|
1733
|
+
}
|
|
1734
|
+
}
|
|
1735
|
+
return headings;
|
|
1736
|
+
}
|
|
1737
|
+
function buildSections(headings, totalLines) {
|
|
1738
|
+
if (headings.length === 0) return [];
|
|
1739
|
+
const sections = [];
|
|
1740
|
+
const stack = [];
|
|
1741
|
+
for (let i = 0; i < headings.length; i++) {
|
|
1742
|
+
const heading = headings[i];
|
|
1743
|
+
const nextHeading = headings[i + 1];
|
|
1744
|
+
const lineEnd = nextHeading ? nextHeading.line - 1 : totalLines;
|
|
1745
|
+
const section = {
|
|
1746
|
+
heading,
|
|
1747
|
+
line_start: heading.line,
|
|
1748
|
+
line_end: lineEnd,
|
|
1749
|
+
subsections: []
|
|
1750
|
+
};
|
|
1751
|
+
while (stack.length > 0 && stack[stack.length - 1].heading.level >= heading.level) {
|
|
1752
|
+
stack.pop();
|
|
1753
|
+
}
|
|
1754
|
+
if (stack.length === 0) {
|
|
1755
|
+
sections.push(section);
|
|
1756
|
+
} else {
|
|
1757
|
+
stack[stack.length - 1].subsections.push(section);
|
|
1758
|
+
}
|
|
1759
|
+
stack.push(section);
|
|
1760
|
+
}
|
|
1761
|
+
return sections;
|
|
1762
|
+
}
|
|
1763
|
+
async function getNoteStructure(index, notePath, vaultPath2) {
|
|
1764
|
+
const note = index.notes.get(notePath);
|
|
1765
|
+
if (!note) return null;
|
|
1766
|
+
const absolutePath = path4.join(vaultPath2, notePath);
|
|
1767
|
+
let content;
|
|
1768
|
+
try {
|
|
1769
|
+
content = await fs5.promises.readFile(absolutePath, "utf-8");
|
|
1770
|
+
} catch {
|
|
1771
|
+
return null;
|
|
1772
|
+
}
|
|
1773
|
+
const lines = content.split("\n");
|
|
1774
|
+
const headings = extractHeadings(content);
|
|
1775
|
+
const sections = buildSections(headings, lines.length);
|
|
1776
|
+
const contentWithoutCode = content.replace(/```[\s\S]*?```/g, "");
|
|
1777
|
+
const words = contentWithoutCode.split(/\s+/).filter((w) => w.length > 0);
|
|
1778
|
+
return {
|
|
1779
|
+
path: notePath,
|
|
1780
|
+
headings,
|
|
1781
|
+
sections,
|
|
1782
|
+
word_count: words.length,
|
|
1783
|
+
line_count: lines.length
|
|
1784
|
+
};
|
|
1785
|
+
}
|
|
1786
|
+
async function getHeadings(index, notePath, vaultPath2) {
|
|
1787
|
+
const note = index.notes.get(notePath);
|
|
1788
|
+
if (!note) return null;
|
|
1789
|
+
const absolutePath = path4.join(vaultPath2, notePath);
|
|
1790
|
+
let content;
|
|
1791
|
+
try {
|
|
1792
|
+
content = await fs5.promises.readFile(absolutePath, "utf-8");
|
|
1793
|
+
} catch {
|
|
1794
|
+
return null;
|
|
1795
|
+
}
|
|
1796
|
+
return extractHeadings(content);
|
|
1797
|
+
}
|
|
1798
|
+
async function getSectionContent(index, notePath, headingText, vaultPath2, includeSubheadings = true) {
|
|
1799
|
+
const note = index.notes.get(notePath);
|
|
1800
|
+
if (!note) return null;
|
|
1801
|
+
const absolutePath = path4.join(vaultPath2, notePath);
|
|
1802
|
+
let content;
|
|
1803
|
+
try {
|
|
1804
|
+
content = await fs5.promises.readFile(absolutePath, "utf-8");
|
|
1805
|
+
} catch {
|
|
1806
|
+
return null;
|
|
1807
|
+
}
|
|
1808
|
+
const lines = content.split("\n");
|
|
1809
|
+
const headings = extractHeadings(content);
|
|
1810
|
+
const targetHeading = headings.find(
|
|
1811
|
+
(h) => h.text.toLowerCase() === headingText.toLowerCase()
|
|
1812
|
+
);
|
|
1813
|
+
if (!targetHeading) return null;
|
|
1814
|
+
let lineEnd = lines.length;
|
|
1815
|
+
for (const h of headings) {
|
|
1816
|
+
if (h.line > targetHeading.line) {
|
|
1817
|
+
if (includeSubheadings) {
|
|
1818
|
+
if (h.level <= targetHeading.level) {
|
|
1819
|
+
lineEnd = h.line - 1;
|
|
1820
|
+
break;
|
|
1821
|
+
}
|
|
1822
|
+
} else {
|
|
1823
|
+
lineEnd = h.line - 1;
|
|
1824
|
+
break;
|
|
1825
|
+
}
|
|
1826
|
+
}
|
|
1827
|
+
}
|
|
1828
|
+
const sectionLines = lines.slice(targetHeading.line, lineEnd);
|
|
1829
|
+
const sectionContent = sectionLines.join("\n").trim();
|
|
1830
|
+
return {
|
|
1831
|
+
heading: targetHeading.text,
|
|
1832
|
+
level: targetHeading.level,
|
|
1833
|
+
content: sectionContent,
|
|
1834
|
+
line_start: targetHeading.line,
|
|
1835
|
+
line_end: lineEnd
|
|
1836
|
+
};
|
|
1837
|
+
}
|
|
1838
|
+
async function findSections(index, headingPattern, vaultPath2, folder) {
|
|
1839
|
+
const regex = new RegExp(headingPattern, "i");
|
|
1840
|
+
const results = [];
|
|
1841
|
+
for (const note of index.notes.values()) {
|
|
1842
|
+
if (folder && !note.path.startsWith(folder)) continue;
|
|
1843
|
+
const absolutePath = path4.join(vaultPath2, note.path);
|
|
1844
|
+
let content;
|
|
1845
|
+
try {
|
|
1846
|
+
content = await fs5.promises.readFile(absolutePath, "utf-8");
|
|
1847
|
+
} catch {
|
|
1848
|
+
continue;
|
|
1849
|
+
}
|
|
1850
|
+
const headings = extractHeadings(content);
|
|
1851
|
+
for (const heading of headings) {
|
|
1852
|
+
if (regex.test(heading.text)) {
|
|
1853
|
+
results.push({
|
|
1854
|
+
path: note.path,
|
|
1855
|
+
heading: heading.text,
|
|
1856
|
+
level: heading.level,
|
|
1857
|
+
line: heading.line
|
|
1858
|
+
});
|
|
1859
|
+
}
|
|
1860
|
+
}
|
|
1861
|
+
}
|
|
1862
|
+
return results;
|
|
1863
|
+
}
|
|
1864
|
+
|
|
1865
|
+
// src/tools/tasks.ts
|
|
1866
|
+
import * as fs6 from "fs";
|
|
1867
|
+
import * as path5 from "path";
|
|
1868
|
+
var TASK_REGEX = /^(\s*)- \[([ xX\-])\]\s+(.+)$/;
|
|
1869
|
+
var TAG_REGEX2 = /#([a-zA-Z][a-zA-Z0-9_/-]*)/g;
|
|
1870
|
+
var DATE_REGEX = /\b(\d{4}-\d{2}-\d{2}|\d{1,2}\/\d{1,2}\/\d{2,4})\b/;
|
|
1871
|
+
var HEADING_REGEX2 = /^(#{1,6})\s+(.+)$/;
|
|
1872
|
+
function parseStatus(char) {
|
|
1873
|
+
if (char === " ") return "open";
|
|
1874
|
+
if (char === "-") return "cancelled";
|
|
1875
|
+
return "completed";
|
|
1876
|
+
}
|
|
1877
|
+
function extractTags2(text) {
|
|
1878
|
+
const tags = [];
|
|
1879
|
+
let match;
|
|
1880
|
+
TAG_REGEX2.lastIndex = 0;
|
|
1881
|
+
while ((match = TAG_REGEX2.exec(text)) !== null) {
|
|
1882
|
+
tags.push(match[1]);
|
|
1883
|
+
}
|
|
1884
|
+
return tags;
|
|
1885
|
+
}
|
|
1886
|
+
function extractDueDate(text) {
|
|
1887
|
+
const match = text.match(DATE_REGEX);
|
|
1888
|
+
return match ? match[1] : void 0;
|
|
1889
|
+
}
|
|
1890
|
+
async function extractTasksFromNote(notePath, absolutePath) {
|
|
1891
|
+
let content;
|
|
1892
|
+
try {
|
|
1893
|
+
content = await fs6.promises.readFile(absolutePath, "utf-8");
|
|
1894
|
+
} catch {
|
|
1895
|
+
return [];
|
|
1896
|
+
}
|
|
1897
|
+
const lines = content.split("\n");
|
|
1898
|
+
const tasks = [];
|
|
1899
|
+
let currentHeading;
|
|
1900
|
+
let inCodeBlock = false;
|
|
1901
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1902
|
+
const line = lines[i];
|
|
1903
|
+
if (line.startsWith("```")) {
|
|
1904
|
+
inCodeBlock = !inCodeBlock;
|
|
1905
|
+
continue;
|
|
1906
|
+
}
|
|
1907
|
+
if (inCodeBlock) continue;
|
|
1908
|
+
const headingMatch = line.match(HEADING_REGEX2);
|
|
1909
|
+
if (headingMatch) {
|
|
1910
|
+
currentHeading = headingMatch[2].trim();
|
|
1911
|
+
continue;
|
|
1912
|
+
}
|
|
1913
|
+
const taskMatch = line.match(TASK_REGEX);
|
|
1914
|
+
if (taskMatch) {
|
|
1915
|
+
const statusChar = taskMatch[2];
|
|
1916
|
+
const text = taskMatch[3].trim();
|
|
1917
|
+
tasks.push({
|
|
1918
|
+
path: notePath,
|
|
1919
|
+
line: i + 1,
|
|
1920
|
+
text,
|
|
1921
|
+
status: parseStatus(statusChar),
|
|
1922
|
+
raw: line,
|
|
1923
|
+
context: currentHeading,
|
|
1924
|
+
tags: extractTags2(text),
|
|
1925
|
+
due_date: extractDueDate(text)
|
|
1926
|
+
});
|
|
1927
|
+
}
|
|
1928
|
+
}
|
|
1929
|
+
return tasks;
|
|
1930
|
+
}
|
|
1931
|
+
async function getAllTasks(index, vaultPath2, options = {}) {
|
|
1932
|
+
const { status = "all", folder, tag, limit } = options;
|
|
1933
|
+
const allTasks = [];
|
|
1934
|
+
for (const note of index.notes.values()) {
|
|
1935
|
+
if (folder && !note.path.startsWith(folder)) continue;
|
|
1936
|
+
const absolutePath = path5.join(vaultPath2, note.path);
|
|
1937
|
+
const tasks = await extractTasksFromNote(note.path, absolutePath);
|
|
1938
|
+
allTasks.push(...tasks);
|
|
1939
|
+
}
|
|
1940
|
+
let filteredTasks = allTasks;
|
|
1941
|
+
if (status !== "all") {
|
|
1942
|
+
filteredTasks = allTasks.filter((t) => t.status === status);
|
|
1943
|
+
}
|
|
1944
|
+
if (tag) {
|
|
1945
|
+
filteredTasks = filteredTasks.filter((t) => t.tags.includes(tag));
|
|
1946
|
+
}
|
|
1947
|
+
const openCount = allTasks.filter((t) => t.status === "open").length;
|
|
1948
|
+
const completedCount = allTasks.filter((t) => t.status === "completed").length;
|
|
1949
|
+
const cancelledCount = allTasks.filter((t) => t.status === "cancelled").length;
|
|
1950
|
+
const returnTasks = limit ? filteredTasks.slice(0, limit) : filteredTasks;
|
|
1951
|
+
return {
|
|
1952
|
+
total: allTasks.length,
|
|
1953
|
+
open_count: openCount,
|
|
1954
|
+
completed_count: completedCount,
|
|
1955
|
+
cancelled_count: cancelledCount,
|
|
1956
|
+
tasks: returnTasks
|
|
1957
|
+
};
|
|
1958
|
+
}
|
|
1959
|
+
async function getTasksFromNote(index, notePath, vaultPath2) {
|
|
1960
|
+
const note = index.notes.get(notePath);
|
|
1961
|
+
if (!note) return null;
|
|
1962
|
+
const absolutePath = path5.join(vaultPath2, notePath);
|
|
1963
|
+
return extractTasksFromNote(notePath, absolutePath);
|
|
1964
|
+
}
|
|
1965
|
+
async function getTasksWithDueDates(index, vaultPath2, options = {}) {
|
|
1966
|
+
const { status = "open", folder } = options;
|
|
1967
|
+
const result = await getAllTasks(index, vaultPath2, { status, folder });
|
|
1968
|
+
return result.tasks.filter((t) => t.due_date).sort((a, b) => {
|
|
1969
|
+
const dateA = a.due_date || "";
|
|
1970
|
+
const dateB = b.due_date || "";
|
|
1971
|
+
return dateA.localeCompare(dateB);
|
|
1972
|
+
});
|
|
1973
|
+
}
|
|
1974
|
+
|
|
1975
|
+
// src/tools/graphAdvanced.ts
|
|
1976
|
+
function getLinkPath(index, fromPath, toPath, maxDepth = 10) {
|
|
1977
|
+
const from = index.notes.has(fromPath) ? fromPath : resolveTarget(index, fromPath);
|
|
1978
|
+
const to = index.notes.has(toPath) ? toPath : resolveTarget(index, toPath);
|
|
1979
|
+
if (!from || !to) {
|
|
1980
|
+
return { exists: false, path: [], length: -1 };
|
|
1981
|
+
}
|
|
1982
|
+
if (from === to) {
|
|
1983
|
+
return { exists: true, path: [from], length: 0 };
|
|
1984
|
+
}
|
|
1985
|
+
const visited = /* @__PURE__ */ new Set();
|
|
1986
|
+
const queue = [{ path: [from], current: from }];
|
|
1987
|
+
while (queue.length > 0) {
|
|
1988
|
+
const { path: currentPath, current } = queue.shift();
|
|
1989
|
+
if (currentPath.length > maxDepth) {
|
|
1990
|
+
continue;
|
|
1991
|
+
}
|
|
1992
|
+
const note = index.notes.get(current);
|
|
1993
|
+
if (!note) continue;
|
|
1994
|
+
for (const link of note.outlinks) {
|
|
1995
|
+
const targetPath = resolveTarget(index, link.target);
|
|
1996
|
+
if (!targetPath) continue;
|
|
1997
|
+
if (targetPath === to) {
|
|
1998
|
+
const fullPath = [...currentPath, targetPath];
|
|
1999
|
+
return {
|
|
2000
|
+
exists: true,
|
|
2001
|
+
path: fullPath,
|
|
2002
|
+
length: fullPath.length - 1
|
|
2003
|
+
};
|
|
2004
|
+
}
|
|
2005
|
+
if (!visited.has(targetPath)) {
|
|
2006
|
+
visited.add(targetPath);
|
|
2007
|
+
queue.push({
|
|
2008
|
+
path: [...currentPath, targetPath],
|
|
2009
|
+
current: targetPath
|
|
2010
|
+
});
|
|
2011
|
+
}
|
|
2012
|
+
}
|
|
2013
|
+
}
|
|
2014
|
+
return { exists: false, path: [], length: -1 };
|
|
2015
|
+
}
|
|
2016
|
+
function getCommonNeighbors(index, noteAPath, noteBPath) {
|
|
2017
|
+
const noteA = index.notes.get(noteAPath);
|
|
2018
|
+
const noteB = index.notes.get(noteBPath);
|
|
2019
|
+
if (!noteA || !noteB) return [];
|
|
2020
|
+
const aTargets = /* @__PURE__ */ new Map();
|
|
2021
|
+
for (const link of noteA.outlinks) {
|
|
2022
|
+
const resolved = resolveTarget(index, link.target);
|
|
2023
|
+
if (resolved) {
|
|
2024
|
+
aTargets.set(resolved, link.line);
|
|
2025
|
+
}
|
|
2026
|
+
}
|
|
2027
|
+
const common = [];
|
|
2028
|
+
for (const link of noteB.outlinks) {
|
|
2029
|
+
const resolved = resolveTarget(index, link.target);
|
|
2030
|
+
if (resolved && aTargets.has(resolved)) {
|
|
2031
|
+
const targetNote = index.notes.get(resolved);
|
|
2032
|
+
if (targetNote) {
|
|
2033
|
+
common.push({
|
|
2034
|
+
path: resolved,
|
|
2035
|
+
title: targetNote.title,
|
|
2036
|
+
linked_from_a_line: aTargets.get(resolved),
|
|
2037
|
+
linked_from_b_line: link.line
|
|
2038
|
+
});
|
|
2039
|
+
}
|
|
2040
|
+
}
|
|
2041
|
+
}
|
|
2042
|
+
return common;
|
|
2043
|
+
}
|
|
2044
|
+
function findBidirectionalLinks(index, notePath) {
|
|
2045
|
+
const results = [];
|
|
2046
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2047
|
+
const notesToCheck = notePath ? [index.notes.get(notePath)].filter(Boolean) : Array.from(index.notes.values());
|
|
2048
|
+
for (const noteA of notesToCheck) {
|
|
2049
|
+
if (!noteA) continue;
|
|
2050
|
+
for (const linkFromA of noteA.outlinks) {
|
|
2051
|
+
const targetPath = resolveTarget(index, linkFromA.target);
|
|
2052
|
+
if (!targetPath) continue;
|
|
2053
|
+
const noteB = index.notes.get(targetPath);
|
|
2054
|
+
if (!noteB) continue;
|
|
2055
|
+
for (const linkFromB of noteB.outlinks) {
|
|
2056
|
+
const backTarget = resolveTarget(index, linkFromB.target);
|
|
2057
|
+
if (backTarget === noteA.path) {
|
|
2058
|
+
const pairKey = [noteA.path, noteB.path].sort().join("|");
|
|
2059
|
+
if (!seen.has(pairKey)) {
|
|
2060
|
+
seen.add(pairKey);
|
|
2061
|
+
results.push({
|
|
2062
|
+
noteA: noteA.path,
|
|
2063
|
+
noteB: noteB.path,
|
|
2064
|
+
a_to_b_line: linkFromA.line,
|
|
2065
|
+
b_to_a_line: linkFromB.line
|
|
2066
|
+
});
|
|
2067
|
+
}
|
|
2068
|
+
}
|
|
2069
|
+
}
|
|
2070
|
+
}
|
|
2071
|
+
}
|
|
2072
|
+
return results;
|
|
2073
|
+
}
|
|
2074
|
+
function findDeadEnds(index, folder, minBacklinks = 1) {
|
|
2075
|
+
const results = [];
|
|
2076
|
+
for (const note of index.notes.values()) {
|
|
2077
|
+
if (folder && !note.path.startsWith(folder)) continue;
|
|
2078
|
+
if (note.outlinks.length === 0) {
|
|
2079
|
+
const backlinkCount = getBacklinksForNote(index, note.path).length;
|
|
2080
|
+
if (backlinkCount >= minBacklinks) {
|
|
2081
|
+
results.push({
|
|
2082
|
+
path: note.path,
|
|
2083
|
+
title: note.title,
|
|
2084
|
+
backlink_count: backlinkCount
|
|
2085
|
+
});
|
|
2086
|
+
}
|
|
2087
|
+
}
|
|
2088
|
+
}
|
|
2089
|
+
return results.sort((a, b) => b.backlink_count - a.backlink_count);
|
|
2090
|
+
}
|
|
2091
|
+
function findSources(index, folder, minOutlinks = 1) {
|
|
2092
|
+
const results = [];
|
|
2093
|
+
for (const note of index.notes.values()) {
|
|
2094
|
+
if (folder && !note.path.startsWith(folder)) continue;
|
|
2095
|
+
const backlinkCount = getBacklinksForNote(index, note.path).length;
|
|
2096
|
+
if (note.outlinks.length >= minOutlinks && backlinkCount === 0) {
|
|
2097
|
+
results.push({
|
|
2098
|
+
path: note.path,
|
|
2099
|
+
title: note.title,
|
|
2100
|
+
outlink_count: note.outlinks.length
|
|
2101
|
+
});
|
|
2102
|
+
}
|
|
2103
|
+
}
|
|
2104
|
+
return results.sort((a, b) => b.outlink_count - a.outlink_count);
|
|
2105
|
+
}
|
|
2106
|
+
function getConnectionStrength(index, noteAPath, noteBPath) {
|
|
2107
|
+
const noteA = index.notes.get(noteAPath);
|
|
2108
|
+
const noteB = index.notes.get(noteBPath);
|
|
2109
|
+
if (!noteA || !noteB) {
|
|
2110
|
+
return {
|
|
2111
|
+
score: 0,
|
|
2112
|
+
factors: {
|
|
2113
|
+
mutual_link: false,
|
|
2114
|
+
shared_tags: [],
|
|
2115
|
+
shared_outlinks: 0,
|
|
2116
|
+
same_folder: false
|
|
2117
|
+
}
|
|
2118
|
+
};
|
|
2119
|
+
}
|
|
2120
|
+
let score = 0;
|
|
2121
|
+
const factors = {
|
|
2122
|
+
mutual_link: false,
|
|
2123
|
+
shared_tags: [],
|
|
2124
|
+
shared_outlinks: 0,
|
|
2125
|
+
same_folder: false
|
|
2126
|
+
};
|
|
2127
|
+
const aLinksToB = noteA.outlinks.some((l) => {
|
|
2128
|
+
const resolved = resolveTarget(index, l.target);
|
|
2129
|
+
return resolved === noteBPath;
|
|
2130
|
+
});
|
|
2131
|
+
const bLinksToA = noteB.outlinks.some((l) => {
|
|
2132
|
+
const resolved = resolveTarget(index, l.target);
|
|
2133
|
+
return resolved === noteAPath;
|
|
2134
|
+
});
|
|
2135
|
+
if (aLinksToB && bLinksToA) {
|
|
2136
|
+
factors.mutual_link = true;
|
|
2137
|
+
score += 3;
|
|
2138
|
+
} else if (aLinksToB || bLinksToA) {
|
|
2139
|
+
score += 1;
|
|
2140
|
+
}
|
|
2141
|
+
const tagsA = new Set(noteA.tags);
|
|
2142
|
+
for (const tag of noteB.tags) {
|
|
2143
|
+
if (tagsA.has(tag)) {
|
|
2144
|
+
factors.shared_tags.push(tag);
|
|
2145
|
+
score += 1;
|
|
2146
|
+
}
|
|
2147
|
+
}
|
|
2148
|
+
const common = getCommonNeighbors(index, noteAPath, noteBPath);
|
|
2149
|
+
factors.shared_outlinks = common.length;
|
|
2150
|
+
score += common.length * 0.5;
|
|
2151
|
+
const folderA = noteAPath.split("/").slice(0, -1).join("/");
|
|
2152
|
+
const folderB = noteBPath.split("/").slice(0, -1).join("/");
|
|
2153
|
+
if (folderA === folderB && folderA !== "") {
|
|
2154
|
+
factors.same_folder = true;
|
|
2155
|
+
score += 1;
|
|
2156
|
+
}
|
|
2157
|
+
return { score, factors };
|
|
2158
|
+
}
|
|
2159
|
+
|
|
2160
|
+
// src/tools/frontmatter.ts
|
|
2161
|
+
function getValueType(value) {
|
|
2162
|
+
if (value === null) return "null";
|
|
2163
|
+
if (value === void 0) return "undefined";
|
|
2164
|
+
if (Array.isArray(value)) return "array";
|
|
2165
|
+
if (value instanceof Date) return "date";
|
|
2166
|
+
return typeof value;
|
|
2167
|
+
}
|
|
2168
|
+
function getFrontmatterSchema(index) {
|
|
2169
|
+
const fieldMap = /* @__PURE__ */ new Map();
|
|
2170
|
+
let notesWithFrontmatter = 0;
|
|
2171
|
+
for (const note of index.notes.values()) {
|
|
2172
|
+
const fm = note.frontmatter;
|
|
2173
|
+
if (!fm || Object.keys(fm).length === 0) continue;
|
|
2174
|
+
notesWithFrontmatter++;
|
|
2175
|
+
for (const [key, value] of Object.entries(fm)) {
|
|
2176
|
+
if (!fieldMap.has(key)) {
|
|
2177
|
+
fieldMap.set(key, {
|
|
2178
|
+
types: /* @__PURE__ */ new Set(),
|
|
2179
|
+
count: 0,
|
|
2180
|
+
examples: [],
|
|
2181
|
+
notes: []
|
|
2182
|
+
});
|
|
2183
|
+
}
|
|
2184
|
+
const info = fieldMap.get(key);
|
|
2185
|
+
info.count++;
|
|
2186
|
+
info.types.add(getValueType(value));
|
|
2187
|
+
if (info.examples.length < 5) {
|
|
2188
|
+
const valueStr = JSON.stringify(value);
|
|
2189
|
+
const existingStrs = info.examples.map((e) => JSON.stringify(e));
|
|
2190
|
+
if (!existingStrs.includes(valueStr)) {
|
|
2191
|
+
info.examples.push(value);
|
|
2192
|
+
}
|
|
2193
|
+
}
|
|
2194
|
+
if (info.notes.length < 5) {
|
|
2195
|
+
info.notes.push(note.path);
|
|
2196
|
+
}
|
|
2197
|
+
}
|
|
2198
|
+
}
|
|
2199
|
+
const fields = Array.from(fieldMap.entries()).map(([name, info]) => ({
|
|
2200
|
+
name,
|
|
2201
|
+
types: Array.from(info.types),
|
|
2202
|
+
count: info.count,
|
|
2203
|
+
examples: info.examples,
|
|
2204
|
+
notes_sample: info.notes
|
|
2205
|
+
})).sort((a, b) => b.count - a.count);
|
|
2206
|
+
return {
|
|
2207
|
+
total_notes: index.notes.size,
|
|
2208
|
+
notes_with_frontmatter: notesWithFrontmatter,
|
|
2209
|
+
field_count: fields.length,
|
|
2210
|
+
fields
|
|
2211
|
+
};
|
|
2212
|
+
}
|
|
2213
|
+
function getFieldValues(index, fieldName) {
|
|
2214
|
+
const valueMap = /* @__PURE__ */ new Map();
|
|
2215
|
+
let totalWithField = 0;
|
|
2216
|
+
for (const note of index.notes.values()) {
|
|
2217
|
+
const value = note.frontmatter[fieldName];
|
|
2218
|
+
if (value === void 0) continue;
|
|
2219
|
+
totalWithField++;
|
|
2220
|
+
const values = Array.isArray(value) ? value : [value];
|
|
2221
|
+
for (const v of values) {
|
|
2222
|
+
const key = JSON.stringify(v);
|
|
2223
|
+
if (!valueMap.has(key)) {
|
|
2224
|
+
valueMap.set(key, {
|
|
2225
|
+
value: v,
|
|
2226
|
+
count: 0,
|
|
2227
|
+
notes: []
|
|
2228
|
+
});
|
|
2229
|
+
}
|
|
2230
|
+
const info = valueMap.get(key);
|
|
2231
|
+
info.count++;
|
|
2232
|
+
info.notes.push(note.path);
|
|
2233
|
+
}
|
|
2234
|
+
}
|
|
2235
|
+
const valuesList = Array.from(valueMap.values()).sort((a, b) => b.count - a.count);
|
|
2236
|
+
return {
|
|
2237
|
+
field: fieldName,
|
|
2238
|
+
total_notes_with_field: totalWithField,
|
|
2239
|
+
unique_values: valuesList.length,
|
|
2240
|
+
values: valuesList
|
|
2241
|
+
};
|
|
2242
|
+
}
|
|
2243
|
+
function findFrontmatterInconsistencies(index) {
|
|
2244
|
+
const schema = getFrontmatterSchema(index);
|
|
2245
|
+
const inconsistencies = [];
|
|
2246
|
+
for (const field of schema.fields) {
|
|
2247
|
+
if (field.types.length > 1) {
|
|
2248
|
+
const examples = [];
|
|
2249
|
+
for (const note of index.notes.values()) {
|
|
2250
|
+
const value = note.frontmatter[field.name];
|
|
2251
|
+
if (value === void 0) continue;
|
|
2252
|
+
const type = getValueType(value);
|
|
2253
|
+
if (!examples.some((e) => e.type === type)) {
|
|
2254
|
+
examples.push({
|
|
2255
|
+
type,
|
|
2256
|
+
value,
|
|
2257
|
+
note: note.path
|
|
2258
|
+
});
|
|
2259
|
+
}
|
|
2260
|
+
if (examples.length >= field.types.length) break;
|
|
2261
|
+
}
|
|
2262
|
+
inconsistencies.push({
|
|
2263
|
+
field: field.name,
|
|
2264
|
+
types_found: field.types,
|
|
2265
|
+
examples
|
|
2266
|
+
});
|
|
2267
|
+
}
|
|
2268
|
+
}
|
|
2269
|
+
return inconsistencies;
|
|
2270
|
+
}
|
|
2271
|
+
|
|
2272
|
+
// src/tools/primitives.ts
|
|
2273
|
+
function registerPrimitiveTools(server2, getIndex, getVaultPath) {
|
|
2274
|
+
server2.registerTool(
|
|
2275
|
+
"get_notes_modified_on",
|
|
2276
|
+
{
|
|
2277
|
+
title: "Get Notes Modified On Date",
|
|
2278
|
+
description: "Get all notes that were modified on a specific date.",
|
|
2279
|
+
inputSchema: {
|
|
2280
|
+
date: z6.string().describe("Date in YYYY-MM-DD format")
|
|
2281
|
+
}
|
|
2282
|
+
},
|
|
2283
|
+
async ({ date }) => {
|
|
2284
|
+
const index = getIndex();
|
|
2285
|
+
const result = getNotesModifiedOn(index, date);
|
|
2286
|
+
return {
|
|
2287
|
+
content: [{ type: "text", text: JSON.stringify({
|
|
2288
|
+
date,
|
|
2289
|
+
count: result.length,
|
|
2290
|
+
notes: result.map((n) => ({
|
|
2291
|
+
...n,
|
|
2292
|
+
created: n.created?.toISOString(),
|
|
2293
|
+
modified: n.modified.toISOString()
|
|
2294
|
+
}))
|
|
2295
|
+
}, null, 2) }]
|
|
2296
|
+
};
|
|
2297
|
+
}
|
|
2298
|
+
);
|
|
2299
|
+
server2.registerTool(
|
|
2300
|
+
"get_notes_in_range",
|
|
2301
|
+
{
|
|
2302
|
+
title: "Get Notes In Date Range",
|
|
2303
|
+
description: "Get all notes modified within a date range.",
|
|
2304
|
+
inputSchema: {
|
|
2305
|
+
start_date: z6.string().describe("Start date in YYYY-MM-DD format"),
|
|
2306
|
+
end_date: z6.string().describe("End date in YYYY-MM-DD format")
|
|
2307
|
+
}
|
|
2308
|
+
},
|
|
2309
|
+
async ({ start_date, end_date }) => {
|
|
2310
|
+
const index = getIndex();
|
|
2311
|
+
const result = getNotesInRange(index, start_date, end_date);
|
|
2312
|
+
return {
|
|
2313
|
+
content: [{ type: "text", text: JSON.stringify({
|
|
2314
|
+
start_date,
|
|
2315
|
+
end_date,
|
|
2316
|
+
count: result.length,
|
|
2317
|
+
notes: result.map((n) => ({
|
|
2318
|
+
...n,
|
|
2319
|
+
created: n.created?.toISOString(),
|
|
2320
|
+
modified: n.modified.toISOString()
|
|
2321
|
+
}))
|
|
2322
|
+
}, null, 2) }]
|
|
2323
|
+
};
|
|
2324
|
+
}
|
|
2325
|
+
);
|
|
2326
|
+
server2.registerTool(
|
|
2327
|
+
"get_stale_notes",
|
|
2328
|
+
{
|
|
2329
|
+
title: "Get Stale Notes",
|
|
2330
|
+
description: "Find important notes (by backlink count) that have not been modified recently.",
|
|
2331
|
+
inputSchema: {
|
|
2332
|
+
days: z6.number().describe("Notes not modified in this many days"),
|
|
2333
|
+
min_backlinks: z6.number().default(1).describe("Minimum backlinks to be considered important"),
|
|
2334
|
+
limit: z6.number().default(50).describe("Maximum results to return")
|
|
2335
|
+
}
|
|
2336
|
+
},
|
|
2337
|
+
async ({ days, min_backlinks, limit }) => {
|
|
2338
|
+
const index = getIndex();
|
|
2339
|
+
const result = getStaleNotes(index, days, min_backlinks).slice(0, limit);
|
|
2340
|
+
return {
|
|
2341
|
+
content: [{ type: "text", text: JSON.stringify({
|
|
2342
|
+
criteria: { days, min_backlinks },
|
|
2343
|
+
count: result.length,
|
|
2344
|
+
notes: result.map((n) => ({
|
|
2345
|
+
...n,
|
|
2346
|
+
modified: n.modified.toISOString()
|
|
2347
|
+
}))
|
|
2348
|
+
}, null, 2) }]
|
|
2349
|
+
};
|
|
2350
|
+
}
|
|
2351
|
+
);
|
|
2352
|
+
server2.registerTool(
|
|
2353
|
+
"get_contemporaneous_notes",
|
|
2354
|
+
{
|
|
2355
|
+
title: "Get Contemporaneous Notes",
|
|
2356
|
+
description: "Find notes that were edited around the same time as a given note.",
|
|
2357
|
+
inputSchema: {
|
|
2358
|
+
path: z6.string().describe("Path to the reference note"),
|
|
2359
|
+
hours: z6.number().default(24).describe("Time window in hours")
|
|
2360
|
+
}
|
|
2361
|
+
},
|
|
2362
|
+
async ({ path: path6, hours }) => {
|
|
2363
|
+
const index = getIndex();
|
|
2364
|
+
const result = getContemporaneousNotes(index, path6, hours);
|
|
2365
|
+
return {
|
|
2366
|
+
content: [{ type: "text", text: JSON.stringify({
|
|
2367
|
+
reference_note: path6,
|
|
2368
|
+
window_hours: hours,
|
|
2369
|
+
count: result.length,
|
|
2370
|
+
notes: result.map((n) => ({
|
|
2371
|
+
...n,
|
|
2372
|
+
modified: n.modified.toISOString()
|
|
2373
|
+
}))
|
|
2374
|
+
}, null, 2) }]
|
|
2375
|
+
};
|
|
2376
|
+
}
|
|
2377
|
+
);
|
|
2378
|
+
server2.registerTool(
|
|
2379
|
+
"get_activity_summary",
|
|
2380
|
+
{
|
|
2381
|
+
title: "Get Activity Summary",
|
|
2382
|
+
description: "Get a summary of vault activity over a period.",
|
|
2383
|
+
inputSchema: {
|
|
2384
|
+
days: z6.number().default(7).describe("Number of days to analyze")
|
|
2385
|
+
}
|
|
2386
|
+
},
|
|
2387
|
+
async ({ days }) => {
|
|
2388
|
+
const index = getIndex();
|
|
2389
|
+
const result = getActivitySummary(index, days);
|
|
2390
|
+
return {
|
|
2391
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
2392
|
+
};
|
|
2393
|
+
}
|
|
2394
|
+
);
|
|
2395
|
+
server2.registerTool(
|
|
2396
|
+
"get_note_structure",
|
|
2397
|
+
{
|
|
2398
|
+
title: "Get Note Structure",
|
|
2399
|
+
description: "Get the heading structure and sections of a note.",
|
|
2400
|
+
inputSchema: {
|
|
2401
|
+
path: z6.string().describe("Path to the note")
|
|
2402
|
+
}
|
|
2403
|
+
},
|
|
2404
|
+
async ({ path: path6 }) => {
|
|
2405
|
+
const index = getIndex();
|
|
2406
|
+
const vaultPath2 = getVaultPath();
|
|
2407
|
+
const result = await getNoteStructure(index, path6, vaultPath2);
|
|
2408
|
+
if (!result) {
|
|
2409
|
+
return {
|
|
2410
|
+
content: [{ type: "text", text: JSON.stringify({ error: "Note not found", path: path6 }, null, 2) }]
|
|
2411
|
+
};
|
|
2412
|
+
}
|
|
2413
|
+
return {
|
|
2414
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
2415
|
+
};
|
|
2416
|
+
}
|
|
2417
|
+
);
|
|
2418
|
+
server2.registerTool(
|
|
2419
|
+
"get_headings",
|
|
2420
|
+
{
|
|
2421
|
+
title: "Get Headings",
|
|
2422
|
+
description: "Get all headings from a note (lightweight).",
|
|
2423
|
+
inputSchema: {
|
|
2424
|
+
path: z6.string().describe("Path to the note")
|
|
2425
|
+
}
|
|
2426
|
+
},
|
|
2427
|
+
async ({ path: path6 }) => {
|
|
2428
|
+
const index = getIndex();
|
|
2429
|
+
const vaultPath2 = getVaultPath();
|
|
2430
|
+
const result = await getHeadings(index, path6, vaultPath2);
|
|
2431
|
+
if (!result) {
|
|
2432
|
+
return {
|
|
2433
|
+
content: [{ type: "text", text: JSON.stringify({ error: "Note not found", path: path6 }, null, 2) }]
|
|
2434
|
+
};
|
|
2435
|
+
}
|
|
2436
|
+
return {
|
|
2437
|
+
content: [{ type: "text", text: JSON.stringify({
|
|
2438
|
+
path: path6,
|
|
2439
|
+
heading_count: result.length,
|
|
2440
|
+
headings: result
|
|
2441
|
+
}, null, 2) }]
|
|
2442
|
+
};
|
|
2443
|
+
}
|
|
2444
|
+
);
|
|
2445
|
+
server2.registerTool(
|
|
2446
|
+
"get_section_content",
|
|
2447
|
+
{
|
|
2448
|
+
title: "Get Section Content",
|
|
2449
|
+
description: "Get the content under a specific heading in a note.",
|
|
2450
|
+
inputSchema: {
|
|
2451
|
+
path: z6.string().describe("Path to the note"),
|
|
2452
|
+
heading: z6.string().describe("Heading text to find"),
|
|
2453
|
+
include_subheadings: z6.boolean().default(true).describe("Include content under subheadings")
|
|
2454
|
+
}
|
|
2455
|
+
},
|
|
2456
|
+
async ({ path: path6, heading, include_subheadings }) => {
|
|
2457
|
+
const index = getIndex();
|
|
2458
|
+
const vaultPath2 = getVaultPath();
|
|
2459
|
+
const result = await getSectionContent(index, path6, heading, vaultPath2, include_subheadings);
|
|
2460
|
+
if (!result) {
|
|
2461
|
+
return {
|
|
2462
|
+
content: [{ type: "text", text: JSON.stringify({
|
|
2463
|
+
error: "Section not found",
|
|
2464
|
+
path: path6,
|
|
2465
|
+
heading
|
|
2466
|
+
}, null, 2) }]
|
|
2467
|
+
};
|
|
2468
|
+
}
|
|
2469
|
+
return {
|
|
2470
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
2471
|
+
};
|
|
2472
|
+
}
|
|
2473
|
+
);
|
|
2474
|
+
server2.registerTool(
|
|
2475
|
+
"find_sections",
|
|
2476
|
+
{
|
|
2477
|
+
title: "Find Sections",
|
|
2478
|
+
description: "Find all sections across vault matching a heading pattern.",
|
|
2479
|
+
inputSchema: {
|
|
2480
|
+
pattern: z6.string().describe("Regex pattern to match heading text"),
|
|
2481
|
+
folder: z6.string().optional().describe("Limit to notes in this folder")
|
|
2482
|
+
}
|
|
2483
|
+
},
|
|
2484
|
+
async ({ pattern, folder }) => {
|
|
2485
|
+
const index = getIndex();
|
|
2486
|
+
const vaultPath2 = getVaultPath();
|
|
2487
|
+
const result = await findSections(index, pattern, vaultPath2, folder);
|
|
2488
|
+
return {
|
|
2489
|
+
content: [{ type: "text", text: JSON.stringify({
|
|
2490
|
+
pattern,
|
|
2491
|
+
folder,
|
|
2492
|
+
count: result.length,
|
|
2493
|
+
sections: result
|
|
2494
|
+
}, null, 2) }]
|
|
2495
|
+
};
|
|
2496
|
+
}
|
|
2497
|
+
);
|
|
2498
|
+
server2.registerTool(
|
|
2499
|
+
"get_all_tasks",
|
|
2500
|
+
{
|
|
2501
|
+
title: "Get All Tasks",
|
|
2502
|
+
description: "Get all tasks from the vault with filtering options.",
|
|
2503
|
+
inputSchema: {
|
|
2504
|
+
status: z6.enum(["open", "completed", "cancelled", "all"]).default("all").describe("Filter by task status"),
|
|
2505
|
+
folder: z6.string().optional().describe("Limit to notes in this folder"),
|
|
2506
|
+
tag: z6.string().optional().describe("Filter to tasks with this tag"),
|
|
2507
|
+
limit: z6.number().default(100).describe("Maximum tasks to return")
|
|
2508
|
+
}
|
|
2509
|
+
},
|
|
2510
|
+
async ({ status, folder, tag, limit }) => {
|
|
2511
|
+
const index = getIndex();
|
|
2512
|
+
const vaultPath2 = getVaultPath();
|
|
2513
|
+
const result = await getAllTasks(index, vaultPath2, { status, folder, tag, limit });
|
|
2514
|
+
return {
|
|
2515
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
2516
|
+
};
|
|
2517
|
+
}
|
|
2518
|
+
);
|
|
2519
|
+
server2.registerTool(
|
|
2520
|
+
"get_tasks_from_note",
|
|
2521
|
+
{
|
|
2522
|
+
title: "Get Tasks From Note",
|
|
2523
|
+
description: "Get all tasks from a specific note.",
|
|
2524
|
+
inputSchema: {
|
|
2525
|
+
path: z6.string().describe("Path to the note")
|
|
2526
|
+
}
|
|
2527
|
+
},
|
|
2528
|
+
async ({ path: path6 }) => {
|
|
2529
|
+
const index = getIndex();
|
|
2530
|
+
const vaultPath2 = getVaultPath();
|
|
2531
|
+
const result = await getTasksFromNote(index, path6, vaultPath2);
|
|
2532
|
+
if (!result) {
|
|
2533
|
+
return {
|
|
2534
|
+
content: [{ type: "text", text: JSON.stringify({ error: "Note not found", path: path6 }, null, 2) }]
|
|
2535
|
+
};
|
|
2536
|
+
}
|
|
2537
|
+
return {
|
|
2538
|
+
content: [{ type: "text", text: JSON.stringify({
|
|
2539
|
+
path: path6,
|
|
2540
|
+
task_count: result.length,
|
|
2541
|
+
open: result.filter((t) => t.status === "open").length,
|
|
2542
|
+
completed: result.filter((t) => t.status === "completed").length,
|
|
2543
|
+
tasks: result
|
|
2544
|
+
}, null, 2) }]
|
|
2545
|
+
};
|
|
2546
|
+
}
|
|
2547
|
+
);
|
|
2548
|
+
server2.registerTool(
|
|
2549
|
+
"get_tasks_with_due_dates",
|
|
2550
|
+
{
|
|
2551
|
+
title: "Get Tasks With Due Dates",
|
|
2552
|
+
description: "Get tasks that have due dates, sorted by date.",
|
|
2553
|
+
inputSchema: {
|
|
2554
|
+
status: z6.enum(["open", "completed", "cancelled", "all"]).default("open").describe("Filter by status"),
|
|
2555
|
+
folder: z6.string().optional().describe("Limit to notes in this folder")
|
|
2556
|
+
}
|
|
2557
|
+
},
|
|
2558
|
+
async ({ status, folder }) => {
|
|
2559
|
+
const index = getIndex();
|
|
2560
|
+
const vaultPath2 = getVaultPath();
|
|
2561
|
+
const result = await getTasksWithDueDates(index, vaultPath2, { status, folder });
|
|
2562
|
+
return {
|
|
2563
|
+
content: [{ type: "text", text: JSON.stringify({
|
|
2564
|
+
count: result.length,
|
|
2565
|
+
tasks: result
|
|
2566
|
+
}, null, 2) }]
|
|
2567
|
+
};
|
|
2568
|
+
}
|
|
2569
|
+
);
|
|
2570
|
+
server2.registerTool(
|
|
2571
|
+
"get_link_path",
|
|
2572
|
+
{
|
|
2573
|
+
title: "Get Link Path",
|
|
2574
|
+
description: "Find the shortest path of links between two notes.",
|
|
2575
|
+
inputSchema: {
|
|
2576
|
+
from: z6.string().describe("Starting note path"),
|
|
2577
|
+
to: z6.string().describe("Target note path"),
|
|
2578
|
+
max_depth: z6.number().default(10).describe("Maximum path length to search")
|
|
2579
|
+
}
|
|
2580
|
+
},
|
|
2581
|
+
async ({ from, to, max_depth }) => {
|
|
2582
|
+
const index = getIndex();
|
|
2583
|
+
const result = getLinkPath(index, from, to, max_depth);
|
|
2584
|
+
return {
|
|
2585
|
+
content: [{ type: "text", text: JSON.stringify({
|
|
2586
|
+
from,
|
|
2587
|
+
to,
|
|
2588
|
+
...result
|
|
2589
|
+
}, null, 2) }]
|
|
2590
|
+
};
|
|
2591
|
+
}
|
|
2592
|
+
);
|
|
2593
|
+
server2.registerTool(
|
|
2594
|
+
"get_common_neighbors",
|
|
2595
|
+
{
|
|
2596
|
+
title: "Get Common Neighbors",
|
|
2597
|
+
description: "Find notes that both specified notes link to.",
|
|
2598
|
+
inputSchema: {
|
|
2599
|
+
note_a: z6.string().describe("First note path"),
|
|
2600
|
+
note_b: z6.string().describe("Second note path")
|
|
2601
|
+
}
|
|
2602
|
+
},
|
|
2603
|
+
async ({ note_a, note_b }) => {
|
|
2604
|
+
const index = getIndex();
|
|
2605
|
+
const result = getCommonNeighbors(index, note_a, note_b);
|
|
2606
|
+
return {
|
|
2607
|
+
content: [{ type: "text", text: JSON.stringify({
|
|
2608
|
+
note_a,
|
|
2609
|
+
note_b,
|
|
2610
|
+
common_count: result.length,
|
|
2611
|
+
common_neighbors: result
|
|
2612
|
+
}, null, 2) }]
|
|
2613
|
+
};
|
|
2614
|
+
}
|
|
2615
|
+
);
|
|
2616
|
+
server2.registerTool(
|
|
2617
|
+
"find_bidirectional_links",
|
|
2618
|
+
{
|
|
2619
|
+
title: "Find Bidirectional Links",
|
|
2620
|
+
description: "Find pairs of notes that link to each other (mutual links).",
|
|
2621
|
+
inputSchema: {
|
|
2622
|
+
path: z6.string().optional().describe("Limit to links involving this note")
|
|
2623
|
+
}
|
|
2624
|
+
},
|
|
2625
|
+
async ({ path: path6 }) => {
|
|
2626
|
+
const index = getIndex();
|
|
2627
|
+
const result = findBidirectionalLinks(index, path6);
|
|
2628
|
+
return {
|
|
2629
|
+
content: [{ type: "text", text: JSON.stringify({
|
|
2630
|
+
scope: path6 || "all",
|
|
2631
|
+
count: result.length,
|
|
2632
|
+
pairs: result
|
|
2633
|
+
}, null, 2) }]
|
|
2634
|
+
};
|
|
2635
|
+
}
|
|
2636
|
+
);
|
|
2637
|
+
server2.registerTool(
|
|
2638
|
+
"find_dead_ends",
|
|
2639
|
+
{
|
|
2640
|
+
title: "Find Dead Ends",
|
|
2641
|
+
description: "Find notes with backlinks but no outgoing links (consume but do not contribute).",
|
|
2642
|
+
inputSchema: {
|
|
2643
|
+
folder: z6.string().optional().describe("Limit to notes in this folder"),
|
|
2644
|
+
min_backlinks: z6.number().default(1).describe("Minimum backlinks required")
|
|
2645
|
+
}
|
|
2646
|
+
},
|
|
2647
|
+
async ({ folder, min_backlinks }) => {
|
|
2648
|
+
const index = getIndex();
|
|
2649
|
+
const result = findDeadEnds(index, folder, min_backlinks);
|
|
2650
|
+
return {
|
|
2651
|
+
content: [{ type: "text", text: JSON.stringify({
|
|
2652
|
+
criteria: { folder, min_backlinks },
|
|
2653
|
+
count: result.length,
|
|
2654
|
+
dead_ends: result
|
|
2655
|
+
}, null, 2) }]
|
|
2656
|
+
};
|
|
2657
|
+
}
|
|
2658
|
+
);
|
|
2659
|
+
server2.registerTool(
|
|
2660
|
+
"find_sources",
|
|
2661
|
+
{
|
|
2662
|
+
title: "Find Sources",
|
|
2663
|
+
description: "Find notes with outgoing links but no backlinks (contribute but not referenced).",
|
|
2664
|
+
inputSchema: {
|
|
2665
|
+
folder: z6.string().optional().describe("Limit to notes in this folder"),
|
|
2666
|
+
min_outlinks: z6.number().default(1).describe("Minimum outlinks required")
|
|
2667
|
+
}
|
|
2668
|
+
},
|
|
2669
|
+
async ({ folder, min_outlinks }) => {
|
|
2670
|
+
const index = getIndex();
|
|
2671
|
+
const result = findSources(index, folder, min_outlinks);
|
|
2672
|
+
return {
|
|
2673
|
+
content: [{ type: "text", text: JSON.stringify({
|
|
2674
|
+
criteria: { folder, min_outlinks },
|
|
2675
|
+
count: result.length,
|
|
2676
|
+
sources: result
|
|
2677
|
+
}, null, 2) }]
|
|
2678
|
+
};
|
|
2679
|
+
}
|
|
2680
|
+
);
|
|
2681
|
+
server2.registerTool(
|
|
2682
|
+
"get_connection_strength",
|
|
2683
|
+
{
|
|
2684
|
+
title: "Get Connection Strength",
|
|
2685
|
+
description: "Calculate the connection strength between two notes based on various factors.",
|
|
2686
|
+
inputSchema: {
|
|
2687
|
+
note_a: z6.string().describe("First note path"),
|
|
2688
|
+
note_b: z6.string().describe("Second note path")
|
|
2689
|
+
}
|
|
2690
|
+
},
|
|
2691
|
+
async ({ note_a, note_b }) => {
|
|
2692
|
+
const index = getIndex();
|
|
2693
|
+
const result = getConnectionStrength(index, note_a, note_b);
|
|
2694
|
+
return {
|
|
2695
|
+
content: [{ type: "text", text: JSON.stringify({
|
|
2696
|
+
note_a,
|
|
2697
|
+
note_b,
|
|
2698
|
+
...result
|
|
2699
|
+
}, null, 2) }]
|
|
2700
|
+
};
|
|
2701
|
+
}
|
|
2702
|
+
);
|
|
2703
|
+
server2.registerTool(
|
|
2704
|
+
"get_frontmatter_schema",
|
|
2705
|
+
{
|
|
2706
|
+
title: "Get Frontmatter Schema",
|
|
2707
|
+
description: "Analyze all frontmatter fields used across the vault.",
|
|
2708
|
+
inputSchema: {}
|
|
2709
|
+
},
|
|
2710
|
+
async () => {
|
|
2711
|
+
const index = getIndex();
|
|
2712
|
+
const result = getFrontmatterSchema(index);
|
|
2713
|
+
return {
|
|
2714
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
2715
|
+
};
|
|
2716
|
+
}
|
|
2717
|
+
);
|
|
2718
|
+
server2.registerTool(
|
|
2719
|
+
"get_field_values",
|
|
2720
|
+
{
|
|
2721
|
+
title: "Get Field Values",
|
|
2722
|
+
description: "Get all unique values for a specific frontmatter field.",
|
|
2723
|
+
inputSchema: {
|
|
2724
|
+
field: z6.string().describe("Frontmatter field name")
|
|
2725
|
+
}
|
|
2726
|
+
},
|
|
2727
|
+
async ({ field }) => {
|
|
2728
|
+
const index = getIndex();
|
|
2729
|
+
const result = getFieldValues(index, field);
|
|
2730
|
+
return {
|
|
2731
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
2732
|
+
};
|
|
2733
|
+
}
|
|
2734
|
+
);
|
|
2735
|
+
server2.registerTool(
|
|
2736
|
+
"find_frontmatter_inconsistencies",
|
|
2737
|
+
{
|
|
2738
|
+
title: "Find Frontmatter Inconsistencies",
|
|
2739
|
+
description: "Find fields that have multiple different types across notes.",
|
|
2740
|
+
inputSchema: {}
|
|
2741
|
+
},
|
|
2742
|
+
async () => {
|
|
2743
|
+
const index = getIndex();
|
|
2744
|
+
const result = findFrontmatterInconsistencies(index);
|
|
2745
|
+
return {
|
|
2746
|
+
content: [{ type: "text", text: JSON.stringify({
|
|
2747
|
+
inconsistency_count: result.length,
|
|
2748
|
+
inconsistencies: result
|
|
2749
|
+
}, null, 2) }]
|
|
2750
|
+
};
|
|
2751
|
+
}
|
|
2752
|
+
);
|
|
2753
|
+
}
|
|
2754
|
+
|
|
1587
2755
|
// src/index.ts
|
|
1588
2756
|
var VAULT_PATH = process.env.OBSIDIAN_VAULT_PATH;
|
|
1589
2757
|
if (!VAULT_PATH) {
|
|
@@ -1594,7 +2762,7 @@ var vaultPath = VAULT_PATH;
|
|
|
1594
2762
|
var vaultIndex;
|
|
1595
2763
|
var server = new McpServer({
|
|
1596
2764
|
name: "smoking-mirror",
|
|
1597
|
-
version: "1.
|
|
2765
|
+
version: "1.2.0"
|
|
1598
2766
|
});
|
|
1599
2767
|
registerGraphTools(
|
|
1600
2768
|
server,
|
|
@@ -1624,6 +2792,11 @@ registerSystemTools(
|
|
|
1624
2792
|
},
|
|
1625
2793
|
() => vaultPath
|
|
1626
2794
|
);
|
|
2795
|
+
registerPrimitiveTools(
|
|
2796
|
+
server,
|
|
2797
|
+
() => vaultIndex,
|
|
2798
|
+
() => vaultPath
|
|
2799
|
+
);
|
|
1627
2800
|
async function main() {
|
|
1628
2801
|
console.error("Building vault index...");
|
|
1629
2802
|
const startTime = Date.now();
|