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.
Files changed (3) hide show
  1. package/README.md +344 -204
  2. package/dist/index.js +1180 -7
  3. 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(path4) {
216
- return path4.toLowerCase().replace(/\.md$/, "");
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, path4] of entities) {
720
+ for (const [name, path6] of entities) {
721
721
  if (name.startsWith(targetLower) || targetLower.startsWith(name)) {
722
- return path4;
722
+ return path6;
723
723
  }
724
724
  }
725
- for (const [name, path4] of entities) {
725
+ for (const [name, path6] of entities) {
726
726
  if (name.includes(targetLower) || targetLower.includes(name)) {
727
- return path4;
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.1.0"
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();