clarity-ai 3.2.0 → 3.3.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/bin/clarity.js +0 -1
- package/package.json +1 -1
- package/src/agents/loop.js +108 -101
- package/src/commands/model.js +85 -11
- package/src/config/settings.js +1 -1
- package/src/main.js +56 -136
- package/src/providers/claude.js +19 -45
- package/src/providers/deepseek.js +38 -44
- package/src/providers/gemini.js +23 -36
- package/src/providers/groq.js +90 -47
- package/src/providers/index.js +39 -22
- package/src/providers/openai.js +38 -43
- package/src/providers/openrouter.js +40 -43
- package/src/ui/banner.js +1 -1
package/bin/clarity.js
CHANGED
package/package.json
CHANGED
package/src/agents/loop.js
CHANGED
|
@@ -2,16 +2,16 @@ import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
|
|
|
2
2
|
import { execSync } from 'child_process';
|
|
3
3
|
import { dirname } from 'path';
|
|
4
4
|
import chalk from 'chalk';
|
|
5
|
-
import { Spinner
|
|
6
|
-
import { renderAI, renderWrite, renderEdit
|
|
7
|
-
import { callProvider
|
|
5
|
+
import { Spinner } from '../ui/spinner.js';
|
|
6
|
+
import { renderAI, renderWrite, renderEdit } from '../ui/blocks.js';
|
|
7
|
+
import { callProvider } from '../providers/index.js';
|
|
8
8
|
import { addMessage, saveHistory } from '../core/history.js';
|
|
9
9
|
import { diffLines } from 'diff';
|
|
10
10
|
|
|
11
11
|
const SYSTEM_PROMPT = 'You are CLARITY, an autonomous AI agent CLI running in Termux on Android.\n\n' +
|
|
12
12
|
'## CRITICAL RULES\n' +
|
|
13
13
|
'1. NEVER fabricate file contents or command outputs.\n' +
|
|
14
|
-
'2. NEVER confirm before acting
|
|
14
|
+
'2. NEVER confirm before acting \u2014 just do it.\n' +
|
|
15
15
|
'3. ALWAYS ground responses in actual tool output.\n' +
|
|
16
16
|
'4. SHORT responses. No filler.\n' +
|
|
17
17
|
'5. When editing files, read first then edit.\n\n' +
|
|
@@ -34,16 +34,16 @@ function detectIntent(message) {
|
|
|
34
34
|
if (p.test(lower)) return 'chat';
|
|
35
35
|
}
|
|
36
36
|
const toolPatterns = [
|
|
37
|
-
{ re: /(create|make|mkdir|new)\s+(a\s+)?(dir|directory|folder)
|
|
38
|
-
{ re: /(create|make|write|generate)\s+(a\s+)?(file|script|code|program)
|
|
39
|
-
{ re: /(run|execute|launch|start)\s
|
|
40
|
-
{ re: /(edit|modify|change|update|fix)\s+(the\s+)?file
|
|
41
|
-
{ re: /(read|show|display|cat|open)\s+(the\s+)?file
|
|
42
|
-
{ re: /(list|ls|show)\s+(files|dir|directory|folders)
|
|
43
|
-
{ re: /(search|find|grep|look for)
|
|
44
|
-
{ re: /(install|npm|pip|pkg)\s
|
|
45
|
-
{ re: /(git\s+)
|
|
46
|
-
{ re: /(cd\s+|navigate to|go to)
|
|
37
|
+
{ re: /(create|make|mkdir|new)\s+(a\s+)?(dir|directory|folder)/ },
|
|
38
|
+
{ re: /(create|make|write|generate)\s+(a\s+)?(file|script|code|program)/ },
|
|
39
|
+
{ re: /(run|execute|launch|start)\s+/ },
|
|
40
|
+
{ re: /(edit|modify|change|update|fix)\s+(the\s+)?file/ },
|
|
41
|
+
{ re: /(read|show|display|cat|open)\s+(the\s+)?file/ },
|
|
42
|
+
{ re: /(list|ls|show)\s+(files|dir|directory|folders)/ },
|
|
43
|
+
{ re: /(search|find|grep|look for)/ },
|
|
44
|
+
{ re: /(install|npm|pip|pkg)\s+/ },
|
|
45
|
+
{ re: /(git\s+)/ },
|
|
46
|
+
{ re: /(cd\s+|navigate to|go to)/ },
|
|
47
47
|
];
|
|
48
48
|
for (const { re } of toolPatterns) {
|
|
49
49
|
if (re.test(lower)) return 'agent';
|
|
@@ -185,112 +185,119 @@ function renderToolStep(name, args, result, elapsed) {
|
|
|
185
185
|
}
|
|
186
186
|
|
|
187
187
|
export async function agentLoop(userMessage, config, history) {
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
188
|
+
try {
|
|
189
|
+
const intent = detectIntent(userMessage);
|
|
190
|
+
const spinner = new Spinner();
|
|
191
|
+
const msgHistory = Array.isArray(history) ? history : (history.messages || []);
|
|
191
192
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
193
|
+
if (intent === 'chat') {
|
|
194
|
+
spinner.start('Thinking', 'think');
|
|
195
|
+
const t0 = Date.now();
|
|
195
196
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
197
|
+
const messages = [
|
|
198
|
+
{ role: 'system', content: 'You are CLARITY-AI, a helpful assistant. Keep responses short.' },
|
|
199
|
+
...msgHistory.slice(-10).map(m => ({ role: m.role === 'assistant' ? 'assistant' : 'user', content: m.content })),
|
|
200
|
+
{ role: 'user', content: userMessage },
|
|
201
|
+
];
|
|
201
202
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
203
|
+
const result = await callProvider(config, messages);
|
|
204
|
+
const elapsed = Date.now() - t0;
|
|
205
|
+
spinner.stop('Thought ' + (elapsed < 1000 ? elapsed + 'ms' : (elapsed/1000).toFixed(1) + 's'), 'done');
|
|
205
206
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
207
|
+
addMessage(msgHistory, 'user', userMessage);
|
|
208
|
+
addMessage(msgHistory, 'assistant', result.content);
|
|
209
|
+
saveHistory(msgHistory);
|
|
209
210
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
211
|
+
console.log();
|
|
212
|
+
console.log(renderAI(result.content));
|
|
213
|
+
console.log();
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
215
216
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
217
|
+
let messages = [
|
|
218
|
+
{ role: 'system', content: SYSTEM_PROMPT },
|
|
219
|
+
...msgHistory.slice(-20).map(m => ({ role: m.role === 'assistant' ? 'assistant' : 'user', content: m.content })),
|
|
220
|
+
{ role: 'user', content: userMessage },
|
|
221
|
+
];
|
|
221
222
|
|
|
222
|
-
|
|
223
|
-
|
|
223
|
+
let loopCount = 0;
|
|
224
|
+
const MAX_LOOPS = 15;
|
|
224
225
|
|
|
225
|
-
|
|
226
|
-
|
|
226
|
+
while (loopCount < MAX_LOOPS) {
|
|
227
|
+
loopCount++;
|
|
227
228
|
|
|
228
|
-
|
|
229
|
-
|
|
229
|
+
spinner.start('Thinking', 'think');
|
|
230
|
+
const t0 = Date.now();
|
|
230
231
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
for await (const chunk of stream) {
|
|
234
|
-
fullResponse += chunk;
|
|
235
|
-
}
|
|
232
|
+
const result = await callProvider(config, messages);
|
|
233
|
+
const fullResponse = result.content;
|
|
236
234
|
|
|
237
|
-
|
|
238
|
-
|
|
235
|
+
const thinkMs = Date.now() - t0;
|
|
236
|
+
spinner.stop('Thought ' + (thinkMs < 1000 ? thinkMs + 'ms' : (thinkMs/1000).toFixed(1) + 's'), 'done');
|
|
239
237
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
238
|
+
const jsonMatch = fullResponse.match(/\{[^]*\}/);
|
|
239
|
+
if (!jsonMatch) {
|
|
240
|
+
console.log();
|
|
241
|
+
console.log(renderAI(fullResponse));
|
|
242
|
+
console.log();
|
|
243
|
+
addMessage(msgHistory, 'user', userMessage);
|
|
244
|
+
addMessage(msgHistory, 'assistant', fullResponse);
|
|
245
|
+
saveHistory(msgHistory);
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
250
248
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
249
|
+
let parsed;
|
|
250
|
+
try {
|
|
251
|
+
parsed = JSON.parse(jsonMatch[0]);
|
|
252
|
+
} catch {
|
|
253
|
+
console.log();
|
|
254
|
+
console.log(renderAI(fullResponse));
|
|
255
|
+
console.log();
|
|
256
|
+
addMessage(msgHistory, 'user', userMessage);
|
|
257
|
+
addMessage(msgHistory, 'assistant', fullResponse);
|
|
258
|
+
saveHistory(msgHistory);
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
263
261
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
262
|
+
if (parsed.response) {
|
|
263
|
+
const finalText = parsed.response;
|
|
264
|
+
console.log();
|
|
265
|
+
console.log(renderAI(finalText));
|
|
266
|
+
console.log();
|
|
267
|
+
addMessage(msgHistory, 'user', userMessage);
|
|
268
|
+
addMessage(msgHistory, 'assistant', finalText);
|
|
269
|
+
saveHistory(msgHistory);
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
274
272
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
273
|
+
if (parsed.tool) {
|
|
274
|
+
const name = parsed.tool;
|
|
275
|
+
const args = parsed.args || {};
|
|
276
|
+
const argPreview = args.command || args.path || args.pattern || '';
|
|
279
277
|
|
|
280
|
-
|
|
281
|
-
|
|
278
|
+
spinner.start('Running ' + name, 'tool');
|
|
279
|
+
spinner.update('Running ' + name, argPreview.slice(0, 40));
|
|
282
280
|
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
281
|
+
const ts = Date.now();
|
|
282
|
+
const toolResult = await executeTool(name, args);
|
|
283
|
+
const te = Date.now() - ts;
|
|
286
284
|
|
|
287
|
-
|
|
288
|
-
|
|
285
|
+
spinner.stop(name + ' \u00b7 ' + argPreview.slice(0, 30), 'done');
|
|
286
|
+
renderToolStep(name, args, toolResult, te);
|
|
289
287
|
|
|
290
|
-
|
|
291
|
-
|
|
288
|
+
messages.push({ role: 'assistant', content: fullResponse });
|
|
289
|
+
messages.push({ role: 'user', content: 'Tool result: ' + toolResult });
|
|
290
|
+
}
|
|
292
291
|
}
|
|
293
|
-
}
|
|
294
292
|
|
|
295
|
-
|
|
293
|
+
console.log(chalk.hex('#FFB800')('\u26a0 Max tool calls reached. Stopping.'));
|
|
294
|
+
} catch (err) {
|
|
295
|
+
console.log();
|
|
296
|
+
console.log(chalk.hex('#FF4757')('\u2716 Error: ') + chalk.white(err.message.split('\n')[0]));
|
|
297
|
+
const extra = err.message.split('\n').slice(1);
|
|
298
|
+
for (const line of extra) {
|
|
299
|
+
console.log(chalk.dim(' ' + line));
|
|
300
|
+
}
|
|
301
|
+
console.log();
|
|
302
|
+
}
|
|
296
303
|
}
|
package/src/commands/model.js
CHANGED
|
@@ -1,26 +1,100 @@
|
|
|
1
|
-
import { saveConfig } from '../config/settings.js';
|
|
2
|
-
import { PROVIDERS, getProvider } from '../providers/index.js';
|
|
3
1
|
import chalk from 'chalk';
|
|
2
|
+
import { saveConfig } from '../config/settings.js';
|
|
3
|
+
import { getProvider } from '../providers/index.js';
|
|
4
|
+
|
|
5
|
+
const PROVIDER_MODELS = {
|
|
6
|
+
groq: [
|
|
7
|
+
'llama-3.3-70b-versatile',
|
|
8
|
+
'llama-3.1-8b-instant',
|
|
9
|
+
'llama-4-scout-17b-16e-instruct',
|
|
10
|
+
'deepseek-r1-distill-llama-70b',
|
|
11
|
+
'gemma2-9b-it',
|
|
12
|
+
'mixtral-8x7b-32768',
|
|
13
|
+
'kimi-k2-instruct',
|
|
14
|
+
'compound-beta',
|
|
15
|
+
],
|
|
16
|
+
gemini: [
|
|
17
|
+
'gemini-1.5-flash',
|
|
18
|
+
'gemini-1.5-pro',
|
|
19
|
+
'gemini-2.0-flash',
|
|
20
|
+
'gemini-2.5-flash',
|
|
21
|
+
],
|
|
22
|
+
openrouter: [
|
|
23
|
+
'mistralai/mistral-7b-instruct:free',
|
|
24
|
+
'meta-llama/llama-3.3-70b-instruct:free',
|
|
25
|
+
'deepseek/deepseek-chat',
|
|
26
|
+
'anthropic/claude-3.5-sonnet',
|
|
27
|
+
'openai/gpt-4o',
|
|
28
|
+
],
|
|
29
|
+
openai: [
|
|
30
|
+
'gpt-4o',
|
|
31
|
+
'gpt-4o-mini',
|
|
32
|
+
'gpt-4-turbo',
|
|
33
|
+
'o1',
|
|
34
|
+
'o1-mini',
|
|
35
|
+
],
|
|
36
|
+
anthropic: [
|
|
37
|
+
'claude-opus-4-5',
|
|
38
|
+
'claude-sonnet-4-5',
|
|
39
|
+
'claude-3-5-haiku-20241022',
|
|
40
|
+
],
|
|
41
|
+
deepseek: [
|
|
42
|
+
'deepseek-chat',
|
|
43
|
+
'deepseek-reasoner',
|
|
44
|
+
],
|
|
45
|
+
};
|
|
4
46
|
|
|
5
47
|
export async function modelCommand(args, config) {
|
|
6
|
-
|
|
7
|
-
|
|
48
|
+
const provider = config.provider || 'groq';
|
|
49
|
+
const knownModels = PROVIDER_MODELS[provider] || [];
|
|
50
|
+
|
|
51
|
+
if (!args || args.length === 0) {
|
|
52
|
+
console.log(chalk.hex('#00FFFF')('Current model: ') + chalk.white(config.model));
|
|
53
|
+
console.log(chalk.dim('\nAvailable models for ' + provider + ':'));
|
|
54
|
+
for (const m of knownModels) {
|
|
55
|
+
const isCurrent = m === config.model;
|
|
56
|
+
console.log(
|
|
57
|
+
(isCurrent ? chalk.hex('#00FF9F')(' \u276f ') : chalk.dim(' ')) +
|
|
58
|
+
chalk.white(m) +
|
|
59
|
+
(isCurrent ? chalk.hex('#00FF9F')(' \u2190 current') : '')
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
console.log(chalk.dim('\nUsage: /model <model-name>'));
|
|
8
63
|
return;
|
|
9
64
|
}
|
|
10
65
|
|
|
11
|
-
const
|
|
66
|
+
const requested = args.join(' ').trim();
|
|
12
67
|
|
|
13
|
-
const prov = getProvider(
|
|
68
|
+
const prov = getProvider(requested);
|
|
14
69
|
if (prov) {
|
|
15
70
|
config.provider = prov.value;
|
|
16
71
|
config.model = prov.freeModel;
|
|
17
72
|
saveConfig(config);
|
|
18
|
-
console.log(chalk.hex('#00FF9F')('Switched to provider: ') + chalk.white(prov.name) + chalk.dim(' (' + prov.freeModel + ')'));
|
|
19
|
-
return
|
|
73
|
+
console.log(chalk.hex('#00FF9F')('\u2714 Switched to provider: ') + chalk.white(prov.name) + chalk.dim(' (' + prov.freeModel + ')'));
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const isKnown = knownModels.some(m => m.toLowerCase() === requested.toLowerCase());
|
|
78
|
+
|
|
79
|
+
if (!isKnown && knownModels.length > 0) {
|
|
80
|
+
const suggestion = knownModels.find(m =>
|
|
81
|
+
m.toLowerCase().includes(requested.toLowerCase())
|
|
82
|
+
) || knownModels.find(m =>
|
|
83
|
+
requested.toLowerCase().includes(m.split('-')[0].toLowerCase())
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
if (suggestion) {
|
|
87
|
+
console.log(chalk.hex('#FFB800')('\u26a0 Unknown model: ') + chalk.white(requested));
|
|
88
|
+
console.log(chalk.dim(' Did you mean: ') + chalk.hex('#00FFFF')(suggestion));
|
|
89
|
+
console.log(chalk.dim(' Run: /model ' + suggestion));
|
|
90
|
+
} else {
|
|
91
|
+
console.log(chalk.hex('#FFB800')('\u26a0 Unknown model for ' + provider + ': ') + chalk.white(requested));
|
|
92
|
+
console.log(chalk.dim(' Run /model with no args to see valid models'));
|
|
93
|
+
}
|
|
94
|
+
console.log(chalk.dim(' Saving anyway. If it fails, the provider will tell you.'));
|
|
20
95
|
}
|
|
21
96
|
|
|
22
|
-
config.model =
|
|
97
|
+
config.model = requested;
|
|
23
98
|
saveConfig(config);
|
|
24
|
-
console.log(chalk.hex('#00FF9F')('Model set to: ') + chalk.white(
|
|
25
|
-
return config;
|
|
99
|
+
console.log(chalk.hex('#00FF9F')('\u2714 Model set to: ') + chalk.white(requested));
|
|
26
100
|
}
|
package/src/config/settings.js
CHANGED
package/src/main.js
CHANGED
|
@@ -1,168 +1,88 @@
|
|
|
1
|
+
import readline from 'readline';
|
|
1
2
|
import chalk from 'chalk';
|
|
2
|
-
import { renderUser, renderAI, divider } from './ui/blocks.js';
|
|
3
|
-
import {
|
|
4
|
-
import { dispatchCommand } from './commands/index.js';
|
|
3
|
+
import { renderUser, renderAI, renderInfo, renderError, divider } from './ui/blocks.js';
|
|
4
|
+
import { dispatchCommand, ALL_COMMANDS } from './commands/index.js';
|
|
5
5
|
import { agentLoop } from './agents/loop.js';
|
|
6
6
|
import { showBanner } from './ui/banner.js';
|
|
7
7
|
|
|
8
8
|
export async function startChat(config) {
|
|
9
9
|
const history = [];
|
|
10
|
-
const palette = new SlashPalette();
|
|
11
|
-
let slashMode = false;
|
|
12
|
-
let slashBuffer = '';
|
|
13
10
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
11
|
+
console.log(
|
|
12
|
+
chalk.hex('#00FF9F')('\u2714 ') +
|
|
13
|
+
chalk.white('CLARITY-AI v' + config.version + ' interactive session started.') +
|
|
14
|
+
chalk.dim(' Type /help for commands. Ctrl+C to exit.')
|
|
15
|
+
);
|
|
17
16
|
console.log(divider());
|
|
18
17
|
console.log();
|
|
19
18
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
process.stdout.write('\r\x1b[2K');
|
|
28
|
-
process.stdout.write(getPrompt(config.provider, config.model));
|
|
29
|
-
if (currentInput) process.stdout.write(currentInput);
|
|
19
|
+
function makePrompt() {
|
|
20
|
+
const W = process.stdout.columns || 80;
|
|
21
|
+
const label = config.provider + '/' + config.model;
|
|
22
|
+
const agentTag = config.agentMode ? chalk.hex('#00FF9F')('agent:ON') : chalk.dim('agent:OFF');
|
|
23
|
+
const leftPart = chalk.hex('#333333')('\u2502 ') + chalk.hex('#00FFFF')('\u276f ');
|
|
24
|
+
const rightPart = chalk.dim(' ' + label + ' ') + agentTag + chalk.hex('#333333')(' \u2502');
|
|
25
|
+
return leftPart + rightPart + '\n' + chalk.hex('#333333')('\u2514' + '\u2500'.repeat(Math.max(0, W - 2)) + '\u2518') + '\n' + chalk.hex('#00FFFF')(' \u276f ');
|
|
30
26
|
}
|
|
31
27
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
28
|
+
const rl = readline.createInterface({
|
|
29
|
+
input: process.stdin,
|
|
30
|
+
output: process.stdout,
|
|
31
|
+
terminal: true,
|
|
32
|
+
historySize: 200,
|
|
33
|
+
prompt: makePrompt(),
|
|
34
|
+
});
|
|
36
35
|
|
|
37
|
-
|
|
38
|
-
|
|
36
|
+
rl.setPrompt(makePrompt());
|
|
37
|
+
rl.prompt();
|
|
39
38
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
palette.hide();
|
|
43
|
-
slashMode = false;
|
|
44
|
-
slashBuffer = '';
|
|
45
|
-
currentInput = '';
|
|
46
|
-
drawPromptLine();
|
|
47
|
-
return;
|
|
48
|
-
}
|
|
49
|
-
console.log('\n' + chalk.hex('#00FF9F')('\u2714 ') + chalk.dim('Session ended. Goodbye.'));
|
|
50
|
-
process.exit(0);
|
|
51
|
-
}
|
|
39
|
+
rl.on('line', async (rawInput) => {
|
|
40
|
+
const input = rawInput.trim();
|
|
52
41
|
|
|
53
|
-
if (
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
drawPromptLine();
|
|
42
|
+
if (!input) {
|
|
43
|
+
rl.setPrompt(makePrompt());
|
|
44
|
+
rl.prompt();
|
|
57
45
|
return;
|
|
58
46
|
}
|
|
59
47
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
slashBuffer = '';
|
|
64
|
-
drawPromptLine();
|
|
65
|
-
return;
|
|
66
|
-
}
|
|
48
|
+
console.log();
|
|
49
|
+
console.log(renderUser(input));
|
|
50
|
+
console.log();
|
|
67
51
|
|
|
68
|
-
if (
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
52
|
+
if (input === '/') {
|
|
53
|
+
console.log(chalk.hex('#00FFFF').bold(' Commands:'));
|
|
54
|
+
console.log(chalk.hex('#333333')(' ' + '\u2500'.repeat(50)));
|
|
55
|
+
for (const cmd of ALL_COMMANDS) {
|
|
56
|
+
console.log(
|
|
57
|
+
chalk.hex('#9B59FF')(' ' + cmd.name.padEnd(16)) +
|
|
58
|
+
chalk.dim(cmd.description)
|
|
59
|
+
);
|
|
75
60
|
}
|
|
61
|
+
console.log(chalk.hex('#333333')(' ' + '\u2500'.repeat(50)));
|
|
62
|
+
rl.setPrompt(makePrompt());
|
|
63
|
+
rl.prompt();
|
|
76
64
|
return;
|
|
77
65
|
}
|
|
78
66
|
|
|
79
|
-
if (
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
}
|
|
84
|
-
if (key.name === 'down') {
|
|
85
|
-
palette.selectNext();
|
|
86
|
-
return;
|
|
87
|
-
}
|
|
88
|
-
if (key.name === 'return') {
|
|
89
|
-
const selected = palette.getSelected();
|
|
90
|
-
palette.hide();
|
|
91
|
-
slashMode = false;
|
|
92
|
-
currentInput = '';
|
|
93
|
-
drawPromptLine();
|
|
94
|
-
if (selected) {
|
|
95
|
-
console.log();
|
|
96
|
-
await dispatchCommand(selected.name, config, history);
|
|
97
|
-
}
|
|
98
|
-
clearAndPrompt();
|
|
99
|
-
return;
|
|
100
|
-
}
|
|
101
|
-
if (key.name === 'backspace') {
|
|
102
|
-
slashBuffer = slashBuffer.slice(0, -1);
|
|
103
|
-
currentInput = '/' + slashBuffer;
|
|
104
|
-
palette.filter(slashBuffer);
|
|
105
|
-
drawPromptLine();
|
|
106
|
-
return;
|
|
107
|
-
}
|
|
108
|
-
if (str && !key.ctrl && !key.meta) {
|
|
109
|
-
slashBuffer += str;
|
|
110
|
-
currentInput = '/' + slashBuffer;
|
|
111
|
-
palette.filter(slashBuffer);
|
|
112
|
-
drawPromptLine();
|
|
113
|
-
return;
|
|
114
|
-
}
|
|
67
|
+
if (input.startsWith('/')) {
|
|
68
|
+
await dispatchCommand(input, config, history);
|
|
69
|
+
rl.setPrompt(makePrompt());
|
|
70
|
+
rl.prompt();
|
|
115
71
|
return;
|
|
116
72
|
}
|
|
117
73
|
|
|
118
|
-
|
|
119
|
-
currentInput = currentInput.slice(0, -1);
|
|
120
|
-
drawPromptLine();
|
|
121
|
-
return;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
if (key.name === 'return') {
|
|
125
|
-
const input = currentInput.trim();
|
|
126
|
-
currentInput = '';
|
|
127
|
-
|
|
128
|
-
if (!input) {
|
|
129
|
-
console.log();
|
|
130
|
-
drawPromptLine();
|
|
131
|
-
return;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
process.stdout.write('\r\x1b[2K');
|
|
135
|
-
console.log();
|
|
136
|
-
console.log(renderUser(input));
|
|
137
|
-
console.log();
|
|
138
|
-
|
|
139
|
-
if (input.startsWith('/')) {
|
|
140
|
-
await dispatchCommand(input, config, history);
|
|
141
|
-
clearAndPrompt();
|
|
142
|
-
return;
|
|
143
|
-
}
|
|
144
|
-
|
|
74
|
+
try {
|
|
145
75
|
await agentLoop(input, config, history);
|
|
146
|
-
|
|
147
|
-
|
|
76
|
+
} catch (err) {
|
|
77
|
+
console.log(chalk.hex('#FF4757')('\u2716 ') + chalk.hex('#FF4757')(err.message));
|
|
148
78
|
}
|
|
149
79
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
slashBuffer = '';
|
|
153
|
-
currentInput = '/';
|
|
154
|
-
console.log();
|
|
155
|
-
palette.show();
|
|
156
|
-
drawPromptLine();
|
|
157
|
-
return;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
if (str && !key.ctrl && !key.meta) {
|
|
161
|
-
currentInput += str;
|
|
162
|
-
drawPromptLine();
|
|
163
|
-
}
|
|
80
|
+
rl.setPrompt(makePrompt());
|
|
81
|
+
rl.prompt();
|
|
164
82
|
});
|
|
165
83
|
|
|
166
|
-
|
|
167
|
-
|
|
84
|
+
rl.on('SIGINT', () => {
|
|
85
|
+
console.log('\n' + chalk.hex('#00FF9F')('\u2714 ') + chalk.dim('Goodbye.'));
|
|
86
|
+
process.exit(0);
|
|
87
|
+
});
|
|
168
88
|
}
|
package/src/providers/claude.js
CHANGED
|
@@ -1,61 +1,35 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
baseURL: 'https://api.anthropic.com/v1',
|
|
7
|
-
};
|
|
1
|
+
export async function callAnthropic(config, messages, tools = null) {
|
|
2
|
+
const apiKey = config.apiKeys?.anthropic;
|
|
3
|
+
if (!apiKey) throw new Error('No Anthropic API key set. Run: /keys anthropic <your-key>');
|
|
4
|
+
|
|
5
|
+
const model = config.model || 'claude-3-5-haiku-20241022';
|
|
8
6
|
|
|
9
|
-
async function* sendMessage(apiKey, messages, model = 'claude-3-5-haiku-20241022', stream = true) {
|
|
10
|
-
const url = `${PROVIDER.baseURL}/messages`;
|
|
11
|
-
|
|
12
7
|
const systemMsg = messages.filter(m => m.role === 'system');
|
|
13
8
|
const chatMessages = messages.filter(m => m.role !== 'system');
|
|
14
|
-
|
|
9
|
+
|
|
15
10
|
const body = {
|
|
16
11
|
model,
|
|
17
|
-
max_tokens: 4096,
|
|
12
|
+
max_tokens: config.maxTokens || 4096,
|
|
18
13
|
messages: chatMessages.map(m => ({ role: m.role, content: m.content })),
|
|
19
|
-
stream,
|
|
20
14
|
};
|
|
21
15
|
if (systemMsg.length > 0) body.system = systemMsg.map(m => m.content).join('\n');
|
|
22
16
|
|
|
23
|
-
const res = await fetch(
|
|
17
|
+
const res = await fetch('https://api.anthropic.com/v1/messages', {
|
|
24
18
|
method: 'POST',
|
|
25
|
-
headers: {
|
|
19
|
+
headers: {
|
|
20
|
+
'x-api-key': apiKey,
|
|
21
|
+
'anthropic-version': '2023-06-01',
|
|
22
|
+
'Content-Type': 'application/json',
|
|
23
|
+
},
|
|
26
24
|
body: JSON.stringify(body),
|
|
27
25
|
});
|
|
28
26
|
|
|
29
|
-
if (!res.ok)
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
const data = await res.json();
|
|
33
|
-
yield data.content[0].text;
|
|
34
|
-
return;
|
|
27
|
+
if (!res.ok) {
|
|
28
|
+
const detail = await res.text();
|
|
29
|
+
throw new Error('Anthropic API error: ' + res.status + ' \u2014 ' + (detail.slice(0, 200)));
|
|
35
30
|
}
|
|
36
31
|
|
|
37
|
-
const
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
while (true) {
|
|
42
|
-
const { done, value } = await reader.read();
|
|
43
|
-
if (done) break;
|
|
44
|
-
buffer += decoder.decode(value, { stream: true });
|
|
45
|
-
const lines = buffer.split('\n');
|
|
46
|
-
buffer = lines.pop() || '';
|
|
47
|
-
|
|
48
|
-
for (const line of lines) {
|
|
49
|
-
if (line.startsWith('data: ')) {
|
|
50
|
-
try {
|
|
51
|
-
const json = JSON.parse(line.slice(6));
|
|
52
|
-
if (json.type === 'content_block_delta' && json.delta?.text) {
|
|
53
|
-
yield json.delta.text;
|
|
54
|
-
}
|
|
55
|
-
} catch {}
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
}
|
|
32
|
+
const data = await res.json();
|
|
33
|
+
const text = data.content?.[0]?.text || '';
|
|
34
|
+
return { content: text, tool_calls: null };
|
|
59
35
|
}
|
|
60
|
-
|
|
61
|
-
export { PROVIDER, sendMessage };
|
|
@@ -1,53 +1,47 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
cheap: true,
|
|
5
|
-
streaming: true,
|
|
6
|
-
models: ['deepseek-chat', 'deepseek-coder'],
|
|
7
|
-
baseURL: 'https://api.deepseek.com/v1',
|
|
8
|
-
};
|
|
1
|
+
export async function callDeepSeek(config, messages, tools = null) {
|
|
2
|
+
const apiKey = config.apiKeys?.deepseek;
|
|
3
|
+
if (!apiKey) throw new Error('No DeepSeek API key set. Run: /keys deepseek <your-key>');
|
|
9
4
|
|
|
10
|
-
|
|
11
|
-
const
|
|
12
|
-
|
|
5
|
+
const model = config.model || 'deepseek-chat';
|
|
6
|
+
const body = {
|
|
7
|
+
model,
|
|
8
|
+
messages,
|
|
9
|
+
max_tokens: config.maxTokens || 4096,
|
|
10
|
+
temperature: config.temperature || 0.7,
|
|
11
|
+
};
|
|
13
12
|
|
|
14
|
-
|
|
13
|
+
if (tools && tools.length > 0) {
|
|
14
|
+
body.tools = tools.map(t => ({
|
|
15
|
+
type: 'function',
|
|
16
|
+
function: {
|
|
17
|
+
name: t.name,
|
|
18
|
+
description: t.description,
|
|
19
|
+
parameters: t.parameters,
|
|
20
|
+
},
|
|
21
|
+
}));
|
|
22
|
+
body.tool_choice = 'auto';
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const res = await fetch('https://api.deepseek.com/v1/chat/completions', {
|
|
15
26
|
method: 'POST',
|
|
16
|
-
headers: {
|
|
27
|
+
headers: {
|
|
28
|
+
'Content-Type': 'application/json',
|
|
29
|
+
'Authorization': 'Bearer ' + apiKey,
|
|
30
|
+
},
|
|
17
31
|
body: JSON.stringify(body),
|
|
18
32
|
});
|
|
19
33
|
|
|
20
|
-
if (!res.ok)
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
const data = await res.json();
|
|
24
|
-
yield data.choices[0].message.content;
|
|
25
|
-
return;
|
|
34
|
+
if (!res.ok) {
|
|
35
|
+
const detail = await res.text();
|
|
36
|
+
throw new Error('DeepSeek API error: ' + res.status + ' \u2014 ' + (detail.slice(0, 200)));
|
|
26
37
|
}
|
|
27
38
|
|
|
28
|
-
const
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
const lines = buffer.split('\n');
|
|
37
|
-
buffer = lines.pop() || '';
|
|
38
|
-
|
|
39
|
-
for (const line of lines) {
|
|
40
|
-
if (line.startsWith('data: ')) {
|
|
41
|
-
const data = line.slice(6).trim();
|
|
42
|
-
if (data === '[DONE]') return;
|
|
43
|
-
try {
|
|
44
|
-
const json = JSON.parse(data);
|
|
45
|
-
const delta = json.choices?.[0]?.delta?.content;
|
|
46
|
-
if (delta) yield delta;
|
|
47
|
-
} catch {}
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
}
|
|
39
|
+
const data = await res.json();
|
|
40
|
+
const choice = data.choices?.[0];
|
|
41
|
+
if (!choice) throw new Error('Empty response from DeepSeek');
|
|
42
|
+
const message = choice.message;
|
|
43
|
+
return {
|
|
44
|
+
content: message.content || '',
|
|
45
|
+
tool_calls: message.tool_calls || null,
|
|
46
|
+
};
|
|
51
47
|
}
|
|
52
|
-
|
|
53
|
-
export { PROVIDER, sendMessage };
|
package/src/providers/gemini.js
CHANGED
|
@@ -1,48 +1,35 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
};
|
|
1
|
+
export async function callGemini(config, messages, tools = null) {
|
|
2
|
+
const apiKey = config.apiKeys?.gemini;
|
|
3
|
+
if (!apiKey) throw new Error('No Gemini API key set. Run: /keys gemini <your-key>');
|
|
4
|
+
|
|
5
|
+
const model = config.model || 'gemini-1.5-flash';
|
|
6
|
+
const url = 'https://generativelanguage.googleapis.com/v1beta/models/' + model + ':generateContent?key=' + apiKey;
|
|
8
7
|
|
|
9
|
-
async function* sendMessage(apiKey, messages, model = 'gemini-1.5-flash', stream = true) {
|
|
10
|
-
const url = `${PROVIDER.baseURL}/models/${model}:streamGenerateContent?key=${apiKey}&alt=sse`;
|
|
11
|
-
|
|
12
8
|
const contents = messages.map(m => ({
|
|
13
9
|
role: m.role === 'assistant' ? 'model' : m.role,
|
|
14
|
-
parts: [{ text: m.content }]
|
|
10
|
+
parts: [{ text: m.content }],
|
|
15
11
|
}));
|
|
16
12
|
|
|
13
|
+
const body = {
|
|
14
|
+
contents,
|
|
15
|
+
generationConfig: {
|
|
16
|
+
maxOutputTokens: config.maxTokens || 4096,
|
|
17
|
+
temperature: config.temperature || 0.7,
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
|
|
17
21
|
const res = await fetch(url, {
|
|
18
22
|
method: 'POST',
|
|
19
23
|
headers: { 'Content-Type': 'application/json' },
|
|
20
|
-
body: JSON.stringify(
|
|
24
|
+
body: JSON.stringify(body),
|
|
21
25
|
});
|
|
22
26
|
|
|
23
|
-
if (!res.ok)
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
const decoder = new TextDecoder();
|
|
27
|
-
let buffer = '';
|
|
28
|
-
|
|
29
|
-
while (true) {
|
|
30
|
-
const { done, value } = await reader.read();
|
|
31
|
-
if (done) break;
|
|
32
|
-
buffer += decoder.decode(value, { stream: true });
|
|
33
|
-
const lines = buffer.split('\n');
|
|
34
|
-
buffer = lines.pop() || '';
|
|
35
|
-
|
|
36
|
-
for (const line of lines) {
|
|
37
|
-
if (line.startsWith('data: ')) {
|
|
38
|
-
try {
|
|
39
|
-
const json = JSON.parse(line.slice(6));
|
|
40
|
-
const text = json.candidates?.[0]?.content?.parts?.[0]?.text;
|
|
41
|
-
if (text) yield text;
|
|
42
|
-
} catch {}
|
|
43
|
-
}
|
|
44
|
-
}
|
|
27
|
+
if (!res.ok) {
|
|
28
|
+
const detail = await res.text();
|
|
29
|
+
throw new Error('Gemini API error: ' + res.status + ' \u2014 ' + (detail.slice(0, 200)));
|
|
45
30
|
}
|
|
46
|
-
}
|
|
47
31
|
|
|
48
|
-
|
|
32
|
+
const data = await res.json();
|
|
33
|
+
const text = data.candidates?.[0]?.content?.parts?.[0]?.text || '';
|
|
34
|
+
return { content: text, tool_calls: null };
|
|
35
|
+
}
|
package/src/providers/groq.js
CHANGED
|
@@ -1,56 +1,99 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
|
|
3
|
+
const GROQ_MODELS = [
|
|
4
|
+
'llama-3.3-70b-versatile',
|
|
5
|
+
'llama-3.1-8b-instant',
|
|
6
|
+
'llama-3.1-70b-versatile',
|
|
7
|
+
'llama-4-scout-17b-16e-instruct',
|
|
8
|
+
'llama-4-maverick-17b-128e-instruct',
|
|
9
|
+
'gemma2-9b-it',
|
|
10
|
+
'mixtral-8x7b-32768',
|
|
11
|
+
'deepseek-r1-distill-llama-70b',
|
|
12
|
+
'deepseek-r1-distill-qwen-32b',
|
|
13
|
+
'compound-beta',
|
|
14
|
+
'compound-beta-mini',
|
|
15
|
+
'allam-2-7b',
|
|
16
|
+
'kimi-k2-instruct',
|
|
17
|
+
'moonshotai/kimi-k2-instruct',
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
function fuzzyMatch(input, list) {
|
|
21
|
+
const lower = input.toLowerCase();
|
|
22
|
+
const exact = list.find(m => m.toLowerCase() === lower);
|
|
23
|
+
if (exact) return exact;
|
|
24
|
+
const contains = list.find(m => m.toLowerCase().includes(lower) || lower.includes(m.split('-')[0]));
|
|
25
|
+
return contains || null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function callGroq(config, messages, tools = null) {
|
|
29
|
+
const apiKey = config.apiKeys?.groq;
|
|
30
|
+
if (!apiKey) throw new Error('No Groq API key set. Run: /keys groq <your-key>');
|
|
31
|
+
|
|
32
|
+
let model = config.model;
|
|
33
|
+
const matched = fuzzyMatch(model, GROQ_MODELS);
|
|
34
|
+
|
|
35
|
+
if (!matched) {
|
|
36
|
+
console.log(
|
|
37
|
+
chalk.hex('#FFB800')('\u26a0 Unknown Groq model: ') + chalk.white(model) + '\n' +
|
|
38
|
+
chalk.dim(' Valid models: ') + chalk.dim(GROQ_MODELS.slice(0, 5).join(', ') + '...')
|
|
39
|
+
);
|
|
40
|
+
console.log(chalk.dim(' Falling back to: llama-3.3-70b-versatile'));
|
|
41
|
+
model = 'llama-3.3-70b-versatile';
|
|
42
|
+
} else if (matched !== model) {
|
|
43
|
+
console.log(chalk.dim(' Auto-corrected model: ' + model + ' \u2192 ' + matched));
|
|
44
|
+
model = matched;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const body = {
|
|
48
|
+
model,
|
|
49
|
+
messages,
|
|
50
|
+
max_tokens: config.maxTokens || 4096,
|
|
51
|
+
temperature: config.temperature || 0.7,
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
if (tools && tools.length > 0) {
|
|
55
|
+
body.tools = tools.map(t => ({
|
|
56
|
+
type: 'function',
|
|
57
|
+
function: {
|
|
58
|
+
name: t.name,
|
|
59
|
+
description: t.description,
|
|
60
|
+
parameters: t.parameters,
|
|
61
|
+
},
|
|
62
|
+
}));
|
|
63
|
+
body.tool_choice = 'auto';
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const res = await fetch('https://api.groq.com/openai/v1/chat/completions', {
|
|
14
67
|
method: 'POST',
|
|
15
|
-
headers: {
|
|
68
|
+
headers: {
|
|
69
|
+
'Content-Type': 'application/json',
|
|
70
|
+
'Authorization': 'Bearer ' + apiKey,
|
|
71
|
+
},
|
|
16
72
|
body: JSON.stringify(body),
|
|
17
73
|
});
|
|
18
74
|
|
|
19
75
|
if (!res.ok) {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
76
|
+
const detail = await res.text();
|
|
77
|
+
let msg = 'Groq API error: ' + res.status;
|
|
78
|
+
try {
|
|
79
|
+
const parsed = JSON.parse(detail);
|
|
80
|
+
msg = parsed.error?.message || msg;
|
|
81
|
+
if (res.status === 404 || msg.includes('does not exist')) {
|
|
82
|
+
msg += '\n Valid Groq models include:\n ' + GROQ_MODELS.slice(0, 6).map(m => ' \u2022 ' + m).join('\n');
|
|
83
|
+
msg += '\n Use /model <name> to switch.';
|
|
84
|
+
}
|
|
85
|
+
} catch {}
|
|
86
|
+
throw new Error(msg);
|
|
23
87
|
}
|
|
24
88
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
return;
|
|
29
|
-
}
|
|
89
|
+
const data = await res.json();
|
|
90
|
+
const choice = data.choices?.[0];
|
|
91
|
+
if (!choice) throw new Error('Empty response from Groq');
|
|
30
92
|
|
|
31
|
-
const
|
|
32
|
-
const decoder = new TextDecoder();
|
|
33
|
-
let buffer = '';
|
|
34
|
-
|
|
35
|
-
while (true) {
|
|
36
|
-
const { done, value } = await reader.read();
|
|
37
|
-
if (done) break;
|
|
38
|
-
buffer += decoder.decode(value, { stream: true });
|
|
39
|
-
const lines = buffer.split('\n');
|
|
40
|
-
buffer = lines.pop() || '';
|
|
41
|
-
|
|
42
|
-
for (const line of lines) {
|
|
43
|
-
if (line.startsWith('data: ')) {
|
|
44
|
-
const data = line.slice(6).trim();
|
|
45
|
-
if (data === '[DONE]') return;
|
|
46
|
-
try {
|
|
47
|
-
const json = JSON.parse(data);
|
|
48
|
-
const delta = json.choices?.[0]?.delta?.content;
|
|
49
|
-
if (delta) yield delta;
|
|
50
|
-
} catch {}
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
}
|
|
93
|
+
const message = choice.message;
|
|
55
94
|
|
|
56
|
-
|
|
95
|
+
return {
|
|
96
|
+
content: message.content || '',
|
|
97
|
+
tool_calls: message.tool_calls || null,
|
|
98
|
+
};
|
|
99
|
+
}
|
package/src/providers/index.js
CHANGED
|
@@ -1,36 +1,53 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
1
|
+
import { callGroq } from './groq.js';
|
|
2
|
+
import { callGemini } from './gemini.js';
|
|
3
|
+
import { callOpenRouter } from './openrouter.js';
|
|
4
|
+
import { callOpenAI } from './openai.js';
|
|
5
|
+
import { callAnthropic } from './claude.js';
|
|
6
|
+
import { callDeepSeek } from './deepseek.js';
|
|
7
7
|
|
|
8
8
|
export const PROVIDERS = [
|
|
9
9
|
{ name: 'Groq', value: 'groq', freeModel: 'llama-3.3-70b-versatile', keyUrl: 'https://console.groq.com/keys' },
|
|
10
10
|
{ name: 'Gemini', value: 'gemini', freeModel: 'gemini-1.5-flash', keyUrl: 'https://aistudio.google.com/app/apikey' },
|
|
11
11
|
{ name: 'OpenRouter', value: 'openrouter', freeModel: 'mistralai/mistral-7b-instruct:free', keyUrl: 'https://openrouter.ai/keys' },
|
|
12
12
|
{ name: 'OpenAI', value: 'openai', freeModel: 'gpt-4o', keyUrl: 'https://platform.openai.com/api-keys' },
|
|
13
|
-
{ name: 'Anthropic', value: 'anthropic', freeModel: 'claude-3-5-
|
|
13
|
+
{ name: 'Anthropic', value: 'anthropic', freeModel: 'claude-3-5-haiku-20241022', keyUrl: 'https://console.anthropic.com/settings/keys' },
|
|
14
14
|
{ name: 'DeepSeek', value: 'deepseek', freeModel: 'deepseek-chat', keyUrl: 'https://platform.deepseek.com/api_keys' },
|
|
15
15
|
];
|
|
16
16
|
|
|
17
|
-
const
|
|
17
|
+
const callers = {
|
|
18
|
+
groq: callGroq,
|
|
19
|
+
gemini: callGemini,
|
|
20
|
+
openrouter: callOpenRouter,
|
|
21
|
+
openai: callOpenAI,
|
|
22
|
+
anthropic: callAnthropic,
|
|
23
|
+
deepseek: callDeepSeek,
|
|
24
|
+
};
|
|
18
25
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
26
|
+
const SYSTEM_PROMPT = `You are CLARITY, an autonomous AI agent CLI for Termux.
|
|
27
|
+
|
|
28
|
+
RULES:
|
|
29
|
+
- Never fabricate file contents, directory listings, or command output
|
|
30
|
+
- Only call tools when the user asks for a real action (file, bash, search)
|
|
31
|
+
- For greetings or questions, just reply in plain text \u2014 no tools
|
|
32
|
+
- Keep replies concise. No filler words. No "Certainly!"
|
|
33
|
+
- If a tool fails, report the actual error. Never pretend it succeeded.
|
|
34
|
+
- You are running on Android/Termux. Use Termux-compatible commands.`;
|
|
35
|
+
|
|
36
|
+
export async function callProvider(config, messagesOrHistory, tools = null) {
|
|
37
|
+
let messages = Array.isArray(messagesOrHistory)
|
|
38
|
+
? messagesOrHistory
|
|
39
|
+
: (messagesOrHistory?.messages || []);
|
|
40
|
+
|
|
41
|
+
const withSystem = [
|
|
42
|
+
{ role: 'system', content: SYSTEM_PROMPT },
|
|
43
|
+
...messages.filter(m => m.role !== 'system'),
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
const payload = { ...config, model: config.model };
|
|
47
|
+
const caller = callers[config.provider];
|
|
48
|
+
if (!caller) throw new Error('Unknown provider: ' + config.provider + '. Use /provider to switch.');
|
|
27
49
|
|
|
28
|
-
|
|
29
|
-
let full = '';
|
|
30
|
-
for await (const chunk of streamProvider(config, messages)) {
|
|
31
|
-
full += chunk;
|
|
32
|
-
}
|
|
33
|
-
return full;
|
|
50
|
+
return caller(payload, withSystem, tools);
|
|
34
51
|
}
|
|
35
52
|
|
|
36
53
|
export function getProvider(name) {
|
package/src/providers/openai.js
CHANGED
|
@@ -1,52 +1,47 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
streaming: true,
|
|
5
|
-
models: ['gpt-4o-mini', 'gpt-4o', 'gpt-3.5-turbo'],
|
|
6
|
-
baseURL: 'https://api.openai.com/v1',
|
|
7
|
-
};
|
|
1
|
+
export async function callOpenAI(config, messages, tools = null) {
|
|
2
|
+
const apiKey = config.apiKeys?.openai;
|
|
3
|
+
if (!apiKey) throw new Error('No OpenAI API key set. Run: /keys openai <your-key>');
|
|
8
4
|
|
|
9
|
-
|
|
10
|
-
const
|
|
11
|
-
|
|
5
|
+
const model = config.model || 'gpt-4o-mini';
|
|
6
|
+
const body = {
|
|
7
|
+
model,
|
|
8
|
+
messages,
|
|
9
|
+
max_tokens: config.maxTokens || 4096,
|
|
10
|
+
temperature: config.temperature || 0.7,
|
|
11
|
+
};
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
if (tools && tools.length > 0) {
|
|
14
|
+
body.tools = tools.map(t => ({
|
|
15
|
+
type: 'function',
|
|
16
|
+
function: {
|
|
17
|
+
name: t.name,
|
|
18
|
+
description: t.description,
|
|
19
|
+
parameters: t.parameters,
|
|
20
|
+
},
|
|
21
|
+
}));
|
|
22
|
+
body.tool_choice = 'auto';
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const res = await fetch('https://api.openai.com/v1/chat/completions', {
|
|
14
26
|
method: 'POST',
|
|
15
|
-
headers: {
|
|
27
|
+
headers: {
|
|
28
|
+
'Content-Type': 'application/json',
|
|
29
|
+
'Authorization': 'Bearer ' + apiKey,
|
|
30
|
+
},
|
|
16
31
|
body: JSON.stringify(body),
|
|
17
32
|
});
|
|
18
33
|
|
|
19
|
-
if (!res.ok)
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
const data = await res.json();
|
|
23
|
-
yield data.choices[0].message.content;
|
|
24
|
-
return;
|
|
34
|
+
if (!res.ok) {
|
|
35
|
+
const detail = await res.text();
|
|
36
|
+
throw new Error('OpenAI API error: ' + res.status + ' \u2014 ' + (detail.slice(0, 200)));
|
|
25
37
|
}
|
|
26
38
|
|
|
27
|
-
const
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
const lines = buffer.split('\n');
|
|
36
|
-
buffer = lines.pop() || '';
|
|
37
|
-
|
|
38
|
-
for (const line of lines) {
|
|
39
|
-
if (line.startsWith('data: ')) {
|
|
40
|
-
const data = line.slice(6).trim();
|
|
41
|
-
if (data === '[DONE]') return;
|
|
42
|
-
try {
|
|
43
|
-
const json = JSON.parse(data);
|
|
44
|
-
const delta = json.choices?.[0]?.delta?.content;
|
|
45
|
-
if (delta) yield delta;
|
|
46
|
-
} catch {}
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
}
|
|
39
|
+
const data = await res.json();
|
|
40
|
+
const choice = data.choices?.[0];
|
|
41
|
+
if (!choice) throw new Error('Empty response from OpenAI');
|
|
42
|
+
const message = choice.message;
|
|
43
|
+
return {
|
|
44
|
+
content: message.content || '',
|
|
45
|
+
tool_calls: message.tool_calls || null,
|
|
46
|
+
};
|
|
50
47
|
}
|
|
51
|
-
|
|
52
|
-
export { PROVIDER, sendMessage };
|
|
@@ -1,52 +1,49 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
streaming: true,
|
|
5
|
-
models: ['meta-llama/llama-3.1-8b-instruct:free', 'google/gemma-2-9b-it:free', 'mistralai/mistral-7b-instruct:free'],
|
|
6
|
-
baseURL: 'https://openrouter.ai/api/v1',
|
|
7
|
-
};
|
|
1
|
+
export async function callOpenRouter(config, messages, tools = null) {
|
|
2
|
+
const apiKey = config.apiKeys?.openrouter;
|
|
3
|
+
if (!apiKey) throw new Error('No OpenRouter API key set. Run: /keys openrouter <your-key>');
|
|
8
4
|
|
|
9
|
-
|
|
10
|
-
const
|
|
11
|
-
|
|
5
|
+
const model = config.model || 'mistralai/mistral-7b-instruct:free';
|
|
6
|
+
const body = {
|
|
7
|
+
model,
|
|
8
|
+
messages,
|
|
9
|
+
max_tokens: config.maxTokens || 4096,
|
|
10
|
+
temperature: config.temperature || 0.7,
|
|
11
|
+
};
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
if (tools && tools.length > 0) {
|
|
14
|
+
body.tools = tools.map(t => ({
|
|
15
|
+
type: 'function',
|
|
16
|
+
function: {
|
|
17
|
+
name: t.name,
|
|
18
|
+
description: t.description,
|
|
19
|
+
parameters: t.parameters,
|
|
20
|
+
},
|
|
21
|
+
}));
|
|
22
|
+
body.tool_choice = 'auto';
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const res = await fetch('https://openrouter.ai/api/v1/chat/completions', {
|
|
14
26
|
method: 'POST',
|
|
15
|
-
headers: {
|
|
27
|
+
headers: {
|
|
28
|
+
'Content-Type': 'application/json',
|
|
29
|
+
'Authorization': 'Bearer ' + apiKey,
|
|
30
|
+
'HTTP-Referer': 'https://clarity-ai.local',
|
|
31
|
+
'X-Title': 'CLARITY AI',
|
|
32
|
+
},
|
|
16
33
|
body: JSON.stringify(body),
|
|
17
34
|
});
|
|
18
35
|
|
|
19
|
-
if (!res.ok)
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
const data = await res.json();
|
|
23
|
-
yield data.choices[0].message.content;
|
|
24
|
-
return;
|
|
36
|
+
if (!res.ok) {
|
|
37
|
+
const detail = await res.text();
|
|
38
|
+
throw new Error('OpenRouter API error: ' + res.status + ' \u2014 ' + (detail.slice(0, 200)));
|
|
25
39
|
}
|
|
26
40
|
|
|
27
|
-
const
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
const lines = buffer.split('\n');
|
|
36
|
-
buffer = lines.pop() || '';
|
|
37
|
-
|
|
38
|
-
for (const line of lines) {
|
|
39
|
-
if (line.startsWith('data: ')) {
|
|
40
|
-
const data = line.slice(6).trim();
|
|
41
|
-
if (data === '[DONE]') return;
|
|
42
|
-
try {
|
|
43
|
-
const json = JSON.parse(data);
|
|
44
|
-
const delta = json.choices?.[0]?.delta?.content;
|
|
45
|
-
if (delta) yield delta;
|
|
46
|
-
} catch {}
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
}
|
|
41
|
+
const data = await res.json();
|
|
42
|
+
const choice = data.choices?.[0];
|
|
43
|
+
if (!choice) throw new Error('Empty response from OpenRouter');
|
|
44
|
+
const message = choice.message;
|
|
45
|
+
return {
|
|
46
|
+
content: message.content || '',
|
|
47
|
+
tool_calls: message.tool_calls || null,
|
|
48
|
+
};
|
|
50
49
|
}
|
|
51
|
-
|
|
52
|
-
export { PROVIDER, sendMessage };
|
package/src/ui/banner.js
CHANGED
|
@@ -6,7 +6,7 @@ import { clr } from './colors.js';
|
|
|
6
6
|
|
|
7
7
|
const clarityGradient = gradient(['#00FFFF', '#7B2FFF', '#FF6B6B']);
|
|
8
8
|
|
|
9
|
-
export async function showBanner(version = '3.
|
|
9
|
+
export async function showBanner(version = '3.3.0', provider = 'groq', model = 'llama-3.3-70b') {
|
|
10
10
|
// Clear terminal
|
|
11
11
|
process.stdout.write('\x1Bc');
|
|
12
12
|
await new Promise(r => setTimeout(r, 50));
|