project-mcp 3.2.1 → 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.1",
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,38 +1,50 @@
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
- * This tool analyzes brain dump markdown files from .project/thoughts/todos/
6
- * and transforms them into properly structured YAML tasks.
5
+ * This tool reads brain dump files and provides context for the LLM to analyze.
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, PRIORITY_KEYWORDS, VALID_PRIORITIES } from '../lib/constants.js';
11
- import { readFile, writeFile, join, fileExists, ensureThoughtsTodosDir, matter } from '../lib/files.js';
12
- import { getCurrentDate, getISODate } from '../lib/dates.js';
13
- import { loadAllTasks, getNextTaskId, getExistingTaskIds } from '../lib/tasks.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';
22
+ import { loadAllTasks } from '../lib/tasks.js';
14
23
  import { loadAllFiles, getCachedFiles } from '../lib/search.js';
15
24
 
25
+ const ARCHIVE_LOG_FILE = '.archive-log.md';
26
+
16
27
  /**
17
28
  * Tool definitions
18
29
  */
19
30
  export const definitions = [
20
31
  {
21
32
  name: 'process_thoughts',
22
- description: `Analyzes brain dump markdown files from .project/thoughts/todos/ and transforms them into structured YAML tasks. This tool is intelligent and understands:
33
+ description: `Reads brain dump markdown files from .project/thoughts/todos/ and returns the content along with project context for analysis.
23
34
 
24
- 1. **Explicit Intent** - What the user literally says they want to do
25
- 2. **Shadow Intent** - Implied goals, underlying motivations, hidden needs
26
- 3. **Practical Intent** - What actually needs to happen in concrete terms
35
+ This tool gathers:
36
+ 1. **Raw thought content** - The unstructured brain dump as written
37
+ 2. **Project context** - Existing tasks, roadmap milestones, decisions for reference
38
+ 3. **Task format guide** - The YAML structure for creating tasks
27
39
 
28
- The tool will:
29
- - Parse unstructured markdown brain dumps
30
- - Extract actionable tasks from free-form text
31
- - Infer priority from context, urgency words, and project alignment
32
- - Cross-reference with project docs (ROADMAP, existing tasks, decisions) for context
33
- - Output properly formatted YAML tasks ready for creation
40
+ YOU (the LLM) should then analyze the content to:
41
+ - Understand the user's intent (explicit, shadow/underlying, practical)
42
+ - Identify logical task groupings (consolidate related items)
43
+ - Determine appropriate priorities based on context
44
+ - Create well-structured tasks using create_task
45
+ - **After creating tasks, use archive_thought to archive the processed file**
34
46
 
35
- Use this when you have messy notes or brain dumps that need to be converted into actionable, tracked tasks.`,
47
+ The tool does NOT automatically create tasks - it provides you with everything needed to make intelligent decisions about task creation.`,
36
48
  inputSchema: {
37
49
  type: 'object',
38
50
  properties: {
@@ -43,63 +55,92 @@ Use this when you have messy notes or brain dumps that need to be converted into
43
55
  },
44
56
  project: {
45
57
  type: 'string',
46
- description:
47
- 'Project prefix for generated task IDs (e.g., "AUTH", "API"). Required for task creation.',
58
+ description: 'Project prefix for task IDs when you create tasks (e.g., "AUTH", "API").',
48
59
  },
49
- mode: {
60
+ },
61
+ required: ['project'],
62
+ },
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: {
50
73
  type: 'string',
51
- description:
52
- 'Processing mode: "analyze" (returns analysis without creating tasks), "create" (creates tasks directly), "preview" (shows what would be created). Default: "analyze".',
53
- enum: ['analyze', 'create', 'preview'],
54
- default: 'analyze',
74
+ description: 'The thought file to archive (e.g., "my-ideas.md").',
55
75
  },
56
- default_owner: {
57
- type: 'string',
58
- description: 'Default owner for generated tasks. Default: "unassigned".',
59
- default: 'unassigned',
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"]).',
60
80
  },
61
- include_context: {
62
- type: 'boolean',
63
- description:
64
- 'Include project context analysis (searches project docs for relevant info). Default: true.',
65
- default: true,
81
+ notes: {
82
+ type: 'string',
83
+ description: 'Optional notes about the processing (e.g., "Consolidated 5 items into 2 tasks").',
66
84
  },
67
85
  },
68
- required: ['project'],
86
+ required: ['file'],
69
87
  },
70
88
  },
