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,432 @@
1
+ /**
2
+ * TemplateProvider - No-AI Fallback Provider
3
+ *
4
+ * Provides template-based text generation without actual AI calls.
5
+ * Useful for:
6
+ * - No API key scenarios
7
+ * - Offline mode
8
+ * - Testing without API costs
9
+ * - Deterministic output for CI/CD
10
+ * - Development without rate limits
11
+ *
12
+ * Features:
13
+ * - Simple variable substitution: {{variable}}
14
+ * - Nested property access: {{object.property}}
15
+ * - Conditionals: {{#if condition}}...{{/if}}
16
+ * - Loops: {{#each array}}...{{/each}}
17
+ * - Simulated streaming support
18
+ *
19
+ * @extends AbstractAIProvider
20
+ */
21
+
22
+ const AbstractAIProvider = require('./AbstractAIProvider');
23
+ const AIProviderError = require('../errors/AIProviderError');
24
+
25
+ /**
26
+ * Default templates for common use cases
27
+ */
28
+ const DEFAULT_TEMPLATES = {
29
+ default: 'Response: {{prompt}}',
30
+
31
+ prd_parse: `
32
+ PRD Analysis: {{prdTitle}}
33
+
34
+ Key Features:
35
+ {{#each features}}
36
+ - {{name}}: {{description}}
37
+ {{/each}}
38
+
39
+ Technical Approach:
40
+ {{technicalApproach}}
41
+
42
+ Estimated Effort: {{effortEstimate}}
43
+ `.trim(),
44
+
45
+ epic_decompose: `
46
+ Epic: {{epicName}}
47
+
48
+ Description: {{description}}
49
+
50
+ Tasks:
51
+ {{#each tasks}}
52
+ {{index}}. {{name}} ({{effort}})
53
+ {{#if dependencies}}Dependencies: {{dependencies}}{{/if}}
54
+ {{/each}}
55
+
56
+ Total Effort: {{totalEffort}}
57
+ `.trim(),
58
+
59
+ task_generate: `
60
+ Task: {{taskTitle}}
61
+
62
+ Description: {{description}}
63
+
64
+ Acceptance Criteria:
65
+ {{#each criteria}}
66
+ - {{.}}
67
+ {{/each}}
68
+
69
+ Effort: {{effort}}
70
+ Priority: {{priority}}
71
+ `.trim()
72
+ };
73
+
74
+ class TemplateProvider extends AbstractAIProvider {
75
+ /**
76
+ * Create a new TemplateProvider
77
+ *
78
+ * @param {Object} config - Configuration options
79
+ * @param {Object} config.templates - Custom templates
80
+ * @param {string} config.templates.default - Default template
81
+ * @param {Object} config.templates.* - Named templates
82
+ */
83
+ constructor(config = {}) {
84
+ super(config);
85
+ this.templates = config.templates || {};
86
+ }
87
+
88
+ /**
89
+ * Get the default model identifier
90
+ *
91
+ * @returns {string} Model name
92
+ */
93
+ getDefaultModel() {
94
+ return 'template-v1';
95
+ }
96
+
97
+ /**
98
+ * Get the environment variable name for API key
99
+ * (Not required for template provider)
100
+ *
101
+ * @returns {string} Environment variable name
102
+ */
103
+ getApiKeyEnvVar() {
104
+ return 'TEMPLATE_PROVIDER_API_KEY';
105
+ }
106
+
107
+ /**
108
+ * Generate text using template rendering
109
+ *
110
+ * @param {string} prompt - Input prompt (available as {{prompt}} in template)
111
+ * @param {Object} options - Generation options
112
+ * @param {string} options.template - Template string to use
113
+ * @param {Object} options.data - Data for template substitution
114
+ * @returns {Promise<string>} Rendered template
115
+ */
116
+ async complete(prompt, options = {}) {
117
+ try {
118
+ const template = options.template || this.templates.default || DEFAULT_TEMPLATES.default;
119
+ const data = options.data || { prompt };
120
+
121
+ // Ensure prompt is in data
122
+ if (!data.prompt) {
123
+ data.prompt = prompt;
124
+ }
125
+
126
+ return this.render(template, data);
127
+ } catch (error) {
128
+ throw this.formatError(error);
129
+ }
130
+ }
131
+
132
+ /**
133
+ * Generate streaming text by yielding words incrementally
134
+ * Simulates streaming behavior without actual AI
135
+ *
136
+ * @param {string} prompt - Input prompt
137
+ * @param {Object} options - Generation options
138
+ * @yields {string} Word chunks with spaces
139
+ */
140
+ async *stream(prompt, options = {}) {
141
+ try {
142
+ const result = await this.complete(prompt, options);
143
+
144
+ // Simulate streaming by yielding word by word
145
+ const words = result.split(' ');
146
+ for (const word of words) {
147
+ if (word) {
148
+ yield word + ' ';
149
+ await this._delay(10); // Small delay to simulate streaming
150
+ }
151
+ }
152
+ } catch (error) {
153
+ throw this.formatError(error);
154
+ }
155
+ }
156
+
157
+ /**
158
+ * Check if streaming is supported
159
+ *
160
+ * @returns {boolean} True (simulated streaming supported)
161
+ */
162
+ supportsStreaming() {
163
+ return true;
164
+ }
165
+
166
+ /**
167
+ * Check if function calling is supported
168
+ *
169
+ * @returns {boolean} False (not supported)
170
+ */
171
+ supportsFunctionCalling() {
172
+ return false;
173
+ }
174
+
175
+ /**
176
+ * Check if chat mode is supported
177
+ *
178
+ * @returns {boolean} False (basic templates only)
179
+ */
180
+ supportsChat() {
181
+ return false;
182
+ }
183
+
184
+ /**
185
+ * Check if vision/image analysis is supported
186
+ *
187
+ * @returns {boolean} False (not supported)
188
+ */
189
+ supportsVision() {
190
+ return false;
191
+ }
192
+
193
+ /**
194
+ * Validate provider configuration
195
+ * Always returns true as no API key needed
196
+ *
197
+ * @returns {Promise<boolean>} True
198
+ */
199
+ async validate() {
200
+ return true;
201
+ }
202
+
203
+ /**
204
+ * Test connection to provider
205
+ * Always returns true as no external connection needed
206
+ *
207
+ * @returns {Promise<boolean>} True
208
+ */
209
+ async testConnection() {
210
+ return true;
211
+ }
212
+
213
+ /**
214
+ * Render a template with data
215
+ * Processes in order: variables → nested properties → conditionals → loops
216
+ *
217
+ * @param {string} template - Template string
218
+ * @param {Object} data - Data for substitution
219
+ * @returns {string} Rendered result
220
+ */
221
+ render(template, data) {
222
+ if (!template) return '';
223
+ if (!data) data = {};
224
+
225
+ let result = String(template);
226
+
227
+ // Process in specific order for correct nesting
228
+ // 1. Process loops first (they may contain conditionals and variables)
229
+ result = this._processLoops(result, data);
230
+
231
+ // 2. Process conditionals (they may contain variables)
232
+ result = this._processConditionals(result, data);
233
+
234
+ // 3. Process nested properties (more specific than simple variables)
235
+ result = this._replaceNestedProperties(result, data);
236
+
237
+ // 4. Process simple variables last (catch remaining)
238
+ result = this._replaceVariables(result, data);
239
+
240
+ return result;
241
+ }
242
+
243
+ /**
244
+ * Replace simple variables like {{variable}}
245
+ *
246
+ * @private
247
+ * @param {string} template - Template string
248
+ * @param {Object} data - Data object
249
+ * @returns {string} Template with variables replaced
250
+ */
251
+ _replaceVariables(template, data) {
252
+ return template.replace(/\{\{(\w+)\}\}/g, (match, key) => {
253
+ return data[key] !== undefined ? String(data[key]) : match;
254
+ });
255
+ }
256
+
257
+ /**
258
+ * Replace nested properties like {{object.property}}
259
+ *
260
+ * @private
261
+ * @param {string} template - Template string
262
+ * @param {Object} data - Data object
263
+ * @returns {string} Template with nested properties replaced
264
+ */
265
+ _replaceNestedProperties(template, data) {
266
+ return template.replace(/\{\{([\w.]+)\}\}/g, (match, path) => {
267
+ // Skip if it's a simple variable (no dots)
268
+ if (!path.includes('.')) {
269
+ return match;
270
+ }
271
+
272
+ const value = this._getNestedProperty(data, path);
273
+ return value !== undefined ? String(value) : match;
274
+ });
275
+ }
276
+
277
+ /**
278
+ * Get nested property from object using dot notation
279
+ *
280
+ * @private
281
+ * @param {Object} obj - Object to traverse
282
+ * @param {string} path - Dot-separated path
283
+ * @returns {*} Property value or undefined
284
+ */
285
+ _getNestedProperty(obj, path) {
286
+ return path.split('.').reduce((current, key) =>
287
+ current?.[key], obj
288
+ );
289
+ }
290
+
291
+ /**
292
+ * Process conditional blocks {{#if condition}}...{{/if}}
293
+ *
294
+ * @private
295
+ * @param {string} template - Template string
296
+ * @param {Object} data - Data object
297
+ * @returns {string} Template with conditionals processed
298
+ */
299
+ _processConditionals(template, data) {
300
+ // Process nested conditionals by repeatedly applying the regex
301
+ // Use non-greedy matching and process innermost first
302
+ let result = template;
303
+ let previousResult;
304
+ let iterations = 0;
305
+ const maxIterations = 100; // Prevent infinite loops
306
+
307
+ do {
308
+ previousResult = result;
309
+ iterations++;
310
+
311
+ result = result.replace(
312
+ /\{\{#if\s+([\w.]+)\}\}([\s\S]*?)\{\{\/if\}\}/g,
313
+ (match, conditionPath, content) => {
314
+ const condition = this._getNestedProperty(data, conditionPath) || data[conditionPath];
315
+
316
+ // Check truthiness (like JavaScript)
317
+ return condition ? content : '';
318
+ }
319
+ );
320
+
321
+ if (iterations >= maxIterations) {
322
+ break; // Safety exit
323
+ }
324
+ } while (result !== previousResult); // Continue until no more changes
325
+
326
+ return result;
327
+ }
328
+
329
+ /**
330
+ * Process loop blocks {{#each array}}...{{/each}}
331
+ *
332
+ * @private
333
+ * @param {string} template - Template string
334
+ * @param {Object} data - Data object
335
+ * @returns {string} Template with loops processed
336
+ */
337
+ _processLoops(template, data, depth = 0) {
338
+ // Prevent infinite recursion
339
+ if (depth > 10) {
340
+ return template;
341
+ }
342
+
343
+ // Process loops with recursion for nested loops
344
+ const result = template.replace(
345
+ /\{\{#each\s+(\w+)\}\}([\s\S]*?)\{\{\/each\}\}/g,
346
+ (match, arrayName, itemTemplate) => {
347
+ const array = data[arrayName];
348
+
349
+ if (!Array.isArray(array)) {
350
+ return '';
351
+ }
352
+
353
+ return array.map(item => {
354
+ // If item is primitive, wrap in object with 'item' key
355
+ const itemData = typeof item === 'object' && item !== null
356
+ ? item
357
+ : { item };
358
+
359
+ // Merge with parent data for access to outer variables
360
+ const mergedData = { ...data, ...itemData };
361
+
362
+ // Process the item template
363
+ let itemResult = itemTemplate;
364
+
365
+ // First replace {{.}} with the actual item value for primitives
366
+ if (typeof item !== 'object' || item === null) {
367
+ itemResult = itemResult.replace(/\{\{\.\}\}/g, String(item));
368
+ }
369
+
370
+ // Recursively process nested loops with merged data FIRST
371
+ if (itemResult.includes('{{#each')) {
372
+ itemResult = this._processLoops(itemResult, mergedData, depth + 1);
373
+ }
374
+
375
+ // Then replace variables (these should not interfere with processed loops)
376
+ itemResult = itemResult.replace(/\{\{([\w.]+)\}\}/g, (m, path) => {
377
+ // Skip if it looks like a template tag
378
+ if (path === 'each' || path === 'if' || path === '/each' || path === '/if') {
379
+ return m;
380
+ }
381
+ const value = this._getNestedProperty(mergedData, path) || mergedData[path];
382
+ return value !== undefined ? String(value) : m;
383
+ });
384
+
385
+ // Process conditionals within loop
386
+ itemResult = itemResult.replace(
387
+ /\{\{#if\s+([\w.]+)\}\}([\s\S]*?)\{\{\/if\}\}/g,
388
+ (m, condPath, content) => {
389
+ const cond = this._getNestedProperty(mergedData, condPath) || mergedData[condPath];
390
+ return cond ? content : '';
391
+ }
392
+ );
393
+
394
+ return itemResult;
395
+ }).join('');
396
+ }
397
+ );
398
+
399
+ return result;
400
+ }
401
+
402
+ /**
403
+ * Helper to delay execution (for simulated streaming)
404
+ *
405
+ * @private
406
+ * @param {number} ms - Milliseconds to delay
407
+ * @returns {Promise} Resolves after delay
408
+ */
409
+ _delay(ms) {
410
+ return new Promise(resolve => setTimeout(resolve, ms));
411
+ }
412
+
413
+ /**
414
+ * Format error as AIProviderError
415
+ *
416
+ * @param {Error} error - Original error
417
+ * @returns {AIProviderError} Formatted error
418
+ */
419
+ formatError(error) {
420
+ if (error instanceof AIProviderError) {
421
+ return error;
422
+ }
423
+
424
+ return new AIProviderError(
425
+ 'TEMPLATE_ERROR',
426
+ `Template rendering error: ${error.message}`,
427
+ true // isOperational
428
+ );
429
+ }
430
+ }
431
+
432
+ module.exports = TemplateProvider;
@@ -0,0 +1,206 @@
1
+ /**
2
+ * CLI Agent Commands
3
+ *
4
+ * Provides agent management commands for listing, searching, and invoking agents.
5
+ * Implements subcommands for list, search, and invoke operations.
6
+ *
7
+ * @module cli/commands/agent
8
+ * @requires ../../services/AgentService
9
+ * @requires ora
10
+ * @requires chalk
11
+ */
12
+
13
+ const AgentService = require('../../services/AgentService');
14
+ const ora = require('ora');
15
+ const chalk = require('chalk');
16
+
17
+ /**
18
+ * List all available agents
19
+ * @param {Object} argv - Command arguments
20
+ */
21
+ async function agentList(argv) {
22
+ const spinner = ora('Loading agents...').start();
23
+ const agentService = new AgentService();
24
+
25
+ try {
26
+ const agents = await agentService.listAgents();
27
+
28
+ spinner.succeed(chalk.green(`Found ${agents.length} agents`));
29
+
30
+ // Display agent list
31
+ if (agents.length > 0) {
32
+ console.log(chalk.green(`\n${agents.length} agents available:`));
33
+ agents.forEach(agent => {
34
+ console.log(` - ${agent.name}: ${agent.title} [${agent.category}]`);
35
+ });
36
+ } else {
37
+ console.log(chalk.yellow('\nNo agents found.'));
38
+ }
39
+ } catch (error) {
40
+ spinner.fail(chalk.red('Failed to list agents'));
41
+ console.error(chalk.red(`\nError: ${error.message}`));
42
+ }
43
+ }
44
+
45
+ /**
46
+ * Search agents by keyword
47
+ * @param {Object} argv - Command arguments
48
+ */
49
+ async function agentSearch(argv) {
50
+ const spinner = ora(`Searching agents for: ${argv.query}`).start();
51
+ const agentService = new AgentService();
52
+
53
+ try {
54
+ const agents = await agentService.searchAgents(argv.query);
55
+
56
+ spinner.succeed(chalk.green(`Found ${agents.length} agent${agents.length === 1 ? '' : 's'}`));
57
+
58
+ // Display search results
59
+ if (agents.length > 0) {
60
+ console.log(chalk.green(`\n${agents.length} agent${agents.length === 1 ? '' : 's'} matching '${argv.query}':`));
61
+ agents.forEach(agent => {
62
+ console.log(` - ${agent.name}: ${agent.title}`);
63
+ if (agent.specialization) {
64
+ console.log(` ${agent.specialization}`);
65
+ }
66
+ });
67
+ } else {
68
+ console.log(chalk.yellow(`\nNo agents found matching '${argv.query}'.`));
69
+ }
70
+ } catch (error) {
71
+ spinner.fail(chalk.red('Failed to search agents'));
72
+ console.error(chalk.red(`\nError: ${error.message}`));
73
+ }
74
+ }
75
+
76
+ /**
77
+ * Invoke an agent with a task
78
+ * @param {Object} argv - Command arguments
79
+ */
80
+ async function agentInvoke(argv) {
81
+ const spinner = ora(`Invoking agent: ${argv.name}`).start();
82
+ const agentService = new AgentService();
83
+
84
+ try {
85
+ if (argv.stream) {
86
+ // Streaming mode
87
+ spinner.text = 'Streaming agent response...';
88
+
89
+ for await (const chunk of agentService.invokeStream(argv.name, argv.task, {})) {
90
+ process.stdout.write(chunk);
91
+ }
92
+
93
+ spinner.succeed(chalk.green('Agent invoked successfully'));
94
+ } else {
95
+ // Non-streaming mode
96
+ const result = await agentService.invoke(argv.name, argv.task, {});
97
+
98
+ spinner.succeed(chalk.green('Agent invoked successfully'));
99
+
100
+ console.log(chalk.green('\nAgent Response:'));
101
+ console.log(result);
102
+ }
103
+ } catch (error) {
104
+ spinner.fail(chalk.red('Failed to invoke agent'));
105
+ console.error(chalk.red(`\nError: ${error.message}`));
106
+ }
107
+ }
108
+
109
+ /**
110
+ * Main command handler
111
+ * @param {Object} argv - Command arguments
112
+ */
113
+ async function handler(argv) {
114
+ // Validate action
115
+ const validActions = ['list', 'search', 'invoke'];
116
+
117
+ if (!validActions.includes(argv.action)) {
118
+ console.error(chalk.red(`\nError: Unknown action: ${argv.action}`));
119
+ console.error(chalk.yellow(`Valid actions: ${validActions.join(', ')}`));
120
+ return;
121
+ }
122
+
123
+ // Route to appropriate handler
124
+ try {
125
+ switch (argv.action) {
126
+ case 'list':
127
+ await agentList(argv);
128
+ break;
129
+ case 'search':
130
+ await agentSearch(argv);
131
+ break;
132
+ case 'invoke':
133
+ await agentInvoke(argv);
134
+ break;
135
+ }
136
+ } catch (error) {
137
+ // Global error handler for unexpected errors
138
+ console.error(chalk.red(`\nUnexpected error: ${error.message}`));
139
+ }
140
+ }
141
+
142
+ /**
143
+ * Command builder - registers all subcommands
144
+ * @param {Object} yargs - Yargs instance
145
+ * @returns {Object} Configured yargs instance
146
+ */
147
+ function builder(yargs) {
148
+ return yargs
149
+ .command(
150
+ 'list',
151
+ 'List all available agents',
152
+ (yargs) => {
153
+ return yargs;
154
+ }
155
+ )
156
+ .command(
157
+ 'search <query>',
158
+ 'Search agents by keyword',
159
+ (yargs) => {
160
+ return yargs
161
+ .positional('query', {
162
+ describe: 'Search query',
163
+ type: 'string'
164
+ });
165
+ }
166
+ )
167
+ .command(
168
+ 'invoke <name>',
169
+ 'Invoke an agent with a task',
170
+ (yargs) => {
171
+ return yargs
172
+ .positional('name', {
173
+ describe: 'Agent name',
174
+ type: 'string'
175
+ })
176
+ .option('task', {
177
+ describe: 'Task description',
178
+ type: 'string',
179
+ demandOption: true
180
+ })
181
+ .option('stream', {
182
+ describe: 'Use streaming mode',
183
+ type: 'boolean',
184
+ default: false
185
+ });
186
+ }
187
+ )
188
+ .demandCommand(1, 'You must specify an agent action')
189
+ .strictCommands()
190
+ .help();
191
+ }
192
+
193
+ /**
194
+ * Command export
195
+ */
196
+ module.exports = {
197
+ command: 'agent <action>',
198
+ describe: 'Manage agents',
199
+ builder,
200
+ handler,
201
+ handlers: {
202
+ list: agentList,
203
+ search: agentSearch,
204
+ invoke: agentInvoke
205
+ }
206
+ };