clarity-ai 3.3.0 → 4.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 (89) hide show
  1. package/bin/clarity.js +9 -15
  2. package/core/agent.js +52 -0
  3. package/core/config.js +39 -0
  4. package/core/intentDetect.js +13 -0
  5. package/core/keyCheck.js +21 -0
  6. package/core/providers/index.js +79 -0
  7. package/core/tools.js +178 -0
  8. package/package.json +12 -15
  9. package/ui/App.js +129 -0
  10. package/ui/Banner.js +13 -0
  11. package/ui/CommandPicker.js +74 -0
  12. package/ui/InputBar.js +33 -0
  13. package/ui/MessageList.js +50 -0
  14. package/ui/ModelPicker.js +96 -0
  15. package/ui/StatusBar.js +21 -0
  16. /package/{AGENTS.md → AGENTS.md.old} +0 -0
  17. /package/{src → src.old}/agents/code-agent.js +0 -0
  18. /package/{src → src.old}/agents/file-agent.js +0 -0
  19. /package/{src → src.old}/agents/git-agent.js +0 -0
  20. /package/{src → src.old}/agents/loop.js +0 -0
  21. /package/{src → src.old}/agents/monitor-agent.js +0 -0
  22. /package/{src → src.old}/agents/planner.js +0 -0
  23. /package/{src → src.old}/agents/shell-agent.js +0 -0
  24. /package/{src → src.old}/agents/web-agent.js +0 -0
  25. /package/{src → src.old}/commands/agent.js +0 -0
  26. /package/{src → src.old}/commands/chat.js +0 -0
  27. /package/{src → src.old}/commands/clear.js +0 -0
  28. /package/{src → src.old}/commands/config.js +0 -0
  29. /package/{src → src.old}/commands/diff.js +0 -0
  30. /package/{src → src.old}/commands/exit.js +0 -0
  31. /package/{src → src.old}/commands/export.js +0 -0
  32. /package/{src → src.old}/commands/fetch.js +0 -0
  33. /package/{src → src.old}/commands/git.js +0 -0
  34. /package/{src → src.old}/commands/help.js +0 -0
  35. /package/{src → src.old}/commands/history.js +0 -0
  36. /package/{src → src.old}/commands/index.js +0 -0
  37. /package/{src → src.old}/commands/keys.js +0 -0
  38. /package/{src → src.old}/commands/memory.js +0 -0
  39. /package/{src → src.old}/commands/model.js +0 -0
  40. /package/{src → src.old}/commands/provider.js +0 -0
  41. /package/{src → src.old}/commands/run.js +0 -0
  42. /package/{src → src.old}/commands/search.js +0 -0
  43. /package/{src → src.old}/commands/task.js +0 -0
  44. /package/{src → src.old}/commands/tools.js +0 -0
  45. /package/{src → src.old}/commands/undo.js +0 -0
  46. /package/{src → src.old}/config/settings.js +0 -0
  47. /package/{src → src.old}/core/context.js +0 -0
  48. /package/{src → src.old}/core/history.js +0 -0
  49. /package/{src → src.old}/core/memory.js +0 -0
  50. /package/{src → src.old}/core/setup.js +0 -0
  51. /package/{src → src.old}/main.js +0 -0
  52. /package/{src → src.old}/providers/claude.js +0 -0
  53. /package/{src → src.old}/providers/deepseek.js +0 -0
  54. /package/{src → src.old}/providers/gemini.js +0 -0
  55. /package/{src → src.old}/providers/groq.js +0 -0
  56. /package/{src → src.old}/providers/index.js +0 -0
  57. /package/{src → src.old}/providers/openai.js +0 -0
  58. /package/{src → src.old}/providers/openrouter.js +0 -0
  59. /package/{src → src.old}/tools/agent-spawn.js +0 -0
  60. /package/{src → src.old}/tools/bash.js +0 -0
  61. /package/{src → src.old}/tools/clipboard-tool.js +0 -0
  62. /package/{src → src.old}/tools/code-runner.js +0 -0
  63. /package/{src → src.old}/tools/compress-tool.js +0 -0
  64. /package/{src → src.old}/tools/context-tool.js +0 -0
  65. /package/{src → src.old}/tools/delete-file.js +0 -0
  66. /package/{src → src.old}/tools/diff-tool.js +0 -0
  67. /package/{src → src.old}/tools/edit-file.js +0 -0
  68. /package/{src → src.old}/tools/env-tool.js +0 -0
  69. /package/{src → src.old}/tools/git-tool.js +0 -0
  70. /package/{src → src.old}/tools/grep.js +0 -0
  71. /package/{src → src.old}/tools/list-dir.js +0 -0
  72. /package/{src → src.old}/tools/memory-tool.js +0 -0
  73. /package/{src → src.old}/tools/notify-tool.js +0 -0
  74. /package/{src → src.old}/tools/pkg-manager.js +0 -0
  75. /package/{src → src.old}/tools/read-file.js +0 -0
  76. /package/{src → src.old}/tools/run-tests.js +0 -0
  77. /package/{src → src.old}/tools/screenshot-tool.js +0 -0
  78. /package/{src → src.old}/tools/search-files.js +0 -0
  79. /package/{src → src.old}/tools/task-planner.js +0 -0
  80. /package/{src → src.old}/tools/version-check.js +0 -0
  81. /package/{src → src.old}/tools/web-fetch.js +0 -0
  82. /package/{src → src.old}/tools/web-search.js +0 -0
  83. /package/{src → src.old}/tools/write-file.js +0 -0
  84. /package/{src → src.old}/ui/banner.js +0 -0
  85. /package/{src → src.old}/ui/blocks.js +0 -0
  86. /package/{src → src.old}/ui/colors.js +0 -0
  87. /package/{src → src.old}/ui/input.js +0 -0
  88. /package/{src → src.old}/ui/prompt.js +0 -0
  89. /package/{src → src.old}/ui/spinner.js +0 -0
