project-mcp 3.2.2 → 3.2.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "project-mcp",
3
- "version": "3.2.2",
3
+ "version": "3.2.3",
4
4
  "description": "Intent-based MCP server for project documentation search. Maps natural language queries to the right sources automatically—no configuration needed. The standard for AI agent documentation search.",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -14,6 +14,7 @@ export const ARCHIVE_DIR = join(PROJECT_DIR, 'archive');
14
14
  export const BACKLOG_FILE = join(PROJECT_DIR, 'BACKLOG.md');
15
15
  export const THOUGHTS_DIR = join(PROJECT_DIR, 'thoughts');
16
16
  export const THOUGHTS_TODOS_DIR = join(THOUGHTS_DIR, 'todos');
17
+ export const THOUGHTS_ARCHIVE_DIR = join(THOUGHTS_TODOS_DIR, '.archive');
17
18
 
18
19
  /**
19
20
  * Intent to source mapping.
package/src/lib/files.js CHANGED
@@ -5,7 +5,14 @@
5
5
  import { readFile, readdir, stat, writeFile, mkdir, unlink, rename } from 'fs/promises';
6
6
  import { join, extname, basename } from 'path';
7
7
  import matter from 'gray-matter';
8
- import { PROJECT_DIR, TODOS_DIR, ARCHIVE_DIR, THOUGHTS_DIR, THOUGHTS_TODOS_DIR } from './constants.js';
8
+ import {
9
+ PROJECT_DIR,
10
+ TODOS_DIR,
11
+ ARCHIVE_DIR,
12
+ THOUGHTS_DIR,
13
+ THOUGHTS_TODOS_DIR,
14
+ THOUGHTS_ARCHIVE_DIR,
15
+ } from './constants.js';
9
16
 
10
17
  /**
11
18
  * Ensure .project directory exists
@@ -62,6 +69,17 @@ export async function ensureThoughtsTodosDir() {
62
69
  }
63
70
  }
64
71
 
72
+ /**
73
+ * Ensure .project/thoughts/todos/.archive directory exists
74
+ */
75
+ export async function ensureThoughtsArchiveDir() {
76
+ try {
77
+ await mkdir(THOUGHTS_ARCHIVE_DIR, { recursive: true });
78
+ } catch (error) {
79
+ // Directory might already exist
80
+ }
81
+ }
82
+
65
83
  /**
66
84
  * Check if a file exists
67
85
  * @param {string} filePath - Path to check
@@ -1,17 +1,29 @@
1
1
  /**
2
2
  * Thought processing tools.
3
- * Handles: process_thoughts, list_thoughts, get_thought
3
+ * Handles: process_thoughts, list_thoughts, get_thought, archive_thought, list_archived_thoughts
4
4
  *
5
5
  * This tool reads brain dump files and provides context for the LLM to analyze.
6
6
  * The LLM does the natural language understanding - the tool just gathers data.
7
7
  */
8
8
 
9
9
  import { readdir } from 'fs/promises';
10
- import { THOUGHTS_TODOS_DIR, PROJECT_DIR } from '../lib/constants.js';
11
- import { readFile, join, fileExists, ensureThoughtsTodosDir, matter } from '../lib/files.js';
10
+ import { THOUGHTS_TODOS_DIR, THOUGHTS_ARCHIVE_DIR } from '../lib/constants.js';
11
+ import {
12
+ readFile,
13
+ writeFile,
14
+ rename,
15
+ join,
16
+ fileExists,
17
+ ensureThoughtsTodosDir,
18
+ ensureThoughtsArchiveDir,
19
+ matter,
20
+ } from '../lib/files.js';
21
+ import { getISODate, getCurrentDate } from '../lib/dates.js';
12
22
  import { loadAllTasks } from '../lib/tasks.js';
13
23
  import { loadAllFiles, getCachedFiles } from '../lib/search.js';
14
24
 
25
+ const ARCHIVE_LOG_FILE = '.archive-log.md';
26
+
15
27
  /**
16
28
  * Tool definitions
17
29
  */
@@ -30,6 +42,7 @@ YOU (the LLM) should then analyze the content to:
30
42
  - Identify logical task groupings (consolidate related items)
31
43
  - Determine appropriate priorities based on context
32
44
  - Create well-structured tasks using create_task
45
+ - **After creating tasks, use archive_thought to archive the processed file**
33
46
 
