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.
- package/README.md +339 -151
- package/dist/index.js +1629 -7
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -212,8 +212,8 @@ var PROGRESS_INTERVAL = 100;
|
|
|
212
212
|
function normalizeTarget(target) {
|
|
213
213
|
return target.toLowerCase().replace(/\.md$/, "");
|
|
214
214
|
}
|
|
215
|
-
function normalizeNotePath(
|
|
216
|
-
return
|
|
215
|
+
function normalizeNotePath(path6) {
|
|
216
|
+
return path6.toLowerCase().replace(/\.md$/, "");
|
|
217
217
|
}
|
|
218
218
|
async function buildVaultIndex(vaultPath2, options = {}) {
|
|
219
219
|
const { timeoutMs = DEFAULT_TIMEOUT_MS, onProgress } = options;
|
|
@@ -717,14 +717,14 @@ function registerWikilinkTools(server2, getIndex, getVaultPath) {
|
|
|
717
717
|
};
|
|
718
718
|
function findSimilarEntity(target, entities) {
|
|
719
719
|
const targetLower = target.toLowerCase();
|
|
720
|
-
for (const [name,
|
|
720
|
+
for (const [name, path6] of entities) {
|
|
721
721
|
if (name.startsWith(targetLower) || targetLower.startsWith(name)) {
|
|
722
|
-
return
|
|
722
|
+
return path6;
|
|
723
723
|
}
|
|
724
724
|
}
|
|
725
|
-
for (const [name,
|
|
725
|
+
for (const [name, path6] of entities) {
|
|
726
726
|
if (name.includes(targetLower) || targetLower.includes(name)) {
|
|
727
|
-
return
|
|
727
|
+
return path6;
|
|
728
728
|
}
|
|
729
729
|
}
|
|
730
730
|
return void 0;
|
|
@@ -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.
|
|
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();
|