@hiveforge/hivemind-mcp 2.0.0 → 2.1.1

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 CHANGED
@@ -4,7 +4,13 @@
4
4
 
5
5
  # Hivemind MCP Server
6
6
 
7
- [![npm version](https://img.shields.io/npm/v/@hiveforge/hivemind-mcp.svg)](https://www.npmjs.com/package/@hiveforge/hivemind-mcp)
7
+ [![NPM Version](https://img.shields.io/npm/v/@hiveforge/hivemind-mcp.svg)](https://www.npmjs.com/package/@hiveforge/hivemind-mcp)
8
+ [![Build Status](https://img.shields.io/github/actions/workflow/status/hiveforge-io/hivemind/test.yml?branch=master&label=tests)](https://github.com/hiveforge-io/hivemind/actions/workflows/test.yml)
9
+ [![Release](https://img.shields.io/github/actions/workflow/status/hiveforge-io/hivemind/release.yml?branch=master&label=release)](https://github.com/hiveforge-io/hivemind/actions/workflows/release.yml)
10
+ [![codecov](https://codecov.io/gh/hiveforge-io/hivemind/branch/master/graph/badge.svg)](https://codecov.io/gh/hiveforge-io/hivemind)
11
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
12
+ [![GitHub issues](https://img.shields.io/github/issues/hiveforge-io/hivemind)](https://github.com/hiveforge-io/hivemind/issues)
13
+ [![GitHub stars](https://img.shields.io/github/stars/hiveforge-io/hivemind)](https://github.com/hiveforge-io/hivemind/stargazers)
8
14
 
9
15
  An MCP (Model Context Protocol) server for Obsidian worldbuilding vaults that provides AI tools with consistent, canonical context from your fictional worlds.
10
16
 
@@ -134,39 +140,65 @@ Obsidian Vault → File Watcher → Markdown Parser → Knowledge Graph
134
140
 
135
141
  ## Development Status
136
142
 
137
- **Current Phase**: Phase 1 - MVP Complete ✅
143
+ **Current Phase**: Milestone 1.0 Complete ✅
138
144
 
139
- ### Roadmap
145
+ ### What's Included
140
146
 
141
- See [.planning/PROJECT.md](.planning/PROJECT.md) for the active requirements and progress tracking.
142
-
143
- **Recently Completed:**
144
- - [x] Project setup and dependencies
145
- - [x] Vault reading and file watching (VaultReader, VaultWatcher)
146
- - [x] Markdown parsing with wikilinks (MarkdownParser)
147
- - [x] Knowledge graph construction (GraphBuilder, HivemindDatabase)
148
- - [x] HybridRAG search implementation (SearchEngine)
149
- - [x] MCP tools (query_character, query_location, search_vault, rebuild_index)
150
- - [x] CLI vault override flag (--vault)
151
- - [x] Automatic stale index detection on startup
147
+ - [x] MCP server with hybrid search (vector, graph, keyword)
148
+ - [x] Vault templates for all entity types (Character, Location, Event, Faction, Lore, Asset)
149
+ - [x] Canon workflow tools (status management, consistency validation)
150
+ - [x] Asset management with full provenance tracking
152
151
  - [x] ComfyUI integration with workflow management
153
- - [x] Obsidian plugin with image generation and frontmatter tools
154
- - [x] GitHub release automation for plugin distribution
155
- - [x] Test coverage improvement (37% → 45%)
156
- - [x] CodeQL security scanning integration
152
+ - [x] Obsidian plugin with image generation
153
+ - [x] CI/CD with semantic-release and CodeQL scanning
157
154
 
158
155
  **Up Next:**
159
- - [ ] Obsidian plugin community submission (automated release ready)
160
- - [ ] Testing and validation
161
- - [ ] Vault templates standardization
162
- - [ ] Canon workflow implementation
156
+ - [ ] Obsidian community plugin submission
157
+ - [ ] Template System (make Hivemind domain-agnostic)
158
+
159
+ ## MCP Tools
160
+
161
+ ### Query Tools
162
+ | Tool | Description |
163
+ |------|-------------|
164
+ | `query_character` | Get character with relationships and content |
165
+ | `query_location` | Get location with hierarchy and connected entities |
166
+ | `search_vault` | Hybrid search across all content with filters |
167
+
168
+ ### Asset Management
169
+ | Tool | Description |
170
+ |------|-------------|
171
+ | `store_asset` | Store generated image with provenance metadata |
172
+ | `query_asset` | Get asset with generation settings |
173
+ | `list_assets` | Filter assets by entity, type, status, workflow |
174
+
175
+ ### Canon Workflow
176
+ | Tool | Description |
177
+ |------|-------------|
178
+ | `get_canon_status` | List entities grouped by status (draft/pending/canon) |
179
+ | `submit_for_review` | Move entity from draft to pending review |
180
+ | `validate_consistency` | Check for broken links, duplicates, conflicts |
181
+
182
+ ### ComfyUI Integration (when enabled)
183
+ | Tool | Description |
184
+ |------|-------------|
185
+ | `store_workflow` | Save ComfyUI workflow to vault |
186
+ | `list_workflows` | Browse saved workflows |
187
+ | `get_workflow` | Retrieve workflow by ID |
188
+ | `generate_image` | Generate image with vault context injection |
189
+
190
+ ### Utility
191
+ | Tool | Description |
192
+ |------|-------------|
193
+ | `rebuild_index` | Force complete re-index of vault |
194
+ | `get_vault_stats` | Vault statistics and token savings metrics |
163
195
 
164
196
  ## Documentation
165
197
 
166
- - [Project Requirements & Roadmap](.planning/PROJECT.md)
167
- - [Architecture Research](.planning/research/ARCHITECTURE.md)
168
- - [Technology Stack](.planning/research/STACK.md)
169
- - [Features Specification](.planning/research/FEATURES.md)
198
+ - [Setup Guide](docs/SETUP_GUIDE.md)
199
+ - [ComfyUI Integration](docs/COMFYUI_INTEGRATION.md)
200
+ - [Obsidian Plugin Workflow](docs/OBSIDIAN_PLUGIN_WORKFLOW.md)
201
+ - [Vault Templates](sample-vault/Templates/README.md)
170
202
 
171
203
  ## License
172
204
 
package/dist/server.d.ts CHANGED
@@ -28,6 +28,11 @@ export declare class HivemindServer {
28
28
  private handleGetWorkflow;
29
29
  private handleGenerateImage;
30
30
  private handleStoreAsset;
31
+ private handleQueryAsset;
32
+ private handleListAssets;
33
+ private handleGetCanonStatus;
34
+ private handleSubmitForReview;
35
+ private handleValidateConsistency;
31
36
  start(): Promise<void>;
32
37
  private printReadyBanner;
33
38
  }
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAQA,OAAO,EAOL,KAAK,cAAc,EACpB,MAAM,kBAAkB,CAAC;AAY1B,qBAAa,cAAc;IACzB,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,MAAM,CAAiB;IAC/B,OAAO,CAAC,WAAW,CAAc;IACjC,OAAO,CAAC,YAAY,CAAe;IACnC,OAAO,CAAC,QAAQ,CAAmB;IACnC,OAAO,CAAC,YAAY,CAAe;IACnC,OAAO,CAAC,YAAY,CAAe;IACnC,OAAO,CAAC,aAAa,CAAC,CAAgB;IACtC,OAAO,CAAC,eAAe,CAAC,CAAkB;IAC1C,OAAO,CAAC,SAAS,CAAkB;IACnC,OAAO,CAAC,YAAY,CAIlB;gBAEU,MAAM,EAAE,cAAc;IA+ClC,OAAO,CAAC,aAAa;YA8XP,oBAAoB;YA+CpB,mBAAmB;YA+CnB,iBAAiB;YA2CjB,kBAAkB;YAiClB,mBAAmB;IA6FjC,OAAO,CAAC,iBAAiB;YAcX,aAAa;IAwC3B,OAAO,CAAC,gCAAgC;IAwExC,OAAO,CAAC,+BAA+B;IA8CvC,OAAO,CAAC,mBAAmB;YA2Bb,mBAAmB;YAkBnB,mBAAmB;YA8BnB,iBAAiB;YA0BjB,mBAAmB;YAwInB,gBAAgB;IAkCxB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;YAkCd,gBAAgB;CA2B/B"}
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AASA,OAAO,EAYL,KAAK,cAAc,EACpB,MAAM,kBAAkB,CAAC;AAY1B,qBAAa,cAAc;IACzB,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,MAAM,CAAiB;IAC/B,OAAO,CAAC,WAAW,CAAc;IACjC,OAAO,CAAC,YAAY,CAAe;IACnC,OAAO,CAAC,QAAQ,CAAmB;IACnC,OAAO,CAAC,YAAY,CAAe;IACnC,OAAO,CAAC,YAAY,CAAe;IACnC,OAAO,CAAC,aAAa,CAAC,CAAgB;IACtC,OAAO,CAAC,eAAe,CAAC,CAAkB;IAC1C,OAAO,CAAC,SAAS,CAAkB;IACnC,OAAO,CAAC,YAAY,CAIlB;gBAEU,MAAM,EAAE,cAAc;IA+ClC,OAAO,CAAC,aAAa;YA0fP,oBAAoB;YA+CpB,mBAAmB;YA+CnB,iBAAiB;YA2CjB,kBAAkB;YAiClB,mBAAmB;IA6FjC,OAAO,CAAC,iBAAiB;YAcX,aAAa;IAwC3B,OAAO,CAAC,gCAAgC;IAwExC,OAAO,CAAC,+BAA+B;IA8CvC,OAAO,CAAC,mBAAmB;YA2Bb,mBAAmB;YAkBnB,mBAAmB;YA8BnB,iBAAiB;YA0BjB,mBAAmB;YAwInB,gBAAgB;YAkChB,gBAAgB;YAkDhB,gBAAgB;YA4DhB,oBAAoB;YAqEpB,qBAAqB;YAsDrB,yBAAyB;IA4GjC,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;YAkCd,gBAAgB;CA2B/B"}
package/dist/server.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { Server } from '@modelcontextprotocol/sdk/server/index.js';
2
2
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
3
3
  import { CallToolRequestSchema, ListToolsRequestSchema, ListResourcesRequestSchema, ReadResourceRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
4
- import { QueryCharacterArgsSchema, QueryLocationArgsSchema, SearchVaultArgsSchema, StoreWorkflowArgsSchema, GenerateImageArgsSchema, StoreAssetArgsSchema, } from './types/index.js';
4
+ import { QueryCharacterArgsSchema, QueryLocationArgsSchema, SearchVaultArgsSchema, StoreWorkflowArgsSchema, GenerateImageArgsSchema, StoreAssetArgsSchema, QueryAssetArgsSchema, ListAssetsArgsSchema, GetCanonStatusArgsSchema, SubmitForReviewArgsSchema, ValidateConsistencyArgsSchema, } from './types/index.js';
5
5
  import { VaultReader } from './vault/reader.js';
6
6
  import { VaultWatcher } from './vault/watcher.js';
7
7
  import { HivemindDatabase } from './graph/database.js';
@@ -188,6 +188,110 @@ export class HivemindServer {
188
188
  required: [],
189
189
  },
190
190
  },
191
+ {
192
+ name: 'query_asset',
193
+ description: 'Get a specific asset by ID with its generation settings and metadata',
194
+ inputSchema: {
195
+ type: 'object',
196
+ properties: {
197
+ id: {
198
+ type: 'string',
199
+ description: 'Asset ID to query',
200
+ },
201
+ },
202
+ required: ['id'],
203
+ },
204
+ },
205
+ {
206
+ name: 'list_assets',
207
+ description: 'List assets with optional filters by entity, type, status, or workflow',
208
+ inputSchema: {
209
+ type: 'object',
210
+ properties: {
211
+ depicts: {
212
+ type: 'string',
213
+ description: 'Filter by entity ID depicted in asset',
214
+ },
215
+ assetType: {
216
+ type: 'string',
217
+ enum: ['image', 'audio', 'video', 'document'],
218
+ description: 'Filter by asset type',
219
+ },
220
+ status: {
221
+ type: 'string',
222
+ enum: ['draft', 'pending', 'canon', 'non-canon', 'archived'],
223
+ description: 'Filter by approval status',
224
+ },
225
+ workflowId: {
226
+ type: 'string',
227
+ description: 'Filter by workflow used',
228
+ },
229
+ limit: {
230
+ type: 'number',
231
+ description: 'Maximum results to return (1-100, default 20)',
232
+ default: 20,
233
+ },
234
+ },
235
+ required: [],
236
+ },
237
+ },
238
+ {
239
+ name: 'get_canon_status',
240
+ description: 'Get entities grouped by their canon status (draft, pending, canon, non-canon, archived)',
241
+ inputSchema: {
242
+ type: 'object',
243
+ properties: {
244
+ status: {
245
+ type: 'string',
246
+ enum: ['draft', 'pending', 'canon', 'non-canon', 'archived'],
247
+ description: 'Filter to specific status',
248
+ },
249
+ type: {
250
+ type: 'string',
251
+ enum: ['character', 'location', 'event', 'faction', 'lore', 'asset'],
252
+ description: 'Filter by entity type',
253
+ },
254
+ },
255
+ required: [],
256
+ },
257
+ },
258
+ {
259
+ name: 'submit_for_review',
260
+ description: 'Submit an entity for review, changing its status from draft to pending',
261
+ inputSchema: {
262
+ type: 'object',
263
+ properties: {
264
+ id: {
265
+ type: 'string',
266
+ description: 'Entity ID to submit for review',
267
+ },
268
+ notes: {
269
+ type: 'string',
270
+ description: 'Optional notes for reviewers',
271
+ },
272
+ },
273
+ required: ['id'],
274
+ },
275
+ },
276
+ {
277
+ name: 'validate_consistency',
278
+ description: 'Check for consistency issues in the vault (duplicate names, broken links, conflicting data)',
279
+ inputSchema: {
280
+ type: 'object',
281
+ properties: {
282
+ id: {
283
+ type: 'string',
284
+ description: 'Specific entity ID to validate, or omit for full vault check',
285
+ },
286
+ type: {
287
+ type: 'string',
288
+ enum: ['character', 'location', 'event', 'faction', 'lore', 'asset'],
289
+ description: 'Filter validation to specific type',
290
+ },
291
+ },
292
+ required: [],
293
+ },
294
+ },
191
295
  ...(this.config.comfyui?.enabled ? [
192
296
  {
193
297
  name: 'store_workflow',
@@ -372,6 +476,26 @@ export class HivemindServer {
372
476
  const parsed = StoreAssetArgsSchema.parse(args);
373
477
  return await this.handleStoreAsset(parsed);
374
478
  }
479
+ case 'query_asset': {
480
+ const parsed = QueryAssetArgsSchema.parse(args);
481
+ return await this.handleQueryAsset(parsed);
482
+ }
483
+ case 'list_assets': {
484
+ const parsed = ListAssetsArgsSchema.parse(args);
485
+ return await this.handleListAssets(parsed);
486
+ }
487
+ case 'get_canon_status': {
488
+ const parsed = GetCanonStatusArgsSchema.parse(args);
489
+ return await this.handleGetCanonStatus(parsed);
490
+ }
491
+ case 'submit_for_review': {
492
+ const parsed = SubmitForReviewArgsSchema.parse(args);
493
+ return await this.handleSubmitForReview(parsed);
494
+ }
495
+ case 'validate_consistency': {
496
+ const parsed = ValidateConsistencyArgsSchema.parse(args);
497
+ return await this.handleValidateConsistency(parsed);
498
+ }
375
499
  default:
376
500
  throw new Error(`Unknown tool: ${name}`);
377
501
  }
@@ -1034,6 +1158,302 @@ File watcher keeps index updated automatically.
1034
1158
  }],
1035
1159
  };
1036
1160
  }
1161
+ async handleQueryAsset(args) {
1162
+ const asset = this.database.db.prepare(`
1163
+ SELECT * FROM assets WHERE id = ?
1164
+ `).get(args.id);
1165
+ if (!asset) {
1166
+ return {
1167
+ content: [{
1168
+ type: 'text',
1169
+ text: `Asset not found: "${args.id}"`,
1170
+ }],
1171
+ };
1172
+ }
1173
+ const depicts = asset.depicts ? JSON.parse(asset.depicts) : [];
1174
+ const parameters = asset.parameters ? JSON.parse(asset.parameters) : {};
1175
+ let response = `# Asset: ${asset.id}\n\n`;
1176
+ response += `**Type**: ${asset.asset_type} | **Status**: ${asset.status}\n\n`;
1177
+ response += `## File\n\`${asset.file_path}\`\n\n`;
1178
+ if (depicts.length > 0) {
1179
+ response += `## Depicts\n${depicts.map((d) => `- ${d}`).join('\n')}\n\n`;
1180
+ }
1181
+ if (asset.workflow_id) {
1182
+ response += `## Generation\n`;
1183
+ response += `- **Workflow**: ${asset.workflow_id}\n`;
1184
+ if (asset.prompt)
1185
+ response += `- **Prompt**: ${asset.prompt}\n`;
1186
+ if (Object.keys(parameters).length > 0) {
1187
+ response += `- **Parameters**:\n`;
1188
+ for (const [key, value] of Object.entries(parameters)) {
1189
+ response += ` - ${key}: ${value}\n`;
1190
+ }
1191
+ }
1192
+ response += `\n`;
1193
+ }
1194
+ response += `## Metadata\n`;
1195
+ response += `- **Created**: ${asset.created}\n`;
1196
+ response += `- **Status**: ${asset.status}\n`;
1197
+ return {
1198
+ content: [{
1199
+ type: 'text',
1200
+ text: response,
1201
+ }],
1202
+ };
1203
+ }
1204
+ async handleListAssets(args) {
1205
+ let query = 'SELECT * FROM assets WHERE 1=1';
1206
+ const params = [];
1207
+ if (args.depicts) {
1208
+ query += ` AND depicts LIKE ?`;
1209
+ params.push(`%${args.depicts}%`);
1210
+ }
1211
+ if (args.assetType) {
1212
+ query += ` AND asset_type = ?`;
1213
+ params.push(args.assetType);
1214
+ }
1215
+ if (args.status) {
1216
+ query += ` AND status = ?`;
1217
+ params.push(args.status);
1218
+ }
1219
+ if (args.workflowId) {
1220
+ query += ` AND workflow_id = ?`;
1221
+ params.push(args.workflowId);
1222
+ }
1223
+ query += ` ORDER BY created DESC LIMIT ?`;
1224
+ params.push(args.limit || 20);
1225
+ const assets = this.database.db.prepare(query).all(...params);
1226
+ if (assets.length === 0) {
1227
+ return {
1228
+ content: [{
1229
+ type: 'text',
1230
+ text: 'No assets found matching the specified filters.',
1231
+ }],
1232
+ };
1233
+ }
1234
+ let response = `# Assets\n\nFound ${assets.length} asset(s):\n\n`;
1235
+ for (const asset of assets) {
1236
+ const depicts = asset.depicts ? JSON.parse(asset.depicts) : [];
1237
+ response += `## ${asset.id}\n`;
1238
+ response += `- **Type**: ${asset.asset_type} | **Status**: ${asset.status}\n`;
1239
+ response += `- **Path**: \`${asset.file_path}\`\n`;
1240
+ if (depicts.length > 0) {
1241
+ response += `- **Depicts**: ${depicts.join(', ')}\n`;
1242
+ }
1243
+ if (asset.workflow_id) {
1244
+ response += `- **Workflow**: ${asset.workflow_id}\n`;
1245
+ }
1246
+ response += `- **Created**: ${asset.created}\n`;
1247
+ response += `\n`;
1248
+ }
1249
+ return {
1250
+ content: [{
1251
+ type: 'text',
1252
+ text: response,
1253
+ }],
1254
+ };
1255
+ }
1256
+ async handleGetCanonStatus(args) {
1257
+ await this.ensureIndexed();
1258
+ const allNotes = this.vaultReader.getAllNotes();
1259
+ // Group notes by status
1260
+ const byStatus = {
1261
+ draft: [],
1262
+ pending: [],
1263
+ canon: [],
1264
+ 'non-canon': [],
1265
+ archived: [],
1266
+ };
1267
+ for (const note of allNotes) {
1268
+ const status = note.frontmatter.status || 'draft';
1269
+ const type = note.frontmatter.type;
1270
+ // Apply filters
1271
+ if (args.status && status !== args.status)
1272
+ continue;
1273
+ if (args.type && type !== args.type)
1274
+ continue;
1275
+ if (byStatus[status]) {
1276
+ byStatus[status].push({
1277
+ id: note.id,
1278
+ title: note.frontmatter.title || note.fileName,
1279
+ type: type,
1280
+ path: note.filePath,
1281
+ });
1282
+ }
1283
+ }
1284
+ let response = `# Canon Status Overview\n\n`;
1285
+ const statusLabels = {
1286
+ draft: 'Draft (Work in Progress)',
1287
+ pending: 'Pending Review',
1288
+ canon: 'Canon (Approved)',
1289
+ 'non-canon': 'Non-Canon',
1290
+ archived: 'Archived',
1291
+ };
1292
+ for (const [status, notes] of Object.entries(byStatus)) {
1293
+ if (args.status && status !== args.status)
1294
+ continue;
1295
+ response += `## ${statusLabels[status]} (${notes.length})\n\n`;
1296
+ if (notes.length === 0) {
1297
+ response += `*No entities*\n\n`;
1298
+ }
1299
+ else {
1300
+ for (const note of notes) {
1301
+ response += `- **${note.title}** (${note.type}) — \`${note.id}\`\n`;
1302
+ }
1303
+ response += `\n`;
1304
+ }
1305
+ }
1306
+ // Summary
1307
+ const total = Object.values(byStatus).reduce((sum, arr) => sum + arr.length, 0);
1308
+ response += `---\n**Total**: ${total} entities\n`;
1309
+ return {
1310
+ content: [{
1311
+ type: 'text',
1312
+ text: response,
1313
+ }],
1314
+ };
1315
+ }
1316
+ async handleSubmitForReview(args) {
1317
+ await this.ensureIndexed();
1318
+ // Find the note
1319
+ const note = this.vaultReader.getAllNotes().find(n => n.id === args.id);
1320
+ if (!note) {
1321
+ return {
1322
+ content: [{
1323
+ type: 'text',
1324
+ text: `Entity not found: "${args.id}"`,
1325
+ }],
1326
+ isError: true,
1327
+ };
1328
+ }
1329
+ const currentStatus = note.frontmatter.status || 'draft';
1330
+ if (currentStatus !== 'draft') {
1331
+ return {
1332
+ content: [{
1333
+ type: 'text',
1334
+ text: `Cannot submit for review: Entity "${args.id}" is currently "${currentStatus}", not "draft".\n\nOnly draft entities can be submitted for review.`,
1335
+ }],
1336
+ isError: true,
1337
+ };
1338
+ }
1339
+ // Note: This tool reports what needs to change but doesn't modify files directly.
1340
+ // The user should update the frontmatter manually or use Obsidian.
1341
+ let response = `# Submit for Review: ${note.frontmatter.title || note.fileName}\n\n`;
1342
+ response += `**Entity**: ${args.id}\n`;
1343
+ response += `**Current Status**: ${currentStatus}\n`;
1344
+ response += `**New Status**: pending\n\n`;
1345
+ if (args.notes) {
1346
+ response += `**Review Notes**: ${args.notes}\n\n`;
1347
+ }
1348
+ response += `## Action Required\n\n`;
1349
+ response += `Update the frontmatter in \`${note.filePath}\`:\n\n`;
1350
+ response += `\`\`\`yaml\n`;
1351
+ response += `status: pending\n`;
1352
+ response += `\`\`\`\n\n`;
1353
+ response += `The MCP server will detect the change automatically.\n`;
1354
+ return {
1355
+ content: [{
1356
+ type: 'text',
1357
+ text: response,
1358
+ }],
1359
+ };
1360
+ }
1361
+ async handleValidateConsistency(args) {
1362
+ await this.ensureIndexed();
1363
+ const allNotes = this.vaultReader.getAllNotes();
1364
+ const issues = [];
1365
+ // Filter notes if specific ID or type requested
1366
+ let notesToCheck = allNotes;
1367
+ if (args.id) {
1368
+ notesToCheck = allNotes.filter(n => n.id === args.id);
1369
+ if (notesToCheck.length === 0) {
1370
+ return {
1371
+ content: [{
1372
+ type: 'text',
1373
+ text: `Entity not found: "${args.id}"`,
1374
+ }],
1375
+ isError: true,
1376
+ };
1377
+ }
1378
+ }
1379
+ if (args.type) {
1380
+ notesToCheck = notesToCheck.filter(n => n.frontmatter.type === args.type);
1381
+ }
1382
+ // Check 1: Duplicate IDs
1383
+ const idCounts = new Map();
1384
+ for (const note of allNotes) {
1385
+ const paths = idCounts.get(note.id) || [];
1386
+ paths.push(note.filePath);
1387
+ idCounts.set(note.id, paths);
1388
+ }
1389
+ for (const [id, paths] of idCounts) {
1390
+ if (paths.length > 1) {
1391
+ issues.push(`**Duplicate ID** \`${id}\` found in:\n${paths.map(p => ` - ${p}`).join('\n')}`);
1392
+ }
1393
+ }
1394
+ // Check 2: Broken wikilinks
1395
+ for (const note of notesToCheck) {
1396
+ for (const link of note.links) {
1397
+ const linkLower = link.toLowerCase();
1398
+ // Check if link resolves to any note
1399
+ const found = allNotes.some(n => n.id === link ||
1400
+ (n.frontmatter.title || n.fileName).toLowerCase() === linkLower ||
1401
+ n.fileName.toLowerCase() === linkLower ||
1402
+ n.fileName.toLowerCase() === linkLower + '.md');
1403
+ if (!found) {
1404
+ issues.push(`**Broken link** in \`${note.filePath}\`: [[${link}]]`);
1405
+ }
1406
+ }
1407
+ }
1408
+ // Check 3: Missing required fields
1409
+ for (const note of notesToCheck) {
1410
+ if (!note.frontmatter.type) {
1411
+ issues.push(`**Missing type** in \`${note.filePath}\``);
1412
+ }
1413
+ if (!note.frontmatter.id && !note.id) {
1414
+ issues.push(`**Missing ID** in \`${note.filePath}\``);
1415
+ }
1416
+ }
1417
+ // Check 4: Canon entities referencing non-canon
1418
+ const canonNotes = allNotes.filter(n => n.frontmatter.status === 'canon');
1419
+ for (const note of canonNotes) {
1420
+ if (!notesToCheck.includes(note) && !args.id)
1421
+ continue;
1422
+ for (const link of note.links) {
1423
+ const linkedNote = allNotes.find(n => n.id === link ||
1424
+ (n.frontmatter.title || n.fileName).toLowerCase() === link.toLowerCase());
1425
+ if (linkedNote && linkedNote.frontmatter.status === 'draft') {
1426
+ issues.push(`**Canon → Draft reference**: \`${note.filePath}\` links to draft entity [[${link}]]`);
1427
+ }
1428
+ }
1429
+ }
1430
+ // Format response
1431
+ let response = `# Consistency Validation\n\n`;
1432
+ if (args.id) {
1433
+ response += `**Checking**: ${args.id}\n\n`;
1434
+ }
1435
+ else if (args.type) {
1436
+ response += `**Checking**: All ${args.type} entities\n\n`;
1437
+ }
1438
+ else {
1439
+ response += `**Checking**: Full vault (${allNotes.length} entities)\n\n`;
1440
+ }
1441
+ if (issues.length === 0) {
1442
+ response += `✅ **No issues found!**\n\nAll checked entities are consistent.`;
1443
+ }
1444
+ else {
1445
+ response += `⚠️ **Found ${issues.length} issue(s)**:\n\n`;
1446
+ for (const issue of issues) {
1447
+ response += `${issue}\n\n`;
1448
+ }
1449
+ }
1450
+ return {
1451
+ content: [{
1452
+ type: 'text',
1453
+ text: response,
1454
+ }],
1455
+ };
1456
+ }
1037
1457
  async start() {
1038
1458
  // Initial vault scan
1039
1459
  console.error('Performing initial vault scan...');