clarity-ai 1.3.0 → 1.3.1

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.3.0",
3
+ "version": "1.3.1",
4
4
  "description": "AI agent CLI for Termux and terminal — chat, code, automate",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -267,6 +267,7 @@ const commandRegistry = {
267
267
  }]);
268
268
  settings.set('defaultModel', model);
269
269
  blocks.success('Model Set', `Default model: ${model}`);
270
+ return { resetRL: true };
270
271
  },
271
272
 
272
273
  async provider(args) {
@@ -300,6 +301,7 @@ const commandRegistry = {
300
301
  } else {
301
302
  blocks.warn('No Models', `No models listed for ${PROVIDER_NAMES[provider]}`);
302
303
  }
304
+ return { resetRL: true };
303
305
  },
304
306
 
305
307
  config(args) {
package/src/ui/chatbox.js CHANGED
@@ -15,10 +15,15 @@ import { readFileSync } from 'fs';
15
15
 
16
16
  const PKG = JSON.parse(readFileSync(new URL('../../package.json', import.meta.url)));
17
17
  const VERSION = PKG.version;
18
+
18
19
  const RESET = '\x1b[0m';
20
+ const GREY_BG = '\x1b[48;5;236m';
21
+ const PURPLE_BG = '\x1b[48;5;53m';
22
+ const FILL = '░';
19
23
 
20
24
  let rl = null;
21
25
  let conversation = [];
26
+ let cmdBuffer = '';
22
27
 
23
28
  const SYSTEM_PROMPT = `You are CLARITY, an autonomous AI agent running in the user's terminal (Termux on Android).
24
29
 
@@ -34,20 +39,26 @@ Current environment: ${process.platform} ${process.arch}, Node ${process.version
34
39
  Directory: ${process.cwd()}
35
40
  Termux: ${isTermux()}`;
36
41
 
37
- const GREY_BG = '\x1b[48;5;236m';
42
+ function getW() {
43
+ return process.stdout.columns || 80;
44
+ }
45
+
46
+ function fill(w) {
47
+ return FILL.repeat(w);
48
+ }
38
49
 
39
50
  function renderPromptBar() {
40
51
  const model = settings.get('defaultModel') || 'groq/llama-3.3-70b-versatile';
41
- const w = process.stdout.columns || 80;
52
+ const w = getW();
42
53
  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);
54
+ const left = ` ${model} `;
55
+ const right = `v${VERSION} /help `;
56
+ const gap = Math.max(0, w - 4 - left.length - right.length);
46
57
 
47
58
  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('┃')}`));
59
+ console.log(c.accent(` ┃${GREY_BG}${fill(w - 4)}${RESET}${c.accent('┃')}`));
60
+ console.log(c.accent(` ┃${GREY_BG}${c.muted(left)}${fill(gap)}${c.muted(right)}${RESET}${c.accent('┃')}`));
61
+ console.log(c.accent(` ┃${GREY_BG}${fill(w - 4)}${RESET}${c.accent('┃')}`));
51
62
  console.log(c.accent(` ┗${line}┛`));
52
63
  }
53
64
 
@@ -55,6 +66,61 @@ function showPrompt() {
55
66
  process.stdout.write(` ${c.accent('◆')} `);
56
67
  }
57
68
 
69
+ function attachReadlineHandlers() {
70
+ rl.removeAllListeners('line');
71
+ rl.removeAllListeners('close');
72
+ rl.removeAllListeners('SIGINT');
73
+
74
+ rl.on('line', onLine);
75
+ rl.on('close', () => {
76
+ console.log(c.muted('\nGoodbye!'));
77
+ process.exit(0);
78
+ });
79
+ rl.on('SIGINT', () => {
80
+ console.log(c.muted('\nUse /exit to quit'));
81
+ renderPromptBar();
82
+ showPrompt();
83
+ });
84
+ }
85
+
86
+ async function onLine(line) {
87
+ const input = line.trim();
88
+ if (!input) { renderPromptBar(); showPrompt(); return; }
89
+ cmdBuffer = '';
90
+ addToHistory(input);
91
+
92
+ if (input.startsWith('/')) {
93
+ const result = await commandRegistry.execute(input, { rl, conversation, showPrompt });
94
+ if (result?.exit) { closeChat(); return; }
95
+ if (result?.resetRL) {
96
+ rl.close();
97
+ rl = createPrompt();
98
+ attachReadlineHandlers();
99
+ }
100
+ } else {
101
+ conversation.push({ role: 'user', content: input });
102
+ renderUserMsg(input);
103
+ await handleAIResponse();
104
+ }
105
+ renderPromptBar();
106
+ showPrompt();
107
+ }
108
+
109
+ function renderUserMsg(text) {
110
+ const w = getW();
111
+ const line = '─'.repeat(w - 4);
112
+ console.log();
113
+ console.log(c.primary(` ┌${line}┐`));
114
+ console.log(c.primary(` │${GREY_BG}${fill(w - 4)}${RESET}${c.primary('│')}`));
115
+ text.split('\n').forEach(l => {
116
+ const clean = l.replace(/\x1b\[[0-9;]*m/g, '');
117
+ console.log(c.primary(` │${GREY_BG} ${c.user(l)}${fill(Math.max(0, w - 6 - clean.length))}${RESET}${c.primary('│')}`));
118
+ });
119
+ console.log(c.primary(` │${GREY_BG}${fill(w - 4)}${RESET}${c.primary('│')}`));
120
+ console.log(c.primary(` └${line}┘`));
121
+ console.log();
122
+ }
123
+
58
124
  function startChat() {
59
125
  if (!hasAnyKeys()) {
60
126
  blocks.warn('No API Keys', 'Run /init to configure API keys first.');
@@ -68,64 +134,40 @@ function startChat() {
68
134
  renderPromptBar();
69
135
  showPrompt();
70
136
  rl = createPrompt();
137
+ attachReadlineHandlers();
71
138
 
72
- let cmdBuffer = '';
139
+ process.stdin.on('keypress', onKeypress);
140
+ }
73
141
 
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
- });
142
+ function onKeypress(char, key) {
143
+ if (!key) return;
87
144
 
88
- rl.on('line', async (line) => {
89
- const input = line.trim();
90
- if (!input) { renderPromptBar(); showPrompt(); return; }
145
+ if (key.name === 'slash' && !cmdBuffer) {
146
+ cmdBuffer = '/';
147
+ showCmdList();
148
+ return;
149
+ }
150
+ if (key.name === 'escape') {
91
151
  cmdBuffer = '';
92
- addToHistory(input);
93
-
94
- if (input.startsWith('/')) {
95
- const result = await commandRegistry.execute(input, { rl, conversation, showPrompt });
96
- if (result?.exit) { closeChat(); return; }
97
- } else {
98
- conversation.push({ role: 'user', content: input });
99
- await handleAIResponse();
100
- }
101
- renderPromptBar();
102
- showPrompt();
103
- });
104
-
105
- rl.on('close', () => {
106
- console.log(c.muted('\nGoodbye!'));
107
- process.exit(0);
108
- });
109
-
110
- rl.on('SIGINT', () => {
111
- console.log(c.muted('\nUse /exit to quit'));
112
- renderPromptBar();
113
- showPrompt();
114
- });
152
+ }
115
153
  }
116
154
 
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)}`));
155
+ function showCmdList() {
156
+ const w = getW();
157
+ readline.clearLine(process.stdout, 0);
158
+ readline.cursorTo(process.stdout, 0);
159
+ console.log(c.accent(` ┌${'─'.repeat(w - 6)}┐`));
160
+ console.log(c.accent(` │${GREY_BG}${fill(w - 6)}${RESET}${c.accent('│')}`));
161
+ const top = ALL_COMMANDS.slice(0, 8);
124
162
  top.forEach(([cmd, desc], i) => {
125
- const tag = blocks.badge('/' + cmd, i === 0 ? 'cyan' : 'purple');
126
- console.log(` ${tag} ${c.muted(desc)}`);
163
+ const tag = i === 0 ? c.primary('/' + cmd) : c.accent('/' + cmd);
164
+ console.log(c.accent(` │${GREY_BG} ${tag}${fill(Math.max(0, w - 10 - cmd.length - desc.length))}${c.muted(desc)} ${RESET}${c.accent('│')}`));
127
165
  });
128
- console.log(c.dim(` ${'─'.repeat(w - 4)}`));
166
+ if (ALL_COMMANDS.length > 8) {
167
+ console.log(c.accent(` │${GREY_BG} ${c.muted('... and ' + (ALL_COMMANDS.length - 8) + ' more — Tab to complete')}${fill(Math.max(0, w - 12 - 28))}${RESET}${c.accent('│')}`));
168
+ }
169
+ console.log(c.accent(` │${GREY_BG}${fill(w - 6)}${RESET}${c.accent('│')}`));
170
+ console.log(c.accent(` └${'─'.repeat(w - 6)}┘`));
129
171
  }