package/bin/clarity.js CHANGED
@@ -1,23 +1,17 @@
1
1
  #!/usr/bin/env node
2
- import { showBanner } from '../src/ui/banner.js';
3
- import { isFirstRun, loadConfig } from '../src/config/settings.js';
4
- import { runSetupWizard } from '../src/core/setup.js';
5
- import { startChat } from '../src/main.js';
2
+ import React from 'react';
3
+ import { render } from 'ink';
4
+ import { App } from '../ui/App.js';
5
+ import { loadConfig } from '../core/config.js';
6
+ import { ensureApiKey } from '../core/keyCheck.js';
6
7
 
7
8
  async function main() {
8
- const firstRun = isFirstRun();
9
- let config = firstRun ? {} : loadConfig();
10
-
11
- await showBanner(config.version, config.provider, config.model);
12
-
13
- if (firstRun) {
14
- config = await runSetupWizard();
15
- }
16
-
17
- await startChat(config);
9
+ const config = loadConfig();
10
+ await ensureApiKey(config.provider || 'groq');
11
+ render(React.createElement(App, { config }), { fullscreen: true });
18
12
  }
19
13
 
20
14
  main().catch(err => {
21
- console.error('\x1b[31mFatal error:\x1b[0m', err.message);
15
+ console.error('\n\x1b[31mFatal error:\x1b[0m', err.message);
22
16
  process.exit(1);
23
17
  });
