@agile-vibe-coding/avc 0.1.0 → 0.1.1

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,27 @@
1
+ import Anthropic from '@anthropic-ai/sdk';
2
+ import { LLMProvider } from './llm-provider.js';
3
+
4
+ export class ClaudeProvider extends LLMProvider {
5
+ constructor(model) { super('claude', model); }
6
+
7
+ _createClient() {
8
+ const apiKey = process.env.ANTHROPIC_API_KEY;
9
+ if (!apiKey) throw new Error('ANTHROPIC_API_KEY not set. Add it to your .env file.');
10
+ return new Anthropic({ apiKey });
11
+ }
12
+
13
+ async _callProvider(prompt, maxTokens, systemInstructions) {
14
+ const params = {
15
+ model: this.model,
16
+ max_tokens: maxTokens,
17
+ messages: [{ role: 'user', content: prompt }]
18
+ };
19
+
20
+ if (systemInstructions) {
21
+ params.system = systemInstructions;
22
+ }
23
+
24
+ const response = await this._client.messages.create(params);
25
+ return response.content[0].text;
26
+ }
27
+ }
@@ -0,0 +1,30 @@
1
+ import { GoogleGenAI } from '@google/genai';
2
+ import { LLMProvider } from './llm-provider.js';
3
+
4
+ export class GeminiProvider extends LLMProvider {
5
+ constructor(model = 'gemini-2.5-flash') { super('gemini', model); }
6
+
7
+ _createClient() {
8
+ const apiKey = process.env.GEMINI_API_KEY;
9
+ if (!apiKey) throw new Error('GEMINI_API_KEY not set. Add it to your .env file.');
10
+ return new GoogleGenAI({ apiKey });
11
+ }
12
+
13
+ async _callProvider(prompt, maxTokens, systemInstructions) {
14
+ const params = {
15
+ model: this.model,
16
+ contents: prompt,
17
+ generationConfig: { maxOutputTokens: maxTokens }
18
+ };
19
+
20
+ if (systemInstructions) {
21
+ params.systemInstruction = systemInstructions;
22
+ }
23
+
24
+ const response = await this._client.models.generateContent(params);
25
+ if (!response.text) {
26
+ throw new Error('Gemini returned no text (possible safety filter block).');
27
+ }
28
+ return response.text;
29
+ }
30
+ }
@@ -0,0 +1,63 @@
1
+ export class LLMProvider {
2
+ constructor(providerName, model) {
3
+ this.providerName = providerName;
4
+ this.model = model;
5
+ this._client = null;
6
+ }
7
+
8
+ // Factory — async because of dynamic import (only loads the SDK you need)
9
+ static async create(providerName, model) {
10
+ switch (providerName) {
11
+ case 'claude': {
12
+ const { ClaudeProvider } = await import('./llm-claude.js');
13
+ return new ClaudeProvider(model);
14
+ }
15
+ case 'gemini': {
16
+ const { GeminiProvider } = await import('./llm-gemini.js');
17
+ return new GeminiProvider(model);
18
+ }
19
+ default:
20
+ throw new Error(`Unknown LLM provider: "${providerName}". Supported: claude, gemini`);
21
+ }
22
+ }
23
+
24
+ // Public API — single method, callers never touch SDK objects
25
+ async generate(prompt, maxTokens = 256, systemInstructions = null) {
26
+ if (!this._client) {
27
+ this._client = this._createClient();
28
+ }
29
+ return this._callProvider(prompt, maxTokens, systemInstructions);
30
+ }
31
+
32
+ // Validate API key and provider connectivity with a minimal test call
33
+ async validateApiKey() {
34
+ try {
35
+ // Make a minimal API call (just asks for a single word)
36
+ await this.generate('Reply with only the word "ok"', 10);
37
+ return { valid: true };
38
+ } catch (error) {
39
+ return {
40
+ valid: false,
41
+ error: error.message,
42
+ code: error.status || error.code
43
+ };
44
+ }
45
+ }
46
+
47
+ // Static helper to validate a provider config
48
+ static async validate(providerName, model) {
49
+ try {
50
+ const provider = await LLMProvider.create(providerName, model);
51
+ return await provider.validateApiKey();
52
+ } catch (error) {
53
+ return {
54
+ valid: false,
55
+ error: error.message
56
+ };
57
+ }
58
+ }
59
+
60
+ // Subclass hooks — throw if not overridden
61
+ _createClient() { throw new Error(`${this.constructor.name} must implement _createClient()`); }
62
+ async _callProvider(prompt, maxTokens, systemInstructions) { throw new Error(`${this.constructor.name} must implement _callProvider()`); }
63
+ }
package/cli/logger.js CHANGED
@@ -5,31 +5,48 @@ import os from 'os';
5
5
  /**
6
6
  * Logger - Writes debug and error logs to file
7
7
  *
8
- * Log location: ~/.avc/logs/avc.log
8
+ * Log location: <project>/.avc/logs/avc.log (or ~/.avc/logs/avc.log if no project)
9
9
  * Log rotation: Keeps last 10MB, rotates to avc.log.old
10
10
  */
