clarity-ai 1.1.1 → 1.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clarity-ai",
3
- "version": "1.1.1",
3
+ "version": "1.3.0",
4
4
  "description": "AI agent CLI for Termux and terminal — chat, code, automate",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -17,25 +17,14 @@
17
17
  "dependencies": {
18
18
  "chalk": "^5.3.0",
19
19
  "inquirer": "^9.2.12",
20
- "node-fetch": "^3.3.2",
21
20
  "conf": "^12.0.0",
22
21
  "marked": "^12.0.0",
23
22
  "marked-terminal": "^7.1.0",
24
- "ora": "^8.0.1",
25
- "boxen": "^7.1.1",
26
23
  "figlet": "^1.7.0",
27
24
  "gradient-string": "^2.0.2",
28
25
  "cli-table3": "^0.6.3",
29
- "commander": "^12.0.0",
30
- "dotenv": "^16.4.5",
31
- "axios": "^1.6.8",
32
- "fs-extra": "^11.2.0",
33
- "glob": "^10.3.12",
34
- "open": "^10.1.0",
35
- "update-notifier": "^7.0.0",
36
26
  "semver": "^7.6.0",
37
- "strip-ansi": "^7.1.0",
38
- "wrap-ansi": "^9.0.0"
27
+ "glob": "^10.3.12"
39
28
  },