130
172
 
131
173
  async function handleAIResponse() {
@@ -134,40 +176,51 @@ async function handleAIResponse() {
134
176
  const apiKey = getKey(provider);
135
177
 
136
178
  if (!apiKey) {
137
- blocks.error('Key Missing', `No API key for ${PROVIDER_NAMES[provider] || provider}`);
179
+ blocks.error('Key Missing', `No key for ${PROVIDER_NAMES[provider] || provider}`);
138
180
  return;
139
181
  }
140
182
 
141
- const systemMsg = { role: 'system', content: SYSTEM_PROMPT + '\n\nUser memory: ' + (memory.show().filter(m => m.role === 'system').map(m => m.content).join('; ') || 'none') };
183
+ const systemMsg = {
184
+ role: 'system',
185
+ content: SYSTEM_PROMPT + '\n\nUser memory: ' + (memory.show().filter(m => m.role === 'system').map(m => m.content).join('; ') || 'none')
186
+ };
142
187
  const messages = [systemMsg, ...conversation];
143
188
 
144
- spin.start('CLARITY is thinking...');
189
+ spin.start('thinking...');
145
190
 
146
191
  try {
147
192
  const stream = sendMessage(apiKey, messages, model, settings.get('stream'));
193
+ const streaming = settings.get('stream');
148
194
 
149
- if (settings.get('stream')) {
195
+ if (streaming) {
150
196
  spin.stop();
151
- const w = process.stdout.columns || 80;
197
+ const w = getW();
152
198
  const line = '━'.repeat(w - 4);
153
- const PURPLE_BG = '\x1b[48;5;53m';
199
+
154
200
  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('')}`));
201
+ console.log(c.accent(` ┃${PURPLE_BG}${fill(w - 4)}${RESET}${c.accent('┃')}`));
202
+ process.stdout.write(c.accent(` ┃${PURPLE_BG} ${c.ai('CLARITY')} `));
157
203
  let full = '';
204
+
158
205
  try {
159
206
  for await (const chunk of stream) {
160
207
  full += chunk;
161
- process.stdout.write(c.white(chunk));
208
+ process.stdout.write(chunk);
162
209
  }
163
210
  } catch (streamErr) {
164
- if (full) process.stdout.write(c.warning('\n\n[stream interrupted]'));
211
+ if (full) process.stdout.write(c.warning('\n\n[interrupted]'));
165
212
  else throw streamErr;
166
213
  }
214
+
215
+ const clean = full.replace(/\x1b\[[0-9;]*m/g, '');
216
+ const lastLine = clean.includes('\n') ? clean.split('\n').pop() : clean;
217
+ const remain = Math.max(0, w - 8 - lastLine.length);
218
+ process.stdout.write(`${fill(remain)}${RESET}${c.accent('┃')}`);
167
219
  console.log();
168
- console.log(c.accent(` ┃${PURPLE_BG}${' '.repeat(w - 4)}${RESET}${c.accent('┃')}`));
220
+ console.log(c.accent(` ┃${PURPLE_BG}${fill(w - 4)}${RESET}${c.accent('┃')}`));
169
221
  console.log(c.accent(` ┗${line}┛`));
170
222
  console.log();
223
+
171
224
  if (full.trim()) {
172
225
  conversation.push({ role: 'assistant', content: full });
173
226
  memory.add(conversation);
@@ -186,9 +239,9 @@ async function handleAIResponse() {
186
239
  }
187
240
 
188
241
  if (settings.get('showTokens')) {
189
- const inTokens = Math.ceil(conversation.reduce((s, m) => s + m.content.length, 0) / 4);
190
- const outTokens = Math.ceil(conversation.filter(m => m.role === 'assistant').reduce((s, m) => s + m.content.length, 0) / 4);
191
- console.log(c.dim(` tokens: ${inTokens} in / ${outTokens} out • free`));
242
+ const inTok = Math.ceil(conversation.reduce((s, m) => s + m.content.length, 0) / 4);
243
+ const outTok = Math.ceil(conversation.filter(m => m.role === 'assistant').reduce((s, m) => s + m.content.length, 0) / 4);
244
+ console.log(c.dim(` tokens: ${inTok} in / ${outTok} out`));
192
245
  }
193
246
  } catch (err) {
194
247
  spin.fail('Error');
@@ -197,7 +250,9 @@ async function handleAIResponse() {
197
250
  }
198
251
 
199
252
  function closeChat() {
253
+ process.stdin.removeListener('keypress', onKeypress);
200
254
  if (rl) rl.close();
255
+ if (process.stdin.isTTY) process.stdin.setRawMode(false);
201
256
  console.log(c.muted('\nGoodbye!'));
202
257
  process.exit(0);
203
258
  }
package/src/ui/prompt.js CHANGED
@@ -108,6 +108,17 @@ function createPrompt() {
108
108
  historySize: MAX_HISTORY,
109
109
  terminal: true,
110
110
  prompt: '',
111
+ completer: (line) => {
112
+ if (line.startsWith('/')) {
113
+ const hits = ALL_COMMANDS.filter(([cmd]) => {
114
+ const fullCmd = '/' + cmd;
115
+ return fullCmd.startsWith(line);
116
+ }).map(([cmd]) => '/' + cmd);
117
+ if (hits.length === 0) return [[], line];
118
+ return [hits, line];
119
+ }
120
+ return [[], line];
121
+ }
111
122
  });
112
123
  return rl;
113
124
  }