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 +1 -1
- package/src/lib/constants.js +1 -0
- package/src/lib/files.js +19 -1
- package/src/tools/thoughts.js +373 -514
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "project-mcp",
|
|
3
|
-
"version": "3.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",
|
package/src/lib/constants.js
CHANGED
|
@@ -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 {
|
|
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
|
package/src/tools/thoughts.js
CHANGED
|
@@ -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
|
|
6
|
-
*
|
|
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,
|
|
11
|
-
import {
|
|
12
|
-
|
|
13
|
-
|
|
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: `
|
|
33
|
+
description: `Reads brain dump markdown files from .project/thoughts/todos/ and returns the content along with project context for analysis.
|
|
23
34
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
|
|
29
|
-
-
|
|
30
|
-
-
|
|
31
|
-
-
|
|
32
|
-
-
|
|
33
|
-
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
57
|
-
type: '
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
|
|
62
|
-
type: '
|
|
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: ['
|
|
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
|
|
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: '
|
|
110
|
+
name: 'list_archived_thoughts',
|
|
89
111
|
description:
|
|
90
|
-
'
|
|
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").
|
|
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
|
-
*
|
|
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
|
-
|
|
156
|
+
roadmap: null,
|
|
358
157
|
decisions: [],
|
|
359
|
-
|
|
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
|
|
172
|
+
// Load project files
|
|
373
173
|
await loadAllFiles();
|
|
374
174
|
const files = getCachedFiles();
|
|
375
175
|
|
|
376
|
-
//
|
|
176
|
+
// Get roadmap content
|
|
377
177
|
const roadmapFile = files.find(f => f.path.includes('ROADMAP'));
|
|
378
178
|
if (roadmapFile) {
|
|
379
|
-
|
|
380
|
-
context.roadmapItems = milestones.map(m => m.replace('##', '').trim());
|
|
179
|
+
context.roadmap = roadmapFile.content.substring(0, 2000);
|
|
381
180
|
}
|
|
382
181
|
|
|
383
|
-
//
|
|
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
|
-
//
|
|
391
|
-
const
|
|
392
|
-
if (
|
|
393
|
-
|
|
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
|
-
|
|
406
|
-
|
|
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
|
-
*
|
|
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
|
|
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
|
|
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
|
-
//
|
|
489
|
-
|
|
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
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
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
|
-
//
|
|
545
|
-
|
|
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
|
-
|
|
552
|
-
|
|
553
|
-
}
|
|
262
|
+
// Build the response for the LLM
|
|
263
|
+
let result = `# Thought Processing Data
|
|
554
264
|
|
|
555
|
-
|
|
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
|
-
|
|
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
|
-
|
|
568
|
-
|
|
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 +=
|
|
310
|
+
result += `---
|
|
573
311
|
|
|
574
|
-
|
|
312
|
+
## Project Context
|
|
575
313
|
|
|
576
|
-
|
|
577
|
-
const todo = allAnalyzedTodos[i];
|
|
314
|
+
Use this to understand what already exists and align new tasks appropriately.
|
|
578
315
|
|
|
579
|
-
|
|
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
|
-
|
|
584
|
-
result += `**Section:** ${todo.section}\n`;
|
|
585
|
-
}
|
|
318
|
+
`;
|
|
586
319
|
|
|
587
|
-
|
|
588
|
-
result +=
|
|
589
|
-
|
|
590
|
-
|
|
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 (
|
|
593
|
-
result +=
|
|
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
|
-
|
|
597
|
-
|
|
598
|
-
|
|
333
|
+
if (projectContext.roadmap) {
|
|
334
|
+
result += `\n### Roadmap Overview\n\n`;
|
|
335
|
+
result += '```\n' + projectContext.roadmap + '\n```\n';
|
|
336
|
+
}
|
|
599
337
|
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
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
|
-
|
|
608
|
-
|
|
609
|
-
|
|
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
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
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
|
-
|
|
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
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
result += `\n
|
|
660
|
-
|
|
661
|
-
|
|
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
|
-
*
|
|
466
|
+
* List archived thoughts handler
|
|
677
467
|
*/
|
|
678
|
-
function
|
|
679
|
-
|
|
468
|
+
async function listArchivedThoughts(args) {
|
|
469
|
+
const { limit = 20 } = args || {};
|
|
470
|
+
|
|
471
|
+
await ensureThoughtsArchiveDir();
|
|
680
472
|
|
|
681
|
-
|
|
682
|
-
|
|
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
|
-
|
|
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
|
-
|
|
688
|
-
|
|
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 (
|
|
692
|
-
|
|
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
|
-
|
|
521
|
+
result += `\n---\n`;
|
|
522
|
+
result += `**Tools:** \`get_thought\` with \`from_archive: true\` to read archived files`;
|
|
696
523
|
|
|
697
|
-
return
|
|
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
|
|
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
|
-
|
|
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/${
|
|
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/${
|
|
796
|
-
result += `**
|
|
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
|
-
|
|
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
|
};
|