package/core/agent.js ADDED
@@ -0,0 +1,52 @@
1
+ import { callAI } from './providers/index.js';
2
+ import { TOOLS, executeTool } from './tools.js';
3
+ import { extractCommandFromText } from './intentDetect.js';
4
+
5
+ export async function runAgent({ messages, model, provider, agentMode, onToolCall }) {
6
+ let history = [...messages];
7
+ const MAX_ITERATIONS = 10;
8
+
9
+ for (let i = 0; i < MAX_ITERATIONS; i++) {
10
+ const response = await callAI({
11
+ model, provider,
12
+ messages: history,
13
+ tools: agentMode ? TOOLS : undefined,
14
+ });
15
+
16
+ const { content, tool_calls, finish_reason } = response;
17
+
18
+ if (!tool_calls || tool_calls.length === 0 || finish_reason === 'stop') {
19
+ if (agentMode) {
20
+ const cmd = extractCommandFromText(content || '');
21
+ if (cmd) {
22
+ onToolCall?.('bash(' + cmd.slice(0, 60) + ')');
23
+ const result = await executeTool('bash', { command: cmd });
24
+ history.push({ role: 'assistant', content });
25
+ history.push({ role: 'tool', name: 'bash', content: String(result) });
26
+ continue;
27
+ }
28
+ }
29
+ return content;
30
+ }
31
+
32
+ history.push({ role: 'assistant', content, tool_calls });
33
+
34
+ for (const call of tool_calls) {
35
+ const name = call.function.name;
36
+ const args = JSON.parse(call.function.arguments || '{}');
37
+
38
+ onToolCall?.(name + '(' + JSON.stringify(args).slice(0, 80) + ')');
39
+
40
+ const result = await executeTool(name, args);
41
+
42
+ history.push({
43
+ role: 'tool',
44
+ tool_call_id: call.id,
45
+ name,
46
+ content: String(result),
47
+ });
48
+ }
49
+ }
50
+
51
+ return 'Max iterations reached.';
52
+ }
package/core/config.js ADDED
@@ -0,0 +1,39 @@
1
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
2
+ import { homedir } from 'os';
3
+ import { join } from 'path';
4
+
5
+ const CONFIG_DIR = join(homedir(), '.clarity');
6
+ const CONFIG_FILE = join(CONFIG_DIR, 'config.json');
7
+
8
+ const DEFAULTS = {
9
+ provider: 'groq',
10
+ model: 'groq/llama-3.3-70b-versatile',
11
+ keys: {},
12
+ agentMode: true,
13
+ maxTokens: 4096,
14
+ temperature: 0.7,
15
+ };
16
+
17
+ export function loadConfig() {
18
+ if (!existsSync(CONFIG_FILE)) {
19
+ return { ...DEFAULTS };
20
+ }
21
+ try {
22
+ const raw = readFileSync(CONFIG_FILE, 'utf-8');
23
+ return { ...DEFAULTS, ...JSON.parse(raw) };
24
+ } catch {
25
+ return { ...DEFAULTS };
26
+ }
27
+ }
28
+
29
+ export function saveConfig(config) {
30
+ if (!existsSync(CONFIG_DIR)) mkdirSync(CONFIG_DIR, { recursive: true });
31
+ writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), 'utf-8');
32
+ }
33
+
34
+ export function saveKey(provider, key) {
35
+ const config = loadConfig();
36
+ config.keys = config.keys || {};
37
+ config.keys[provider] = key;
38
+ saveConfig(config);
39
+ }
@@ -0,0 +1,13 @@
1
+ export function extractCommandFromText(text) {
2
+ const patterns = [
3
+ /```(?:bash|sh)\n([\s\S]+?)```/,
4
+ /Run(?:: | the following)?\s*`([^`]+)`/i,
5
+ /execute[:\s]+`([^`]+)`/i,
6
+ /run (?:this |the following )?command[:\s]*`([^`]+)`/i,
7
+ ];
8
+ for (const p of patterns) {
9
+ const m = text.match(p);
10
+ if (m) return m[1].trim();
11
+ }
12
+ return null;
13
+ }
@@ -0,0 +1,21 @@
1
+ import { createInterface } from 'readline';
2
+ import { loadConfig, saveKey } from './config.js';
3
+
4
+ export function ensureApiKey(provider) {
5
+ const config = loadConfig();
6
+ const key = config.keys?.[provider];
7
+ if (key) return key;
8
+
9
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
10
+ return new Promise((resolve) => {
11
+ rl.question(
12
+ '\n\x1b[33m No ' + provider + ' API key found.\x1b[0m\n Enter your ' + provider + ' key: ',
13
+ (answer) => {
14
+ rl.close();
15
+ const trimmed = answer.trim();
16
+ saveKey(provider, trimmed);
17
+ resolve(trimmed);
18
+ }
19
+ );
20
+ });
21
+ }
@@ -0,0 +1,79 @@
1
+ import Groq from 'groq-sdk';
2
+
3
+ export async function callAI({ model, provider, messages, tools }) {
4
+ const providerName = provider || 'groq';
5
+ const modelName = model.replace(/^[^/]+\//, '');
6
+
7
+ switch (providerName.toLowerCase()) {
8
+ case 'groq': {
9
+ const { loadConfig } = await import('../config.js');
10
+ const config = loadConfig();
11
+ const apiKey = config.keys?.groq;
12
+ if (!apiKey) throw new Error('No Groq API key set. Use /keys groq <key>.');
13
+
14
+ const client = new Groq({ apiKey });
15
+ const res = await client.chat.completions.create({
16
+ model: modelName,
17
+ messages,
18
+ tools: tools && tools.length > 0 ? tools : undefined,
19
+ tool_choice: tools && tools.length > 0 ? 'auto' : undefined,
20
+ });
21
+ const choice = res.choices[0];
22
+ return {
23
+ content: choice.message.content || '',
24
+ tool_calls: choice.message.tool_calls || null,
25
+ finish_reason: choice.finish_reason,
26
+ };
27
+ }
28
+
29
+ case 'openrouter': {
30
+ const { loadConfig } = await import('../config.js');
31
+ const config = loadConfig();
32
+ const apiKey = config.keys?.openrouter;
33
+ if (!apiKey) throw new Error('No OpenRouter API key set.');
34
+
35
+ const res = await fetch('https://openrouter.ai/api/v1/chat/completions', {
36
+ method: 'POST',
37
+ headers: {
38
+ 'Content-Type': 'application/json',
39
+ 'Authorization': 'Bearer ' + apiKey,
40
+ 'HTTP-Referer': 'https://clarity-ai.local',
41
+ 'X-Title': 'CLARITY AI',
42
+ },
43
+ body: JSON.stringify({
44
+ model: modelName,
45
+ messages,
46
+ tools: tools && tools.length > 0 ? tools : undefined,
47
+ }),
48
+ });
49
+ const data = await res.json();
50
+ const choice = data.choices?.[0];
51
+ return {
52
+ content: choice?.message?.content || '',
53
+ tool_calls: choice?.message?.tool_calls || null,
54
+ finish_reason: choice?.finish_reason,
55
+ };
56
+ }
57
+
58
+ case 'gemini': {
59
+ const { GoogleGenerativeAI } = await import('@google/generative-ai');
60
+ const { loadConfig } = await import('../config.js');
61
+ const config = loadConfig();
62
+ const apiKey = config.keys?.gemini;
63
+ if (!apiKey) throw new Error('No Gemini API key set.');
64
+
65
+ const genAI = new GoogleGenerativeAI(apiKey);
66
+ const model_ = genAI.getGenerativeModel({ model: modelName });
67
+ const chat = model_.startChat({ messages });
68
+ const result = await chat.sendMessage(messages[messages.length - 1]?.content || '');
69
+ return {
70
+ content: result.response.text() || '',
71
+ tool_calls: null,
72
+ finish_reason: 'stop',
73
+ };
74
+ }
75
+
76
+ default:
77
+ throw new Error('Unknown provider: ' + providerName);
78
+ }
79
+ }
package/core/tools.js ADDED
@@ -0,0 +1,178 @@
1
+ import { exec } from 'child_process';
2
+ import { readFile, writeFile, readdir, mkdir } from 'fs/promises';
3
+ import { existsSync } from 'fs';
4
+ import { dirname } from 'path';
5
+ import { promisify } from 'util';
6
+
7
+ const execAsync = promisify(exec);
8
+
9
+ async function outputFile(path, content) {
10
+ const dir = dirname(path);
11
+ if (!existsSync(dir)) await mkdir(dir, { recursive: true });
12
+ await writeFile(path, content, 'utf-8');
13
+ }
14
+
15
+ export const TOOLS = [
16
+ {
17
+ type: 'function',
18
+ function: {
19
+ name: 'bash',
20
+ description: 'Execute a bash command in the Termux shell and return stdout/stderr',
21
+ parameters: {
22
+ type: 'object',
23
+ properties: {
24
+ command: { type: 'string', description: 'The bash command to run' },
25
+ },
26
+ required: ['command'],
27
+ },
28
+ },
29
+ },
30
+ {
31
+ type: 'function',
32
+ function: {
33
+ name: 'read_file',
34
+ description: 'Read a file from the filesystem',
35
+ parameters: {
36
+ type: 'object',
37
+ properties: {
38
+ path: { type: 'string' },
39
+ offset: { type: 'number', description: 'Line offset to start reading' },
40
+ limit: { type: 'number', description: 'Max lines to read' },
41
+ },
42
+ required: ['path'],
43
+ },
44
+ },
45
+ },
46
+ {
47
+ type: 'function',
48
+ function: {
49
+ name: 'write_file',
50
+ description: 'Write content to a file',
51
+ parameters: {
52
+ type: 'object',
53
+ properties: {
54
+ path: { type: 'string' },
55
+ content: { type: 'string' },
56
+ },
57
+ required: ['path', 'content'],
58
+ },
59
+ },
60
+ },
61
+ {
62
+ type: 'function',
63
+ function: {
64
+ name: 'list_dir',
65
+ description: 'List files in a directory',
66
+ parameters: {
67
+ type: 'object',
68
+ properties: {
69
+ path: { type: 'string', default: '.' },
70
+ },
71
+ },
72
+ },
73
+ },
74
+ {
75
+ type: 'function',
76
+ function: {
77
+ name: 'grep',
78
+ description: 'Search for a pattern in files',
79
+ parameters: {
80
+ type: 'object',
81
+ properties: {
82
+ pattern: { type: 'string' },
83
+ path: { type: 'string' },
84
+ flags: { type: 'string', default: '-r' },
85
+ },
86
+ required: ['pattern', 'path'],
87
+ },
88
+ },
89
+ },
90
+ {
91
+ type: 'function',
92
+ function: {
93
+ name: 'web_search',
94
+ description: 'Search the web for information',
95
+ parameters: {
96
+ type: 'object',
97
+ properties: {
98
+ query: { type: 'string' },
99
+ },
100
+ required: ['query'],
101
+ },
102
+ },
103
+ },
104
+ {
105
+ type: 'function',
106
+ function: {
107
+ name: 'web_fetch',
108
+ description: 'Fetch a URL and return its text content',
109
+ parameters: {
110
+ type: 'object',
111
+ properties: {
112
+ url: { type: 'string', format: 'uri' },
113
+ },
114
+ required: ['url'],
115
+ },
116
+ },
117
+ },
118
+ ];
119
+
120
+ export async function executeTool(name, args) {
121
+ switch (name) {
122
+ case 'bash': {
123
+ try {
124
+ const { stdout, stderr } = await execAsync(args.command, { timeout: 30000 });
125
+ return stdout || stderr || '(no output)';
126
+ } catch (e) {
127
+ return 'Error: ' + (e.stderr || e.message);
128
+ }
129
+ }
130
+ case 'read_file': {
131
+ const content = await readFile(args.path, 'utf8');
132
+ const lines = content.split('\n');
133
+ const start = args.offset || 0;
134
+ const end = args.limit ? start + args.limit : lines.length;
135
+ return lines.slice(start, end).join('\n');
136
+ }
137
+ case 'write_file': {
138
+ await outputFile(args.path, args.content);
139
+ return 'Written to ' + args.path;
140
+ }
141
+ case 'list_dir': {
142
+ const files = await readdir(args.path || '.');
143
+ return files.join('\n');
144
+ }
145
+ case 'grep': {
146
+ try {
147
+ const { stdout } = await execAsync(
148
+ 'grep ' + (args.flags || '-r') + ' "' + args.pattern + '" "' + args.path + '"',
149
+ { timeout: 10000 }
150
+ );
151
+ return stdout || '(no matches)';
152
+ } catch (e) {
153
+ return e.stdout || '(no matches)';
154
+ }
155
+ }
156
+ case 'web_search': {
157
+ try {
158
+ const url = 'https://api.duckduckgo.com/?q=' + encodeURIComponent(args.query) + '&format=json&no_html=1';
159
+ const res = await fetch(url);
160
+ const data = await res.json();
161
+ return data.AbstractText || data.RelatedTopics?.slice(0,3)?.map(t => t.Text).join('\n') || 'No results';
162
+ } catch (e) {
163
+ return 'Search error: ' + e.message;
164
+ }
165
+ }
166
+ case 'web_fetch': {
167
+ try {
168
+ const res = await fetch(args.url);
169
+ const text = await res.text();
170
+ return text.slice(0, 5000);
171
+ } catch (e) {
172
+ return 'Fetch error: ' + e.message;
173
+ }
174
+ }
175
+ default:
176
+ return 'Unknown tool: ' + name;
177
+ }
178
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "clarity-ai",
3
- "version": "3.3.0",
4
- "description": "Autonomous AI Agent CLI for Termux",
3
+ "version": "4.0.0",
4
+ "description": "Ink+React TUI Autonomous AI Agent CLI for Termux",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "clarity": "bin/clarity.js"
@@ -14,18 +14,15 @@
14
14
  "node": ">=18.0.0"
15
15
  },
16
16
  "dependencies": {
17
- "chalk": "^5.3.0",
18
- "figlet": "^1.7.0",
19
- "gradient-string": "^2.0.2",
20
- "ora": "^8.1.0",
21
- "inquirer": "^9.2.12",
22
- "cli-table3": "^0.6.3",
23
- "glob": "^10.3.10",
24
- "chokidar": "^3.5.3",
25
- "marked": "^9.1.6",
26
- "marked-terminal": "^6.1.0",
27
- "diff": "^5.1.0",
28
- "archiver": "^6.0.1",
29
- "strip-ansi": "^7.1.0"
17
+ "@google/generative-ai": "^0.24.1",
18
+ "@inkjs/ui": "^2.0.0",
19
+ "@langchain/langgraph": "^1.3.5",
20
+ "groq-sdk": "^1.2.1",
21
+ "ink": "^7.0.5",
22
+ "ink-big-text": "^2.0.0",
23
+ "ink-gradient": "^4.0.1",
24
+ "ink-select-input": "^6.2.0",
25
+ "ink-spinner": "^5.0.0",
26
+ "ink-text-input": "^6.0.0"
30
27
  }
31
28
  }
package/ui/App.js ADDED
@@ -0,0 +1,129 @@
1
+ import React, { useState } from 'react';
2
+ import { Box, Text, useApp } from 'ink';
3
+ import { Banner } from './Banner.js';
4
+ import { MessageList } from './MessageList.js';
5
+ import { InputBar } from './InputBar.js';
6
+ import { StatusBar } from './StatusBar.js';
7
+ import { CommandPicker } from './CommandPicker.js';
8
+ import { ModelPicker } from './ModelPicker.js';
9
+ import { runAgent } from '../core/agent.js';
10
+ import { saveKey } from '../core/config.js';
11
+ const { createElement: h } = React;
12
+
13
+ function extractModelProvider(modelId) {
14
+ const parts = String(modelId).split('/');
15
+ return { provider: parts[0] || 'groq', model: parts[1] || modelId };
16
+ }
17
+
18
+ export function App({ config: initialConfig }) {
19
+ const [messages, setMessages] = useState([]);
20
+ const [thinking, setThinking] = useState(false);
21
+ const [showCommands, setShowCommands] = useState(false);
22
+ const [showModels, setShowModels] = useState(false);
23
+ const [model, setModel] = useState(initialConfig.model || 'groq/llama-3.3-70b-versatile');
24
+ const [provider, setProvider] = useState(initialConfig.provider || 'groq');
25
+ const [agentMode, setAgentMode] = useState(initialConfig.agentMode !== false);
26
+ const { exit } = useApp();
27
+
28
+ async function handleSubmit(input) {
29
+ if (!input.trim()) return;
30
+
31
+ if (input.startsWith('/')) {
32
+ handleCommand(input);
33
+ return;
34
+ }
35
+
36
+ setMessages(m => [...m, { role: 'user', content: input }]);
37
+ setThinking(true);
38
+
39
+ try {
40
+ const response = await runAgent({
41
+ messages: [...messages.filter(m => m.role !== 'tool'), { role: 'user', content: input }],
42
+ model,
43
+ provider,
44
+ agentMode,
45
+ onToolCall: (tool) => {
46
+ setMessages(m => [...m, { role: 'tool', content: tool }]);
47
+ },
48
+ });
49
+ setMessages(m => [...m, { role: 'assistant', content: response }]);
50
+ } catch (err) {
51
+ setMessages(m => [...m, { role: 'error', content: err.message }]);
52
+ } finally {
53
+ setThinking(false);
54
+ }
55
+ }
56
+
57
+ function handleCommand(input) {
58
+ const parts = input.trim().split(/\s+/);
59
+ const cmd = parts[0];
60
+ const args = parts.slice(1);
61
+
62
+ switch (cmd) {
63
+ case '/model':
64
+ case '/models':
65
+ setShowModels(true);
66
+ break;
67
+ case '/agent':
68
+ setAgentMode(a => !a);
69
+ setMessages(m => [...m, { role: 'system', content: 'Agent mode ' + (!agentMode ? 'ON' : 'OFF') }]);
70
+ break;
71
+ case '/clear':
72
+ setMessages([]);
73
+ break;
74
+ case '/keys': {
75
+ if (args.length >= 2) {
76
+ saveKey(args[0], args[1]);
77
+ setMessages(m => [...m, { role: 'system', content: 'Key saved for ' + args[0] }]);
78
+ } else {
79
+ setMessages(m => [...m, { role: 'error', content: 'Usage: /keys <provider> <key>' }]);
80
+ }
81
+ break;
82
+ }
83
+ case '/provider':
84
+ setShowModels(true);
85
+ break;
86
+ case '/help':
87
+ setShowCommands(true);
88
+ break;
89
+ case '/exit':
90
+ exit();
91
+ break;
92
+ default:
93
+ setMessages(m => [...m, { role: 'error', content: 'Unknown command: ' + cmd + '. Type / to see all commands.' }]);
94
+ }
95
+ }
96
+
97
+ function handleModelSelect(modelId) {
98
+ const { provider: p, model: m } = extractModelProvider(modelId);
99
+ setProvider(p);
100
+ setModel(modelId);
101
+ setShowModels(false);
102
+ setMessages(msgs => [...msgs, { role: 'system', content: 'Switched to ' + p + '/' + m }]);
103
+ }
104
+
105
+ return h(Box, { flexDirection: 'column', height: '100%' },
106
+ h(Banner),
107
+ h(Box, { flexGrow: 1, flexDirection: 'column', overflowY: 'hidden' },
108
+ h(MessageList, { messages, thinking })
109
+ ),
110
+ showCommands ? h(CommandPicker, {
111
+ onSelect: (cmd) => { setShowCommands(false); handleCommand(cmd); },
112
+ onClose: () => setShowCommands(false),
113
+ }) : null,
114
+ showModels ? h(ModelPicker, {
115
+ current: model,
116
+ onSelect: handleModelSelect,
117
+ onClose: () => setShowModels(false),
118
+ }) : null,
119
+ h(InputBar, {
120
+ onSubmit: handleSubmit,
121
+ model,
122
+ provider,
123
+ agentMode,
124
+ thinking,
125
+ onSlash: () => setShowCommands(true),
126
+ }),
127
+ h(StatusBar, { provider, model, agentMode })
128
+ );
129
+ }
package/ui/Banner.js ADDED
@@ -0,0 +1,13 @@
1
+ import React from 'react';
2
+ import { Box, Text } from 'ink';
3
+ import BigText from 'ink-big-text';
4
+ import Gradient from 'ink-gradient';
5
+ const { createElement: h } = React;
6
+
7
+ export function Banner() {
8
+ return h(Box, { justifyContent: 'center', marginBottom: 0 },
9
+ h(Gradient, { name: 'cristal' },
10
+ h(BigText, { text: 'CLARITY', font: 'block' })
11
+ )
12
+ );
13
+ }
@@ -0,0 +1,74 @@
1
+ import React, { useState } from 'react';
2
+ import { Box, Text, useInput } from 'ink';
3
+ const { createElement: h } = React;
4
+
5
+ const COMMANDS = [
6
+ { cmd: '/agent', desc: 'Toggle agent mode on/off' },
7
+ { cmd: '/model', desc: 'Switch model' },
8
+ { cmd: '/provider', desc: 'Connect provider' },
9
+ { cmd: '/compact', desc: 'Compact session' },
10
+ { cmd: '/copy', desc: 'Copy session transcript' },
11
+ { cmd: '/diff', desc: 'Open diff viewer' },
12
+ { cmd: '/export', desc: 'Export session transcript' },
13
+ { cmd: '/keys', desc: 'Manage API keys' },
14
+ { cmd: '/history', desc: 'View conversation history' },
15
+ { cmd: '/clear', desc: 'Clear screen' },
16
+ { cmd: '/memory', desc: 'View or clear memory' },
17
+ { cmd: '/config', desc: 'View or edit settings' },
18
+ { cmd: '/search', desc: 'Web search' },
19
+ { cmd: '/fetch', desc: 'Fetch a URL' },
20
+ { cmd: '/run', desc: 'Execute a script file' },
21
+ { cmd: '/undo', desc: 'Revert last AI file change' },
22
+ { cmd: '/help', desc: 'Show help' },
23
+ { cmd: '/exit', desc: 'Exit CLARITY' },
24
+ ];
25
+
26
+ export function CommandPicker({ onSelect, onClose }) {
27
+ const [cursor, setCursor] = useState(0);
28
+ const [filter, setFilter] = useState('');
29
+
30
+ const filtered = COMMANDS.filter(c =>
31
+ c.cmd.includes(filter) || c.desc.toLowerCase().includes(filter)
32
+ );
33
+
34
+ useInput((input, key) => {
35
+ if (key.escape) { onClose(); return; }
36
+ if (key.upArrow) setCursor(c => Math.max(0, c - 1));
37
+ if (key.downArrow) setCursor(c => Math.min(filtered.length - 1, c + 1));
38
+ if (key.return) { onSelect(filtered[cursor]?.cmd); return; }
39
+ if (key.backspace) setFilter(f => f.slice(0, -1));
40
+ else if (input && !key.ctrl && !key.meta) setFilter(f => f + input);
41
+ });
42
+
43
+ return h(Box, {
44
+ borderStyle: 'round',
45
+ borderColor: 'cyan',
46
+ flexDirection: 'column',
47
+ paddingX: 1,
48
+ },
49
+ h(Box, { justifyContent: 'space-between' },
50
+ h(Text, { bold: true, color: 'white' }, 'Commands'),
51
+ h(Text, { color: 'gray' }, 'esc')
52
+ ),
53
+ h(Box, null,
54
+ h(Text, { color: 'gray' }, 'Search '),
55
+ h(Text, { color: filter ? 'white' : 'gray' }, filter || ''),
56
+ h(Text, { color: 'gray' }, '\u2588')
57
+ ),
58
+ h(Box, { flexDirection: 'column', marginTop: 1 },
59
+ filtered.map((c, i) =>
60
+ h(Box, { key: c.cmd, gap: 2 },
61
+ h(Text, {
62
+ color: i === cursor ? 'black' : 'cyan',
63
+ backgroundColor: i === cursor ? 'cyan' : undefined,
64
+ bold: i === cursor,
65
+ }, c.cmd.padEnd(14)),
66
+ h(Text, {
67
+ color: i === cursor ? 'black' : 'gray',
68
+ backgroundColor: i === cursor ? 'cyan' : undefined,
69
+ }, c.desc)
70
+ )
71
+ )
72
+ )
73
+ );
74
+ }
package/ui/InputBar.js ADDED
@@ -0,0 +1,33 @@
1
+ import React, { useState } from 'react';
2
+ import { Box, Text } from 'ink';
3
+ import TextInput from 'ink-text-input';
4
+ const { createElement: h } = React;
5
+
6
+ export function InputBar({ onSubmit, model, provider, agentMode, thinking, onSlash }) {
7
+ const [value, setValue] = useState('');
8
+
9
+ return h(Box, {
10
+ borderStyle: 'round',
11
+ borderColor: thinking ? 'yellow' : 'cyan',
12
+ paddingX: 1,
13
+ flexDirection: 'row',
14
+ alignItems: 'center',
15
+ },
16
+ h(Text, { color: 'magenta', bold: true }, '> '),
17
+ h(TextInput, {
18
+ value,
19
+ onChange: (val) => {
20
+ setValue(val);
21
+ if (val === '/') {
22
+ setValue('');
23
+ onSlash?.();
24
+ }
25
+ },
26
+ onSubmit: (val) => {
27
+ setValue('');
28
+ onSubmit(val);
29
+ },
30
+ placeholder: thinking ? 'Thinking...' : 'Ask anything or type / for commands',
31
+ })
32
+ );
33
+ }
@@ -0,0 +1,50 @@
1
+ import React from 'react';
2
+ import { Box, Text } from 'ink';
3
+ import Spinner from 'ink-spinner';
4
+ const { createElement: h } = React;
5
+
6
+ export function MessageList({ messages, thinking }) {
7
+ return h(Box, { flexDirection: 'column', gap: 1, paddingX: 1 },
8
+ messages.map((msg, i) => {
9
+ if (msg.role === 'user') {
10
+ return h(Box, { key: i, flexDirection: 'column' },
11
+ h(Text, { color: 'magenta', bold: true }, 'YOU \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500'),
12
+ h(Box, { borderStyle: 'single', borderColor: 'magenta', paddingX: 1 },
13
+ h(Text, null, msg.content)
14
+ )
15
+ );
16
+ }
17
+ if (msg.role === 'assistant') {
18
+ return h(Box, { key: i, flexDirection: 'column' },
19
+ h(Text, { color: 'cyan', bold: true }, 'CLARITY \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500'),
20
+ h(Text, null, msg.content)
21
+ );
22
+ }
23
+ if (msg.role === 'tool') {
24
+ return h(Box, { key: i, gap: 1 },
25
+ h(Text, { color: 'green' }, '\u223c'),
26
+ h(Text, { color: 'gray' }, msg.content)
27
+ );
28
+ }
29
+ if (msg.role === 'error') {
30
+ return h(Box, { key: i, gap: 1 },
31
+ h(Text, { color: 'red' }, '\u2717'),
32
+ h(Text, { color: 'red' }, msg.content)
33
+ );
34
+ }
35
+ if (msg.role === 'system') {
36
+ return h(Box, { key: i, gap: 1 },
37
+ h(Text, { color: 'yellow' }, '\u2714'),
38
+ h(Text, { color: 'yellow' }, msg.content)
39
+ );
40
+ }
41
+ return null;
42
+ }),
43
+ thinking ? h(Box, { gap: 1 },
44
+ h(Text, { color: 'yellow' },
45
+ h(Spinner, { type: 'dots' })
46
+ ),
47
+ h(Text, { color: 'yellow' }, 'Thinking')
48
+ ) : null
49
+ );
50
+ }
@@ -0,0 +1,96 @@
1
+ import React, { useState } from 'react';
2
+ import { Box, Text, useInput } from 'ink';
3
+ const { createElement: h } = React;
4
+
5
+ const MODEL_GROUPS = {
6
+ Recent: [
7
+ { id: 'groq/llama-3.3-70b-versatile', label: 'Llama 3.3 70B Versatile', badge: 'Free' },
8
+ ],
9
+ Groq: [
10
+ { id: 'groq/llama-3.3-70b-versatile', label: 'Llama 3.3 70B Versatile' },
11
+ { id: 'groq/llama-3.1-8b-instant', label: 'Llama 3.1 8B Instant' },
12
+ { id: 'groq/llama-4-scout-17b-16e-instruct', label: 'Llama 4 Scout 17B' },
13
+ { id: 'groq/compound-beta', label: 'Compound' },
14
+ { id: 'groq/compound-beta-mini', label: 'Compound Mini' },
15
+ { id: 'groq/kimi-k2-instruct', label: 'Kimi K2 Instruct 0905' },
16
+ { id: 'groq/allam-2-7b', label: 'ALLaM-2-7b' },
17
+ ],
18
+ Gemini: [
19
+ { id: 'gemini/gemini-2.0-flash', label: 'Gemini 2.0 Flash' },
20
+ { id: 'gemini/gemini-2.5-flash-preview', label: 'Gemini 2.5 Flash Preview' },
21
+ { id: 'gemini/gemini-2.5-pro-preview', label: 'Gemini 2.5 Pro Preview' },
22
+ ],
23
+ OpenRouter: [
24
+ { id: 'openrouter/deepseek/deepseek-r1:free', label: 'DeepSeek R1 Free', badge: 'Free' },
25
+ { id: 'openrouter/meta-llama/llama-3.3-70b-instruct:free', label: 'Llama 3.3 70B Free', badge: 'Free' },
26
+ ],
27
+ };
28
+
29
+ export function ModelPicker({ current, onSelect, onClose }) {
30
+ const [filter, setFilter] = useState('');
31
+ const allItems = Object.entries(MODEL_GROUPS).flatMap(([group, models]) =>
32
+ models.map(m => ({ ...m, group }))
33
+ );
34
+ const filtered = filter
35
+ ? allItems.filter(m => m.label.toLowerCase().includes(filter.toLowerCase()))
36
+ : allItems;
37
+
38
+ const [cursor, setCursor] = useState(0);
39
+
40
+ useInput((input, key) => {
41
+ if (key.escape) { onClose(); return; }
42
+ if (key.upArrow) setCursor(c => Math.max(0, c - 1));
43
+ if (key.downArrow) setCursor(c => Math.min(filtered.length - 1, c + 1));
44
+ if (key.return) { onSelect(filtered[cursor]?.id); return; }
45
+ if (key.backspace) setFilter(f => f.slice(0, -1));
46
+ else if (input && !key.ctrl && !key.meta) setFilter(f => f + input);
47
+ });
48
+
49
+ const grouped = {};
50
+ for (const item of filtered) {
51
+ if (!grouped[item.group]) grouped[item.group] = [];
52
+ grouped[item.group].push(item);
53
+ }
54
+
55
+ let globalIdx = 0;
56
+
57
+ return h(Box, {
58
+ borderStyle: 'round',
59
+ borderColor: 'cyan',
60
+ flexDirection: 'column',
61
+ paddingX: 1,
62
+ },
63
+ h(Box, { justifyContent: 'space-between' },
64
+ h(Text, { bold: true }, 'Select model'),
65
+ h(Text, { color: 'gray' }, 'esc')
66
+ ),
67
+ h(Box, null,
68
+ h(Text, { color: filter ? 'white' : 'gray' }, filter || 'Search')
69
+ ),
70
+ h(Box, { flexDirection: 'column', marginTop: 1 },
71
+ Object.entries(grouped).map(([group, models]) =>
72
+ h(Box, { key: group, flexDirection: 'column' },
73
+ h(Text, { color: 'cyan', bold: true }, group),
74
+ models.map((m) => {
75
+ const idx = globalIdx++;
76
+ const selected = idx === cursor;
77
+ return h(Box, { key: m.id, justifyContent: 'space-between' },
78
+ h(Text, {
79
+ backgroundColor: selected ? 'cyan' : undefined,
80
+ color: selected ? 'black' : 'white',
81
+ }, ' ' + m.label),
82
+ m.badge ? h(Text, {
83
+ color: selected ? 'black' : 'gray',
84
+ backgroundColor: selected ? 'cyan' : undefined,
85
+ }, m.badge) : null
86
+ );
87
+ })
88
+ )
89
+ )
90
+ ),
91
+ h(Box, { marginTop: 1, gap: 3 },
92
+ h(Text, { color: 'gray' }, 'Connect provider ', h(Text, { color: 'white' }, 'ctrl+a')),
93
+ h(Text, { color: 'gray' }, 'Favorite ', h(Text, { color: 'white' }, 'ctrl+f'))
94
+ )
95
+ );
96
+ }
@@ -0,0 +1,21 @@
1
+ import React from 'react';
2
+ import { Box, Text } from 'ink';
3
+ const { createElement: h } = React;
4
+
5
+ export function StatusBar({ provider, model, agentMode, contextPct }) {
6
+ return h(Box, { justifyContent: 'space-between', paddingX: 1 },
7
+ h(Box, { gap: 2 },
8
+ h(Text, { color: 'blue', bold: true }, provider),
9
+ h(Text, { color: 'white' }, '\u00b7'),
10
+ h(Text, { color: 'cyan' }, model),
11
+ h(Text, { color: 'white' }, '\u00b7'),
12
+ h(Text, { color: agentMode ? 'green' : 'gray' },
13
+ 'agent:' + (agentMode ? 'ON' : 'OFF')
14
+ )
15
+ ),
16
+ h(Box, { gap: 2 },
17
+ contextPct ? h(Text, { color: 'gray' }, String(contextPct) + 'K') : null,
18
+ h(Text, { color: 'gray', dimColor: true }, 'ctrl+p commands')
19
+ )
20
+ );
21
+ }
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes