directory-indexer 0.1.6 → 0.1.7

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/dist/cli.js CHANGED
@@ -4,579 +4,11 @@ import { fileURLToPath } from "url";
4
4
  import { readFileSync } from "fs";
5
5
  import { dirname, join } from "path";
6
6
  import { indexDirectories } from "./indexing.js";
7
- import { getChunkContent, getFileContent, findSimilarFiles, searchContent } from "./search.js";
7
+ import { searchContent, findSimilarFiles, getFileContent } from "./search.js";
8
8
  import { loadConfig } from "./config.js";
9
9
  import { getIndexStatus } from "./storage.js";
10
- import { Server } from "@modelcontextprotocol/sdk/server/index.js";
11
- import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
12
- import { ListToolsRequestSchema, CallToolRequestSchema } from "@modelcontextprotocol/sdk/types.js";
13
- class PrerequisiteError extends Error {
14
- constructor(message, cause) {
15
- super(message);
16
- this.cause = cause;
17
- this.name = "PrerequisiteError";
18
- }
19
- }
20
- async function checkQdrant(config) {
21
- try {
22
- const response = await fetch(`${config.storage.qdrantEndpoint}/healthz`);
23
- return response.ok;
24
- } catch {
25
- return false;
26
- }
27
- }
28
- async function checkOllama(config) {
29
- try {
30
- const response = await fetch(`${config.embedding.endpoint}/api/tags`);
31
- return response.ok;
32
- } catch {
33
- return false;
34
- }
35
- }
36
- async function checkOllamaModel(config) {
37
- try {
38
- const response = await fetch(`${config.embedding.endpoint}/api/tags`);
39
- if (!response.ok) return false;
40
- const data = await response.json();
41
- const models = data.models || [];
42
- return models.some((model) => model.name.includes(config.embedding.model));
43
- } catch {
44
- return false;
45
- }
46
- }
47
- async function checkOpenAI(config) {
48
- if (!process.env.OPENAI_API_KEY) return false;
49
- try {
50
- const response = await fetch("https://api.openai.com/v1/embeddings", {
51
- method: "POST",
52
- headers: {
53
- "Content-Type": "application/json",
54
- "Authorization": `Bearer ${process.env.OPENAI_API_KEY}`
55
- },
56
- body: JSON.stringify({
57
- model: config.embedding.model,
58
- input: "test"
59
- })
60
- });
61
- return response.ok;
62
- } catch {
63
- return false;
64
- }
65
- }
66
- async function checkAllPrerequisitesDetailed(config) {
67
- const services = [];
68
- const qdrantOk = await checkQdrant(config);
69
- services.push({
70
- service: "qdrant",
71
- status: qdrantOk ? "available" : "unavailable",
72
- details: qdrantOk ? void 0 : `Cannot connect to Qdrant at ${config.storage.qdrantEndpoint}`
73
- });
74
- let embeddingOk = false;
75
- let embeddingDetails;
76
- if (config.embedding.provider === "ollama") {
77
- const ollamaOk = await checkOllama(config);
78
- const modelOk = ollamaOk ? await checkOllamaModel(config) : false;
79
- embeddingOk = ollamaOk && modelOk;
80
- if (!ollamaOk) {
81
- embeddingDetails = `Cannot connect to Ollama at ${config.embedding.endpoint}`;
82
- } else if (!modelOk) {
83
- embeddingDetails = `Model "${config.embedding.model}" not available in Ollama`;
84
- }
85
- } else if (config.embedding.provider === "openai") {
86
- embeddingOk = await checkOpenAI(config);
87
- if (!embeddingOk) {
88
- embeddingDetails = process.env.OPENAI_API_KEY ? "OpenAI API request failed" : "OPENAI_API_KEY environment variable not set";
89
- }
90
- } else if (config.embedding.provider === "mock") {
91
- embeddingOk = true;
92
- } else {
93
- embeddingDetails = `Unknown embedding provider: ${config.embedding.provider}`;
94
- }
95
- services.push({
96
- service: config.embedding.provider,
97
- status: embeddingOk ? "available" : "unavailable",
98
- details: embeddingDetails
99
- });
100
- return {
101
- allPassed: services.every((s) => s.status === "available"),
102
- services
103
- };
104
- }
105
- function createComprehensiveErrorMessage(result) {
106
- const unavailableServices = result.services.filter((s) => s.status === "unavailable");
107
- const serviceDescriptions = unavailableServices.map((service) => {
108
- return `${service.service} (${service.details})`;
109
- });
110
- const message = `Required services are not available to use directory-indexer features: ${serviceDescriptions.join(", ")}.`;
111
- const setup = "For setup instructions, see: https://github.com/peteretelej/directory-indexer#setup";
112
- return `${message}
113
-
114
- ${setup}`;
115
- }
116
- async function validateIndexPrerequisites(config) {
117
- const result = await checkAllPrerequisitesDetailed(config);
118
- if (!result.allPassed) {
119
- throw new PrerequisiteError(createComprehensiveErrorMessage(result));
120
- }
121
- }
122
- async function validateSearchPrerequisites(config) {
123
- const result = await checkAllPrerequisitesDetailed(config);
124
- const qdrantService = result.services.find((s) => s.service === "qdrant");
125
- if (qdrantService?.status === "unavailable") {
126
- const qdrantOnlyResult = {
127
- services: [qdrantService]
128
- };
129
- throw new PrerequisiteError(createComprehensiveErrorMessage(qdrantOnlyResult));
130
- }
131
- }
132
- async function checkEmbeddingService(config) {
133
- switch (config.embedding.provider) {
134
- case "ollama":
135
- return await checkOllama(config) && await checkOllamaModel(config);
136
- case "openai":
137
- return await checkOpenAI(config);
138
- case "mock":
139
- return true;
140
- default:
141
- return false;
142
- }
143
- }
144
- async function getServiceStatus(config) {
145
- const [qdrantOk, embeddingOk] = await Promise.all([
146
- checkQdrant(config),
147
- checkEmbeddingService(config)
148
- ]);
149
- return {
150
- qdrant: qdrantOk,
151
- embedding: embeddingOk,
152
- embeddingProvider: config.embedding.provider
153
- };
154
- }
155
- function isIndexToolArgs(args) {
156
- return typeof args === "object" && args !== null && typeof args.directory_path === "string";
157
- }
158
- function isSearchToolArgs(args) {
159
- return typeof args === "object" && args !== null && typeof args.query === "string";
160
- }
161
- function isSimilarFilesToolArgs(args) {
162
- return typeof args === "object" && args !== null && typeof args.file_path === "string";
163
- }
164
- function isGetContentToolArgs(args) {
165
- return typeof args === "object" && args !== null && typeof args.file_path === "string";
166
- }
167
- function isGetChunkToolArgs(args) {
168
- return typeof args === "object" && args !== null && typeof args.file_path === "string" && typeof args.chunk_id === "string";
169
- }
170
- async function handleIndexTool(args, config) {
171
- if (!isIndexToolArgs(args)) {
172
- throw new Error("directory_path is required");
173
- }
174
- await validateIndexPrerequisites(config);
175
- const paths = args.directory_path.split(",").map((p) => p.trim());
176
- const result = await indexDirectories(paths, config);
177
- let responseText = `Indexed ${result.indexed} files, skipped ${result.skipped} files, ${result.failed} failed`;
178
- if (result.errors.length > 0) {
179
- responseText += `
180
- Errors: [
181
- `;
182
- result.errors.forEach((error) => {
183
- responseText += ` '${error}'
184
- `;
185
- });
186
- responseText += `]`;
187
- }
188
- return {
189
- content: [
190
- {
191
- type: "text",
192
- text: responseText
193
- }
194
- ]
195
- };
196
- }
197
- async function validateWorkspace(workspace) {
198
- if (!workspace) return { workspace };
199
- const config = (await import("./config.js")).loadConfig();
200
- const { getAvailableWorkspaces } = await import("./config.js");
201
- const availableWorkspaces = getAvailableWorkspaces(config);
202
- if (availableWorkspaces.includes(workspace)) {
203
- return { workspace };
204
- }
205
- const message = availableWorkspaces.length > 0 ? `Note: Workspace '${workspace}' not found. Searching all content instead. Available workspaces: ${availableWorkspaces.join(", ")}. Use server_info tool to see workspace details.` : `Note: Workspace '${workspace}' not found and no workspaces are configured. Searching all indexed content.`;
206
- return { workspace: void 0, message };
207
- }
208
- async function handleSearchTool(args) {
209
- if (!isSearchToolArgs(args)) {
210
- throw new Error("query is required");
211
- }
212
- const config = (await import("./config.js")).loadConfig();
213
- await validateSearchPrerequisites(config);
214
- const { workspace, message } = await validateWorkspace(args.workspace);
215
- const results = await searchContent(args.query, { limit: args.limit || 10, workspace });
216
- const response = message ? `${message}
217
-
218
- ${JSON.stringify(results, null, 2)}` : JSON.stringify(results, null, 2);
219
- return {
220
- content: [{ type: "text", text: response }]
221
- };
222
- }
223
- async function handleSimilarFilesTool(args) {
224
- if (!isSimilarFilesToolArgs(args)) {
225
- throw new Error("file_path is required");
226
- }
227
- const config = (await import("./config.js")).loadConfig();
228
- await validateSearchPrerequisites(config);
229
- const { workspace, message } = await validateWorkspace(args.workspace);
230
- const results = await findSimilarFiles(args.file_path, args.limit || 10, workspace);
231
- const response = message ? `${message}
232
-
233
- ${JSON.stringify(results, null, 2)}` : JSON.stringify(results, null, 2);
234
- return {
235
- content: [{ type: "text", text: response }]
236
- };
237
- }
238
- async function handleGetContentTool(args) {
239
- if (!isGetContentToolArgs(args)) {
240
- throw new Error("file_path is required");
241
- }
242
- const content = await getFileContent(args.file_path, args.chunks);
243
- return {
244
- content: [
245
- {
246
- type: "text",
247
- text: content
248
- }
249
- ]
250
- };
251
- }
252
- async function handleGetChunkTool(args) {
253
- if (!isGetChunkToolArgs(args)) {
254
- throw new Error("file_path and chunk_id are required");
255
- }
256
- const content = await getChunkContent(args.file_path, args.chunk_id);
257
- return {
258
- content: [
259
- {
260
- type: "text",
261
- text: content
262
- }
263
- ]
264
- };
265
- }
266
- async function handleServerInfoTool(version) {
267
- const status = await getIndexStatus();
268
- return {
269
- content: [
270
- {
271
- type: "text",
272
- text: JSON.stringify({
273
- name: "directory-indexer",
274
- version,
275
- status
276
- }, null, 2)
277
- }
278
- ]
279
- };
280
- }
281
- function formatErrorResponse(error) {
282
- const errorMessage = error instanceof Error ? error.message : "Unknown error";
283
- return {
284
- content: [
285
- {
286
- type: "text",
287
- text: `Error: ${errorMessage}`
288
- }
289
- ],
290
- isError: true
291
- };
292
- }
293
- const __dirname$1 = dirname(fileURLToPath(import.meta.url));
294
- const packageJsonPath$1 = join(__dirname$1, "../package.json");
295
- const packageJson$1 = JSON.parse(readFileSync(packageJsonPath$1, "utf-8"));
296
- const VERSION$1 = packageJson$1.version;
297
- const MCP_TOOLS = [
298
- {
299
- name: "index",
300
- description: `Index directories to make their files searchable. Processes files to create vector embeddings for semantic search.
301
-
302
- When to use this tool:
303
- - User specifically requests indexing a directory as a knowledge base
304
- - Adding new documentation, code repositories, or file collections to search
305
- - Updating index when many files have changed
306
-
307
- How it works:
308
- - Recursively scans directories for supported file types
309
- - Extracts text content and splits into chunks
310
- - Generates vector embeddings for semantic similarity
311
- - Stores in database for fast retrieval
312
-
313
- Examples:
314
- - Index documentation: "/home/user/docs/project-wiki"
315
- - Index codebase: "/home/user/projects/api-server"
316
- - Index multiple directories: "/home/user/docs,/home/user/configs"
317
-
318
- Indexing can take several minutes for large directories. Most users will already have directories indexed and can directly use search tool. Use server_info to check current indexing status first.`,
319
- inputSchema: {
320
- type: "object",
321
- properties: {
322
- directory_path: {
323
- type: "string",
324
- description: 'Comma-separated list of absolute directory paths to index. Must be absolute paths since MCP server runs independently. Examples: "/home/user/projects" (Unix) or "C:\\Users\\user\\projects" (Windows)'
325
- }
326
- },
327
- required: ["directory_path"]
328
- }
329
- },
330
- {
331
- name: "search",
332
- description: `Search indexed files using natural language queries. Finds files containing content semantically similar to the query.
333
-
334
- When to use this tool:
335
- - Find documentation, guides, or explanations about specific topics
336
- - Locate code files implementing certain functionality or patterns
337
- - Discover configuration files, scripts, or settings related to a topic
338
- - Search for files covering specific concepts or technologies
339
-
340
- How it works:
341
- - Converts query to vector embedding using semantic similarity
342
- - Searches all indexed file chunks for relevant content
343
- - Groups results by file and calculates average relevance scores
344
- - Returns files ranked by relevance score
345
-
346
- Examples:
347
- - "database configuration and connection pooling setup" - finds config files, documentation about DB setup
348
- - "comprehensive error handling patterns and exception management" - finds code files with exception handling
349
- - "JWT authentication implementation and session management" - finds auth-related code and docs
350
- - "REST API documentation and endpoint specifications" - finds API guides, endpoint definitions
351
- - "Docker deployment scripts and CI/CD pipeline configuration" - finds deployment automation
352
-
353
- Returns files with similarity scores and chunk information. Use get_content to retrieve full file content or get_chunk to retrieve specific chunk content by chunk ID.
354
- - Groups results by file to avoid duplicates from multiple matching sections
355
-
356
- Response format:
357
- - Returns lightweight metadata including file paths, relevance scores, and chunk IDs
358
- - Use 'get_chunk' or 'get_content' tools to fetch actual content from search results
359
- - Chunks are sorted by relevance score within each file
360
- - Average similarity score calculated across all matching chunks per file
361
-
362
- Example queries:
363
- - "error handling patterns and exception management strategies" (finds try/catch, error classes, logging)
364
- - "database migration scripts and schema versioning approaches" (finds SQL, schema changes, migration files)
365
- - "authentication middleware and JWT token validation logic" (finds auth logic, JWT handling, middleware functions)`,
366
- inputSchema: {
367
- type: "object",
368
- properties: {
369
- query: {
370
- type: "string",
371
- description: "Natural language search query describing what you are looking for. Can be concepts, functionality, or specific technical terms."
372
- },
373
- limit: {
374
- type: "number",
375
- description: "Maximum number of files to return (default: 10). Each file may contain multiple matching chunks.",
376
- default: 10
377
- },
378
- workspace: {
379
- type: "string",
380
- description: "Optional workspace name to filter search results. Only files within the workspace directories will be searched. IMPORTANT: Use server_info tool first to discover available workspace names - using invalid workspace names will result in empty results."
381
- }
382
- },
383
- required: ["query"]
384
- }
385
- },
386
- {
387
- name: "similar_files",
388
- description: `Find files with content similar to a reference file. Uses semantic similarity to find related documents, code files, or any text content.
389
-
390
- When to use this tool:
391
- - Find documentation similar to a specific guide or README
392
- - Locate related code files, configuration files, or scripts
393
- - Discover alternative implementations or approaches
394
- - Find files covering similar topics or concepts
395
-
396
- How it works:
397
- - Analyzes the semantic content of the reference file
398
- - Compares against all indexed files using vector similarity
399
- - Returns files ranked by content similarity score
400
-
401
- Examples:
402
- - Given "deployment-guide.md" - finds other deployment docs, CI/CD guides, infrastructure setup
403
- - Given "troubleshooting.md" - finds other troubleshooting guides, FAQ files, error documentation
404
- - Given "config.yaml" - finds other configuration files, settings, environment setups
405
- - Given "auth.py" - finds other authentication modules, security code, middleware
406
-
407
- Returns file paths with similarity scores. Use get_content to read full files or get_chunk for specific sections.`,
408
- inputSchema: {
409
- type: "object",
410
- properties: {
411
- file_path: {
412
- type: "string",
413
- description: "Absolute or relative path to the reference file. This file must have been previously indexed."
414
- },
415
- limit: {
416
- type: "number",
417
- description: "Maximum number of similar files to return (default: 10). Results are sorted by similarity score.",
418
- default: 10
419
- },
420
- workspace: {
421
- type: "string",
422
- description: "Optional workspace name to filter results. Only files within the workspace directories will be considered. IMPORTANT: Use server_info tool first to discover available workspace names - using invalid workspace names will result in empty results."
423
- }
424
- },
425
- required: ["file_path"]
426
- }
427
- },
428
- {
429
- name: "get_content",
430
- description: `Retrieve the full content of a file or specific chunks. Reads files directly from the filesystem.
431
-
432
- When to use this tool:
433
- - Get complete file content after finding files through search
434
- - Read documentation, code files, or configuration files for analysis
435
- - Extract specific sections of large files using chunk ranges
436
- - Access any text-based file content
437
-
438
- How it works:
439
- - Reads files directly from filesystem (not from search index)
440
- - Returns entire file by default
441
- - Can return specific chunk ranges for indexed files
442
- - Preserves original formatting and content
443
-
444
- Examples:
445
- - Get full file: file_path="/home/user/docs/api.md"
446
- - Get specific chunks: file_path="/home/user/code/main.py", chunks="2-5"
447
- - Get single chunk: file_path="/home/user/config.json", chunks="1"
448
-
449
- Returns file content as text. Use this after search or similar_files to read actual content.`,
450
- inputSchema: {
451
- type: "object",
452
- properties: {
453
- file_path: {
454
- type: "string",
455
- description: "Absolute or relative path to the file to retrieve. File must be readable and text-based."
456
- },
457
- chunks: {
458
- type: "string",
459
- description: 'Optional chunk range specification. Examples: "3" (single chunk), "2-5" (chunks 2 through 5), "1-3" (first three chunks). Only works for indexed files.'
460
- }
461
- },
462
- required: ["file_path"]
463
- }
464
- },
465
- {
466
- name: "get_chunk",
467
- description: `Retrieve content of a specific chunk from an indexed file. Gets exact text segments identified during search.
468
-
469
- When to use this tool:
470
- - Get specific relevant sections after performing a search
471
- - Access only the most pertinent parts of large files
472
- - Retrieve content from high-scoring chunks identified in search results
473
- - Avoid reading entire files when only specific sections are needed
474
-
475
- How it works:
476
- - Files are split into overlapping text chunks during indexing
477
- - Each chunk has a sequential ID ("0", "1", "2", etc.)
478
- - Search results include chunk IDs for relevant sections
479
- - Returns the exact content that was semantically matched
480
-
481
- Examples:
482
- - After search returns chunk "3" from "api-docs.md" with high score
483
- - Get chunk content: file_path="/docs/api-docs.md", chunk_id="3"
484
- - Returns the specific text segment that matched your query
485
-
486
- Returns chunk content as text. Use this with chunk IDs from search results to get precise content sections.`,
487
- inputSchema: {
488
- type: "object",
489
- properties: {
490
- file_path: {
491
- type: "string",
492
- description: "Absolute or relative path to the indexed file containing the desired chunk."
493
- },
494
- chunk_id: {
495
- type: "string",
496
- description: 'ID of the specific chunk to retrieve. This is typically obtained from search results and is a sequential string like "0", "1", "2", etc.'
497
- }
498
- },
499
- required: ["file_path", "chunk_id"]
500
- }
501
- },
502
- {
503
- name: "server_info",
504
- description: `Get information about server status and indexed content. Shows what directories and files are available for search.
505
-
506
- When to use this tool:
507
- - REQUIRED: Check available workspace names before using workspace parameter in search or similar_files tools
508
- - Check what content is already indexed before performing searches
509
- - Verify system is working properly
510
- - See indexing statistics and status
511
- - Understand scope of available searchable content
512
-
513
- How it works:
514
- - Reports total indexed directories, files, and chunks
515
- - Shows database size and last indexing time
516
- - Lists all indexed directories with file counts
517
- - Lists all configured workspaces with their paths and file counts
518
- - Reports any errors or issues
519
-
520
- Examples:
521
- - Check workspaces before searching: "What workspaces are available?"
522
- - Check before searching: "What content is indexed?"
523
- - Verify after indexing: "Did the indexing complete successfully?"
524
- - Monitor system: "How many files are searchable?"
525
-
526
- Returns server version, indexing statistics, directory list, workspace information, and any errors. IMPORTANT: Always use this tool first to discover available workspace names when you need to search within specific workspaces.`,
527
- inputSchema: {
528
- type: "object",
529
- properties: {},
530
- additionalProperties: false
531
- }
532
- }
533
- ];
534
- async function startMcpServer(config) {
535
- const server = new Server(
536
- {
537
- name: "directory-indexer",
538
- version: VERSION$1
539
- },
540
- {
541
- capabilities: {
542
- tools: {}
543
- }
544
- }
545
- );
546
- server.setRequestHandler(ListToolsRequestSchema, async () => {
547
- return {
548
- tools: MCP_TOOLS
549
- };
550
- });
551
- server.setRequestHandler(CallToolRequestSchema, async (request) => {
552
- const { name, arguments: args } = request.params;
553
- try {
554
- switch (name) {
555
- case "index":
556
- return await handleIndexTool(args, config);
557
- case "search":
558
- return await handleSearchTool(args);
559
- case "similar_files":
560
- return await handleSimilarFilesTool(args);
561
- case "get_content":
562
- return await handleGetContentTool(args);
563
- case "get_chunk":
564
- return await handleGetChunkTool(args);
565
- case "server_info":
566
- return await handleServerInfoTool(VERSION$1);
567
- default:
568
- throw new Error(`Unknown tool: ${name}`);
569
- }
570
- } catch (error) {
571
- return formatErrorResponse(error);
572
- }
573
- });
574
- const transport = new StdioServerTransport();
575
- await server.connect(transport);
576
- if (config.verbose) {
577
- console.error("MCP server started successfully");
578
- }
579
- }
10
+ import { startMcpServer } from "./mcp.js";
11
+ import { validateIndexPrerequisites, validateSearchPrerequisites, getServiceStatus } from "./prerequisites.js";
580
12
  const __dirname = dirname(fileURLToPath(import.meta.url));