71
89
  {
72
90
  name: 'list_thoughts',
73
91
  description:
74
- 'Lists all thought files in the .project/thoughts/ directory structure. Shows available brain dump files organized by category (todos, etc.).',
92
+ 'Lists all thought files in the .project/thoughts/ directory structure. Shows available brain dump files organized by category.',
75
93
  inputSchema: {
76
94
  type: 'object',
77
95
  properties: {
78
96
  category: {
79
97
  type: 'string',
80
- description:
81
- 'Optional: Filter by thought category. Currently supported: "todos". More categories coming in the future.',
98
+ description: 'Optional: Filter by thought category. Currently supported: "todos".',
82
99
  enum: ['todos', ''],
83
100
  },
101
+ include_archived: {
102
+ type: 'boolean',
103
+ description: 'Include archived thoughts in the listing. Default: false.',
104
+ default: false,
105
+ },
84
106
  },
85
107
  },
86
108
  },
87
109
  {
88
- name: 'get_thought',
110
+ name: 'list_archived_thoughts',
89
111
  description:
90
- 'Reads a specific thought file and returns its raw content. Use this to review a brain dump before processing.',
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
+ },
121
+ },
122
+ },
123
+ },
124
+ {
125
+ name: 'get_thought',
126
+ description: 'Reads a specific thought file and returns its raw content for review.',
91
127
  inputSchema: {
92
128
  type: 'object',
93
129
  properties: {
94
130
  file: {
95
131
  type: 'string',
96
- description: 'The thought file to read (e.g., "my-ideas.md"). Can be in any thoughts subdirectory.',
132
+ description: 'The thought file to read (e.g., "my-ideas.md").',
97
133
  },
98
134
  category: {
99
135
  type: 'string',
100
136
  description: 'The category/subdirectory. Default: "todos".',
101
137
  default: 'todos',
102
138
  },
139
+ from_archive: {
140
+ type: 'boolean',
141
+ description: 'Read from archive instead of active thoughts. Default: false.',
142
+ default: false,
143
+ },
103
144
  },
104
145
  required: ['file'],
105
146
  },
@@ -107,256 +148,14 @@ Use this when you have messy notes or brain dumps that need to be converted into
107
148
  ];
108
149
 
