clarity-ai 1.2.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 +1 -1
- package/src/commands/index.js +69 -20
- package/src/config/settings.js +2 -2
- package/src/providers/index.js +6 -6
- package/src/ui/chatbox.js +151 -74
- package/src/ui/prompt.js +14 -18
package/package.json
CHANGED
package/src/commands/index.js
CHANGED
|
@@ -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', '/
|
|
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,27 +233,75 @@ const commandRegistry = {
|
|
|
232
233
|
|
|
233
234
|
async model(args) {
|
|
234
235
|
const sub = args[0];
|
|
235
|
-
if (
|
|
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
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
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
|
+
return { resetRL: true };
|
|
271
|
+
},
|
|
272
|
+
|
|
273
|
+
async provider(args) {
|
|
274
|
+
const configured = Object.keys(listKeys());
|
|
275
|
+
if (configured.length === 0) { blocks.info('No Keys', 'Configure keys first with /init'); return; }
|
|
276
|
+
|
|
277
|
+
const { default: inquirer } = await import('inquirer');
|
|
278
|
+
const { provider } = await inquirer.prompt([{
|
|
279
|
+
type: 'list',
|
|
280
|
+
name: 'provider',
|
|
281
|
+
message: 'Select AI provider:',
|
|
282
|
+
choices: configured.map(p => ({
|
|
283
|
+
name: PROVIDER_NAMES[p] || p,
|
|
284
|
+
value: p,
|
|
285
|
+
})),
|
|
286
|
+
}]);
|
|
287
|
+
|
|
288
|
+
const models = listModels(provider);
|
|
289
|
+
if (models.length > 0) {
|
|
290
|
+
const { model } = await inquirer.prompt([{
|
|
291
|
+
type: 'list',
|
|
292
|
+
name: 'model',
|
|
293
|
+
message: `Select ${PROVIDER_NAMES[provider]} model:`,
|
|
294
|
+
choices: models.map(m => ({
|
|
295
|
+
name: `${provider}/${m}`,
|
|
296
|
+
value: `${provider}/${m}`,
|
|
297
|
+
})),
|
|
298
|
+
}]);
|
|
299
|
+
settings.set('defaultModel', model);
|
|
300
|
+
blocks.success('Provider & Model Set', `${PROVIDER_NAMES[provider]} → ${model}`);
|
|
301
|
+
} else {
|
|
302
|
+
blocks.warn('No Models', `No models listed for ${PROVIDER_NAMES[provider]}`);
|
|
255
303
|
}
|
|
304
|
+
return { resetRL: true };
|
|
256
305
|
},
|
|
257
306
|
|
|
258
307
|
config(args) {
|
package/src/config/settings.js
CHANGED
|
@@ -3,7 +3,7 @@ import paths from './paths.js';
|
|
|
3
3
|
|
|
4
4
|
const schema = {
|
|
5
5
|
theme: { type: 'string', default: 'dark' },
|
|
6
|
-
|
|
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/
|
|
20
|
+
defaultModel: 'groq/llama-3.3-70b-versatile',
|
|
21
21
|
stream: true,
|
|
22
22
|
showTokens: true,
|
|
23
23
|
saveHistory: true,
|
package/src/providers/index.js
CHANGED
|
@@ -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: ['
|
|
14
|
-
gemini: { free: true, streaming: true, models: ['gemini-
|
|
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.
|
|
17
|
-
anthropic: { free: false, streaming: true, models: ['claude-3-5-haiku-
|
|
18
|
-
openai: { free: false, streaming: true, models: ['gpt-4o-mini', 'gpt-4o', 'gpt-
|
|
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/chatbox.js
CHANGED
|
@@ -4,17 +4,26 @@ 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,
|
|
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
|
-
import agentManager from '../agents/manager.js';
|
|
11
10
|
import commandRegistry from '../commands/index.js';
|
|
12
11
|
import memory from '../memory/store.js';
|
|
13
|
-
import { runTool } from '../tools/index.js';
|
|
14
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
|
+
|
|
19
|
+
const RESET = '\x1b[0m';
|
|
20
|
+
const GREY_BG = '\x1b[48;5;236m';
|
|
21
|
+
const PURPLE_BG = '\x1b[48;5;53m';
|
|
22
|
+
const FILL = '░';
|
|
15
23
|
|
|
16
24
|
let rl = null;
|
|
17
25
|
let conversation = [];
|
|
26
|
+
let cmdBuffer = '';
|
|
18
27
|
|
|
19
28
|
const SYSTEM_PROMPT = `You are CLARITY, an autonomous AI agent running in the user's terminal (Termux on Android).
|
|
20
29
|
|
|
@@ -30,122 +39,188 @@ Current environment: ${process.platform} ${process.arch}, Node ${process.version
|
|
|
30
39
|
Directory: ${process.cwd()}
|
|
31
40
|
Termux: ${isTermux()}`;
|
|
32
41
|
|
|
33
|
-
function
|
|
34
|
-
|
|
35
|
-
const w = process.stdout.columns || 80;
|
|
36
|
-
const left = c.accent('◈');
|
|
37
|
-
const mid = c.muted(` ${model} `);
|
|
38
|
-
const right = c.muted('/help');
|
|
39
|
-
const pad = '.'.repeat(Math.max(0, Math.floor((w - mid.length - right.length - 8) / 2)));
|
|
40
|
-
console.log(c.border(` ${left}${c.dim(pad)}${mid}${c.dim(pad)}${right}`));
|
|
41
|
-
blocks.divider();
|
|
42
|
+
function getW() {
|
|
43
|
+
return process.stdout.columns || 80;
|
|
42
44
|
}
|
|
43
45
|
|
|
44
|
-
function
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
return;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
loadHistory();
|
|
51
|
-
console.clear();
|
|
52
|
-
renderBanner();
|
|
53
|
-
renderHeader();
|
|
54
|
-
console.log();
|
|
46
|
+
function fill(w) {
|
|
47
|
+
return FILL.repeat(w);
|
|
48
|
+
}
|
|
55
49
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
50
|
+
function renderPromptBar() {
|
|
51
|
+
const model = settings.get('defaultModel') || 'groq/llama-3.3-70b-versatile';
|
|
52
|
+
const w = getW();
|
|
53
|
+
const line = '━'.repeat(w - 4);
|
|
54
|
+
const left = ` ${model} `;
|
|
55
|
+
const right = `v${VERSION} /help `;
|
|
56
|
+
const gap = Math.max(0, w - 4 - left.length - right.length);
|
|
59
57
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
if (cmdBuffer) { cmdBuffer = ''; }
|
|
67
|
-
}
|
|
68
|
-
});
|
|
58
|
+
console.log(c.accent(` ┏${line}┓`));
|
|
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('┃')}`));
|
|
62
|
+
console.log(c.accent(` ┗${line}┛`));
|
|
63
|
+
}
|
|
69
64
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
cmdBuffer = '';
|
|
74
|
-
addToHistory(input);
|
|
65
|
+
function showPrompt() {
|
|
66
|
+
process.stdout.write(` ${c.accent('◆')} `);
|
|
67
|
+
}
|
|
75
68
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
conversation.push({ role: 'user', content: input });
|
|
81
|
-
await handleAIResponse();
|
|
82
|
-
}
|
|
83
|
-
showPrompt();
|
|
84
|
-
});
|
|
69
|
+
function attachReadlineHandlers() {
|
|
70
|
+
rl.removeAllListeners('line');
|
|
71
|
+
rl.removeAllListeners('close');
|
|
72
|
+
rl.removeAllListeners('SIGINT');
|
|
85
73
|
|
|
74
|
+
rl.on('line', onLine);
|
|
86
75
|
rl.on('close', () => {
|
|
87
76
|
console.log(c.muted('\nGoodbye!'));
|
|
88
77
|
process.exit(0);
|
|
89
78
|
});
|
|
90
|
-
|
|
91
79
|
rl.on('SIGINT', () => {
|
|
92
80
|
console.log(c.muted('\nUse /exit to quit'));
|
|
81
|
+
renderPromptBar();
|
|
93
82
|
showPrompt();
|
|
94
83
|
});
|
|
95
84
|
}
|
|
96
85
|
|
|
97
|
-
function
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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);
|
|
102
112
|
console.log();
|
|
103
|
-
console.log(c.
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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('│')}`));
|
|
107
118
|
});
|
|
108
|
-
console.log(c.
|
|
119
|
+
console.log(c.primary(` │${GREY_BG}${fill(w - 4)}${RESET}${c.primary('│')}`));
|
|
120
|
+
console.log(c.primary(` └${line}┘`));
|
|
121
|
+
console.log();
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function startChat() {
|
|
125
|
+
if (!hasAnyKeys()) {
|
|
126
|
+
blocks.warn('No API Keys', 'Run /init to configure API keys first.');
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
loadHistory();
|
|
131
|
+
console.clear();
|
|
132
|
+
renderBanner();
|
|
109
133
|
console.log();
|
|
134
|
+
renderPromptBar();
|
|
110
135
|
showPrompt();
|
|
136
|
+
rl = createPrompt();
|
|
137
|
+
attachReadlineHandlers();
|
|
138
|
+
|
|
139
|
+
process.stdin.on('keypress', onKeypress);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function onKeypress(char, key) {
|
|
143
|
+
if (!key) return;
|
|
144
|
+
|
|
145
|
+
if (key.name === 'slash' && !cmdBuffer) {
|
|
146
|
+
cmdBuffer = '/';
|
|
147
|
+
showCmdList();
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
if (key.name === 'escape') {
|
|
151
|
+
cmdBuffer = '';
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
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);
|
|
162
|
+
top.forEach(([cmd, desc], i) => {
|
|
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('│')}`));
|
|
165
|
+
});
|
|
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)}┘`));
|
|
111
171
|
}
|
|
112
172
|
|
|
113
173
|
async function handleAIResponse() {
|
|
114
|
-
const model = settings.get('defaultModel') || 'groq/
|
|
174
|
+
const model = settings.get('defaultModel') || 'groq/llama-3.3-70b-versatile';
|
|
115
175
|
const [provider] = model.split('/');
|
|
116
176
|
const apiKey = getKey(provider);
|
|
117
177
|
|
|
118
178
|
if (!apiKey) {
|
|
119
|
-
blocks.error('Key Missing', `No
|
|
179
|
+
blocks.error('Key Missing', `No key for ${PROVIDER_NAMES[provider] || provider}`);
|
|
120
180
|
return;
|
|
121
181
|
}
|
|
122
182
|
|
|
123
|
-
const systemMsg = {
|
|
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
|
+
};
|
|
124
187
|
const messages = [systemMsg, ...conversation];
|
|
125
188
|
|
|
126
|
-
spin.start('
|
|
189
|
+
spin.start('thinking...');
|
|
127
190
|
|
|
128
191
|
try {
|
|
129
192
|
const stream = sendMessage(apiKey, messages, model, settings.get('stream'));
|
|
193
|
+
const streaming = settings.get('stream');
|
|
130
194
|
|
|
131
|
-
if (
|
|
195
|
+
if (streaming) {
|
|
132
196
|
spin.stop();
|
|
133
|
-
const w =
|
|
134
|
-
|
|
135
|
-
|
|
197
|
+
const w = getW();
|
|
198
|
+
const line = '━'.repeat(w - 4);
|
|
199
|
+
|
|
200
|
+
console.log(c.accent(` ┏${line}┓`));
|
|
201
|
+
console.log(c.accent(` ┃${PURPLE_BG}${fill(w - 4)}${RESET}${c.accent('┃')}`));
|
|
202
|
+
process.stdout.write(c.accent(` ┃${PURPLE_BG} ${c.ai('CLARITY')} `));
|
|
136
203
|
let full = '';
|
|
204
|
+
|
|
137
205
|
try {
|
|
138
206
|
for await (const chunk of stream) {
|
|
139
207
|
full += chunk;
|
|
140
|
-
process.stdout.write(
|
|
208
|
+
process.stdout.write(chunk);
|
|
141
209
|
}
|
|
142
210
|
} catch (streamErr) {
|
|
143
|
-
if (full) process.stdout.write(c.warning('\n\n[
|
|
211
|
+
if (full) process.stdout.write(c.warning('\n\n[interrupted]'));
|
|
144
212
|
else throw streamErr;
|
|
145
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('┃')}`);
|
|
146
219
|
console.log();
|
|
147
|
-
console.log(c.accent(`
|
|
220
|
+
console.log(c.accent(` ┃${PURPLE_BG}${fill(w - 4)}${RESET}${c.accent('┃')}`));
|
|
221
|
+
console.log(c.accent(` ┗${line}┛`));
|
|
148
222
|
console.log();
|
|
223
|
+
|
|
149
224
|
if (full.trim()) {
|
|
150
225
|
conversation.push({ role: 'assistant', content: full });
|
|
151
226
|
memory.add(conversation);
|
|
@@ -164,9 +239,9 @@ async function handleAIResponse() {
|
|
|
164
239
|
}
|
|
165
240
|
|
|
166
241
|
if (settings.get('showTokens')) {
|
|
167
|
-
const
|
|
168
|
-
const
|
|
169
|
-
console.log(c.dim(` tokens: ${
|
|
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`));
|
|
170
245
|
}
|
|
171
246
|
} catch (err) {
|
|
172
247
|
spin.fail('Error');
|
|
@@ -175,7 +250,9 @@ async function handleAIResponse() {
|
|
|
175
250
|
}
|
|
176
251
|
|
|
177
252
|
function closeChat() {
|
|
253
|
+
process.stdin.removeListener('keypress', onKeypress);
|
|
178
254
|
if (rl) rl.close();
|
|
255
|
+
if (process.stdin.isTTY) process.stdin.setRawMode(false);
|
|
179
256
|
console.log(c.muted('\nGoodbye!'));
|
|
180
257
|
process.exit(0);
|
|
181
258
|
}
|
package/src/ui/prompt.js
CHANGED
|
@@ -2,7 +2,6 @@ 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;
|
|
8
7
|
const ALL_COMMANDS = [
|
|
@@ -16,6 +15,8 @@ const ALL_COMMANDS = [
|
|
|
16
15
|
['keys list', 'List API keys'],
|
|
17
16
|
['keys remove', 'Remove API key'],
|
|
18
17
|
['keys test', 'Test API key'],
|
|
18
|
+
['provider', 'Switch AI provider'],
|
|
19
|
+
['model', 'Select AI model'],
|
|
19
20
|
['model set', 'Set default model'],
|
|
20
21
|
['model list', 'List models'],
|
|
21
22
|
['config show', 'Show config'],
|
|
@@ -73,7 +74,6 @@ const ALL_COMMANDS = [
|
|
|
73
74
|
|
|
74
75
|
const MAX_HISTORY = 500;
|
|
75
76
|
let history = [];
|
|
76
|
-
let showCmdSuggest = false;
|
|
77
77
|
|
|
78
78
|
function loadHistory() {
|
|
79
79
|
try {
|
|
@@ -99,17 +99,6 @@ function addToHistory(line) {
|
|
|
99
99
|
}
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
-
function showSuggestions(rl, partial) {
|
|
103
|
-
const p = partial.replace('/', '');
|
|
104
|
-
if (!p) {
|
|
105
|
-
showCmdSuggest = true;
|
|
106
|
-
return ALL_COMMANDS.slice(0, 8);
|
|
107
|
-
}
|
|
108
|
-
const matches = ALL_COMMANDS.filter(([cmd]) => cmd.startsWith(p));
|
|
109
|
-
showCmdSuggest = matches.length > 0;
|
|
110
|
-
return matches.slice(0, 8);
|
|
111
|
-
}
|
|
112
|
-
|
|
113
102
|
function createPrompt() {
|
|
114
103
|
loadHistory();
|
|
115
104
|
const rl = readline.createInterface({
|
|
@@ -119,12 +108,19 @@ function createPrompt() {
|
|
|
119
108
|
historySize: MAX_HISTORY,
|
|
120
109
|
terminal: true,
|
|
121
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
|
+
}
|
|
122
122
|
});
|
|
123
123
|
return rl;
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
-
|
|
127
|
-
process.stdout.write(` ${c.primary('◆')} `);
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
export { createPrompt, showPrompt, addToHistory, loadHistory, history, showSuggestions, ALL_COMMANDS };
|
|
126
|
+
export { createPrompt, addToHistory, loadHistory, history, ALL_COMMANDS };
|