@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.
Files changed (59) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +281 -0
  3. package/dist/errors/errors.d.ts +109 -0
  4. package/dist/errors/errors.js +199 -0
  5. package/dist/errors/index.d.ts +4 -0
  6. package/dist/errors/index.js +4 -0
  7. package/dist/features/task-management/models/artifact.d.ts +169 -0
  8. package/dist/features/task-management/models/artifact.js +155 -0
  9. package/dist/features/task-management/models/config.d.ts +54 -0
  10. package/dist/features/task-management/models/config.js +54 -0
  11. package/dist/features/task-management/models/index.d.ts +6 -0
  12. package/dist/features/task-management/models/index.js +6 -0
  13. package/dist/features/task-management/models/task.d.ts +173 -0
  14. package/dist/features/task-management/models/task.js +84 -0
  15. package/dist/features/task-management/storage/file-storage.d.ts +130 -0
  16. package/dist/features/task-management/storage/file-storage.js +575 -0
  17. package/dist/features/task-management/storage/index.d.ts +5 -0
  18. package/dist/features/task-management/storage/index.js +5 -0
  19. package/dist/features/task-management/storage/storage.d.ts +159 -0
  20. package/dist/features/task-management/storage/storage.js +37 -0
  21. package/dist/features/task-management/tools/artifacts/index.d.ts +6 -0
  22. package/dist/features/task-management/tools/artifacts/index.js +174 -0
  23. package/dist/features/task-management/tools/base/handlers.d.ts +7 -0
  24. package/dist/features/task-management/tools/base/handlers.js +15 -0
  25. package/dist/features/task-management/tools/base/index.d.ts +3 -0
  26. package/dist/features/task-management/tools/base/index.js +3 -0
  27. package/dist/features/task-management/tools/base/schemas.d.ts +3 -0
  28. package/dist/features/task-management/tools/base/schemas.js +6 -0
  29. package/dist/features/task-management/tools/base/types.d.ts +13 -0
  30. package/dist/features/task-management/tools/base/types.js +1 -0
  31. package/dist/features/task-management/tools/tasks/index.d.ts +10 -0
  32. package/dist/features/task-management/tools/tasks/index.js +500 -0
  33. package/dist/index.d.ts +2 -0
  34. package/dist/index.js +57 -0
  35. package/dist/server.d.ts +11 -0
  36. package/dist/server.js +61 -0
  37. package/dist/types/common.d.ts +10 -0
  38. package/dist/types/common.js +1 -0
  39. package/dist/types/index.d.ts +5 -0
  40. package/dist/types/index.js +5 -0
  41. package/dist/utils/cache.d.ts +104 -0
  42. package/dist/utils/cache.js +196 -0
  43. package/dist/utils/file-utils.d.ts +101 -0
  44. package/dist/utils/file-utils.js +270 -0
  45. package/dist/utils/index.d.ts +12 -0
  46. package/dist/utils/index.js +12 -0
  47. package/dist/utils/logger.d.ts +77 -0
  48. package/dist/utils/logger.js +173 -0
  49. package/dist/utils/response-builder.d.ts +4 -0
  50. package/dist/utils/response-builder.js +19 -0
  51. package/dist/utils/storage-config.d.ts +29 -0
  52. package/dist/utils/storage-config.js +51 -0
  53. package/dist/utils/string-utils.d.ts +2 -0
  54. package/dist/utils/string-utils.js +16 -0
  55. package/dist/utils/validation.d.ts +9 -0
  56. package/dist/utils/validation.js +9 -0
  57. package/dist/utils/version.d.ts +9 -0
  58. package/dist/utils/version.js +41 -0
  59. 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
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Storage layer exports
3
+ */
4
+ export * from './storage.js';
5
+ export * from './file-storage.js';
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Storage layer exports
3
+ */
4
+ export * from './storage.js';
5
+ export * from './file-storage.js';