34
47
  The tool does NOT automatically create tasks - it provides you with everything needed to make intelligent decisions about task creation.`,
35
48
  inputSchema: {
@@ -48,6 +61,31 @@ The tool does NOT automatically create tasks - it provides you with everything n
48
61
  required: ['project'],
49
62
  },
50
63
  },
64
+ {
65
+ name: 'archive_thought',
66
+ description: `Archives a processed thought file by moving it to .project/thoughts/todos/.archive/.
67
+ Use this after you've created tasks from a thought file to keep the active thoughts folder clean.
68
+ Also logs the archive action with timestamp and created task IDs.`,
69
+ inputSchema: {
70
+ type: 'object',
71
+ properties: {
72
+ file: {
73
+ type: 'string',
74
+ description: 'The thought file to archive (e.g., "my-ideas.md").',
75
+ },
76
+ created_tasks: {
77
+ type: 'array',
78
+ items: { type: 'string' },
79
+ description: 'Array of task IDs that were created from this thought (e.g., ["AUTH-001", "AUTH-002"]).',
80
+ },
81
+ notes: {
82
+ type: 'string',
83
+ description: 'Optional notes about the processing (e.g., "Consolidated 5 items into 2 tasks").',
84
+ },
85
+ },
86
+ required: ['file'],
87
+ },
88
+ },
51
89
  {
52
90
  name: 'list_thoughts',
53
91
  description:
@@ -60,6 +98,26 @@ The tool does NOT automatically create tasks - it provides you with everything n
60
98
  description: 'Optional: Filter by thought category. Currently supported: "todos".',
61
99
  enum: ['todos', ''],
62
100
  },
101
+ include_archived: {
102
+ type: 'boolean',
103
+ description: 'Include archived thoughts in the listing. Default: false.',
104
+ default: false,
105
+ },
106
+ },
107
+ },
108
+ },
109
+ {
110
+ name: 'list_archived_thoughts',
111
+ description:
112
+ 'Lists all archived thought files with their processing history. Shows what thoughts were processed, when, and what tasks were created.',
113
+ inputSchema: {
114
+ type: 'object',
115
+ properties: {
116
+ limit: {
117
+ type: 'number',
118
+ description: 'Maximum number of archived thoughts to show. Default: 20.',
119
+ default: 20,
120
+ },
63
121
  },
64
122
  },
65
123
  },
@@ -78,6 +136,11 @@ The tool does NOT automatically create tasks - it provides you with everything n
78
136
  description: 'The category/subdirectory. Default: "todos".',
79
137
  default: 'todos',
80
138
  },
139
+ from_archive: {
140
+ type: 'boolean',
141
+ description: 'Read from archive instead of active thoughts. Default: false.',
142
+ default: false,
143
+ },
81
144
  },
82
145
  required: ['file'],
83
146
  },
@@ -113,7 +176,7 @@ async function getProjectContext() {
113
176
  // Get roadmap content
114
177
  const roadmapFile = files.find(f => f.path.includes('ROADMAP'));
115
178
  if (roadmapFile) {
116
- context.roadmap = roadmapFile.content.substring(0, 2000); // First 2000 chars
179
+ context.roadmap = roadmapFile.content.substring(0, 2000);
117
180
  }
118
181
 
119
182
  // Get decisions
@@ -126,7 +189,7 @@ async function getProjectContext() {
126
189
  // Get current status
127
190
  const statusFile = files.find(f => f.path.includes('STATUS'));
128
191
  if (statusFile) {
129
- context.status = statusFile.content.substring(0, 1000); // First 1000 chars
192
+ context.status = statusFile.content.substring(0, 1000);
130
193
  }
131
194
  } catch {
132
195
  // Context loading failed, continue without it
@@ -163,7 +226,7 @@ async function processThoughts(args) {
163
226
  try {
164
227
  const files = await readdir(THOUGHTS_TODOS_DIR);
165
228
  filesToProcess = files
166
- .filter(f => f.endsWith('.md'))
229
+ .filter(f => f.endsWith('.md') && !f.startsWith('.'))
167
230
  .map(f => ({ name: f, path: join(THOUGHTS_TODOS_DIR, f) }));
168
231
  } catch {
169
232
  // Directory might not exist yet
@@ -227,6 +290,10 @@ Analyze the thought content below and create appropriate tasks. Consider:
227
290
  - tags: Relevant categorization
228
291
  - subtasks: Array of subtask strings (for consolidated items)
229
292
 
293
+ 5. **After creating tasks, archive the thought file** using \`archive_thought\`:
294
+ - Pass the filename and array of created task IDs
295
+ - This keeps the thoughts folder clean and maintains a processing log
296
+
230
297
  ---
231
298
 
232
299
  ## Thought Files to Process
@@ -275,6 +342,7 @@ Use this to understand what already exists and align new tasks appropriately.
275
342
  }
276
343
  }
277
344
 
345
+ const filenames = thoughtContents.map(t => t.filename).join('", "');
278
346
  result += `
