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.

Potentially problematic release.


This version of closed-loop-cli might be problematic. Click here for more details.

Files changed (3) hide show
  1. package/dist/index.js +227 -58
  2. package/package.json +1 -1
  3. 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
- const padRaw = (label, value) => {
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
- async function startInteractiveCLI(effort = 'standard', codeactMode = false) {
72
- printHeader();
73
- const rl = readline.createInterface({
74
- input: process.stdin,
75
- output: process.stdout,
76
- prompt: '\x1b[38;5;99mclosed-loop\x1b[0m \x1b[38;5;86m❯\x1b[0m '
77
- });
78
- const history = [];
79
- rl.prompt();
80
- rl.on('line', async (line) => {
81
- const input = line.trim();
82
- if (!input) {
83
- rl.prompt();
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 (input.toLowerCase() === 'exit' || input.toLowerCase() === 'quit') {
87
- console.log('\x1b[90mGoodbye!\x1b[0m');
88
- process.exit(0);
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
- rl.pause(); // Pause standard input while agent runs
91
- const spinner = new tui_tools_1.Spinner('Thinking...');
92
- spinner.start();
93
- try {
94
- const response = await (0, task_agent_1.runTaskAgent)(input, {
95
- role: 'coder_codeact', // Coder with command execution tools
96
- effort,
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
- catch (err) {
108
- spinner.stop(false, 'Failed');
109
- console.error('\n\x1b[31m[Agent Error]:\x1b[0m', err.message);
212
+ if (key.name === 'left') {
213
+ if (this.cursorIdx > 0) {
214
+ this.cursorIdx--;
215
+ this.render();
216
+ }
217
+ return;
110
218
  }
111
- rl.resume();
112
- rl.prompt();
113
- }).on('close', () => {
114
- console.log('\n\x1b[90mGoodbye!\x1b[0m');
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "closed-loop-cli",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
4
4
  "description": "Self-Developing Multi-Agent CLI Coding Assistant",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
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('\x1b[38;5;99m╭──────────────────────────────────────────────────────────────────╮\x1b[0m');
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
- async function startInteractiveCLI(effort: 'standard' | 'ultracode' = 'standard', codeactMode = false) {
43
- printHeader();
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
- const rl = readline.createInterface({
46
- input: process.stdin,
47
- output: process.stdout,
48
- prompt: '\x1b[38;5;99mclosed-loop\x1b[0m \x1b[38;5;86m❯\x1b[0m '
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
- const history: any[] = [];
52
- rl.prompt();
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
- rl.on('line', async (line) => {
55
- const input = line.trim();
56
- if (!input) {
57
- rl.prompt();
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 (input.toLowerCase() === 'exit' || input.toLowerCase() === 'quit') {
62
- console.log('\x1b[90mGoodbye!\x1b[0m');
63
- process.exit(0);
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
- rl.pause(); // Pause standard input while agent runs
67
- const spinner = new Spinner('Thinking...');
68
- spinner.start();
69
- try {
70
- const response = await runTaskAgent(input, {
71
- role: 'coder_codeact' as any, // Coder with command execution tools
72
- effort,
73
- history: history
74
- });
75
-
76
- spinner.stop(true, 'Done!');
77
-
78
- // Update history
79
- history.push({ role: 'user', content: input });
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
- rl.resume();
90
- rl.prompt();
91
- }).on('close', () => {
92
- console.log('\n\x1b[90mGoodbye!\x1b[0m');
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