smoking-mirror 1.0.0 → 1.1.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 +48 -0
- package/dist/index.js +456 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -188,6 +188,54 @@ Output: [{ path: "work/Current.md", title: "Current", frontmatter: {...}, tags:
|
|
|
188
188
|
- `order` - "asc" or "desc"
|
|
189
189
|
- `limit` - Maximum results to return
|
|
190
190
|
|
|
191
|
+
### System (v1.1)
|
|
192
|
+
|
|
193
|
+
#### `refresh_index`
|
|
194
|
+
Rebuild the vault index without restarting the server.
|
|
195
|
+
|
|
196
|
+
```
|
|
197
|
+
Output: { success: true, notes_count: 1444, entities_count: 3421, duration_ms: 1823 }
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
#### `get_all_entities`
|
|
201
|
+
Get all linkable entities (note titles and aliases).
|
|
202
|
+
|
|
203
|
+
```
|
|
204
|
+
Input: { include_aliases: true, limit: 100 }
|
|
205
|
+
Output: { entity_count: 100, entities: [{ name: "John Smith", path: "people/John Smith.md", is_alias: false }] }
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
#### `get_recent_notes`
|
|
209
|
+
Get notes modified within the last N days.
|
|
210
|
+
|
|
211
|
+
```
|
|
212
|
+
Input: { days: 7, limit: 50 }
|
|
213
|
+
Output: { count: 23, days: 7, notes: [{ path: "daily/2024-01-15.md", title: "2024-01-15", modified: "...", tags: [...] }] }
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
#### `get_unlinked_mentions`
|
|
217
|
+
Find mentions of an entity that aren't linked (linking opportunities).
|
|
218
|
+
|
|
219
|
+
```
|
|
220
|
+
Input: { entity: "John Smith", limit: 50 }
|
|
221
|
+
Output: { entity: "John Smith", mention_count: 12, mentions: [{ path: "meetings/standup.md", line: 5, context: "Talked to John Smith about..." }] }
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
#### `get_note_metadata`
|
|
225
|
+
Get metadata about a note without reading full content.
|
|
226
|
+
|
|
227
|
+
```
|
|
228
|
+
Input: { path: "projects/API.md", include_word_count: true }
|
|
229
|
+
Output: { path: "...", title: "API", frontmatter: {...}, tags: [...], outlink_count: 15, backlink_count: 8, word_count: 2340 }
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
#### `get_folder_structure`
|
|
233
|
+
Get the folder structure of the vault with note counts.
|
|
234
|
+
|
|
235
|
+
```
|
|
236
|
+
Output: { folder_count: 12, folders: [{ path: "daily", note_count: 365, subfolder_count: 0 }] }
|
|
237
|
+
```
|
|
238
|
+
|
|
191
239
|
## Architecture
|
|
192
240
|
|
|
193
241
|
- **File-first**: Parses markdown directly, no database required
|
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(path4) {
|
|
216
|
+
return path4.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, path4] of entities) {
|
|
721
721
|
if (name.startsWith(targetLower) || targetLower.startsWith(name)) {
|
|
722
|
-
return
|
|
722
|
+
return path4;
|
|
723
723
|
}
|
|
724
724
|
}
|
|
725
|
-
for (const [name,
|
|
725
|
+
for (const [name, path4] of entities) {
|
|
726
726
|
if (name.includes(targetLower) || targetLower.includes(name)) {
|
|
727
|
-
return
|
|
727
|
+
return path4;
|
|
728
728
|
}
|
|
729
729
|
}
|
|
730
730
|
return void 0;
|
|
@@ -1143,6 +1143,447 @@ 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
|
+
|
|
1146
1587
|
// src/index.ts
|
|
1147
1588
|
var VAULT_PATH = process.env.OBSIDIAN_VAULT_PATH;
|
|
1148
1589
|
if (!VAULT_PATH) {
|
|
@@ -1153,7 +1594,7 @@ var vaultPath = VAULT_PATH;
|
|
|
1153
1594
|
var vaultIndex;
|
|
1154
1595
|
var server = new McpServer({
|
|
1155
1596
|
name: "smoking-mirror",
|
|
1156
|
-
version: "1.
|
|
1597
|
+
version: "1.1.0"
|
|
1157
1598
|
});
|
|
1158
1599
|
registerGraphTools(
|
|
1159
1600
|
server,
|
|
@@ -1175,6 +1616,14 @@ registerQueryTools(
|
|
|
1175
1616
|
() => vaultIndex,
|
|
1176
1617
|
() => vaultPath
|
|
1177
1618
|
);
|
|
1619
|
+
registerSystemTools(
|
|
1620
|
+
server,
|
|
1621
|
+
() => vaultIndex,
|
|
1622
|
+
(newIndex) => {
|
|
1623
|
+
vaultIndex = newIndex;
|
|
1624
|
+
},
|
|
1625
|
+
() => vaultPath
|
|
1626
|
+
);
|
|
1178
1627
|
async function main() {
|
|
1179
1628
|
console.error("Building vault index...");
|
|
1180
1629
|
const startTime = Date.now();
|