279
347
  ---
280
348
 
@@ -285,6 +353,10 @@ Now analyze the thought content above and:
285
353
  1. Identify the distinct tasks/initiatives (consolidate related items)
286
354
  2. For each task, determine title, priority, and relevant context
287
355
  3. Use \`create_task\` to create well-structured tasks
356
+ 4. **After creating tasks, use \`archive_thought\`** to archive each processed file:
357
+ \`\`\`
358
+ archive_thought(file: "${thoughtContents[0]?.filename || 'filename.md'}", created_tasks: ["${project.toUpperCase()}-001", ...])
359
+ \`\`\`
288
360
 
289
361
  Remember: Quality over quantity. Create fewer, well-scoped tasks rather than many granular ones.
290
362
  `;
@@ -294,11 +366,171 @@ Remember: Quality over quantity. Create fewer, well-scoped tasks rather than man
294
366
  };
295
367
  }
296
368
 
369
+ /**
370
+ * Archive thought handler
371
+ */
372
+ async function archiveThought(args) {
373
+ const { file, created_tasks = [], notes } = args;
374
+
375
+ await ensureThoughtsTodosDir();
376
+ await ensureThoughtsArchiveDir();
377
+
378
+ const sourcePath = join(THOUGHTS_TODOS_DIR, file);
379
+ const archivePath = join(THOUGHTS_ARCHIVE_DIR, file);
380
+ const logPath = join(THOUGHTS_ARCHIVE_DIR, ARCHIVE_LOG_FILE);
381
+
382
+ // Check if file exists
383
+ if (!(await fileExists(sourcePath))) {
384
+ return {
385
+ content: [
386
+ {
387
+ type: 'text',
388
+ text: `❌ File not found: ${file}\n\nUse \`list_thoughts\` to see available files.`,
389
+ },
390
+ ],
391
+ isError: true,
392
+ };
393
+ }
394
+
395
+ // Read the original content for the log
396
+ const originalContent = await readFile(sourcePath, 'utf-8');
397
+ const lineCount = originalContent.split('\n').length;
398
+
399
+ // Move file to archive
400
+ await rename(sourcePath, archivePath);
401
+
402
+ // Update archive log
403
+ let logContent = '';
404
+ try {
405
+ if (await fileExists(logPath)) {
406
+ logContent = await readFile(logPath, 'utf-8');
407
+ }
408
+ } catch {
409
+ // Log file doesn't exist yet
410
+ }
411
+
412
+ // Create log entry
413
+ const logEntry = `
414
+ ## ${file}
415
+
416
+ **Archived:** ${getCurrentDate()}
417
+ **Original Lines:** ${lineCount}
418
+ **Tasks Created:** ${created_tasks.length > 0 ? created_tasks.join(', ') : 'None specified'}
419
+ ${notes ? `**Notes:** ${notes}` : ''}
420
+
421
+ ---
422
+ `;
423
+
424
+ // Prepend new entry to log (newest first)
425
+ if (!logContent.includes('# Thought Archive Log')) {
426
+ logContent = `# Thought Archive Log
427
+
428
+ This file tracks all processed thoughts and the tasks created from them.
429
+
430
+ ---
431
+ ${logEntry}`;
432
+ } else {
433
+ logContent = logContent.replace(
434
+ '---\n',
435
+ `---
436
+ ${logEntry}`
437
+ );
438
+ }
439
+
440
+ await writeFile(logPath, logContent, 'utf-8');
441
+
442
+ let result = `## Thought Archived: ${file}\n\n`;
443
+ result += `**Moved to:** \`.project/thoughts/todos/.archive/${file}\`\n`;
444
+ result += `**Archived:** ${getCurrentDate()}\n`;
445
+ result += `**Lines:** ${lineCount}\n`;
446
+
447
+ if (created_tasks.length > 0) {
448
+ result += `\n**Tasks Created:**\n`;
449
+ for (const taskId of created_tasks) {
450
+ result += `- ${taskId}\n`;
451
+ }
452
+ }
453
+
454
+ if (notes) {
455
+ result += `\n**Notes:** ${notes}\n`;
456
+ }
457
+
458
+ result += `\n✅ Thought file archived successfully. Use \`list_archived_thoughts\` to see archive history.`;
459
+
460
+ return {
461
+ content: [{ type: 'text', text: result }],
462
+ };
463
+ }
464
+
465
+ /**
466
+ * List archived thoughts handler
467
+ */
468
+ async function listArchivedThoughts(args) {
469
+ const { limit = 20 } = args || {};
470
+
471
+ await ensureThoughtsArchiveDir();
472
+
473
+ let result = `## Archived Thoughts\n\n`;
474
+ result += `**Path:** \`.project/thoughts/todos/.archive/\`\n\n`;
475
+
476
+ // Read archive log if exists
477
+ const logPath = join(THOUGHTS_ARCHIVE_DIR, ARCHIVE_LOG_FILE);
478
+ let logContent = '';
479
+
480
+ try {
481
+ if (await fileExists(logPath)) {
482
+ logContent = await readFile(logPath, 'utf-8');
483
+ }
484
+ } catch {
485
+ // Log doesn't exist
486
+ }
487
+
488
+ if (logContent) {
489
+ result += `### Archive Log\n\n`;
490
+ // Show the log content (it's already formatted)
491
+ const entries = logContent.split('## ').slice(1, limit + 1);
492
+ for (const entry of entries) {
493
+ result += `## ${entry}\n`;
494
+ }
495
+ }
496
+
497
+ // Also list actual files in archive
498
+ try {
499
+ const files = await readdir(THOUGHTS_ARCHIVE_DIR);
500
+ const mdFiles = files.filter(f => f.endsWith('.md') && f !== ARCHIVE_LOG_FILE);
501
+
502
+ if (mdFiles.length > 0) {
503
+ result += `\n### Archived Files (${mdFiles.length})\n\n`;
504
+ result += `| File | Size |\n`;
505
+ result += `|------|------|\n`;
506
+ for (const file of mdFiles.slice(0, limit)) {
507
+ const filePath = join(THOUGHTS_ARCHIVE_DIR, file);
508
+ const content = await readFile(filePath, 'utf-8');
509
+ const lines = content.split('\n').length;
510
+ result += `| ${file} | ${lines} lines |\n`;
511
+ }
512
+ }
513
+ } catch {
514
+ // Archive directory might not exist
515
+ }
516
+
517
+ if (!logContent && result.includes('Archived Files') === false) {
518
+ result += `*No archived thoughts yet. Use \`archive_thought\` after processing thoughts.*\n`;
519
+ }
520
+
521
+ result += `\n---\n`;
522
+ result += `**Tools:** \`get_thought\` with \`from_archive: true\` to read archived files`;
523
+
524
+ return {
525
+ content: [{ type: 'text', text: result }],
526
+ };
527
+ }
528
+
297
529
  /**
298
530
  * List thoughts handler
299
531
  */
