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 CHANGED
@@ -1,5 +1,4 @@
1
1
  #!/usr/bin/env node
2
- // bin/clarity.js
3
2
  import { showBanner } from '../src/ui/banner.js';
4
3
  import { isFirstRun, loadConfig } from '../src/config/settings.js';
5
4
  import { runSetupWizard } from '../src/core/setup.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clarity-ai",
3
- "version": "3.2.0",
3
+ "version": "3.3.0",
4
4
  "description": "Autonomous AI Agent CLI for Termux",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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, CollapsibleStep } from '../ui/spinner.js';
6
- import { renderAI, renderWrite, renderEdit, renderTool } from '../ui/blocks.js';
7
- import { callProvider, streamProvider } from '../providers/index.js';
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 just do it.\n' +
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)/, intent: 'bash' },
38
- { re: /(create|make|write|generate)\s+(a\s+)?(file|script|code|program)/, intent: 'write_file' },
39
- { re: /(run|execute|launch|start)\s+/, intent: 'bash' },
40
- { re: /(edit|modify|change|update|fix)\s+(the\s+)?file/, intent: 'edit_file' },
41
- { re: /(read|show|display|cat|open)\s+(the\s+)?file/, intent: 'read_file' },
42
- { re: /(list|ls|show)\s+(files|dir|directory|folders)/, intent: 'list_directory' },
43
- { re: /(search|find|grep|look for)/, intent: 'search' },
44
- { re: /(install|npm|pip|pkg)\s+/, intent: 'bash' },
45
- { re: /(git\s+)/, intent: 'git' },
46
- { re: /(cd\s+|navigate to|go to)/, intent: 'bash' },
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
- const intent = detectIntent(userMessage);
189
- const spinner = new Spinner();
190
- const msgHistory = Array.isArray(history) ? history : (history.messages || []);
188
+ try {
189
+ const intent = detectIntent(userMessage);
190
+ const spinner = new Spinner();
191
+ const msgHistory = Array.isArray(history) ? history : (history.messages || []);
191
192
 
192
- if (intent === 'chat') {
193
- spinner.start('Thinking', 'think');
194
- const t0 = Date.now();
193
+ if (intent === 'chat') {
194
+ spinner.start('Thinking', 'think');
195
+ const t0 = Date.now();
195
196
 
196
- const messages = [
197
- { role: 'system', content: 'You are CLARITY-AI, a helpful assistant. Keep responses short.' },
198
- ...msgHistory.slice(-10).map(m => ({ role: m.role === 'assistant' ? 'assistant' : 'user', content: m.content })),
199
- { role: 'user', content: userMessage },
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
- const response = await callProvider(config, messages);
203
- const elapsed = Date.now() - t0;
204
- spinner.stop('Thought ' + (elapsed < 1000 ? elapsed + 'ms' : (elapsed/1000).toFixed(1) + 's'), 'done');
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
- addMessage(msgHistory, 'user', userMessage);
207
- addMessage(msgHistory, 'assistant', response);
208
- saveHistory(msgHistory);
207
+ addMessage(msgHistory, 'user', userMessage);
208
+ addMessage(msgHistory, 'assistant', result.content);
209
+ saveHistory(msgHistory);
209
210
 
210
- console.log();
211
- console.log(renderAI(response));
212
- console.log();
213
- return;
214
- }
211
+ console.log();
212
+ console.log(renderAI(result.content));
213
+ console.log();
214
+ return;
215
+ }
215
216
 
216
- let messages = [
217
- { role: 'system', content: SYSTEM_PROMPT },
218
- ...msgHistory.slice(-20).map(m => ({ role: m.role === 'assistant' ? 'assistant' : 'user', content: m.content })),
219
- { role: 'user', content: userMessage },
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
- let loopCount = 0;
223
- const MAX_LOOPS = 15;
223
+ let loopCount = 0;
224
+ const MAX_LOOPS = 15;
224
225
 
225
- while (loopCount < MAX_LOOPS) {
226
- loopCount++;
226
+ while (loopCount < MAX_LOOPS) {
227
+ loopCount++;
227
228
 
228
- spinner.start('Thinking', 'think');
229
- const t0 = Date.now();
229
+ spinner.start('Thinking', 'think');
230
+ const t0 = Date.now();
230
231
 
231
- let fullResponse = '';
232
- const stream = streamProvider(config, messages);
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
- const thinkMs = Date.now() - t0;
238
- spinner.stop('Thought ' + (thinkMs < 1000 ? thinkMs + 'ms' : (thinkMs/1000).toFixed(1) + 's'), 'done');
235
+ const thinkMs = Date.now() - t0;
236
+ spinner.stop('Thought ' + (thinkMs < 1000 ? thinkMs + 'ms' : (thinkMs/1000).toFixed(1) + 's'), 'done');
239
237
 
240
- const jsonMatch = fullResponse.match(/\{[^]*\}/);
241
- if (!jsonMatch) {
242
- console.log();
243
- console.log(renderAI(fullResponse));
244
- console.log();
245
- addMessage(msgHistory, 'user', userMessage);
246
- addMessage(msgHistory, 'assistant', fullResponse);
247
- saveHistory(msgHistory);
248
- return;
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
- let parsed;
252
- try {
253
- parsed = JSON.parse(jsonMatch[0]);
254
- } catch {
255
- console.log();
256
- console.log(renderAI(fullResponse));
257
- console.log();
258
- addMessage(msgHistory, 'user', userMessage);
259
- addMessage(msgHistory, 'assistant', fullResponse);
260
- saveHistory(msgHistory);
261
- return;
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
- if (parsed.response) {
265
- const finalText = parsed.response;
266
- console.log();
267
- console.log(renderAI(finalText));
268
- console.log();
269
- addMessage(msgHistory, 'user', userMessage);
270
- addMessage(msgHistory, 'assistant', finalText);
271
- saveHistory(msgHistory);
272
- return;
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
- if (parsed.tool) {
276
- const name = parsed.tool;
277
- const args = parsed.args || {};
278
- const argPreview = args.command || args.path || args.pattern || '';
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
- spinner.start('Running ' + name, 'tool');
281
- spinner.update('Running ' + name, argPreview.slice(0, 40));
278
+ spinner.start('Running ' + name, 'tool');
279
+ spinner.update('Running ' + name, argPreview.slice(0, 40));
282
280
 
283
- const ts = Date.now();
284
- const result = await executeTool(name, args);
285
- const te = Date.now() - ts;
281
+ const ts = Date.now();
282
+ const toolResult = await executeTool(name, args);
283
+ const te = Date.now() - ts;
286
284
 
287
- spinner.stop(name + ' \u00b7 ' + argPreview.slice(0, 30), 'done');
288
- renderToolStep(name, args, result, te);
285
+ spinner.stop(name + ' \u00b7 ' + argPreview.slice(0, 30), 'done');
286
+ renderToolStep(name, args, toolResult, te);
289
287
 
290
- messages.push({ role: 'assistant', content: fullResponse });
291
- messages.push({ role: 'user', content: 'Tool result: ' + result });
288
+ messages.push({ role: 'assistant', content: fullResponse });
289
+ messages.push({ role: 'user', content: 'Tool result: ' + toolResult });
290
+ }
292
291
  }
293
- }
294
292
 
295
- console.log(chalk.hex('#FFB800')('\u26a0 Max tool calls reached. Stopping.'));
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
  }
@@ -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
- if (args.length === 0) {
7
- console.log(chalk.hex('#54A0FF')('Current model: ') + chalk.white(config.provider + '/' + config.model));
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 modelName = args[0];
66
+ const requested = args.join(' ').trim();
12
67
 
13
- const prov = getProvider(modelName);
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 config;
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 = modelName;
97
+ config.model = requested;
23
98
  saveConfig(config);
24
- console.log(chalk.hex('#00FF9F')('Model set to: ') + chalk.white(modelName));
25
- return config;
99
+ console.log(chalk.hex('#00FF9F')('\u2714 Model set to: ') + chalk.white(requested));
26
100
  }
@@ -8,7 +8,7 @@ const CONFIG_FILE = join(CONFIG_DIR, 'config.json');
8
8
  const MEMORY_FILE = join(CONFIG_DIR, 'memory.json');
9
9
 
10
10
  const DEFAULTS = {
11
- version: '3.0.0',
11
+ version: '3.3.0',
12
12
  provider: 'groq',
13
13
  model: 'llama-3.3-70b-versatile',
14
14
  apiKeys: {},
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 { getPrompt, drawPromptBox, SlashPalette } from './ui/prompt.js';
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
- const { top } = drawPromptBox(config.provider, config.model, config.agentMode);
15
- console.log(chalk.hex('#00FF9F')('\u2714 ') + chalk.white('CLARITY-AI v' + config.version + ' interactive session started.') +
16
- chalk.dim(' Type /help for commands. Ctrl+C to exit.'));
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
- const { emitKeypressEvents } = await import('readline');
21
- emitKeypressEvents(process.stdin);
22
- if (process.stdin.isTTY) process.stdin.setRawMode(true);
23
-
24
- let currentInput = '';
25
-
26
- function drawPromptLine() {
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
- function clearAndPrompt() {
33
- console.log();
34
- drawPromptLine();
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
- process.stdin.on('keypress', async (str, key) => {
38
- if (!key) return;
36
+ rl.setPrompt(makePrompt());
37
+ rl.prompt();
39
38
 
40
- if (key.ctrl && key.name === 'c') {
41
- if (slashMode) {
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 (key.ctrl && key.name === 'l') {
54
- process.stdout.write('\x1Bc');
55
- await showBanner(config.version, config.provider, config.model);
56
- drawPromptLine();
42
+ if (!input) {
43
+ rl.setPrompt(makePrompt());
44
+ rl.prompt();
57
45
  return;
58
46
  }
59
47
 
60
- if (key.ctrl && key.name === 'u') {
61
- currentInput = '';
62
- slashMode = false;
63
- slashBuffer = '';
64
- drawPromptLine();
65
- return;
66
- }
48
+ console.log();
49
+ console.log(renderUser(input));
50
+ console.log();
67
51
 
68
- if (key.name === 'escape') {
69
- if (slashMode) {
70
- palette.hide();
71
- slashMode = false;
72
- slashBuffer = '';
73
- currentInput = '';
74
- drawPromptLine();
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 (slashMode) {
80
- if (key.name === 'up') {
81
- palette.selectPrev();
82
- return;
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
- if (key.name === 'backspace') {
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
- clearAndPrompt();
147
- return;
76
+ } catch (err) {
77
+ console.log(chalk.hex('#FF4757')('\u2716 ') + chalk.hex('#FF4757')(err.message));
148
78
  }
149
79
 
150
- if (str === '/' && currentInput === '') {
151
- slashMode = true;
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
- console.log(top);
167
- drawPromptLine();
84
+ rl.on('SIGINT', () => {
85
+ console.log('\n' + chalk.hex('#00FF9F')('\u2714 ') + chalk.dim('Goodbye.'));
86
+ process.exit(0);
87
+ });
168
88
  }
@@ -1,61 +1,35 @@
1
- const PROVIDER = {
2
- name: 'anthropic',
3
- free: false,
4
- streaming: true,
5
- models: ['claude-3-5-haiku-20241022', 'claude-3-5-sonnet-20241022'],
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(url, {
17
+ const res = await fetch('https://api.anthropic.com/v1/messages', {
24
18
  method: 'POST',
25
- headers: { 'x-api-key': apiKey, 'anthropic-version': '2023-06-01', 'Content-Type': 'application/json' },
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) throw new Error(`Anthropic API error: ${res.status} ${res.statusText}`);
30
-
31
- if (!stream) {
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 reader = res.body.getReader();
38
- const decoder = new TextDecoder();
39
- let buffer = '';
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
- const PROVIDER = {
2
- name: 'deepseek',
3
- free: false,
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
- async function* sendMessage(apiKey, messages, model = 'deepseek-chat', stream = true) {
11
- const url = `${PROVIDER.baseURL}/chat/completions`;
12
- const body = { model, messages, stream };
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
- const res = await fetch(url, {
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: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json' },
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) throw new Error(`DeepSeek API error: ${res.status} ${res.statusText}`);
21
-
22
- if (!stream) {
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 reader = res.body.getReader();
29
- const decoder = new TextDecoder();
30
- let buffer = '';
31
-
32
- while (true) {
33
- const { done, value } = await reader.read();
34
- if (done) break;
35
- buffer += decoder.decode(value, { stream: true });
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 };
@@ -1,48 +1,35 @@
1
- const PROVIDER = {
2
- name: 'gemini',
3
- free: true,
4
- streaming: true,
5
- models: ['gemini-1.5-flash', 'gemini-1.5-pro', 'gemini-2.0-flash-exp'],
6
- baseURL: 'https://generativelanguage.googleapis.com/v1beta',
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({ contents }),
24
+ body: JSON.stringify(body),
21
25
  });
22
26
 
23
- if (!res.ok) throw new Error(`Gemini API error: ${res.status} ${res.statusText}`);
24
-
25
- const reader = res.body.getReader();
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
- export { PROVIDER, sendMessage };
32
+ const data = await res.json();
33
+ const text = data.candidates?.[0]?.content?.parts?.[0]?.text || '';
34
+ return { content: text, tool_calls: null };
35
+ }
@@ -1,56 +1,99 @@
1
- const PROVIDER = {
2
- name: 'groq',
3
- free: true,
4
- streaming: true,
5
- models: ['llama3-70b-8192', 'llama3-8b-8192', 'mixtral-8x7b-32768', 'gemma2-9b-it'],
6
- baseURL: 'https://api.groq.com/openai/v1',
7
- };
8
-
9
- async function* sendMessage(apiKey, messages, model = 'llama3-70b-8192', stream = true) {
10
- const url = `${PROVIDER.baseURL}/chat/completions`;
11
- const body = { model, messages, stream, max_tokens: 4096 };
12
-
13
- const res = await fetch(url, {
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: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json' },
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
- let detail = '';
21
- try { const e = await res.json(); detail = e.error?.message || JSON.stringify(e); } catch { detail = res.statusText; }
22
- throw new Error(`Groq API error: ${res.status} — ${detail}`);
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
- if (!stream) {
26
- const data = await res.json();
27
- yield data.choices[0].message.content;
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 reader = res.body.getReader();
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
- export { PROVIDER, sendMessage };
95
+ return {
96
+ content: message.content || '',
97
+ tool_calls: message.tool_calls || null,
98
+ };
99
+ }
@@ -1,36 +1,53 @@
1
- import { sendMessage as groqSend } from './groq.js';
2
- import { sendMessage as geminiSend } from './gemini.js';
3
- import { sendMessage as openrouterSend } from './openrouter.js';
4
- import { sendMessage as openaiSend } from './openai.js';
5
- import { sendMessage as anthropicSend } from './claude.js';
6
- import { sendMessage as deepseekSend } from './deepseek.js';
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-sonnet-20241022', keyUrl: 'https://console.anthropic.com/settings/keys' },
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 senders = { groq: groqSend, gemini: geminiSend, openrouter: openrouterSend, openai: openaiSend, anthropic: anthropicSend, deepseek: deepseekSend };
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
- export function streamProvider(config, messages) {
20
- const provider = config.provider || 'groq';
21
- const model = config.model || 'llama-3.3-70b-versatile';
22
- const apiKey = config.apiKeys?.[provider] || process.env[provider.toUpperCase() + '_API_KEY'] || '';
23
- const sender = senders[provider];
24
- if (!sender) throw new Error('Unknown provider: ' + provider);
25
- return sender(apiKey, messages, model, true);
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
- export async function callProvider(config, messages) {
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) {
@@ -1,52 +1,47 @@
1
- const PROVIDER = {
2
- name: 'openai',
3
- free: false,
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
- async function* sendMessage(apiKey, messages, model = 'gpt-4o-mini', stream = true) {
10
- const url = `${PROVIDER.baseURL}/chat/completions`;
11
- const body = { model, messages, stream };
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
- const res = await fetch(url, {
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: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json' },
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) throw new Error(`OpenAI API error: ${res.status} ${res.statusText}`);
20
-
21
- if (!stream) {
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 reader = res.body.getReader();
28
- const decoder = new TextDecoder();
29
- let buffer = '';
30
-
31
- while (true) {
32
- const { done, value } = await reader.read();
33
- if (done) break;
34
- buffer += decoder.decode(value, { stream: true });
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
- const PROVIDER = {
2
- name: 'openrouter',
3
- free: true,
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
- async function* sendMessage(apiKey, messages, model = 'meta-llama/llama-3.1-8b-instruct:free', stream = true) {
10
- const url = `${PROVIDER.baseURL}/chat/completions`;
11
- const body = { model, messages, stream };
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
- const res = await fetch(url, {
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: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json', 'HTTP-Referer': 'https://clarity-ai.local', 'X-Title': 'CLARITY AI' },
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) throw new Error(`OpenRouter API error: ${res.status} ${res.statusText}`);
20
-
21
- if (!stream) {
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 reader = res.body.getReader();
28
- const decoder = new TextDecoder();
29
- let buffer = '';
30
-
31
- while (true) {
32
- const { done, value } = await reader.read();
33
- if (done) break;
34
- buffer += decoder.decode(value, { stream: true });
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.0.0', provider = 'groq', model = 'llama-3.3-70b') {
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));