clarity-ai 3.1.0 → 3.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +71 -0
- package/package.json +1 -1
- package/src/agents/loop.js +240 -149
- package/src/commands/index.js +120 -56
- package/src/commands/model.js +19 -4
- package/src/main.js +138 -67
- package/src/providers/index.js +10 -2
- package/src/ui/prompt.js +127 -0
- package/src/ui/spinner.js +98 -23
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# Change Log
|
|
2
|
+
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
## 3.1.0 (2026-06-05)
|
|
6
|
+
|
|
7
|
+
### UI Rewrite — OpenCode Style
|
|
8
|
+
- **User messages**: Light purple background (`#2D1B4E`), `#C39BD3` text, `❯ YOU` header bar
|
|
9
|
+
- **AI responses**: Left border only (`│`), no box, no background, `◆ CLARITY` header
|
|
10
|
+
- **File edits**: OpenCode diff format — `✎ filename` bar + `+`/`-` lines with line numbers
|
|
11
|
+
- **File writes**: `+ filename` header with line count written
|
|
12
|
+
- **Tool calls**: Inline `⚙ toolname · input` with preview output (max 8 lines)
|
|
13
|
+
- **Info/Error/Success/Warning**: Clean single-line renders with icons
|
|
14
|
+
|
|
15
|
+
### System Prompt — Anti-Hallucination
|
|
16
|
+
- Strict rules: NEVER fabricate content, NEVER confirm before acting
|
|
17
|
+
- Always ground responses in actual tool output
|
|
18
|
+
- Read files before editing, never overwrite unread files
|
|
19
|
+
- Short, direct responses — no filler
|
|
20
|
+
|
|
21
|
+
### Agent Loop — Inline Diff Display
|
|
22
|
+
- `executeToolCall()` handles write_file, edit_file, bash, read_file natively
|
|
23
|
+
- edit_file computes unified diff using `diff` package (`diffLines`)
|
|
24
|
+
- `filterContextHunks()` shows ±2 context lines around changes
|
|
25
|
+
- Dynamic fallback for other tools via `import()` from `tools/` directory
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## 3.0.2 (2026-06-05)
|
|
30
|
+
|
|
31
|
+
- Complete rebuild per CLARITY_V3_PROMPT.md spec
|
|
32
|
+
- Removed all Ink+React code (15 files) — replaced with readline-based TUI
|
|
33
|
+
- 25 tool modules in `src/tools/`, 20 command modules in `src/commands/`
|
|
34
|
+
- 6 provider modules with streaming SSE parsing
|
|
35
|
+
- ReAct agent loop with JSON-only tool call parsing
|
|
36
|
+
- Figlet+gradient animated banner, first-run setup wizard
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## 3.0.1 (2026-06-05)
|
|
41
|
+
|
|
42
|
+
- Fixed JSX → createElement conversion in all Ink components
|
|
43
|
+
- Fixed import errors and TTY check
|
|
44
|
+
- Fixed Termux bin wrapper (symlink → bash script)
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## 3.0.0 (2026-06-05)
|
|
49
|
+
|
|
50
|
+
- Complete Ink+React TUI rewrite
|
|
51
|
+
- 11 React components, global state with reducer
|
|
52
|
+
- Parallel subagent spawning
|
|
53
|
+
- 15 tools, 17 commands
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## 2.0.0 (2026-06-04)
|
|
58
|
+
|
|
59
|
+
- Figlet+gradient banner, 12 block types with timestamps
|
|
60
|
+
- 22-token color palette
|
|
61
|
+
- ReAct agent loop with JSON parsing and 3 retry attempts
|
|
62
|
+
- 20 tools, first-run wizard, split commands
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## 1.3.0 (2026-06-04)
|
|
67
|
+
|
|
68
|
+
- Provider model lists updated to 2026
|
|
69
|
+
- Interactive `/model` selector, `/provider` command
|
|
70
|
+
- Grey-fill + purple-outline prompt bar
|
|
71
|
+
- Tab completer on readline
|
package/package.json
CHANGED
package/src/agents/loop.js
CHANGED
|
@@ -1,142 +1,117 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { buildSystemPrompt } from '../core/context.js';
|
|
3
|
-
import { addMessage } from '../core/history.js';
|
|
4
|
-
import { renderEdit, renderWrite, renderTool } from '../ui/blocks.js';
|
|
5
|
-
import { diffLines } from 'diff';
|
|
1
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
|
|
6
2
|
import { execSync } from 'child_process';
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
'
|
|
16
|
-
'
|
|
17
|
-
'
|
|
18
|
-
'
|
|
19
|
-
'
|
|
20
|
-
'
|
|
21
|
-
'
|
|
22
|
-
'
|
|
23
|
-
'
|
|
24
|
-
'
|
|
25
|
-
'
|
|
26
|
-
'
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
'
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
'
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
'Respond with JSON:\n' +
|
|
57
|
-
'{"tool": "tool_name", "args": {"arg1": "val1"}}\n' +
|
|
58
|
-
'or to respond to user:\n' +
|
|
59
|
-
'{"response": "your final answer here"}';
|
|
60
|
-
|
|
61
|
-
function filterContextHunks(hunks, ctx) {
|
|
62
|
-
const changedIdx = hunks
|
|
63
|
-
.map((h, i) => (h.type !== 'context' ? i : -1))
|
|
64
|
-
.filter((i) => i >= 0);
|
|
65
|
-
return hunks.filter((_, i) => {
|
|
66
|
-
if (hunks[i].type !== 'context') return true;
|
|
67
|
-
for (const c of changedIdx) {
|
|
68
|
-
if (Math.abs(i - c) <= ctx) return true;
|
|
69
|
-
}
|
|
70
|
-
return false;
|
|
71
|
-
});
|
|
3
|
+
import { dirname } from 'path';
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
import { Spinner, CollapsibleStep } from '../ui/spinner.js';
|
|
6
|
+
import { renderAI, renderWrite, renderEdit, renderTool } from '../ui/blocks.js';
|
|
7
|
+
import { callProvider, streamProvider } from '../providers/index.js';
|
|
8
|
+
import { addMessage, saveHistory } from '../core/history.js';
|
|
9
|
+
import { diffLines } from 'diff';
|
|
10
|
+
|
|
11
|
+
const SYSTEM_PROMPT = 'You are CLARITY, an autonomous AI agent CLI running in Termux on Android.\n\n' +
|
|
12
|
+
'## CRITICAL RULES\n' +
|
|
13
|
+
'1. NEVER fabricate file contents or command outputs.\n' +
|
|
14
|
+
'2. NEVER confirm before acting — just do it.\n' +
|
|
15
|
+
'3. ALWAYS ground responses in actual tool output.\n' +
|
|
16
|
+
'4. SHORT responses. No filler.\n' +
|
|
17
|
+
'5. When editing files, read first then edit.\n\n' +
|
|
18
|
+
'## RESPONSE FORMAT\n' +
|
|
19
|
+
'Respond with JSON ONLY:\n' +
|
|
20
|
+
'To use a tool: {"tool": "tool_name", "args": {"key": "value"}}\n' +
|
|
21
|
+
'To answer: {"response": "your message"}\n\n' +
|
|
22
|
+
'Available tools: bash, read_file, write_file, edit_file, list_directory, search_files, grep, web_search, web_fetch, git, memory, code_runner, pkg_manager';
|
|
23
|
+
|
|
24
|
+
function detectIntent(message) {
|
|
25
|
+
const lower = message.toLowerCase().trim();
|
|
26
|
+
const chatPatterns = [
|
|
27
|
+
/^(hi|hello|hey|sup|yo|what'?s up|howdy)/,
|
|
28
|
+
/^(how are you|how do you|what are you|who are you|what can you)/,
|
|
29
|
+
/^(thanks|thank you|ok|okay|cool|nice|great|good|sure|yes|no|yep|nope)/,
|
|
30
|
+
/^(what is|what's|explain|tell me about|describe|define)/,
|
|
31
|
+
/^(can you|could you|would you|will you)\s+(explain|tell|describe|help me understand)/,
|
|
32
|
+
];
|
|
33
|
+
for (const p of chatPatterns) {
|
|
34
|
+
if (p.test(lower)) return 'chat';
|
|
35
|
+
}
|
|
36
|
+
const toolPatterns = [
|
|
37
|
+
{ re: /(create|make|mkdir|new)\s+(a\s+)?(dir|directory|folder)/, intent: 'bash' },
|
|
38
|
+
{ re: /(create|make|write|generate)\s+(a\s+)?(file|script|code|program)/, intent: 'write_file' },
|
|
39
|
+
{ re: /(run|execute|launch|start)\s+/, intent: 'bash' },
|
|
40
|
+
{ re: /(edit|modify|change|update|fix)\s+(the\s+)?file/, intent: 'edit_file' },
|
|
41
|
+
{ re: /(read|show|display|cat|open)\s+(the\s+)?file/, intent: 'read_file' },
|
|
42
|
+
{ re: /(list|ls|show)\s+(files|dir|directory|folders)/, intent: 'list_directory' },
|
|
43
|
+
{ re: /(search|find|grep|look for)/, intent: 'search' },
|
|
44
|
+
{ re: /(install|npm|pip|pkg)\s+/, intent: 'bash' },
|
|
45
|
+
{ re: /(git\s+)/, intent: 'git' },
|
|
46
|
+
{ re: /(cd\s+|navigate to|go to)/, intent: 'bash' },
|
|
47
|
+
];
|
|
48
|
+
for (const { re } of toolPatterns) {
|
|
49
|
+
if (re.test(lower)) return 'agent';
|
|
50
|
+
}
|
|
51
|
+
return 'chat';
|
|
72
52
|
}
|
|
73
53
|
|
|
74
|
-
async function
|
|
75
|
-
switch (
|
|
54
|
+
async function executeTool(name, args) {
|
|
55
|
+
switch (name) {
|
|
56
|
+
case 'bash': {
|
|
57
|
+
try {
|
|
58
|
+
const out = execSync(args.command, {
|
|
59
|
+
encoding: 'utf-8', timeout: 30000,
|
|
60
|
+
cwd: process.env.HOME || '/data/data/com.termux/files/home',
|
|
61
|
+
});
|
|
62
|
+
return out.trim() || '(no output)';
|
|
63
|
+
} catch (e) {
|
|
64
|
+
return 'ERROR: ' + (e.stderr || e.message || String(e));
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
case 'read_file': {
|
|
68
|
+
if (!existsSync(args.path)) return 'ERROR: File not found: ' + args.path;
|
|
69
|
+
return readFileSync(args.path, 'utf-8');
|
|
70
|
+
}
|
|
76
71
|
case 'write_file': {
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
writeFileSync(path, content, 'utf-8');
|
|
80
|
-
|
|
81
|
-
return 'Written ' + lines + ' lines to ' + path;
|
|
72
|
+
const dir = dirname(args.path);
|
|
73
|
+
if (dir !== '.') mkdirSync(dir, { recursive: true });
|
|
74
|
+
writeFileSync(args.path, args.content, 'utf-8');
|
|
75
|
+
const lines = args.content.split('\n').length;
|
|
76
|
+
return 'Written ' + lines + ' lines to ' + args.path;
|
|
82
77
|
}
|
|
83
|
-
|
|
84
78
|
case 'edit_file': {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
const updated = original.replace(old_str, new_str);
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
hunks.push({ type: 'remove', line, lineNum });
|
|
99
|
-
} else {
|
|
100
|
-
hunks.push({ type: 'context', line, lineNum });
|
|
101
|
-
}
|
|
102
|
-
lineNum++;
|
|
103
|
-
}
|
|
79
|
+
if (!existsSync(args.path)) return 'ERROR: File not found: ' + args.path;
|
|
80
|
+
const original = readFileSync(args.path, 'utf-8');
|
|
81
|
+
if (!original.includes(args.old_str)) return 'ERROR: String not found in ' + args.path;
|
|
82
|
+
const updated = original.replace(args.old_str, args.new_str);
|
|
83
|
+
writeFileSync(args.path, updated, 'utf-8');
|
|
84
|
+
return 'Edited ' + args.path;
|
|
85
|
+
}
|
|
86
|
+
case 'list_directory': {
|
|
87
|
+
try {
|
|
88
|
+
const out = execSync('ls -la "' + (args.path || '.') + '"', { encoding: 'utf-8' });
|
|
89
|
+
return out.trim();
|
|
90
|
+
} catch (e) {
|
|
91
|
+
return 'ERROR: ' + e.message;
|
|
104
92
|
}
|
|
105
|
-
|
|
106
|
-
const filtered = filterContextHunks(hunks, 2);
|
|
107
|
-
writeFileSync(path, updated, 'utf-8');
|
|
108
|
-
console.log(renderEdit(path, filtered));
|
|
109
|
-
return 'Edited ' + path;
|
|
110
93
|
}
|
|
111
|
-
|
|
112
|
-
case 'bash': {
|
|
113
|
-
const { command } = toolInput;
|
|
94
|
+
case 'search_files': {
|
|
114
95
|
try {
|
|
115
|
-
const
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
return
|
|
119
|
-
} catch (err) {
|
|
120
|
-
const errMsg = err.stderr || err.message;
|
|
121
|
-
console.log(renderTool('bash', command, errMsg));
|
|
122
|
-
return 'Error: ' + errMsg;
|
|
96
|
+
const out = execSync('find "' + (args.path || '.') + '" -name "' + args.pattern + '" 2>/dev/null | head -20', { encoding: 'utf-8' });
|
|
97
|
+
return out.trim() || 'No files found';
|
|
98
|
+
} catch (e) {
|
|
99
|
+
return 'ERROR: ' + e.message;
|
|
123
100
|
}
|
|
124
101
|
}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
102
|
+
case 'grep': {
|
|
103
|
+
try {
|
|
104
|
+
const out = execSync('grep -rn "' + args.pattern + '" "' + args.path + '" 2>/dev/null | head -20', { encoding: 'utf-8' });
|
|
105
|
+
return out.trim() || 'No matches';
|
|
106
|
+
} catch (e) {
|
|
107
|
+
return 'No matches or error: ' + e.message;
|
|
108
|
+
}
|
|
132
109
|
}
|
|
133
|
-
|
|
134
110
|
default: {
|
|
135
111
|
try {
|
|
136
|
-
const mod = await import('../tools/' +
|
|
112
|
+
const mod = await import('../tools/' + name + '.js');
|
|
137
113
|
const toolFn = Object.values(mod)[0];
|
|
138
|
-
const result = await toolFn(
|
|
139
|
-
console.log(renderTool(toolName, JSON.stringify(toolInput).slice(0, 60), result));
|
|
114
|
+
const result = await toolFn(args);
|
|
140
115
|
return result;
|
|
141
116
|
} catch (e) {
|
|
142
117
|
return 'Tool error: ' + e.message;
|
|
@@ -145,61 +120,177 @@ async function executeToolCall(toolName, toolInput) {
|
|
|
145
120
|
}
|
|
146
121
|
}
|
|
147
122
|
|
|
123
|
+
function renderToolStep(name, args, result, elapsed) {
|
|
124
|
+
const argPreview = (() => {
|
|
125
|
+
if (args.command) return String(args.command).slice(0, 50);
|
|
126
|
+
if (args.path) return args.path;
|
|
127
|
+
if (args.pattern) return args.pattern;
|
|
128
|
+
return JSON.stringify(args).slice(0, 50);
|
|
129
|
+
})();
|
|
130
|
+
|
|
131
|
+
const header =
|
|
132
|
+
chalk.hex('#00FFFF')('\u2699 ') +
|
|
133
|
+
chalk.hex('#00FFFF').bold(name) +
|
|
134
|
+
chalk.dim(' \u00b7 ' + argPreview) +
|
|
135
|
+
chalk.dim(' (' + elapsed + 'ms)');
|
|
136
|
+
console.log(header);
|
|
137
|
+
|
|
138
|
+
if (name === 'write_file' && args.content) {
|
|
139
|
+
const lineCount = args.content.split('\n').length;
|
|
140
|
+
console.log(renderWrite(args.path, lineCount));
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (name === 'edit_file' && existsSync(args.path)) {
|
|
145
|
+
try {
|
|
146
|
+
const original = readFileSync(args.path, 'utf-8');
|
|
147
|
+
const updated = original.replace(args.old_str || '', args.new_str || '');
|
|
148
|
+
const changes = diffLines(original, updated);
|
|
149
|
+
const hunks = [];
|
|
150
|
+
let lineNum = 1;
|
|
151
|
+
for (const part of changes) {
|
|
152
|
+
const partLines = part.value.split('\n');
|
|
153
|
+
if (partLines[partLines.length - 1] === '') partLines.pop();
|
|
154
|
+
for (const line of partLines) {
|
|
155
|
+
if (part.added) hunks.push({ type: 'add', line, lineNum });
|
|
156
|
+
else if (part.removed) hunks.push({ type: 'remove', line, lineNum });
|
|
157
|
+
else hunks.push({ type: 'context', line, lineNum });
|
|
158
|
+
lineNum++;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
const changed = new Set(hunks.map((h, i) => h.type !== 'context' ? i : -1).filter(i => i >= 0));
|
|
162
|
+
const filtered = hunks.filter((_, i) => {
|
|
163
|
+
if (hunks[i].type !== 'context') return true;
|
|
164
|
+
for (const c of changed) if (Math.abs(i - c) <= 2) return true;
|
|
165
|
+
return false;
|
|
166
|
+
});
|
|
167
|
+
console.log(renderEdit(args.path, filtered));
|
|
168
|
+
} catch {}
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (result && result !== '(no output)' && !result.startsWith('ERROR') && name !== 'read_file') {
|
|
173
|
+
const resultLines = String(result).split('\n').slice(0, 6);
|
|
174
|
+
for (const l of resultLines) {
|
|
175
|
+
console.log(chalk.dim(' \u2502 ') + chalk.hex('#AAAAAA')(l));
|
|
176
|
+
}
|
|
177
|
+
if (String(result).split('\n').length > 6) {
|
|
178
|
+
console.log(chalk.dim(' \u2502 ...'));
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (result && String(result).startsWith('ERROR')) {
|
|
183
|
+
console.log(chalk.hex('#FF4757')(' \u2716 ') + chalk.hex('#FF4757')(result));
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
148
187
|
export async function agentLoop(userMessage, config, history) {
|
|
149
|
-
const
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
188
|
+
const intent = detectIntent(userMessage);
|
|
189
|
+
const spinner = new Spinner();
|
|
190
|
+
const msgHistory = Array.isArray(history) ? history : (history.messages || []);
|
|
191
|
+
|
|
192
|
+
if (intent === 'chat') {
|
|
193
|
+
spinner.start('Thinking', 'think');
|
|
194
|
+
const t0 = Date.now();
|
|
195
|
+
|
|
196
|
+
const messages = [
|
|
197
|
+
{ role: 'system', content: 'You are CLARITY-AI, a helpful assistant. Keep responses short.' },
|
|
198
|
+
...msgHistory.slice(-10).map(m => ({ role: m.role === 'assistant' ? 'assistant' : 'user', content: m.content })),
|
|
199
|
+
{ role: 'user', content: userMessage },
|
|
200
|
+
];
|
|
201
|
+
|
|
202
|
+
const response = await callProvider(config, messages);
|
|
203
|
+
const elapsed = Date.now() - t0;
|
|
204
|
+
spinner.stop('Thought ' + (elapsed < 1000 ? elapsed + 'ms' : (elapsed/1000).toFixed(1) + 's'), 'done');
|
|
205
|
+
|
|
206
|
+
addMessage(msgHistory, 'user', userMessage);
|
|
207
|
+
addMessage(msgHistory, 'assistant', response);
|
|
208
|
+
saveHistory(msgHistory);
|
|
209
|
+
|
|
210
|
+
console.log();
|
|
211
|
+
console.log(renderAI(response));
|
|
212
|
+
console.log();
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
let messages = [
|
|
217
|
+
{ role: 'system', content: SYSTEM_PROMPT },
|
|
218
|
+
...msgHistory.slice(-20).map(m => ({ role: m.role === 'assistant' ? 'assistant' : 'user', content: m.content })),
|
|
155
219
|
{ role: 'user', content: userMessage },
|
|
156
220
|
];
|
|
157
221
|
|
|
158
|
-
let
|
|
159
|
-
|
|
160
|
-
|
|
222
|
+
let loopCount = 0;
|
|
223
|
+
const MAX_LOOPS = 15;
|
|
224
|
+
|
|
225
|
+
while (loopCount < MAX_LOOPS) {
|
|
226
|
+
loopCount++;
|
|
227
|
+
|
|
228
|
+
spinner.start('Thinking', 'think');
|
|
229
|
+
const t0 = Date.now();
|
|
161
230
|
|
|
162
|
-
while (toolCalls < maxToolCalls) {
|
|
163
231
|
let fullResponse = '';
|
|
164
|
-
const stream =
|
|
232
|
+
const stream = streamProvider(config, messages);
|
|
165
233
|
for await (const chunk of stream) {
|
|
166
234
|
fullResponse += chunk;
|
|
167
235
|
}
|
|
168
236
|
|
|
237
|
+
const thinkMs = Date.now() - t0;
|
|
238
|
+
spinner.stop('Thought ' + (thinkMs < 1000 ? thinkMs + 'ms' : (thinkMs/1000).toFixed(1) + 's'), 'done');
|
|
239
|
+
|
|
169
240
|
const jsonMatch = fullResponse.match(/\{[^]*\}/);
|
|
170
241
|
if (!jsonMatch) {
|
|
171
|
-
|
|
172
|
-
|
|
242
|
+
console.log();
|
|
243
|
+
console.log(renderAI(fullResponse));
|
|
244
|
+
console.log();
|
|
245
|
+
addMessage(msgHistory, 'user', userMessage);
|
|
246
|
+
addMessage(msgHistory, 'assistant', fullResponse);
|
|
247
|
+
saveHistory(msgHistory);
|
|
248
|
+
return;
|
|
173
249
|
}
|
|
174
250
|
|
|
175
251
|
let parsed;
|
|
176
252
|
try {
|
|
177
253
|
parsed = JSON.parse(jsonMatch[0]);
|
|
178
254
|
} catch {
|
|
179
|
-
|
|
180
|
-
|
|
255
|
+
console.log();
|
|
256
|
+
console.log(renderAI(fullResponse));
|
|
257
|
+
console.log();
|
|
258
|
+
addMessage(msgHistory, 'user', userMessage);
|
|
259
|
+
addMessage(msgHistory, 'assistant', fullResponse);
|
|
260
|
+
saveHistory(msgHistory);
|
|
261
|
+
return;
|
|
181
262
|
}
|
|
182
263
|
|
|
183
264
|
if (parsed.response) {
|
|
184
|
-
|
|
185
|
-
|
|
265
|
+
const finalText = parsed.response;
|
|
266
|
+
console.log();
|
|
267
|
+
console.log(renderAI(finalText));
|
|
268
|
+
console.log();
|
|
269
|
+
addMessage(msgHistory, 'user', userMessage);
|
|
270
|
+
addMessage(msgHistory, 'assistant', finalText);
|
|
271
|
+
saveHistory(msgHistory);
|
|
272
|
+
return;
|
|
186
273
|
}
|
|
187
274
|
|
|
188
275
|
if (parsed.tool) {
|
|
189
|
-
|
|
190
|
-
const toolName = parsed.tool;
|
|
276
|
+
const name = parsed.tool;
|
|
191
277
|
const args = parsed.args || {};
|
|
278
|
+
const argPreview = args.command || args.path || args.pattern || '';
|
|
192
279
|
|
|
193
|
-
|
|
280
|
+
spinner.start('Running ' + name, 'tool');
|
|
281
|
+
spinner.update('Running ' + name, argPreview.slice(0, 40));
|
|
282
|
+
|
|
283
|
+
const ts = Date.now();
|
|
284
|
+
const result = await executeTool(name, args);
|
|
285
|
+
const te = Date.now() - ts;
|
|
286
|
+
|
|
287
|
+
spinner.stop(name + ' \u00b7 ' + argPreview.slice(0, 30), 'done');
|
|
288
|
+
renderToolStep(name, args, result, te);
|
|
194
289
|
|
|
195
290
|
messages.push({ role: 'assistant', content: fullResponse });
|
|
196
291
|
messages.push({ role: 'user', content: 'Tool result: ' + result });
|
|
197
292
|
}
|
|
198
293
|
}
|
|
199
294
|
|
|
200
|
-
|
|
201
|
-
finalResponse += '\n\n(Max tool calls reached)';
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
return finalResponse || 'No response generated';
|
|
295
|
+
console.log(chalk.hex('#FFB800')('\u26a0 Max tool calls reached. Stopping.'));
|
|
205
296
|
}
|
package/src/commands/index.js
CHANGED
|
@@ -1,61 +1,125 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
1
|
+
export const ALL_COMMANDS = [
|
|
2
|
+
{ name: '/help', description: 'Show all commands' },
|
|
3
|
+
{ name: '/agent', description: 'Toggle agent mode on/off' },
|
|
4
|
+
{ name: '/model', description: 'Switch AI model' },
|
|
5
|
+
{ name: '/provider', description: 'Switch AI provider' },
|
|
6
|
+
{ name: '/tools', description: 'List all tools + permissions' },
|
|
7
|
+
{ name: '/memory', description: 'View or clear memory' },
|
|
8
|
+
{ name: '/history', description: 'View conversation history' },
|
|
9
|
+
{ name: '/chat', description: 'Start new chat session' },
|
|
10
|
+
{ name: '/clear', description: 'Clear screen + redraw banner' },
|
|
11
|
+
{ name: '/task', description: 'Run a full autonomous task' },
|
|
12
|
+
{ name: '/search', description: 'Web search' },
|
|
13
|
+
{ name: '/fetch', description: 'Fetch a URL' },
|
|
14
|
+
{ name: '/git', description: 'Git operations' },
|
|
15
|
+
{ name: '/diff', description: 'Compare two files' },
|
|
16
|
+
{ name: '/run', description: 'Execute a script file' },
|
|
17
|
+
{ name: '/keys', description: 'Manage API keys' },
|
|
18
|
+
{ name: '/config', description: 'View or edit settings' },
|
|
19
|
+
{ name: '/export', description: 'Export chat to markdown' },
|
|
20
|
+
{ name: '/compact', description: 'Compress conversation history' },
|
|
21
|
+
{ name: '/undo', description: 'Revert last AI file change' },
|
|
22
|
+
{ name: '/exit', description: 'Exit CLARITY' },
|
|
23
|
+
];
|
|
21
24
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
'/tools': toolsCommand,
|
|
27
|
-
'/memory': memoryCommand,
|
|
28
|
-
'/history': historyCommand,
|
|
29
|
-
'/model': modelCommand,
|
|
30
|
-
'/provider': providerCommand,
|
|
31
|
-
'/config': configCommand,
|
|
32
|
-
'/clear': clearCommand,
|
|
33
|
-
'/run': runCommand,
|
|
34
|
-
'/task': taskCommand,
|
|
35
|
-
'/search': searchCommand,
|
|
36
|
-
'/fetch': fetchCommand,
|
|
37
|
-
'/git': gitCommand,
|
|
38
|
-
'/diff': diffCommand,
|
|
39
|
-
'/undo': undoCommand,
|
|
40
|
-
'/export': exportCommand,
|
|
41
|
-
'/keys': keysCommand,
|
|
42
|
-
'/exit': exitCommand,
|
|
43
|
-
};
|
|
25
|
+
export async function dispatchCommand(input, config, history) {
|
|
26
|
+
const parts = String(input).trim().split(/\s+/);
|
|
27
|
+
const cmd = parts[0].toLowerCase();
|
|
28
|
+
const args = parts.slice(1);
|
|
44
29
|
|
|
45
|
-
export async function dispatchCommand(input, config, rl) {
|
|
46
|
-
const parts = input.trim().split(/\s+/);
|
|
47
|
-
const cmd = parts[0].toLowerCase();
|
|
48
|
-
const args = parts.slice(1);
|
|
49
|
-
const handler = COMMANDS[cmd];
|
|
50
|
-
if (!handler) {
|
|
51
|
-
console.log(`Unknown command: ${cmd}. Type /help for commands.`);
|
|
52
|
-
return config;
|
|
53
|
-
}
|
|
54
30
|
try {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
31
|
+
switch (cmd) {
|
|
32
|
+
case '/help': {
|
|
33
|
+
const m = await import('./help.js');
|
|
34
|
+
return m.helpCommand(args, config);
|
|
35
|
+
}
|
|
36
|
+
case '/agent': {
|
|
37
|
+
const m = await import('./agent.js');
|
|
38
|
+
return m.agentCommand(args, config);
|
|
39
|
+
}
|
|
40
|
+
case '/model': {
|
|
41
|
+
const m = await import('./model.js');
|
|
42
|
+
return m.modelCommand(args, config);
|
|
43
|
+
}
|
|
44
|
+
case '/provider': {
|
|
45
|
+
const m = await import('./provider.js');
|
|
46
|
+
return m.providerCommand(args, config);
|
|
47
|
+
}
|
|
48
|
+
case '/tools': {
|
|
49
|
+
const m = await import('./tools.js');
|
|
50
|
+
return m.toolsCommand(args, config);
|
|
51
|
+
}
|
|
52
|
+
case '/memory': {
|
|
53
|
+
const m = await import('./memory.js');
|
|
54
|
+
return m.memoryCommand(args, config);
|
|
55
|
+
}
|
|
56
|
+
case '/history': {
|
|
57
|
+
const m = await import('./history.js');
|
|
58
|
+
return m.historyCommand(args, config);
|
|
59
|
+
}
|
|
60
|
+
case '/chat': {
|
|
61
|
+
const m = await import('./chat.js');
|
|
62
|
+
return m.chatCommand(args, config);
|
|
63
|
+
}
|
|
64
|
+
case '/clear': {
|
|
65
|
+
const { showBanner } = await import('../ui/banner.js');
|
|
66
|
+
process.stdout.write('\x1Bc');
|
|
67
|
+
await showBanner(config.version, config.provider, config.model);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
case '/task': {
|
|
71
|
+
const m = await import('./task.js');
|
|
72
|
+
return m.taskCommand(args, config);
|
|
73
|
+
}
|
|
74
|
+
case '/search': {
|
|
75
|
+
const m = await import('./search.js');
|
|
76
|
+
return m.searchCommand(args, config);
|
|
77
|
+
}
|
|
78
|
+
case '/fetch': {
|
|
79
|
+
const m = await import('./fetch.js');
|
|
80
|
+
return m.fetchCommand(args, config);
|
|
81
|
+
}
|
|
82
|
+
case '/git': {
|
|
83
|
+
const m = await import('./git.js');
|
|
84
|
+
return m.gitCommand(args, config);
|
|
85
|
+
}
|
|
86
|
+
case '/diff': {
|
|
87
|
+
const m = await import('./diff.js');
|
|
88
|
+
return m.diffCommand(args, config);
|
|
89
|
+
}
|
|
90
|
+
case '/run': {
|
|
91
|
+
const m = await import('./run.js');
|
|
92
|
+
return m.runCommand(args, config);
|
|
93
|
+
}
|
|
94
|
+
case '/keys': {
|
|
95
|
+
const m = await import('./keys.js');
|
|
96
|
+
return m.keysCommand(args, config);
|
|
97
|
+
}
|
|
98
|
+
case '/config': {
|
|
99
|
+
const m = await import('./config.js');
|
|
100
|
+
return m.configCommand(args, config);
|
|
101
|
+
}
|
|
102
|
+
case '/export': {
|
|
103
|
+
const m = await import('./export.js');
|
|
104
|
+
return m.exportCommand(args, config);
|
|
105
|
+
}
|
|
106
|
+
case '/undo': {
|
|
107
|
+
const m = await import('./undo.js');
|
|
108
|
+
return m.undoCommand(args, config);
|
|
109
|
+
}
|
|
110
|
+
case '/exit': {
|
|
111
|
+
const chalk = (await import('chalk')).default;
|
|
112
|
+
console.log(chalk.hex('#00FF9F')('\n\u2714 Goodbye.'));
|
|
113
|
+
process.exit(0);
|
|
114
|
+
}
|
|
115
|
+
default: {
|
|
116
|
+
const chalk = (await import('chalk')).default;
|
|
117
|
+
console.log(chalk.hex('#FF4757')('\u2716 Unknown command: ') + cmd);
|
|
118
|
+
console.log(chalk.dim(' Type / to see all commands'));
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
} catch (e) {
|
|
122
|
+
const chalk = (await import('chalk')).default;
|
|
123
|
+
console.log(chalk.hex('#FF4757')('\u2716 Command error: ') + e.message);
|
|
60
124
|
}
|
|
61
125
|
}
|
package/src/commands/model.js
CHANGED
|
@@ -1,11 +1,26 @@
|
|
|
1
1
|
import { saveConfig } from '../config/settings.js';
|
|
2
|
-
import {
|
|
2
|
+
import { PROVIDERS, getProvider } from '../providers/index.js';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
|
|
3
5
|
export async function modelCommand(args, config) {
|
|
4
6
|
if (args.length === 0) {
|
|
5
|
-
|
|
7
|
+
console.log(chalk.hex('#54A0FF')('Current model: ') + chalk.white(config.provider + '/' + config.model));
|
|
8
|
+
return;
|
|
6
9
|
}
|
|
7
|
-
|
|
10
|
+
|
|
11
|
+
const modelName = args[0];
|
|
12
|
+
|
|
13
|
+
const prov = getProvider(modelName);
|
|
14
|
+
if (prov) {
|
|
15
|
+
config.provider = prov.value;
|
|
16
|
+
config.model = prov.freeModel;
|
|
17
|
+
saveConfig(config);
|
|
18
|
+
console.log(chalk.hex('#00FF9F')('Switched to provider: ') + chalk.white(prov.name) + chalk.dim(' (' + prov.freeModel + ')'));
|
|
19
|
+
return config;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
config.model = modelName;
|
|
8
23
|
saveConfig(config);
|
|
9
|
-
console.log(
|
|
24
|
+
console.log(chalk.hex('#00FF9F')('Model set to: ') + chalk.white(modelName));
|
|
10
25
|
return config;
|
|
11
26
|
}
|
package/src/main.js
CHANGED
|
@@ -1,97 +1,168 @@
|
|
|
1
|
-
import readline from 'readline';
|
|
2
1
|
import chalk from 'chalk';
|
|
3
|
-
import {
|
|
2
|
+
import { renderUser, renderAI, divider } from './ui/blocks.js';
|
|
3
|
+
import { getPrompt, drawPromptBox, SlashPalette } from './ui/prompt.js';
|
|
4
4
|
import { dispatchCommand } from './commands/index.js';
|
|
5
|
-
import { callProvider } from './providers/index.js';
|
|
6
|
-
import { loadHistory, saveHistory, addMessage } from './core/history.js';
|
|
7
5
|
import { agentLoop } from './agents/loop.js';
|
|
6
|
+
import { showBanner } from './ui/banner.js';
|
|
8
7
|
|
|
9
8
|
export async function startChat(config) {
|
|
10
|
-
const history =
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
)
|
|
9
|
+
const history = [];
|
|
10
|
+
const palette = new SlashPalette();
|
|
11
|
+
let slashMode = false;
|
|
12
|
+
let slashBuffer = '';
|
|
13
|
+
|
|
14
|
+
const { top } = drawPromptBox(config.provider, config.model, config.agentMode);
|
|
15
|
+
console.log(chalk.hex('#00FF9F')('\u2714 ') + chalk.white('CLARITY-AI v' + config.version + ' interactive session started.') +
|
|
16
|
+
chalk.dim(' Type /help for commands. Ctrl+C to exit.'));
|
|
16
17
|
console.log(divider());
|
|
17
18
|
console.log();
|
|
18
19
|
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
20
|
+
const { emitKeypressEvents } = await import('readline');
|
|
21
|
+
emitKeypressEvents(process.stdin);
|
|
22
|
+
if (process.stdin.isTTY) process.stdin.setRawMode(true);
|
|
23
|
+
|
|
24
|
+
let currentInput = '';
|
|
25
|
+
|
|
26
|
+
function drawPromptLine() {
|
|
27
|
+
process.stdout.write('\r\x1b[2K');
|
|
28
|
+
process.stdout.write(getPrompt(config.provider, config.model));
|
|
29
|
+
if (currentInput) process.stdout.write(currentInput);
|
|
30
|
+
}
|
|
25
31
|
|
|
26
|
-
function
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
chalk.dim(config.provider + '/' + config.model) +
|
|
30
|
-
chalk.hex('#00FFFF')(' \u276f ');
|
|
31
|
-
rl.setPrompt(pre);
|
|
32
|
-
rl.prompt();
|
|
32
|
+
function clearAndPrompt() {
|
|
33
|
+
console.log();
|
|
34
|
+
drawPromptLine();
|
|
33
35
|
}
|
|
34
36
|
|
|
35
|
-
process.stdin.on('keypress', (str, key) => {
|
|
36
|
-
if (key
|
|
37
|
+
process.stdin.on('keypress', async (str, key) => {
|
|
38
|
+
if (!key) return;
|
|
39
|
+
|
|
40
|
+
if (key.ctrl && key.name === 'c') {
|
|
41
|
+
if (slashMode) {
|
|
42
|
+
palette.hide();
|
|
43
|
+
slashMode = false;
|
|
44
|
+
slashBuffer = '';
|
|
45
|
+
currentInput = '';
|
|
46
|
+
drawPromptLine();
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
console.log('\n' + chalk.hex('#00FF9F')('\u2714 ') + chalk.dim('Session ended. Goodbye.'));
|
|
50
|
+
process.exit(0);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (key.ctrl && key.name === 'l') {
|
|
37
54
|
process.stdout.write('\x1Bc');
|
|
38
|
-
|
|
55
|
+
await showBanner(config.version, config.provider, config.model);
|
|
56
|
+
drawPromptLine();
|
|
57
|
+
return;
|
|
39
58
|
}
|
|
40
|
-
});
|
|
41
59
|
|
|
42
|
-
|
|
60
|
+
if (key.ctrl && key.name === 'u') {
|
|
61
|
+
currentInput = '';
|
|
62
|
+
slashMode = false;
|
|
63
|
+
slashBuffer = '';
|
|
64
|
+
drawPromptLine();
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
43
67
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
68
|
+
if (key.name === 'escape') {
|
|
69
|
+
if (slashMode) {
|
|
70
|
+
palette.hide();
|
|
71
|
+
slashMode = false;
|
|
72
|
+
slashBuffer = '';
|
|
73
|
+
currentInput = '';
|
|
74
|
+
drawPromptLine();
|
|
75
|
+
}
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
47
78
|
|
|
48
|
-
if (
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
79
|
+
if (slashMode) {
|
|
80
|
+
if (key.name === 'up') {
|
|
81
|
+
palette.selectPrev();
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
if (key.name === 'down') {
|
|
85
|
+
palette.selectNext();
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
if (key.name === 'return') {
|
|
89
|
+
const selected = palette.getSelected();
|
|
90
|
+
palette.hide();
|
|
91
|
+
slashMode = false;
|
|
92
|
+
currentInput = '';
|
|
93
|
+
drawPromptLine();
|
|
94
|
+
if (selected) {
|
|
95
|
+
console.log();
|
|
96
|
+
await dispatchCommand(selected.name, config, history);
|
|
97
|
+
}
|
|
98
|
+
clearAndPrompt();
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
if (key.name === 'backspace') {
|
|
102
|
+
slashBuffer = slashBuffer.slice(0, -1);
|
|
103
|
+
currentInput = '/' + slashBuffer;
|
|
104
|
+
palette.filter(slashBuffer);
|
|
105
|
+
drawPromptLine();
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
if (str && !key.ctrl && !key.meta) {
|
|
109
|
+
slashBuffer += str;
|
|
110
|
+
currentInput = '/' + slashBuffer;
|
|
111
|
+
palette.filter(slashBuffer);
|
|
112
|
+
drawPromptLine();
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
52
115
|
return;
|
|
53
116
|
}
|
|
54
117
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
118
|
+
if (key.name === 'backspace') {
|
|
119
|
+
currentInput = currentInput.slice(0, -1);
|
|
120
|
+
drawPromptLine();
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
58
123
|
|
|
59
|
-
if (
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
} else {
|
|
68
|
-
addMessage(history, 'user', input);
|
|
69
|
-
console.log(chalk.hex('#555555')('\u25c6 Thinking...'));
|
|
70
|
-
|
|
71
|
-
let fullResponse = '';
|
|
72
|
-
const stream = callProvider(config, [
|
|
73
|
-
{ role: 'system', content: 'You are CLARITY-AI, a helpful AI assistant. Keep responses short and direct.' },
|
|
74
|
-
...history.slice(-10).map(m => ({ role: m.role === 'assistant' ? 'assistant' : 'user', content: m.content })),
|
|
75
|
-
{ role: 'user', content: input },
|
|
76
|
-
]);
|
|
77
|
-
|
|
78
|
-
for await (const chunk of stream) {
|
|
79
|
-
fullResponse += chunk;
|
|
124
|
+
if (key.name === 'return') {
|
|
125
|
+
const input = currentInput.trim();
|
|
126
|
+
currentInput = '';
|
|
127
|
+
|
|
128
|
+
if (!input) {
|
|
129
|
+
console.log();
|
|
130
|
+
drawPromptLine();
|
|
131
|
+
return;
|
|
80
132
|
}
|
|
81
133
|
|
|
134
|
+
process.stdout.write('\r\x1b[2K');
|
|
82
135
|
console.log();
|
|
83
|
-
console.log(
|
|
136
|
+
console.log(renderUser(input));
|
|
84
137
|
console.log();
|
|
85
138
|
|
|
86
|
-
|
|
87
|
-
|
|
139
|
+
if (input.startsWith('/')) {
|
|
140
|
+
await dispatchCommand(input, config, history);
|
|
141
|
+
clearAndPrompt();
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
await agentLoop(input, config, history);
|
|
146
|
+
clearAndPrompt();
|
|
147
|
+
return;
|
|
88
148
|
}
|
|
89
149
|
|
|
90
|
-
|
|
91
|
-
|
|
150
|
+
if (str === '/' && currentInput === '') {
|
|
151
|
+
slashMode = true;
|
|
152
|
+
slashBuffer = '';
|
|
153
|
+
currentInput = '/';
|
|
154
|
+
console.log();
|
|
155
|
+
palette.show();
|
|
156
|
+
drawPromptLine();
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
92
159
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
160
|
+
if (str && !key.ctrl && !key.meta) {
|
|
161
|
+
currentInput += str;
|
|
162
|
+
drawPromptLine();
|
|
163
|
+
}
|
|
96
164
|
});
|
|
165
|
+
|
|
166
|
+
console.log(top);
|
|
167
|
+
drawPromptLine();
|
|
97
168
|
}
|
package/src/providers/index.js
CHANGED
|
@@ -16,15 +16,23 @@ export const PROVIDERS = [
|
|
|
16
16
|
|
|
17
17
|
const senders = { groq: groqSend, gemini: geminiSend, openrouter: openrouterSend, openai: openaiSend, anthropic: anthropicSend, deepseek: deepseekSend };
|
|
18
18
|
|
|
19
|
-
export function
|
|
19
|
+
export function streamProvider(config, messages) {
|
|
20
20
|
const provider = config.provider || 'groq';
|
|
21
21
|
const model = config.model || 'llama-3.3-70b-versatile';
|
|
22
22
|
const apiKey = config.apiKeys?.[provider] || process.env[provider.toUpperCase() + '_API_KEY'] || '';
|
|
23
23
|
const sender = senders[provider];
|
|
24
|
-
if (!sender) throw new Error(
|
|
24
|
+
if (!sender) throw new Error('Unknown provider: ' + provider);
|
|
25
25
|
return sender(apiKey, messages, model, true);
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
export async function callProvider(config, messages) {
|
|
29
|
+
let full = '';
|
|
30
|
+
for await (const chunk of streamProvider(config, messages)) {
|
|
31
|
+
full += chunk;
|
|
32
|
+
}
|
|
33
|
+
return full;
|
|
34
|
+
}
|
|
35
|
+
|
|
28
36
|
export function getProvider(name) {
|
|
29
37
|
return PROVIDERS.find(p => p.value === name);
|
|
30
38
|
}
|
package/src/ui/prompt.js
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import readline from 'readline';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import { ALL_COMMANDS } from '../commands/index.js';
|
|
4
|
+
|
|
5
|
+
const W = () => process.stdout.columns || 80;
|
|
6
|
+
|
|
7
|
+
export function drawPromptBox(provider, model, agentMode = true) {
|
|
8
|
+
const w = Math.min(W(), 80);
|
|
9
|
+
const inner = w - 2;
|
|
10
|
+
const top = chalk.hex('#333333')('\u250c') + chalk.hex('#333333')('\u2500'.repeat(inner)) + chalk.hex('#333333')('\u2510');
|
|
11
|
+
const bottom =
|
|
12
|
+
chalk.hex('#333333')('\u2514') +
|
|
13
|
+
chalk.hex('#333333')('\u2500 ') +
|
|
14
|
+
chalk.dim(provider + '/' + model) +
|
|
15
|
+
chalk.hex('#333333')(' ' + '\u2500'.repeat(Math.max(0, inner - (provider + '/' + model).length - 14))) +
|
|
16
|
+
chalk.hex('#00FF9F')(agentMode ? 'agent: ON ' : 'agent: OFF') +
|
|
17
|
+
chalk.hex('#333333')('\u2500\u2518');
|
|
18
|
+
return { top, bottom };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function getPrompt(provider, model) {
|
|
22
|
+
return (
|
|
23
|
+
chalk.hex('#333333')('\u2502 ') +
|
|
24
|
+
chalk.hex('#00FFFF')('\u276f ') +
|
|
25
|
+
chalk.dim(provider + '/' + model) +
|
|
26
|
+
chalk.hex('#00FFFF')(' \u276f ')
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export class SlashPalette {
|
|
31
|
+
constructor() {
|
|
32
|
+
this.visible = false;
|
|
33
|
+
this.query = '';
|
|
34
|
+
this.selected = 0;
|
|
35
|
+
this.commands = [];
|
|
36
|
+
this._lastLineCount = 0;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
show() {
|
|
40
|
+
this.visible = true;
|
|
41
|
+
this.commands = ALL_COMMANDS;
|
|
42
|
+
this.selected = 0;
|
|
43
|
+
this.query = '';
|
|
44
|
+
this.render();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
hide() {
|
|
48
|
+
this.visible = false;
|
|
49
|
+
this.query = '';
|
|
50
|
+
this.selected = 0;
|
|
51
|
+
const lines = this._lastLineCount;
|
|
52
|
+
this._lastLineCount = 0;
|
|
53
|
+
for (let i = 0; i < lines; i++) {
|
|
54
|
+
process.stdout.write('\x1b[A\x1b[2K');
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
filter(query) {
|
|
59
|
+
this.query = query;
|
|
60
|
+
this.selected = 0;
|
|
61
|
+
this.render();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
selectNext() {
|
|
65
|
+
const filtered = this._filtered();
|
|
66
|
+
this.selected = Math.min(this.selected + 1, filtered.length - 1);
|
|
67
|
+
this.render();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
selectPrev() {
|
|
71
|
+
this.selected = Math.max(this.selected - 1, 0);
|
|
72
|
+
this.render();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
getSelected() {
|
|
76
|
+
const filtered = this._filtered();
|
|
77
|
+
return filtered[this.selected] || null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
_filtered() {
|
|
81
|
+
if (!this.query) return this.commands;
|
|
82
|
+
return this.commands.filter(c =>
|
|
83
|
+
c.name.includes(this.query) || c.description.toLowerCase().includes(this.query.toLowerCase())
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
render() {
|
|
88
|
+
const filtered = this._filtered();
|
|
89
|
+
const show = filtered.slice(0, 8);
|
|
90
|
+
const w = Math.min(W(), 80);
|
|
91
|
+
|
|
92
|
+
if (this._lastLineCount) {
|
|
93
|
+
for (let i = 0; i < this._lastLineCount; i++) {
|
|
94
|
+
process.stdout.write('\x1b[A\x1b[2K');
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const lines = [];
|
|
99
|
+
lines.push(
|
|
100
|
+
chalk.bgHex('#1A1A2E').hex('#00FFFF').bold(' Commands ') +
|
|
101
|
+
chalk.bgHex('#1A1A2E').dim(' type to filter ') +
|
|
102
|
+
chalk.bgHex('#1A1A2E').dim('esc to close'.padEnd(w - 28, ' '))
|
|
103
|
+
);
|
|
104
|
+
lines.push(chalk.hex('#333333')('\u2500'.repeat(w)));
|
|
105
|
+
|
|
106
|
+
if (show.length === 0) {
|
|
107
|
+
lines.push(chalk.dim(' No commands match "' + this.query + '"'));
|
|
108
|
+
} else {
|
|
109
|
+
show.forEach((cmd, i) => {
|
|
110
|
+
const isSelected = i === this.selected;
|
|
111
|
+
const prefix = isSelected ? chalk.hex('#00FFFF')(' \u276f ') : ' ';
|
|
112
|
+
const name = isSelected
|
|
113
|
+
? chalk.bgHex('#1A1A2E').hex('#00FFFF').bold((cmd.name).padEnd(18))
|
|
114
|
+
: chalk.hex('#9B59FF')((cmd.name).padEnd(18));
|
|
115
|
+
const desc = isSelected
|
|
116
|
+
? chalk.white(cmd.description)
|
|
117
|
+
: chalk.dim(cmd.description);
|
|
118
|
+
lines.push(prefix + name + ' ' + desc);
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
lines.push(chalk.hex('#333333')('\u2500'.repeat(w)));
|
|
123
|
+
|
|
124
|
+
process.stdout.write(lines.join('\n') + '\n');
|
|
125
|
+
this._lastLineCount = lines.length;
|
|
126
|
+
}
|
|
127
|
+
}
|
package/src/ui/spinner.js
CHANGED
|
@@ -1,25 +1,100 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
2
|
+
|
|
3
|
+
const FRAMES = ['\u280b','\u2819','\u2839','\u2838','\u283c','\u2834','\u2826','\u2827','\u2807','\u280f'];
|
|
4
|
+
const SUBAGENT_FRAMES = ['\u25f0\u25f1\u25f1\u25f1\u25f1','\u25f0\u25f0\u25f1\u25f1\u25f1','\u25f0\u25f0\u25f0\u25f1\u25f1','\u25f0\u25f0\u25f0\u25f0\u25f1','\u25f0\u25f0\u25f0\u25f0\u25f0','\u25f1\u25f0\u25f0\u25f0\u25f0','\u25f1\u25f1\u25f0\u25f0\u25f0','\u25f1\u25f1\u25f1\u25f0\u25f0','\u25f1\u25f1\u25f1\u25f1\u25f0','\u25f1\u25f1\u25f1\u25f1\u25f1'];
|
|
5
|
+
|
|
6
|
+
export class Spinner {
|
|
7
|
+
constructor() {
|
|
8
|
+
this.frame = 0;
|
|
9
|
+
this.interval = null;
|
|
10
|
+
this.label = '';
|
|
11
|
+
this.sub = '';
|
|
12
|
+
this._active = false;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
start(label, type = 'think') {
|
|
16
|
+
this._active = true;
|
|
17
|
+
this.label = label;
|
|
18
|
+
this.type = type;
|
|
19
|
+
this.frame = 0;
|
|
20
|
+
|
|
21
|
+
this.interval = setInterval(() => {
|
|
22
|
+
this.frame = (this.frame + 1) % FRAMES.length;
|
|
23
|
+
this._draw();
|
|
24
|
+
}, 80);
|
|
25
|
+
|
|
26
|
+
this._draw();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
update(label, sub = '') {
|
|
30
|
+
this.label = label;
|
|
31
|
+
this.sub = sub;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
stop(finalLabel, status = 'done') {
|
|
35
|
+
if (this.interval) clearInterval(this.interval);
|
|
36
|
+
this._active = false;
|
|
37
|
+
process.stdout.write('\r\x1b[2K');
|
|
38
|
+
|
|
39
|
+
const icon =
|
|
40
|
+
status === 'done' ? chalk.hex('#00FF9F')('\u2714') :
|
|
41
|
+
status === 'error' ? chalk.hex('#FF4757')('\u2716') :
|
|
42
|
+
status === 'skip' ? chalk.dim('\u2500') :
|
|
43
|
+
chalk.hex('#00FF9F')('\u2714');
|
|
44
|
+
|
|
45
|
+
process.stdout.write(icon + ' ' + chalk.dim(finalLabel) + '\n');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
_draw() {
|
|
49
|
+
process.stdout.write('\r\x1b[2K');
|
|
50
|
+
|
|
51
|
+
let spinChar, color;
|
|
52
|
+
if (this.type === 'think') {
|
|
53
|
+
spinChar = FRAMES[this.frame % FRAMES.length];
|
|
54
|
+
color = '#9B59FF';
|
|
55
|
+
} else if (this.type === 'tool') {
|
|
56
|
+
spinChar = FRAMES[this.frame % FRAMES.length];
|
|
57
|
+
color = '#00FFFF';
|
|
58
|
+
} else {
|
|
59
|
+
spinChar = SUBAGENT_FRAMES[this.frame % SUBAGENT_FRAMES.length];
|
|
60
|
+
color = '#FFD700';
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const sub = this.sub ? chalk.dim(' \u00b7 ' + this.sub.slice(0, 40)) : '';
|
|
64
|
+
process.stdout.write(
|
|
65
|
+
chalk.hex(color)(spinChar) + ' ' +
|
|
66
|
+
chalk.white(this.label) +
|
|
67
|
+
sub
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export class CollapsibleStep {
|
|
73
|
+
constructor(label, type = 'think') {
|
|
74
|
+
this.label = label;
|
|
75
|
+
this.type = type;
|
|
76
|
+
this.lines = [];
|
|
77
|
+
this._done = false;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
addLine(line) {
|
|
81
|
+
this.lines.push(line);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
printCollapsed(elapsed) {
|
|
85
|
+
const icon =
|
|
86
|
+
this.type === 'think' ? chalk.hex('#9B59FF')('+') :
|
|
87
|
+
this.type === 'tool' ? chalk.hex('#00FFFF')('\u2699') :
|
|
88
|
+
chalk.hex('#FFD700')('\u25c6');
|
|
89
|
+
|
|
90
|
+
const time = elapsed ? chalk.dim(' ' + elapsed + 'ms') : '';
|
|
91
|
+
console.log(icon + ' ' + chalk.white(this.label) + time);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
printExpanded(elapsed) {
|
|
95
|
+
this.printCollapsed(elapsed);
|
|
96
|
+
for (const l of this.lines) {
|
|
97
|
+
console.log(chalk.dim(' \u2502 ') + chalk.hex('#888888')(l));
|
|
98
|
+
}
|
|
99
|
+
}
|
|
25
100
|
}
|