300
532
  async function listThoughts(args) {
301
- const { category } = args || {};
533
+ const { category, include_archived = false } = args || {};
302
534
 
303
535
  await ensureThoughtsTodosDir();
304
536
 
@@ -311,7 +543,7 @@ async function listThoughts(args) {
311
543
  try {
312
544
  if (await fileExists(catDir)) {
313
545
  const files = await readdir(catDir);
314
- const mdFiles = files.filter(f => f.endsWith('.md'));
546
+ const mdFiles = files.filter(f => f.endsWith('.md') && !f.startsWith('.'));
315
547
 
316
548
  results[cat] = [];
317
549
  for (const file of mdFiles) {
@@ -353,9 +585,24 @@ async function listThoughts(args) {
353
585
  }
354
586
  }
355
587
 
588
+ // Show archived count if requested
589
+ if (include_archived) {
590
+ try {
591
+ await ensureThoughtsArchiveDir();
592
+ const archivedFiles = await readdir(THOUGHTS_ARCHIVE_DIR);
593
+ const archivedMd = archivedFiles.filter(f => f.endsWith('.md') && f !== ARCHIVE_LOG_FILE);
594
+ if (archivedMd.length > 0) {
595
+ result += `### 📦 .archive/ (${archivedMd.length} files)\n\n`;
596
+ result += `Use \`list_archived_thoughts\` to see details.\n\n`;
597
+ }
598
+ } catch {
599
+ // Archive doesn't exist
600
+ }
601
+ }
602
+
356
603
  result += `---\n`;
357
- result += `**Total files:** ${totalFiles}\n\n`;
358
- result += `**Tools:** \`get_thought\` to read | \`process_thoughts\` to analyze`;
604
+ result += `**Total active files:** ${totalFiles}\n\n`;
605
+ result += `**Tools:** \`get_thought\` to read | \`process_thoughts\` to analyze | \`archive_thought\` after processing`;
359
606
 
360
607
  return {
361
608
  content: [{ type: 'text', text: result }],
@@ -366,19 +613,27 @@ async function listThoughts(args) {
366
613
  * Get thought handler
367
614
  */
368
615
  async function getThought(args) {
369
- const { file, category = 'todos' } = args;
616
+ const { file, category = 'todos', from_archive = false } = args;
370
617
 
371
618
  await ensureThoughtsTodosDir();
372
619
 
373
- const catDir = category === 'todos' ? THOUGHTS_TODOS_DIR : join(THOUGHTS_TODOS_DIR, '..', category);
620
+ let catDir;
621
+ if (from_archive) {
622
+ await ensureThoughtsArchiveDir();
623
+ catDir = THOUGHTS_ARCHIVE_DIR;
624
+ } else {
625
+ catDir = category === 'todos' ? THOUGHTS_TODOS_DIR : join(THOUGHTS_TODOS_DIR, '..', category);
626
+ }
627
+
374
628
  const filePath = join(catDir, file);
375
629
 
376
630
  if (!(await fileExists(filePath))) {
631
+ const location = from_archive ? '.archive' : category;
377
632
  return {
378
633
  content: [
379
634
  {
380
635
  type: 'text',
381
- text: `❌ File not found: \`.project/thoughts/${category}/${file}\`\n\nUse \`list_thoughts\` to see available files.`,
636
+ text: `❌ File not found: \`.project/thoughts/todos/${location}/${file}\`\n\nUse \`list_thoughts\` or \`list_archived_thoughts\` to see available files.`,
382
637
  },
383
638
  ],
384
639
  isError: true,
@@ -388,9 +643,10 @@ async function getThought(args) {
388
643
  const content = await readFile(filePath, 'utf-8');
389
644
  const parsed = matter(content);
390
645
 
646
+ const location = from_archive ? 'todos/.archive' : category;
391
647
  let result = `## Thought File: ${file}\n\n`;
392
- result += `**Path:** \`.project/thoughts/${category}/${file}\`\n`;
393
- result += `**Category:** ${category}\n\n`;
648
+ result += `**Path:** \`.project/thoughts/${location}/${file}\`\n`;
649
+ result += `**Status:** ${from_archive ? '📦 Archived' : '📝 Active'}\n\n`;
394
650
 
395
651
  if (Object.keys(parsed.data).length > 0) {
396
652
  result += `### Frontmatter\n\n`;
@@ -405,7 +661,11 @@ async function getThought(args) {
405
661
  result += parsed.content;
406
662
 
407
663
  result += `\n\n---\n`;
408
- result += `**Tools:** \`process_thoughts\` to analyze and create tasks`;
664
+ if (from_archive) {
665
+ result += `**This file has been archived.** It was already processed into tasks.`;
666
+ } else {
667
+ result += `**Tools:** \`process_thoughts\` to analyze | \`archive_thought\` after creating tasks`;
668
+ }
409
669
 
410
670
  return {
411
671
  content: [{ type: 'text', text: result }],
@@ -417,6 +677,8 @@ async function getThought(args) {
417
677
  */
418
678
  export const handlers = {
419
679
  process_thoughts: processThoughts,
680
+ archive_thought: archiveThought,
420
681
  list_thoughts: listThoughts,
682
+ list_archived_thoughts: listArchivedThoughts,
421
683
  get_thought: getThought,
422
684
  };