claude-autopm 1.30.1 → 2.1.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.
Files changed (36) hide show
  1. package/autopm/.claude/mcp/test-server.md +10 -0
  2. package/autopm/.claude/scripts/github/dependency-tracker.js +554 -0
  3. package/autopm/.claude/scripts/github/dependency-validator.js +545 -0
  4. package/autopm/.claude/scripts/github/dependency-visualizer.js +477 -0
  5. package/autopm/.claude/scripts/pm/lib/epic-discovery.js +119 -0
  6. package/autopm/.claude/scripts/pm/next.js +56 -58
  7. package/bin/autopm-poc.js +348 -0
  8. package/bin/autopm.js +6 -0
  9. package/lib/ai-providers/AbstractAIProvider.js +524 -0
  10. package/lib/ai-providers/ClaudeProvider.js +423 -0
  11. package/lib/ai-providers/TemplateProvider.js +432 -0
  12. package/lib/cli/commands/agent.js +206 -0
  13. package/lib/cli/commands/config.js +488 -0
  14. package/lib/cli/commands/prd.js +345 -0
  15. package/lib/cli/commands/task.js +206 -0
  16. package/lib/config/ConfigManager.js +531 -0
  17. package/lib/errors/AIProviderError.js +164 -0
  18. package/lib/services/AgentService.js +557 -0
  19. package/lib/services/EpicService.js +609 -0
  20. package/lib/services/PRDService.js +1003 -0
  21. package/lib/services/TaskService.js +760 -0
  22. package/lib/services/interfaces.js +753 -0
  23. package/lib/utils/CircuitBreaker.js +165 -0
  24. package/lib/utils/Encryption.js +201 -0
  25. package/lib/utils/RateLimiter.js +241 -0
  26. package/lib/utils/ServiceFactory.js +165 -0
  27. package/package.json +9 -5
  28. package/scripts/config/get.js +108 -0
  29. package/scripts/config/init.js +100 -0
  30. package/scripts/config/list-providers.js +93 -0
  31. package/scripts/config/set-api-key.js +107 -0
  32. package/scripts/config/set-provider.js +201 -0
  33. package/scripts/config/set.js +139 -0
  34. package/scripts/config/show.js +181 -0
  35. package/autopm/.claude/.env +0 -158
  36. package/autopm/.claude/settings.local.json +0 -9