109
150
  /**
110
- * Intent analysis types
111
- */
112
- const INTENT_MARKERS = {
113
- // Explicit intent markers - direct statements of what to do
114
- explicit: [
115
- /\b(need to|have to|must|should|will|going to|want to|plan to)\b/i,
116
- /\b(implement|create|build|add|fix|update|change|remove|delete)\b/i,
117
- /\b(task|todo|action item|deliverable)\b/i,
118
- ],
119
- // Shadow intent markers - underlying motivations
120
- shadow: [
121
- /\b(because|since|so that|in order to|to enable|to allow|to prevent)\b/i,
122
- /\b(worried about|concerned|frustrated|annoying|painful|tedious)\b/i,
123
- /\b(would be nice|could|might|maybe|possibly|eventually)\b/i,
124
- /\b(users|customers|team|stakeholders)\s+(want|need|expect|complain)/i,
125
- ],
126
- // Practical intent markers - concrete actions
127
- practical: [
128
- /\b(step \d+|first|then|next|finally|after that)\b/i,
129
- /\b(file|function|class|module|component|api|endpoint|database)\b/i,
130
- /\b(test|deploy|configure|setup|install|migrate)\b/i,
131
- ],
132
- // Urgency markers
133
- urgency: {
134
- P0: [/\b(critical|blocker|urgent|asap|immediately|breaking|down|outage)\b/i],
135
- P1: [/\b(important|high priority|soon|this week|pressing|significant)\b/i],
136
- P2: [/\b(medium|normal|standard|regular|when possible)\b/i],
137
- P3: [/\b(low priority|nice to have|eventually|someday|minor|trivial)\b/i],
138
- },
139
- };
140
-
141
- /**
142
- * Extract todos from unstructured markdown content
143
- * @param {string} content - Raw markdown content
144
- * @returns {Array} Extracted todo items with metadata
145
- */
146
- function extractTodosFromContent(content) {
147
- const todos = [];
148
- const lines = content.split('\n');
149
-
150
- let currentContext = [];
151
- let currentSection = null;
152
-
153
- for (let i = 0; i < lines.length; i++) {
154
- const line = lines[i];
155
- const trimmed = line.trim();
156
-
157
- // Track section headers for context
158
- const headerMatch = trimmed.match(/^(#{1,6})\s+(.+)/);
159
- if (headerMatch) {
160
- currentSection = headerMatch[2];
161
- currentContext = [];
162
- continue;
163
- }
164
-
165
- // Skip empty lines but reset context after multiple empties
166
- if (!trimmed) {
167
- if (currentContext.length > 0 && lines[i - 1]?.trim() === '') {
168
- currentContext = [];
169
- }
170
- continue;
171
- }
172
-
173
- // Check for explicit todo markers
174
- const todoMatch =
175
- trimmed.match(/^[-*]\s*\[[ x]\]\s*(.+)/) ||
176
- trimmed.match(/^[-*]\s+(.+)/) ||
177
- trimmed.match(/^(\d+)\.\s+(.+)/);
178
-
179
- if (todoMatch) {
180
- const text = todoMatch[2] || todoMatch[1];
181
- if (text && text.length >= 5) {
182
- todos.push({
183
- raw: text.trim(),
184
- section: currentSection,
185
- context: [...currentContext],
186
- lineNumber: i + 1,
187
- isExplicitTodo: /^\[[ x]\]/.test(trimmed),
188
- });
189
- }
190
- } else if (hasActionableIntent(trimmed)) {
191
- // Lines with strong action intent even without list markers
192
- todos.push({
193
- raw: trimmed,
194
- section: currentSection,
195
- context: [...currentContext],
196
- lineNumber: i + 1,
197
- isExplicitTodo: false,
198
- });
199
- } else {
200
- // Add to context for following items
201
- currentContext.push(trimmed);
202
- if (currentContext.length > 3) {
203
- currentContext.shift();
204
- }
205
- }
206
- }
207
-
208
- return todos;
209
- }
210
-
211
- /**
212
- * Check if a line has actionable intent
213
- * @param {string} line - Line to check
214
- * @returns {boolean}
215
- */
216
- function hasActionableIntent(line) {
217
- // Must have at least one explicit intent marker
218
- const hasExplicit = INTENT_MARKERS.explicit.some(rx => rx.test(line));
219
- if (!hasExplicit) return false;
220
-
221
- // Must be long enough to be meaningful
222
- if (line.length < 15) return false;
223
-
224
- // Must not be a question or observation
225
- if (/^(what|how|why|when|where|who|is|are|was|were|do|does)\b/i.test(line)) return false;
226
- if (line.endsWith('?')) return false;
227
-
228
- return true;
229
- }
230
-
231
- /**
232
- * Analyze intent layers for a todo item
233
- * @param {object} todo - Todo item with raw text and context
234
- * @returns {object} Intent analysis
235
- */
236
- function analyzeIntent(todo) {
237
- const text = todo.raw;
238
- const context = todo.context.join(' ');
239
- const combined = `${text} ${context}`;
240
-
241
- const analysis = {
242
- explicit: null,
243
- shadow: null,
244
- practical: null,
245
- priority: 'P2',
246
- confidence: 0,
247
- tags: [],
248
- };
249
-
250
- // Extract explicit intent - what they say they want
251
- analysis.explicit = text;
252
-
253
- // Extract shadow intent - why they want it
254
- const shadowMatches = [];
255
- INTENT_MARKERS.shadow.forEach(rx => {
256
- const match = combined.match(rx);
257
- if (match) {
258
- // Get surrounding context
259
- const idx = combined.indexOf(match[0]);
260
- const start = Math.max(0, idx - 20);
261
- const end = Math.min(combined.length, idx + match[0].length + 50);
262
- shadowMatches.push(combined.substring(start, end).trim());
263
- }
264
- });
265
- if (shadowMatches.length > 0) {
266
- analysis.shadow = shadowMatches.join('; ');
267
- }
268
-
269
- // Extract practical intent - concrete actions
270
- const practicalMatches = [];
271
- INTENT_MARKERS.practical.forEach(rx => {
272
- if (rx.test(combined)) {
273
- practicalMatches.push(rx.source.replace(/\\b|\(|\)/g, ''));
274
- }
275
- });
276
- if (practicalMatches.length > 0) {
277
- analysis.practical = `Involves: ${practicalMatches.join(', ')}`;
278
- }
279
-
280
- // Determine priority from urgency markers
281
- for (const [priority, patterns] of Object.entries(INTENT_MARKERS.urgency)) {
282
- if (patterns.some(rx => rx.test(combined))) {
283
- analysis.priority = priority;
284
- break;
285
- }
286
- }
287
-
288
- // Also check keyword-based priority
289
- const textLower = combined.toLowerCase();
290
- for (const [keyword, pri] of Object.entries(PRIORITY_KEYWORDS)) {
291
- if (textLower.includes(keyword)) {
292
- // Only upgrade priority, don't downgrade
293
- const currentOrder = { P0: 0, P1: 1, P2: 2, P3: 3 };
294
- if (currentOrder[pri] < currentOrder[analysis.priority]) {
295
- analysis.priority = pri;
296
- }
297
- break;
298
- }
299
- }
300
-
301
- // Extract potential tags from brackets or hashtags
302
- const tagMatches = text.match(/\[([^\]]+)\]/g) || [];
303
- const hashTags = text.match(/#(\w+)/g) || [];
304
- analysis.tags = [
305
- ...tagMatches.map(t => t.slice(1, -1).toLowerCase()),
306
- ...hashTags.map(t => t.slice(1).toLowerCase()),
307
- ];
308
-
309
- // Calculate confidence based on markers found
310
- let confidence = 0;
311
- if (todo.isExplicitTodo) confidence += 40;
312
- if (INTENT_MARKERS.explicit.some(rx => rx.test(text))) confidence += 30;
313
- if (analysis.shadow) confidence += 15;
314
- if (analysis.practical) confidence += 15;
315
- analysis.confidence = Math.min(100, confidence);
316
-
317
- return analysis;
318
- }
319
-
320
- /**
321
- * Generate a clean title from raw todo text
322
- * @param {string} raw - Raw todo text
323
- * @returns {string} Clean title
324
- */
325
- function generateTitle(raw) {
326
- let title = raw
327
- // Remove checkbox markers
328
- .replace(/^\[[ x]\]\s*/, '')
329
- // Remove tag brackets
330
- .replace(/\[[^\]]+\]/g, '')
331
- // Remove hashtags
332
- .replace(/#\w+/g, '')
333
- // Remove leading action words that are too generic
334
- .replace(/^(need to|have to|must|should|will|want to)\s+/i, '')
335
- // Clean up whitespace
336
- .replace(/\s+/g, ' ')
337
- .trim();
338
-
339
- // Capitalize first letter
340
- title = title.charAt(0).toUpperCase() + title.slice(1);
341
-
342
- // Truncate if too long
343
- if (title.length > 80) {
344
- title = title.substring(0, 77) + '...';
345
- }
346
-
347
- return title;
348
- }
349
-
350
- /**
351
- * Get project context from existing docs and tasks
352
- * @returns {Promise<object>} Project context summary
151
+ * Get project context for the LLM
353
152
  */
354
153
  async function getProjectContext() {
355
154
  const context = {
356
155
  existingTasks: [],
357
- roadmapItems: [],
156
+ roadmap: null,
358
157
  decisions: [],
359
- summary: '',
158
+ status: null,
360
159
  };
361
160
 
362
161
  try {
@@ -367,83 +166,43 @@ async function getProjectContext() {
367
166
  title: t.title,
368
167
  status: t.status,
369
168
  priority: t.priority,
169
+ tags: t.tags || [],
370
170
  }));
371
171
 
372
- // Load project files for context
172
+ // Load project files
373
173
  await loadAllFiles();
374
174
  const files = getCachedFiles();
375
175
 
376
- // Find roadmap items
176
+ // Get roadmap content
377
177
  const roadmapFile = files.find(f => f.path.includes('ROADMAP'));
378
178
  if (roadmapFile) {
379
- const milestones = roadmapFile.content.match(/##\s+[^#\n]+/g) || [];
380
- context.roadmapItems = milestones.map(m => m.replace('##', '').trim());
179
+ context.roadmap = roadmapFile.content.substring(0, 2000);
381
180
  }
382
181
 
383
- // Find decisions
182
+ // Get decisions
384
183
  const decisionsFile = files.find(f => f.path.includes('DECISIONS'));
385
184
  if (decisionsFile) {
386
185
  const adrs = decisionsFile.content.match(/## ADR-\d+: [^\n]+/g) || [];
387
186
  context.decisions = adrs.map(a => a.replace('## ', ''));
388
187
  }
389
188
 
390
- // Build summary
391
- const parts = [];
392
- if (context.existingTasks.length > 0) {
393
- const inProgress = context.existingTasks.filter(t => t.status === 'in_progress');
394
- const todoCount = context.existingTasks.filter(t => t.status === 'todo').length;
395
- parts.push(
396
- `${context.existingTasks.length} existing tasks (${inProgress.length} in progress, ${todoCount} todo)`
397
- );
398
- }
399
- if (context.roadmapItems.length > 0) {
400
- parts.push(`${context.roadmapItems.length} roadmap milestones`);
401
- }
402
- if (context.decisions.length > 0) {
403
- parts.push(`${context.decisions.length} architecture decisions`);
189
+ // Get current status
190
+ const statusFile = files.find(f => f.path.includes('STATUS'));
191
+ if (statusFile) {
192
+ context.status = statusFile.content.substring(0, 1000);
404
193
  }
405
- context.summary = parts.join(', ') || 'No existing project context found';
406
- } catch (error) {
407
- context.summary = 'Unable to load project context';
194
+ } catch {
195
+ // Context loading failed, continue without it
408
196
  }
409
197
 
410
198
  return context;
411
199
  }
412
200
 
413
201
  /**
414
- * Check for potential duplicates or related tasks
415
- * @param {string} title - Task title to check
416
- * @param {Array} existingTasks - Existing tasks
417
- * @returns {Array} Related tasks
418
- */
419
- function findRelatedTasks(title, existingTasks) {
420
- const titleWords = title
421
- .toLowerCase()
422
- .split(/\s+/)
423
- .filter(w => w.length > 3);
424
- const related = [];
425
-
426
- for (const task of existingTasks) {
427
- const taskTitle = task.title.toLowerCase();
428
- const matchingWords = titleWords.filter(w => taskTitle.includes(w));
429
- if (matchingWords.length >= 2 || matchingWords.length / titleWords.length > 0.5) {
430
- related.push({
431
- id: task.id,
432
- title: task.title,
433
- status: task.status,
434
- similarity: matchingWords.length / titleWords.length,
435
- });
436
- }
437
- }
438
-
439
- return related.sort((a, b) => b.similarity - a.similarity).slice(0, 3);
440
- }
441
-
442
- /**
443
- * Process thoughts handler
202
+ * Process thoughts handler - returns data for LLM analysis
444
203
  */
445
204
  async function processThoughts(args) {
446
- const { file, project, mode = 'analyze', default_owner = 'unassigned', include_context = true } = args;
205
+ const { file, project } = args;
447
206
 
448
207
  await ensureThoughtsTodosDir();
449
208
 
@@ -467,7 +226,7 @@ async function processThoughts(args) {
467
226
  try {
468
227
  const files = await readdir(THOUGHTS_TODOS_DIR);
469
228
  filesToProcess = files
470
- .filter(f => f.endsWith('.md'))
229
+ .filter(f => f.endsWith('.md') && !f.startsWith('.'))
471
230
  .map(f => ({ name: f, path: join(THOUGHTS_TODOS_DIR, f) }));
472
231
  } catch {
473
232
  // Directory might not exist yet
@@ -479,229 +238,299 @@ async function processThoughts(args) {
479
238
  content: [
480
239
  {
481
240
  type: 'text',
482
- text: `⚠️ No thought files found in \`.project/thoughts/todos/\`\n\nCreate markdown files with your brain dumps, then run this tool to process them into structured tasks.`,
241
+ text: `⚠️ No thought files found in \`.project/thoughts/todos/\`\n\nCreate markdown files with your brain dumps, then run this tool to process them.`,
483
242
  },
484
243
  ],
485
244
  };
486
245
  }
487
246
 
488
- // Get project context if requested
489
- let projectContext = null;
490
- if (include_context) {
491
- projectContext = await getProjectContext();
492
- }
493
-
494
- // Process each file
495
- const allAnalyzedTodos = [];
496
- const processedFiles = [];
497
-
247
+ // Read all thought files
248
+ const thoughtContents = [];
498
249
  for (const thoughtFile of filesToProcess) {
499
250
  const content = await readFile(thoughtFile.path, 'utf-8');
500
251
  const parsed = matter(content);
501
- const rawContent = parsed.content;
502
-
503
- // Extract todos from content
504
- const extractedTodos = extractTodosFromContent(rawContent);
505
-
506
- // Analyze each todo
507
- for (const todo of extractedTodos) {
508
- const intent = analyzeIntent(todo);
509
- const title = generateTitle(todo.raw);
510
-
511
- // Skip if too low confidence
512
- if (intent.confidence < 30) continue;
513
-
514
- // Find related existing tasks
515
- const related = projectContext ? findRelatedTasks(title, projectContext.existingTasks) : [];
516
-
517
- allAnalyzedTodos.push({
518
- sourceFile: thoughtFile.name,
519
- lineNumber: todo.lineNumber,
520
- section: todo.section,
521
- raw: todo.raw,
522
- title,
523
- intent,
524
- related,
525
- taskData: {
526
- title,
527
- project: project.toUpperCase(),
528
- priority: intent.priority,
529
- status: 'todo',
530
- owner: default_owner,
531
- tags: intent.tags,
532
- description: buildDescription(todo, intent),
533
- },
534
- });
535
- }
536
-
537
- processedFiles.push({
538
- name: thoughtFile.name,
539
- todosFound: extractedTodos.length,
540
- todosKept: allAnalyzedTodos.filter(t => t.sourceFile === thoughtFile.name).length,
252
+ thoughtContents.push({
253
+ filename: thoughtFile.name,
254
+ content: parsed.content,
255
+ frontmatter: parsed.data,
541
256
  });
542
257
  }
543
258
 
544
- // Build result based on mode
545
- let result = `## Thought Processing Results\n\n`;
546
- result += `**Mode:** ${mode}\n`;
547
- result += `**Project:** ${project.toUpperCase()}\n`;
548
- result += `**Files Processed:** ${processedFiles.length}\n`;
549
- result += `**Todos Extracted:** ${allAnalyzedTodos.length}\n`;
259
+ // Get project context
260
+ const projectContext = await getProjectContext();
550
261
 
551
- if (projectContext) {
552
- result += `\n### Project Context\n\n${projectContext.summary}\n`;
553
- }
262
+ // Build the response for the LLM
263
+ let result = `# Thought Processing Data
554
264
 
555
- result += `\n### Processed Files\n\n`;
556
- for (const pf of processedFiles) {
557
- result += `- **${pf.name}**: ${pf.todosFound} items found, ${pf.todosKept} actionable\n`;
558
- }
265
+ ## Instructions for You (the LLM)
559
266
 
560
- if (allAnalyzedTodos.length === 0) {
561
- result += `\n⚠️ No actionable todos found. The content may not contain clear task items, or confidence was too low.\n`;
562
- result += `\n**Tips:**\n`;
563
- result += `- Use checkbox syntax: \`- [ ] Task description\`\n`;
564
- result += `- Include action verbs: implement, create, fix, add, update\n`;
565
- result += `- Add urgency markers: critical, urgent, important, soon\n`;
267
+ Analyze the thought content below and create appropriate tasks. Consider:
566
268
 
567
- return {
568
- content: [{ type: 'text', text: result }],
569
- };
269
+ 1. **Intent Analysis**
270
+ - **Explicit intent**: What does the user literally say they want?
271
+ - **Shadow intent**: What's the underlying motivation? Why do they want this?
272
+ - **Practical intent**: What concrete actions are needed?
273
+
274
+ 2. **Task Consolidation**
275
+ - Group related items into single tasks with subtasks
276
+ - Don't create a separate task for every bullet point
277
+ - Section headers often indicate a logical task grouping
278
+
279
+ 3. **Priority Assessment**
280
+ - P0: Critical/blocker/urgent - system down, security issue
281
+ - P1: High priority - important, needed soon
282
+ - P2: Medium (default) - normal work items
283
+ - P3: Low - nice to have, eventually
284
+
285
+ 4. **Use \`create_task\` to create tasks** with this structure:
286
+ - title: Clear, actionable task title
287
+ - project: "${project.toUpperCase()}"
288
+ - description: Include context and subtasks
289
+ - priority: P0-P3 based on your analysis
290
+ - tags: Relevant categorization
291
+ - subtasks: Array of subtask strings (for consolidated items)
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
+
297
+ ---
298
+
299
+ ## Thought Files to Process
300
+
301
+ `;
302
+
303
+ for (const thought of thoughtContents) {
304
+ result += `### File: ${thought.filename}\n\n`;
305
+ result += '```markdown\n';
306
+ result += thought.content;
307
+ result += '\n```\n\n';
570
308
  }
571
309
 
572
- result += `\n---\n\n## Extracted Todos\n\n`;
310
+ result += `---
573
311
 
574
- const createdTasks = [];
312
+ ## Project Context
575
313
 
576
- for (let i = 0; i < allAnalyzedTodos.length; i++) {
577
- const todo = allAnalyzedTodos[i];
314
+ Use this to understand what already exists and align new tasks appropriately.
578
315
 
579
- result += `### ${i + 1}. ${todo.title}\n\n`;
580
- result += `**Source:** \`${todo.sourceFile}\` (line ${todo.lineNumber})\n`;
581
- result += `**Priority:** ${todo.intent.priority} (confidence: ${todo.intent.confidence}%)\n`;
316
+ ### Existing Tasks (${projectContext.existingTasks.length} total)
582
317
 
583
- if (todo.section) {
584
- result += `**Section:** ${todo.section}\n`;
585
- }
318
+ `;
586
319
 
587
- result += `\n**Intent Analysis:**\n`;
588
- result += `- **Explicit:** ${todo.intent.explicit}\n`;
589
- if (todo.intent.shadow) {
590
- result += `- **Shadow (why):** ${todo.intent.shadow}\n`;
320
+ if (projectContext.existingTasks.length > 0) {
321
+ result += '| ID | Title | Status | Priority |\n';
322
+ result += '|----|-------|--------|----------|\n';
323
+ for (const task of projectContext.existingTasks.slice(0, 20)) {
324
+ result += `| ${task.id} | ${task.title.substring(0, 40)}${task.title.length > 40 ? '...' : ''} | ${task.status} | ${task.priority} |\n`;
591
325
  }
592
- if (todo.intent.practical) {
593
- result += `- **Practical:** ${todo.intent.practical}\n`;
326
+ if (projectContext.existingTasks.length > 20) {
327
+ result += `\n*...and ${projectContext.existingTasks.length - 20} more tasks*\n`;
594
328
  }
329
+ } else {
330
+ result += '*No existing tasks*\n';
331
+ }
595
332
 
596
- if (todo.intent.tags.length > 0) {
597
- result += `- **Tags:** ${todo.intent.tags.join(', ')}\n`;
598
- }
333
+ if (projectContext.roadmap) {
334
+ result += `\n### Roadmap Overview\n\n`;
335
+ result += '```\n' + projectContext.roadmap + '\n```\n';
336
+ }
599
337
 
600
- if (todo.related.length > 0) {
601
- result += `\n**⚠️ Potentially Related Tasks:**\n`;
602
- for (const rel of todo.related) {
603
- result += `- ${rel.id}: ${rel.title} (${rel.status})\n`;
604
- }
338
+ if (projectContext.decisions.length > 0) {
339
+ result += `\n### Architecture Decisions\n\n`;
340
+ for (const decision of projectContext.decisions) {
341
+ result += `- ${decision}\n`;
605
342
  }
343
+ }
606
344
 
607
- // Show YAML preview
608
- result += `\n**Generated YAML:**\n`;
609
- result += `\`\`\`yaml\n`;
610
- result += `title: "${todo.taskData.title}"\n`;
611
- result += `project: ${todo.taskData.project}\n`;
612
- result += `priority: ${todo.taskData.priority}\n`;
613
- result += `status: ${todo.taskData.status}\n`;
614
- result += `owner: ${todo.taskData.owner}\n`;
615
- if (todo.taskData.tags.length > 0) {
616
- result += `tags: [${todo.taskData.tags.join(', ')}]\n`;
617
- }
618
- result += `\`\`\`\n\n`;
345
+ const filenames = thoughtContents.map(t => t.filename).join('", "');
346
+ result += `
347
+ ---
619
348
 
620
- // Create tasks in create mode
621
- if (mode === 'create') {
622
- try {
623
- // Import create_task handler dynamically to avoid circular deps
624
- const { handlers: taskHandlers } = await import('./tasks.js');
625
- const createResult = await taskHandlers.create_task({
626
- title: todo.taskData.title,
627
- project: todo.taskData.project,
628
- description: todo.taskData.description,
629
- owner: todo.taskData.owner,
630
- priority: todo.taskData.priority,
631
- tags: todo.taskData.tags,
632
- });
633
-
634
- // Extract task ID from result
635
- const idMatch = createResult.content[0].text.match(/\*\*([A-Z]+-\d+)\*\*/);
636
- if (idMatch) {
637
- createdTasks.push({
638
- id: idMatch[1],
639
- title: todo.taskData.title,
640
- });
641
- result += `✅ **Created:** ${idMatch[1]}\n\n`;
642
- }
643
- } catch (error) {
644
- result += `❌ **Failed to create:** ${error.message}\n\n`;
645
- }
349
+ ## Your Task
350
+
351
+ Now analyze the thought content above and:
352
+
353
+ 1. Identify the distinct tasks/initiatives (consolidate related items)
354
+ 2. For each task, determine title, priority, and relevant context
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
+ \`\`\`
360
+
361
+ Remember: Quality over quantity. Create fewer, well-scoped tasks rather than many granular ones.
362
+ `;
363
+
364
+ return {
365
+ content: [{ type: 'text', text: result }],
366
+ };
367
+ }
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');
646
407
  }
408
+ } catch {
409
+ // Log file doesn't exist yet
410
+ }
647
411
 
648
- result += `---\n\n`;
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
+ );
649
438
  }
650
439
 
651
- // Summary footer
652
- if (mode === 'analyze') {
653
- result += `\n## Next Steps\n\n`;
654
- result += `1. Review the extracted todos above\n`;
655
- result += `2. Run with \`mode: "preview"\` to see final task format\n`;
656
- result += `3. Run with \`mode: "create"\` to create the tasks\n`;
657
- result += `\nOr use \`create_task\` manually for more control over individual tasks.\n`;
658
- } else if (mode === 'create' && createdTasks.length > 0) {
659
- result += `\n## Created Tasks Summary\n\n`;
660
- result += `**${createdTasks.length} tasks created:**\n`;
661
- for (const task of createdTasks) {
662
- result += `- ${task.id}: ${task.title}\n`;
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`;
663
451
  }
664
- result += `\nUse \`get_next_task\` to see the execution queue.\n`;
665
- } else if (mode === 'preview') {
666
- result += `\n## Preview Complete\n\n`;
667
- result += `Run with \`mode: "create"\` to create these ${allAnalyzedTodos.length} tasks.\n`;
668
452
  }
669
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
+
670
460
  return {
671
461
  content: [{ type: 'text', text: result }],
672
462
  };
673
463
  }
674
464
 
675
465
  /**
676
- * Build task description from todo and intent analysis
466
+ * List archived thoughts handler
677
467
  */
678
- function buildDescription(todo, intent) {
679
- let desc = '';
468
+ async function listArchivedThoughts(args) {
469
+ const { limit = 20 } = args || {};
470
+
471
+ await ensureThoughtsArchiveDir();
680
472
 
681
- if (todo.section) {
682
- desc += `**From:** ${todo.section}\n\n`;
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
683
486
  }
684
487
 
685
- desc += `**Original thought:**\n> ${todo.raw}\n\n`;
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
+ }
686
496
 
687
- if (intent.shadow) {
688
- desc += `**Context (why):**\n${intent.shadow}\n\n`;
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
689
515
  }
690
516
 
691
- if (todo.context.length > 0) {
692
- desc += `**Surrounding context:**\n${todo.context.map(c => `> ${c}`).join('\n')}\n\n`;
517
+ if (!logContent && result.includes('Archived Files') === false) {
518
+ result += `*No archived thoughts yet. Use \`archive_thought\` after processing thoughts.*\n`;
693
519
  }
694
520
 
695
- desc += `---\n*Extracted from thought dump via process_thoughts*`;
521
+ result += `\n---\n`;
522
+ result += `**Tools:** \`get_thought\` with \`from_archive: true\` to read archived files`;
696
523
 
697
- return desc;
524
+ return {
525
+ content: [{ type: 'text', text: result }],
526
+ };
698
527
  }
699
528
 
700
529
  /**
701
530
  * List thoughts handler
702
531
  */
703
532
  async function listThoughts(args) {
704
- const { category } = args || {};
533
+ const { category, include_archived = false } = args || {};
705
534
 
706
535
  await ensureThoughtsTodosDir();
707
536
 
@@ -714,7 +543,7 @@ async function listThoughts(args) {
714
543
  try {
715
544
  if (await fileExists(catDir)) {
716
545
  const files = await readdir(catDir);
717
- const mdFiles = files.filter(f => f.endsWith('.md'));
546
+ const mdFiles = files.filter(f => f.endsWith('.md') && !f.startsWith('.'));
718
547
 
719
548
  results[cat] = [];
720
549
  for (const file of mdFiles) {
@@ -756,9 +585,24 @@ async function listThoughts(args) {
756
585
  }
757
586
  }
758
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
+
759
603
  result += `---\n`;
760
- result += `**Total files:** ${totalFiles}\n\n`;
761
- result += `**Tools:** \`get_thought\` to read | \`process_thoughts\` to convert to tasks`;
604
+ result += `**Total active files:** ${totalFiles}\n\n`;
605
+ result += `**Tools:** \`get_thought\` to read | \`process_thoughts\` to analyze | \`archive_thought\` after processing`;
762
606
 
763
607
  return {
764
608
  content: [{ type: 'text', text: result }],
@@ -769,19 +613,27 @@ async function listThoughts(args) {
769
613
  * Get thought handler
770
614
  */
771
615
  async function getThought(args) {
772
- const { file, category = 'todos' } = args;
616
+ const { file, category = 'todos', from_archive = false } = args;
773
617
 
774
618
  await ensureThoughtsTodosDir();
775
619
 
776
- 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
+
777
628
  const filePath = join(catDir, file);
778
629
 
779
630
  if (!(await fileExists(filePath))) {
631
+ const location = from_archive ? '.archive' : category;
780
632
  return {
781
633
  content: [
782
634
  {
783
635
  type: 'text',
784
- 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.`,
785
637
  },
786
638
  ],
787
639
  isError: true,
@@ -791,9 +643,10 @@ async function getThought(args) {
791
643
  const content = await readFile(filePath, 'utf-8');
792
644
  const parsed = matter(content);
793
645
 
646
+ const location = from_archive ? 'todos/.archive' : category;
794
647
  let result = `## Thought File: ${file}\n\n`;
795
- result += `**Path:** \`.project/thoughts/${category}/${file}\`\n`;
796
- result += `**Category:** ${category}\n\n`;
648
+ result += `**Path:** \`.project/thoughts/${location}/${file}\`\n`;
649
+ result += `**Status:** ${from_archive ? '📦 Archived' : '📝 Active'}\n\n`;
797
650
 
798
651
  if (Object.keys(parsed.data).length > 0) {
799
652
  result += `### Frontmatter\n\n`;
@@ -808,7 +661,11 @@ async function getThought(args) {
808
661
  result += parsed.content;
809
662
 
810
663
  result += `\n\n---\n`;
811
- result += `**Tools:** \`process_thoughts\` to convert to 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
+ }
812
669
 
813
670
  return {
814
671
  content: [{ type: 'text', text: result }],
@@ -820,6 +677,8 @@ async function getThought(args) {
820
677
  */
821
678
  export const handlers = {
822
679
  process_thoughts: processThoughts,
680
+ archive_thought: archiveThought,
823
681
  list_thoughts: listThoughts,
682
+ list_archived_thoughts: listArchivedThoughts,
824
683
  get_thought: getThought,
825
684
  };