40
29
  "keywords": [
41
30
  "ai",
@@ -1,6 +1,5 @@
1
1
  import figlet from 'figlet';
2
2
  import gradient from 'gradient-string';
3
- import boxen from 'boxen';
4
3
  import chalk from 'chalk';
5
4
 
6
5
  const GRADIENT = gradient(['#00d2ff', '#7b2ff7']);
@@ -14,40 +13,42 @@ try {
14
13
 
15
14
  const nodeVersion = process.versions.node;
16
15
  const [major] = nodeVersion.split('.').map(Number);
16
+ const W = process.stdout.columns || 80;
17
+ const line = '═'.repeat(W - 4);
17
18
 
18
19
  if (major < 18) {
19
- console.log(boxen(
20
- chalk.hex('#ff4466').bold(`Node.js 18+ required. You have v${nodeVersion}`) + '\n' +
21
- chalk.hex('#ffcc00')('Termux: pkg install nodejs-lts'),
22
- { padding: 1, borderColor: 'red', borderStyle: 'round' }
23
- ));
20
+ console.error();
21
+ console.error(chalk.hex('#ff4466')(` ╔${line}╗`));
22
+ console.error(chalk.hex('#ff4466')(` ║ ✗ Node.js 18+ required. You have v${nodeVersion}`));
23
+ console.error(chalk.hex('#ffcc00')(` ║ Termux: pkg install nodejs-lts`));
24
+ console.error(chalk.hex('#ff4466')(` ╚${line}╝`));
25
+ console.error();
24
26
  process.exit(1);
25
27
  }
26
28
 
27
29
  const isTermux = process.env.PREFIX?.includes('com.termux');
28
30
 
29
- console.log(boxen(
30
- chalk.hex('#00ff88').bold('CLARITY installed successfully!') + '\n' +
31
- chalk.hex('#00d2ff')('Run: clarity init'),
32
- { padding: 1, borderColor: 'green', borderStyle: 'round' }
33
- ));
34
-
35
- console.log(chalk.hex('#666888')("Run 'clarity init' to configure your AI keys and get started."));
31
+ console.log();
32
+ console.log(chalk.hex('#00ff88')(` ╔${line}╗`));
33
+ console.log(chalk.hex('#00ff88')(` ║ ✓ CLARITY installed successfully!`));
34
+ console.log(chalk.hex('#00d2ff')(` ║ Run: clarity init`));
35
+ console.log(chalk.hex('#00ff88')(` ╚${line}╝`));
36
+ console.log();
37
+ console.log(chalk.hex('#666888')(" Run 'clarity init' to configure your AI keys and get started."));
36
38
 
37
39
  const binDir = process.env.npm_config_prefix ? `${process.env.npm_config_prefix}/bin` : null;
38
40
  if (isTermux && binDir) {
39
- const { existsSync, writeFileSync, chmodSync, unlinkSync } = await import('fs');
41
+ const { existsSync, writeFileSync, chmodSync, unlinkSync, lstatSync } = await import('fs');
40
42
  const { resolve } = await import('path');
41
43
  const wrapperPath = resolve(binDir, 'clarity');
42
- const targetPath = resolve(process.argv[1] || '.', '..', 'bin', 'clarity.js');
43
44
 
44
45
  try {
45
- const stat = await import('fs').then(fs => fs.lstatSync(wrapperPath));
46
+ const stat = lstatSync(wrapperPath);
46
47
  if (stat.isSymbolicLink()) {
47
48
  unlinkSync(wrapperPath);
48
49
  writeFileSync(wrapperPath, `#!/data/data/com.termux/files/usr/bin/bash\nexec node "${resolve(process.env.PREFIX || '/usr', 'lib/node_modules/clarity-ai/bin/clarity.js')}" "$@"\n`);
49
50
  chmodSync(wrapperPath, '755');
50
- console.log(chalk.hex('#00ff88')('✓ Termux wrapper created at:'), chalk.hex('#00d2ff')(wrapperPath));
51
+ console.log(chalk.hex('#00ff88')(' ✓ Termux wrapper created at:'), chalk.hex('#00d2ff')(wrapperPath));
51
52
  }
52
53
  } catch {}
53
54
  }
@@ -33,6 +33,7 @@ const commandRegistry = {
33
33
 
34
34
  case '/keys': return this.keys(args);
35
35
  case '/model': return this.model(args);
36
+ case '/provider': return this.provider(args);
36
37
  case '/config': return this.config(args);
37
38
  case '/theme': return this.theme(args);
38
39
 
@@ -147,7 +148,7 @@ const commandRegistry = {
147
148
 
148
149
  const categories = {
149
150
  'CHAT & AI': ['/chat', '/ask <q>', '/clear', '/history show|clear|export', '/memory show|add|clear'],
150
- 'CONFIGURATION': ['/init', '/keys set|list|remove|test', '/model set|list', '/config show|reset', '/theme set|list'],
151
+ 'CONFIGURATION': ['/init', '/keys set|list|remove|test', '/provider', '/model', '/config show|reset', '/theme set|list'],
151
152
  'FILES': ['/file create|read|delete|list|edit', '/bash <command>', '/code run|write|explain|fix|review|refactor|test|docs'],
152
153
  'WEB & SEARCH': ['/web <url>', '/search <query>', '/translate <text>', '/summarize <file|url>'],
153
154
  'AGENTS & TOOLS': ['/agent start|stop|list|logs', '/tools list|run'],
@@ -232,26 +233,72 @@ const commandRegistry = {
232
233
 
233
234
  async model(args) {
234
235
  const sub = args[0];
235
- if (!sub) { blocks.warn('Usage', '/model list|set <provider/model>'); return; }
236
+ if (sub === 'set') {
237
+ if (args.length < 2) { blocks.warn('Usage', '/model set <provider/model>'); return; }
238
+ settings.set('defaultModel', args[1]);
239
+ blocks.success('Model Set', `Default model: ${args[1]}`);
240
+ return;
241
+ }
236
242
 
237
- switch (sub) {
238
- case 'list': {
239
- const configured = Object.keys(listKeys());
240
- if (configured.length === 0) { blocks.info('No Keys', 'Configure keys first with /init'); return; }
241
- for (const p of configured) {
242
- const models = listModels(p);
243
- blocks.info(p, models.map(m => `${p}/${m}`).join('\n'));
244
- }
245
- break;
246
- }
247
- case 'set': {
248
- if (args.length < 2) { blocks.warn('Usage', '/model set <provider/model>'); return; }
249
- settings.set('defaultModel', args[1]);
250
- blocks.success('Model Set', `Default model: ${args[1]}`);
251
- break;
252
- }
253
- default:
254
- blocks.warn('Usage', '/model list|set <provider/model>');
243
+ const configured = Object.keys(listKeys());
244
+ if (configured.length === 0) { blocks.info('No Keys', 'Configure keys first with /init'); return; }
245
+
246
+ const { default: inquirer } = await import('inquirer');
247
+ const choices = [];
248
+ for (const p of configured) {
249
+ const models = listModels(p);
250
+ models.forEach(m => {
251
+ choices.push({ name: `${p}/${m}`, value: `${p}/${m}` });
252
+ });
253
+ }
254
+
255
+ if (choices.length === 0) { blocks.warn('No Models', 'No models available for configured providers.'); return; }
256
+
257
+ const current = settings.get('defaultModel') || 'groq/llama-3.3-70b-versatile';
258
+ const { model } = await inquirer.prompt([{
259
+ type: 'list',
260
+ name: 'model',
261
+ message: 'Select AI model:',
262
+ pageSize: 15,
263
+ choices: choices.map(c => ({
264
+ ...c,
265
+ checked: c.value === current,
266
+ })),
267
+ }]);
268
+ settings.set('defaultModel', model);
269
+ blocks.success('Model Set', `Default model: ${model}`);
270
+ },
271
+
272
+ async provider(args) {
273
+ const configured = Object.keys(listKeys());
274
+ if (configured.length === 0) { blocks.info('No Keys', 'Configure keys first with /init'); return; }
275
+
276
+ const { default: inquirer } = await import('inquirer');
277
+ const { provider } = await inquirer.prompt([{
278
+ type: 'list',
279
+ name: 'provider',
280
+ message: 'Select AI provider:',
281
+ choices: configured.map(p => ({
282
+ name: PROVIDER_NAMES[p] || p,
283
+ value: p,
284
+ })),
285
+ }]);
286
+
287
+ const models = listModels(provider);
288
+ if (models.length > 0) {
289
+ const { model } = await inquirer.prompt([{
290
+ type: 'list',
291
+ name: 'model',
292
+ message: `Select ${PROVIDER_NAMES[provider]} model:`,
293
+ choices: models.map(m => ({
294
+ name: `${provider}/${m}`,
295
+ value: `${provider}/${m}`,
296
+ })),
297
+ }]);
298
+ settings.set('defaultModel', model);
299
+ blocks.success('Provider & Model Set', `${PROVIDER_NAMES[provider]} → ${model}`);
300
+ } else {
301
+ blocks.warn('No Models', `No models listed for ${PROVIDER_NAMES[provider]}`);
255
302
  }
256
303
  },
257
304
 
@@ -3,7 +3,7 @@ import paths from './paths.js';
3
3
 
4
4
  const schema = {
5
5
  theme: { type: 'string', default: 'dark' },
6
- defaultModel: { type: 'string', default: 'groq/llama3-70b-8192' },
6
+ defaultModel: { type: 'string', default: 'groq/llama-3.3-70b-versatile' },
7
7
  stream: { type: 'boolean', default: true },
8
8
  showTokens: { type: 'boolean', default: true },
9
9
  saveHistory: { type: 'boolean', default: true },
@@ -17,7 +17,7 @@ const settings = new Conf({
17
17
  cwd: paths.config,
18
18
  defaults: {
19
19
  theme: 'dark',
20
- defaultModel: 'groq/llama3-70b-8192',
20
+ defaultModel: 'groq/llama-3.3-70b-versatile',
21
21
  stream: true,
22
22
  showTokens: true,
23
23
  saveHistory: true,
@@ -8,15 +8,19 @@ const PROVIDER = {
8
8
 
9
9
  async function* sendMessage(apiKey, messages, model = 'llama3-70b-8192', stream = true) {
10
10
  const url = `${PROVIDER.baseURL}/chat/completions`;
11
- const body = { model, messages, stream };
12
-
11
+ const body = { model, messages, stream, max_tokens: 4096 };
12
+
13
13
  const res = await fetch(url, {
14
14
  method: 'POST',
15
15
  headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json' },
16
16
  body: JSON.stringify(body),
17
17
  });
18
18
 
19
- if (!res.ok) throw new Error(`Groq API error: ${res.status} ${res.statusText}`);
19
+ 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}`);
23
+ }
20
24
 
21
25
  if (!stream) {
22
26
  const data = await res.json();
@@ -34,7 +38,7 @@ async function* sendMessage(apiKey, messages, model = 'llama3-70b-8192', stream
34
38
  buffer += decoder.decode(value, { stream: true });
35
39
  const lines = buffer.split('\n');
36
40
  buffer = lines.pop() || '';
37
-
41
+
38
42
  for (const line of lines) {
39
43
  if (line.startsWith('data: ')) {
40
44
  const data = line.slice(6).trim();
@@ -10,12 +10,12 @@ const providers = { groq, gemini, deepseek, openrouter, openai, anthropic: claud
10
10
  const PRIORITY = ['groq', 'gemini', 'deepseek', 'openrouter', 'openai', 'anthropic'];
11
11
 
12
12
  const capabilities = {
13
- groq: { free: true, streaming: true, models: ['llama3-70b-8192', 'llama3-8b-8192', 'mixtral-8x7b-32768', 'gemma2-9b-it'], baseURL: 'https://api.groq.com/openai/v1' },
14
- gemini: { free: true, streaming: true, models: ['gemini-1.5-flash', 'gemini-1.5-pro', 'gemini-2.0-flash-exp'], baseURL: 'https://generativelanguage.googleapis.com/v1beta' },
15
- deepseek: { free: false, cheap: true, streaming: true, models: ['deepseek-chat', 'deepseek-coder'], baseURL: 'https://api.deepseek.com/v1' },
16
- openrouter: { free: true, streaming: true, models: ['meta-llama/llama-3.1-8b-instruct:free', 'google/gemma-2-9b-it:free', 'mistralai/mistral-7b-instruct:free'], baseURL: 'https://openrouter.ai/api/v1' },
17
- anthropic: { free: false, streaming: true, models: ['claude-3-5-haiku-20241022', 'claude-3-5-sonnet-20241022'], baseURL: 'https://api.anthropic.com/v1' },
18
- openai: { free: false, streaming: true, models: ['gpt-4o-mini', 'gpt-4o', 'gpt-3.5-turbo'], baseURL: 'https://api.openai.com/v1' },
13
+ groq: { free: true, streaming: true, models: ['llama-3.3-70b-versatile', 'llama-3.1-8b-instant', 'meta-llama/llama-4-scout-17b-16e-instruct', 'meta-llama/llama-4-maverick-17b-128e-instruct', 'mixtral-8x7b-32768', 'gemma2-9b-it', 'qwen/qwen3-32b', 'deepseek-r1-distill-llama-70b', 'moonshotai/kimi-k2-instruct'], baseURL: 'https://api.groq.com/openai/v1' },
14
+ gemini: { free: true, streaming: true, models: ['gemini-2.0-flash', 'gemini-2.5-pro-exp-03-25', 'gemini-1.5-flash', 'gemini-1.5-pro'], baseURL: 'https://generativelanguage.googleapis.com/v1beta' },
15
+ deepseek: { free: false, cheap: true, streaming: true, models: ['deepseek-chat', 'deepseek-coder', 'deepseek-reasoner'], baseURL: 'https://api.deepseek.com/v1' },
16
+ openrouter: { free: true, streaming: true, models: ['meta-llama/llama-3.3-70b-instruct:free', 'google/gemma-2-9b-it:free', 'mistralai/mistral-7b-instruct:free', 'deepseek/deepseek-chat:free', 'qwen/qwen-2.5-72b-instruct:free'], baseURL: 'https://openrouter.ai/api/v1' },
17
+ anthropic: { free: false, streaming: true, models: ['claude-3-5-haiku-latest', 'claude-3-5-sonnet-latest', 'claude-3-haiku'], baseURL: 'https://api.anthropic.com/v1' },
18
+ openai: { free: false, streaming: true, models: ['gpt-4o-mini', 'gpt-4o', 'gpt-4.1', 'o3-mini'], baseURL: 'https://api.openai.com/v1' },
19
19
  };
20
20
 
21
21
  function sendMessage(apiKey, messages, model, stream = true) {
package/src/ui/blocks.js CHANGED
@@ -1,74 +1,82 @@
1
1
  import cliTable3 from 'cli-table3';
2
2
  import c from './colors.js';
3
3
 
4
- function hr(char = '─', color = c.dim) {
5
- const w = process.stdout.columns || 80;
6
- console.log(color(char.repeat(w)));
7
- }
4
+ const W = () => process.stdout.columns || 80;
8
5
 
9
- function gap(n = 1) {
10
- console.log('\n'.repeat(n - 1));
6
+ function mid(text) {
7
+ const w = W();
8
+ const pad = Math.max(0, Math.floor((w - text.length) / 2));
9
+ return ' '.repeat(pad) + text;
11
10
  }
12
11
 
13
- const blocks = {
14
- info(title, msg) {
15
- gap();
16
- console.log(c.info(` ┌── ${title} ${''.repeat(Math.max(0, (process.stdout.columns || 80) - title.length - 10))}┐`));
17
- msg.split('\n').forEach(l => console.log(c.info(' │') + c.white(` ${l}`.padEnd((process.stdout.columns || 80) - 4)) + c.info('')));
18
- console.log(c.info(` └${'─'.repeat((process.stdout.columns || 80) - 4)}┘`));
19
- gap();
20
- },
12
+ const FILL = '░';
13
+ const FILL_BG = '\x1b[48;5;17m';
14
+ const FILL_BG_PURPLE = '\x1b[48;5;53m';
15
+ const FILL_BG_RED = '\x1b[48;5;52m';
16
+ const FILL_BG_GREEN = '\x1b[48;5;22m';
17
+ const FILL_BG_YELLOW = '\x1b[48;5;58m';
18
+ const FILL_BG_BLUE = '\x1b[48;5;19m';
19
+ const RESET = '\x1b[0m';
21
20
 
22
- success(title, msg) {
23
- gap();
24
- console.log(c.success(` ┌── ${title} ${''.repeat(Math.max(0, (process.stdout.columns || 80) - title.length - 10))}┐`));
25
- msg.split('\n').forEach(l => console.log(c.success(' │') + c.white(` ${l}`.padEnd((process.stdout.columns || 80) - 4)) + c.success('│')));
26
- console.log(c.success(` └${'─'.repeat((process.stdout.columns || 80) - 4)}┘`));
27
- gap();
28
- },
29
-
30
- warn(title, msg) {
31
- gap();
32
- console.log(c.warning(` ┌── ⚠ ${title} ${'─'.repeat(Math.max(0, (process.stdout.columns || 80) - title.length - 10))}┐`));
33
- msg.split('\n').forEach(l => console.log(c.warning(' │') + c.white(` ${l}`.padEnd((process.stdout.columns || 80) - 4)) + c.warning('│')));
34
- console.log(c.warning(` └${'─'.repeat((process.stdout.columns || 80) - 4)}┘`));
35
- gap();
36
- },
21
+ function shadeBox(color, title, msg) {
22
+ const w = W();
23
+ const line = ''.repeat(w - 4);
24
+ console.log();
25
+ console.log(color(` ┏${line}┓`));
26
+ console.log(color(` ┃${FILL_BG}${' '.repeat(w - 4)}${RESET}${color('┃')}`));
27
+ if (title) console.log(color(` ┃${FILL_BG} ${title}${' '.repeat(Math.max(0, w - 8 - title.length))}${RESET}${color('┃')}`));
28
+ msg.split('\n').forEach(l => console.log(color(` ┃${FILL_BG} ${c.white(l)}${' '.repeat(Math.max(0, w - 6 - l.length))}${RESET}${color('┃')}`)));
29
+ console.log(color(` ┃${FILL_BG}${' '.repeat(w - 4)}${RESET}${color('┃')}`));
30
+ console.log(color(` ┗${line}┛`));
31
+ console.log();
32
+ }
37
33
 
38
- error(title, msg) {
39
- gap();
40
- console.log(c.error(` ┌── ${title} ${'─'.repeat(Math.max(0, (process.stdout.columns || 80) - title.length - 10))}┐`));
41
- msg.split('\n').forEach(l => console.log(c.error(' │') + c.white(` ${l}`.padEnd((process.stdout.columns || 80) - 4)) + c.error('│')));
42
- console.log(c.error(` └${'─'.repeat((process.stdout.columns || 80) - 4)}┘`));
43
- gap();
44
- },
34
+ const blocks = {
35
+ info(title, msg) { shadeBox(c.info, `ℹ ${title}`, msg); },
36
+ success(title, msg) { shadeBox(c.success, `✓ ${title}`, msg); },
37
+ warn(title, msg) { shadeBox(c.warning, `⚠ ${title}`, msg); },
38
+ error(title, msg) { shadeBox(c.error, `✗ ${title}`, msg); },
45
39
 
46
40
  code(lang, code) {
47
- console.log(c.dim(` ┌─ ${lang} ${'─'.repeat(Math.max(0, (process.stdout.columns || 80) - lang.length - 8))}`));
41
+ const w = W();
42
+ console.log(c.dim(` ┌─ ${lang} ${'─'.repeat(Math.max(0, w - lang.length - 8))}`));
48
43
  code.split('\n').forEach(l => console.log(c.code(` │ ${l}`)));
49
- console.log(c.dim(` └${'─'.repeat((process.stdout.columns || 80) - 4)}`));
44
+ console.log(c.dim(` └${'─'.repeat(w - 4)}`));
50
45
  },
51
46
 
52
47
  file(path, content) {
48
+ const w = W();
53
49
  console.log(c.filename(` ┌─ ${path}`));
54
50
  content.split('\n').forEach(l => console.log(c.white(` │ ${l}`)));
55
- console.log(c.dim(` └${'─'.repeat((process.stdout.columns || 80) - 4)}`));
51
+ console.log(c.dim(` └${'─'.repeat(w - 4)}`));
56
52
  },
57
53
 
58
- ai(msg) {
59
- const w = process.stdout.columns || 80;
60
- console.log(c.accent(` ╔${'═'.repeat(w - 4)}╗`));
61
- msg.split('\n').forEach(l => console.log(c.accent(' ║') + c.ai(` ${l}`.padEnd(w - 4)) + c.accent('║')));
62
- console.log(c.accent(` ╚${'═'.repeat(w - 4)}╝`));
63
- gap();
54
+ ai(msg, title = 'CLARITY') {
55
+ const w = W();
56
+ const line = '═'.repeat(w - 4);
57
+ console.log();
58
+ console.log(c.accent(` ╔${line}╗`));
59
+ console.log(c.accent(` ║${FILL_BG_PURPLE}${' '.repeat(w - 4)}${RESET}${c.accent('║')}`));
60
+ if (title) console.log(c.accent(` ║${FILL_BG_PURPLE} ${c.ai(title)}${' '.repeat(Math.max(0, w - 8 - title.length))}${RESET}${c.accent('║')}`));
61
+ msg.split('\n').forEach(l => {
62
+ const clean = l.replace(/\x1b\[[0-9;]*m/g, '');
63
+ console.log(c.accent(` ║${FILL_BG_PURPLE} ${c.white(l)}${' '.repeat(Math.max(0, w - 6 - clean.length))}${RESET}${c.accent('║')}`));
64
+ });
65
+ console.log(c.accent(` ║${FILL_BG_PURPLE}${' '.repeat(w - 4)}${RESET}${c.accent('║')}`));
66
+ console.log(c.accent(` ╚${line}╝`));
67
+ console.log();
64
68
  },
65
69
 
66
70
  user(msg) {
67
- const w = process.stdout.columns || 80;
68
- console.log(c.primary(` ┌${'─'.repeat(w - 4)}┐`));
69
- msg.split('\n').forEach(l => console.log(c.primary(' │') + c.user(` ${l}`.padEnd(w - 4)) + c.primary('│')));
70
- console.log(c.primary(` └${'─'.repeat(w - 4)}┘`));
71
- gap();
71
+ const w = W();
72
+ const line = '─'.repeat(w - 4);
73
+ console.log();
74
+ console.log(c.primary(` ┌${line}┐`));
75
+ console.log(c.primary(` │${FILL_BG}${' '.repeat(w - 4)}${RESET}${c.primary('│')}`));
76
+ msg.split('\n').forEach(l => console.log(c.primary(` │${FILL_BG} ${c.user(l)}${' '.repeat(Math.max(0, w - 6 - l.length))}${RESET}${c.primary('│')}`)));
77
+ console.log(c.primary(` │${FILL_BG}${' '.repeat(w - 4)}${RESET}${c.primary('│')}`));
78
+ console.log(c.primary(` └${line}┘`));
79
+ console.log();
72
80
  },
73
81
 
74
82
  table(headers, rows) {
@@ -85,12 +93,12 @@ const blocks = {
85
93
  },
86
94
 
87
95
  divider(label) {
88
- const w = process.stdout.columns || 80;
96
+ const w = W();
89
97
  if (label) {
90
- const side = Math.floor((w - label.length - 4) / 2);
98
+ const side = Math.max(0, Math.floor((w - label.length - 4) / 2));
91
99
  console.log(c.dim(` ${'─'.repeat(side)} ${label} ${'─'.repeat(side)}`));
92
100
  } else {
93
- hr();
101
+ console.log(c.dim('─'.repeat(w)));
94
102
  }
95
103
  },
96
104
 
@@ -104,6 +112,18 @@ const blocks = {
104
112
  const filled = Math.round(w * pct / 100);
105
113
  const bar = c.primary('█'.repeat(filled)) + c.dim('░'.repeat(w - filled));
106
114
  console.log(` ${c.muted(label)} ${bar} ${c.white(String(pct))}%`);
115
+ },
116
+
117
+ cmdsuggest(cmds) {
118
+ const w = W();
119
+ console.log();
120
+ console.log(c.dim(` ${'─'.repeat(w - 4)}`));
121
+ cmds.forEach(([cmd, desc], i) => {
122
+ const tag = c.badge(cmd, i === 0 ? 'cyan' : 'purple');
123
+ console.log(` ${tag} ${c.muted(desc)}`);
124
+ });
125
+ console.log(c.dim(` ${'─'.repeat(w - 4)}`));
126
+ console.log();
107
127
  }
108
128
  };
109
129
 
package/src/ui/chatbox.js CHANGED
@@ -4,25 +4,55 @@ import settings from '../config/settings.js';
4
4
  import { hasAnyKeys } from '../config/keys.js';
5
5
  import { getKey, PROVIDER_NAMES } from '../config/keys.js';
6
6
  import { sendMessage } from '../providers/index.js';
7
- import { createPrompt, showPrompt, addToHistory, loadHistory } from './prompt.js';
7
+ import { createPrompt, addToHistory, loadHistory, ALL_COMMANDS } from './prompt.js';
8
8
  import blocks from './blocks.js';
9
9
  import spin from './spinner.js';
10
10
  import commandRegistry from '../commands/index.js';
11
11
  import memory from '../memory/store.js';
12
+ import { isTermux } from '../utils/termux.js';
13
+ import readline from 'readline';
14
+ import { readFileSync } from 'fs';
15
+
16
+ const PKG = JSON.parse(readFileSync(new URL('../../package.json', import.meta.url)));
17
+ const VERSION = PKG.version;
18
+ const RESET = '\x1b[0m';
12
19
 
13
20
  let rl = null;
14
21
  let conversation = [];
15
22
 
16
- function renderHeader() {
17
- const model = settings.get('defaultModel') || 'groq/llama3-70b-8192';
23
+ const SYSTEM_PROMPT = `You are CLARITY, an autonomous AI agent running in the user's terminal (Termux on Android).
24
+
25
+ You have FULL access to tools: bash execution, file operations, web search, code execution, git, and package management.
26
+ When the user asks something that requires action — run commands, create/edit files, search the web, execute code — DO IT AUTOMATICALLY using your tools. Do not ask for permission.
27
+
28
+ Be proactive. If the user asks to do a task, break it down and execute each step.
29
+ Keep responses concise and terminal-friendly. Use markdown for code blocks.
30
+
31
+ Available tools: bash, files (create/read/delete/list), web fetch, web search, code run (js/python/bash), git, pkg (npm/pip/pkg), system info
32
+
33
+ Current environment: ${process.platform} ${process.arch}, Node ${process.versions.node}
34
+ Directory: ${process.cwd()}
35
+ Termux: ${isTermux()}`;
36
+
37
+ const GREY_BG = '\x1b[48;5;236m';
38
+
39
+ function renderPromptBar() {
40
+ const model = settings.get('defaultModel') || 'groq/llama-3.3-70b-versatile';
18
41
  const w = process.stdout.columns || 80;
19
- const left = c.accent('◈');
20
- const mid = c.muted(` ${model} `);
21
- const right = c.muted('/help');
22
- const total = 6 + mid.length + right.length + 2;
23
- const pad = '.'.repeat(Math.max(0, w - total - 4));
24
- console.log(c.border(` ${left}${c.dim(pad)}${mid}${c.dim(pad)}${right}`));
25
- blocks.divider();
42
+ const line = '━'.repeat(w - 4);
43
+ const leftText = ` ${model} `;
44
+ const rightText = `v${VERSION} /help `;
45
+ const fill = Math.max(0, w - leftText.length - rightText.length - 8);
46
+
47
+ console.log(c.accent(` ┏${line}┓`));
48
+ console.log(c.accent(` ┃${GREY_BG}${' '.repeat(w - 4)}${RESET}${c.accent('┃')}`));
49
+ console.log(c.accent(` ┃${GREY_BG} ${c.muted(leftText)}${' '.repeat(fill)}${c.muted(rightText)}${RESET}${c.accent('┃')}`));
50
+ console.log(c.accent(` ┃${GREY_BG}${' '.repeat(w - 4)}${RESET}${c.accent('┃')}`));
51
+ console.log(c.accent(` ┗${line}┛`));
52
+ }
53
+
54
+ function showPrompt() {
55
+ process.stdout.write(` ${c.accent('◆')} `);
26
56
  }
27
57
 
28
58
  function startChat() {
@@ -34,16 +64,31 @@ function startChat() {
34
64
  loadHistory();
35
65
  console.clear();
36
66
  renderBanner();
37
- renderHeader();
38
67
  console.log();
39
-
40
- rl = createPrompt();
68
+ renderPromptBar();
41
69
  showPrompt();
70
+ rl = createPrompt();
71
+
72
+ let cmdBuffer = '';
73
+
74
+ process.stdin.on('keypress', (char, key) => {
75
+ if (key && key.name === 'slash' && !cmdBuffer) {
76
+ cmdBuffer = '/';
77
+ const input = rl.line || '';
78
+ readline.clearLine(process.stdout, 0);
79
+ readline.cursorTo(process.stdout, 0);
80
+ suggestCommands(input);
81
+ rl._refreshLine();
82
+ }
83
+ if (key && key.name === 'escape') {
84
+ if (cmdBuffer) { cmdBuffer = ''; }
85
+ }
86
+ });
42
87
 
43
88
  rl.on('line', async (line) => {
44
89
  const input = line.trim();
45
- if (!input) { showPrompt(); return; }
46
-
90
+ if (!input) { renderPromptBar(); showPrompt(); return; }
91
+ cmdBuffer = '';
47
92
  addToHistory(input);
48
93
 
49
94
  if (input.startsWith('/')) {
@@ -51,9 +96,9 @@ function startChat() {
51
96
  if (result?.exit) { closeChat(); return; }
52
97
  } else {
53
98
  conversation.push({ role: 'user', content: input });
54
- blocks.user(input);
55
99
  await handleAIResponse();
56
100
  }
101
+ renderPromptBar();
57
102
  showPrompt();
58
103
  });
59
104
 
@@ -64,12 +109,27 @@ function startChat() {
64
109
 
65
110
  rl.on('SIGINT', () => {
66
111
  console.log(c.muted('\nUse /exit to quit'));
112
+ renderPromptBar();
67
113
  showPrompt();
68
114
  });
69
115
  }
70
116
 
117
+ function suggestCommands(partial) {
118
+ const w = process.stdout.columns || 80;
119
+ const p = partial.replace('/', '');
120
+ const matches = p ? ALL_COMMANDS.filter(([cmd]) => cmd.startsWith(p)) : ALL_COMMANDS;
121
+ const top = matches.slice(0, 8);
122
+ if (top.length === 0) return;
123
+ console.log(c.dim(` ${'─'.repeat(w - 4)}`));
124
+ top.forEach(([cmd, desc], i) => {
125
+ const tag = blocks.badge('/' + cmd, i === 0 ? 'cyan' : 'purple');
126
+ console.log(` ${tag} ${c.muted(desc)}`);
127
+ });
128
+ console.log(c.dim(` ${'─'.repeat(w - 4)}`));
129
+ }
130
+
71
131
  async function handleAIResponse() {
72
- const model = settings.get('defaultModel') || 'groq/llama3-70b-8192';
132
+ const model = settings.get('defaultModel') || 'groq/llama-3.3-70b-versatile';
73
133
  const [provider] = model.split('/');
74
134
  const apiKey = getKey(provider);
75
135
 
@@ -78,8 +138,8 @@ async function handleAIResponse() {
78
138
  return;
79
139
  }
80
140
 
81
- const ctx = memory.getContext();
82
- const messages = [...ctx, ...conversation];
141
+ const systemMsg = { role: 'system', content: SYSTEM_PROMPT + '\n\nUser memory: ' + (memory.show().filter(m => m.role === 'system').map(m => m.content).join('; ') || 'none') };
142
+ const messages = [systemMsg, ...conversation];
83
143
 
84
144
  spin.start('CLARITY is thinking...');
85
145
 
@@ -89,27 +149,40 @@ async function handleAIResponse() {
89
149
  if (settings.get('stream')) {
90
150
  spin.stop();
91
151
  const w = process.stdout.columns || 80;
92
- console.log(c.accent(` ╔${''.repeat(w - 4)}╗`));
93
- process.stdout.write(c.accent(' ║ ') + c.ai('CLARITY ') + c.white(''));
152
+ const line = ''.repeat(w - 4);
153
+ const PURPLE_BG = '\x1b[48;5;53m';
154
+ console.log(c.accent(` ┏${line}┓`));
155
+ console.log(c.accent(` ┃${PURPLE_BG}${' '.repeat(w - 4)}${RESET}${c.accent('┃')}`));
156
+ process.stdout.write(c.accent(` ┃${PURPLE_BG} ${c.ai('CLARITY')} ${RESET}${c.white('')}`));
94
157
  let full = '';
95
- for await (const chunk of stream) {
96
- full += chunk;
97
- process.stdout.write(c.white(chunk));
158
+ try {
159
+ for await (const chunk of stream) {
160
+ full += chunk;
161
+ process.stdout.write(c.white(chunk));
162
+ }
163
+ } catch (streamErr) {
164
+ if (full) process.stdout.write(c.warning('\n\n[stream interrupted]'));
165
+ else throw streamErr;
98
166
  }
99
167
  console.log();
100
- console.log(c.accent(` ╚${''.repeat(w - 4)}╝`));
168
+ console.log(c.accent(` ┃${PURPLE_BG}${' '.repeat(w - 4)}${RESET}${c.accent('┃')}`));
169
+ console.log(c.accent(` ┗${line}┛`));
101
170
  console.log();
102
- conversation.push({ role: 'assistant', content: full });
103
- memory.add(conversation);
171
+ if (full.trim()) {
172
+ conversation.push({ role: 'assistant', content: full });
173
+ memory.add(conversation);
174
+ }
104
175
  } else {
105
176
  spin.stop();
106
177
  let full = '';
107
178
  for await (const chunk of stream) {
108
179
  full += chunk;
109
180
  }
110
- blocks.ai(full);
111
- conversation.push({ role: 'assistant', content: full });
112
- memory.add(conversation);
181
+ if (full.trim()) {
182
+ blocks.ai(full);
183
+ conversation.push({ role: 'assistant', content: full });
184
+ memory.add(conversation);
185
+ }
113
186
  }
114
187
 
115
188
  if (settings.get('showTokens')) {
package/src/ui/prompt.js CHANGED
@@ -2,9 +2,76 @@ import readline from 'readline';
2
2
  import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
3
3
  import { dirname } from 'path';
4
4
  import paths from '../config/paths.js';
5
- import c from './colors.js';
6
5
 
7
6
  const HISTORY_FILE = paths.history;
7
+ const ALL_COMMANDS = [
8
+ ['help', 'Show all commands'],
9
+ ['init', 'Setup wizard'],
10
+ ['chat', 'Start chat session'],
11
+ ['ask', 'One-shot question'],
12
+ ['clear', 'Clear screen'],
13
+ ['exit', 'Exit CLARITY'],
14
+ ['keys set', 'Set API key'],
15
+ ['keys list', 'List API keys'],
16
+ ['keys remove', 'Remove API key'],
17
+ ['keys test', 'Test API key'],
18
+ ['provider', 'Switch AI provider'],
19
+ ['model', 'Select AI model'],
20
+ ['model set', 'Set default model'],
21
+ ['model list', 'List models'],
22
+ ['config show', 'Show config'],
23
+ ['config reset', 'Reset config'],
24
+ ['theme set', 'Set theme'],
25
+ ['theme list', 'List themes'],
26
+ ['file create', 'Create file'],
27
+ ['file read', 'Read file'],
28
+ ['file delete', 'Delete file'],
29
+ ['file list', 'List directory'],
30
+ ['file edit', 'Edit file'],
31
+ ['bash', 'Run command'],
32
+ ['code run', 'Run code file'],
33
+ ['code write', 'AI write code'],
34
+ ['code explain', 'Explain code'],
35
+ ['code fix', 'Fix code'],
36
+ ['code review', 'Review code'],
37
+ ['code refactor', 'Refactor code'],
38
+ ['code test', 'Generate tests'],
39
+ ['code docs', 'Generate docs'],
40
+ ['web', 'Fetch URL'],
41
+ ['search', 'Web search'],
42
+ ['git', 'Git operations'],
43
+ ['pkg install', 'Install package'],
44
+ ['pkg remove', 'Remove package'],
45
+ ['agent start', 'Start agent'],
46
+ ['agent stop', 'Stop agent'],
47
+ ['agent list', 'List agents'],
48
+ ['agent logs', 'Show agent logs'],
49
+ ['tools list', 'List tools'],
50
+ ['tools run', 'Run tool'],
51
+ ['memory show', 'Show memory'],
52
+ ['memory add', 'Add memory'],
53
+ ['memory clear', 'Clear memory'],
54
+ ['history show', 'Show history'],
55
+ ['history clear', 'Clear history'],
56
+ ['history export', 'Export history'],
57
+ ['env show', 'Show env vars'],
58
+ ['env set', 'Set env var'],
59
+ ['status', 'System status'],
60
+ ['version', 'Version info'],
61
+ ['update', 'Check update'],
62
+ ['alias create', 'Create alias'],
63
+ ['alias list', 'List aliases'],
64
+ ['export', 'Export chat'],
65
+ ['import', 'Import chat'],
66
+ ['summarize', 'Summarize content'],
67
+ ['translate', 'Translate text'],
68
+ ['generate', 'Generate content'],
69
+ ['deploy', 'Deploy project'],
70
+ ['debug', 'Debug file'],
71
+ ['explain', 'Explain code'],
72
+ ['run', 'Run command'],
73
+ ];
74
+
8
75
  const MAX_HISTORY = 500;
9
76
  let history = [];
10
77
 
@@ -45,9 +112,4 @@ function createPrompt() {
45
112
  return rl;
46
113
  }
47
114
 
48
- function showPrompt() {
49
- const prefix = c.primary('◇');
50
- process.stdout.write(` ${prefix} `);
51
- }
52
-
53
- export { createPrompt, showPrompt, addToHistory, loadHistory, history };
115
+ export { createPrompt, addToHistory, loadHistory, history, ALL_COMMANDS };
@@ -1,18 +1,28 @@
1
1
  import chalk from 'chalk';
2
- import boxen from 'boxen';
3
2
  import semver from 'semver';
4
3
  import { isTermux } from './termux.js';
5
4
 
6
5
  const MIN_NODE = '18.0.0';
6
+ const W = () => process.stdout.columns || 80;
7
+ const line = '═'.repeat(W() - 4);
8
+
9
+ function simpleBox(colorFn, title, msg) {
10
+ console.error();
11
+ console.error(colorFn(` ╔${line}╗`));
12
+ if (title) console.error(colorFn(` ║ ${title}`));
13
+ msg.split('\n').forEach(l => console.error(colorFn(` ║ ${l}`)));
14
+ console.error(colorFn(` ╚${line}╝`));
15
+ console.error();
16
+ }
7
17
 
8
18
  function checkNodeVersion() {
9
19
  const current = process.versions.node;
10
20
  if (!semver.satisfies(current, `>=${MIN_NODE}`)) {
11
- console.error(boxen(
12
- chalk.hex('#ff4466').bold(`[ERROR] Node.js ${MIN_NODE}+ required. You have v${current}\n`) +
13
- chalk.hex('#ffcc00')(isTermux() ? 'Termux: pkg install nodejs-lts' : 'Install Node.js 18+ from https://nodejs.org'),
14
- { padding: 1, borderColor: 'red', borderStyle: 'round' }
15
- ));
21
+ simpleBox(
22
+ chalk.hex('#ff4466'),
23
+ `✗ Node.js ${MIN_NODE}+ required`,
24
+ `You have v${current}\n${isTermux() ? 'Termux: pkg install nodejs-lts' : 'Install Node.js 18+ from https://nodejs.org'}`
25
+ );
16
26
  process.exit(1);
17
27
  }
18
28
  }
@@ -22,11 +32,11 @@ async function checkDependencies(pkgDir) {
22
32
  const { resolve } = await import('path');
23
33
  const pkgPath = resolve(pkgDir, 'package.json');
24
34
  if (!existsSync(pkgPath)) return true;
25
-
35
+
26
36
  const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
27
37
  const deps = { ...pkg.dependencies, ...pkg.peerDependencies };
28
38
  const missing = [];
29
-
39
+
30
40
  for (const dep of Object.keys(deps || {})) {
31
41
  try {
32
42
  await import(dep);
@@ -34,13 +44,13 @@ async function checkDependencies(pkgDir) {
34
44
  missing.push(dep);
35
45
  }
36
46
  }
37
-
47
+
38
48
  if (missing.length > 0) {
39
- console.warn(boxen(
40
- chalk.hex('#ffcc00').bold('Missing dependencies: ') + missing.join(', ') + '\n' +
41
- chalk.dim('Auto-installing...'),
42
- { padding: 1, borderColor: 'yellow', borderStyle: 'round' }
43
- ));
49
+ simpleBox(
50
+ chalk.hex('#ffcc00'),
51
+ '⚠ Missing dependencies',
52
+ missing.join(', ')
53
+ );
44
54
  const { execSync } = await import('child_process');
45
55
  execSync('npm install', { cwd: pkgDir, stdio: 'inherit' });
46
56
  }
@@ -54,11 +64,11 @@ async function checkUpdates() {
54
64
  const res = await fetch('https://registry.npmjs.org/clarity-ai/latest');
55
65
  const data = await res.json();
56
66
  if (data.version && semver.gt(data.version, current)) {
57
- console.log(boxen(
58
- chalk.hex('#00d2ff').bold(`clarity-ai v${current} → v${data.version}\n`) +
59
- chalk.hex('#7b2ff7')('Run: npm install -g clarity-ai'),
60
- { padding: 1, borderColor: 'cyan', borderStyle: 'round' }
61
- ));
67
+ simpleBox(
68
+ chalk.hex('#00d2ff'),
69
+ 'Update Available',
70
+ `clarity-ai v${current} v${data.version}\nRun: npm install -g clarity-ai`
71
+ );
62
72
  }
63
73
  } catch {}
64
74
  }