@@ -0,0 +1,477 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * GitHub Issue Dependency Visualizer
4
+ *
5
+ * Generates visual representations of issue dependency graphs:
6
+ * - ASCII tree output for console
7
+ * - Mermaid diagram format
8
+ * - Graphviz DOT format
9
+ * - JSON graph data
10
+ *
11
+ * @module dependency-visualizer
12
+ */
13
+
14
+ const fs = require('fs');
15
+ const path = require('path');
16
+ const DependencyTracker = require('./dependency-tracker.js');
17
+
18
+ /**
19
+ * DependencyVisualizer class for visualizing GitHub issue dependencies
20
+ */
21
+ class DependencyVisualizer {
22
+ /**
23
+ * Initialize dependency visualizer
24
+ *
25
+ * @param {Object} config - Configuration options
26
+ * @param {string} config.owner - GitHub repository owner
27
+ * @param {string} config.repo - GitHub repository name
28
+ * @param {string} config.token - GitHub personal access token
29
+ * @param {boolean} config.localMode - Enable local mode
30
+ * @param {string} config.cacheDir - Directory for local cache
31
+ */
32
+ constructor(config = {}) {
33
+ this.localMode = config.localMode || false;
34
+ this.cacheDir = config.cacheDir || path.join(process.cwd(), '.claude', 'cache');
35
+ this.tracker = new DependencyTracker(config);
36
+ }
37
+
38
+ /**
39
+ * Generate dependency graph data structure
40
+ *
41
+ * @param {number} issueNumber - Root issue to start from
42
+ * @param {Object} options - Generation options
43
+ * @param {number} options.depth - Maximum depth to traverse
44
+ * @returns {Promise<Object>} Graph with nodes and edges
45
+ */
46
+ async generateGraph(issueNumber, options = {}) {
47
+ const maxDepth = options.depth || 10;
48
+ const nodes = [];
49
+ const edges = [];
50
+ const visited = new Set();
51
+ let hasCircular = false;
52
+
53
+ /**
54
+ * Recursively build graph
55
+ */
56
+ const buildGraph = async (current, depth = 0) => {
57
+ if (depth > maxDepth) return;
58
+
59
+ if (visited.has(current)) {
60
+ hasCircular = true;
61
+ return;
62
+ }
63
+
64
+ visited.add(current);
65
+
66
+ try {
67
+ // Get issue details
68
+ let issueData;
69
+
70
+ if (this.localMode) {
71
+ issueData = await this._getIssueLocal(current);
72
+ } else {
73
+ const issue = await this.tracker.octokit.rest.issues.get({
74
+ owner: this.tracker.owner,
75
+ repo: this.tracker.repo,
76
+ issue_number: current
77
+ });
78
+ issueData = issue.data;
79
+ }
80
+
81
+ // Add node
82
+ nodes.push({
83
+ number: issueData.number,
84
+ title: issueData.title,
85
+ state: issueData.state,
86
+ labels: issueData.labels || [],
87
+ assignees: issueData.assignees || []
88
+ });
89
+
90
+ // Get dependencies
91
+ const dependencies = await this.tracker.getDependencies(current);
92
+
93
+ // Add edges and recurse
94
+ for (const dep of dependencies) {
95
+ edges.push({
96
+ from: current,
97
+ to: dep
98
+ });
99
+
100
+ await buildGraph(dep, depth + 1);
101
+ }
102
+ } catch (error) {
103
+ // Add error node
104
+ nodes.push({
105
+ number: current,
106
+ title: 'Error loading issue',
107
+ state: 'error',
108
+ error: error.message
109
+ });
110
+ }
111
+ };
112
+
113
+ try {
114
+ await buildGraph(issueNumber);
115
+
116
+ return {
117
+ nodes,
118
+ edges,
119
+ circular: hasCircular,
120
+ rootIssue: issueNumber
121
+ };
122
+ } catch (error) {
123
+ return {
124
+ nodes: [],
125
+ edges: [],
126
+ error: error.message,
127
+ rootIssue: issueNumber
128
+ };
129
+ }
130
+ }
131
+
132
+ /**
133
+ * Get issue from local cache
134
+ * @private
135
+ */
136
+ async _getIssueLocal(issueNumber) {
137
+ const issuesFile = path.join(this.cacheDir, 'issues.json');
138
+
139
+ if (!fs.existsSync(issuesFile)) {
140
+ return {
141
+ number: issueNumber,
142
+ title: `Issue #${issueNumber}`,
143
+ state: 'unknown',
144
+ labels: [],
145
+ assignees: []
146
+ };
147
+ }
148
+
149
+ const issues = JSON.parse(fs.readFileSync(issuesFile, 'utf8'));
150
+ return issues[issueNumber.toString()] || {
151
+ number: issueNumber,
152
+ title: `Issue #${issueNumber}`,
153
+ state: 'unknown',
154
+ labels: [],
155
+ assignees: []
156
+ };
157
+ }
158
+
159
+ /**
160
+ * Export dependency graph as Mermaid diagram
161
+ *
162
+ * @param {number} issueNumber - Root issue
163
+ * @param {Object} options - Export options
164
+ * @param {boolean} options.showBlockedBy - Show issues this blocks
165
+ * @param {string} options.outputFile - File to save to
166
+ * @returns {Promise<string>} Mermaid diagram code
167
+ */
168
+ async exportMermaid(issueNumber, options = {}) {
169
+ const graph = await this.generateGraph(issueNumber);
170
+ const lines = ['graph TD'];
171
+
172
+ // Add nodes
173
+ for (const node of graph.nodes) {
174
+ const title = this._escapeMermaid(node.title);
175
+ const label = `#${node.number}: ${title}`;
176
+ lines.push(` ${node.number}["${label}"]`);
177
+
178
+ // Add styling based on state
179
+ if (node.state === 'closed') {
180
+ lines.push(` style ${node.number} fill:#d1ffd1,stroke:#4caf50`);
181
+ } else if (node.state === 'open') {
182
+ lines.push(` style ${node.number} fill:#fff3cd,stroke:#ff9800`);
183
+ } else if (node.state === 'error') {
184
+ lines.push(` style ${node.number} fill:#ffcccc,stroke:#f44336`);
185
+ }
186
+ }
187
+
188
+ // Add edges
189
+ for (const edge of graph.edges) {
190
+ lines.push(` ${edge.from} --> ${edge.to}`);
191
+ }
192
+
193
+ // Add blocked-by relationships if requested
194
+ if (options.showBlockedBy) {
195
+ try {
196
+ const blocked = await this.tracker.getBlockedIssues(issueNumber);
197
+ for (const blockedNum of blocked) {
198
+ if (!graph.nodes.find(n => n.number === blockedNum)) {
199
+ const blockedIssue = await this.tracker.octokit.rest.issues.get({
200
+ owner: this.tracker.owner,
201
+ repo: this.tracker.repo,
202
+ issue_number: blockedNum
203
+ });
204
+
205
+ const title = this._escapeMermaid(blockedIssue.data.title);
206
+ lines.push(` ${blockedNum}["#${blockedNum}: ${title}"]`);
207
+ lines.push(` ${blockedNum} -.-> ${issueNumber}`);
208
+ }
209
+ }
210
+ } catch {
211
+ // Silently ignore errors
212
+ }
213
+ }
214
+
215
+ const mermaid = lines.join('\n');
216
+
217
+ // Save to file if requested
218
+ if (options.outputFile) {
219
+ fs.writeFileSync(options.outputFile, mermaid, 'utf8');
220
+ }
221
+
222
+ return mermaid;
223
+ }
224
+
225
+ /**
226
+ * Escape special characters for Mermaid
227
+ * @private
228
+ */
229
+ _escapeMermaid(text) {
230
+ if (!text) return '';
231
+ return text
232
+ .replace(/"/g, '&#34;')
233
+ .replace(/\[/g, '&#91;')
234
+ .replace(/\]/g, '&#93;')
235
+ .replace(/\(/g, '&#40;')
236
+ .replace(/\)/g, '&#41;')
237
+ .substring(0, 50); // Limit length
238
+ }
239
+
240
+ /**
241
+ * Export dependency graph as Graphviz DOT format
242
+ *
243
+ * @param {number} issueNumber - Root issue
244
+ * @param {Object} options - Export options
245
+ * @param {string} options.outputFile - File to save to
246
+ * @returns {Promise<string>} DOT format code
247
+ */
248
+ async exportGraphviz(issueNumber, options = {}) {
249
+ const graph = await this.generateGraph(issueNumber);
250
+ const lines = ['digraph dependencies {'];
251
+ lines.push(' rankdir=LR;');
252
+ lines.push(' node [shape=box, style=filled];');
253
+
254
+ // Add nodes
255
+ for (const node of graph.nodes) {
256
+ const title = this._escapeGraphviz(node.title);
257
+ const label = `#${node.number}: ${title}`;
258
+
259
+ let color = 'lightblue';
260
+ if (node.state === 'closed') {
261
+ color = 'lightgreen';
262
+ } else if (node.state === 'error') {
263
+ color = 'lightcoral';
264
+ }
265
+
266
+ lines.push(` ${node.number} [label="${label}", fillcolor="${color}"];`);
267
+ }
268
+
269
+ // Add edges
270
+ for (const edge of graph.edges) {
271
+ lines.push(` ${edge.from} -> ${edge.to};`);
272
+ }
273
+
274
+ lines.push('}');
275
+
276
+ const dot = lines.join('\n');
277
+
278
+ // Save to file if requested
279
+ if (options.outputFile) {
280
+ fs.writeFileSync(options.outputFile, dot, 'utf8');
281
+ }
282
+
283
+ return dot;
284
+ }
285
+
286
+ /**
287
+ * Escape special characters for Graphviz
288
+ * @private
289
+ */
290
+ _escapeGraphviz(text) {
291
+ if (!text) return '';
292
+ return text
293
+ .replace(/"/g, '\\"')
294
+ .replace(/\n/g, '\\n')
295
+ .substring(0, 50);
296
+ }
297
+
298
+ /**
299
+ * Print dependency tree to console
300
+ *
301
+ * @param {number} issueNumber - Root issue
302
+ * @param {Object} options - Print options
303
+ * @returns {Promise<void>}
304
+ */
305
+ async printTree(issueNumber, options = {}) {
306
+ const visited = new Set();
307
+
308
+ /**
309
+ * Recursively print tree
310
+ */
311
+ const printNode = async (current, prefix = '', isLast = true, depth = 0) => {
312
+ if (depth > 10) return;
313
+
314
+ if (visited.has(current)) {
315
+ console.log(`${prefix}${isLast ? '└── ' : '├── '}#${current} (circular)`);
316
+ return;
317
+ }
318
+
319
+ visited.add(current);
320
+
321
+ try {
322
+ // Get issue details
323
+ let issueData;
324
+
325
+ if (this.localMode) {
326
+ issueData = await this._getIssueLocal(current);
327
+ } else {
328
+ const issue = await this.tracker.octokit.rest.issues.get({
329
+ owner: this.tracker.owner,
330
+ repo: this.tracker.repo,
331
+ issue_number: current
332
+ });
333
+ issueData = issue.data;
334
+ }
335
+
336
+ // Print node
337
+ const icon = issueData.state === 'closed' ? '✓' : '○';
338
+ const title = issueData.title.substring(0, 60);
339
+ console.log(`${prefix}${isLast ? '└── ' : '├── '}${icon} #${current}: ${title}`);
340
+
341
+ // Get and print dependencies
342
+ const dependencies = await this.tracker.getDependencies(current);
343
+
344
+ if (dependencies.length > 0) {
345
+ const newPrefix = prefix + (isLast ? ' ' : '│ ');
346
+
347
+ for (let i = 0; i < dependencies.length; i++) {
348
+ const dep = dependencies[i];
349
+ const isLastDep = i === dependencies.length - 1;
350
+ await printNode(dep, newPrefix, isLastDep, depth + 1);
351
+ }
352
+ }
353
+ } catch (error) {
354
+ console.log(`${prefix}${isLast ? '└── ' : '├── '}✗ #${current}: Error - ${error.message}`);
355
+ }
356
+ };
357
+
358
+ console.log('\nDependency Tree:');
359
+ console.log('═'.repeat(50));
360
+ await printNode(issueNumber);
361
+ console.log('');
362
+ }
363
+
364
+ /**
365
+ * Save graph data as JSON
366
+ *
367
+ * @param {Object} graph - Graph data from generateGraph
368
+ * @param {string} filePath - File path to save to
369
+ */
370
+ saveGraphData(graph, filePath) {
371
+ fs.writeFileSync(filePath, JSON.stringify(graph, null, 2), 'utf8');
372
+ }
373
+
374
+ /**
375
+ * Generate summary statistics for graph
376
+ *
377
+ * @param {Object} graph - Graph data
378
+ * @returns {Object} Statistics
379
+ */
380
+ getGraphStats(graph) {
381
+ const total = graph.nodes.length;
382
+ const open = graph.nodes.filter(n => n.state === 'open').length;
383
+ const closed = graph.nodes.filter(n => n.state === 'closed').length;
384
+ const errors = graph.nodes.filter(n => n.state === 'error').length;
385
+
386
+ return {
387
+ total,
388
+ open,
389
+ closed,
390
+ errors,
391
+ edges: graph.edges.length,
392
+ circular: graph.circular,
393
+ progress: total > 0 ? Math.round((closed / total) * 100) : 0
394
+ };
395
+ }
396
+ }
397
+
398
+ // CLI interface
399
+ if (require.main === module) {
400
+ const command = process.argv[2];
401
+ const issueNumber = parseInt(process.argv[3], 10);
402
+ const outputFile = process.argv[4];
403
+
404
+ const visualizer = new DependencyVisualizer();
405
+
406
+ (async () => {
407
+ switch (command) {
408
+ case 'tree':
409
+ await visualizer.printTree(issueNumber);
410
+ break;
411
+
412
+ case 'mermaid':
413
+ const mermaid = await visualizer.exportMermaid(issueNumber, {
414
+ outputFile
415
+ });
416
+ if (!outputFile) {
417
+ console.log(mermaid);
418
+ } else {
419
+ console.log(`✓ Mermaid diagram saved to: ${outputFile}`);
420
+ }
421
+ break;
422
+
423
+ case 'graphviz':
424
+ case 'dot':
425
+ const dot = await visualizer.exportGraphviz(issueNumber, {
426
+ outputFile
427
+ });
428
+ if (!outputFile) {
429
+ console.log(dot);
430
+ } else {
431
+ console.log(`✓ Graphviz DOT saved to: ${outputFile}`);
432
+ }
433
+ break;
434
+
435
+ case 'json':
436
+ const graph = await visualizer.generateGraph(issueNumber);
437
+ if (outputFile) {
438
+ visualizer.saveGraphData(graph, outputFile);
439
+ console.log(`✓ Graph data saved to: ${outputFile}`);
440
+ } else {
441
+ console.log(JSON.stringify(graph, null, 2));
442
+ }
443
+ break;
444
+
445
+ case 'stats':
446
+ const statsGraph = await visualizer.generateGraph(issueNumber);
447
+ const stats = visualizer.getGraphStats(statsGraph);
448
+
449
+ console.log('\nDependency Graph Statistics:');
450
+ console.log('─'.repeat(50));
451
+ console.log(`Total Issues: ${stats.total}`);
452
+ console.log(`Open: ${stats.open}`);
453
+ console.log(`Closed: ${stats.closed}`);
454
+ console.log(`Errors: ${stats.errors}`);
455
+ console.log(`Dependencies: ${stats.edges}`);
456
+ console.log(`Circular: ${stats.circular ? 'YES ⚠' : 'NO'}`);
457
+ console.log(`Progress: ${stats.progress}%`);
458
+ break;
459
+
460
+ default:
461
+ console.error('Usage: dependency-visualizer.js <tree|mermaid|graphviz|json|stats> <issue> [output-file]');
462
+ console.error('');
463
+ console.error('Examples:');
464
+ console.error(' dependency-visualizer.js tree 123');
465
+ console.error(' dependency-visualizer.js mermaid 123 diagram.mmd');
466
+ console.error(' dependency-visualizer.js graphviz 123 graph.dot');
467
+ console.error(' dependency-visualizer.js json 123 data.json');
468
+ console.error(' dependency-visualizer.js stats 123');
469
+ process.exit(1);
470
+ }
471
+ })().catch(error => {
472
+ console.error('Error:', error.message);
473
+ process.exit(1);
474
+ });
475
+ }
476
+
477
+ module.exports = DependencyVisualizer;
@@ -0,0 +1,119 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ /**
5
+ * Epic Discovery Utilities
6
+ * Shared utilities for discovering epic directories in both flat and nested structures
7
+ */
8
+
9
+ /**
10
+ * Helper function to check if directory contains epic.md
11
+ * @param {string} dirPath - Path to check
12
+ * @returns {boolean} - True if directory contains epic.md
13
+ */
14
+ function isEpicDir(dirPath) {
15
+ return fs.existsSync(path.join(dirPath, 'epic.md'));
16
+ }
17
+
18
+ /**
19
+ * Recursive function to find all epic directories
20
+ * Supports both flat and nested multi-epic structures
21
+ * @param {string} basePath - Base path to search from
22
+ * @param {number} depth - Current recursion depth (max 3 levels)
23
+ * @returns {Array<{name: string, path: string}>} - Array of epic directories
24
+ */
25
+ function findEpicDirs(basePath, depth = 0) {
26
+ if (depth >= 3) {
27
+ if (process.env.DEBUG) {
28
+ console.error(`Maximum recursion depth reached at ${basePath}`);
29
+ }
30
+ return [];
31
+ }
32
+
33
+ const epicDirs = [];
34
+
35
+ try {
36
+ const entries = fs.readdirSync(basePath, { withFileTypes: true });
37
+
38
+ for (const entry of entries) {
39
+ if (!entry.isDirectory()) continue;
40
+
41
+ const fullPath = path.join(basePath, entry.name);
42
+
43
+ if (isEpicDir(fullPath)) {
44
+ // This is an epic directory
45
+ const relativePath = path.relative('.claude/epics', fullPath);
46
+ epicDirs.push({ name: relativePath, path: fullPath });
47
+ } else {
48
+ // Check subdirectories (multi-epic structure)
49
+ epicDirs.push(...findEpicDirs(fullPath, depth + 1));
50
+ }
51
+ }
52
+ } catch (err) {
53
+ // Log but don't fail
54
+ if (process.env.DEBUG) {
55
+ console.error(`Error reading directory ${basePath}:`, err.message);
56
+ }
57
+ }
58
+
59
+ return epicDirs;
60
+ }
61
+
62
+ /**
63
+ * Find all epic directories supporting both flat and nested structures
64
+ * @returns {Array<{name: string, path: string}>} - Array of epic directories
65
+ */
66
+ function findAllEpicDirs() {
67
+ const epicsPath = '.claude/epics';
68
+
69
+ if (!fs.existsSync(epicsPath)) {
70
+ return [];
71
+ }
72
+
73
+ const epicDirs = [];
74
+
75
+ try {
76
+ // First check if top-level directories are epic dirs (flat structure)
77
+ // or if we need to search recursively (nested structure)
78
+ const topLevelEntries = fs.readdirSync(epicsPath, { withFileTypes: true })
79
+ .filter(dirent => dirent.isDirectory());
80
+
81
+ for (const entry of topLevelEntries) {
82
+ const fullPath = path.join(epicsPath, entry.name);
83
+
84
+ try {
85
+ // Check if this directory has task files (flat epic) or subdirectories (nested structure)
86
+ const hasTaskFiles = fs.readdirSync(fullPath)
87
+ .some(file => /^\d+.*\.md$/.test(file));
88
+
89
+ const hasEpicMd = isEpicDir(fullPath);
90
+
91
+ if (hasTaskFiles || hasEpicMd) {
92
+ // This is a flat epic directory (backward compatible with epics without epic.md)
93
+ epicDirs.push({ name: entry.name, path: fullPath });
94
+ } else {
95
+ // This might be a parent directory for nested epics
96
+ epicDirs.push(...findEpicDirs(fullPath, 0));
97
+ }
98
+ } catch (err) {
99
+ // Log but don't fail for unreadable directories
100
+ if (process.env.DEBUG) {
101
+ console.error(`Error checking directory ${fullPath}:`, err.message);
102
+ }
103
+ }
104
+ }
105
+ } catch (err) {
106
+ // Log top-level errors in DEBUG mode
107
+ if (process.env.DEBUG) {
108
+ console.error(`Error in findAllEpicDirs:`, err.message);
109
+ }
110
+ }
111
+
112
+ return epicDirs;
113
+ }
114
+
115
+ module.exports = {
116
+ isEpicDir,
117
+ findEpicDirs,
118
+ findAllEpicDirs
119
+ };