closed-loop-cli 1.0.1 → 1.0.3
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/dist/index.js +227 -58
- package/package.json +1 -1
- package/src/index.ts +261 -61
package/dist/index.js
CHANGED
|
@@ -38,82 +38,208 @@ const fs = __importStar(require("fs"));
|
|
|
38
38
|
const path = __importStar(require("path"));
|
|
39
39
|
const readline = __importStar(require("readline"));
|
|
40
40
|
const dotenv = __importStar(require("dotenv"));
|
|
41
|
+
const os = __importStar(require("os"));
|
|
41
42
|
const autogenesis_1 = require("./orchestrator/autogenesis");
|
|
42
43
|
const server_1 = require("./dashboard/server");
|
|
43
44
|
const telegram_bot_1 = require("./orchestrator/telegram-bot");
|
|
44
45
|
const task_agent_1 = require("./orchestrator/task-agent");
|
|
45
|
-
const tui_tools_1 = require("./tools/tui-tools");
|
|
46
46
|
// Phase 3 is initialized
|
|
47
47
|
// Load environment variables
|
|
48
48
|
dotenv.config();
|
|
49
49
|
function printHeader() {
|
|
50
|
+
const banner = `
|
|
51
|
+
\x1b[38;5;99m ___ _ _ _
|
|
52
|
+
/ __\\ | ___ ___ ___ __| | | | ___ ___ _ __
|
|
53
|
+
/ / | |/ _ \\/ __|/ _ \\/ _\` | | |/ _ \\ / _ \\| '_ \\
|
|
54
|
+
/ /___| | (_) \\__ \\ __/ (_| | | | (_) | (_) | |_) |
|
|
55
|
+
\\____/|_|\\___/|___/\\___|\\__,_| |_|\\___/ \\___/| .__/
|
|
56
|
+
|_| \x1b[0m`;
|
|
57
|
+
console.log(banner);
|
|
58
|
+
console.log('\x1b[37mTips for getting started:\x1b[0m');
|
|
59
|
+
console.log('\x1b[90m1. Ask questions, edit files, or run commands.\x1b[0m');
|
|
60
|
+
console.log('\x1b[90m2. Be specific for the best results.\x1b[0m');
|
|
61
|
+
console.log('\x1b[90m3. Type "exit" or "quit" to close the session.\x1b[0m\n');
|
|
50
62
|
const endpoint = process.env.ANTHROPIC_BASE_URL || 'default';
|
|
51
63
|
const model = process.env.ANTHROPIC_MODEL || 'mimo-v2.5-pro[1m]';
|
|
52
64
|
const subagent = process.env.CLAUDE_CODE_SUBAGENT_MODEL || 'mimo-v2.5-pro';
|
|
53
|
-
|
|
54
|
-
let valStr = value;
|
|
55
|
-
if (valStr.length > 48) {
|
|
56
|
-
valStr = valStr.substring(0, 45) + '...';
|
|
57
|
-
}
|
|
58
|
-
const visibleText = ` ${label} ❯ ${valStr}`;
|
|
59
|
-
const padding = ' '.repeat(Math.max(0, 65 - visibleText.length));
|
|
60
|
-
return ` \x1b[38;5;86m${label}\x1b[0m \x1b[90m❯\x1b[0m \x1b[38;5;153m${valStr}\x1b[0m${padding}`;
|
|
61
|
-
};
|
|
62
|
-
console.log('\x1b[38;5;99m╭──────────────────────────────────────────────────────────────────╮\x1b[0m');
|
|
63
|
-
console.log('\x1b[38;5;99m│\x1b[0m \x1b[1;38;5;159m🤖 Closed-Loop Conversational Agent CLI\x1b[0m \x1b[38;5;99m│\x1b[0m');
|
|
64
|
-
console.log('\x1b[38;5;99m├──────────────────────────────────────────────────────────────────┤\x1b[0m');
|
|
65
|
-
console.log('\x1b[38;5;99m│\x1b[0m' + padRaw('Endpoint', endpoint) + '\x1b[38;5;99m│\x1b[0m');
|
|
66
|
-
console.log('\x1b[38;5;99m│\x1b[0m' + padRaw('Model ', model) + '\x1b[38;5;99m│\x1b[0m');
|
|
67
|
-
console.log('\x1b[38;5;99m│\x1b[0m' + padRaw('Subagent', subagent) + '\x1b[38;5;99m│\x1b[0m');
|
|
68
|
-
console.log('\x1b[38;5;99m╰──────────────────────────────────────────────────────────────────╯\x1b[0m\n');
|
|
69
|
-
console.log('\x1b[90m💡 Type your task to begin, or type "exit" to quit.\x1b[0m\n');
|
|
65
|
+
console.log(`\x1b[90mUsing: Endpoint: \x1b[38;5;86m${endpoint}\x1b[90m | Model: \x1b[38;5;153m${model}\x1b[90m | Subagent: \x1b[38;5;208m${subagent}\x1b[0m\n`);
|
|
70
66
|
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
67
|
+
class ClosedLoopTUI {
|
|
68
|
+
history = [];
|
|
69
|
+
input = '';
|
|
70
|
+
cursorIdx = 0;
|
|
71
|
+
isThinking = false;
|
|
72
|
+
effort;
|
|
73
|
+
constructor(effort = 'standard') {
|
|
74
|
+
this.effort = effort;
|
|
75
|
+
}
|
|
76
|
+
start() {
|
|
77
|
+
process.stdout.write('\x1b[?1049h'); // Enter alternate screen
|
|
78
|
+
process.stdout.write('\x1b[?25h'); // Show cursor
|
|
79
|
+
this.render();
|
|
80
|
+
readline.emitKeypressEvents(process.stdin);
|
|
81
|
+
if (process.stdin.isTTY) {
|
|
82
|
+
process.stdin.setRawMode(true);
|
|
83
|
+
}
|
|
84
|
+
process.stdin.on('keypress', this.handleKeypress.bind(this));
|
|
85
|
+
process.stdout.on('resize', () => {
|
|
86
|
+
this.render();
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
render() {
|
|
90
|
+
const rows = process.stdout.rows || 24;
|
|
91
|
+
const cols = process.stdout.columns || 80;
|
|
92
|
+
// Clear screen and move cursor to 1,1
|
|
93
|
+
process.stdout.write('\x1b[2J\x1b[H');
|
|
94
|
+
// 1. Draw Banner & Info
|
|
95
|
+
const banner = `
|
|
96
|
+
\x1b[38;5;99m ___ _ _ _
|
|
97
|
+
/ __\\ | ___ ___ ___ __| | | | ___ ___ _ __
|
|
98
|
+
/ / | |/ _ \\/ __|/ _ \\/ _\` | | |/ _ \\ / _ \\| '_ \\
|
|
99
|
+
/ /___| | (_) \\__ \\ __/ (_| | | | (_) | (_) | |_) |
|
|
100
|
+
\\____/|_|\\___/|___/\\___|\\__,_| |_|\\___/ \\___/| .__/
|
|
101
|
+
|_| \x1b[0m`;
|
|
102
|
+
console.log(banner);
|
|
103
|
+
console.log('\x1b[37mTips for getting started:\x1b[0m');
|
|
104
|
+
console.log('\x1b[90m1. Ask questions, edit files, or run commands. 2. Be specific. 3. Type "exit" to quit.\x1b[0m');
|
|
105
|
+
const endpoint = process.env.ANTHROPIC_BASE_URL || 'default';
|
|
106
|
+
const model = process.env.ANTHROPIC_MODEL || 'mimo-v2.5-pro';
|
|
107
|
+
console.log(`\x1b[90mUsing: Endpoint: \x1b[38;5;86m${endpoint}\x1b[90m | Model: \x1b[38;5;153m${model}\x1b[0m\n`);
|
|
108
|
+
// 2. Draw History in the middle
|
|
109
|
+
const historyHeight = rows - 14;
|
|
110
|
+
const historyLines = [];
|
|
111
|
+
// Format history messages into lines
|
|
112
|
+
for (const msg of this.history) {
|
|
113
|
+
if (msg.role === 'user') {
|
|
114
|
+
historyLines.push(`\x1b[38;5;99mUser ❯\x1b[0m ${msg.content}`);
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
const lines = msg.content.split('\n');
|
|
118
|
+
for (const line of lines) {
|
|
119
|
+
historyLines.push(`\x1b[38;5;86mAgent ❯\x1b[0m ${line}`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
// Slice history to fit historyHeight
|
|
124
|
+
const startIdx = Math.max(0, historyLines.length - historyHeight);
|
|
125
|
+
const visibleHistory = historyLines.slice(startIdx, startIdx + historyHeight);
|
|
126
|
+
for (let i = 0; i < historyHeight; i++) {
|
|
127
|
+
if (i < visibleHistory.length) {
|
|
128
|
+
console.log(visibleHistory[i]);
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
console.log(''); // empty line
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
// 3. Draw Bottom Input Box (3 lines)
|
|
135
|
+
const border = '─'.repeat(cols - 2);
|
|
136
|
+
console.log(`\x1b[38;5;99m╭${border}╮\x1b[0m`);
|
|
137
|
+
const promptStr = this.isThinking ? ' \x1b[36m⠋ Thinking...\x1b[0m' : ` › ${this.input}`;
|
|
138
|
+
const visiblePromptLen = this.isThinking ? 14 : 3 + this.input.length;
|
|
139
|
+
const padding = ' '.repeat(Math.max(0, cols - 4 - visiblePromptLen));
|
|
140
|
+
console.log(`\x1b[38;5;99m│\x1b[0m${promptStr}${padding}\x1b[38;5;99m│\x1b[0m`);
|
|
141
|
+
console.log(`\x1b[38;5;99m╰${border}╯\x1b[0m`);
|
|
142
|
+
// Position cursor
|
|
143
|
+
if (!this.isThinking) {
|
|
144
|
+
const cursorCol = 4 + this.cursorIdx;
|
|
145
|
+
const cursorRow = rows - 1;
|
|
146
|
+
process.stdout.write(`\x1b[${cursorRow};${cursorCol}H`);
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
process.stdout.write('\x1b[?25l');
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
async handleKeypress(str, key) {
|
|
153
|
+
if (this.isThinking)
|
|
84
154
|
return;
|
|
155
|
+
if (key.ctrl && key.name === 'c') {
|
|
156
|
+
this.exit();
|
|
85
157
|
}
|
|
86
|
-
if (
|
|
87
|
-
|
|
88
|
-
|
|
158
|
+
if (key.name === 'return') {
|
|
159
|
+
const cmd = this.input.trim();
|
|
160
|
+
if (!cmd)
|
|
161
|
+
return;
|
|
162
|
+
if (cmd.toLowerCase() === 'exit' || cmd.toLowerCase() === 'quit') {
|
|
163
|
+
this.exit();
|
|
164
|
+
}
|
|
165
|
+
this.history.push({ role: 'user', content: cmd });
|
|
166
|
+
this.input = '';
|
|
167
|
+
this.cursorIdx = 0;
|
|
168
|
+
this.isThinking = true;
|
|
169
|
+
this.render();
|
|
170
|
+
try {
|
|
171
|
+
// Temporarily leave alternate screen buffer for execution
|
|
172
|
+
process.stdout.write('\x1b[?1049l');
|
|
173
|
+
process.stdout.write('\x1b[?25h');
|
|
174
|
+
if (process.stdin.isTTY) {
|
|
175
|
+
process.stdin.setRawMode(false);
|
|
176
|
+
}
|
|
177
|
+
console.log(`\n\x1b[35;1m=================== EXECUTION MODE ===================\x1b[0m`);
|
|
178
|
+
console.log(`\x1b[90mRunning Task:\x1b[0m ${cmd}\n`);
|
|
179
|
+
const response = await (0, task_agent_1.runTaskAgent)(cmd, {
|
|
180
|
+
role: 'coder_codeact',
|
|
181
|
+
effort: this.effort,
|
|
182
|
+
history: this.history.slice(0, -1)
|
|
183
|
+
});
|
|
184
|
+
// Re-enter TUI and raw mode
|
|
185
|
+
process.stdout.write('\x1b[?1049h');
|
|
186
|
+
if (process.stdin.isTTY) {
|
|
187
|
+
process.stdin.setRawMode(true);
|
|
188
|
+
}
|
|
189
|
+
this.history.push({ role: 'assistant', content: response.result });
|
|
190
|
+
}
|
|
191
|
+
catch (err) {
|
|
192
|
+
// Re-enter TUI and raw mode
|
|
193
|
+
process.stdout.write('\x1b[?1049h');
|
|
194
|
+
if (process.stdin.isTTY) {
|
|
195
|
+
process.stdin.setRawMode(true);
|
|
196
|
+
}
|
|
197
|
+
this.history.push({ role: 'assistant', content: `Error: ${err.message}` });
|
|
198
|
+
}
|
|
199
|
+
this.isThinking = false;
|
|
200
|
+
process.stdout.write('\x1b[?25h');
|
|
201
|
+
this.render();
|
|
202
|
+
return;
|
|
89
203
|
}
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
history: history
|
|
98
|
-
});
|
|
99
|
-
spinner.stop(true, 'Done!');
|
|
100
|
-
// Update history
|
|
101
|
-
history.push({ role: 'user', content: input });
|
|
102
|
-
history.push({ role: 'assistant', content: response.result });
|
|
103
|
-
console.log(`\n\x1b[38;5;86m╭── Agent Response ─────────────────────────────────────────────────────────────╮\x1b[0m`);
|
|
104
|
-
console.log(response.result);
|
|
105
|
-
console.log(`\x1b[38;5;86m╰───────────────────────────────────────────────────────────────────────────────╯\x1b[0m\n`);
|
|
204
|
+
if (key.name === 'backspace') {
|
|
205
|
+
if (this.cursorIdx > 0) {
|
|
206
|
+
this.input = this.input.slice(0, this.cursorIdx - 1) + this.input.slice(this.cursorIdx);
|
|
207
|
+
this.cursorIdx--;
|
|
208
|
+
this.render();
|
|
209
|
+
}
|
|
210
|
+
return;
|
|
106
211
|
}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
212
|
+
if (key.name === 'left') {
|
|
213
|
+
if (this.cursorIdx > 0) {
|
|
214
|
+
this.cursorIdx--;
|
|
215
|
+
this.render();
|
|
216
|
+
}
|
|
217
|
+
return;
|
|
110
218
|
}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
219
|
+
if (key.name === 'right') {
|
|
220
|
+
if (this.cursorIdx < this.input.length) {
|
|
221
|
+
this.cursorIdx++;
|
|
222
|
+
this.render();
|
|
223
|
+
}
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
// Handle normal character inputs
|
|
227
|
+
if (str && str.length === 1 && !key.ctrl && !key.meta) {
|
|
228
|
+
this.input = this.input.slice(0, this.cursorIdx) + str + this.input.slice(this.cursorIdx);
|
|
229
|
+
this.cursorIdx++;
|
|
230
|
+
this.render();
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
exit() {
|
|
234
|
+
process.stdout.write('\x1b[?1049l');
|
|
235
|
+
process.stdout.write('\x1b[?25h');
|
|
236
|
+
console.log('Goodbye!');
|
|
115
237
|
process.exit(0);
|
|
116
|
-
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
async function startInteractiveCLI(effort = 'standard', codeactMode = false) {
|
|
241
|
+
const tui = new ClosedLoopTUI(effort);
|
|
242
|
+
tui.start();
|
|
117
243
|
}
|
|
118
244
|
function setupLogRedirection() {
|
|
119
245
|
const logFile = fs.createWriteStream(path.join(process.cwd(), 'evolution.log'), { flags: 'a' });
|
|
@@ -128,7 +254,50 @@ function setupLogRedirection() {
|
|
|
128
254
|
logFile.write('ERROR: ' + args.join(' ') + '\n');
|
|
129
255
|
};
|
|
130
256
|
}
|
|
257
|
+
async function checkAndPromptAPIKey() {
|
|
258
|
+
if (process.env.ANTHROPIC_API_KEY || process.env.ANTHROPIC_AUTH_TOKEN) {
|
|
259
|
+
return process.env.ANTHROPIC_API_KEY || process.env.ANTHROPIC_AUTH_TOKEN || '';
|
|
260
|
+
}
|
|
261
|
+
const configPath = path.join(os.homedir(), '.closed-loop.json');
|
|
262
|
+
if (fs.existsSync(configPath)) {
|
|
263
|
+
try {
|
|
264
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
265
|
+
if (config.apiKey) {
|
|
266
|
+
process.env.ANTHROPIC_API_KEY = config.apiKey;
|
|
267
|
+
return config.apiKey;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
catch (e) { }
|
|
271
|
+
}
|
|
272
|
+
console.log('\n\x1b[33m🔑 Anthropic API key not found in environment or local .env file.\x1b[0m');
|
|
273
|
+
console.log('To run this assistant, please configure your API key below.');
|
|
274
|
+
const rl = readline.createInterface({
|
|
275
|
+
input: process.stdin,
|
|
276
|
+
output: process.stdout
|
|
277
|
+
});
|
|
278
|
+
const question = (query) => {
|
|
279
|
+
return new Promise(resolve => rl.question(query, resolve));
|
|
280
|
+
};
|
|
281
|
+
const key = await question('\x1b[35mEnter your Anthropic API Key:\x1b[0m ');
|
|
282
|
+
rl.close();
|
|
283
|
+
const trimmedKey = key.trim();
|
|
284
|
+
if (!trimmedKey) {
|
|
285
|
+
console.error('\x1b[31mError: API Key cannot be empty.\x1b[0m');
|
|
286
|
+
process.exit(1);
|
|
287
|
+
}
|
|
288
|
+
try {
|
|
289
|
+
fs.writeFileSync(configPath, JSON.stringify({ apiKey: trimmedKey }, null, 2), 'utf8');
|
|
290
|
+
console.log(`\x1b[32m✔ Saved API key to ${configPath}\x1b[0m\n`);
|
|
291
|
+
}
|
|
292
|
+
catch (err) {
|
|
293
|
+
console.warn(`\x1b[33mWarning: Could not save API key to config: ${err.message}\x1b[0m`);
|
|
294
|
+
}
|
|
295
|
+
process.env.ANTHROPIC_API_KEY = trimmedKey;
|
|
296
|
+
return trimmedKey;
|
|
297
|
+
}
|
|
131
298
|
async function main() {
|
|
299
|
+
// Ensure API Key is configured before running any agent logic
|
|
300
|
+
await checkAndPromptAPIKey();
|
|
132
301
|
const args = process.argv.slice(2);
|
|
133
302
|
// Parse codeact option
|
|
134
303
|
let codeactMode = false;
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -3,6 +3,7 @@ import * as fs from 'fs';
|
|
|
3
3
|
import * as path from 'path';
|
|
4
4
|
import * as readline from 'readline';
|
|
5
5
|
import * as dotenv from 'dotenv';
|
|
6
|
+
import * as os from 'os';
|
|
6
7
|
import { AutogenesisEngine } from './orchestrator/autogenesis';
|
|
7
8
|
import { startDashboardServer } from './dashboard/server';
|
|
8
9
|
import { startTelegramBot } from './orchestrator/telegram-bot';
|
|
@@ -15,83 +16,231 @@ import { Spinner } from './tools/tui-tools';
|
|
|
15
16
|
dotenv.config();
|
|
16
17
|
|
|
17
18
|
function printHeader() {
|
|
19
|
+
const banner = `
|
|
20
|
+
\x1b[38;5;99m ___ _ _ _
|
|
21
|
+
/ __\\ | ___ ___ ___ __| | | | ___ ___ _ __
|
|
22
|
+
/ / | |/ _ \\/ __|/ _ \\/ _\` | | |/ _ \\ / _ \\| '_ \\
|
|
23
|
+
/ /___| | (_) \\__ \\ __/ (_| | | | (_) | (_) | |_) |
|
|
24
|
+
\\____/|_|\\___/|___/\\___|\\__,_| |_|\\___/ \\___/| .__/
|
|
25
|
+
|_| \x1b[0m`;
|
|
26
|
+
|
|
27
|
+
console.log(banner);
|
|
28
|
+
console.log('\x1b[37mTips for getting started:\x1b[0m');
|
|
29
|
+
console.log('\x1b[90m1. Ask questions, edit files, or run commands.\x1b[0m');
|
|
30
|
+
console.log('\x1b[90m2. Be specific for the best results.\x1b[0m');
|
|
31
|
+
console.log('\x1b[90m3. Type "exit" or "quit" to close the session.\x1b[0m\n');
|
|
32
|
+
|
|
18
33
|
const endpoint = process.env.ANTHROPIC_BASE_URL || 'default';
|
|
19
34
|
const model = process.env.ANTHROPIC_MODEL || 'mimo-v2.5-pro[1m]';
|
|
20
35
|
const subagent = process.env.CLAUDE_CODE_SUBAGENT_MODEL || 'mimo-v2.5-pro';
|
|
21
|
-
|
|
22
|
-
const padRaw = (label: string, value: string) => {
|
|
23
|
-
let valStr = value;
|
|
24
|
-
if (valStr.length > 48) {
|
|
25
|
-
valStr = valStr.substring(0, 45) + '...';
|
|
26
|
-
}
|
|
27
|
-
const visibleText = ` ${label} ❯ ${valStr}`;
|
|
28
|
-
const padding = ' '.repeat(Math.max(0, 65 - visibleText.length));
|
|
29
|
-
return ` \x1b[38;5;86m${label}\x1b[0m \x1b[90m❯\x1b[0m \x1b[38;5;153m${valStr}\x1b[0m${padding}`;
|
|
30
|
-
};
|
|
31
36
|
|
|
32
|
-
console.log(
|
|
33
|
-
console.log('\x1b[38;5;99m│\x1b[0m \x1b[1;38;5;159m🤖 Closed-Loop Conversational Agent CLI\x1b[0m \x1b[38;5;99m│\x1b[0m');
|
|
34
|
-
console.log('\x1b[38;5;99m├──────────────────────────────────────────────────────────────────┤\x1b[0m');
|
|
35
|
-
console.log('\x1b[38;5;99m│\x1b[0m' + padRaw('Endpoint', endpoint) + '\x1b[38;5;99m│\x1b[0m');
|
|
36
|
-
console.log('\x1b[38;5;99m│\x1b[0m' + padRaw('Model ', model) + '\x1b[38;5;99m│\x1b[0m');
|
|
37
|
-
console.log('\x1b[38;5;99m│\x1b[0m' + padRaw('Subagent', subagent) + '\x1b[38;5;99m│\x1b[0m');
|
|
38
|
-
console.log('\x1b[38;5;99m╰──────────────────────────────────────────────────────────────────╯\x1b[0m\n');
|
|
39
|
-
console.log('\x1b[90m💡 Type your task to begin, or type "exit" to quit.\x1b[0m\n');
|
|
37
|
+
console.log(`\x1b[90mUsing: Endpoint: \x1b[38;5;86m${endpoint}\x1b[90m | Model: \x1b[38;5;153m${model}\x1b[90m | Subagent: \x1b[38;5;208m${subagent}\x1b[0m\n`);
|
|
40
38
|
}
|
|
41
39
|
|
|
42
|
-
|
|
43
|
-
|
|
40
|
+
class ClosedLoopTUI {
|
|
41
|
+
private history: { role: 'user' | 'assistant'; content: string }[] = [];
|
|
42
|
+
private input = '';
|
|
43
|
+
private cursorIdx = 0;
|
|
44
|
+
private isThinking = false;
|
|
45
|
+
private effort: 'standard' | 'ultracode';
|
|
44
46
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
47
|
+
constructor(effort: 'standard' | 'ultracode' = 'standard') {
|
|
48
|
+
this.effort = effort;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
start() {
|
|
52
|
+
process.stdout.write('\x1b[?1049h'); // Enter alternate screen
|
|
53
|
+
process.stdout.write('\x1b[?25h'); // Show cursor
|
|
54
|
+
this.render();
|
|
50
55
|
|
|
51
|
-
|
|
52
|
-
|
|
56
|
+
readline.emitKeypressEvents(process.stdin);
|
|
57
|
+
if (process.stdin.isTTY) {
|
|
58
|
+
process.stdin.setRawMode(true);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
process.stdin.on('keypress', this.handleKeypress.bind(this));
|
|
62
|
+
process.stdout.on('resize', () => {
|
|
63
|
+
this.render();
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
render() {
|
|
68
|
+
const rows = process.stdout.rows || 24;
|
|
69
|
+
const cols = process.stdout.columns || 80;
|
|
70
|
+
|
|
71
|
+
// Clear screen and move cursor to 1,1
|
|
72
|
+
process.stdout.write('\x1b[2J\x1b[H');
|
|
73
|
+
|
|
74
|
+
// 1. Draw Banner & Info
|
|
75
|
+
const banner = `
|
|
76
|
+
\x1b[38;5;99m ___ _ _ _
|
|
77
|
+
/ __\\ | ___ ___ ___ __| | | | ___ ___ _ __
|
|
78
|
+
/ / | |/ _ \\/ __|/ _ \\/ _\` | | |/ _ \\ / _ \\| '_ \\
|
|
79
|
+
/ /___| | (_) \\__ \\ __/ (_| | | | (_) | (_) | |_) |
|
|
80
|
+
\\____/|_|\\___/|___/\\___|\\__,_| |_|\\___/ \\___/| .__/
|
|
81
|
+
|_| \x1b[0m`;
|
|
82
|
+
console.log(banner);
|
|
83
|
+
|
|
84
|
+
console.log('\x1b[37mTips for getting started:\x1b[0m');
|
|
85
|
+
console.log('\x1b[90m1. Ask questions, edit files, or run commands. 2. Be specific. 3. Type "exit" to quit.\x1b[0m');
|
|
86
|
+
|
|
87
|
+
const endpoint = process.env.ANTHROPIC_BASE_URL || 'default';
|
|
88
|
+
const model = process.env.ANTHROPIC_MODEL || 'mimo-v2.5-pro';
|
|
89
|
+
console.log(`\x1b[90mUsing: Endpoint: \x1b[38;5;86m${endpoint}\x1b[90m | Model: \x1b[38;5;153m${model}\x1b[0m\n`);
|
|
90
|
+
|
|
91
|
+
// 2. Draw History in the middle
|
|
92
|
+
const historyHeight = rows - 14;
|
|
93
|
+
const historyLines: string[] = [];
|
|
94
|
+
|
|
95
|
+
// Format history messages into lines
|
|
96
|
+
for (const msg of this.history) {
|
|
97
|
+
if (msg.role === 'user') {
|
|
98
|
+
historyLines.push(`\x1b[38;5;99mUser ❯\x1b[0m ${msg.content}`);
|
|
99
|
+
} else {
|
|
100
|
+
const lines = msg.content.split('\n');
|
|
101
|
+
for (const line of lines) {
|
|
102
|
+
historyLines.push(`\x1b[38;5;86mAgent ❯\x1b[0m ${line}`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
53
106
|
|
|
54
|
-
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
107
|
+
// Slice history to fit historyHeight
|
|
108
|
+
const startIdx = Math.max(0, historyLines.length - historyHeight);
|
|
109
|
+
const visibleHistory = historyLines.slice(startIdx, startIdx + historyHeight);
|
|
110
|
+
|
|
111
|
+
for (let i = 0; i < historyHeight; i++) {
|
|
112
|
+
if (i < visibleHistory.length) {
|
|
113
|
+
console.log(visibleHistory[i]);
|
|
114
|
+
} else {
|
|
115
|
+
console.log(''); // empty line
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// 3. Draw Bottom Input Box (3 lines)
|
|
120
|
+
const border = '─'.repeat(cols - 2);
|
|
121
|
+
console.log(`\x1b[38;5;99m╭${border}╮\x1b[0m`);
|
|
122
|
+
|
|
123
|
+
const promptStr = this.isThinking ? ' \x1b[36m⠋ Thinking...\x1b[0m' : ` › ${this.input}`;
|
|
124
|
+
const visiblePromptLen = this.isThinking ? 14 : 3 + this.input.length;
|
|
125
|
+
const padding = ' '.repeat(Math.max(0, cols - 4 - visiblePromptLen));
|
|
126
|
+
console.log(`\x1b[38;5;99m│\x1b[0m${promptStr}${padding}\x1b[38;5;99m│\x1b[0m`);
|
|
127
|
+
|
|
128
|
+
console.log(`\x1b[38;5;99m╰${border}╯\x1b[0m`);
|
|
129
|
+
|
|
130
|
+
// Position cursor
|
|
131
|
+
if (!this.isThinking) {
|
|
132
|
+
const cursorCol = 4 + this.cursorIdx;
|
|
133
|
+
const cursorRow = rows - 1;
|
|
134
|
+
process.stdout.write(`\x1b[${cursorRow};${cursorCol}H`);
|
|
135
|
+
} else {
|
|
136
|
+
process.stdout.write('\x1b[?25l');
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async handleKeypress(str: string, key: any) {
|
|
141
|
+
if (this.isThinking) return;
|
|
142
|
+
|
|
143
|
+
if (key.ctrl && key.name === 'c') {
|
|
144
|
+
this.exit();
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (key.name === 'return') {
|
|
148
|
+
const cmd = this.input.trim();
|
|
149
|
+
if (!cmd) return;
|
|
150
|
+
|
|
151
|
+
if (cmd.toLowerCase() === 'exit' || cmd.toLowerCase() === 'quit') {
|
|
152
|
+
this.exit();
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
this.history.push({ role: 'user', content: cmd });
|
|
156
|
+
this.input = '';
|
|
157
|
+
this.cursorIdx = 0;
|
|
158
|
+
this.isThinking = true;
|
|
159
|
+
this.render();
|
|
160
|
+
|
|
161
|
+
try {
|
|
162
|
+
// Temporarily leave alternate screen buffer for execution
|
|
163
|
+
process.stdout.write('\x1b[?1049l');
|
|
164
|
+
process.stdout.write('\x1b[?25h');
|
|
165
|
+
if (process.stdin.isTTY) {
|
|
166
|
+
process.stdin.setRawMode(false);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
console.log(`\n\x1b[35;1m=================== EXECUTION MODE ===================\x1b[0m`);
|
|
170
|
+
console.log(`\x1b[90mRunning Task:\x1b[0m ${cmd}\n`);
|
|
171
|
+
|
|
172
|
+
const response = await runTaskAgent(cmd, {
|
|
173
|
+
role: 'coder_codeact' as any,
|
|
174
|
+
effort: this.effort,
|
|
175
|
+
history: this.history.slice(0, -1)
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
// Re-enter TUI and raw mode
|
|
179
|
+
process.stdout.write('\x1b[?1049h');
|
|
180
|
+
if (process.stdin.isTTY) {
|
|
181
|
+
process.stdin.setRawMode(true);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
this.history.push({ role: 'assistant', content: response.result });
|
|
185
|
+
} catch (err: any) {
|
|
186
|
+
// Re-enter TUI and raw mode
|
|
187
|
+
process.stdout.write('\x1b[?1049h');
|
|
188
|
+
if (process.stdin.isTTY) {
|
|
189
|
+
process.stdin.setRawMode(true);
|
|
190
|
+
}
|
|
191
|
+
this.history.push({ role: 'assistant', content: `Error: ${err.message}` });
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
this.isThinking = false;
|
|
195
|
+
process.stdout.write('\x1b[?25h');
|
|
196
|
+
this.render();
|
|
58
197
|
return;
|
|
59
198
|
}
|
|
60
199
|
|
|
61
|
-
if (
|
|
62
|
-
|
|
63
|
-
|
|
200
|
+
if (key.name === 'backspace') {
|
|
201
|
+
if (this.cursorIdx > 0) {
|
|
202
|
+
this.input = this.input.slice(0, this.cursorIdx - 1) + this.input.slice(this.cursorIdx);
|
|
203
|
+
this.cursorIdx--;
|
|
204
|
+
this.render();
|
|
205
|
+
}
|
|
206
|
+
return;
|
|
64
207
|
}
|
|
65
208
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
history.push({ role: 'assistant', content: response.result });
|
|
81
|
-
|
|
82
|
-
console.log(`\n\x1b[38;5;86m╭── Agent Response ─────────────────────────────────────────────────────────────╮\x1b[0m`);
|
|
83
|
-
console.log(response.result);
|
|
84
|
-
console.log(`\x1b[38;5;86m╰───────────────────────────────────────────────────────────────────────────────╯\x1b[0m\n`);
|
|
85
|
-
} catch (err: any) {
|
|
86
|
-
spinner.stop(false, 'Failed');
|
|
87
|
-
console.error('\n\x1b[31m[Agent Error]:\x1b[0m', err.message);
|
|
209
|
+
if (key.name === 'left') {
|
|
210
|
+
if (this.cursorIdx > 0) {
|
|
211
|
+
this.cursorIdx--;
|
|
212
|
+
this.render();
|
|
213
|
+
}
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (key.name === 'right') {
|
|
218
|
+
if (this.cursorIdx < this.input.length) {
|
|
219
|
+
this.cursorIdx++;
|
|
220
|
+
this.render();
|
|
221
|
+
}
|
|
222
|
+
return;
|
|
88
223
|
}
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
224
|
+
|
|
225
|
+
// Handle normal character inputs
|
|
226
|
+
if (str && str.length === 1 && !key.ctrl && !key.meta) {
|
|
227
|
+
this.input = this.input.slice(0, this.cursorIdx) + str + this.input.slice(this.cursorIdx);
|
|
228
|
+
this.cursorIdx++;
|
|
229
|
+
this.render();
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
exit() {
|
|
234
|
+
process.stdout.write('\x1b[?1049l');
|
|
235
|
+
process.stdout.write('\x1b[?25h');
|
|
236
|
+
console.log('Goodbye!');
|
|
93
237
|
process.exit(0);
|
|
94
|
-
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
async function startInteractiveCLI(effort: 'standard' | 'ultracode' = 'standard', codeactMode = false) {
|
|
242
|
+
const tui = new ClosedLoopTUI(effort);
|
|
243
|
+
tui.start();
|
|
95
244
|
}
|
|
96
245
|
|
|
97
246
|
function setupLogRedirection() {
|
|
@@ -109,7 +258,58 @@ function setupLogRedirection() {
|
|
|
109
258
|
};
|
|
110
259
|
}
|
|
111
260
|
|
|
261
|
+
async function checkAndPromptAPIKey(): Promise<string> {
|
|
262
|
+
if (process.env.ANTHROPIC_API_KEY || process.env.ANTHROPIC_AUTH_TOKEN) {
|
|
263
|
+
return process.env.ANTHROPIC_API_KEY || process.env.ANTHROPIC_AUTH_TOKEN || '';
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const configPath = path.join(os.homedir(), '.closed-loop.json');
|
|
267
|
+
if (fs.existsSync(configPath)) {
|
|
268
|
+
try {
|
|
269
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
270
|
+
if (config.apiKey) {
|
|
271
|
+
process.env.ANTHROPIC_API_KEY = config.apiKey;
|
|
272
|
+
return config.apiKey;
|
|
273
|
+
}
|
|
274
|
+
} catch (e) {}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
console.log('\n\x1b[33m🔑 Anthropic API key not found in environment or local .env file.\x1b[0m');
|
|
278
|
+
console.log('To run this assistant, please configure your API key below.');
|
|
279
|
+
|
|
280
|
+
const rl = readline.createInterface({
|
|
281
|
+
input: process.stdin,
|
|
282
|
+
output: process.stdout
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
const question = (query: string): Promise<string> => {
|
|
286
|
+
return new Promise(resolve => rl.question(query, resolve));
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
const key = await question('\x1b[35mEnter your Anthropic API Key:\x1b[0m ');
|
|
290
|
+
rl.close();
|
|
291
|
+
|
|
292
|
+
const trimmedKey = key.trim();
|
|
293
|
+
if (!trimmedKey) {
|
|
294
|
+
console.error('\x1b[31mError: API Key cannot be empty.\x1b[0m');
|
|
295
|
+
process.exit(1);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
try {
|
|
299
|
+
fs.writeFileSync(configPath, JSON.stringify({ apiKey: trimmedKey }, null, 2), 'utf8');
|
|
300
|
+
console.log(`\x1b[32m✔ Saved API key to ${configPath}\x1b[0m\n`);
|
|
301
|
+
} catch (err: any) {
|
|
302
|
+
console.warn(`\x1b[33mWarning: Could not save API key to config: ${err.message}\x1b[0m`);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
process.env.ANTHROPIC_API_KEY = trimmedKey;
|
|
306
|
+
return trimmedKey;
|
|
307
|
+
}
|
|
308
|
+
|
|
112
309
|
async function main() {
|
|
310
|
+
// Ensure API Key is configured before running any agent logic
|
|
311
|
+
await checkAndPromptAPIKey();
|
|
312
|
+
|
|
113
313
|
const args = process.argv.slice(2);
|
|
114
314
|
|
|
115
315
|
// Parse codeact option
|