smoking-mirror 1.0.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 +339 -151
  2. package/dist/index.js +1629 -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(path3) {
216
- return path3.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, path3] of entities) {
720
+ for (const [name, path6] of entities) {
721
721
  if (name.startsWith(targetLower) || targetLower.startsWith(name)) {
722
- return path3;
722
+ return path6;
723
723
  }
724
724
  }
725
- for (const [name, path3] of entities) {
725
+ for (const [name, path6] of entities) {
726
726
  if (name.includes(targetLower) || targetLower.includes(name)) {
727
- return path3;
727
+ return path6;
728
728
  }
729
729
  }
730
730
  return void 0;
@@ -1143,6 +1143,1615 @@ function registerQueryTools(server2, getIndex, getVaultPath) {
1143
1143
  );
1144
1144
  }
1145
1145
 
1146
+ // src/tools/system.ts
1147
+ import * as fs4 from "fs";
1148
+ import * as path3 from "path";
1149
+ import { z as z5 } from "zod";
1150
+ function registerSystemTools(server2, getIndex, setIndex, getVaultPath) {
1151
+ const RefreshIndexOutputSchema = {
1152
+ success: z5.boolean().describe("Whether the refresh succeeded"),
1153
+ notes_count: z5.number().describe("Number of notes indexed"),
1154
+ entities_count: z5.number().describe("Number of entities (titles + aliases)"),
1155
+ duration_ms: z5.number().describe("Time taken to rebuild index")
1156
+ };
1157
+ server2.registerTool(
1158
+ "refresh_index",
1159
+ {
1160
+ title: "Refresh Index",
1161
+ description: "Rebuild the vault index without restarting the server. Use after making changes to notes in Obsidian.",
1162
+ inputSchema: {},
1163
+ outputSchema: RefreshIndexOutputSchema
1164
+ },
1165
+ async () => {
1166
+ const vaultPath2 = getVaultPath();
1167
+ const startTime = Date.now();
1168
+ try {
1169
+ const newIndex = await buildVaultIndex(vaultPath2);
1170
+ setIndex(newIndex);
1171
+ const output = {
1172
+ success: true,
1173
+ notes_count: newIndex.notes.size,
1174
+ entities_count: newIndex.entities.size,
1175
+ duration_ms: Date.now() - startTime
1176
+ };
1177
+ return {
1178
+ content: [
1179
+ {
1180
+ type: "text",
1181
+ text: JSON.stringify(output, null, 2)
1182
+ }
1183
+ ],
1184
+ structuredContent: output
1185
+ };
1186
+ } catch (err) {
1187
+ const output = {
1188
+ success: false,
1189
+ notes_count: 0,
1190
+ entities_count: 0,
1191
+ duration_ms: Date.now() - startTime
1192
+ };
1193
+ return {
1194
+ content: [
1195
+ {
1196
+ type: "text",
1197
+ text: `Error refreshing index: ${err instanceof Error ? err.message : String(err)}`
1198
+ }
1199
+ ],
1200
+ structuredContent: output
1201
+ };
1202
+ }
1203
+ }
1204
+ );
1205
+ const GetAllEntitiesOutputSchema = {
1206
+ entity_count: z5.number().describe("Total number of entities"),
1207
+ entities: z5.array(
1208
+ z5.object({
1209
+ name: z5.string().describe("Entity name (title or alias)"),
1210
+ path: z5.string().describe("Path to the note"),
1211
+ is_alias: z5.boolean().describe("Whether this is an alias vs title")
1212
+ })
1213
+ ).describe("List of all entities")
1214
+ };
1215
+ server2.registerTool(
1216
+ "get_all_entities",
1217
+ {
1218
+ title: "Get All Entities",
1219
+ description: "Get all linkable entities in the vault (note titles and aliases). Useful for understanding what can be linked to.",
1220
+ inputSchema: {
1221
+ include_aliases: z5.boolean().default(true).describe("Include aliases in addition to titles"),
1222
+ limit: z5.number().optional().describe("Maximum number of entities to return")
1223
+ },
1224
+ outputSchema: GetAllEntitiesOutputSchema
1225
+ },
1226
+ async ({
1227
+ include_aliases,
1228
+ limit
1229
+ }) => {
1230
+ const index = getIndex();
1231
+ const entities = [];
1232
+ for (const note of index.notes.values()) {
1233
+ entities.push({
1234
+ name: note.title,
1235
+ path: note.path,
1236
+ is_alias: false
1237
+ });
1238
+ if (include_aliases) {
1239
+ for (const alias of note.aliases) {
1240
+ entities.push({
1241
+ name: alias,
1242
+ path: note.path,
1243
+ is_alias: true
1244
+ });
1245
+ }
1246
+ }
1247
+ }
1248
+ entities.sort((a, b) => a.name.localeCompare(b.name));
1249
+ const limitedEntities = limit ? entities.slice(0, limit) : entities;
1250
+ const output = {
1251
+ entity_count: limitedEntities.length,
1252
+ entities: limitedEntities
1253
+ };
1254
+ return {
1255
+ content: [
1256
+ {
1257
+ type: "text",
1258
+ text: JSON.stringify(output, null, 2)
1259
+ }
1260
+ ],
1261
+ structuredContent: output
1262
+ };
1263
+ }
1264
+ );
1265
+ const GetRecentNotesOutputSchema = {
1266
+ count: z5.number().describe("Number of notes returned"),
1267
+ days: z5.number().describe("Number of days looked back"),
1268
+ notes: z5.array(
1269
+ z5.object({
1270
+ path: z5.string().describe("Path to the note"),
1271
+ title: z5.string().describe("Note title"),
1272
+ modified: z5.string().describe("Last modified date (ISO format)"),
1273
+ tags: z5.array(z5.string()).describe("Tags on this note")
1274
+ })
1275
+ ).describe("List of recently modified notes")
1276
+ };
1277
+ server2.registerTool(
1278
+ "get_recent_notes",
1279
+ {
1280
+ title: "Get Recent Notes",
1281
+ description: "Get notes modified within the last N days. Useful for generating reviews and understanding recent activity.",
1282
+ inputSchema: {
1283
+ days: z5.number().default(7).describe("Number of days to look back"),
1284
+ limit: z5.number().default(50).describe("Maximum number of notes to return"),
1285
+ folder: z5.string().optional().describe("Limit to notes in this folder")
1286
+ },
1287
+ outputSchema: GetRecentNotesOutputSchema
1288
+ },
1289
+ async ({
1290
+ days,
1291
+ limit,
1292
+ folder
1293
+ }) => {
1294
+ const index = getIndex();
1295
+ const cutoffDate = /* @__PURE__ */ new Date();
1296
+ cutoffDate.setDate(cutoffDate.getDate() - days);
1297
+ const recentNotes = [];
1298
+ for (const note of index.notes.values()) {
1299
+ if (folder && !note.path.startsWith(folder)) {
1300
+ continue;
1301
+ }
1302
+ if (note.modified >= cutoffDate) {
1303
+ recentNotes.push({
1304
+ path: note.path,
1305
+ title: note.title,
1306
+ modified: note.modified.toISOString(),
1307
+ tags: note.tags,
1308
+ modifiedDate: note.modified
1309
+ });
1310
+ }
1311
+ }
1312
+ recentNotes.sort(
1313
+ (a, b) => b.modifiedDate.getTime() - a.modifiedDate.getTime()
1314
+ );
1315
+ const limitedNotes = recentNotes.slice(0, limit);
1316
+ const output = {
1317
+ count: limitedNotes.length,
1318
+ days,
1319
+ notes: limitedNotes.map((n) => ({
1320
+ path: n.path,
1321
+ title: n.title,
1322
+ modified: n.modified,
1323
+ tags: n.tags
1324
+ }))
1325
+ };
1326
+ return {
1327
+ content: [
1328
+ {
1329
+ type: "text",
1330
+ text: JSON.stringify(output, null, 2)
1331
+ }
1332
+ ],
1333
+ structuredContent: output
1334
+ };
1335
+ }
1336
+ );
1337
+ const GetUnlinkedMentionsOutputSchema = {
1338
+ entity: z5.string().describe("The entity searched for"),
1339
+ resolved_path: z5.string().optional().describe("Path of the note this entity refers to"),
1340
+ mention_count: z5.number().describe("Total unlinked mentions found"),
1341
+ mentions: z5.array(
1342
+ z5.object({
1343
+ path: z5.string().describe("Path of note with unlinked mention"),
1344
+ line: z5.number().describe("Line number of mention"),
1345
+ context: z5.string().describe("Surrounding text")
1346
+ })
1347
+ ).describe("List of unlinked mentions")
1348
+ };
1349
+ server2.registerTool(
1350
+ "get_unlinked_mentions",
1351
+ {
1352
+ title: "Get Unlinked Mentions",
1353
+ description: "Find places where an entity (note title or alias) is mentioned in text but not linked. Useful for finding linking opportunities.",
1354
+ inputSchema: {
1355
+ entity: z5.string().describe('Entity to search for (e.g., "John Smith")'),
1356
+ limit: z5.number().default(50).describe("Maximum number of mentions to return")
1357
+ },
1358
+ outputSchema: GetUnlinkedMentionsOutputSchema
1359
+ },
1360
+ async ({
1361
+ entity,
1362
+ limit
1363
+ }) => {
1364
+ const index = getIndex();
1365
+ const vaultPath2 = getVaultPath();
1366
+ const normalizedEntity = entity.toLowerCase();
1367
+ const resolvedPath = index.entities.get(normalizedEntity);
1368
+ const mentions = [];
1369
+ for (const note of index.notes.values()) {
1370
+ if (resolvedPath && note.path === resolvedPath) {
1371
+ continue;
1372
+ }
1373
+ try {
1374
+ const fullPath = path3.join(vaultPath2, note.path);
1375
+ const content = await fs4.promises.readFile(fullPath, "utf-8");
1376
+ const lines = content.split("\n");
1377
+ for (let i = 0; i < lines.length; i++) {
1378
+ const line = lines[i];
1379
+ const lowerLine = line.toLowerCase();
1380
+ if (!lowerLine.includes(normalizedEntity)) {
1381
+ continue;
1382
+ }
1383
+ const linkPattern = new RegExp(
1384
+ `\\[\\[[^\\]]*${entity.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}[^\\]]*\\]\\]`,
1385
+ "i"
1386
+ );
1387
+ if (linkPattern.test(line)) {
1388
+ continue;
1389
+ }
1390
+ mentions.push({
1391
+ path: note.path,
1392
+ line: i + 1,
1393
+ context: line.trim().slice(0, 200)
1394
+ });
1395
+ if (mentions.length >= limit) {
1396
+ break;
1397
+ }
1398
+ }
1399
+ } catch {
1400
+ }
1401
+ if (mentions.length >= limit) {
1402
+ break;
1403
+ }
1404
+ }
1405
+ const output = {
1406
+ entity,
1407
+ resolved_path: resolvedPath,
1408
+ mention_count: mentions.length,
1409
+ mentions
1410
+ };
1411
+ return {
1412
+ content: [
1413
+ {
1414
+ type: "text",
1415
+ text: JSON.stringify(output, null, 2)
1416
+ }
1417
+ ],
1418
+ structuredContent: output
1419
+ };
1420
+ }
1421
+ );
1422
+ const GetNoteMetadataOutputSchema = {
1423
+ path: z5.string().describe("Path to the note"),
1424
+ title: z5.string().describe("Note title"),
1425
+ exists: z5.boolean().describe("Whether the note exists"),
1426
+ frontmatter: z5.record(z5.unknown()).describe("Frontmatter properties"),
1427
+ tags: z5.array(z5.string()).describe("Tags on this note"),
1428
+ aliases: z5.array(z5.string()).describe("Aliases for this note"),
1429
+ outlink_count: z5.number().describe("Number of outgoing links"),
1430
+ backlink_count: z5.number().describe("Number of incoming links"),
1431
+ word_count: z5.number().optional().describe("Approximate word count"),
1432
+ created: z5.string().optional().describe("Created date (ISO format)"),
1433
+ modified: z5.string().describe("Last modified date (ISO format)")
1434
+ };
1435
+ server2.registerTool(
1436
+ "get_note_metadata",
1437
+ {
1438
+ title: "Get Note Metadata",
1439
+ description: "Get metadata about a note (frontmatter, tags, link counts) without reading full content. Useful for quick analysis.",
1440
+ inputSchema: {
1441
+ path: z5.string().describe("Path to the note"),
1442
+ include_word_count: z5.boolean().default(false).describe("Count words (requires reading file)")
1443
+ },
1444
+ outputSchema: GetNoteMetadataOutputSchema
1445
+ },
1446
+ async ({
1447
+ path: notePath,
1448
+ include_word_count
1449
+ }) => {
1450
+ const index = getIndex();
1451
+ const vaultPath2 = getVaultPath();
1452
+ let resolvedPath = notePath;
1453
+ if (!notePath.endsWith(".md")) {
1454
+ const resolved = index.entities.get(notePath.toLowerCase());
1455
+ if (resolved) {
1456
+ resolvedPath = resolved;
1457
+ } else {
1458
+ resolvedPath = notePath + ".md";
1459
+ }
1460
+ }
1461
+ const note = index.notes.get(resolvedPath);
1462
+ if (!note) {
1463
+ const output2 = {
1464
+ path: resolvedPath,
1465
+ title: resolvedPath.replace(/\.md$/, "").split("/").pop() || "",
1466
+ exists: false,
1467
+ frontmatter: {},
1468
+ tags: [],
1469
+ aliases: [],
1470
+ outlink_count: 0,
1471
+ backlink_count: 0,
1472
+ modified: (/* @__PURE__ */ new Date()).toISOString()
1473
+ };
1474
+ return {
1475
+ content: [
1476
+ {
1477
+ type: "text",
1478
+ text: JSON.stringify(output2, null, 2)
1479
+ }
1480
+ ],
1481
+ structuredContent: output2
1482
+ };
1483
+ }
1484
+ const normalizedPath = resolvedPath.toLowerCase().replace(/\.md$/, "");
1485
+ const backlinks = index.backlinks.get(normalizedPath) || [];
1486
+ let wordCount;
1487
+ if (include_word_count) {
1488
+ try {
1489
+ const fullPath = path3.join(vaultPath2, resolvedPath);
1490
+ const content = await fs4.promises.readFile(fullPath, "utf-8");
1491
+ wordCount = content.split(/\s+/).filter((w) => w.length > 0).length;
1492
+ } catch {
1493
+ }
1494
+ }
1495
+ const output = {
1496
+ path: note.path,
1497
+ title: note.title,
1498
+ exists: true,
1499
+ frontmatter: note.frontmatter,
1500
+ tags: note.tags,
1501
+ aliases: note.aliases,
1502
+ outlink_count: note.outlinks.length,
1503
+ backlink_count: backlinks.length,
1504
+ word_count: wordCount,
1505
+ created: note.created?.toISOString(),
1506
+ modified: note.modified.toISOString()
1507
+ };
1508
+ return {
1509
+ content: [
1510
+ {
1511
+ type: "text",
1512
+ text: JSON.stringify(output, null, 2)
1513
+ }
1514
+ ],
1515
+ structuredContent: output
1516
+ };
1517
+ }
1518
+ );
1519
+ const GetFolderStructureOutputSchema = {
1520
+ folder_count: z5.number().describe("Total number of folders"),
1521
+ folders: z5.array(
1522
+ z5.object({
1523
+ path: z5.string().describe("Folder path"),
1524
+ note_count: z5.number().describe("Number of notes in this folder"),
1525
+ subfolder_count: z5.number().describe("Number of direct subfolders")
1526
+ })
1527
+ ).describe("List of folders with note counts")
1528
+ };
1529
+ server2.registerTool(
1530
+ "get_folder_structure",
1531
+ {
1532
+ title: "Get Folder Structure",
1533
+ description: "Get the folder structure of the vault with note counts. Useful for understanding vault organization.",
1534
+ inputSchema: {},
1535
+ outputSchema: GetFolderStructureOutputSchema
1536
+ },
1537
+ async () => {
1538
+ const index = getIndex();
1539
+ const folderCounts = /* @__PURE__ */ new Map();
1540
+ const subfolders = /* @__PURE__ */ new Map();
1541
+ for (const note of index.notes.values()) {
1542
+ const parts = note.path.split("/");
1543
+ if (parts.length === 1) {
1544
+ folderCounts.set("/", (folderCounts.get("/") || 0) + 1);
1545
+ continue;
1546
+ }
1547
+ const folderPath = parts.slice(0, -1).join("/");
1548
+ folderCounts.set(folderPath, (folderCounts.get(folderPath) || 0) + 1);
1549
+ for (let i = 1; i < parts.length - 1; i++) {
1550
+ const parent = parts.slice(0, i).join("/") || "/";
1551
+ const child = parts.slice(0, i + 1).join("/");
1552
+ if (!subfolders.has(parent)) {
1553
+ subfolders.set(parent, /* @__PURE__ */ new Set());
1554
+ }
1555
+ subfolders.get(parent).add(child);
1556
+ if (!folderCounts.has(parent)) {
1557
+ folderCounts.set(parent, 0);
1558
+ }
1559
+ }
1560
+ }
1561
+ const folders = [];
1562
+ for (const [folderPath, noteCount] of folderCounts) {
1563
+ folders.push({
1564
+ path: folderPath,
1565
+ note_count: noteCount,
1566
+ subfolder_count: subfolders.get(folderPath)?.size || 0
1567
+ });
1568
+ }
1569
+ folders.sort((a, b) => a.path.localeCompare(b.path));
1570
+ const output = {
1571
+ folder_count: folders.length,
1572
+ folders
1573
+ };
1574
+ return {
1575
+ content: [
1576
+ {
1577
+ type: "text",
1578
+ text: JSON.stringify(output, null, 2)
1579
+ }
1580
+ ],
1581
+ structuredContent: output
1582
+ };
1583
+ }
1584
+ );
1585
+ }
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
+
1146
2755
  // src/index.ts
1147
2756
  var VAULT_PATH = process.env.OBSIDIAN_VAULT_PATH;
1148
2757
  if (!VAULT_PATH) {
@@ -1153,7 +2762,7 @@ var vaultPath = VAULT_PATH;
1153
2762
  var vaultIndex;
1154
2763
  var server = new McpServer({
1155
2764
  name: "smoking-mirror",
1156
- version: "1.0.0"
2765
+ version: "1.2.0"
1157
2766
  });
1158
2767
  registerGraphTools(
1159
2768
  server,
@@ -1175,6 +2784,19 @@ registerQueryTools(
1175
2784
  () => vaultIndex,
1176
2785
  () => vaultPath
1177
2786
  );
2787
+ registerSystemTools(
2788
+ server,
2789
+ () => vaultIndex,
2790
+ (newIndex) => {
2791
+ vaultIndex = newIndex;
2792
+ },
2793
+ () => vaultPath
2794
+ );
2795
+ registerPrimitiveTools(
2796
+ server,
2797
+ () => vaultIndex,
2798
+ () => vaultPath
2799
+ );
1178
2800
  async function main() {
1179
2801
  console.error("Building vault index...");
1180
2802
  const startTime = Date.now();