claude-autopm 1.30.0 → 1.31.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,297 @@
1
+ /**
2
+ * Visual Diff Renderer
3
+ *
4
+ * Renders ASCII side-by-side comparisons of text differences.
5
+ * Highlights conflicting sections and provides context lines.
6
+ *
7
+ * @example
8
+ * const VisualDiff = require('./lib/visual-diff');
9
+ *
10
+ * const diff = new VisualDiff({
11
+ * columnWidth: 80
12
+ * });
13
+ *
14
+ * // Render side-by-side comparison
15
+ * const rendered = diff.sideBySide(localContent, remoteContent);
16
+ * console.log(rendered);
17
+ *
18
+ * // Highlight conflicts
19
+ * const highlighted = diff.highlightConflicts(text, conflicts);
20
+ *
21
+ * // Render with context
22
+ * const context = diff.renderContext(text, [5, 10], 3);
23
+ */
24
+
25
+ class VisualDiff {
26
+ /**
27
+ * Create a new VisualDiff instance
28
+ *
29
+ * @param {Object} options - Configuration options
30
+ * @param {number} options.columnWidth - Width of each column in side-by-side view (default: 60)
31
+ * @param {boolean} options.showLineNumbers - Show line numbers (default: true)
32
+ * @param {string} options.separator - Column separator (default: ' | ')
33
+ */
34
+ constructor(options = {}) {
35
+ // Validate configuration
36
+ if (options.columnWidth !== undefined) {
37
+ if (typeof options.columnWidth !== 'number' || options.columnWidth <= 0) {
38
+ throw new Error('columnWidth must be a positive number');
39
+ }
40
+ }
41
+
42
+ this.options = {
43
+ columnWidth: options.columnWidth || 60,
44
+ showLineNumbers: options.showLineNumbers !== false,
45
+ separator: options.separator || ' | '
46
+ };
47
+ }
48
+
49
+ /**
50
+ * Render side-by-side comparison of two text versions
51
+ *
52
+ * @param {string} left - Left side content (local)
53
+ * @param {string} right - Right side content (remote)
54
+ * @param {Object} options - Rendering options
55
+ * @returns {string} ASCII side-by-side comparison
56
+ */
57
+ sideBySide(left, right, options = {}) {
58
+ const leftLines = left.split('\n');
59
+ const rightLines = right.split('\n');
60
+
61
+ const maxLines = Math.max(leftLines.length, rightLines.length);
62
+
63
+ const output = [];
64
+
65
+ // Header
66
+ const colWidth = this.options.columnWidth;
67
+ const header = this._padRight('LOCAL', colWidth) +
68
+ this.options.separator +
69
+ this._padRight('REMOTE', colWidth);
70
+ output.push(header);
71
+ output.push('='.repeat(header.length));
72
+
73
+ // Content lines
74
+ for (let i = 0; i < maxLines; i++) {
75
+ const leftLine = leftLines[i] || '';
76
+ const rightLine = rightLines[i] || '';
77
+
78
+ const lineNum = this.options.showLineNumbers ? `${(i + 1).toString().padStart(4)} ` : '';
79
+
80
+ const leftContent = this._truncate(leftLine, colWidth - lineNum.length);
81
+ const rightContent = this._truncate(rightLine, colWidth - lineNum.length);
82
+
83
+ // Mark differences
84
+ const marker = leftLine !== rightLine ? '*' : ' ';
85
+
86
+ const line = marker + lineNum + this._padRight(leftContent, colWidth - lineNum.length - 1) +
87
+ this.options.separator +
88
+ marker + lineNum + this._padRight(rightContent, colWidth - lineNum.length - 1);
89
+
90
+ output.push(line);
91
+ }
92
+
93
+ return output.join('\n');
94
+ }
95
+
96
+ /**
97
+ * Highlight conflicts in text with conflict markers
98
+ *
99
+ * @param {string} text - Original text
100
+ * @param {Array<Object>} conflicts - Array of conflict objects
101
+ * @returns {string} Text with conflict markers inserted
102
+ */
103
+ highlightConflicts(text, conflicts) {
104
+ const lines = text.split('\n');
105
+
106
+ // Sort conflicts by line number (ascending)
107
+ const sortedConflicts = [...conflicts].sort((a, b) => a.line - b.line);
108
+
109
+ // Build result in a single pass
110
+ const result = [];
111
+ let conflictIdx = 0;
112
+ for (let i = 0; i < lines.length; i++) {
113
+ // Check if current line matches the next conflict
114
+ if (
115
+ conflictIdx < sortedConflicts.length &&
116
+ sortedConflicts[conflictIdx].line - 1 === i
117
+ ) {
118
+ const conflict = sortedConflicts[conflictIdx];
119
+ result.push('<<<<<<< LOCAL');
120
+ result.push(conflict.localContent);
121
+ result.push('=======');
122
+ result.push(conflict.remoteContent);
123
+ result.push('>>>>>>> REMOTE');
124
+ conflictIdx++;
125
+ } else {
126
+ result.push(lines[i]);
127
+ }
128
+ }
129
+
130
+ return result.join('\n');
131
+ }
132
+
133
+ /**
134
+ * Render context lines around specified line numbers
135
+ *
136
+ * @param {string} text - Full text content
137
+ * @param {Array<number>} lineNumbers - Line numbers to show context for
138
+ * @param {number} contextLines - Number of context lines before and after (default: 3)
139
+ * @returns {string} Text with only context lines
140
+ */
141
+ renderContext(text, lineNumbers, contextLines = 3) {
142
+ const lines = text.split('\n');
143
+ const lineSet = new Set();
144
+
145
+ // Add context lines for each specified line number
146
+ for (const lineNum of lineNumbers) {
147
+ const lineIndex = lineNum - 1; // Convert to 0-based
148
+
149
+ // Add lines in context range
150
+ const start = Math.max(0, lineIndex - contextLines);
151
+ const end = Math.min(lines.length - 1, lineIndex + contextLines);
152
+
153
+ for (let i = start; i <= end; i++) {
154
+ lineSet.add(i);
155
+ }
156
+ }
157
+
158
+ // Convert to sorted array
159
+ const includedLines = Array.from(lineSet).sort((a, b) => a - b);
160
+
161
+ // Build output with ellipsis for gaps
162
+ const output = [];
163
+ let lastLine = -2;
164
+
165
+ for (const lineIndex of includedLines) {
166
+ // Add ellipsis if there's a gap
167
+ if (lineIndex > lastLine + 1) {
168
+ output.push('...');
169
+ }
170
+
171
+ // Add the line with line number
172
+ const lineNum = (lineIndex + 1).toString().padStart(4);
173
+ output.push(`${lineNum} | ${lines[lineIndex]}`);
174
+
175
+ lastLine = lineIndex;
176
+ }
177
+
178
+ return output.join('\n');
179
+ }
180
+
181
+ /**
182
+ * Calculate diff statistics
183
+ *
184
+ * @param {string} left - Left side content
185
+ * @param {string} right - Right side content
186
+ * @returns {Object} Diff statistics
187
+ */
188
+ getStats(left, right) {
189
+ const leftLines = left.split('\n');
190
+ const rightLines = right.split('\n');
191
+
192
+ let added = 0;
193
+ let removed = 0;
194
+ let modified = 0;
195
+ let unchanged = 0;
196
+
197
+ const maxLines = Math.max(leftLines.length, rightLines.length);
198
+
199
+ for (let i = 0; i < maxLines; i++) {
200
+ const leftLine = leftLines[i];
201
+ const rightLine = rightLines[i];
202
+
203
+ if (leftLine === undefined && rightLine !== undefined) {
204
+ added++;
205
+ } else if (leftLine !== undefined && rightLine === undefined) {
206
+ removed++;
207
+ } else if (leftLine !== rightLine) {
208
+ modified++;
209
+ } else {
210
+ unchanged++;
211
+ }
212
+ }
213
+
214
+ return {
215
+ added,
216
+ removed,
217
+ modified,
218
+ unchanged,
219
+ total: maxLines
220
+ };
221
+ }
222
+
223
+ /**
224
+ * Truncate text to fit within column width
225
+ *
226
+ * @private
227
+ * @param {string} text - Text to truncate
228
+ * @param {number} maxWidth - Maximum width
229
+ * @returns {string} Truncated text
230
+ */
231
+ _truncate(text, maxWidth) {
232
+ if (text.length <= maxWidth) {
233
+ return text;
234
+ }
235
+ // If maxWidth is less than 3, we can't fit the ellipsis, so just truncate to maxWidth
236
+ if (maxWidth < 3) {
237
+ return text.substring(0, maxWidth);
238
+ }
239
+ return text.substring(0, maxWidth - 3) + '...';
240
+ }
241
+
242
+ /**
243
+ * Pad text to the right with spaces
244
+ *
245
+ * @private
246
+ * @param {string} text - Text to pad
247
+ * @param {number} width - Target width
248
+ * @returns {string} Padded text
249
+ */
250
+ _padRight(text, width) {
251
+ if (text.length >= width) {
252
+ return text.substring(0, width);
253
+ }
254
+
255
+ return text + ' '.repeat(width - text.length);
256
+ }
257
+
258
+ /**
259
+ * Render unified diff format (similar to git diff)
260
+ *
261
+ * @param {string} left - Left side content
262
+ * @param {string} right - Right side content
263
+ * @param {Object} options - Rendering options
264
+ * @returns {string} Unified diff output
265
+ */
266
+ unified(left, right, options = {}) {
267
+ const leftLines = left.split('\n');
268
+ const rightLines = right.split('\n');
269
+
270
+ const output = [];
271
+ output.push('--- LOCAL');
272
+ output.push('+++ REMOTE');
273
+ output.push(`@@ -1,${leftLines.length} +1,${rightLines.length} @@`);
274
+
275
+ const maxLines = Math.max(leftLines.length, rightLines.length);
276
+
277
+ for (let i = 0; i < maxLines; i++) {
278
+ const leftLine = leftLines[i];
279
+ const rightLine = rightLines[i];
280
+
281
+ if (leftLine === rightLine) {
282
+ output.push(` ${leftLine || ''}`);
283
+ } else if (leftLine === undefined) {
284
+ output.push(`+${rightLine}`);
285
+ } else if (rightLine === undefined) {
286
+ output.push(`-${leftLine}`);
287
+ } else {
288
+ output.push(`-${leftLine}`);
289
+ output.push(`+${rightLine}`);
290
+ }
291
+ }
292
+
293
+ return output.join('\n');
294
+ }
295
+ }
296
+
297
+ module.exports = VisualDiff;
@@ -0,0 +1,216 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * AutoPM POC - CLI for testing Claude API integration
4
+ *
5
+ * Usage:
6
+ * autopm-poc parse <prd-file> - Parse PRD with streaming output
7
+ * autopm-poc parse <prd-file> --json - Parse PRD and output JSON
8
+ * autopm-poc summarize <prd-file> - Get one-paragraph summary
9
+ * autopm-poc test - Test API connection
10
+ *
11
+ * Environment:
12
+ * ANTHROPIC_API_KEY - Required for all operations
13
+ */
14
+
15
+ const fs = require('fs');
16
+ const path = require('path');
17
+ const ClaudeProvider = require('../lib/ai-providers/ClaudeProvider');
18
+ const PRDService = require('../lib/services/PRDService');
19
+
20
+ /**
21
+ * Print usage information
22
+ */
23
+ function printUsage() {
24
+ console.log(`
25
+ AutoPM POC - Claude API Integration Demo
26
+ =========================================
27
+
28
+ Usage:
29
+ autopm-poc parse <prd-file> Parse PRD with streaming output
30
+ autopm-poc parse <prd-file> --json Parse PRD and extract epics as JSON
31
+ autopm-poc summarize <prd-file> Get one-paragraph summary
32
+ autopm-poc test Test API connection
33
+ autopm-poc help Show this help message
34
+
35
+ Environment Variables:
36
+ ANTHROPIC_API_KEY Required - Your Anthropic API key
37
+
38
+ Examples:
39
+ export ANTHROPIC_API_KEY="sk-ant-..."
40
+ autopm-poc parse examples/sample-prd.md
41
+ autopm-poc summarize examples/sample-prd.md
42
+ autopm-poc parse examples/sample-prd.md --json
43
+ `);
44
+ }
45
+
46
+ /**
47
+ * Test API connection
48
+ */
49
+ async function testConnection(provider) {
50
+ console.log('🔍 Testing API connection...\n');
51
+
52
+ try {
53
+ const result = await provider.complete('Say "Connection successful!"', {
54
+ maxTokens: 50
55
+ });
56
+
57
+ console.log('✅ API Connection Test: SUCCESS');
58
+ console.log(`📝 Response: ${result}\n`);
59
+ return true;
60
+ } catch (error) {
61
+ console.error('❌ API Connection Test: FAILED');
62
+ console.error(` Error: ${error.message}\n`);
63
+ return false;
64
+ }
65
+ }
66
+
67
+ /**
68
+ * Parse PRD with streaming output
69
+ */
70
+ async function parsePRDStream(service, content) {
71
+ console.log('🔍 Analyzing PRD with Claude AI...\n');
72
+ console.log('📝 Streaming response:\n');
73
+ console.log('─'.repeat(60));
74
+
75
+ let fullResponse = '';
76
+ try {
77
+ for await (const chunk of service.parseStream(content)) {
78
+ process.stdout.write(chunk);
79
+ fullResponse += chunk;
80
+ }
81
+
82
+ console.log('\n' + '─'.repeat(60));
83
+ console.log('\n✅ Analysis complete!');
84
+ console.log(`📊 Total response length: ${fullResponse.length} characters\n`);
85
+ } catch (error) {
86
+ console.error('\n❌ Error during parsing:', error.message);
87
+ process.exit(1);
88
+ }
89
+ }
90
+
91
+ /**
92
+ * Parse PRD and extract epics as JSON
93
+ */
94
+ async function parsePRDJson(service, content) {
95
+ console.log('🔍 Extracting epics from PRD...\n');
96
+
97
+ try {
98
+ const epics = await service.extractEpics(content);
99
+
100
+ console.log('✅ Extraction complete!\n');
101
+ console.log('📋 Extracted Epics:\n');
102
+ console.log(JSON.stringify(epics, null, 2));
103
+ console.log(`\n📊 Found ${Array.isArray(epics) ? epics.length : 0} epic(s)\n`);
104
+ } catch (error) {
105
+ console.error('❌ Error during JSON extraction:', error.message);
106
+ process.exit(1);
107
+ }
108
+ }
109
+
110
+ /**
111
+ * Summarize PRD
112
+ */
113
+ async function summarizePRD(service, content) {
114
+ console.log('🔍 Summarizing PRD...\n');
115
+
116
+ try {
117
+ const summary = await service.summarize(content);
118
+
119
+ console.log('✅ Summary complete!\n');
120
+ console.log('📝 Summary:\n');
121
+ console.log(summary);
122
+ console.log();
123
+ } catch (error) {
124
+ console.error('❌ Error during summarization:', error.message);
125
+ process.exit(1);
126
+ }
127
+ }
128
+
129
+ /**
130
+ * Main entry point
131
+ */
132
+ async function main() {
133
+ const args = process.argv.slice(2);
134
+ const command = args[0];
135
+
136
+ // Handle help
137
+ if (!command || command === 'help' || command === '--help' || command === '-h') {
138
+ printUsage();
139
+ process.exit(0);
140
+ }
141
+
142
+ // Check for API key
143
+ const apiKey = process.env.ANTHROPIC_API_KEY;
144
+ if (!apiKey) {
145
+ console.error('❌ Error: ANTHROPIC_API_KEY environment variable required\n');
146
+ console.error('Set it with:');
147
+ console.error(' export ANTHROPIC_API_KEY="sk-ant-..."\n');
148
+ process.exit(1);
149
+ }
150
+
151
+ // Initialize provider and service
152
+ const provider = new ClaudeProvider(apiKey);
153
+ const service = new PRDService(provider);
154
+
155
+ // Handle test command
156
+ if (command === 'test') {
157
+ const success = await testConnection(provider);
158
+ process.exit(success ? 0 : 1);
159
+ }
160
+
161
+ // Handle parse and summarize commands
162
+ if (command === 'parse' || command === 'summarize') {
163
+ const file = args[1];
164
+
165
+ if (!file) {
166
+ console.error(`❌ Error: PRD file required for '${command}' command\n`);
167
+ console.error(`Usage: autopm-poc ${command} <prd-file>\n`);
168
+ process.exit(1);
169
+ }
170
+
171
+ // Check if file exists
172
+ if (!fs.existsSync(file)) {
173
+ console.error(`❌ Error: File not found: ${file}\n`);
174
+ process.exit(1);
175
+ }
176
+
177
+ // Read file content
178
+ const content = fs.readFileSync(file, 'utf8');
179
+
180
+ if (!content.trim()) {
181
+ console.error('❌ Error: PRD file is empty\n');
182
+ process.exit(1);
183
+ }
184
+
185
+ console.log(`📄 Reading PRD from: ${file}`);
186
+ console.log(`📏 File size: ${content.length} characters\n`);
187
+
188
+ // Execute command
189
+ if (command === 'parse') {
190
+ const jsonFlag = args[2] === '--json';
191
+
192
+ if (jsonFlag) {
193
+ await parsePRDJson(service, content);
194
+ } else {
195
+ await parsePRDStream(service, content);
196
+ }
197
+ } else if (command === 'summarize') {
198
+ await summarizePRD(service, content);
199
+ }
200
+
201
+ process.exit(0);
202
+ }
203
+
204
+ // Unknown command
205
+ console.error(`❌ Error: Unknown command: ${command}\n`);
206
+ printUsage();
207
+ process.exit(1);
208
+ }
209
+
210
+ // Run main with error handling
211
+ main().catch(err => {
212
+ console.error('❌ Fatal error:', err.message);
213
+ console.error('\nStack trace:');
214
+ console.error(err.stack);
215
+ process.exit(1);
216
+ });
@@ -46,7 +46,8 @@ class Installer {
46
46
  '.claude/mcp-servers.json',
47
47
  '.claude/.env.example',
48
48
  '.claude/teams.json',
49
- '.claude-code'
49
+ '.claude-code',
50
+ 'lib' // Template engine and other utilities
50
51
  ];
51
52
 
52
53
  // Parse command line arguments
@@ -0,0 +1,112 @@
1
+ /**
2
+ * ClaudeProvider - Anthropic Claude API integration
3
+ *
4
+ * Provides both synchronous completion and streaming capabilities
5
+ * for Claude AI interactions.
6
+ *
7
+ * Documentation Queries:
8
+ * - mcp://context7/anthropic/sdk - Anthropic SDK patterns
9
+ * - mcp://context7/anthropic/streaming - Streaming best practices
10
+ * - mcp://context7/nodejs/async-generators - Async generator patterns
11
+ */
12
+
13
+ const Anthropic = require('@anthropic-ai/sdk');
14
+
15
+ /**
16
+ * ClaudeProvider class for Anthropic Claude API integration
17
+ */
18
+ class ClaudeProvider {
19
+ /**
20
+ * Create a new ClaudeProvider instance
21
+ * @param {string} apiKey - Anthropic API key
22
+ * @throws {Error} If API key is not provided
23
+ */
24
+ constructor(apiKey) {
25
+ if (!apiKey) {
26
+ throw new Error('API key is required for ClaudeProvider');
27
+ }
28
+
29
+ this.apiKey = apiKey;
30
+ this.client = new Anthropic({ apiKey });
31
+ }
32
+
33
+ /**
34
+ * Complete a prompt synchronously (wait for full response)
35
+ * @param {string} prompt - The prompt to complete
36
+ * @param {Object} options - Optional configuration
37
+ * @param {string} options.model - Model to use (default: claude-sonnet-4-20250514)
38
+ * @param {number} options.maxTokens - Maximum tokens to generate (default: 4096)
39
+ * @returns {Promise<string>} The completed text
40
+ */
41
+ async complete(prompt, options = {}) {
42
+ try {
43
+ const response = await this.client.messages.create({
44
+ model: options.model || 'claude-sonnet-4-20250514',
45
+ max_tokens: options.maxTokens || 4096,
46
+ messages: [{ role: 'user', content: prompt }]
47
+ });
48
+
49
+ // Extract text from response
50
+ if (response.content && response.content.length > 0) {
51
+ return response.content[0].text;
52
+ }
53
+
54
+ return '';
55
+ } catch (error) {
56
+ throw new Error(`Claude API error: ${error.message}`);
57
+ }
58
+ }
59
+
60
+ /**
61
+ * Stream a prompt response (async generator for real-time feedback)
62
+ * @param {string} prompt - The prompt to complete
63
+ * @param {Object} options - Optional configuration
64
+ * @param {string} options.model - Model to use (default: claude-sonnet-4-20250514)
65
+ * @param {number} options.maxTokens - Maximum tokens to generate (default: 4096)
66
+ * @yields {string} Text chunks as they arrive
67
+ */
68
+ async *stream(prompt, options = {}) {
69
+ try {
70
+ const stream = await this.client.messages.create({
71
+ model: options.model || 'claude-sonnet-4-20250514',
72
+ max_tokens: options.maxTokens || 4096,
73
+ stream: true,
74
+ messages: [{ role: 'user', content: prompt }]
75
+ });
76
+
77
+ // Yield text deltas as they arrive
78
+ for await (const event of stream) {
79
+ if (event.type === 'content_block_delta' &&
80
+ event.delta &&
81
+ event.delta.type === 'text_delta') {
82
+ yield event.delta.text;
83
+ }
84
+ }
85
+ } catch (error) {
86
+ throw new Error(`Claude API streaming error: ${error.message}`);
87
+ }
88
+ }
89
+
90
+ /**
91
+ * Get the current model being used
92
+ * @returns {string} The default model name
93
+ */
94
+ getModel() {
95
+ return 'claude-sonnet-4-20250514';
96
+ }
97
+
98
+ /**
99
+ * Test the API connection
100
+ * @returns {Promise<boolean>} True if connection is successful
101
+ */
102
+ async testConnection() {
103
+ try {
104
+ await this.complete('Hello');
105
+ return true;
106
+ } catch (error) {
107
+ return false;
108
+ }
109
+ }
110
+ }
111
+
112
+ module.exports = ClaudeProvider;