clarity-ai 1.0.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.
Files changed (44) hide show
  1. package/README.md +108 -0
  2. package/bin/clarity.js +2 -0
  3. package/package.json +51 -0
  4. package/scripts/postinstall.js +53 -0
  5. package/src/agents/BaseAgent.js +54 -0
  6. package/src/agents/CodeAgent.js +57 -0
  7. package/src/agents/FileAgent.js +39 -0
  8. package/src/agents/MonitorAgent.js +54 -0
  9. package/src/agents/ShellAgent.js +31 -0
  10. package/src/agents/WebAgent.js +39 -0
  11. package/src/agents/manager.js +116 -0
  12. package/src/commands/index.js +725 -0
  13. package/src/config/keys.js +104 -0
  14. package/src/config/paths.js +22 -0
  15. package/src/config/settings.js +28 -0
  16. package/src/index.js +86 -0
  17. package/src/memory/context.js +38 -0
  18. package/src/memory/store.js +54 -0
  19. package/src/providers/claude.js +61 -0
  20. package/src/providers/deepseek.js +53 -0
  21. package/src/providers/gemini.js +48 -0
  22. package/src/providers/groq.js +52 -0
  23. package/src/providers/index.js +39 -0
  24. package/src/providers/openai.js +52 -0
  25. package/src/providers/openrouter.js +52 -0
  26. package/src/tools/bash.js +25 -0
  27. package/src/tools/code.js +52 -0
  28. package/src/tools/files.js +62 -0
  29. package/src/tools/git.js +67 -0
  30. package/src/tools/index.js +40 -0
  31. package/src/tools/pkg.js +46 -0
  32. package/src/tools/search.js +29 -0
  33. package/src/tools/system.js +29 -0
  34. package/src/tools/web.js +24 -0
  35. package/src/ui/banner.js +15 -0
  36. package/src/ui/blocks.js +55 -0
  37. package/src/ui/chatbox.js +126 -0
  38. package/src/ui/colors.js +22 -0
  39. package/src/ui/prompt.js +49 -0
  40. package/src/ui/spinner.js +43 -0
  41. package/src/utils/logger.js +40 -0
  42. package/src/utils/markdown.js +25 -0
  43. package/src/utils/termux.js +38 -0
  44. package/src/utils/version-check.js +66 -0
