codeep 1.2.14 → 1.2.16
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/dist/api/index.js +77 -27
- package/dist/config/providers.js +27 -3
- package/dist/config/providers.test.js +27 -1
- package/dist/renderer/App.js +2 -1
- package/dist/renderer/Input.d.ts +13 -1
- package/dist/renderer/Input.js +64 -10
- package/dist/renderer/Screen.d.ts +5 -0
- package/dist/renderer/Screen.js +11 -1
- package/dist/renderer/main.js +38 -5
- package/dist/utils/agent.js +1 -1
- package/dist/utils/tools.d.ts +11 -0
- package/dist/utils/tools.js +102 -2
- package/dist/utils/tools.test.js +9 -2
- package/package.json +1 -1
package/dist/api/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { config, getApiKey } from '../config/index.js';
|
|
2
|
-
import { withRetry, isNetworkError
|
|
2
|
+
import { withRetry, isNetworkError } from '../utils/retry.js';
|
|
3
3
|
import { getProvider, getProviderBaseUrl, getProviderAuthHeader } from '../config/providers.js';
|
|
4
4
|
import { logApiRequest, logApiResponse } from '../utils/logger.js';
|
|
5
5
|
import { loadProjectIntelligence, generateContextFromIntelligence } from '../utils/projectIntelligence.js';
|
|
@@ -92,6 +92,30 @@ export function setProjectContext(ctx) {
|
|
|
92
92
|
cachedIntelligence = null;
|
|
93
93
|
}
|
|
94
94
|
}
|
|
95
|
+
/**
|
|
96
|
+
* Parse API error response body into a human-readable message.
|
|
97
|
+
* Handles JSON error responses from OpenAI, Anthropic, and other providers.
|
|
98
|
+
*/
|
|
99
|
+
function parseApiError(status, body) {
|
|
100
|
+
try {
|
|
101
|
+
const json = JSON.parse(body);
|
|
102
|
+
// OpenAI format: { error: { message: "..." } }
|
|
103
|
+
if (json.error?.message)
|
|
104
|
+
return `${status} - ${json.error.message}`;
|
|
105
|
+
// Anthropic format: { error: { type: "...", message: "..." } }
|
|
106
|
+
if (json.message)
|
|
107
|
+
return `${status} - ${json.message}`;
|
|
108
|
+
// Other: { detail: "..." }
|
|
109
|
+
if (json.detail)
|
|
110
|
+
return `${status} - ${json.detail}`;
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
// Not JSON — use raw body but truncate
|
|
114
|
+
}
|
|
115
|
+
// Truncate long raw error bodies
|
|
116
|
+
const truncated = body.length > 200 ? body.slice(0, 200) + '...' : body;
|
|
117
|
+
return `${status} - ${truncated}`;
|
|
118
|
+
}
|
|
95
119
|
export async function chat(message, history = [], onChunk, onRetry, projectContext, abortSignal) {
|
|
96
120
|
// Update project context if provided
|
|
97
121
|
if (projectContext !== undefined) {
|
|
@@ -119,13 +143,15 @@ export async function chat(message, history = [], onChunk, onRetry, projectConte
|
|
|
119
143
|
}
|
|
120
144
|
},
|
|
121
145
|
shouldRetry: (error) => {
|
|
122
|
-
// Don't retry on user abort
|
|
146
|
+
// Don't retry on user abort or timeout
|
|
123
147
|
if (error.name === 'AbortError')
|
|
124
148
|
return false;
|
|
149
|
+
if (error.isTimeout)
|
|
150
|
+
return false;
|
|
125
151
|
// Don't retry on 4xx client errors (except 429 rate limit)
|
|
126
152
|
if (error.status >= 400 && error.status < 500 && error.status !== 429)
|
|
127
153
|
return false;
|
|
128
|
-
// Retry on network errors,
|
|
154
|
+
// Retry on network errors, 5xx, and rate limits
|
|
129
155
|
return true;
|
|
130
156
|
},
|
|
131
157
|
});
|
|
@@ -135,9 +161,14 @@ export async function chat(message, history = [], onChunk, onRetry, projectConte
|
|
|
135
161
|
}
|
|
136
162
|
catch (error) {
|
|
137
163
|
const err = error;
|
|
138
|
-
//
|
|
164
|
+
// Timeout errors (from chatOpenAI/chatAnthropic) — show user-friendly message
|
|
165
|
+
if (err.isTimeout) {
|
|
166
|
+
logApiResponse(providerId, false, undefined, 'timeout');
|
|
167
|
+
throw error;
|
|
168
|
+
}
|
|
169
|
+
// User cancel (Escape key) — re-throw silently without logging
|
|
139
170
|
if (err.name === 'AbortError' || err.message?.includes('aborted')) {
|
|
140
|
-
throw error;
|
|
171
|
+
throw error;
|
|
141
172
|
}
|
|
142
173
|
// Log error
|
|
143
174
|
logApiResponse(providerId, false, undefined, err.message);
|
|
@@ -145,9 +176,6 @@ export async function chat(message, history = [], onChunk, onRetry, projectConte
|
|
|
145
176
|
if (isNetworkError(error)) {
|
|
146
177
|
throw new Error(getErrorMessage('noInternet'));
|
|
147
178
|
}
|
|
148
|
-
if (isTimeoutError(error)) {
|
|
149
|
-
throw new Error(getErrorMessage('timeout'));
|
|
150
|
-
}
|
|
151
179
|
throw error;
|
|
152
180
|
}
|
|
153
181
|
}
|
|
@@ -247,10 +275,11 @@ async function chatOpenAI(message, history, model, apiKey, onChunk, abortSignal)
|
|
|
247
275
|
if (!baseUrl) {
|
|
248
276
|
throw new Error(`Provider ${providerId} does not support OpenAI protocol`);
|
|
249
277
|
}
|
|
250
|
-
// Create abort controller
|
|
251
|
-
const controller =
|
|
252
|
-
|
|
253
|
-
|
|
278
|
+
// Create abort controller with timeout flag to distinguish from user cancel
|
|
279
|
+
const controller = new AbortController();
|
|
280
|
+
let timedOut = false;
|
|
281
|
+
const timeoutId = setTimeout(() => { timedOut = true; controller.abort(); }, timeout);
|
|
282
|
+
// Listen to external abort signal if provided (user cancel)
|
|
254
283
|
if (abortSignal) {
|
|
255
284
|
abortSignal.addEventListener('abort', () => controller.abort());
|
|
256
285
|
}
|
|
@@ -279,8 +308,8 @@ async function chatOpenAI(message, history, model, apiKey, onChunk, abortSignal)
|
|
|
279
308
|
signal: controller.signal,
|
|
280
309
|
});
|
|
281
310
|
if (!response.ok) {
|
|
282
|
-
const
|
|
283
|
-
const err = new Error(`${getErrorMessage('apiError')}: ${response.status
|
|
311
|
+
const body = await response.text();
|
|
312
|
+
const err = new Error(`${getErrorMessage('apiError')}: ${parseApiError(response.status, body)}`);
|
|
284
313
|
err.status = response.status;
|
|
285
314
|
throw err;
|
|
286
315
|
}
|
|
@@ -296,6 +325,14 @@ async function chatOpenAI(message, history, model, apiKey, onChunk, abortSignal)
|
|
|
296
325
|
return stripThinkTags(content);
|
|
297
326
|
}
|
|
298
327
|
}
|
|
328
|
+
catch (error) {
|
|
329
|
+
if (timedOut) {
|
|
330
|
+
const err = new Error(getErrorMessage('timeout'));
|
|
331
|
+
err.isTimeout = true;
|
|
332
|
+
throw err;
|
|
333
|
+
}
|
|
334
|
+
throw error;
|
|
335
|
+
}
|
|
299
336
|
finally {
|
|
300
337
|
clearTimeout(timeoutId);
|
|
301
338
|
}
|
|
@@ -342,27 +379,31 @@ async function handleOpenAIStream(body, onChunk) {
|
|
|
342
379
|
}
|
|
343
380
|
async function chatAnthropic(message, history, model, apiKey, onChunk, abortSignal) {
|
|
344
381
|
const systemPrompt = getSystemPrompt();
|
|
345
|
-
const
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
{ role: 'user', content: message }
|
|
350
|
-
|
|
382
|
+
const providerId = config.get('provider');
|
|
383
|
+
// Use native system parameter for Anthropic, fake turns for other providers
|
|
384
|
+
const useNativeSystem = providerId === 'anthropic';
|
|
385
|
+
const messages = useNativeSystem
|
|
386
|
+
? [...history, { role: 'user', content: message }]
|
|
387
|
+
: [
|
|
388
|
+
{ role: 'user', content: systemPrompt },
|
|
389
|
+
{ role: 'assistant', content: 'Understood.' },
|
|
390
|
+
...history,
|
|
391
|
+
{ role: 'user', content: message },
|
|
392
|
+
];
|
|
351
393
|
const stream = Boolean(onChunk);
|
|
352
394
|
const timeout = config.get('apiTimeout');
|
|
353
395
|
const temperature = config.get('temperature');
|
|
354
396
|
const maxTokens = config.get('maxTokens');
|
|
355
|
-
// Get provider-specific URL and auth
|
|
356
|
-
const providerId = config.get('provider');
|
|
357
397
|
const baseUrl = getProviderBaseUrl(providerId, 'anthropic');
|
|
358
398
|
const authHeader = getProviderAuthHeader(providerId, 'anthropic');
|
|
359
399
|
if (!baseUrl) {
|
|
360
400
|
throw new Error(`Provider ${providerId} does not support Anthropic protocol`);
|
|
361
401
|
}
|
|
362
|
-
// Create abort controller
|
|
402
|
+
// Create abort controller with timeout flag to distinguish from user cancel
|
|
363
403
|
const controller = new AbortController();
|
|
364
|
-
|
|
365
|
-
|
|
404
|
+
let timedOut = false;
|
|
405
|
+
const timeoutId = setTimeout(() => { timedOut = true; controller.abort(); }, timeout);
|
|
406
|
+
// Listen to external abort signal if provided (user cancel)
|
|
366
407
|
if (abortSignal) {
|
|
367
408
|
abortSignal.addEventListener('abort', () => controller.abort());
|
|
368
409
|
}
|
|
@@ -387,12 +428,13 @@ async function chatAnthropic(message, history, model, apiKey, onChunk, abortSign
|
|
|
387
428
|
max_tokens: maxTokens,
|
|
388
429
|
temperature,
|
|
389
430
|
stream,
|
|
431
|
+
...(useNativeSystem ? { system: systemPrompt } : {}),
|
|
390
432
|
}),
|
|
391
433
|
signal: controller.signal,
|
|
392
434
|
});
|
|
393
435
|
if (!response.ok) {
|
|
394
|
-
const
|
|
395
|
-
const err = new Error(`${getErrorMessage('apiError')}: ${response.status
|
|
436
|
+
const body = await response.text();
|
|
437
|
+
const err = new Error(`${getErrorMessage('apiError')}: ${parseApiError(response.status, body)}`);
|
|
396
438
|
err.status = response.status;
|
|
397
439
|
throw err;
|
|
398
440
|
}
|
|
@@ -408,6 +450,14 @@ async function chatAnthropic(message, history, model, apiKey, onChunk, abortSign
|
|
|
408
450
|
return stripThinkTags(content);
|
|
409
451
|
}
|
|
410
452
|
}
|
|
453
|
+
catch (error) {
|
|
454
|
+
if (timedOut) {
|
|
455
|
+
const err = new Error(getErrorMessage('timeout'));
|
|
456
|
+
err.isTimeout = true;
|
|
457
|
+
throw err;
|
|
458
|
+
}
|
|
459
|
+
throw error;
|
|
460
|
+
}
|
|
411
461
|
finally {
|
|
412
462
|
clearTimeout(timeoutId);
|
|
413
463
|
}
|
package/dist/config/providers.js
CHANGED
|
@@ -85,6 +85,29 @@ export const PROVIDERS = {
|
|
|
85
85
|
envKey: 'MINIMAX_API_KEY',
|
|
86
86
|
subscribeUrl: 'https://platform.minimax.io/subscribe/coding-plan?code=2lWvoWUhrp&source=link',
|
|
87
87
|
},
|
|
88
|
+
'minimax-cn': {
|
|
89
|
+
name: 'MiniMax China',
|
|
90
|
+
description: 'MiniMax Coding Plan (China)',
|
|
91
|
+
protocols: {
|
|
92
|
+
openai: {
|
|
93
|
+
baseUrl: 'https://api.minimaxi.com/v1',
|
|
94
|
+
authHeader: 'Bearer',
|
|
95
|
+
supportsNativeTools: true,
|
|
96
|
+
},
|
|
97
|
+
anthropic: {
|
|
98
|
+
baseUrl: 'https://api.minimaxi.com/anthropic',
|
|
99
|
+
authHeader: 'x-api-key',
|
|
100
|
+
supportsNativeTools: false,
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
models: [
|
|
104
|
+
{ id: 'MiniMax-M2.5', name: 'MiniMax M2.5', description: 'Latest MiniMax coding model' },
|
|
105
|
+
],
|
|
106
|
+
defaultModel: 'MiniMax-M2.5',
|
|
107
|
+
defaultProtocol: 'anthropic',
|
|
108
|
+
envKey: 'MINIMAX_CN_API_KEY',
|
|
109
|
+
subscribeUrl: 'https://platform.minimaxi.com',
|
|
110
|
+
},
|
|
88
111
|
'deepseek': {
|
|
89
112
|
name: 'DeepSeek',
|
|
90
113
|
description: 'DeepSeek AI models',
|
|
@@ -115,11 +138,12 @@ export const PROVIDERS = {
|
|
|
115
138
|
},
|
|
116
139
|
},
|
|
117
140
|
models: [
|
|
118
|
-
{ id: 'claude-sonnet-4-
|
|
119
|
-
{ id: 'claude-haiku-4-5-20251001', name: 'Claude Haiku 4.5', description: 'Fastest and most affordable' },
|
|
141
|
+
{ id: 'claude-sonnet-4-6', name: 'Claude Sonnet 4.6', description: 'Latest Sonnet — best balance of speed and intelligence' },
|
|
120
142
|
{ id: 'claude-opus-4-6', name: 'Claude Opus 4.6', description: 'Most capable model' },
|
|
143
|
+
{ id: 'claude-sonnet-4-5-20250929', name: 'Claude Sonnet 4.5', description: 'Previous generation Sonnet' },
|
|
144
|
+
{ id: 'claude-haiku-4-5-20251001', name: 'Claude Haiku 4.5', description: 'Fastest and most affordable' },
|
|
121
145
|
],
|
|
122
|
-
defaultModel: 'claude-sonnet-4-
|
|
146
|
+
defaultModel: 'claude-sonnet-4-6',
|
|
123
147
|
defaultProtocol: 'anthropic',
|
|
124
148
|
envKey: 'ANTHROPIC_API_KEY',
|
|
125
149
|
},
|
|
@@ -72,12 +72,14 @@ describe('providers', () => {
|
|
|
72
72
|
expect(item.description).toBeDefined();
|
|
73
73
|
}
|
|
74
74
|
});
|
|
75
|
-
it('should include z.ai, z.ai-cn, and
|
|
75
|
+
it('should include z.ai, z.ai-cn, minimax, minimax-cn, and anthropic', () => {
|
|
76
76
|
const list = getProviderList();
|
|
77
77
|
const ids = list.map(p => p.id);
|
|
78
78
|
expect(ids).toContain('z.ai');
|
|
79
79
|
expect(ids).toContain('z.ai-cn');
|
|
80
80
|
expect(ids).toContain('minimax');
|
|
81
|
+
expect(ids).toContain('minimax-cn');
|
|
82
|
+
expect(ids).toContain('anthropic');
|
|
81
83
|
});
|
|
82
84
|
});
|
|
83
85
|
describe('getProviderModels', () => {
|
|
@@ -136,6 +138,30 @@ describe('providers', () => {
|
|
|
136
138
|
it('should have env key for minimax', () => {
|
|
137
139
|
expect(PROVIDERS['minimax'].envKey).toBe('MINIMAX_API_KEY');
|
|
138
140
|
});
|
|
141
|
+
it('should have env key for minimax-cn', () => {
|
|
142
|
+
expect(PROVIDERS['minimax-cn'].envKey).toBe('MINIMAX_CN_API_KEY');
|
|
143
|
+
});
|
|
144
|
+
it('should have env key for anthropic', () => {
|
|
145
|
+
expect(PROVIDERS['anthropic'].envKey).toBe('ANTHROPIC_API_KEY');
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
describe('minimax-cn provider', () => {
|
|
149
|
+
it('should have correct name and endpoints', () => {
|
|
150
|
+
expect(PROVIDERS['minimax-cn']).toBeDefined();
|
|
151
|
+
expect(PROVIDERS['minimax-cn'].name).toBe('MiniMax China');
|
|
152
|
+
expect(getProviderBaseUrl('minimax-cn', 'openai')).toContain('api.minimaxi.com');
|
|
153
|
+
expect(getProviderBaseUrl('minimax-cn', 'anthropic')).toContain('api.minimaxi.com');
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
describe('anthropic provider', () => {
|
|
157
|
+
it('should include Claude Sonnet 4.6 as default model', () => {
|
|
158
|
+
expect(PROVIDERS['anthropic'].defaultModel).toBe('claude-sonnet-4-6');
|
|
159
|
+
const modelIds = PROVIDERS['anthropic'].models.map(m => m.id);
|
|
160
|
+
expect(modelIds).toContain('claude-sonnet-4-6');
|
|
161
|
+
expect(modelIds).toContain('claude-opus-4-6');
|
|
162
|
+
expect(modelIds).toContain('claude-sonnet-4-5-20250929');
|
|
163
|
+
expect(modelIds).toContain('claude-haiku-4-5-20251001');
|
|
164
|
+
});
|
|
139
165
|
});
|
|
140
166
|
describe('MCP endpoints', () => {
|
|
141
167
|
it('should have MCP endpoints for z.ai', () => {
|
package/dist/renderer/App.js
CHANGED
|
@@ -358,6 +358,7 @@ export class App {
|
|
|
358
358
|
this.screen.init();
|
|
359
359
|
this.input.start();
|
|
360
360
|
this.input.onKey((event) => this.handleKey(event));
|
|
361
|
+
this.screen.onResize(() => this.render());
|
|
361
362
|
this.render();
|
|
362
363
|
}
|
|
363
364
|
/**
|
|
@@ -1733,7 +1734,7 @@ export class App {
|
|
|
1733
1734
|
if (this.pasteInfoOpen && this.pasteInfo) {
|
|
1734
1735
|
this.renderInlinePasteInfo(statusLine + 1, width);
|
|
1735
1736
|
}
|
|
1736
|
-
this.screen.
|
|
1737
|
+
this.screen.render();
|
|
1737
1738
|
}
|
|
1738
1739
|
/**
|
|
1739
1740
|
* Render inline confirmation dialog below status bar
|
package/dist/renderer/Input.d.ts
CHANGED
|
@@ -58,7 +58,19 @@ export declare class LineEditor {
|
|
|
58
58
|
*/
|
|
59
59
|
setCursorPos(pos: number): void;
|
|
60
60
|
/**
|
|
61
|
-
*
|
|
61
|
+
* Check if character is a word boundary (space, path separator, punctuation)
|
|
62
|
+
*/
|
|
63
|
+
private isWordBoundary;
|
|
64
|
+
/**
|
|
65
|
+
* Move cursor to previous word boundary (Ctrl+Left)
|
|
66
|
+
*/
|
|
67
|
+
wordLeft(): void;
|
|
68
|
+
/**
|
|
69
|
+
* Move cursor to next word boundary (Ctrl+Right)
|
|
70
|
+
*/
|
|
71
|
+
wordRight(): void;
|
|
72
|
+
/**
|
|
73
|
+
* Delete word backward (Ctrl+W) — respects path separators
|
|
62
74
|
*/
|
|
63
75
|
deleteWordBackward(): void;
|
|
64
76
|
/**
|
package/dist/renderer/Input.js
CHANGED
|
@@ -19,7 +19,8 @@ export class Input {
|
|
|
19
19
|
// Enable mouse tracking (SGR mode for better compatibility)
|
|
20
20
|
// \x1b[?1000h - enable basic mouse tracking
|
|
21
21
|
// \x1b[?1006h - enable SGR extended mouse mode
|
|
22
|
-
|
|
22
|
+
// \x1b[?2004h - enable bracketed paste mode (wraps pastes in \x1b[200~ ... \x1b[201~)
|
|
23
|
+
process.stdout.write('\x1b[?1000h\x1b[?1006h\x1b[?2004h');
|
|
23
24
|
this.dataHandler = (data) => {
|
|
24
25
|
const event = this.parseKey(data);
|
|
25
26
|
this.emit(event);
|
|
@@ -35,8 +36,8 @@ export class Input {
|
|
|
35
36
|
process.stdin.removeListener('data', this.dataHandler);
|
|
36
37
|
this.dataHandler = null;
|
|
37
38
|
}
|
|
38
|
-
// Disable mouse tracking
|
|
39
|
-
process.stdout.write('\x1b[?1006l\x1b[?1000l');
|
|
39
|
+
// Disable mouse tracking and bracketed paste mode
|
|
40
|
+
process.stdout.write('\x1b[?2004l\x1b[?1006l\x1b[?1000l');
|
|
40
41
|
if (process.stdin.isTTY) {
|
|
41
42
|
process.stdin.setRawMode(false);
|
|
42
43
|
}
|
|
@@ -239,6 +240,16 @@ export class Input {
|
|
|
239
240
|
case '6~':
|
|
240
241
|
event.key = 'pagedown';
|
|
241
242
|
break;
|
|
243
|
+
// Ctrl+Right (word jump forward)
|
|
244
|
+
case '1;5C':
|
|
245
|
+
event.key = 'ctrl-right';
|
|
246
|
+
event.ctrl = true;
|
|
247
|
+
break;
|
|
248
|
+
// Ctrl+Left (word jump backward)
|
|
249
|
+
case '1;5D':
|
|
250
|
+
event.key = 'ctrl-left';
|
|
251
|
+
event.ctrl = true;
|
|
252
|
+
break;
|
|
242
253
|
default:
|
|
243
254
|
event.key = 'unknown';
|
|
244
255
|
}
|
|
@@ -300,20 +311,57 @@ export class LineEditor {
|
|
|
300
311
|
this.cursorPos = Math.max(0, Math.min(pos, this.value.length));
|
|
301
312
|
}
|
|
302
313
|
/**
|
|
303
|
-
*
|
|
314
|
+
* Check if character is a word boundary (space, path separator, punctuation)
|
|
315
|
+
*/
|
|
316
|
+
isWordBoundary(ch) {
|
|
317
|
+
return ' \t/\\.-_:'.includes(ch);
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* Move cursor to previous word boundary (Ctrl+Left)
|
|
321
|
+
*/
|
|
322
|
+
wordLeft() {
|
|
323
|
+
if (this.cursorPos === 0)
|
|
324
|
+
return;
|
|
325
|
+
let i = this.cursorPos - 1;
|
|
326
|
+
// Skip current boundary chars
|
|
327
|
+
while (i > 0 && this.isWordBoundary(this.value[i]))
|
|
328
|
+
i--;
|
|
329
|
+
// Move to start of word
|
|
330
|
+
while (i > 0 && !this.isWordBoundary(this.value[i - 1]))
|
|
331
|
+
i--;
|
|
332
|
+
this.cursorPos = i;
|
|
333
|
+
}
|
|
334
|
+
/**
|
|
335
|
+
* Move cursor to next word boundary (Ctrl+Right)
|
|
336
|
+
*/
|
|
337
|
+
wordRight() {
|
|
338
|
+
const len = this.value.length;
|
|
339
|
+
if (this.cursorPos >= len)
|
|
340
|
+
return;
|
|
341
|
+
let i = this.cursorPos;
|
|
342
|
+
// Skip current word chars
|
|
343
|
+
while (i < len && !this.isWordBoundary(this.value[i]))
|
|
344
|
+
i++;
|
|
345
|
+
// Skip boundary chars
|
|
346
|
+
while (i < len && this.isWordBoundary(this.value[i]))
|
|
347
|
+
i++;
|
|
348
|
+
this.cursorPos = i;
|
|
349
|
+
}
|
|
350
|
+
/**
|
|
351
|
+
* Delete word backward (Ctrl+W) — respects path separators
|
|
304
352
|
*/
|
|
305
353
|
deleteWordBackward() {
|
|
306
354
|
if (this.cursorPos === 0)
|
|
307
355
|
return;
|
|
308
|
-
const beforeCursor = this.value.slice(0, this.cursorPos);
|
|
309
356
|
const afterCursor = this.value.slice(this.cursorPos);
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
while (i >= 0 &&
|
|
357
|
+
let i = this.cursorPos - 1;
|
|
358
|
+
// Skip trailing spaces
|
|
359
|
+
while (i >= 0 && this.value[i] === ' ')
|
|
313
360
|
i--;
|
|
314
|
-
|
|
361
|
+
// Delete back to next word boundary
|
|
362
|
+
while (i >= 0 && !this.isWordBoundary(this.value[i]))
|
|
315
363
|
i--;
|
|
316
|
-
const newBefore =
|
|
364
|
+
const newBefore = this.value.slice(0, i + 1);
|
|
317
365
|
this.value = newBefore + afterCursor;
|
|
318
366
|
this.cursorPos = newBefore.length;
|
|
319
367
|
}
|
|
@@ -361,6 +409,12 @@ export class LineEditor {
|
|
|
361
409
|
this.cursorPos++;
|
|
362
410
|
}
|
|
363
411
|
break;
|
|
412
|
+
case 'ctrl-left':
|
|
413
|
+
this.wordLeft();
|
|
414
|
+
break;
|
|
415
|
+
case 'ctrl-right':
|
|
416
|
+
this.wordRight();
|
|
417
|
+
break;
|
|
364
418
|
case 'home':
|
|
365
419
|
this.cursorPos = 0;
|
|
366
420
|
break;
|
|
@@ -14,7 +14,12 @@ export declare class Screen {
|
|
|
14
14
|
private cursorX;
|
|
15
15
|
private cursorY;
|
|
16
16
|
private cursorVisible;
|
|
17
|
+
private resizeCallback;
|
|
17
18
|
constructor();
|
|
19
|
+
/**
|
|
20
|
+
* Register a callback to be called on terminal resize
|
|
21
|
+
*/
|
|
22
|
+
onResize(callback: () => void): void;
|
|
18
23
|
private createEmptyBuffer;
|
|
19
24
|
/**
|
|
20
25
|
* Get terminal dimensions
|
package/dist/renderer/Screen.js
CHANGED
|
@@ -11,6 +11,7 @@ export class Screen {
|
|
|
11
11
|
cursorX = 0;
|
|
12
12
|
cursorY = 0;
|
|
13
13
|
cursorVisible = true;
|
|
14
|
+
resizeCallback = null;
|
|
14
15
|
constructor() {
|
|
15
16
|
this.width = process.stdout.columns || 80;
|
|
16
17
|
this.height = process.stdout.rows || 24;
|
|
@@ -22,9 +23,18 @@ export class Screen {
|
|
|
22
23
|
this.height = process.stdout.rows || 24;
|
|
23
24
|
this.buffer = this.createEmptyBuffer();
|
|
24
25
|
this.rendered = this.createEmptyBuffer();
|
|
25
|
-
|
|
26
|
+
// Notify the app to re-render with new dimensions
|
|
27
|
+
if (this.resizeCallback) {
|
|
28
|
+
this.resizeCallback();
|
|
29
|
+
}
|
|
26
30
|
});
|
|
27
31
|
}
|
|
32
|
+
/**
|
|
33
|
+
* Register a callback to be called on terminal resize
|
|
34
|
+
*/
|
|
35
|
+
onResize(callback) {
|
|
36
|
+
this.resizeCallback = callback;
|
|
37
|
+
}
|
|
28
38
|
createEmptyBuffer() {
|
|
29
39
|
const buffer = [];
|
|
30
40
|
for (let y = 0; y < this.height; y++) {
|
package/dist/renderer/main.js
CHANGED
|
@@ -16,6 +16,7 @@ import { isProjectDirectory, getProjectContext } from '../utils/project.js';
|
|
|
16
16
|
import { getCurrentVersion } from '../utils/update.js';
|
|
17
17
|
import { getProviderList, getProvider } from '../config/providers.js';
|
|
18
18
|
import { getSessionStats } from '../utils/tokenTracker.js';
|
|
19
|
+
import { checkApiRateLimit } from '../utils/ratelimit.js';
|
|
19
20
|
// State
|
|
20
21
|
let projectPath = process.cwd();
|
|
21
22
|
let projectContext = null;
|
|
@@ -127,6 +128,12 @@ async function handleSubmit(message) {
|
|
|
127
128
|
runAgentTask(message, false);
|
|
128
129
|
return;
|
|
129
130
|
}
|
|
131
|
+
// Check API rate limit
|
|
132
|
+
const rateCheck = checkApiRateLimit();
|
|
133
|
+
if (!rateCheck.allowed) {
|
|
134
|
+
app.notify(rateCheck.message || 'Rate limit exceeded', 5000);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
130
137
|
try {
|
|
131
138
|
app.startStreaming();
|
|
132
139
|
// Get conversation history for context
|
|
@@ -144,6 +151,9 @@ async function handleSubmit(message) {
|
|
|
144
151
|
catch (error) {
|
|
145
152
|
app.endStreaming();
|
|
146
153
|
const err = error;
|
|
154
|
+
// Don't show error for user-cancelled requests
|
|
155
|
+
if (err.name === 'AbortError')
|
|
156
|
+
return;
|
|
147
157
|
app.notify(`Error: ${err.message}`, 5000);
|
|
148
158
|
}
|
|
149
159
|
}
|
|
@@ -966,12 +976,23 @@ function handleCommand(command, args) {
|
|
|
966
976
|
for (const p of configuredProviders) {
|
|
967
977
|
clearApiKey(p.id);
|
|
968
978
|
}
|
|
969
|
-
app.notify('Logged out from all providers');
|
|
979
|
+
app.notify('Logged out from all providers. Use /login to sign in.');
|
|
970
980
|
}
|
|
971
981
|
else {
|
|
972
982
|
clearApiKey(result);
|
|
973
983
|
const provider = configuredProviders.find(p => p.id === result);
|
|
974
984
|
app.notify(`Logged out from ${provider?.name || result}`);
|
|
985
|
+
// If we logged out from the active provider, switch to another configured one
|
|
986
|
+
if (result === currentProvider.id) {
|
|
987
|
+
const remaining = configuredProviders.filter(p => p.id !== result);
|
|
988
|
+
if (remaining.length > 0) {
|
|
989
|
+
setProvider(remaining[0].id);
|
|
990
|
+
app.notify(`Switched to ${remaining[0].name}`);
|
|
991
|
+
}
|
|
992
|
+
else {
|
|
993
|
+
app.notify('No providers configured. Use /login to sign in.');
|
|
994
|
+
}
|
|
995
|
+
}
|
|
975
996
|
}
|
|
976
997
|
});
|
|
977
998
|
break;
|
|
@@ -1057,12 +1078,24 @@ function handleCommand(command, args) {
|
|
|
1057
1078
|
app.notify('No assistant response to apply');
|
|
1058
1079
|
return;
|
|
1059
1080
|
}
|
|
1060
|
-
// Find file changes in the response
|
|
1061
|
-
const filePattern = /```(\w+)?\s*\n\/\/\s*(?:File:|Path:)\s*([^\n]+)\n([\s\S]*?)```/g;
|
|
1081
|
+
// Find file changes in the response using multiple patterns
|
|
1062
1082
|
const changes = [];
|
|
1083
|
+
// Pattern 1: ```lang filepath\n...\n``` (most common AI format)
|
|
1084
|
+
const fenceFilePattern = /```\w*\s+([\w./\\-]+(?:\.\w+))\n([\s\S]*?)```/g;
|
|
1063
1085
|
let match;
|
|
1064
|
-
while ((match =
|
|
1065
|
-
|
|
1086
|
+
while ((match = fenceFilePattern.exec(lastAssistant.content)) !== null) {
|
|
1087
|
+
const path = match[1].trim();
|
|
1088
|
+
// Must look like a file path (has extension, no spaces)
|
|
1089
|
+
if (path.includes('.') && !path.includes(' ')) {
|
|
1090
|
+
changes.push({ path, content: match[2] });
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
// Pattern 2: // File: or // Path: comment on first line of code block
|
|
1094
|
+
if (changes.length === 0) {
|
|
1095
|
+
const commentPattern = /```(\w+)?\s*\n(?:\/\/|#|--|\/\*)\s*(?:File|Path|file|path):\s*([^\n*]+)\n([\s\S]*?)```/g;
|
|
1096
|
+
while ((match = commentPattern.exec(lastAssistant.content)) !== null) {
|
|
1097
|
+
changes.push({ path: match[2].trim(), content: match[3] });
|
|
1098
|
+
}
|
|
1066
1099
|
}
|
|
1067
1100
|
if (changes.length === 0) {
|
|
1068
1101
|
app.notify('No file changes found in response');
|
package/dist/utils/agent.js
CHANGED
|
@@ -80,7 +80,7 @@ export function loadProjectRules(projectRoot) {
|
|
|
80
80
|
* Keeps the most recent messages within a character budget so the agent
|
|
81
81
|
* has conversational context without overwhelming the context window.
|
|
82
82
|
*/
|
|
83
|
-
export function formatChatHistoryForAgent(history, maxChars =
|
|
83
|
+
export function formatChatHistoryForAgent(history, maxChars = 16000) {
|
|
84
84
|
if (!history || history.length === 0)
|
|
85
85
|
return '';
|
|
86
86
|
// Filter out agent execution messages
|
package/dist/utils/tools.d.ts
CHANGED
|
@@ -262,6 +262,17 @@ export declare const AGENT_TOOLS: {
|
|
|
262
262
|
};
|
|
263
263
|
};
|
|
264
264
|
};
|
|
265
|
+
minimax_web_search: {
|
|
266
|
+
name: string;
|
|
267
|
+
description: string;
|
|
268
|
+
parameters: {
|
|
269
|
+
query: {
|
|
270
|
+
type: string;
|
|
271
|
+
description: string;
|
|
272
|
+
required: boolean;
|
|
273
|
+
};
|
|
274
|
+
};
|
|
275
|
+
};
|
|
265
276
|
};
|
|
266
277
|
export declare function formatToolDefinitions(): string;
|
|
267
278
|
/**
|
package/dist/utils/tools.js
CHANGED
|
@@ -13,11 +13,15 @@ import { executeCommand } from './shell.js';
|
|
|
13
13
|
import { recordWrite, recordEdit, recordDelete, recordMkdir, recordCommand } from './history.js';
|
|
14
14
|
import { loadIgnoreRules, isIgnored } from './gitignore.js';
|
|
15
15
|
import { config, getApiKey } from '../config/index.js';
|
|
16
|
-
import { getProviderMcpEndpoints } from '../config/providers.js';
|
|
16
|
+
import { getProviderMcpEndpoints, PROVIDERS } from '../config/providers.js';
|
|
17
17
|
// Z.AI MCP tool names (available when user has any Z.AI API key)
|
|
18
18
|
const ZAI_MCP_TOOLS = ['web_search', 'web_read', 'github_read'];
|
|
19
19
|
// Z.AI provider IDs that have MCP endpoints
|
|
20
20
|
const ZAI_PROVIDER_IDS = ['z.ai', 'z.ai-cn'];
|
|
21
|
+
// MiniMax MCP tool names (available when user has any MiniMax API key)
|
|
22
|
+
const MINIMAX_MCP_TOOLS = ['minimax_web_search'];
|
|
23
|
+
// MiniMax provider IDs
|
|
24
|
+
const MINIMAX_PROVIDER_IDS = ['minimax', 'minimax-cn'];
|
|
21
25
|
/**
|
|
22
26
|
* Find a Z.AI provider that has an API key configured.
|
|
23
27
|
* Returns the provider ID and API key, or null if none found.
|
|
@@ -49,6 +53,77 @@ function getZaiMcpConfig() {
|
|
|
49
53
|
function hasZaiMcpAccess() {
|
|
50
54
|
return getZaiMcpConfig() !== null;
|
|
51
55
|
}
|
|
56
|
+
/**
|
|
57
|
+
* Find a MiniMax provider that has an API key configured.
|
|
58
|
+
* Returns the base host URL and API key, or null if none found.
|
|
59
|
+
*/
|
|
60
|
+
function getMinimaxMcpConfig() {
|
|
61
|
+
// First check if active provider is MiniMax
|
|
62
|
+
const activeProvider = config.get('provider');
|
|
63
|
+
if (MINIMAX_PROVIDER_IDS.includes(activeProvider)) {
|
|
64
|
+
const key = getApiKey(activeProvider);
|
|
65
|
+
if (key) {
|
|
66
|
+
const provider = PROVIDERS[activeProvider];
|
|
67
|
+
const baseUrl = provider?.protocols?.openai?.baseUrl;
|
|
68
|
+
if (baseUrl) {
|
|
69
|
+
// Extract host from baseUrl (e.g. https://api.minimax.io/v1 -> https://api.minimax.io)
|
|
70
|
+
const host = baseUrl.replace(/\/v1\/?$/, '');
|
|
71
|
+
return { host, apiKey: key };
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
// Otherwise check all MiniMax providers
|
|
76
|
+
for (const pid of MINIMAX_PROVIDER_IDS) {
|
|
77
|
+
const key = getApiKey(pid);
|
|
78
|
+
if (key) {
|
|
79
|
+
const provider = PROVIDERS[pid];
|
|
80
|
+
const baseUrl = provider?.protocols?.openai?.baseUrl;
|
|
81
|
+
if (baseUrl) {
|
|
82
|
+
const host = baseUrl.replace(/\/v1\/?$/, '');
|
|
83
|
+
return { host, apiKey: key };
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Check if MiniMax MCP tools are available
|
|
91
|
+
*/
|
|
92
|
+
function hasMinimaxMcpAccess() {
|
|
93
|
+
return getMinimaxMcpConfig() !== null;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Call a MiniMax MCP REST API endpoint
|
|
97
|
+
*/
|
|
98
|
+
async function callMinimaxApi(host, path, body, apiKey) {
|
|
99
|
+
const controller = new AbortController();
|
|
100
|
+
const timeout = setTimeout(() => controller.abort(), 60000);
|
|
101
|
+
try {
|
|
102
|
+
const response = await fetch(`${host}${path}`, {
|
|
103
|
+
method: 'POST',
|
|
104
|
+
headers: {
|
|
105
|
+
'Content-Type': 'application/json',
|
|
106
|
+
'Authorization': `Bearer ${apiKey}`,
|
|
107
|
+
},
|
|
108
|
+
body: JSON.stringify(body),
|
|
109
|
+
signal: controller.signal,
|
|
110
|
+
});
|
|
111
|
+
if (!response.ok) {
|
|
112
|
+
const errorText = await response.text().catch(() => '');
|
|
113
|
+
throw new Error(`MiniMax API error ${response.status}: ${errorText || response.statusText}`);
|
|
114
|
+
}
|
|
115
|
+
const data = await response.json();
|
|
116
|
+
// MiniMax returns content array like MCP
|
|
117
|
+
if (data.content && Array.isArray(data.content)) {
|
|
118
|
+
return data.content.map((c) => c.text || '').join('\n');
|
|
119
|
+
}
|
|
120
|
+
// Fallback: return raw result
|
|
121
|
+
return typeof data === 'string' ? data : JSON.stringify(data);
|
|
122
|
+
}
|
|
123
|
+
finally {
|
|
124
|
+
clearTimeout(timeout);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
52
127
|
/**
|
|
53
128
|
* Call a Z.AI MCP endpoint via JSON-RPC 2.0
|
|
54
129
|
*/
|
|
@@ -196,18 +271,28 @@ export const AGENT_TOOLS = {
|
|
|
196
271
|
path: { type: 'string', description: 'File path (for action=read_file) or directory path (for action=tree)', required: false },
|
|
197
272
|
},
|
|
198
273
|
},
|
|
274
|
+
minimax_web_search: {
|
|
275
|
+
name: 'minimax_web_search',
|
|
276
|
+
description: 'Search the web using MiniMax search engine. Returns relevant results with summaries. Requires a MiniMax API key.',
|
|
277
|
+
parameters: {
|
|
278
|
+
query: { type: 'string', description: 'Search query', required: true },
|
|
279
|
+
},
|
|
280
|
+
},
|
|
199
281
|
};
|
|
200
282
|
/**
|
|
201
283
|
* Format tool definitions for system prompt (text-based fallback)
|
|
202
284
|
*/
|
|
203
285
|
/**
|
|
204
|
-
* Get filtered tool entries (excludes
|
|
286
|
+
* Get filtered tool entries (excludes provider-specific tools when API key not available)
|
|
205
287
|
*/
|
|
206
288
|
function getFilteredToolEntries() {
|
|
207
289
|
const hasMcp = hasZaiMcpAccess();
|
|
290
|
+
const hasMinimaxMcp = hasMinimaxMcpAccess();
|
|
208
291
|
return Object.entries(AGENT_TOOLS).filter(([name]) => {
|
|
209
292
|
if (ZAI_MCP_TOOLS.includes(name))
|
|
210
293
|
return hasMcp;
|
|
294
|
+
if (MINIMAX_MCP_TOOLS.includes(name))
|
|
295
|
+
return hasMinimaxMcp;
|
|
211
296
|
return true;
|
|
212
297
|
});
|
|
213
298
|
}
|
|
@@ -1099,6 +1184,20 @@ export async function executeTool(toolCall, projectRoot) {
|
|
|
1099
1184
|
const output = result.length > 15000 ? result.substring(0, 15000) + '\n\n... (truncated)' : result;
|
|
1100
1185
|
return { success: true, output, tool, parameters };
|
|
1101
1186
|
}
|
|
1187
|
+
// === MiniMax MCP Tools ===
|
|
1188
|
+
case 'minimax_web_search': {
|
|
1189
|
+
const mmConfig = getMinimaxMcpConfig();
|
|
1190
|
+
if (!mmConfig) {
|
|
1191
|
+
return { success: false, output: '', error: 'minimax_web_search requires a MiniMax API key. Configure one via /provider minimax', tool, parameters };
|
|
1192
|
+
}
|
|
1193
|
+
const query = parameters.query;
|
|
1194
|
+
if (!query) {
|
|
1195
|
+
return { success: false, output: '', error: 'Missing required parameter: query', tool, parameters };
|
|
1196
|
+
}
|
|
1197
|
+
const result = await callMinimaxApi(mmConfig.host, '/v1/coding_plan/search', { q: query }, mmConfig.apiKey);
|
|
1198
|
+
const output = result.length > 15000 ? result.substring(0, 15000) + '\n\n... (truncated)' : result;
|
|
1199
|
+
return { success: true, output, tool, parameters };
|
|
1200
|
+
}
|
|
1102
1201
|
default:
|
|
1103
1202
|
return { success: false, output: '', error: `Unknown tool: ${tool}`, tool, parameters };
|
|
1104
1203
|
}
|
|
@@ -1217,6 +1316,7 @@ export function createActionLog(toolCall, result) {
|
|
|
1217
1316
|
web_search: 'fetch',
|
|
1218
1317
|
web_read: 'fetch',
|
|
1219
1318
|
github_read: 'fetch',
|
|
1319
|
+
minimax_web_search: 'fetch',
|
|
1220
1320
|
};
|
|
1221
1321
|
const target = toolCall.parameters.path ||
|
|
1222
1322
|
toolCall.parameters.command ||
|
package/dist/utils/tools.test.js
CHANGED
|
@@ -2,9 +2,11 @@ import { describe, it, expect } from 'vitest';
|
|
|
2
2
|
import { getOpenAITools, getAnthropicTools, parseToolCalls, parseOpenAIToolCalls, parseAnthropicToolCalls, createActionLog, AGENT_TOOLS, } from './tools.js';
|
|
3
3
|
// ─── CONSTANTS ───────────────────────────────────────────────────────────────
|
|
4
4
|
const ALL_TOOL_NAMES = Object.keys(AGENT_TOOLS);
|
|
5
|
-
// MCP tools are filtered out when no
|
|
5
|
+
// MCP tools are filtered out when no API key is configured (e.g. in tests)
|
|
6
6
|
const ZAI_MCP_TOOLS = ['web_search', 'web_read', 'github_read'];
|
|
7
|
-
const
|
|
7
|
+
const MINIMAX_MCP_TOOLS = ['minimax_web_search'];
|
|
8
|
+
const MCP_TOOLS = [...ZAI_MCP_TOOLS, ...MINIMAX_MCP_TOOLS];
|
|
9
|
+
const CORE_TOOL_NAMES = ALL_TOOL_NAMES.filter(n => !MCP_TOOLS.includes(n));
|
|
8
10
|
// ─── getOpenAITools ──────────────────────────────────────────────────────────
|
|
9
11
|
describe('getOpenAITools', () => {
|
|
10
12
|
it('should return one entry per AGENT_TOOLS definition', () => {
|
|
@@ -578,6 +580,11 @@ describe('createActionLog', () => {
|
|
|
578
580
|
const log = createActionLog(tc, makeResult({ tool: 'fetch_url' }));
|
|
579
581
|
expect(log.type).toBe('fetch');
|
|
580
582
|
});
|
|
583
|
+
it('should map minimax_web_search to type "fetch"', () => {
|
|
584
|
+
const tc = { tool: 'minimax_web_search', parameters: { query: 'test' } };
|
|
585
|
+
const log = createActionLog(tc, makeResult({ tool: 'minimax_web_search' }));
|
|
586
|
+
expect(log.type).toBe('fetch');
|
|
587
|
+
});
|
|
581
588
|
it('should default to type "command" for unknown tools', () => {
|
|
582
589
|
const tc = { tool: 'unknown_tool', parameters: {} };
|
|
583
590
|
const log = createActionLog(tc, makeResult({ tool: 'unknown_tool' }));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codeep",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.16",
|
|
4
4
|
"description": "AI-powered coding assistant built for the terminal. Multiple LLM providers, project-aware context, and a seamless development workflow.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|