11
11
  export class Logger {
12
- constructor(componentName = 'AVC') {
12
+ constructor(componentName = 'AVC', projectRoot = null) {
13
13
  this.componentName = componentName;
14
- this.logDir = path.join(os.homedir(), '.avc', 'logs');
14
+
15
+ // Only log if .avc folder exists - don't create it, don't use fallback
16
+ const baseDir = projectRoot || process.cwd();
17
+ const projectAvcDir = path.join(baseDir, '.avc');
18
+
19
+ if (!fs.existsSync(projectAvcDir)) {
20
+ // No .avc folder - disable logging
21
+ this.loggingDisabled = true;
22
+ return;
23
+ }
24
+
25
+ // .avc exists, set up logging
26
+ this.logDir = path.join(projectAvcDir, 'logs');
15
27
  this.logFile = path.join(this.logDir, 'avc.log');
16
28
  this.maxLogSize = 10 * 1024 * 1024; // 10MB
29
+ this.loggingDisabled = false;
17
30
 
18
31
  this.ensureLogDir();
19
32
  }
20
33
 
21
34
  ensureLogDir() {
35
+ if (this.loggingDisabled) return;
36
+
22
37
  try {
23
38
  if (!fs.existsSync(this.logDir)) {
24
39
  fs.mkdirSync(this.logDir, { recursive: true });
25
40
  }
26
41
  } catch (error) {
27
42
  // Silently fail if we can't create log directory
28
- console.error('Failed to create log directory:', error.message);
43
+ this.loggingDisabled = true;
29
44
  }
30
45
  }
31
46
 
32
47
  rotateLogIfNeeded() {
48
+ if (this.loggingDisabled) return;
49
+
33
50
  try {
34
51
  if (fs.existsSync(this.logFile)) {
35
52
  const stats = fs.statSync(this.logFile);
@@ -71,13 +88,17 @@ export class Logger {
71
88
  }
72
89
 
73
90
  writeLog(level, message, data = null) {
91
+ // Skip logging if directory creation failed
92
+ if (this.loggingDisabled) return;
93
+
74
94
  try {
75
95
  this.rotateLogIfNeeded();
76
96
  const logMessage = this.formatMessage(level, message, data);
77
97
  fs.appendFileSync(this.logFile, logMessage, 'utf8');
78
98
  } catch (error) {
79
99
  // Silently fail if we can't write to log
80
- console.error('Failed to write log:', error.message);
100
+ // Disable further logging attempts to avoid repeated errors
101
+ this.loggingDisabled = true;
81
102
  }
82
103
  }
83
104
 
@@ -99,6 +120,10 @@ export class Logger {
99
120
 
100
121
  // Read recent logs (last N lines)
101
122
  readRecentLogs(lines = 50) {
123
+ if (this.loggingDisabled) {
124
+ return 'No logs available (project not initialized).';
125
+ }
126
+
102
127
  try {
103
128
  if (!fs.existsSync(this.logFile)) {
104
129
  return 'No logs available yet.';
@@ -116,6 +141,8 @@ export class Logger {
116
141
 
117
142
  // Clear all logs
118
143
  clearLogs() {
144
+ if (this.loggingDisabled) return;
145
+
119
146
  try {
120
147
  if (fs.existsSync(this.logFile)) {
121
148
  fs.unlinkSync(this.logFile);
@@ -0,0 +1,261 @@
1
+ import { spawn } from 'child_process';
2
+ import { EventEmitter } from 'events';
3
+
4
+ /**
5
+ * Background Process Manager
6
+ * Manages lifecycle of long-running background processes
7
+ */
8
+ export class BackgroundProcessManager extends EventEmitter {
9
+ constructor() {
10
+ super();
11
+ this.processes = new Map(); // id -> processMetadata
12
+ this.maxOutputLines = 500; // Keep last 500 lines per process
13
+ }
14
+
15
+ /**
16
+ * Start a background process
17
+ * @param {Object} options
18
+ * @param {string} options.name - Human-readable name
19
+ * @param {string} options.command - Command to execute
20
+ * @param {string[]} options.args - Command arguments
21
+ * @param {string} options.cwd - Working directory
22
+ * @param {Object} options.env - Environment variables
23
+ * @returns {string} Process ID
24
+ */
25
+ startProcess({ name, command, args = [], cwd, env = process.env }) {
26
+ const id = `${this.sanitizeName(name)}-${Date.now()}`;
27
+
28
+ const childProcess = spawn(command, args, {
29
+ cwd,
30
+ env,
31
+ stdio: 'pipe',
32
+ detached: false
33
+ });
34
+
35
+ const metadata = {
36
+ id,
37
+ name,
38
+ command: `${command} ${args.join(' ')}`,
39
+ cwd,
40
+ pid: childProcess.pid,
41
+ status: 'running',
42
+ startTime: new Date().toISOString(),
43
+ exitCode: null,
44
+ exitSignal: null,
45
+ output: [],
46
+ process: childProcess
47
+ };
48
+
49
+ this.processes.set(id, metadata);
50
+
51
+ // Capture stdout
52
+ childProcess.stdout.on('data', (data) => {
53
+ this.appendOutput(id, 'stdout', data.toString());
54
+ });
55
+
56
+ // Capture stderr
57
+ childProcess.stderr.on('data', (data) => {
58
+ this.appendOutput(id, 'stderr', data.toString());
59
+ });
60
+
61
+ // Handle process exit
62
+ childProcess.on('exit', (code, signal) => {
63
+ this.handleProcessExit(id, code, signal);
64
+ });
65
+
66
+ // Handle process error
67
+ childProcess.on('error', (error) => {
68
+ this.handleProcessError(id, error);
69
+ });
70
+
71
+ this.emit('process-started', { id, name });
72
+
73
+ return id;
74
+ }
75
+
76
+ /**
77
+ * Stop a running process
78
+ */
79
+ stopProcess(id) {
80
+ const metadata = this.processes.get(id);
81
+ if (!metadata) return false;
82
+
83
+ if (metadata.status === 'running' && metadata.process) {
84
+ metadata.process.kill('SIGTERM');
85
+ metadata.status = 'stopped';
86
+ this.emit('process-stopped', { id, name: metadata.name });
87
+ return true;
88
+ }
89
+
90
+ return false;
91
+ }
92
+
93
+ /**
94
+ * Find process by PID
95
+ * @param {number} pid - Process ID to search for
96
+ * @returns {Object|null} Process metadata or null if not found
97
+ */
98
+ findProcessByPid(pid) {
99
+ for (const metadata of this.processes.values()) {
100
+ if (metadata.pid === pid) {
101
+ return metadata;
102
+ }
103
+ }
104
+ return null;
105
+ }
106
+
107
+ /**
108
+ * Remove process from manager by PID
109
+ * Used when external kill is performed on a managed process
110
+ * @param {number} pid - Process ID to remove
111
+ * @returns {boolean} True if process was found and removed
112
+ */
113
+ removeProcessByPid(pid) {
114
+ const process = this.findProcessByPid(pid);
115
+ if (process) {
116
+ this.processes.delete(process.id);
117
+ this.emit('process-removed', { id: process.id, name: process.name, pid });
118
+ return true;
119
+ }
120
+ return false;
121
+ }
122
+
123
+ /**
124
+ * Get process metadata
125
+ */
126
+ getProcess(id) {
127
+ return this.processes.get(id);
128
+ }
129
+
130
+ /**
131
+ * Get all processes
132
+ */
133
+ getAllProcesses() {
134
+ return Array.from(this.processes.values());
135
+ }
136
+
137
+ /**
138
+ * Get running processes only
139
+ */
140
+ getRunningProcesses() {
141
+ return this.getAllProcesses().filter(p => p.status === 'running');
142
+ }
143
+
144
+ /**
145
+ * Append output to process buffer
146
+ */
147
+ appendOutput(id, type, text) {
148
+ const metadata = this.processes.get(id);
149
+ if (!metadata) return;
150
+
151
+ const lines = text.split('\n').filter(line => line.trim());
152
+
153
+ for (const line of lines) {
154
+ metadata.output.push({
155
+ timestamp: new Date().toISOString(),
156
+ type,
157
+ text: line
158
+ });
159
+ }
160
+
161
+ // Trim to max lines
162
+ if (metadata.output.length > this.maxOutputLines) {
163
+ metadata.output = metadata.output.slice(-this.maxOutputLines);
164
+ }
165
+
166
+ this.emit('output', { id, type, text });
167
+ }
168
+
169
+ /**
170
+ * Handle process exit
171
+ */
172
+ handleProcessExit(id, code, signal) {
173
+ const metadata = this.processes.get(id);
174
+ if (!metadata) return;
175
+
176
+ metadata.exitCode = code;
177
+ metadata.exitSignal = signal;
178
+ metadata.status = code === 0 ? 'exited' : 'crashed';
179
+
180
+ this.emit('process-exited', {
181
+ id,
182
+ name: metadata.name,
183
+ code,
184
+ signal,
185
+ status: metadata.status
186
+ });
187
+ }
188
+
189
+ /**
190
+ * Handle process error
191
+ */
192
+ handleProcessError(id, error) {
193
+ const metadata = this.processes.get(id);
194
+ if (!metadata) return;
195
+
196
+ metadata.status = 'crashed';
197
+ this.appendOutput(id, 'stderr', `Process error: ${error.message}`);
198
+
199
+ this.emit('process-error', { id, name: metadata.name, error });
200
+ }
201
+
202
+ /**
203
+ * Clean up finished processes
204
+ * Removes processes with status: exited, crashed, or stopped
205
+ */
206
+ cleanupFinished() {
207
+ const finished = this.getAllProcesses().filter(
208
+ p => p.status === 'exited' || p.status === 'crashed' || p.status === 'stopped'
209
+ );
210
+
211
+ for (const process of finished) {
212
+ this.processes.delete(process.id);
213
+ }
214
+
215
+ return finished.length;
216
+ }
217
+
218
+ /**
219
+ * Stop all running processes
220
+ */
221
+ stopAll() {
222
+ const running = this.getRunningProcesses();
223
+
224
+ for (const process of running) {
225
+ this.stopProcess(process.id);
226
+ }
227
+
228
+ return running.length;
229
+ }
230
+
231
+ /**
232
+ * Get process uptime in seconds
233
+ */
234
+ getUptime(id) {
235
+ const metadata = this.processes.get(id);
236
+ if (!metadata) return 0;
237
+
238
+ const start = new Date(metadata.startTime);
239
+ const now = new Date();
240
+ return Math.floor((now - start) / 1000);
241
+ }
242
+
243
+ /**
244
+ * Format uptime as human-readable string
245
+ */
246
+ formatUptime(seconds) {
247
+ if (seconds < 60) return `${seconds}s`;
248
+ if (seconds < 3600) return `${Math.floor(seconds / 60)}m ${seconds % 60}s`;
249
+
250
+ const hours = Math.floor(seconds / 3600);
251
+ const minutes = Math.floor((seconds % 3600) / 60);
252
+ return `${hours}h ${minutes}m`;
253
+ }
254
+
255
+ /**
256
+ * Sanitize name for use as ID prefix
257
+ */
258
+ sanitizeName(name) {
259
+ return name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
260
+ }
261
+ }