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,557 @@
1
+ /**
2
+ * AgentService - Standalone Agent Invocation System
3
+ *
4
+ * Provides programmatic access to ClaudeAutoPM agents without requiring
5
+ * the full framework. Enables agent invocation with AI provider abstraction.
6
+ *
7
+ * Features:
8
+ * - Load and parse agent metadata from .md files
9
+ * - Invoke agents with structured prompts (XML-based per Anthropic best practices)
10
+ * - Stream agent responses with AsyncGenerator
11
+ * - List and search available agents
12
+ * - Multi-turn conversations with context persistence
13
+ *
14
+ * Architecture:
15
+ * - Uses AbstractAIProvider interface for AI provider abstraction
16
+ * - Follows Anthropic prompt engineering best practices (XML tags, chain-of-thought)
17
+ * - Parses agent markdown to extract specialization, tools, documentation queries
18
+ * - Injects Context7 queries into system prompts
19
+ *
20
+ * Usage:
21
+ * const service = new AgentService(aiProvider);
22
+ * const result = await service.invoke('code-analyzer', 'Review security', context);
23
+ *
24
+ * @class AgentService
25
+ */
26
+
27
+ const fs = require('fs').promises;
28
+ const path = require('path');
29
+
30
+ class AgentService {
31
+ /**
32
+ * Create an AgentService instance
33
+ * @param {AbstractAIProvider} aiProvider - AI provider instance for completions
34
+ */
35
+ constructor(aiProvider) {
36
+ if (!aiProvider) {
37
+ throw new Error('AgentService requires an AI provider instance');
38
+ }
39
+
40
+ this.aiProvider = aiProvider;
41
+ this.agentsBaseDir = path.join(process.cwd(), 'autopm/.claude/agents');
42
+ this.conversationHistories = new Map();
43
+
44
+ // Cache for loaded agents (performance optimization)
45
+ this._agentCache = new Map();
46
+ }
47
+
48
+ /**
49
+ * Load agent metadata from .md file
50
+ *
51
+ * Searches for agent in subdirectories (core/, cloud/, devops/, etc.)
52
+ * and parses the markdown content to extract metadata.
53
+ *
54
+ * @param {string} agentName - Name of the agent (without .md extension)
55
+ * @returns {Promise<Object>} Agent metadata
56
+ * @throws {Error} If agent not found or failed to parse
57
+ */
58
+ async loadAgent(agentName) {
59
+ // Check cache first
60
+ if (this._agentCache.has(agentName)) {
61
+ return this._agentCache.get(agentName);
62
+ }
63
+
64
+ try {
65
+ // Find agent file in subdirectories
66
+ const agentPath = await this._findAgentFile(agentName);
67
+
68
+ if (!agentPath) {
69
+ throw new Error(`Agent not found: ${agentName}`);
70
+ }
71
+
72
+ // Read and parse agent file
73
+ const content = await fs.readFile(agentPath, 'utf-8');
74
+ const metadata = this.parseAgent(content);
75
+
76
+ // Validate that agent has required fields
77
+ if (!metadata.title) {
78
+ throw new Error(`Agent ${agentName} is missing required field: title`);
79
+ }
80
+
81
+ // Add name and path to metadata
82
+ metadata.name = agentName;
83
+ metadata.path = agentPath;
84
+
85
+ // Determine category from path
86
+ metadata.category = this._extractCategory(agentPath);
87
+
88
+ // Cache the result
89
+ this._agentCache.set(agentName, metadata);
90
+
91
+ return metadata;
92
+
93
+ } catch (error) {
94
+ if (error.message.includes('Agent not found')) {
95
+ throw error;
96
+ }
97
+
98
+ // Wrap parsing errors
99
+ throw new Error(`Failed to parse agent ${agentName}: ${error.message}`);
100
+ }
101
+ }
102
+
103
+ /**
104
+ * Parse agent markdown content
105
+ *
106
+ * Extracts:
107
+ * - Title (# heading)
108
+ * - Specialization (**Specialization:** line)
109
+ * - Documentation Queries (mcp:// links)
110
+ * - Methodologies (**Methodologies:** section)
111
+ * - Tools (**Tools:** line)
112
+ *
113
+ * @param {string} markdownContent - Raw markdown content
114
+ * @returns {Object} Parsed agent metadata
115
+ */
116
+ parseAgent(markdownContent) {
117
+ const metadata = {
118
+ title: '',
119
+ specialization: '',
120
+ documentationQueries: [],
121
+ methodologies: [],
122
+ tools: '',
123
+ rawContent: markdownContent
124
+ };
125
+
126
+ // Split into lines for parsing
127
+ const lines = markdownContent.split('\n');
128
+
129
+ let inDocQueriesSection = false;
130
+ let inMethodologiesSection = false;
131
+
132
+ for (let i = 0; i < lines.length; i++) {
133
+ const line = lines[i].trim();
134
+
135
+ // Extract title from # heading
136
+ if (line.startsWith('# ') && !metadata.title) {
137
+ metadata.title = line.substring(2).trim();
138
+ continue;
139
+ }
140
+
141
+ // Extract specialization
142
+ if (line.startsWith('**Specialization:**')) {
143
+ metadata.specialization = line
144
+ .substring('**Specialization:**'.length)
145
+ .trim();
146
+ continue;
147
+ }
148
+
149
+ // Start of Documentation Queries section
150
+ if (line.startsWith('**Documentation Queries:**')) {
151
+ inDocQueriesSection = true;
152
+ inMethodologiesSection = false;
153
+ continue;
154
+ }
155
+
156
+ // Start of Methodologies section
157
+ if (line.startsWith('**Methodologies:**')) {
158
+ inMethodologiesSection = true;
159
+ inDocQueriesSection = false;
160
+ continue;
161
+ }
162
+
163
+ // Extract tools
164
+ if (line.startsWith('**Tools:**')) {
165
+ metadata.tools = line
166
+ .substring('**Tools:**'.length)
167
+ .trim();
168
+ inDocQueriesSection = false;
169
+ inMethodologiesSection = false;
170
+ continue;
171
+ }
172
+
173
+ // Parse Documentation Queries
174
+ if (inDocQueriesSection && line.startsWith('-')) {
175
+ // Extract the full line (query and description)
176
+ const query = line.substring(1).trim();
177
+ if (query) {
178
+ metadata.documentationQueries.push(query);
179
+ }
180
+ continue;
181
+ }
182
+
183
+ // Parse Methodologies
184
+ if (inMethodologiesSection && line.startsWith('-')) {
185
+ const methodology = line.substring(1).trim();
186
+ if (methodology) {
187
+ metadata.methodologies.push(methodology);
188
+ }
189
+ continue;
190
+ }
191
+
192
+ // End sections if we hit another ** heading
193
+ if (line.startsWith('**') && !line.startsWith('**Documentation Queries:**') && !line.startsWith('**Methodologies:**')) {
194
+ inDocQueriesSection = false;
195
+ inMethodologiesSection = false;
196
+ }
197
+ }
198
+
199
+ return metadata;
200
+ }
201
+
202
+ /**
203
+ * Invoke an agent with a task and context
204
+ *
205
+ * Loads the agent, builds structured prompts with XML tags,
206
+ * and calls the AI provider's complete() method.
207
+ *
208
+ * @param {string} agentName - Name of the agent to invoke
209
+ * @param {string} task - Task description for the agent
210
+ * @param {Object} context - Additional context (will be JSON stringified)
211
+ * @param {Object} options - Options
212
+ * @param {string} [options.conversationId] - ID for multi-turn conversations
213
+ * @param {number} [options.maxTokens] - Override max tokens
214
+ * @param {number} [options.temperature] - Override temperature
215
+ * @returns {Promise<string>} Agent response
216
+ * @throws {Error} If agent not found or invocation fails
217
+ */
218
+ async invoke(agentName, task, context = {}, options = {}) {
219
+ try {
220
+ // Load agent metadata
221
+ const agentMetadata = await this.loadAgent(agentName);
222
+
223
+ // Build system prompt with agent role and Context7 queries
224
+ const systemPrompt = this._buildSystemPrompt(agentMetadata);
225
+
226
+ // Build user prompt with XML-structured task and context
227
+ const userPrompt = this._buildUserPrompt(task, context, agentName);
228
+
229
+ // Track conversation history if conversationId provided
230
+ let conversationHistory = null;
231
+ if (options.conversationId) {
232
+ if (!this.conversationHistories.has(options.conversationId)) {
233
+ this.conversationHistories.set(options.conversationId, []);
234
+ }
235
+ conversationHistory = this.conversationHistories.get(options.conversationId);
236
+ }
237
+
238
+ // Call AI provider
239
+ const response = await this.aiProvider.complete(userPrompt, {
240
+ system: systemPrompt,
241
+ conversationId: options.conversationId,
242
+ maxTokens: options.maxTokens,
243
+ temperature: options.temperature
244
+ });
245
+
246
+ // Store in conversation history
247
+ if (conversationHistory) {
248
+ conversationHistory.push({
249
+ role: 'user',
250
+ content: userPrompt,
251
+ timestamp: Date.now()
252
+ });
253
+ conversationHistory.push({
254
+ role: 'assistant',
255
+ content: response,
256
+ timestamp: Date.now()
257
+ });
258
+ }
259
+
260
+ return response;
261
+
262
+ } catch (error) {
263
+ // Check for specific error patterns and provide helpful messages
264
+ if (error.message && error.message.includes('rate limit')) {
265
+ throw new Error(
266
+ `Failed to invoke agent ${agentName}: Rate limit exceeded. ` +
267
+ `Consider using template-based fallback or retry later.`
268
+ );
269
+ }
270
+
271
+ if (error.message && error.message.includes('RATE_LIMIT_EXCEEDED')) {
272
+ throw new Error(
273
+ `Failed to invoke agent ${agentName}: Rate limit exceeded. ` +
274
+ `Try using template-based fallback for offline operation.`
275
+ );
276
+ }
277
+
278
+ // Generic error with template suggestion
279
+ throw new Error(
280
+ `Failed to invoke agent ${agentName}: ${error.message}. ` +
281
+ `Consider using template-based fallback if AI provider is unavailable.`
282
+ );
283
+ }
284
+ }
285
+
286
+ /**
287
+ * Invoke agent with streaming response
288
+ *
289
+ * Returns an AsyncGenerator that yields chunks as they arrive.
290
+ *
291
+ * @param {string} agentName - Name of the agent to invoke
292
+ * @param {string} task - Task description for the agent
293
+ * @param {Object} context - Additional context
294
+ * @param {Object} options - Options (same as invoke)
295
+ * @returns {AsyncGenerator<string>} Stream of response chunks
296
+ * @throws {Error} If agent not found or streaming fails
297
+ */
298
+ async *invokeStream(agentName, task, context = {}, options = {}) {
299
+ try {
300
+ // Load agent metadata
301
+ const agentMetadata = await this.loadAgent(agentName);
302
+
303
+ // Build prompts
304
+ const systemPrompt = this._buildSystemPrompt(agentMetadata);
305
+ const userPrompt = this._buildUserPrompt(task, context, agentName);
306
+
307
+ // Stream from AI provider
308
+ for await (const chunk of this.aiProvider.stream(userPrompt, {
309
+ system: systemPrompt,
310
+ conversationId: options.conversationId,
311
+ maxTokens: options.maxTokens,
312
+ temperature: options.temperature
313
+ })) {
314
+ yield chunk;
315
+ }
316
+
317
+ } catch (error) {
318
+ throw new Error(`Failed to stream from agent ${agentName}: ${error.message}`);
319
+ }
320
+ }
321
+
322
+ /**
323
+ * List all available agents
324
+ *
325
+ * Scans the agents directory and returns metadata for all agents.
326
+ *
327
+ * @param {Object} options - Options
328
+ * @param {boolean} [options.grouped] - Group agents by category
329
+ * @returns {Promise<Array|Object>} Array of agents or grouped object
330
+ */
331
+ async listAgents(options = {}) {
332
+ try {
333
+ const agents = [];
334
+
335
+ // Scan all subdirectories
336
+ const categories = await fs.readdir(this.agentsBaseDir);
337
+
338
+ for (const category of categories) {
339
+ const categoryPath = path.join(this.agentsBaseDir, category);
340
+
341
+ // Skip files (we want directories)
342
+ const stats = await fs.stat(categoryPath);
343
+ if (!stats.isDirectory()) {
344
+ continue;
345
+ }
346
+
347
+ // Read agent files in this category
348
+ const files = await fs.readdir(categoryPath);
349
+
350
+ for (const file of files) {
351
+ if (file.endsWith('.md') && file !== 'README.md') {
352
+ const agentName = file.replace('.md', '');
353
+
354
+ try {
355
+ const metadata = await this.loadAgent(agentName);
356
+ agents.push({
357
+ name: metadata.name,
358
+ title: metadata.title,
359
+ specialization: metadata.specialization,
360
+ category: metadata.category,
361
+ tools: metadata.tools
362
+ });
363
+ } catch (error) {
364
+ // Skip agents that fail to parse
365
+ console.warn(`Warning: Failed to load agent ${agentName}: ${error.message}`);
366
+ }
367
+ }
368
+ }
369
+ }
370
+
371
+ // Return grouped or flat list
372
+ if (options.grouped) {
373
+ return this._groupAgentsByCategory(agents);
374
+ }
375
+
376
+ return agents;
377
+
378
+ } catch (error) {
379
+ throw new Error(`Failed to list agents: ${error.message}`);
380
+ }
381
+ }
382
+
383
+ /**
384
+ * Search agents by keyword
385
+ *
386
+ * Searches in agent name, title, and specialization.
387
+ *
388
+ * @param {string} query - Search query (case-insensitive)
389
+ * @returns {Promise<Array>} Matching agents
390
+ */
391
+ async searchAgents(query) {
392
+ const allAgents = await this.listAgents();
393
+ const lowerQuery = query.toLowerCase();
394
+
395
+ return allAgents.filter(agent => {
396
+ return (
397
+ agent.name.toLowerCase().includes(lowerQuery) ||
398
+ agent.title.toLowerCase().includes(lowerQuery) ||
399
+ agent.specialization.toLowerCase().includes(lowerQuery)
400
+ );
401
+ });
402
+ }
403
+
404
+ // ============================================================
405
+ // PRIVATE HELPER METHODS
406
+ // ============================================================
407
+
408
+ /**
409
+ * Find agent file in subdirectories
410
+ * @private
411
+ * @param {string} agentName - Name of agent
412
+ * @returns {Promise<string|null>} Full path to agent file or null
413
+ */
414
+ async _findAgentFile(agentName) {
415
+ const fileName = `${agentName}.md`;
416
+
417
+ try {
418
+ // Read all subdirectories
419
+ const entries = await fs.readdir(this.agentsBaseDir);
420
+
421
+ for (const entry of entries) {
422
+ const entryPath = path.join(this.agentsBaseDir, entry);
423
+
424
+ // Check if it's a directory
425
+ const stats = await fs.stat(entryPath);
426
+ if (stats.isDirectory()) {
427
+ // Check if agent file exists in this directory
428
+ const agentPath = path.join(entryPath, fileName);
429
+ try {
430
+ await fs.access(agentPath);
431
+ return agentPath;
432
+ } catch {
433
+ // File doesn't exist in this directory, continue
434
+ }
435
+ }
436
+ }
437
+
438
+ return null;
439
+
440
+ } catch (error) {
441
+ throw new Error(`Failed to search for agent: ${error.message}`);
442
+ }
443
+ }
444
+
445
+ /**
446
+ * Extract category from agent path
447
+ * @private
448
+ * @param {string} agentPath - Full path to agent file
449
+ * @returns {string} Category name
450
+ */
451
+ _extractCategory(agentPath) {
452
+ const parts = agentPath.split(path.sep);
453
+ const agentsIndex = parts.indexOf('agents');
454
+ if (agentsIndex >= 0 && agentsIndex < parts.length - 1) {
455
+ return parts[agentsIndex + 1];
456
+ }
457
+ return 'unknown';
458
+ }
459
+
460
+ /**
461
+ * Build system prompt with agent role and Context7 queries
462
+ * @private
463
+ * @param {Object} agentMetadata - Agent metadata
464
+ * @returns {string} System prompt
465
+ */
466
+ _buildSystemPrompt(agentMetadata) {
467
+ const { title, specialization, documentationQueries, methodologies } = agentMetadata;
468
+
469
+ let prompt = `<agent>
470
+ You are ${title || 'an AI assistant'}.
471
+
472
+ <specialization>
473
+ ${specialization || 'General purpose assistant'}
474
+ </specialization>
475
+ `;
476
+
477
+ // Include Context7 documentation queries if available
478
+ if (documentationQueries && documentationQueries.length > 0) {
479
+ prompt += `
480
+ <documentation>
481
+ Before providing your response, consider consulting these documentation resources:
482
+
483
+ `;
484
+ documentationQueries.forEach(query => {
485
+ prompt += `- ${query}\n`;
486
+ });
487
+ prompt += `</documentation>
488
+ `;
489
+ }
490
+
491
+ // Include methodologies if available
492
+ if (methodologies && methodologies.length > 0) {
493
+ prompt += `
494
+ <methodologies>
495
+ Apply these methodologies in your work:
496
+ `;
497
+ methodologies.forEach(method => {
498
+ prompt += `- ${method}\n`;
499
+ });
500
+ prompt += `</methodologies>
501
+ `;
502
+ }
503
+
504
+ prompt += `
505
+ Please analyze the task and context provided below, then provide a thorough and accurate response.
506
+ Use chain-of-thought reasoning when appropriate to explain your approach.
507
+ </agent>`;
508
+
509
+ return prompt;
510
+ }
511
+
512
+ /**
513
+ * Build user prompt with XML-structured task and context
514
+ * @private
515
+ * @param {string} task - Task description
516
+ * @param {Object} context - Context object
517
+ * @param {string} agentName - Agent name for reference
518
+ * @returns {string} User prompt
519
+ */
520
+ _buildUserPrompt(task, context, agentName = '') {
521
+ let prompt = `<task>${task}</task>`;
522
+
523
+ // Add context if provided (compact JSON for parsing compatibility)
524
+ if (context && Object.keys(context).length > 0) {
525
+ prompt += ` <context>${JSON.stringify(context)}</context>`;
526
+ }
527
+
528
+ // Add agent reference (useful for logging/debugging)
529
+ if (agentName) {
530
+ prompt += ` <!-- Agent: ${agentName} -->`;
531
+ }
532
+
533
+ return prompt;
534
+ }
535
+
536
+ /**
537
+ * Group agents by category
538
+ * @private
539
+ * @param {Array} agents - Flat list of agents
540
+ * @returns {Object} Grouped agents
541
+ */
542
+ _groupAgentsByCategory(agents) {
543
+ const grouped = {};
544
+
545
+ agents.forEach(agent => {
546
+ const category = agent.category || 'unknown';
547
+ if (!grouped[category]) {
548
+ grouped[category] = [];
549
+ }
550
+ grouped[category].push(agent);
551
+ });
552
+
553
+ return grouped;
554
+ }
555
+ }
556
+
557
+ module.exports = AgentService;