protoagent 0.0.5 → 0.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.
- package/README.md +99 -19
- package/dist/App.js +602 -0
- package/dist/agentic-loop.js +492 -525
- package/dist/cli.js +39 -0
- package/dist/components/CollapsibleBox.js +26 -0
- package/dist/components/ConfigDialog.js +40 -0
- package/dist/components/ConsolidatedToolMessage.js +41 -0
- package/dist/components/FormattedMessage.js +93 -0
- package/dist/components/Table.js +275 -0
- package/dist/config.js +171 -0
- package/dist/mcp.js +170 -0
- package/dist/providers.js +137 -0
- package/dist/sessions.js +161 -0
- package/dist/skills.js +229 -0
- package/dist/sub-agent.js +103 -0
- package/dist/system-prompt.js +131 -0
- package/dist/tools/bash.js +178 -0
- package/dist/tools/edit-file.js +65 -171
- package/dist/tools/index.js +79 -134
- package/dist/tools/list-directory.js +20 -73
- package/dist/tools/read-file.js +57 -101
- package/dist/tools/search-files.js +74 -162
- package/dist/tools/todo.js +57 -140
- package/dist/tools/webfetch.js +310 -0
- package/dist/tools/write-file.js +44 -135
- package/dist/utils/approval.js +69 -0
- package/dist/utils/compactor.js +87 -0
- package/dist/utils/cost-tracker.js +26 -81
- package/dist/utils/format-message.js +26 -0
- package/dist/utils/logger.js +101 -307
- package/dist/utils/path-validation.js +74 -0
- package/package.json +45 -51
- package/LICENSE +0 -21
- package/dist/config/client.js +0 -315
- package/dist/config/commands.js +0 -223
- package/dist/config/manager.js +0 -117
- package/dist/config/mcp-commands.js +0 -266
- package/dist/config/mcp-manager.js +0 -240
- package/dist/config/mcp-types.js +0 -28
- package/dist/config/providers.js +0 -229
- package/dist/config/setup.js +0 -209
- package/dist/config/system-prompt.js +0 -397
- package/dist/config/types.js +0 -4
- package/dist/index.js +0 -229
- package/dist/tools/create-directory.js +0 -76
- package/dist/tools/directory-operations.js +0 -195
- package/dist/tools/file-operations.js +0 -211
- package/dist/tools/run-shell-command.js +0 -746
- package/dist/tools/search-operations.js +0 -179
- package/dist/tools/shell-operations.js +0 -342
- package/dist/tools/task-complete.js +0 -26
- package/dist/tools/view-directory-tree.js +0 -125
- package/dist/tools.js +0 -2
- package/dist/utils/conversation-compactor.js +0 -140
- package/dist/utils/enhanced-prompt.js +0 -23
- package/dist/utils/file-operations-approval.js +0 -373
- package/dist/utils/interrupt-handler.js +0 -127
- package/dist/utils/user-cancellation.js +0 -34
package/package.json
CHANGED
|
@@ -1,61 +1,55 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "protoagent",
|
|
3
|
-
"version": "0.0
|
|
4
|
-
"description": "Interactive AI coding agent CLI with file system capabilities and shell command execution",
|
|
3
|
+
"version": "0.1.0",
|
|
5
4
|
"type": "module",
|
|
6
|
-
"
|
|
5
|
+
"files": [
|
|
6
|
+
"dist",
|
|
7
|
+
"README.md"
|
|
8
|
+
],
|
|
7
9
|
"bin": {
|
|
8
|
-
"protoagent": "dist/
|
|
10
|
+
"protoagent": "./dist/cli.js"
|
|
9
11
|
},
|
|
10
12
|
"scripts": {
|
|
11
|
-
"
|
|
12
|
-
"dev": "tsx src/
|
|
13
|
-
"
|
|
14
|
-
"
|
|
15
|
-
"
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
"
|
|
19
|
-
"
|
|
20
|
-
"coding-assistant",
|
|
21
|
-
"agent",
|
|
22
|
-
"coding",
|
|
23
|
-
"typescript",
|
|
24
|
-
"interactive",
|
|
25
|
-
"openai",
|
|
26
|
-
"file-system",
|
|
27
|
-
"shell-commands",
|
|
28
|
-
"developer-tools"
|
|
29
|
-
],
|
|
30
|
-
"author": "Thomas Gauvin",
|
|
31
|
-
"license": "MIT",
|
|
32
|
-
"repository": {
|
|
33
|
-
"type": "git",
|
|
34
|
-
"url": "git+https://github.com/thomasgauvin/protoagent.git"
|
|
35
|
-
},
|
|
36
|
-
"bugs": {
|
|
37
|
-
"url": "https://github.com/thomasgauvin/protoagent/issues"
|
|
38
|
-
},
|
|
39
|
-
"homepage": "https://github.com/thomasgauvin/protoagent#readme",
|
|
40
|
-
"devDependencies": {
|
|
41
|
-
"@types/inquirer": "^9.0.9",
|
|
42
|
-
"@types/node": "^20.0.0",
|
|
43
|
-
"dotenv": "^17.2.1",
|
|
44
|
-
"tsx": "^4.7.0",
|
|
45
|
-
"typescript": "^5.0.0"
|
|
13
|
+
"clean": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\"",
|
|
14
|
+
"dev": "tsx src/cli.tsx",
|
|
15
|
+
"test": "node --test --import tsx tests/**/*.test.{ts,tsx}",
|
|
16
|
+
"build": "npm run clean && tsc && node -e \"const fs=require('node:fs'); if (fs.existsSync('dist/cli.js')) fs.chmodSync('dist/cli.js', 0o755)\"",
|
|
17
|
+
"build:watch": "tsc --watch",
|
|
18
|
+
"prepack": "npm run build",
|
|
19
|
+
"docs:dev": "vitepress dev docs",
|
|
20
|
+
"docs:build": "vitepress build docs",
|
|
21
|
+
"docs:preview": "vitepress preview docs"
|
|
46
22
|
},
|
|
23
|
+
"author": "",
|
|
24
|
+
"license": "ISC",
|
|
47
25
|
"dependencies": {
|
|
48
|
-
"
|
|
49
|
-
"
|
|
50
|
-
"
|
|
51
|
-
"
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
"
|
|
26
|
+
"@inkjs/ui": "^2.0.0",
|
|
27
|
+
"@modelcontextprotocol/sdk": "^1.27.1",
|
|
28
|
+
"commander": "^14.0.1",
|
|
29
|
+
"he": "^1.2.0",
|
|
30
|
+
"html-to-text": "^9.0.5",
|
|
31
|
+
"ink": "^6.7.0",
|
|
32
|
+
"ink-big-text": "^2.0.0",
|
|
33
|
+
"openai": "^5.23.1",
|
|
34
|
+
"react": "^19.1.1",
|
|
35
|
+
"turndown": "^7.2.2",
|
|
36
|
+
"yaml": "^2.8.2",
|
|
37
|
+
"yoga-layout": "^3.2.1"
|
|
55
38
|
},
|
|
56
|
-
"
|
|
57
|
-
"
|
|
58
|
-
"
|
|
59
|
-
"
|
|
60
|
-
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@eslint/js": "^9.36.0",
|
|
41
|
+
"@tailwindcss/postcss": "^4.1.18",
|
|
42
|
+
"@types/he": "^1.2.3",
|
|
43
|
+
"@types/html-to-text": "^9.0.4",
|
|
44
|
+
"@types/node": "^24.5.2",
|
|
45
|
+
"@types/react": "^19.1.15",
|
|
46
|
+
"@types/turndown": "^5.0.6",
|
|
47
|
+
"eslint": "^9.36.0",
|
|
48
|
+
"ink-testing-library": "^4.0.0",
|
|
49
|
+
"tailwindcss": "^4.0.0",
|
|
50
|
+
"tsx": "^4.20.6",
|
|
51
|
+
"typescript": "^5.9.2",
|
|
52
|
+
"typescript-eslint": "^8.44.1",
|
|
53
|
+
"vitepress": "^1.6.4"
|
|
54
|
+
}
|
|
61
55
|
}
|
package/LICENSE
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2025 Thomas Gauvin
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|
package/dist/config/client.js
DELETED
|
@@ -1,315 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* OpenAI client manager for ProtoAgent
|
|
3
|
-
*/
|
|
4
|
-
import OpenAI from 'openai';
|
|
5
|
-
import { geminiProvider, cerebrasProvider, anthropicProvider, getModelConfig } from './providers.js';
|
|
6
|
-
import { estimateTokens, getContextInfo } from '../utils/cost-tracker.js';
|
|
7
|
-
import { logger } from '../utils/logger.js';
|
|
8
|
-
/**
|
|
9
|
-
* Create OpenAI client from configuration
|
|
10
|
-
*/
|
|
11
|
-
export function createOpenAIClient(config) {
|
|
12
|
-
logger.debug('🤖 Creating OpenAI client', {
|
|
13
|
-
component: 'OpenAIClient',
|
|
14
|
-
provider: config.provider,
|
|
15
|
-
model: config.model
|
|
16
|
-
});
|
|
17
|
-
if (config.provider === 'openai') {
|
|
18
|
-
logger.debug('🔑 Using OpenAI provider', { component: 'OpenAIClient' });
|
|
19
|
-
return new OpenAI({
|
|
20
|
-
apiKey: config.credentials.OPENAI_API_KEY
|
|
21
|
-
});
|
|
22
|
-
}
|
|
23
|
-
if (config.provider === 'gemini') {
|
|
24
|
-
logger.debug('🔑 Using Gemini provider', {
|
|
25
|
-
component: 'OpenAIClient',
|
|
26
|
-
baseURL: geminiProvider.baseURL
|
|
27
|
-
});
|
|
28
|
-
return new OpenAI({
|
|
29
|
-
apiKey: config.credentials.GEMINI_API_KEY,
|
|
30
|
-
baseURL: geminiProvider.baseURL
|
|
31
|
-
});
|
|
32
|
-
}
|
|
33
|
-
if (config.provider === 'cerebras') {
|
|
34
|
-
logger.debug('🔑 Using Cerebras provider', {
|
|
35
|
-
component: 'OpenAIClient',
|
|
36
|
-
baseURL: cerebrasProvider.baseURL
|
|
37
|
-
});
|
|
38
|
-
return new OpenAI({
|
|
39
|
-
apiKey: config.credentials.CEREBRAS_API_KEY,
|
|
40
|
-
baseURL: cerebrasProvider.baseURL
|
|
41
|
-
});
|
|
42
|
-
}
|
|
43
|
-
if (config.provider === 'anthropic') {
|
|
44
|
-
logger.debug('🔑 Using Anthropic provider', {
|
|
45
|
-
component: 'OpenAIClient',
|
|
46
|
-
baseURL: anthropicProvider.baseURL
|
|
47
|
-
});
|
|
48
|
-
return new OpenAI({
|
|
49
|
-
apiKey: config.credentials.ANTHROPIC_API_KEY,
|
|
50
|
-
baseURL: anthropicProvider.baseURL
|
|
51
|
-
});
|
|
52
|
-
}
|
|
53
|
-
logger.error('❌ Unknown provider', {
|
|
54
|
-
component: 'OpenAIClient',
|
|
55
|
-
provider: config.provider
|
|
56
|
-
});
|
|
57
|
-
throw new Error(`Unknown provider: ${config.provider}`);
|
|
58
|
-
}
|
|
59
|
-
/**
|
|
60
|
-
* Sleep for a given number of milliseconds
|
|
61
|
-
*/
|
|
62
|
-
function sleep(ms) {
|
|
63
|
-
return new Promise(resolve => setTimeout(resolve, ms));
|
|
64
|
-
}
|
|
65
|
-
/**
|
|
66
|
-
* Check if an error is retryable
|
|
67
|
-
*/
|
|
68
|
-
function isRetryableError(error) {
|
|
69
|
-
logger.debug('🔍 Checking if error is retryable', {
|
|
70
|
-
component: 'OpenAIClient',
|
|
71
|
-
status: error?.status,
|
|
72
|
-
code: error?.code
|
|
73
|
-
});
|
|
74
|
-
// Check for rate limiting (429)
|
|
75
|
-
if (error?.status === 429) {
|
|
76
|
-
logger.debug('⏳ Error is rate limit (429)', { component: 'OpenAIClient' });
|
|
77
|
-
return true;
|
|
78
|
-
}
|
|
79
|
-
// Check for server errors (5xx)
|
|
80
|
-
if (error?.status >= 500 && error?.status < 600) {
|
|
81
|
-
logger.debug('🔧 Error is server error (5xx)', {
|
|
82
|
-
component: 'OpenAIClient',
|
|
83
|
-
status: error.status
|
|
84
|
-
});
|
|
85
|
-
return true;
|
|
86
|
-
}
|
|
87
|
-
// Check for network/connection errors
|
|
88
|
-
if (error?.code === 'ECONNRESET' ||
|
|
89
|
-
error?.code === 'ENOTFOUND' ||
|
|
90
|
-
error?.code === 'ECONNREFUSED' ||
|
|
91
|
-
error?.message?.includes('network') ||
|
|
92
|
-
error?.message?.includes('timeout')) {
|
|
93
|
-
logger.debug('🌐 Error is network related', {
|
|
94
|
-
component: 'OpenAIClient',
|
|
95
|
-
code: error.code,
|
|
96
|
-
message: error.message
|
|
97
|
-
});
|
|
98
|
-
return true;
|
|
99
|
-
}
|
|
100
|
-
logger.debug('❌ Error is not retryable', { component: 'OpenAIClient' });
|
|
101
|
-
return false;
|
|
102
|
-
}
|
|
103
|
-
/**
|
|
104
|
-
* Get appropriate delay for rate limiting based on error
|
|
105
|
-
*/
|
|
106
|
-
function getRateLimitDelay(error, attempt) {
|
|
107
|
-
logger.debug('⏱️ Calculating rate limit delay', {
|
|
108
|
-
component: 'OpenAIClient',
|
|
109
|
-
attempt,
|
|
110
|
-
retryAfterHeader: error?.headers?.['retry-after']
|
|
111
|
-
});
|
|
112
|
-
// Check for Retry-After header (OpenAI provides this for rate limits)
|
|
113
|
-
if (error?.headers?.['retry-after']) {
|
|
114
|
-
const retryAfter = parseInt(error.headers['retry-after'], 10);
|
|
115
|
-
if (!isNaN(retryAfter)) {
|
|
116
|
-
const delayMs = retryAfter * 1000;
|
|
117
|
-
logger.debug('📨 Using Retry-After header delay', {
|
|
118
|
-
component: 'OpenAIClient',
|
|
119
|
-
retryAfter,
|
|
120
|
-
delayMs
|
|
121
|
-
});
|
|
122
|
-
return delayMs; // Convert to milliseconds
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
// Default exponential backoff for rate limits: 2^attempt seconds (min 2s, max 60s)
|
|
126
|
-
const baseDelay = Math.min(Math.pow(2, attempt) * 1000, 60000);
|
|
127
|
-
const jitter = Math.random() * 1000; // Add jitter to prevent thundering herd
|
|
128
|
-
const finalDelay = Math.max(baseDelay + jitter, 2000); // Minimum 2 seconds
|
|
129
|
-
logger.debug('⚡ Using exponential backoff delay', {
|
|
130
|
-
component: 'OpenAIClient',
|
|
131
|
-
attempt,
|
|
132
|
-
baseDelay,
|
|
133
|
-
jitter,
|
|
134
|
-
finalDelay
|
|
135
|
-
});
|
|
136
|
-
return finalDelay;
|
|
137
|
-
}
|
|
138
|
-
/**
|
|
139
|
-
* Get delay for general retryable errors
|
|
140
|
-
*/
|
|
141
|
-
function getRetryDelay(attempt) {
|
|
142
|
-
// Exponential backoff: 1, 2, 4 seconds with jitter
|
|
143
|
-
const baseDelay = Math.pow(2, attempt - 1) * 1000;
|
|
144
|
-
const jitter = Math.random() * 500;
|
|
145
|
-
const finalDelay = Math.min(baseDelay + jitter, 4000); // Max 4 seconds
|
|
146
|
-
logger.debug('🔄 Calculating retry delay', {
|
|
147
|
-
component: 'OpenAIClient',
|
|
148
|
-
attempt,
|
|
149
|
-
baseDelay,
|
|
150
|
-
jitter,
|
|
151
|
-
finalDelay
|
|
152
|
-
});
|
|
153
|
-
return finalDelay;
|
|
154
|
-
}
|
|
155
|
-
/**
|
|
156
|
-
* Create chat completion with OpenAI with retry logic and cost tracking
|
|
157
|
-
*/
|
|
158
|
-
export async function createChatCompletion(client, params, config, messages) {
|
|
159
|
-
const maxRetries = 3;
|
|
160
|
-
let lastError;
|
|
161
|
-
logger.debug('🚀 Starting chat completion request', {
|
|
162
|
-
component: 'OpenAIClient',
|
|
163
|
-
provider: config.provider,
|
|
164
|
-
model: config.model,
|
|
165
|
-
messageCount: messages.length,
|
|
166
|
-
maxRetries
|
|
167
|
-
});
|
|
168
|
-
// Get model configuration for cost tracking
|
|
169
|
-
const modelConfig = getModelConfig(config.provider, config.model);
|
|
170
|
-
// Estimate input tokens for cost calculation
|
|
171
|
-
const estimatedInputTokens = messages.reduce((total, msg) => {
|
|
172
|
-
if ('content' in msg && msg.content && typeof msg.content === 'string') {
|
|
173
|
-
return total + estimateTokens(msg.content);
|
|
174
|
-
}
|
|
175
|
-
return total;
|
|
176
|
-
}, 0);
|
|
177
|
-
logger.debug('📊 Token estimation complete', {
|
|
178
|
-
component: 'OpenAIClient',
|
|
179
|
-
estimatedInputTokens,
|
|
180
|
-
hasModelConfig: !!modelConfig
|
|
181
|
-
});
|
|
182
|
-
// Log context information before making the request
|
|
183
|
-
if (modelConfig) {
|
|
184
|
-
const contextInfo = getContextInfo(messages, modelConfig);
|
|
185
|
-
logger.consoleLog(`📊 Context: ${contextInfo.currentTokens}/${contextInfo.maxTokens} tokens (${contextInfo.utilizationPercentage.toFixed(1)}%)`, {
|
|
186
|
-
component: 'OpenAIClient',
|
|
187
|
-
...contextInfo
|
|
188
|
-
});
|
|
189
|
-
if (contextInfo.needsCompaction) {
|
|
190
|
-
logger.consoleLog(`⚠️ Context approaching limit - automatic compaction will trigger soon`, {
|
|
191
|
-
component: 'OpenAIClient',
|
|
192
|
-
needsCompaction: true
|
|
193
|
-
});
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
197
|
-
logger.debug(`🎯 Attempt ${attempt}/${maxRetries}`, {
|
|
198
|
-
component: 'OpenAIClient',
|
|
199
|
-
attempt,
|
|
200
|
-
maxRetries
|
|
201
|
-
});
|
|
202
|
-
try {
|
|
203
|
-
logger.debug('📡 Making API request', {
|
|
204
|
-
component: 'OpenAIClient',
|
|
205
|
-
stream: params.stream || true,
|
|
206
|
-
includeUsage: true
|
|
207
|
-
});
|
|
208
|
-
const stream = await client.chat.completions.create({
|
|
209
|
-
...params,
|
|
210
|
-
stream: true,
|
|
211
|
-
stream_options: { include_usage: true }
|
|
212
|
-
});
|
|
213
|
-
logger.debug('✅ API request successful', {
|
|
214
|
-
component: 'OpenAIClient',
|
|
215
|
-
attempt,
|
|
216
|
-
estimatedInputTokens
|
|
217
|
-
});
|
|
218
|
-
return { stream, estimatedInputTokens };
|
|
219
|
-
}
|
|
220
|
-
catch (error) {
|
|
221
|
-
lastError = error;
|
|
222
|
-
logger.debug('❌ API request failed', {
|
|
223
|
-
component: 'OpenAIClient',
|
|
224
|
-
attempt,
|
|
225
|
-
errorStatus: error?.status,
|
|
226
|
-
errorCode: error?.code,
|
|
227
|
-
errorMessage: error?.message
|
|
228
|
-
});
|
|
229
|
-
// Handle rate limiting (429) specially
|
|
230
|
-
if (error?.status === 429) {
|
|
231
|
-
const delay = getRateLimitDelay(error, attempt);
|
|
232
|
-
const seconds = Math.round(delay / 1000);
|
|
233
|
-
if (attempt < maxRetries) {
|
|
234
|
-
logger.consoleLog(`\n⏳ Rate limited by API. Waiting ${seconds} seconds before retry (attempt ${attempt}/${maxRetries})...`, {
|
|
235
|
-
component: 'OpenAIClient',
|
|
236
|
-
attempt,
|
|
237
|
-
maxRetries,
|
|
238
|
-
delayMs: delay
|
|
239
|
-
});
|
|
240
|
-
await sleep(delay);
|
|
241
|
-
continue;
|
|
242
|
-
}
|
|
243
|
-
else {
|
|
244
|
-
logger.consoleLog('\n❌ Rate limit exceeded. Maximum retries reached.', {
|
|
245
|
-
component: 'OpenAIClient',
|
|
246
|
-
maxRetries
|
|
247
|
-
});
|
|
248
|
-
logger.consoleLog('💡 Tip: Consider upgrading your API plan or waiting before making more requests.');
|
|
249
|
-
break;
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
// Handle other retryable errors
|
|
253
|
-
if (isRetryableError(error) && attempt < maxRetries) {
|
|
254
|
-
const delay = getRetryDelay(attempt);
|
|
255
|
-
const seconds = Math.round(delay / 1000);
|
|
256
|
-
logger.consoleLog(`\n⚠️ API error (${error?.status || error?.code || 'unknown'}). Retrying in ${seconds} seconds (attempt ${attempt}/${maxRetries})...`, {
|
|
257
|
-
component: 'OpenAIClient',
|
|
258
|
-
attempt,
|
|
259
|
-
maxRetries,
|
|
260
|
-
errorType: error?.status || error?.code || 'unknown',
|
|
261
|
-
delayMs: delay
|
|
262
|
-
});
|
|
263
|
-
await sleep(delay);
|
|
264
|
-
continue;
|
|
265
|
-
}
|
|
266
|
-
// For non-retryable errors or max retries reached, break immediately
|
|
267
|
-
logger.error('💥 Breaking retry loop', {
|
|
268
|
-
component: 'OpenAIClient',
|
|
269
|
-
attempt,
|
|
270
|
-
maxRetries,
|
|
271
|
-
isRetryable: isRetryableError(error)
|
|
272
|
-
});
|
|
273
|
-
break;
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
// If we get here, all retries failed
|
|
277
|
-
logger.consoleLog('\n❌ API request failed after all retry attempts.', {
|
|
278
|
-
component: 'OpenAIClient',
|
|
279
|
-
maxRetries,
|
|
280
|
-
lastErrorStatus: lastError?.status,
|
|
281
|
-
lastErrorCode: lastError?.code
|
|
282
|
-
});
|
|
283
|
-
// Provide user-friendly error messages
|
|
284
|
-
if (lastError?.status === 401) {
|
|
285
|
-
logger.consoleLog('💡 This looks like an authentication error. Check your API key configuration.');
|
|
286
|
-
logger.consoleLog(' Run: protoagent config --update-key');
|
|
287
|
-
logger.error('🔐 Authentication error detected', { component: 'OpenAIClient' });
|
|
288
|
-
}
|
|
289
|
-
else if (lastError?.status === 429) {
|
|
290
|
-
logger.consoleLog('💡 Rate limit exceeded. Try again later or upgrade your API plan.');
|
|
291
|
-
logger.error('⏳ Rate limit error detected', { component: 'OpenAIClient' });
|
|
292
|
-
}
|
|
293
|
-
else if (lastError?.status === 403) {
|
|
294
|
-
logger.consoleLog('💡 Access forbidden. Check your API key permissions and billing status.');
|
|
295
|
-
logger.error('🚫 Forbidden access error detected', { component: 'OpenAIClient' });
|
|
296
|
-
}
|
|
297
|
-
else if (lastError?.status >= 500) {
|
|
298
|
-
logger.consoleLog('💡 Server error. The API service may be temporarily unavailable.');
|
|
299
|
-
logger.error('🔧 Server error detected', {
|
|
300
|
-
component: 'OpenAIClient',
|
|
301
|
-
status: lastError.status
|
|
302
|
-
});
|
|
303
|
-
}
|
|
304
|
-
else if (lastError?.code === 'ENOTFOUND' || lastError?.message?.includes('network')) {
|
|
305
|
-
logger.consoleLog('💡 Network connection error. Check your internet connection.');
|
|
306
|
-
logger.error('🌐 Network error detected', {
|
|
307
|
-
component: 'OpenAIClient',
|
|
308
|
-
code: lastError?.code
|
|
309
|
-
});
|
|
310
|
-
}
|
|
311
|
-
// Suggest graceful shutdown
|
|
312
|
-
logger.consoleLog('\n🚪 ProtoAgent will now exit. Please resolve the issue and try again.');
|
|
313
|
-
logger.error('🚪 Exiting due to API failure', { component: 'OpenAIClient' });
|
|
314
|
-
process.exit(1);
|
|
315
|
-
}
|
package/dist/config/commands.js
DELETED
|
@@ -1,223 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Configuration management commands for ProtoAgent CLI
|
|
3
|
-
*/
|
|
4
|
-
import inquirer from 'inquirer';
|
|
5
|
-
import { hasConfig, loadConfig, saveConfig, getConfigDirectory } from './manager.js';
|
|
6
|
-
import { setupConfig } from './setup.js';
|
|
7
|
-
import { openaiProvider, geminiProvider, cerebrasProvider, anthropicProvider } from './providers.js';
|
|
8
|
-
import fs from 'fs/promises';
|
|
9
|
-
import path from 'path';
|
|
10
|
-
import { logger } from '../utils/logger.js';
|
|
11
|
-
/**
|
|
12
|
-
* Mask API key for display (show first 8 and last 4 characters)
|
|
13
|
-
*/
|
|
14
|
-
function maskApiKey(apiKey) {
|
|
15
|
-
if (apiKey.length <= 12) {
|
|
16
|
-
return '*'.repeat(apiKey.length);
|
|
17
|
-
}
|
|
18
|
-
const start = apiKey.substring(0, 8);
|
|
19
|
-
const end = apiKey.substring(apiKey.length - 4);
|
|
20
|
-
const middle = '*'.repeat(apiKey.length - 12);
|
|
21
|
-
return `${start}${middle}${end}`;
|
|
22
|
-
}
|
|
23
|
-
/**
|
|
24
|
-
* Show current configuration (without exposing sensitive data)
|
|
25
|
-
*/
|
|
26
|
-
export async function showCurrentConfig() {
|
|
27
|
-
try {
|
|
28
|
-
const configExists = await hasConfig();
|
|
29
|
-
if (!configExists) {
|
|
30
|
-
logger.consoleLog('❌ No configuration found. Run ProtoAgent to set up.');
|
|
31
|
-
return;
|
|
32
|
-
}
|
|
33
|
-
const config = await loadConfig();
|
|
34
|
-
const configDir = getConfigDirectory();
|
|
35
|
-
logger.consoleLog('\n📋 Current ProtoAgent Configuration:');
|
|
36
|
-
logger.consoleLog('─'.repeat(40));
|
|
37
|
-
logger.consoleLog(`Provider: ${config.provider}`);
|
|
38
|
-
logger.consoleLog(`Model: ${config.model}`);
|
|
39
|
-
// Show the appropriate API key based on provider
|
|
40
|
-
if (config.provider === 'openai' && config.credentials.OPENAI_API_KEY) {
|
|
41
|
-
logger.consoleLog(`API Key: ${maskApiKey(config.credentials.OPENAI_API_KEY)}`);
|
|
42
|
-
}
|
|
43
|
-
else if (config.provider === 'gemini' && config.credentials.GEMINI_API_KEY) {
|
|
44
|
-
logger.consoleLog(`API Key: ${maskApiKey(config.credentials.GEMINI_API_KEY)}`);
|
|
45
|
-
}
|
|
46
|
-
else if (config.provider === 'cerebras' && config.credentials.CEREBRAS_API_KEY) {
|
|
47
|
-
logger.consoleLog(`API Key: ${maskApiKey(config.credentials.CEREBRAS_API_KEY)}`);
|
|
48
|
-
}
|
|
49
|
-
else if (config.provider === 'anthropic' && config.credentials.ANTHROPIC_API_KEY) {
|
|
50
|
-
logger.consoleLog(`API Key: ${maskApiKey(config.credentials.ANTHROPIC_API_KEY)}`);
|
|
51
|
-
}
|
|
52
|
-
logger.consoleLog(`Config Location: ${configDir}/config.json`);
|
|
53
|
-
logger.consoleLog('─'.repeat(40));
|
|
54
|
-
}
|
|
55
|
-
catch (error) {
|
|
56
|
-
console.error(`❌ Error reading configuration: ${error.message}`);
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
/**
|
|
60
|
-
* Update API key
|
|
61
|
-
*/
|
|
62
|
-
export async function updateApiKey() {
|
|
63
|
-
try {
|
|
64
|
-
const configExists = await hasConfig();
|
|
65
|
-
if (!configExists) {
|
|
66
|
-
logger.consoleLog('❌ No configuration found. Run ProtoAgent to set up first.');
|
|
67
|
-
return;
|
|
68
|
-
}
|
|
69
|
-
const config = await loadConfig();
|
|
70
|
-
logger.consoleLog(`\n🔑 Update ${config.provider.toUpperCase()} API Key`);
|
|
71
|
-
// Show current API key based on provider
|
|
72
|
-
let currentKey = '';
|
|
73
|
-
let helpUrl = '';
|
|
74
|
-
let keyPrefix = '';
|
|
75
|
-
if (config.provider === 'openai') {
|
|
76
|
-
currentKey = config.credentials.OPENAI_API_KEY || '';
|
|
77
|
-
helpUrl = 'https://platform.openai.com/api-keys';
|
|
78
|
-
keyPrefix = 'sk-';
|
|
79
|
-
}
|
|
80
|
-
else if (config.provider === 'gemini') {
|
|
81
|
-
currentKey = config.credentials.GEMINI_API_KEY || '';
|
|
82
|
-
helpUrl = 'https://aistudio.google.com/app/apikey';
|
|
83
|
-
keyPrefix = '';
|
|
84
|
-
}
|
|
85
|
-
else if (config.provider === 'cerebras') {
|
|
86
|
-
currentKey = config.credentials.CEREBRAS_API_KEY || '';
|
|
87
|
-
helpUrl = 'https://cloud.cerebras.ai/platform';
|
|
88
|
-
keyPrefix = '';
|
|
89
|
-
}
|
|
90
|
-
else if (config.provider === 'anthropic') {
|
|
91
|
-
currentKey = config.credentials.ANTHROPIC_API_KEY || '';
|
|
92
|
-
helpUrl = 'https://console.anthropic.com/settings/keys';
|
|
93
|
-
keyPrefix = 'sk-';
|
|
94
|
-
}
|
|
95
|
-
logger.consoleLog(`Current API Key: ${maskApiKey(currentKey)}`);
|
|
96
|
-
logger.consoleLog(`Get your API key from: ${helpUrl}\n`);
|
|
97
|
-
const { newApiKey } = await inquirer.prompt([
|
|
98
|
-
{
|
|
99
|
-
type: 'password',
|
|
100
|
-
name: 'newApiKey',
|
|
101
|
-
message: `Enter your new ${config.provider.toUpperCase()} API key:`,
|
|
102
|
-
mask: '*',
|
|
103
|
-
validate: (input) => {
|
|
104
|
-
if (!input || input.trim().length === 0) {
|
|
105
|
-
return 'API key is required';
|
|
106
|
-
}
|
|
107
|
-
if (keyPrefix && !input.trim().startsWith(keyPrefix)) {
|
|
108
|
-
return `${config.provider.toUpperCase()} API keys should start with "${keyPrefix}"`;
|
|
109
|
-
}
|
|
110
|
-
return true;
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
]);
|
|
114
|
-
// Update configuration based on provider
|
|
115
|
-
if (config.provider === 'openai') {
|
|
116
|
-
config.credentials.OPENAI_API_KEY = newApiKey.trim();
|
|
117
|
-
}
|
|
118
|
-
else if (config.provider === 'gemini') {
|
|
119
|
-
config.credentials.GEMINI_API_KEY = newApiKey.trim();
|
|
120
|
-
}
|
|
121
|
-
else if (config.provider === 'cerebras') {
|
|
122
|
-
config.credentials.CEREBRAS_API_KEY = newApiKey.trim();
|
|
123
|
-
}
|
|
124
|
-
else if (config.provider === 'anthropic') {
|
|
125
|
-
config.credentials.ANTHROPIC_API_KEY = newApiKey.trim();
|
|
126
|
-
}
|
|
127
|
-
await saveConfig(config);
|
|
128
|
-
logger.consoleLog('\n✅ API key updated successfully!');
|
|
129
|
-
logger.consoleLog(`New API Key: ${maskApiKey(newApiKey)}`);
|
|
130
|
-
}
|
|
131
|
-
catch (error) {
|
|
132
|
-
console.error(`❌ Error updating API key: ${error.message}`);
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
/**
|
|
136
|
-
* Update model
|
|
137
|
-
*/
|
|
138
|
-
export async function updateModel() {
|
|
139
|
-
try {
|
|
140
|
-
const configExists = await hasConfig();
|
|
141
|
-
if (!configExists) {
|
|
142
|
-
logger.consoleLog('❌ No configuration found. Run ProtoAgent to set up first.');
|
|
143
|
-
return;
|
|
144
|
-
}
|
|
145
|
-
const config = await loadConfig();
|
|
146
|
-
logger.consoleLog(`\n🤖 Update ${config.provider.toUpperCase()} Model`);
|
|
147
|
-
logger.consoleLog(`Current Model: ${config.model}\n`);
|
|
148
|
-
// Get available models based on provider
|
|
149
|
-
let availableModels = [];
|
|
150
|
-
if (config.provider === 'openai') {
|
|
151
|
-
availableModels = openaiProvider.models;
|
|
152
|
-
}
|
|
153
|
-
else if (config.provider === 'gemini') {
|
|
154
|
-
availableModels = geminiProvider.models;
|
|
155
|
-
}
|
|
156
|
-
else if (config.provider === 'cerebras') {
|
|
157
|
-
availableModels = cerebrasProvider.models;
|
|
158
|
-
}
|
|
159
|
-
else if (config.provider === 'anthropic') {
|
|
160
|
-
availableModels = anthropicProvider.models;
|
|
161
|
-
}
|
|
162
|
-
const { newModel } = await inquirer.prompt([
|
|
163
|
-
{
|
|
164
|
-
type: 'list',
|
|
165
|
-
name: 'newModel',
|
|
166
|
-
message: `Select a new ${config.provider.toUpperCase()} model:`,
|
|
167
|
-
choices: availableModels.map(model => ({
|
|
168
|
-
name: model === config.model ? `${model} (current)` : model,
|
|
169
|
-
value: model
|
|
170
|
-
})),
|
|
171
|
-
default: config.model
|
|
172
|
-
}
|
|
173
|
-
]);
|
|
174
|
-
if (newModel === config.model) {
|
|
175
|
-
logger.consoleLog('✨ No change needed - same model selected.');
|
|
176
|
-
return;
|
|
177
|
-
}
|
|
178
|
-
// Update configuration
|
|
179
|
-
const previousModel = config.model;
|
|
180
|
-
config.model = newModel;
|
|
181
|
-
await saveConfig(config);
|
|
182
|
-
logger.consoleLog('\n✅ Model updated successfully!');
|
|
183
|
-
logger.consoleLog(`Previous Model: ${previousModel}`);
|
|
184
|
-
logger.consoleLog(`New Model: ${newModel}`);
|
|
185
|
-
}
|
|
186
|
-
catch (error) {
|
|
187
|
-
console.error(`❌ Error updating model: ${error.message}`);
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
/**
|
|
191
|
-
* Reset configuration (full reconfiguration)
|
|
192
|
-
*/
|
|
193
|
-
export async function resetConfiguration() {
|
|
194
|
-
try {
|
|
195
|
-
const configExists = await hasConfig();
|
|
196
|
-
if (configExists) {
|
|
197
|
-
logger.consoleLog('\n⚠️ Reset Configuration');
|
|
198
|
-
logger.consoleLog('This will delete your current configuration and set up ProtoAgent from scratch.');
|
|
199
|
-
const { confirm } = await inquirer.prompt([
|
|
200
|
-
{
|
|
201
|
-
type: 'confirm',
|
|
202
|
-
name: 'confirm',
|
|
203
|
-
message: 'Are you sure you want to reset your configuration?',
|
|
204
|
-
default: false
|
|
205
|
-
}
|
|
206
|
-
]);
|
|
207
|
-
if (!confirm) {
|
|
208
|
-
logger.consoleLog('Configuration reset cancelled.');
|
|
209
|
-
return;
|
|
210
|
-
}
|
|
211
|
-
// Delete existing configuration
|
|
212
|
-
const configPath = path.join(getConfigDirectory(), 'config.json');
|
|
213
|
-
await fs.unlink(configPath);
|
|
214
|
-
logger.consoleLog('✅ Existing configuration deleted.');
|
|
215
|
-
}
|
|
216
|
-
// Run setup again
|
|
217
|
-
logger.consoleLog('\n🔄 Starting fresh configuration setup...');
|
|
218
|
-
await setupConfig();
|
|
219
|
-
}
|
|
220
|
-
catch (error) {
|
|
221
|
-
console.error(`❌ Error resetting configuration: ${error.message}`);
|
|
222
|
-
}
|
|
223
|
-
}
|