581
13
  const packageJsonPath = join(__dirname, "../package.json");
582
14
  const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
@@ -590,7 +22,7 @@ async function main() {
590
22
  await validateIndexPrerequisites(config);
591
23
  console.log(`Indexing ${paths.length} ${paths.length === 1 ? "directory" : "directories"}: ${paths.join(", ")}`);
592
24
  const result = await indexDirectories(paths, config);
593
- console.log(`Indexed ${result.indexed} files, skipped ${result.skipped} files, ${result.failed} failed`);
25
+ console.log(`Indexed ${result.indexed} files, skipped ${result.skipped} files, cleaned up ${result.deleted} deleted files, ${result.failed} failed`);
594
26
  if (result.errors.length > 0) {
595
27
  console.log(`Errors: [`);
596
28
  result.errors.forEach((error) => {
@@ -724,6 +156,39 @@ async function main() {
724
156
  }
725
157
  });
726
158
  }
159
+ if (status.workspaces.length > 0) {
160
+ console.log("");
161
+ console.log("WORKSPACES:");
162
+ console.log(` • ${status.workspaceHealth.healthy} healthy, ${status.workspaceHealth.warnings} warnings, ${status.workspaceHealth.errors} errors`);
163
+ if (status.workspaceHealth.errors > 0) {
164
+ console.log("");
165
+ console.log("WORKSPACE ERRORS:");
166
+ status.workspaceHealth.criticalIssues.forEach((issue) => {
167
+ console.log(` ❌ ${issue}`);
168
+ });
169
+ }
170
+ if (status.workspaceHealth.recommendations.length > 0) {
171
+ console.log("");
172
+ console.log("WORKSPACE RECOMMENDATIONS:");
173
+ status.workspaceHealth.recommendations.forEach((rec) => {
174
+ console.log(` 💡 ${rec}`);
175
+ });
176
+ }
177
+ if (options.verbose) {
178
+ console.log("");
179
+ console.log("WORKSPACE DETAILS:");
180
+ status.workspaces.forEach((workspace) => {
181
+ console.log("");
182
+ console.log(` Workspace: ${workspace.name}`);
183
+ console.log(` • Status: ${workspace.health.status}`);
184
+ console.log(` • Paths: ${workspace.paths.join(", ")}`);
185
+ console.log(` • Files: ${workspace.filesCount}, Chunks: ${workspace.chunksCount}`);
186
+ if (workspace.health.issues.length > 0) {
187
+ console.log(` • Issues: ${workspace.health.issues.join("; ")}`);
188
+ }
189
+ });
190
+ }
191
+ }
727
192
  if (!status.qdrantConsistency.isConsistent) {
728
193
  console.log("");
729
194
  console.log("SYSTEM STATUS:");