@geanatz/cortex-mcp 5.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +281 -0
- package/dist/errors/errors.d.ts +109 -0
- package/dist/errors/errors.js +199 -0
- package/dist/errors/index.d.ts +4 -0
- package/dist/errors/index.js +4 -0
- package/dist/features/task-management/models/artifact.d.ts +169 -0
- package/dist/features/task-management/models/artifact.js +155 -0
- package/dist/features/task-management/models/config.d.ts +54 -0
- package/dist/features/task-management/models/config.js +54 -0
- package/dist/features/task-management/models/index.d.ts +6 -0
- package/dist/features/task-management/models/index.js +6 -0
- package/dist/features/task-management/models/task.d.ts +173 -0
- package/dist/features/task-management/models/task.js +84 -0
- package/dist/features/task-management/storage/file-storage.d.ts +130 -0
- package/dist/features/task-management/storage/file-storage.js +575 -0
- package/dist/features/task-management/storage/index.d.ts +5 -0
- package/dist/features/task-management/storage/index.js +5 -0
- package/dist/features/task-management/storage/storage.d.ts +159 -0
- package/dist/features/task-management/storage/storage.js +37 -0
- package/dist/features/task-management/tools/artifacts/index.d.ts +6 -0
- package/dist/features/task-management/tools/artifacts/index.js +174 -0
- package/dist/features/task-management/tools/base/handlers.d.ts +7 -0
- package/dist/features/task-management/tools/base/handlers.js +15 -0
- package/dist/features/task-management/tools/base/index.d.ts +3 -0
- package/dist/features/task-management/tools/base/index.js +3 -0
- package/dist/features/task-management/tools/base/schemas.d.ts +3 -0
- package/dist/features/task-management/tools/base/schemas.js +6 -0
- package/dist/features/task-management/tools/base/types.d.ts +13 -0
- package/dist/features/task-management/tools/base/types.js +1 -0
- package/dist/features/task-management/tools/tasks/index.d.ts +10 -0
- package/dist/features/task-management/tools/tasks/index.js +500 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +57 -0
- package/dist/server.d.ts +11 -0
- package/dist/server.js +61 -0
- package/dist/types/common.d.ts +10 -0
- package/dist/types/common.js +1 -0
- package/dist/types/index.d.ts +5 -0
- package/dist/types/index.js +5 -0
- package/dist/utils/cache.d.ts +104 -0
- package/dist/utils/cache.js +196 -0
- package/dist/utils/file-utils.d.ts +101 -0
- package/dist/utils/file-utils.js +270 -0
- package/dist/utils/index.d.ts +12 -0
- package/dist/utils/index.js +12 -0
- package/dist/utils/logger.d.ts +77 -0
- package/dist/utils/logger.js +173 -0
- package/dist/utils/response-builder.d.ts +4 -0
- package/dist/utils/response-builder.js +19 -0
- package/dist/utils/storage-config.d.ts +29 -0
- package/dist/utils/storage-config.js +51 -0
- package/dist/utils/string-utils.d.ts +2 -0
- package/dist/utils/string-utils.js +16 -0
- package/dist/utils/validation.d.ts +9 -0
- package/dist/utils/validation.js +9 -0
- package/dist/utils/version.d.ts +9 -0
- package/dist/utils/version.js +41 -0
- package/package.json +60 -0
|
@@ -0,0 +1,575 @@
|
|
|
1
|
+
import { join } from 'path';
|
|
2
|
+
import { BaseStorage } from './storage.js';
|
|
3
|
+
import { generateNextSubtaskId } from '../models/task.js';
|
|
4
|
+
import { ARTIFACT_PHASES, getArtifactFilename } from '../models/artifact.js';
|
|
5
|
+
import { STORAGE_PATHS, TASK_NUMBERING, FILE_NAMING, CACHE_CONFIG } from '../models/config.js';
|
|
6
|
+
import { fileExists, ensureDirectory, atomicWriteFile, readJsonFileOrNull, writeJsonFile, listDirectory, deleteDirectory, deleteFile, readFileOrNull } from '../../../utils/file-utils.js';
|
|
7
|
+
import { sanitizeFileName, padNumber } from '../../../utils/string-utils.js';
|
|
8
|
+
import { Cache, CacheKeys, InvalidationPatterns } from '../../../utils/cache.js';
|
|
9
|
+
import { NotFoundError, StorageError } from '../../../errors/errors.js';
|
|
10
|
+
import { createLogger } from '../../../utils/logger.js';
|
|
11
|
+
const logger = createLogger('file-storage');
|
|
12
|
+
/**
|
|
13
|
+
* File-based storage implementation - Simplified Model
|
|
14
|
+
*
|
|
15
|
+
* Storage Structure:
|
|
16
|
+
* - .cortex/tasks/{number}-{slug}/.task.json - Contains parent task + subtasks array
|
|
17
|
+
* - .cortex/tasks/{number}-{slug}/{phase}.md - Artifact files (for entire hierarchy)
|
|
18
|
+
*
|
|
19
|
+
* Key Changes:
|
|
20
|
+
* - Subtasks are stored INSIDE the parent .task.json file
|
|
21
|
+
* - No separate folders for subtasks
|
|
22
|
+
* - No dependsOn field - simplified model
|
|
23
|
+
* - Single level nesting only (subtasks cannot have subtasks)
|
|
24
|
+
* - Artifacts belong to the entire task hierarchy
|
|
25
|
+
*
|
|
26
|
+
* Features:
|
|
27
|
+
* - Each parent task has its own folder with sequential numbering
|
|
28
|
+
* - Task ID = folder name (e.g., '001-implement-auth')
|
|
29
|
+
* - Atomic file writes using temp files
|
|
30
|
+
* - In-memory caching with TTL
|
|
31
|
+
*/
|
|
32
|
+
export class FileStorage extends BaseStorage {
|
|
33
|
+
workingDirectory;
|
|
34
|
+
cortexDir;
|
|
35
|
+
tasksDir;
|
|
36
|
+
// Caches for performance
|
|
37
|
+
taskCache;
|
|
38
|
+
artifactCache;
|
|
39
|
+
taskFoldersCache = null;
|
|
40
|
+
taskFoldersCacheTime = 0;
|
|
41
|
+
constructor(workingDirectory) {
|
|
42
|
+
super();
|
|
43
|
+
this.workingDirectory = workingDirectory;
|
|
44
|
+
this.cortexDir = join(workingDirectory, STORAGE_PATHS.ROOT_DIR);
|
|
45
|
+
this.tasksDir = join(this.cortexDir, STORAGE_PATHS.TASKS_DIR);
|
|
46
|
+
// Initialize caches
|
|
47
|
+
this.taskCache = new Cache({
|
|
48
|
+
defaultTtl: CACHE_CONFIG.DEFAULT_TTL,
|
|
49
|
+
maxSize: CACHE_CONFIG.MAX_SIZE
|
|
50
|
+
});
|
|
51
|
+
this.artifactCache = new Cache({
|
|
52
|
+
defaultTtl: CACHE_CONFIG.DEFAULT_TTL,
|
|
53
|
+
maxSize: CACHE_CONFIG.MAX_SIZE
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
// ==================== Initialization ====================
|
|
57
|
+
/**
|
|
58
|
+
* Initialize storage by validating working directory and ensuring directories exist
|
|
59
|
+
*/
|
|
60
|
+
async initialize() {
|
|
61
|
+
if (this.initialized) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
logger.debug('Initializing file storage', { workingDirectory: this.workingDirectory });
|
|
65
|
+
// Validate working directory exists
|
|
66
|
+
if (!await fileExists(this.workingDirectory)) {
|
|
67
|
+
// Try to create the working directory if it doesn't exist
|
|
68
|
+
try {
|
|
69
|
+
await ensureDirectory(this.workingDirectory);
|
|
70
|
+
logger.debug('Created working directory', { workingDirectory: this.workingDirectory });
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
throw StorageError.initializationError(this.workingDirectory, `Working directory does not exist and could not be created: ${this.workingDirectory}. Error: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
// Ensure directories exist
|
|
77
|
+
await ensureDirectory(this.cortexDir);
|
|
78
|
+
await ensureDirectory(this.tasksDir);
|
|
79
|
+
this.initialized = true;
|
|
80
|
+
logger.debug('File storage initialized successfully');
|
|
81
|
+
}
|
|
82
|
+
getWorkingDirectory() {
|
|
83
|
+
return this.workingDirectory;
|
|
84
|
+
}
|
|
85
|
+
// ==================== Private Helpers ====================
|
|
86
|
+
/**
|
|
87
|
+
* Sanitize a string for safe filesystem usage
|
|
88
|
+
*/
|
|
89
|
+
sanitizeName(input) {
|
|
90
|
+
return sanitizeFileName(input, FILE_NAMING.MAX_SLUG_LENGTH);
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Get the next sequential number by scanning existing task folders
|
|
94
|
+
*/
|
|
95
|
+
async getNextNumber() {
|
|
96
|
+
const folders = await this.getTaskFolders();
|
|
97
|
+
const numbers = folders
|
|
98
|
+
.filter(name => TASK_NUMBERING.PATTERN.test(name))
|
|
99
|
+
.map(name => parseInt(name.slice(0, TASK_NUMBERING.DIGITS), 10))
|
|
100
|
+
.filter(n => !isNaN(n));
|
|
101
|
+
if (numbers.length === 0)
|
|
102
|
+
return 1;
|
|
103
|
+
return Math.max(...numbers) + 1;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Generate task ID (folder name) from details
|
|
107
|
+
*/
|
|
108
|
+
generateTaskId(details, number) {
|
|
109
|
+
const paddedNumber = padNumber(number, TASK_NUMBERING.DIGITS);
|
|
110
|
+
const sanitizedSlug = this.sanitizeName(details);
|
|
111
|
+
return `${paddedNumber}-${sanitizedSlug}`;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Get task folder path
|
|
115
|
+
*/
|
|
116
|
+
getTaskFolderPath(taskId) {
|
|
117
|
+
return join(this.tasksDir, taskId);
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Get task file path
|
|
121
|
+
*/
|
|
122
|
+
getTaskFilePath(taskId) {
|
|
123
|
+
return join(this.getTaskFolderPath(taskId), STORAGE_PATHS.TASK_FILE);
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Get artifact file path
|
|
127
|
+
*/
|
|
128
|
+
getArtifactFilePath(taskId, phase) {
|
|
129
|
+
return join(this.getTaskFolderPath(taskId), getArtifactFilename(phase));
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Get all task folder names (parent tasks only, with caching)
|
|
133
|
+
*/
|
|
134
|
+
async getTaskFolders() {
|
|
135
|
+
const now = Date.now();
|
|
136
|
+
const cacheAge = now - this.taskFoldersCacheTime;
|
|
137
|
+
// Use cache if fresh (within 1 second)
|
|
138
|
+
if (this.taskFoldersCache && cacheAge < 1000) {
|
|
139
|
+
return this.taskFoldersCache;
|
|
140
|
+
}
|
|
141
|
+
this.taskFoldersCache = await listDirectory(this.tasksDir, {
|
|
142
|
+
directoriesOnly: true,
|
|
143
|
+
pattern: TASK_NUMBERING.PATTERN,
|
|
144
|
+
sort: true
|
|
145
|
+
});
|
|
146
|
+
this.taskFoldersCacheTime = now;
|
|
147
|
+
return this.taskFoldersCache;
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Invalidate task folders cache
|
|
151
|
+
*/
|
|
152
|
+
invalidateTaskFoldersCache() {
|
|
153
|
+
this.taskFoldersCache = null;
|
|
154
|
+
this.taskFoldersCacheTime = 0;
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Load a task from disk (with caching)
|
|
158
|
+
*/
|
|
159
|
+
async loadTask(taskId) {
|
|
160
|
+
// Check cache first
|
|
161
|
+
const cached = this.taskCache.get(CacheKeys.task(taskId));
|
|
162
|
+
if (cached) {
|
|
163
|
+
return cached;
|
|
164
|
+
}
|
|
165
|
+
const task = await readJsonFileOrNull(this.getTaskFilePath(taskId));
|
|
166
|
+
if (task) {
|
|
167
|
+
// Ensure subtasks array exists
|
|
168
|
+
if (!task.subtasks) {
|
|
169
|
+
task.subtasks = [];
|
|
170
|
+
}
|
|
171
|
+
this.taskCache.set(CacheKeys.task(taskId), task);
|
|
172
|
+
}
|
|
173
|
+
return task;
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Save a task to disk and update cache
|
|
177
|
+
*/
|
|
178
|
+
async saveTask(taskId, task) {
|
|
179
|
+
const folderPath = this.getTaskFolderPath(taskId);
|
|
180
|
+
await ensureDirectory(folderPath);
|
|
181
|
+
await writeJsonFile(this.getTaskFilePath(taskId), task);
|
|
182
|
+
// Update cache
|
|
183
|
+
this.taskCache.set(CacheKeys.task(taskId), task);
|
|
184
|
+
this.invalidateTaskFoldersCache();
|
|
185
|
+
}
|
|
186
|
+
// ==================== Task Operations ====================
|
|
187
|
+
/**
|
|
188
|
+
* Get all parent tasks (subtasks are inside each task)
|
|
189
|
+
*/
|
|
190
|
+
async getTasks() {
|
|
191
|
+
const folders = await this.getTaskFolders();
|
|
192
|
+
const tasks = [];
|
|
193
|
+
// Load all parent tasks (utilizing cache)
|
|
194
|
+
for (const folder of folders) {
|
|
195
|
+
const task = await this.loadTask(folder);
|
|
196
|
+
if (task) {
|
|
197
|
+
// Ensure subtasks array exists
|
|
198
|
+
if (!task.subtasks) {
|
|
199
|
+
task.subtasks = [];
|
|
200
|
+
}
|
|
201
|
+
tasks.push(task);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return tasks;
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Get a single task by ID
|
|
208
|
+
*/
|
|
209
|
+
async getTask(id) {
|
|
210
|
+
const task = await this.loadTask(id);
|
|
211
|
+
if (task && !task.subtasks) {
|
|
212
|
+
task.subtasks = [];
|
|
213
|
+
}
|
|
214
|
+
return task;
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Create a new parent task
|
|
218
|
+
*/
|
|
219
|
+
async createTask(input) {
|
|
220
|
+
// Get next number and generate task ID
|
|
221
|
+
const nextNumber = await this.getNextNumber();
|
|
222
|
+
const taskId = this.generateTaskId(input.details, nextNumber);
|
|
223
|
+
const now = new Date().toISOString();
|
|
224
|
+
const task = {
|
|
225
|
+
id: taskId,
|
|
226
|
+
details: input.details.trim(),
|
|
227
|
+
createdAt: now,
|
|
228
|
+
updatedAt: now,
|
|
229
|
+
status: input.status || 'pending',
|
|
230
|
+
tags: input.tags || [],
|
|
231
|
+
subtasks: [],
|
|
232
|
+
};
|
|
233
|
+
// Save task
|
|
234
|
+
await this.saveTask(taskId, task);
|
|
235
|
+
logger.debug('Task created', { taskId });
|
|
236
|
+
return task;
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Update an existing task
|
|
240
|
+
* Supports updating parent fields and subtask operations
|
|
241
|
+
*/
|
|
242
|
+
async updateTask(id, updates) {
|
|
243
|
+
const task = await this.loadTask(id);
|
|
244
|
+
if (!task)
|
|
245
|
+
return null;
|
|
246
|
+
// Ensure subtasks array exists
|
|
247
|
+
if (!task.subtasks) {
|
|
248
|
+
task.subtasks = [];
|
|
249
|
+
}
|
|
250
|
+
let subtasks = [...task.subtasks];
|
|
251
|
+
let updated = false;
|
|
252
|
+
// Handle addSubtask operation
|
|
253
|
+
if (updates.addSubtask) {
|
|
254
|
+
const newSubtask = {
|
|
255
|
+
id: generateNextSubtaskId(subtasks),
|
|
256
|
+
details: updates.addSubtask.details.trim(),
|
|
257
|
+
status: updates.addSubtask.status || 'pending',
|
|
258
|
+
};
|
|
259
|
+
subtasks.push(newSubtask);
|
|
260
|
+
updated = true;
|
|
261
|
+
}
|
|
262
|
+
// Handle updateSubtask operation
|
|
263
|
+
if (updates.updateSubtask) {
|
|
264
|
+
const subtaskIndex = subtasks.findIndex(s => s.id === updates.updateSubtask.id);
|
|
265
|
+
if (subtaskIndex !== -1) {
|
|
266
|
+
subtasks[subtaskIndex] = {
|
|
267
|
+
...subtasks[subtaskIndex],
|
|
268
|
+
details: updates.updateSubtask.details?.trim() ?? subtasks[subtaskIndex].details,
|
|
269
|
+
status: updates.updateSubtask.status ?? subtasks[subtaskIndex].status,
|
|
270
|
+
};
|
|
271
|
+
updated = true;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
// Handle removeSubtaskId operation
|
|
275
|
+
if (updates.removeSubtaskId) {
|
|
276
|
+
const initialLength = subtasks.length;
|
|
277
|
+
subtasks = subtasks.filter(s => s.id !== updates.removeSubtaskId);
|
|
278
|
+
if (subtasks.length !== initialLength) {
|
|
279
|
+
updated = true;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
// Merge updates
|
|
283
|
+
const updatedTask = {
|
|
284
|
+
...task,
|
|
285
|
+
details: updates.details?.trim() ?? task.details,
|
|
286
|
+
status: updates.status ?? task.status,
|
|
287
|
+
tags: updates.tags ?? task.tags,
|
|
288
|
+
actualHours: updates.actualHours ?? task.actualHours,
|
|
289
|
+
subtasks,
|
|
290
|
+
updatedAt: new Date().toISOString(),
|
|
291
|
+
};
|
|
292
|
+
// Save and invalidate cache
|
|
293
|
+
await this.saveTask(id, updatedTask);
|
|
294
|
+
this.taskCache.invalidate(InvalidationPatterns.task(id));
|
|
295
|
+
logger.debug('Task updated', { taskId: id });
|
|
296
|
+
return updatedTask;
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* Delete a task and all its subtasks
|
|
300
|
+
*/
|
|
301
|
+
async deleteTask(id) {
|
|
302
|
+
const task = await this.loadTask(id);
|
|
303
|
+
if (!task)
|
|
304
|
+
return false;
|
|
305
|
+
// Delete the entire task folder (includes .task.json, artifacts, and effectively all subtasks)
|
|
306
|
+
await deleteDirectory(this.getTaskFolderPath(id));
|
|
307
|
+
// Invalidate caches
|
|
308
|
+
this.taskCache.delete(CacheKeys.task(id));
|
|
309
|
+
this.artifactCache.invalidate(InvalidationPatterns.artifact(id));
|
|
310
|
+
this.invalidateTaskFoldersCache();
|
|
311
|
+
logger.debug('Task deleted', { taskId: id });
|
|
312
|
+
return true;
|
|
313
|
+
}
|
|
314
|
+
// ==================== Subtask Operations ====================
|
|
315
|
+
/**
|
|
316
|
+
* Add a subtask to a parent task
|
|
317
|
+
*/
|
|
318
|
+
async addSubtask(taskId, input) {
|
|
319
|
+
const task = await this.loadTask(taskId);
|
|
320
|
+
if (!task)
|
|
321
|
+
return null;
|
|
322
|
+
const newSubtask = {
|
|
323
|
+
id: generateNextSubtaskId(task.subtasks || []),
|
|
324
|
+
details: input.details.trim(),
|
|
325
|
+
status: input.status || 'pending',
|
|
326
|
+
};
|
|
327
|
+
const updatedTask = {
|
|
328
|
+
...task,
|
|
329
|
+
subtasks: [...(task.subtasks || []), newSubtask],
|
|
330
|
+
updatedAt: new Date().toISOString(),
|
|
331
|
+
};
|
|
332
|
+
await this.saveTask(taskId, updatedTask);
|
|
333
|
+
this.taskCache.set(CacheKeys.task(taskId), updatedTask);
|
|
334
|
+
logger.debug('Subtask added', { taskId, subtaskId: newSubtask.id });
|
|
335
|
+
return newSubtask;
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* Update a subtask
|
|
339
|
+
*/
|
|
340
|
+
async updateSubtask(taskId, input) {
|
|
341
|
+
const task = await this.loadTask(taskId);
|
|
342
|
+
if (!task)
|
|
343
|
+
return null;
|
|
344
|
+
const subtasks = task.subtasks || [];
|
|
345
|
+
const subtaskIndex = subtasks.findIndex(s => s.id === input.id);
|
|
346
|
+
if (subtaskIndex === -1)
|
|
347
|
+
return null;
|
|
348
|
+
const updatedSubtask = {
|
|
349
|
+
...subtasks[subtaskIndex],
|
|
350
|
+
details: input.details?.trim() ?? subtasks[subtaskIndex].details,
|
|
351
|
+
status: input.status ?? subtasks[subtaskIndex].status,
|
|
352
|
+
};
|
|
353
|
+
const updatedSubtasks = [...subtasks];
|
|
354
|
+
updatedSubtasks[subtaskIndex] = updatedSubtask;
|
|
355
|
+
const updatedTask = {
|
|
356
|
+
...task,
|
|
357
|
+
subtasks: updatedSubtasks,
|
|
358
|
+
updatedAt: new Date().toISOString(),
|
|
359
|
+
};
|
|
360
|
+
await this.saveTask(taskId, updatedTask);
|
|
361
|
+
this.taskCache.set(CacheKeys.task(taskId), updatedTask);
|
|
362
|
+
logger.debug('Subtask updated', { taskId, subtaskId: input.id });
|
|
363
|
+
return updatedSubtask;
|
|
364
|
+
}
|
|
365
|
+
/**
|
|
366
|
+
* Remove a subtask by ID
|
|
367
|
+
*/
|
|
368
|
+
async removeSubtask(taskId, subtaskId) {
|
|
369
|
+
const task = await this.loadTask(taskId);
|
|
370
|
+
if (!task)
|
|
371
|
+
return false;
|
|
372
|
+
const initialLength = (task.subtasks || []).length;
|
|
373
|
+
const updatedSubtasks = (task.subtasks || []).filter(s => s.id !== subtaskId);
|
|
374
|
+
if (updatedSubtasks.length === initialLength) {
|
|
375
|
+
return false; // Subtask not found
|
|
376
|
+
}
|
|
377
|
+
const updatedTask = {
|
|
378
|
+
...task,
|
|
379
|
+
subtasks: updatedSubtasks,
|
|
380
|
+
updatedAt: new Date().toISOString(),
|
|
381
|
+
};
|
|
382
|
+
await this.saveTask(taskId, updatedTask);
|
|
383
|
+
this.taskCache.set(CacheKeys.task(taskId), updatedTask);
|
|
384
|
+
logger.debug('Subtask removed', { taskId, subtaskId });
|
|
385
|
+
return true;
|
|
386
|
+
}
|
|
387
|
+
// ==================== Task Hierarchy Operations ====================
|
|
388
|
+
/**
|
|
389
|
+
* Get task hierarchy (all parent tasks with their subtasks)
|
|
390
|
+
*/
|
|
391
|
+
async getTaskHierarchy() {
|
|
392
|
+
const tasks = await this.getTasks();
|
|
393
|
+
return tasks.map(task => ({
|
|
394
|
+
task,
|
|
395
|
+
depth: 0
|
|
396
|
+
}));
|
|
397
|
+
}
|
|
398
|
+
// ==================== Artifact Operations ====================
|
|
399
|
+
/**
|
|
400
|
+
* Parse artifact file content (YAML frontmatter + markdown body)
|
|
401
|
+
*/
|
|
402
|
+
parseArtifactContent(fileContent) {
|
|
403
|
+
const frontmatterRegex = /^---\n([\s\S]*?)\n---\n([\s\S]*)$/;
|
|
404
|
+
const match = fileContent.match(frontmatterRegex);
|
|
405
|
+
if (!match) {
|
|
406
|
+
throw new Error('Invalid artifact format: missing YAML frontmatter');
|
|
407
|
+
}
|
|
408
|
+
const yamlContent = match[1];
|
|
409
|
+
const markdownContent = match[2].trim();
|
|
410
|
+
// Parse YAML (simple key: value pairs)
|
|
411
|
+
const metadata = {};
|
|
412
|
+
for (const line of yamlContent.split('\n')) {
|
|
413
|
+
const colonIndex = line.indexOf(':');
|
|
414
|
+
if (colonIndex > 0) {
|
|
415
|
+
const key = line.slice(0, colonIndex).trim();
|
|
416
|
+
let value = line.slice(colonIndex + 1).trim();
|
|
417
|
+
// Type conversions
|
|
418
|
+
if (value === 'true')
|
|
419
|
+
value = true;
|
|
420
|
+
else if (value === 'false')
|
|
421
|
+
value = false;
|
|
422
|
+
else if (!isNaN(Number(value)) && value !== '')
|
|
423
|
+
value = Number(value);
|
|
424
|
+
else if (typeof value === 'string' && value.startsWith('"') && value.endsWith('"')) {
|
|
425
|
+
value = value.slice(1, -1);
|
|
426
|
+
}
|
|
427
|
+
metadata[key] = value;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
return {
|
|
431
|
+
metadata: {
|
|
432
|
+
phase: metadata.phase,
|
|
433
|
+
status: metadata.status,
|
|
434
|
+
createdAt: metadata.createdAt,
|
|
435
|
+
updatedAt: metadata.updatedAt,
|
|
436
|
+
retries: metadata.retries,
|
|
437
|
+
error: metadata.error
|
|
438
|
+
},
|
|
439
|
+
content: markdownContent
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
/**
|
|
443
|
+
* Serialize artifact to file content
|
|
444
|
+
*/
|
|
445
|
+
serializeArtifact(artifact) {
|
|
446
|
+
const lines = ['---'];
|
|
447
|
+
lines.push(`phase: ${artifact.metadata.phase}`);
|
|
448
|
+
lines.push(`status: ${artifact.metadata.status}`);
|
|
449
|
+
lines.push(`createdAt: ${artifact.metadata.createdAt}`);
|
|
450
|
+
lines.push(`updatedAt: ${artifact.metadata.updatedAt}`);
|
|
451
|
+
if (artifact.metadata.retries !== undefined) {
|
|
452
|
+
lines.push(`retries: ${artifact.metadata.retries}`);
|
|
453
|
+
}
|
|
454
|
+
if (artifact.metadata.error) {
|
|
455
|
+
lines.push(`error: "${artifact.metadata.error.replace(/"/g, '\\"')}"`);
|
|
456
|
+
}
|
|
457
|
+
lines.push('---');
|
|
458
|
+
lines.push('');
|
|
459
|
+
lines.push(artifact.content);
|
|
460
|
+
return lines.join('\n');
|
|
461
|
+
}
|
|
462
|
+
async getArtifact(taskId, phase) {
|
|
463
|
+
// Check cache
|
|
464
|
+
const cacheKey = CacheKeys.artifact(taskId, phase);
|
|
465
|
+
const cached = this.artifactCache.get(cacheKey);
|
|
466
|
+
if (cached) {
|
|
467
|
+
return cached;
|
|
468
|
+
}
|
|
469
|
+
// Verify task exists
|
|
470
|
+
const task = await this.getTask(taskId);
|
|
471
|
+
if (!task)
|
|
472
|
+
return null;
|
|
473
|
+
const content = await readFileOrNull(this.getArtifactFilePath(taskId, phase));
|
|
474
|
+
if (!content)
|
|
475
|
+
return null;
|
|
476
|
+
try {
|
|
477
|
+
const artifact = this.parseArtifactContent(content);
|
|
478
|
+
this.artifactCache.set(cacheKey, artifact);
|
|
479
|
+
return artifact;
|
|
480
|
+
}
|
|
481
|
+
catch {
|
|
482
|
+
return null;
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
async getAllArtifacts(taskId) {
|
|
486
|
+
const artifacts = {};
|
|
487
|
+
for (const phase of ARTIFACT_PHASES) {
|
|
488
|
+
const artifact = await this.getArtifact(taskId, phase);
|
|
489
|
+
if (artifact) {
|
|
490
|
+
artifacts[phase] = artifact;
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
return artifacts;
|
|
494
|
+
}
|
|
495
|
+
async createArtifact(taskId, phase, input) {
|
|
496
|
+
// Verify task exists
|
|
497
|
+
const task = await this.getTask(taskId);
|
|
498
|
+
if (!task) {
|
|
499
|
+
throw NotFoundError.task(taskId);
|
|
500
|
+
}
|
|
501
|
+
const now = new Date().toISOString();
|
|
502
|
+
const artifact = {
|
|
503
|
+
metadata: {
|
|
504
|
+
phase,
|
|
505
|
+
status: input.status || 'completed',
|
|
506
|
+
createdAt: now,
|
|
507
|
+
updatedAt: now,
|
|
508
|
+
retries: input.retries,
|
|
509
|
+
error: input.error
|
|
510
|
+
},
|
|
511
|
+
content: input.content.trim()
|
|
512
|
+
};
|
|
513
|
+
const filePath = this.getArtifactFilePath(taskId, phase);
|
|
514
|
+
await atomicWriteFile(filePath, this.serializeArtifact(artifact));
|
|
515
|
+
// Update cache
|
|
516
|
+
this.artifactCache.set(CacheKeys.artifact(taskId, phase), artifact);
|
|
517
|
+
logger.debug('Artifact created', { taskId, phase });
|
|
518
|
+
return artifact;
|
|
519
|
+
}
|
|
520
|
+
async updateArtifact(taskId, phase, input) {
|
|
521
|
+
const existingArtifact = await this.getArtifact(taskId, phase);
|
|
522
|
+
if (!existingArtifact) {
|
|
523
|
+
return null;
|
|
524
|
+
}
|
|
525
|
+
const now = new Date().toISOString();
|
|
526
|
+
const updatedArtifact = {
|
|
527
|
+
metadata: {
|
|
528
|
+
...existingArtifact.metadata,
|
|
529
|
+
status: input.status ?? existingArtifact.metadata.status,
|
|
530
|
+
updatedAt: now,
|
|
531
|
+
retries: input.retries ?? existingArtifact.metadata.retries,
|
|
532
|
+
error: input.error ?? existingArtifact.metadata.error
|
|
533
|
+
},
|
|
534
|
+
content: input.content?.trim() ?? existingArtifact.content
|
|
535
|
+
};
|
|
536
|
+
const filePath = this.getArtifactFilePath(taskId, phase);
|
|
537
|
+
await atomicWriteFile(filePath, this.serializeArtifact(updatedArtifact));
|
|
538
|
+
// Update cache
|
|
539
|
+
this.artifactCache.set(CacheKeys.artifact(taskId, phase), updatedArtifact);
|
|
540
|
+
logger.debug('Artifact updated', { taskId, phase });
|
|
541
|
+
return updatedArtifact;
|
|
542
|
+
}
|
|
543
|
+
async deleteArtifact(taskId, phase) {
|
|
544
|
+
const deleted = await deleteFile(this.getArtifactFilePath(taskId, phase));
|
|
545
|
+
if (deleted) {
|
|
546
|
+
this.artifactCache.delete(CacheKeys.artifact(taskId, phase));
|
|
547
|
+
logger.debug('Artifact deleted', { taskId, phase });
|
|
548
|
+
}
|
|
549
|
+
return deleted;
|
|
550
|
+
}
|
|
551
|
+
// ==================== Utility Operations ====================
|
|
552
|
+
async getStats() {
|
|
553
|
+
const folders = await this.getTaskFolders();
|
|
554
|
+
let artifactCount = 0;
|
|
555
|
+
for (const folder of folders) {
|
|
556
|
+
for (const phase of ARTIFACT_PHASES) {
|
|
557
|
+
if (await fileExists(this.getArtifactFilePath(folder, phase))) {
|
|
558
|
+
artifactCount++;
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
const cacheStats = this.taskCache.getStats();
|
|
563
|
+
return {
|
|
564
|
+
taskCount: folders.length,
|
|
565
|
+
artifactCount,
|
|
566
|
+
cacheHitRate: cacheStats.hitRate,
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
clearCache() {
|
|
570
|
+
this.taskCache.clear();
|
|
571
|
+
this.artifactCache.clear();
|
|
572
|
+
this.invalidateTaskFoldersCache();
|
|
573
|
+
logger.debug('Cache cleared');
|
|
574
|
+
}
|
|
575
|
+
}
|