@@ -0,0 +1,52 @@
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
+ };
8
+
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 };
12
+
13
+ const res = await fetch(url, {
14
+ method: 'POST',
15
+ headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json', 'HTTP-Referer': 'https://clarity-ai.local', 'X-Title': 'CLARITY AI' },
16
+ body: JSON.stringify(body),
17
+ });
18
+
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;
25
+ }
26
+
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
+ }
50
+ }
51
+
52
+ export { PROVIDER, sendMessage };
@@ -0,0 +1,25 @@
1
+ import { exec } from 'child_process';
2
+ import { promisify } from 'util';
3
+
4
+ const execAsync = promisify(exec);
5
+ const DANGEROUS = ['rm -rf', 'sudo ', 'dd if=', 'mkfs.', ':(){ :|:& };:'];
6
+
7
+ async function execute(args) {
8
+ const command = args.command || args;
9
+ if (typeof command !== 'string') throw new Error('Command must be a string');
10
+
11
+ const isDangerous = DANGEROUS.some(d => command.includes(d));
12
+ if (isDangerous) {
13
+ return { stdout: '', stderr: '', exitCode: -1, duration: 0, warning: 'Potentially dangerous command blocked' };
14
+ }
15
+
16
+ const start = Date.now();
17
+ try {
18
+ const { stdout, stderr } = await execAsync(command, { timeout: 30000 });
19
+ return { stdout: stdout.trim(), stderr: stderr.trim(), exitCode: 0, duration: Date.now() - start };
20
+ } catch (err) {
21
+ return { stdout: err.stdout?.trim() || '', stderr: err.stderr?.trim() || err.message, exitCode: err.code || 1, duration: Date.now() - start };
22
+ }
23
+ }
24
+
25
+ export { execute };
@@ -0,0 +1,52 @@
1
+ import { exec } from 'child_process';
2
+ import { promisify } from 'util';
3
+ import { writeFileSync, unlinkSync, mkdtempSync } from 'fs';
4
+ import { resolve } from 'path';
5
+
6
+ const execAsync = promisify(exec);
7
+
8
+ async function execute(args) {
9
+ const code = args.code || args;
10
+ const language = args.language || 'javascript';
11
+ const start = Date.now();
12
+
13
+ try {
14
+ if (language === 'javascript' || language === 'js') {
15
+ const result = await evalCode(code);
16
+ return { output: result, error: null, duration: Date.now() - start, language };
17
+ } else if (language === 'python' || language === 'py') {
18
+ const tmpDir = mkdtempSync('/tmp/clarity_');
19
+ const tmpFile = resolve(tmpDir, 'script.py');
20
+ writeFileSync(tmpFile, code);
21
+ try {
22
+ const { stdout, stderr } = await execAsync(`python3 "${tmpFile}"`, { timeout: 15000 });
23
+ return { output: stdout.trim(), error: stderr.trim() || null, duration: Date.now() - start, language };
24
+ } finally {
25
+ try { unlinkSync(tmpFile); } catch {}
26
+ }
27
+ } else if (language === 'bash' || language === 'sh') {
28
+ const tmpDir = mkdtempSync('/tmp/clarity_');
29
+ const tmpFile = resolve(tmpDir, 'script.sh');
30
+ writeFileSync(tmpFile, code);
31
+ try {
32
+ const { stdout, stderr } = await execAsync(`bash "${tmpFile}"`, { timeout: 15000 });
33
+ return { output: stdout.trim(), error: stderr.trim() || null, duration: Date.now() - start, language };
34
+ } finally {
35
+ try { unlinkSync(tmpFile); } catch {}
36
+ }
37
+ } else {
38
+ throw new Error(`Unsupported language: ${language}`);
39
+ }
40
+ } catch (err) {
41
+ return { output: null, error: err.message, duration: Date.now() - start, language };
42
+ }
43
+ }
44
+
45
+ async function evalCode(code) {
46
+ const AsyncFunction = Object.getPrototypeOf(async function(){}).constructor;
47
+ const fn = new AsyncFunction('"use strict"; ' + code);
48
+ const result = await fn();
49
+ return result !== undefined ? String(result) : 'undefined';
50
+ }
51
+
52
+ export { execute };
@@ -0,0 +1,62 @@
1
+ import { promises as fs } from 'fs';
2
+ import { resolve, dirname, basename } from 'path';
3
+ import { glob } from 'glob';
4
+
5
+ async function create(args) {
6
+ const filePath = resolve(args.path);
7
+ const content = args.content || '';
8
+ await fs.mkdir(dirname(filePath), { recursive: true });
9
+ await fs.writeFile(filePath, content, 'utf8');
10
+ return { success: true, path: filePath, size: content.length };
11
+ }
12
+
13
+ async function read(args) {
14
+ const filePath = resolve(args.path);
15
+ const content = await fs.readFile(filePath, 'utf8');
16
+ const stat = await fs.stat(filePath);
17
+ return { content, size: content.length, lines: content.split('\n').length, modified: stat.mtime };
18
+ }
19
+
20
+ async function del(args) {
21
+ const filePath = resolve(args.path);
22
+ await fs.unlink(filePath);
23
+ return { success: true, path: filePath };
24
+ }
25
+
26
+ async function list(args) {
27
+ const dir = resolve(args.path || '.');
28
+ const pattern = args.pattern || '*';
29
+ const files = await glob(pattern, { cwd: dir, nodir: false });
30
+ const details = await Promise.all(files.map(async (f) => {
31
+ try {
32
+ const stat = await fs.stat(resolve(dir, f));
33
+ return { name: f, size: stat.size, isDir: stat.isDirectory(), modified: stat.mtime };
34
+ } catch { return { name: f, size: 0, isDir: false }; }
35
+ }));
36
+ return { directory: dir, files: details };
37
+ }
38
+
39
+ async function copy(args) {
40
+ const src = resolve(args.source);
41
+ const dest = resolve(args.destination);
42
+ await fs.mkdir(dirname(dest), { recursive: true });
43
+ await fs.copyFile(src, dest);
44
+ return { success: true, source: src, destination: dest };
45
+ }
46
+
47
+ async function move(args) {
48
+ const src = resolve(args.source);
49
+ const dest = resolve(args.destination);
50
+ await fs.mkdir(dirname(dest), { recursive: true });
51
+ await fs.rename(src, dest);
52
+ return { success: true, source: src, destination: dest };
53
+ }
54
+
55
+ async function stat(args) {
56
+ const filePath = resolve(args.path);
57
+ const s = await fs.stat(filePath);
58
+ return { path: filePath, size: s.size, isDir: s.isDirectory(), isFile: s.isFile(), modified: s.mtime, created: s.birthtime, mode: s.mode };
59
+ }
60
+
61
+ const tools = { create, read, delete: del, list, copy, move, stat };
62
+ export default tools;
@@ -0,0 +1,67 @@
1
+ import { exec } from 'child_process';
2
+ import { promisify } from 'util';
3
+
4
+ const execAsync = promisify(exec);
5
+
6
+ async function execute(args) {
7
+ const action = args.action || args;
8
+ const repo = args.repo || '';
9
+ const message = args.message || 'update';
10
+
11
+ if (typeof action === 'string') {
12
+ let cmd = '';
13
+ switch (action) {
14
+ case 'status': cmd = 'git status'; break;
15
+ case 'diff': cmd = 'git diff'; break;
16
+ case 'log': cmd = 'git log --oneline -10'; break;
17
+ case 'branch': cmd = 'git branch -a'; break;
18
+ default: cmd = `git ${action}`;
19
+ }
20
+ const start = Date.now();
21
+ try {
22
+ const { stdout, stderr } = await execAsync(cmd, { timeout: 30000 });
23
+ return { stdout: stdout.trim(), stderr: stderr.trim(), exitCode: 0, duration: Date.now() - start };
24
+ } catch (err) {
25
+ return { stdout: '', stderr: err.message, exitCode: 1, duration: Date.now() - start };
26
+ }
27
+ }
28
+
29
+ const start = Date.now();
30
+ try {
31
+ let result;
32
+ switch (action) {
33
+ case 'add': {
34
+ const { stdout, stderr } = await execAsync('git add -A', { timeout: 10000 });
35
+ result = { stdout, stderr };
36
+ break;
37
+ }
38
+ case 'commit': {
39
+ const { stdout, stderr } = await execAsync(`git commit -m "${message}"`, { timeout: 10000 });
40
+ result = { stdout, stderr };
41
+ break;
42
+ }
43
+ case 'push': {
44
+ const { stdout, stderr } = await execAsync('git push', { timeout: 60000 });
45
+ result = { stdout, stderr };
46
+ break;
47
+ }
48
+ case 'pull': {
49
+ const { stdout, stderr } = await execAsync('git pull', { timeout: 60000 });
50
+ result = { stdout, stderr };
51
+ break;
52
+ }
53
+ case 'clone': {
54
+ const { stdout, stderr } = await execAsync(`git clone ${repo}`, { timeout: 120000 });
55
+ result = { stdout, stderr };
56
+ break;
57
+ }
58
+ default:
59
+ throw new Error(`Unknown git action: ${action}`);
60
+ }
61
+ return { ...result, exitCode: 0, duration: Date.now() - start };
62
+ } catch (err) {
63
+ return { stdout: '', stderr: err.message, exitCode: 1, duration: Date.now() - start };
64
+ }
65
+ }
66
+
67
+ export { execute };
@@ -0,0 +1,40 @@
1
+ import * as bash from './bash.js';
2
+ import files from './files.js';
3
+ import * as web from './web.js';
4
+ import * as search from './search.js';
5
+ import * as code from './code.js';
6
+ import * as git from './git.js';
7
+ import * as pkg from './pkg.js';
8
+ import * as system from './system.js';
9
+
10
+ const toolRegistry = {
11
+ bash: { name: 'bash', description: 'Execute shell commands', category: 'shell', execute: bash.execute, schema: { command: 'string' } },
12
+ files: { name: 'files', description: 'File operations (create/read/delete/list/copy/move/stat)', category: 'file', execute: (args) => {
13
+ const op = args.operation || 'list';
14
+ return files[op] ? files[op](args) : files.list(args);
15
+ }, schema: { operation: 'string', path: 'string' } },
16
+ web: { name: 'web', description: 'Fetch and display web pages', category: 'web', execute: web.execute, schema: { url: 'string' } },
17
+ search: { name: 'search', description: 'Search the web via DuckDuckGo', category: 'web', execute: search.execute, schema: { query: 'string' } },
18
+ code: { name: 'code', description: 'Execute code (js/python/bash)', category: 'code', execute: code.execute, schema: { code: 'string', language: 'string' } },
19
+ git: { name: 'git', description: 'Git operations', category: 'git', execute: git.execute, schema: { action: 'string', repo: 'string', message: 'string' } },
20
+ pkg: { name: 'pkg', description: 'Package manager (npm/pip/pkg)', category: 'system', execute: pkg.execute, schema: { action: 'string', name: 'string', manager: 'string' } },
21
+ system: { name: 'system', description: 'System information', category: 'system', execute: system.execute, schema: {} },
22
+ };
23
+
24
+ function getTool(name) {
25
+ return toolRegistry[name] || null;
26
+ }
27
+
28
+ function listTools() {
29
+ return Object.entries(toolRegistry).map(([name, t]) => ({
30
+ name, description: t.description, category: t.category
31
+ }));
32
+ }
33
+
34
+ async function runTool(name, args) {
35
+ const tool = toolRegistry[name];
36
+ if (!tool) throw new Error(`Unknown tool: ${name}`);
37
+ return await tool.execute(args);
38
+ }
39
+
40
+ export { getTool, listTools, runTool, toolRegistry };
@@ -0,0 +1,46 @@
1
+ import { exec } from 'child_process';
2
+ import { promisify } from 'util';
3
+ import { isTermux } from '../utils/termux.js';
4
+
5
+ const execAsync = promisify(exec);
6
+
7
+ async function execute(args) {
8
+ const action = args.action || args;
9
+ const name = args.name || args.package || '';
10
+ const manager = args.manager || (isTermux() ? 'pkg' : 'npm');
11
+ const start = Date.now();
12
+
13
+ try {
14
+ let cmd;
15
+ if (manager === 'npm') {
16
+ switch (action) {
17
+ case 'install': cmd = `npm install ${name}`; break;
18
+ case 'remove': cmd = `npm uninstall ${name}`; break;
19
+ case 'search': cmd = `npm search ${name}`; break;
20
+ case 'list': cmd = 'npm list --depth=0'; break;
21
+ default: throw new Error(`Unknown action: ${action}`);
22
+ }
23
+ } else if (manager === 'pip') {
24
+ switch (action) {
25
+ case 'install': cmd = `pip install ${name}`; break;
26
+ case 'remove': cmd = `pip uninstall -y ${name}`; break;
27
+ case 'search': cmd = `pip search ${name}`; break;
28
+ case 'list': cmd = 'pip list'; break;
29
+ default: throw new Error(`Unknown action: ${action}`);
30
+ }
31
+ } else if (manager === 'pkg' && isTermux()) {
32
+ switch (action) {
33
+ case 'install': cmd = `pkg install -y ${name}`; break;
34
+ case 'remove': cmd = `pkg uninstall -y ${name}`; break;
35
+ default: throw new Error(`Unknown action: ${action}`);
36
+ }
37
+ }
38
+
39
+ const { stdout, stderr } = await execAsync(cmd, { timeout: 120000 });
40
+ return { stdout: stdout.trim(), stderr: stderr.trim(), exitCode: 0, duration: Date.now() - start, manager };
41
+ } catch (err) {
42
+ return { stdout: '', stderr: err.message, exitCode: 1, duration: Date.now() - start, manager };
43
+ }
44
+ }
45
+
46
+ export { execute };
@@ -0,0 +1,29 @@
1
+ async function execute(args) {
2
+ const query = args.query || args;
3
+ if (typeof query !== 'string') throw new Error('Query must be a string');
4
+
5
+ const url = `https://api.duckduckgo.com/?q=${encodeURIComponent(query)}&format=json&no_html=1`;
6
+ const res = await fetch(url);
7
+ const data = await res.json();
8
+
9
+ const results = [];
10
+ if (data.AbstractText) {
11
+ results.push({ title: data.AbstractSource || 'Result', url: data.AbstractURL || '', snippet: data.AbstractText });
12
+ }
13
+ if (data.RelatedTopics) {
14
+ for (const topic of data.RelatedTopics.slice(0, 8)) {
15
+ if (topic.Text) {
16
+ results.push({ title: topic.Text.split(' - ')[0], url: topic.FirstURL || '', snippet: topic.Text });
17
+ }
18
+ if (topic.Topics) {
19
+ topic.Topics.slice(0, 3).forEach(t => {
20
+ if (t.Text) results.push({ title: t.Text.split(' - ')[0], url: t.FirstURL || '', snippet: t.Text });
21
+ });
22
+ }
23
+ }
24
+ }
25
+
26
+ return { query, results, total: results.length };
27
+ }
28
+
29
+ export { execute };
@@ -0,0 +1,29 @@
1
+ import os from 'os';
2
+
3
+ async function execute() {
4
+ const mem = process.memoryUsage();
5
+ return {
6
+ node: process.version,
7
+ platform: os.platform(),
8
+ arch: os.arch(),
9
+ release: os.release(),
10
+ hostname: os.hostname(),
11
+ cwd: process.cwd(),
12
+ memory: {
13
+ total: Math.round(os.totalmem() / 1024 / 1024) + 'MB',
14
+ free: Math.round(os.freemem() / 1024 / 1024) + 'MB',
15
+ heapUsed: Math.round(mem.heapUsed / 1024 / 1024) + 'MB',
16
+ heapTotal: Math.round(mem.heapTotal / 1024 / 1024) + 'MB',
17
+ },
18
+ cpu: {
19
+ model: os.cpus()[0]?.model || 'unknown',
20
+ cores: os.cpus().length,
21
+ loadAvg: os.loadavg().map(l => l.toFixed(2)),
22
+ },
23
+ uptime: Math.round(os.uptime() / 3600) + 'h',
24
+ pid: process.pid,
25
+ env: Object.keys(process.env).length + ' variables',
26
+ };
27
+ }
28
+
29
+ export { execute };
@@ -0,0 +1,24 @@
1
+ async function execute(args) {
2
+ const url = args.url || args;
3
+ if (typeof url !== 'string') throw new Error('URL must be a string');
4
+
5
+ const start = Date.now();
6
+ const res = await fetch(url);
7
+ const text = await res.text();
8
+ const clean = text.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, '')
9
+ .replace(/<script[^>]*>[\s\S]*?<\/script>/gi, '')
10
+ .replace(/<[^>]+>/g, ' ')
11
+ .replace(/\s+/g, ' ')
12
+ .trim();
13
+
14
+ return {
15
+ status: res.status,
16
+ headers: Object.fromEntries(res.headers),
17
+ body: clean.slice(0, 2000),
18
+ duration: Date.now() - start,
19
+ url,
20
+ size: text.length,
21
+ };
22
+ }
23
+
24
+ export { execute };
@@ -0,0 +1,15 @@
1
+ import figlet from 'figlet';
2
+ import gradient from 'gradient-string';
3
+ import c from './colors.js';
4
+
5
+ const GRADIENT = gradient(['#00d2ff', '#7b2ff7']);
6
+
7
+ function renderBanner() {
8
+ const text = figlet.textSync('CLARITY', { font: 'ANSI Shadow' });
9
+ console.log(GRADIENT(text));
10
+ console.log(c.muted('AI Agent CLI — v1.0.0 ⚡ Termux Ready'));
11
+ console.log(c.muted('─'.repeat(process.stdout.columns || 80)));
12
+ console.log();
13
+ }
14
+
15
+ export default renderBanner;
@@ -0,0 +1,55 @@
1
+ import boxen from 'boxen';
2
+ import cliTable3 from 'cli-table3';
3
+ import c from './colors.js';
4
+
5
+ const blocks = {
6
+ info(title, message) {
7
+ console.log(boxen(message, { padding: 1, borderColor: '#44aaff', borderStyle: 'round', title: ` ℹ ${title} `, titleAlignment: 'left' }));
8
+ },
9
+ success(title, message) {
10
+ console.log(boxen(message, { padding: 1, borderColor: '#00ff88', borderStyle: 'round', title: ` ✓ ${title} `, titleAlignment: 'left' }));
11
+ },
12
+ warn(title, message) {
13
+ console.log(boxen(message, { padding: 1, borderColor: '#ffcc00', borderStyle: 'round', title: ` ⚠ ${title} `, titleAlignment: 'left' }));
14
+ },
15
+ error(title, message) {
16
+ console.log(boxen(message, { padding: 1, borderColor: '#ff4466', borderStyle: 'round', title: ` ✗ ${title} `, titleAlignment: 'left' }));
17
+ },
18
+ code(language, code) {
19
+ console.log(`\n${c.muted('```' + language)}`);
20
+ console.log(c.code(code));
21
+ console.log(`${c.muted('```')}\n`);
22
+ },
23
+ file(path, content) {
24
+ console.log(boxen(c.filename(path) + '\n\n' + content, { padding: 1, borderColor: '#ffcc66', borderStyle: 'single' }));
25
+ },
26
+ ai(message) {
27
+ console.log(boxen(c.ai(message), { padding: 1, borderColor: '#7b2ff7', borderStyle: 'double' }));
28
+ },
29
+ user(message) {
30
+ console.log(boxen(c.user(message), { padding: 1, borderColor: '#00d2ff', borderStyle: 'single' }));
31
+ },
32
+ table(headers, rows) {
33
+ const t = new cliTable3({ head: headers.map(h => c.primary(h)), style: { border: ['#555577'] } });
34
+ rows.forEach(r => t.push(r));
35
+ console.log(t.toString());
36
+ },
37
+ divider(label) {
38
+ const width = process.stdout.columns || 80;
39
+ const side = Math.floor((width - (label ? label.length + 4 : 0)) / 2);
40
+ console.log(c.muted('─'.repeat(side) + (label ? ` ${label} ` : '') + '─'.repeat(side)));
41
+ },
42
+ badge(text, color = 'cyan') {
43
+ const colors = { cyan: c.primary, purple: c.accent, green: c.success, yellow: c.warning, red: c.error };
44
+ return (colors[color] || c.primary)(`[${text}]`);
45
+ },
46
+ progress(label, pct) {
47
+ const width = 20;
48
+ const filled = Math.round(width * pct / 100);
49
+ const empty = width - filled;
50
+ const bar = '█'.repeat(filled) + '░'.repeat(empty);
51
+ console.log(`${c.muted(label)} ${c.primary(bar)} ${c.white(String(pct))}%`);
52
+ }
53
+ };
54
+
55
+ export default blocks;
@@ -0,0 +1,126 @@
1
+ import readline from 'readline';
2
+ import c from './colors.js';
3
+ import renderBanner from './banner.js';
4
+ import settings from '../config/settings.js';
5
+ import { hasAnyKeys } from '../config/keys.js';
6
+ import { getKey, PROVIDER_NAMES } from '../config/keys.js';
7
+ import { sendMessage } from '../providers/index.js';
8
+ import { createPrompt, addToHistory, loadHistory } from './prompt.js';
9
+ import blocks from './blocks.js';
10
+ import spin from './spinner.js';
11
+ import { renderMarkdown } from '../utils/markdown.js';
12
+ import commandRegistry from '../commands/index.js';
13
+ import memory from '../memory/store.js';
14
+
15
+ let rl = null;
16
+ let conversation = [];
17
+
18
+ function getHeader() {
19
+ const model = settings.get('defaultModel') || 'groq/llama3-70b-8192';
20
+ const width = process.stdout.columns || 80;
21
+ const header = ` CLARITY CHAT ──────── Model: ${model} ── /help for commands `;
22
+ return c.primary('┌─') + c.muted(header.padEnd(width - 4, '─')) + c.primary('┐');
23
+ }
24
+
25
+ function startChat() {
26
+ if (!hasAnyKeys()) {
27
+ blocks.warn('No API Keys', 'Run /init to configure API keys first.');
28
+ return;
29
+ }
30
+
31
+ loadHistory();
32
+ console.clear();
33
+ renderBanner();
34
+ console.log(getHeader());
35
+ console.log();
36
+
37
+ rl = createPrompt();
38
+ rl.prompt();
39
+
40
+ rl.on('line', async (line) => {
41
+ const input = line.trim();
42
+ if (!input) { rl.prompt(); return; }
43
+
44
+ addToHistory(input);
45
+
46
+ if (input.startsWith('/')) {
47
+ const result = await commandRegistry.execute(input, { rl, conversation });
48
+ if (result?.exit) { closeChat(); return; }
49
+ } else {
50
+ conversation.push({ role: 'user', content: input });
51
+ blocks.user(input);
52
+ await handleAIResponse();
53
+ }
54
+ rl.prompt();
55
+ });
56
+
57
+ rl.on('close', () => {
58
+ console.log(c.muted('\nGoodbye!'));
59
+ process.exit(0);
60
+ });
61
+
62
+ rl.on('SIGINT', () => {
63
+ console.log(c.muted('\nUse /exit to quit'));
64
+ rl.prompt();
65
+ });
66
+ }
67
+
68
+ async function handleAIResponse() {
69
+ const model = settings.get('defaultModel') || 'groq/llama3-70b-8192';
70
+ const [provider] = model.split('/');
71
+ const apiKey = getKey(provider);
72
+
73
+ if (!apiKey) {
74
+ blocks.error('Key Missing', `No API key for ${PROVIDER_NAMES[provider] || provider}`);
75
+ return;
76
+ }
77
+
78
+ const ctx = memory.getContext();
79
+ const messages = [...ctx, ...conversation];
80
+
81
+ spin.start('CLARITY is thinking...');
82
+
83
+ try {
84
+ const stream = sendMessage(apiKey, messages, model, settings.get('stream'));
85
+
86
+ if (settings.get('stream')) {
87
+ spin.stop();
88
+ process.stdout.write(c.ai('CLARITY ▸ '));
89
+ let full = '';
90
+ for await (const chunk of stream) {
91
+ full += chunk;
92
+ process.stdout.write(c.white(chunk));
93
+ }
94
+ process.stdout.write('\n\n');
95
+ conversation.push({ role: 'assistant', content: full });
96
+ memory.add(conversation);
97
+ } else {
98
+ let full = '';
99
+ for await (const chunk of stream) {
100
+ full += chunk;
101
+ }
102
+ spin.stop();
103
+ const rendered = renderMarkdown(full);
104
+ blocks.ai(rendered);
105
+ conversation.push({ role: 'assistant', content: full });
106
+ memory.add(conversation);
107
+ }
108
+
109
+ if (settings.get('showTokens')) {
110
+ const inTokens = Math.ceil(conversation.reduce((s, m) => s + m.content.length, 0) / 4);
111
+ const outTokens = Math.ceil(conversation.filter(m => m.role === 'assistant').reduce((s, m) => s + m.content.length, 0) / 4);
112
+ console.log(c.muted(`[tokens: ${inTokens} in / ${outTokens} out | cost: free]`));
113
+ }
114
+ } catch (err) {
115
+ spin.fail('Error');
116
+ blocks.error('AI Error', err.message);
117
+ }
118
+ }
119
+
120
+ function closeChat() {
121
+ if (rl) rl.close();
122
+ console.log(c.muted('\nThank you for using CLARITY!'));
123
+ process.exit(0);
124
+ }
125
+
126
+ export { startChat, closeChat, conversation };
@@ -0,0 +1,22 @@
1
+ import chalk from 'chalk';
2
+ export const c = {
3
+ primary: chalk.hex('#00d2ff'),
4
+ accent: chalk.hex('#7b2ff7'),
5
+ success: chalk.hex('#00ff88'),
6
+ warning: chalk.hex('#ffcc00'),
7
+ error: chalk.hex('#ff4466'),
8
+ info: chalk.hex('#00aaff'),
9
+ muted: chalk.hex('#666888'),
10
+ white: chalk.hex('#f0f0ff'),
11
+ user: chalk.hex('#00d2ff').bold,
12
+ ai: chalk.hex('#bb88ff').bold,
13
+ system: chalk.hex('#888888').italic,
14
+ code: chalk.hex('#aaffcc'),
15
+ filename: chalk.hex('#ffcc66').underline,
16
+ cmd: chalk.hex('#ff8844').bold,
17
+ bgError: chalk.bgHex('#3d0010').hex('#ff4466'),
18
+ bgSuccess: chalk.bgHex('#003d1a').hex('#00ff88'),
19
+ bgInfo: chalk.bgHex('#001a3d').hex('#00aaff'),
20
+ bgWarn: chalk.bgHex('#3d3000').hex('#ffcc00'),
21
+ };
22
+ export default c;