claude-autopm 2.6.0 → 2.7.0

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.
@@ -0,0 +1,595 @@
1
+ /**
2
+ * ContextService - Context Management Service
3
+ *
4
+ * Pure service layer for context operations following ClaudeAutoPM patterns.
5
+ * Manages project context files for AI-assisted development.
6
+ *
7
+ * Provides comprehensive context lifecycle management:
8
+ *
9
+ * 1. Context Creation (1 method):
10
+ * - createContext: Create new context from template
11
+ *
12
+ * 2. Context Priming (1 method):
13
+ * - primeContext: Generate comprehensive project snapshot
14
+ *
15
+ * 3. Context Updates (1 method):
16
+ * - updateContext: Update existing context (append/replace)
17
+ *
18
+ * 4. Context Reading (2 methods):
19
+ * - getContext: Read context file with metadata
20
+ * - listContexts: List all contexts grouped by type
21
+ *
22
+ * 5. Context Validation (1 method):
23
+ * - validateContext: Validate structure and required fields
24
+ *
25
+ * 6. Context Operations (2 methods):
26
+ * - mergeContexts: Merge multiple contexts
27
+ * - analyzeContextUsage: Analyze size, age, and generate recommendations
28
+ *
29
+ * Documentation Queries:
30
+ * - mcp://context7/project-management/context-management - Context management patterns
31
+ * - mcp://context7/documentation/context-files - Context file best practices
32
+ * - mcp://context7/ai/context-optimization - Context optimization strategies
33
+ * - mcp://context7/project-management/project-brief - Project brief formats
34
+ */
35
+
36
+ const fs = require('fs-extra');
37
+ const path = require('path');
38
+
39
+ class ContextService {
40
+ /**
41
+ * Create a new ContextService instance
42
+ *
43
+ * @param {Object} options - Configuration options
44
+ * @param {string} [options.templatesPath] - Path to templates directory
45
+ * @param {string} [options.contextPath] - Path to contexts directory
46
+ * @param {string} [options.epicsPath] - Path to epics directory
47
+ * @param {string} [options.issuesPath] - Path to issues directory
48
+ */
49
+ constructor(options = {}) {
50
+ this.templatesPath = options.templatesPath ||
51
+ path.join(process.cwd(), '.claude/templates/context-templates');
52
+ this.contextPath = options.contextPath ||
53
+ path.join(process.cwd(), '.claude/context');
54
+ this.epicsPath = options.epicsPath ||
55
+ path.join(process.cwd(), '.claude/epics');
56
+ this.issuesPath = options.issuesPath ||
57
+ path.join(process.cwd(), '.claude/issues');
58
+ }
59
+
60
+ // ==========================================
61
+ // 1. CONTEXT CREATION (1 METHOD)
62
+ // ==========================================
63
+
64
+ /**
65
+ * Create new context file from template
66
+ *
67
+ * @param {string} type - Context type: project-brief, progress, tech-context, project-structure
68
+ * @param {object} options - Creation options (name, template, data)
69
+ * @returns {Promise<{path, content, created, type}>}
70
+ * @throws {Error} If template not found
71
+ *
72
+ * @example
73
+ * await createContext('project-brief', {
74
+ * name: 'My Project',
75
+ * description: 'Project description'
76
+ * })
77
+ */
78
+ async createContext(type, options = {}) {
79
+ // Load template
80
+ const templatePath = path.join(this.templatesPath, `${type}.md`);
81
+ const templateExists = await fs.pathExists(templatePath);
82
+
83
+ if (!templateExists) {
84
+ throw new Error(`Template not found for type: ${type}`);
85
+ }
86
+
87
+ let template = await fs.readFile(templatePath, 'utf8');
88
+
89
+ // Prepare variables
90
+ const variables = {
91
+ type,
92
+ name: options.name || type,
93
+ created: new Date().toISOString(),
94
+ ...options
95
+ };
96
+
97
+ // Replace template variables
98
+ let content = template;
99
+ for (const [key, value] of Object.entries(variables)) {
100
+ const regex = new RegExp(`{{${key}}}`, 'g');
101
+ content = content.replace(regex, String(value));
102
+ }
103
+
104
+ // Ensure context directory exists
105
+ await fs.ensureDir(this.contextPath);
106
+
107
+ // Determine output path
108
+ const contextFile = options.name
109
+ ? `${options.name.toLowerCase().replace(/\s+/g, '-')}.md`
110
+ : `${type}.md`;
111
+ const contextFilePath = path.join(this.contextPath, contextFile);
112
+
113
+ // Write context file
114
+ await fs.writeFile(contextFilePath, content);
115
+
116
+ return {
117
+ path: contextFilePath,
118
+ content,
119
+ created: variables.created,
120
+ type
121
+ };
122
+ }
123
+
124
+ // ==========================================
125
+ // 2. CONTEXT PRIMING (1 METHOD)
126
+ // ==========================================
127
+
128
+ /**
129
+ * Generate comprehensive project snapshot
130
+ *
131
+ * @param {object} options - Prime options
132
+ * @param {boolean} [options.includeGit] - Include git information
133
+ * @param {string} [options.output] - Output file path
134
+ * @returns {Promise<{contexts, summary, timestamp, git?}>}
135
+ *
136
+ * @example
137
+ * await primeContext({ includeGit: true, output: './snapshot.md' })
138
+ */
139
+ async primeContext(options = {}) {
140
+ const timestamp = new Date().toISOString();
141
+ const contexts = {
142
+ epics: [],
143
+ issues: [],
144
+ prds: []
145
+ };
146
+
147
+ // Gather epics
148
+ const epicsExists = await fs.pathExists(this.epicsPath);
149
+ if (epicsExists) {
150
+ const epicFiles = await fs.readdir(this.epicsPath);
151
+ for (const file of epicFiles) {
152
+ if (file.endsWith('.md')) {
153
+ const content = await fs.readFile(
154
+ path.join(this.epicsPath, file),
155
+ 'utf8'
156
+ );
157
+ contexts.epics.push({
158
+ file,
159
+ content,
160
+ metadata: this._parseFrontmatter(content)
161
+ });
162
+ }
163
+ }
164
+ }
165
+
166
+ // Gather issues
167
+ const issuesExists = await fs.pathExists(this.issuesPath);
168
+ if (issuesExists) {
169
+ const issueFiles = await fs.readdir(this.issuesPath);
170
+ for (const file of issueFiles) {
171
+ if (/^\d+\.md$/.test(file)) {
172
+ const content = await fs.readFile(
173
+ path.join(this.issuesPath, file),
174
+ 'utf8'
175
+ );
176
+ contexts.issues.push({
177
+ file,
178
+ content,
179
+ metadata: this._parseFrontmatter(content)
180
+ });
181
+ }
182
+ }
183
+ }
184
+
185
+ // Generate summary
186
+ let summary = 'Project Snapshot\n';
187
+ summary += `Generated: ${timestamp}\n\n`;
188
+ summary += `Epics: ${contexts.epics.length}\n`;
189
+ summary += `Issues: ${contexts.issues.length}\n`;
190
+
191
+ if (contexts.epics.length === 0 && contexts.issues.length === 0) {
192
+ summary += '\nProject appears to be empty or newly initialized.';
193
+ }
194
+
195
+ // Include git info if requested
196
+ let git;
197
+ if (options.includeGit) {
198
+ git = await this._getGitInfo();
199
+ }
200
+
201
+ // Write to output if specified
202
+ if (options.output) {
203
+ const outputContent = this._generateSnapshotContent(contexts, summary, git);
204
+ await fs.writeFile(options.output, outputContent);
205
+ }
206
+
207
+ return {
208
+ contexts,
209
+ summary,
210
+ timestamp,
211
+ ...(git && { git })
212
+ };
213
+ }
214
+
215
+ // ==========================================
216
+ // 3. CONTEXT UPDATES (1 METHOD)
217
+ // ==========================================
218
+
219
+ /**
220
+ * Update existing context
221
+ *
222
+ * @param {string} type - Context type to update
223
+ * @param {object} updates - Update data
224
+ * @param {string} [updates.mode] - Update mode: 'append' or 'replace'
225
+ * @param {string} updates.content - New content
226
+ * @returns {Promise<{updated, changes, timestamp}>}
227
+ * @throws {Error} If context not found
228
+ *
229
+ * @example
230
+ * await updateContext('project-brief', {
231
+ * mode: 'append',
232
+ * content: '## New Section'
233
+ * })
234
+ */
235
+ async updateContext(type, updates = {}) {
236
+ const contextFile = `${type}.md`;
237
+ const contextPath = path.join(this.contextPath, contextFile);
238
+
239
+ // Check if context exists
240
+ const exists = await fs.pathExists(contextPath);
241
+ if (!exists) {
242
+ throw new Error(`Context not found: ${type}`);
243
+ }
244
+
245
+ const mode = updates.mode || 'append';
246
+ let newContent;
247
+
248
+ if (mode === 'replace') {
249
+ newContent = updates.content;
250
+ } else {
251
+ // Append mode
252
+ const existingContent = await fs.readFile(contextPath, 'utf8');
253
+ newContent = existingContent + updates.content;
254
+ }
255
+
256
+ // Write updated content
257
+ await fs.writeFile(contextPath, newContent);
258
+
259
+ const timestamp = new Date().toISOString();
260
+
261
+ return {
262
+ updated: true,
263
+ changes: {
264
+ mode,
265
+ timestamp
266
+ },
267
+ timestamp
268
+ };
269
+ }
270
+
271
+ // ==========================================
272
+ // 4. CONTEXT READING (2 METHODS)
273
+ // ==========================================
274
+
275
+ /**
276
+ * Read context file
277
+ *
278
+ * @param {string} type - Context type
279
+ * @returns {Promise<{type, content, metadata, updated}>}
280
+ * @throws {Error} If context not found
281
+ *
282
+ * @example
283
+ * const context = await getContext('project-brief')
284
+ */
285
+ async getContext(type) {
286
+ const contextFile = `${type}.md`;
287
+ const contextPath = path.join(this.contextPath, contextFile);
288
+
289
+ // Check if context exists
290
+ const exists = await fs.pathExists(contextPath);
291
+ if (!exists) {
292
+ throw new Error(`Context not found: ${type}`);
293
+ }
294
+
295
+ const content = await fs.readFile(contextPath, 'utf8');
296
+ const metadata = this._parseFrontmatter(content);
297
+ const stats = await fs.stat(contextPath);
298
+
299
+ return {
300
+ type,
301
+ content,
302
+ metadata,
303
+ updated: stats.mtime.toISOString()
304
+ };
305
+ }
306
+
307
+ /**
308
+ * List all contexts
309
+ *
310
+ * @returns {Promise<{contexts, byType}>}
311
+ *
312
+ * @example
313
+ * const { contexts, byType } = await listContexts()
314
+ */
315
+ async listContexts() {
316
+ const exists = await fs.pathExists(this.contextPath);
317
+ if (!exists) {
318
+ return {
319
+ contexts: [],
320
+ byType: {}
321
+ };
322
+ }
323
+
324
+ const files = await fs.readdir(this.contextPath);
325
+ const contexts = [];
326
+ const byType = {};
327
+
328
+ for (const file of files) {
329
+ if (file.endsWith('.md')) {
330
+ const filePath = path.join(this.contextPath, file);
331
+ const content = await fs.readFile(filePath, 'utf8');
332
+ const metadata = this._parseFrontmatter(content);
333
+ const stats = await fs.stat(filePath);
334
+
335
+ const contextType = metadata.type || 'unknown';
336
+
337
+ const contextInfo = {
338
+ file,
339
+ type: contextType,
340
+ metadata,
341
+ updated: stats.mtime.toISOString(),
342
+ size: stats.size
343
+ };
344
+
345
+ contexts.push(contextInfo);
346
+
347
+ if (!byType[contextType]) {
348
+ byType[contextType] = [];
349
+ }
350
+ byType[contextType].push(contextInfo);
351
+ }
352
+ }
353
+
354
+ return {
355
+ contexts,
356
+ byType
357
+ };
358
+ }
359
+
360
+ // ==========================================
361
+ // 5. CONTEXT VALIDATION (1 METHOD)
362
+ // ==========================================
363
+
364
+ /**
365
+ * Validate context structure
366
+ *
367
+ * @param {string} type - Context type
368
+ * @param {string} content - Context content
369
+ * @returns {Promise<{valid, errors}>}
370
+ *
371
+ * @example
372
+ * const { valid, errors } = await validateContext('project-brief', content)
373
+ */
374
+ async validateContext(type, content) {
375
+ const errors = [];
376
+
377
+ // Check for frontmatter
378
+ if (!content.startsWith('---')) {
379
+ errors.push('Missing frontmatter');
380
+ return { valid: false, errors };
381
+ }
382
+
383
+ // Parse frontmatter
384
+ const metadata = this._parseFrontmatter(content);
385
+
386
+ // Validate required fields based on type
387
+ const requiredFields = {
388
+ 'project-brief': ['type', 'name', 'created'],
389
+ 'progress': ['type', 'name'],
390
+ 'tech-context': ['type', 'name'],
391
+ 'default': ['type']
392
+ };
393
+
394
+ const required = requiredFields[type] || requiredFields.default;
395
+
396
+ for (const field of required) {
397
+ if (!metadata[field]) {
398
+ errors.push(`Missing required field: ${field}`);
399
+ }
400
+ }
401
+
402
+ return {
403
+ valid: errors.length === 0,
404
+ errors
405
+ };
406
+ }
407
+
408
+ // ==========================================
409
+ // 6. CONTEXT OPERATIONS (2 METHODS)
410
+ // ==========================================
411
+
412
+ /**
413
+ * Merge multiple contexts
414
+ *
415
+ * @param {Array} contexts - Array of context objects
416
+ * @returns {Promise<{merged, sources}>}
417
+ *
418
+ * @example
419
+ * const result = await mergeContexts([context1, context2])
420
+ */
421
+ async mergeContexts(contexts) {
422
+ const sources = contexts.map(c => c.type);
423
+ let merged = '';
424
+ const seenSections = new Set();
425
+
426
+ for (const context of contexts) {
427
+ const lines = context.content.split('\n');
428
+
429
+ for (const line of lines) {
430
+ // Track sections to avoid duplicates
431
+ if (line.startsWith('##')) {
432
+ const sectionKey = line.trim();
433
+ if (seenSections.has(sectionKey)) {
434
+ continue; // Skip duplicate section
435
+ }
436
+ seenSections.add(sectionKey);
437
+ }
438
+
439
+ merged += line + '\n';
440
+ }
441
+
442
+ merged += '\n---\n\n';
443
+ }
444
+
445
+ return {
446
+ merged: merged.trim(),
447
+ sources
448
+ };
449
+ }
450
+
451
+ /**
452
+ * Analyze context usage
453
+ *
454
+ * @returns {Promise<{stats, recommendations}>}
455
+ *
456
+ * @example
457
+ * const { stats, recommendations } = await analyzeContextUsage()
458
+ */
459
+ async analyzeContextUsage() {
460
+ const { contexts } = await this.listContexts();
461
+
462
+ let totalSize = 0;
463
+ const recommendations = [];
464
+
465
+ for (const context of contexts) {
466
+ totalSize += context.size;
467
+
468
+ // Check for old contexts (> 90 days)
469
+ const updated = new Date(context.updated);
470
+ const daysSinceUpdate = Math.floor(
471
+ (Date.now() - updated.getTime()) / (1000 * 60 * 60 * 24)
472
+ );
473
+
474
+ if (daysSinceUpdate > 90) {
475
+ recommendations.push(
476
+ `Context "${context.file}" is ${daysSinceUpdate} days old - consider archiving or updating`
477
+ );
478
+ }
479
+
480
+ // Check for large contexts (> 50KB)
481
+ if (context.size > 50000) {
482
+ recommendations.push(
483
+ `Context "${context.file}" is ${Math.round(context.size / 1024)}KB - consider splitting into smaller contexts`
484
+ );
485
+ }
486
+ }
487
+
488
+ const stats = {
489
+ totalContexts: contexts.length,
490
+ totalSize,
491
+ averageSize: contexts.length > 0 ? Math.round(totalSize / contexts.length) : 0
492
+ };
493
+
494
+ return {
495
+ stats,
496
+ recommendations
497
+ };
498
+ }
499
+
500
+ // ==========================================
501
+ // PRIVATE HELPER METHODS
502
+ // ==========================================
503
+
504
+ /**
505
+ * Parse YAML frontmatter from content
506
+ * @private
507
+ */
508
+ _parseFrontmatter(content) {
509
+ if (!content || !content.startsWith('---')) {
510
+ return {};
511
+ }
512
+
513
+ const match = content.match(/^---\n([\s\S]*?)\n---/);
514
+ if (!match) {
515
+ return {};
516
+ }
517
+
518
+ const metadata = {};
519
+ const lines = match[1].split('\n');
520
+
521
+ for (const line of lines) {
522
+ const colonIndex = line.indexOf(':');
523
+ if (colonIndex === -1) continue;
524
+
525
+ const key = line.substring(0, colonIndex).trim();
526
+ const value = line.substring(colonIndex + 1).trim();
527
+
528
+ if (key) {
529
+ metadata[key] = value;
530
+ }
531
+ }
532
+
533
+ return metadata;
534
+ }
535
+
536
+ /**
537
+ * Get git information
538
+ * @private
539
+ */
540
+ async _getGitInfo() {
541
+ const { execSync } = require('child_process');
542
+
543
+ try {
544
+ const branch = execSync('git rev-parse --abbrev-ref HEAD', { encoding: 'utf8' }).trim();
545
+ const commit = execSync('git rev-parse HEAD', { encoding: 'utf8' }).trim();
546
+ const status = execSync('git status --short', { encoding: 'utf8' }).trim();
547
+
548
+ return {
549
+ branch,
550
+ commit,
551
+ status: status || 'clean'
552
+ };
553
+ } catch (error) {
554
+ return {
555
+ error: 'Not a git repository or git not available'
556
+ };
557
+ }
558
+ }
559
+
560
+ /**
561
+ * Generate snapshot content for output
562
+ * @private
563
+ */
564
+ _generateSnapshotContent(contexts, summary, git) {
565
+ let content = `# Project Snapshot\n\n`;
566
+ content += `${summary}\n\n`;
567
+
568
+ if (git) {
569
+ content += `## Git Information\n`;
570
+ content += `- Branch: ${git.branch}\n`;
571
+ content += `- Commit: ${git.commit}\n`;
572
+ content += `- Status: ${git.status}\n\n`;
573
+ }
574
+
575
+ if (contexts.epics.length > 0) {
576
+ content += `## Epics (${contexts.epics.length})\n\n`;
577
+ for (const epic of contexts.epics) {
578
+ content += `### ${epic.file}\n`;
579
+ content += `${epic.content}\n\n---\n\n`;
580
+ }
581
+ }
582
+
583
+ if (contexts.issues.length > 0) {
584
+ content += `## Issues (${contexts.issues.length})\n\n`;
585
+ for (const issue of contexts.issues) {
586
+ content += `### ${issue.file}\n`;
587
+ content += `${issue.content}\n\n---\n\n`;
588
+ }
589
+ }
590
+
591
+ return content;
592
+ }
593
+ }
594
+
595
+ module.exports = ContextService;